From 230983c33bd8991e8491b9b0403519b4cecadbe1 Mon Sep 17 00:00:00 2001 From: Ajinkya Rajandekar <145996984+ajinkyaraj-23@users.noreply.github.com> Date: Mon, 27 Jan 2025 17:17:58 +0000 Subject: [PATCH] Encrypt accounts key in localstorage --- packages/state/package.json | 9 ++- packages/state/src/reducer.ts | 6 ++ packages/state/src/transforms/encryption.ts | 52 +++++++++++++++ packages/state/src/utils/deviceFingerprint.ts | 65 +++++++++++++++++++ pnpm-lock.yaml | 55 ++++++++++++---- 5 files changed, 174 insertions(+), 13 deletions(-) create mode 100644 packages/state/src/transforms/encryption.ts create mode 100644 packages/state/src/utils/deviceFingerprint.ts diff --git a/packages/state/package.json b/packages/state/package.json index 15cf98eb6f..6844709632 100644 --- a/packages/state/package.json +++ b/packages/state/package.json @@ -38,7 +38,9 @@ "prettier": "^3.4.2", "rimraf": "^6.0.1", "tsup": "^8.3.5", - "typescript": "^5.7.2" + "typescript": "^5.7.2", + "@types/crypto-js": "^4.1.1", + "@types/uuid": "^9.0.1" }, "scripts": { "build": "tsup-node --dts", @@ -93,6 +95,9 @@ "redux": "^5.0.1", "redux-persist": "^6.0.0", "redux-thunk": "^3.1.0", - "zod": "^3.24.1" + "zod": "^3.24.1", + "@fingerprintjs/fingerprintjs": "^3.4.0", + "crypto-js": "^4.1.1", + "uuid": "^9.0.0" } } diff --git a/packages/state/src/reducer.ts b/packages/state/src/reducer.ts index f516ea652f..18fa9ec5f2 100644 --- a/packages/state/src/reducer.ts +++ b/packages/state/src/reducer.ts @@ -16,6 +16,7 @@ import { multisigsSlice } from "./slices/multisigs"; import { networksSlice } from "./slices/networks"; import { protocolSettingsSlice } from "./slices/protocolSettings"; import { tokensSlice } from "./slices/tokens"; +import { createEncryptionTransform } from "./transforms/encryption"; let TEST_STORAGE: Storage | undefined; @@ -49,6 +50,11 @@ export const makeReducer = (storage_: Storage | undefined) => { storage, migrate: createAsyncMigrate(accountsMigrations, { debug: false }), blacklist: ["password"], + transforms: [ + createEncryptionTransform({ + whitelist: ["seedPhrases", "secretKeys", "items"] + }) + ], }; const rootReducers = combineReducers({ diff --git a/packages/state/src/transforms/encryption.ts b/packages/state/src/transforms/encryption.ts new file mode 100644 index 0000000000..136af58922 --- /dev/null +++ b/packages/state/src/transforms/encryption.ts @@ -0,0 +1,52 @@ +import CryptoJS from 'crypto-js'; +import { createTransform } from "redux-persist"; + +import { getDeviceFingerprint } from "../utils/deviceFingerprint"; + + +// Initialize the key immediately +const encryptionKey = CryptoJS.SHA256(getDeviceFingerprint() + "umami-salt").toString(); + +export const createEncryptionTransform = (config: { whitelist: string[] }) => + createTransform( + // Transform state on its way to being serialized and persisted. + (inboundState) => { + if (!encryptionKey) { + // Return unencrypted state if key isn't ready yet + return inboundState; + } + + const serialized = JSON.stringify(inboundState); + return CryptoJS.AES.encrypt(serialized, encryptionKey).toString(); + }, + // Transform state being rehydrated + (outboundState) => { + if (!encryptionKey || !outboundState) { + return outboundState; + } + + try { + const decrypted = CryptoJS.AES.decrypt(outboundState as string, encryptionKey); + const decryptedString = decrypted.toString(CryptoJS.enc.Utf8); + return JSON.parse(decryptedString); + } catch (error) { + console.error('Failed to decrypt state:', error); + return outboundState; + } + }, + config + ); + +const isElectron = () => { + return typeof window !== 'undefined' && + (window.process?.type === 'renderer' || window.process?.versions?.electron); +}; + +// In a web browser: +isElectron() // returns false + +// In Electron's renderer process: +isElectron() // returns true because process.type === 'renderer' + +// In Electron's main process: +isElectron() // returns true because process.versions.electron exists \ No newline at end of file diff --git a/packages/state/src/utils/deviceFingerprint.ts b/packages/state/src/utils/deviceFingerprint.ts new file mode 100644 index 0000000000..8354c1211b --- /dev/null +++ b/packages/state/src/utils/deviceFingerprint.ts @@ -0,0 +1,65 @@ +import { v5 as uuidv5 } from 'uuid'; + +let cachedFingerprint: string | null = null; +const NAMESPACE = "1b671a64-40d5-491e-99b0-da01ff1f3341"; // Static UUID namespace + +const isElectron = () => typeof window !== "undefined" && + ((window as any).process?.type === "renderer" || (window as any).process?.versions?.electron); + +const getWebFingerprint = (): string => { + // For web browsers + const components = [ + window.navigator.userAgent, + window.screen.height, + window.screen.width, + window.screen.colorDepth, + new Date().getTimezoneOffset() + ]; + return components.join("|"); +}; + +const getElectronFingerprint = (): string => { + // For Electron + if (isElectron()) { + const os = window.require("os"); + const components = [ + os.hostname(), + os.platform(), + os.arch(), + os.cpus()[0]?.model, + os.totalmem() + ]; + return components.join("|"); + } + throw new Error("Not in Electron environment"); +}; + +export const getDeviceFingerprint = (): string => { + if (cachedFingerprint) { + return cachedFingerprint; + } + + try { + // Get raw fingerprint data based on environment + const rawFingerprint = isElectron() + ? getElectronFingerprint() + : getWebFingerprint(); + + // Generate a consistent UUID from the fingerprint data + cachedFingerprint = uuidv5(rawFingerprint, NAMESPACE); + return cachedFingerprint; + + } catch (error) { + // Fallback: Generate a random but persistent fingerprint + const storageKey = 'umami-device-id'; + let fallbackId = localStorage.getItem(storageKey); + + if (!fallbackId) { + fallbackId = uuidv5(Date.now().toString(), NAMESPACE); + localStorage.setItem(storageKey, fallbackId); + } + + cachedFingerprint = fallbackId; + return fallbackId; + } +}; \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9918165765..1adac37173 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -255,7 +255,7 @@ importers: version: 33.2.1 electron-builder: specifier: ^25.1.8 - version: 25.1.8(electron-builder-squirrel-windows@24.13.3(dmg-builder@25.1.8)) + version: 25.1.8(electron-builder-squirrel-windows@24.13.3) electronmon: specifier: ^2.0.3 version: 2.0.3 @@ -1890,6 +1890,9 @@ importers: '@airgap/beacon-wallet': specifier: ^4.3.1 version: 4.3.1 + '@fingerprintjs/fingerprintjs': + specifier: ^3.4.0 + version: 3.4.2 '@reduxjs/toolkit': specifier: ^2.5.0 version: 2.5.0(react-redux@9.2.0(@types/react@18.3.12)(react@18.3.1)(redux@5.0.1))(react@18.3.1) @@ -1941,6 +1944,9 @@ importers: bip39: specifier: ^3.1.0 version: 3.1.0 + crypto-js: + specifier: ^4.1.1 + version: 4.2.0 framer-motion: specifier: ^11.15.0 version: 11.15.0(@emotion/is-prop-valid@1.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -1968,6 +1974,9 @@ importers: redux-thunk: specifier: ^3.1.0 version: 3.1.0(redux@5.0.1) + uuid: + specifier: ^9.0.0 + version: 9.0.1 zod: specifier: ^3.24.1 version: 3.24.1 @@ -1984,6 +1993,9 @@ importers: '@types/babel__core': specifier: ^7.20.5 version: 7.20.5 + '@types/crypto-js': + specifier: ^4.1.1 + version: 4.2.2 '@types/eslint': specifier: ^8 version: 8.56.11 @@ -1996,6 +2008,9 @@ importers: '@types/react-dom': specifier: 18.3.1 version: 18.3.1 + '@types/uuid': + specifier: ^9.0.1 + version: 9.0.8 '@umami/eslint-config': specifier: workspace:^ version: link:../eslint-config @@ -4197,6 +4212,9 @@ packages: resolution: {integrity: sha512-ReZxZ8pdnoI3tP/dNnJdnmAk7uLT4FjsKDGW7YeDdvdOMz2XCQSmSCM9IWlrXuWtMF9zeSB6WJtEhCQ41gQOfw==} hasBin: true + '@fingerprintjs/fingerprintjs@3.4.2': + resolution: {integrity: sha512-3Ncze6JsJpB7BpYhqIgvBpfvEX1jsEKrad5hQBpyRQxtoAp6hx3+R46zqfsuQG4D9egQZ+xftQ0u4LPFMB7Wmg==} + '@floating-ui/core@1.6.8': resolution: {integrity: sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA==} @@ -6287,6 +6305,9 @@ packages: '@types/cookie@0.6.0': resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} + '@types/crypto-js@4.2.2': + resolution: {integrity: sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==} + '@types/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} @@ -7948,6 +7969,9 @@ packages: resolution: {integrity: sha512-r4ESw/IlusD17lgQi1O20Fa3qNnsckR126TdUuBgAu7GBYSIPvdNyONd3Zrxh0xCwA4+6w/TDArBPsMvhur+KQ==} engines: {node: '>= 0.10'} + crypto-js@4.2.0: + resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==} + crypto-random-string@2.0.0: resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==} engines: {node: '>=8'} @@ -10607,6 +10631,7 @@ packages: lodash.isequal@4.5.0: resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} + deprecated: This package is deprecated. Use require('node:util').isDeepStrictEqual instead. lodash.isplainobject@4.0.6: resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} @@ -16975,6 +17000,10 @@ snapshots: find-up: 5.0.0 js-yaml: 4.1.0 + '@fingerprintjs/fingerprintjs@3.4.2': + dependencies: + tslib: 2.8.1 + '@floating-ui/core@1.6.8': dependencies: '@floating-ui/utils': 0.2.8 @@ -20233,6 +20262,8 @@ snapshots: '@types/cookie@0.6.0': {} + '@types/crypto-js@4.2.2': {} + '@types/debug@4.1.12': dependencies: '@types/ms': 0.7.34 @@ -21477,7 +21508,7 @@ snapshots: app-builder-bin@5.0.0-alpha.10: {} - app-builder-lib@24.13.3(dmg-builder@25.1.8(electron-builder-squirrel-windows@24.13.3))(electron-builder-squirrel-windows@24.13.3(dmg-builder@25.1.8)): + app-builder-lib@24.13.3(dmg-builder@25.1.8)(electron-builder-squirrel-windows@24.13.3): dependencies: '@develar/schema-utils': 2.6.5 '@electron/notarize': 2.2.1 @@ -21511,7 +21542,7 @@ snapshots: transitivePeerDependencies: - supports-color - app-builder-lib@25.1.8(dmg-builder@25.1.8(electron-builder-squirrel-windows@24.13.3))(electron-builder-squirrel-windows@24.13.3(dmg-builder@25.1.8)): + app-builder-lib@25.1.8(dmg-builder@25.1.8)(electron-builder-squirrel-windows@24.13.3): dependencies: '@develar/schema-utils': 2.6.5 '@electron/notarize': 2.5.0 @@ -22784,6 +22815,8 @@ snapshots: randombytes: 2.1.0 randomfill: 1.0.4 + crypto-js@4.2.0: {} + crypto-random-string@2.0.0: {} css-box-model@1.2.1: @@ -23122,7 +23155,7 @@ snapshots: dmg-builder@25.1.8(electron-builder-squirrel-windows@24.13.3): dependencies: - app-builder-lib: 25.1.8(dmg-builder@25.1.8(electron-builder-squirrel-windows@24.13.3))(electron-builder-squirrel-windows@24.13.3(dmg-builder@25.1.8)) + app-builder-lib: 25.1.8(dmg-builder@25.1.8)(electron-builder-squirrel-windows@24.13.3) builder-util: 25.1.7 builder-util-runtime: 9.2.10 fs-extra: 10.1.0 @@ -23233,7 +23266,7 @@ snapshots: electron-builder-squirrel-windows@24.13.3(dmg-builder@25.1.8): dependencies: - app-builder-lib: 24.13.3(dmg-builder@25.1.8(electron-builder-squirrel-windows@24.13.3))(electron-builder-squirrel-windows@24.13.3(dmg-builder@25.1.8)) + app-builder-lib: 24.13.3(dmg-builder@25.1.8)(electron-builder-squirrel-windows@24.13.3) archiver: 5.3.2 builder-util: 24.13.1 fs-extra: 10.1.0 @@ -23241,9 +23274,9 @@ snapshots: - dmg-builder - supports-color - electron-builder@25.1.8(electron-builder-squirrel-windows@24.13.3(dmg-builder@25.1.8)): + electron-builder@25.1.8(electron-builder-squirrel-windows@24.13.3): dependencies: - app-builder-lib: 25.1.8(dmg-builder@25.1.8(electron-builder-squirrel-windows@24.13.3))(electron-builder-squirrel-windows@24.13.3(dmg-builder@25.1.8)) + app-builder-lib: 25.1.8(dmg-builder@25.1.8)(electron-builder-squirrel-windows@24.13.3) builder-util: 25.1.7 builder-util-runtime: 9.2.10 chalk: 4.1.2 @@ -23656,7 +23689,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@8.19.0(eslint@8.57.0)(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0)(eslint@8.57.0))(eslint@8.57.0): + eslint-module-utils@2.12.0(@typescript-eslint/parser@8.19.0(eslint@8.57.0)(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.0): dependencies: debug: 3.2.7 optionalDependencies: @@ -23667,7 +23700,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@8.19.0(eslint@8.57.0)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0)(eslint@8.57.0))(eslint@8.57.0): + eslint-module-utils@2.12.0(@typescript-eslint/parser@8.19.0(eslint@8.57.0)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.0): dependencies: debug: 3.2.7 optionalDependencies: @@ -23716,7 +23749,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.19.0(eslint@8.57.0)(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0)(eslint@8.57.0))(eslint@8.57.0) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.19.0(eslint@8.57.0)(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.0) hasown: 2.0.2 is-core-module: 2.15.1 is-glob: 4.0.3 @@ -23745,7 +23778,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.19.0(eslint@8.57.0)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0)(eslint@8.57.0))(eslint@8.57.0) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.19.0(eslint@8.57.0)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.0) hasown: 2.0.2 is-core-module: 2.15.1 is-glob: 4.0.3