From 30fbc4cc87cc85e7b185825b5376cf79b692582d Mon Sep 17 00:00:00 2001 From: Mateusz Jasiuk Date: Mon, 10 Jul 2023 12:15:26 +0200 Subject: [PATCH] revert: do not decrypt mnemonic --- apps/extension/src/background/index.ts | 9 +- .../src/background/keyring/crypto.ts | 116 ++++++++++++------ .../src/background/keyring/keyring.ts | 51 ++++++-- .../src/background/keyring/service.ts | 5 +- .../extension/src/background/keyring/types.ts | 27 ++-- .../web-workers/submit-transfer-web-worker.ts | 6 +- apps/extension/src/provider/Anoma.test.ts | 7 +- apps/extension/src/router/types/enums.ts | 2 +- apps/extension/src/test/init.ts | 20 +-- packages/crypto/lib/Cargo.toml | 3 +- packages/shared/lib/Cargo.toml | 3 +- .../storage/src/store/IndexedDBKVStore.ts | 6 +- packages/storage/src/store/types.ts | 4 +- 13 files changed, 169 insertions(+), 90 deletions(-) diff --git a/apps/extension/src/background/index.ts b/apps/extension/src/background/index.ts index 91c9980615..b34b5ca5dd 100644 --- a/apps/extension/src/background/index.ts +++ b/apps/extension/src/background/index.ts @@ -25,11 +25,12 @@ import { init as initKeyRing, SDK_KEY, PARENT_ACCOUNT_ID_KEY, + UtilityStore, } from "./keyring"; const store = new IndexedDBKVStore(KVPrefix.IndexedDB); -const activeAccountStore = new IndexedDBKVStore(KVPrefix.ActiveAccount); +const utilityStore = new IndexedDBKVStore(KVPrefix.Utility); // TODO: For now we will be running two stores side by side const sdkStore = new IndexedDBKVStore(KVPrefix.SDK); const extensionStore = new ExtensionKVStore(KVPrefix.LocalStorage, { @@ -71,9 +72,7 @@ const { REACT_APP_NAMADA_URL = DEFAULT_URL } = process.env; const sdkData: Record | undefined = await sdkStore.get( SDK_KEY ); - const activeAccount = await activeAccountStore.get( - PARENT_ACCOUNT_ID_KEY - ); + const activeAccount = await utilityStore.get(PARENT_ACCOUNT_ID_KEY); if (sdkData && activeAccount) { const data = new TextEncoder().encode(sdkData[activeAccount]); @@ -84,7 +83,7 @@ const { REACT_APP_NAMADA_URL = DEFAULT_URL } = process.env; const keyRingService = new KeyRingService( store, sdkStore, - activeAccountStore, + utilityStore, connectedTabsStore, extensionStore, defaultChainId, diff --git a/apps/extension/src/background/keyring/crypto.ts b/apps/extension/src/background/keyring/crypto.ts index f8732cd4f7..ce8bc5c92c 100644 --- a/apps/extension/src/background/keyring/crypto.ts +++ b/apps/extension/src/background/keyring/crypto.ts @@ -1,7 +1,15 @@ -import { AES, Argon2, Argon2Params, ByteSize, Rng, Salt } from "@anoma/crypto"; +import { + AES, + Argon2, + Argon2Params, + ByteSize, + Rng, + Salt, + VecU8Pointer, +} from "@anoma/crypto"; import { AccountType, Bip44Path } from "@anoma/types"; import { Argon2Config } from "config"; -import { KdfType, KeyStore } from "./types"; +import { CryptoRecord, KdfType, KeyStore } from "./types"; import { readVecU8Pointer } from "@anoma/crypto/src/utils"; type CryptoArgs = { @@ -31,21 +39,10 @@ export class Crypto { text, type, } = args; - const saltInstance = Salt.generate(); - const salt = saltInstance.as_string(); - const { m_cost, t_cost, p_cost } = Argon2Config; - const argon2Params = new Argon2Params(m_cost, t_cost, p_cost); - const argon2 = new Argon2(password, salt, argon2Params); - const params = argon2.params(); - const key = argon2.key(); - const iv = Rng.generate_bytes(ByteSize.N12); - const aes = new AES(key, iv); - const encrypted = aes.encrypt(text); - - saltInstance.free(); - argon2.free(); - aes.free(); + const { params, key, iv, salt } = this.encryptionParams(password); + const cipherText = this.encryptWithAES(key, iv, text); + const crypto = this.cryptoRecord(cipherText, params, iv, salt); return { alias, @@ -55,32 +52,46 @@ export class Crypto { id, parentId, path, - crypto: { - cipher: { - type: "aes-256-gcm", - iv, - text: encrypted, - }, - kdf: { - type: KdfType.Argon2, - params: { - m_cost: params.m_cost, - t_cost: params.t_cost, - p_cost: params.p_cost, - salt, - }, + crypto, + type, + }; + } + + private cryptoRecord( + cipherText: Uint8Array, + { m_cost, t_cost, p_cost }: Argon2Params, + iv: Uint8Array, + salt: string + ): CryptoRecord { + return { + cipher: { + type: "aes-256-gcm", + iv, + text: cipherText, + }, + kdf: { + type: KdfType.Argon2, + params: { + m_cost, + t_cost, + p_cost, + salt, }, }, - type, }; } + public encryptAuthKey(password: string, uuid: string): CryptoRecord { + const { params, key, iv, salt } = this.encryptionParams(password); + const cipherText = this.encryptWithAES(key, iv, uuid); + return this.cryptoRecord(cipherText, params, iv, salt); + } + public decrypt( - store: KeyStore, + crypto: CryptoRecord, password: string, cryptoMemory: WebAssembly.Memory ): string { - const { crypto } = store; const { cipher, kdf } = crypto; const { m_cost, t_cost, p_cost, salt } = kdf.params; @@ -91,11 +102,46 @@ export class Crypto { const aes = new AES(newKey, cipher.iv); const vecU8Pointer = aes.decrypt(cipher.text); const decrypted = readVecU8Pointer(vecU8Pointer, cryptoMemory); - const phrase = new TextDecoder().decode(decrypted); + const plainText = new TextDecoder().decode(decrypted); aes.free(); vecU8Pointer.free(); - return phrase; + return plainText; + } + + private encryptionParams(password: string): { + params: Argon2Params; + key: VecU8Pointer; + salt: string; + iv: Uint8Array; + } { + const saltInstance = Salt.generate(); + + const salt = saltInstance.as_string(); + saltInstance.free(); + + const { m_cost, t_cost, p_cost } = Argon2Config; + const argon2Params = new Argon2Params(m_cost, t_cost, p_cost); + const argon2 = new Argon2(password, salt, argon2Params); + const params = argon2.params(); + const key = argon2.key(); + argon2.free(); + + const iv = Rng.generate_bytes(ByteSize.N12); + + return { params, key, salt, iv }; + } + + private encryptWithAES( + key: VecU8Pointer, + iv: Uint8Array, + plainText: string + ): Uint8Array { + const aes = new AES(key, iv); + const cipherText = aes.encrypt(plainText); + aes.free(); + + return cipherText; } } diff --git a/apps/extension/src/background/keyring/keyring.ts b/apps/extension/src/background/keyring/keyring.ts index 88a82a89b1..34311cf75e 100644 --- a/apps/extension/src/background/keyring/keyring.ts +++ b/apps/extension/src/background/keyring/keyring.ts @@ -1,5 +1,5 @@ -import { v5 as uuid } from "uuid"; import BigNumber from "bignumber.js"; +import { v5 as uuid, v4 as uuidV4 } from "uuid"; import { HDWallet, @@ -32,6 +32,8 @@ import { KeyStore, ResetPasswordError, DeleteAccountError, + CryptoRecord, + UtilityStore, } from "./types"; import { readVecStringPointer, @@ -61,6 +63,7 @@ const getId = (name: string, ...args: (number | string)[]): string => { export const KEYSTORE_KEY = "key-store"; export const SDK_KEY = "sdk-store"; export const PARENT_ACCOUNT_ID_KEY = "parent-account-id"; +export const AUTHKEY_KEY = "auth-key-store"; const crypto = new Crypto(); @@ -83,7 +86,7 @@ export class KeyRing { constructor( protected readonly kvStore: KVStore, protected readonly sdkStore: KVStore>, - protected readonly activeAccountStore: KVStore, + protected readonly utilityStore: KVStore, protected readonly extensionStore: KVStore, protected readonly chainId: string, protected readonly sdk: Sdk, @@ -115,11 +118,11 @@ export class KeyRing { } public async getActiveAccountId(): Promise { - return await this.activeAccountStore.get(PARENT_ACCOUNT_ID_KEY); + return await this.utilityStore.get(PARENT_ACCOUNT_ID_KEY); } public async setActiveAccountId(parentId: string): Promise { - await this.activeAccountStore.set(PARENT_ACCOUNT_ID_KEY, parentId); + await this.utilityStore.set(PARENT_ACCOUNT_ID_KEY, parentId); // To sync sdk wallet with DB const sdkData = await this.sdkStore.get(SDK_KEY); @@ -134,12 +137,16 @@ export class KeyRing { ): Promise { // default to active account if no account provided const idToCheck = accountId ?? (await this.getActiveAccountId()); + if (!idToCheck) { + throw new Error("No account to check password against"); + } - const mnemonic = await this._keyStore.getRecord("id", idToCheck); - // TODO: Generate arbitrary data to check decryption against - if (mnemonic) { + const authKeys = await this.utilityStore.get<{ + [id: string]: CryptoRecord; + }>(AUTHKEY_KEY); + if (authKeys) { try { - crypto.decrypt(mnemonic, password, this._cryptoMemory); + crypto.decrypt(authKeys[idToCheck], password, this._cryptoMemory); return true; } catch (error) { console.warn(error); @@ -172,7 +179,7 @@ export class KeyRing { for (const account of allAccounts) { const decryptedSecret = crypto.decrypt( - account, + account.crypto, currentPassword, this._cryptoMemory ); @@ -264,6 +271,7 @@ export class KeyRing { }); await this._keyStore.append(mnemonicStore); + await this.generateAuthKey(id, password); // When we are adding new top level account we have to clear the storage // to prevent adding top level secret key to existing keys this.sdk.clear_storage(); @@ -278,6 +286,21 @@ export class KeyRing { return false; } + public async generateAuthKey( + accountId: string, + password: string + ): Promise { + const id = uuidV4(); + const authKey = crypto.encryptAuthKey(password, id); + const entries = await this.utilityStore.get<{ [id: string]: CryptoRecord }>( + AUTHKEY_KEY + ); + await this.utilityStore.set(AUTHKEY_KEY, { + ...entries, + [accountId]: authKey, + }); + } + public deriveTransparentAccount( seed: VecU8Pointer, path: Bip44Path, @@ -465,7 +488,7 @@ export class KeyRing { const parentId = storedMnemonic.id; try { const phrase = crypto.decrypt( - storedMnemonic, + storedMnemonic.crypto, password, this._cryptoMemory ); @@ -616,7 +639,7 @@ export class KeyRing { let pk: string; try { - pk = crypto.decrypt(account, this._password, this._cryptoMemory); + pk = crypto.decrypt(account.crypto, this._password, this._cryptoMemory); } catch (e) { throw new Error(`Could not unlock account for ${address}: ${e}`); } @@ -673,7 +696,11 @@ export class KeyRing { if (!account) { throw new Error(`Account not found.`); } - const text = crypto.decrypt(account, this._password, this._cryptoMemory); + const text = crypto.decrypt( + account.crypto, + this._password, + this._cryptoMemory + ); // For shielded accounts we need to return the spending key as well. const extendedSpendingKey = diff --git a/apps/extension/src/background/keyring/service.ts b/apps/extension/src/background/keyring/service.ts index 558824546f..85278bddcb 100644 --- a/apps/extension/src/background/keyring/service.ts +++ b/apps/extension/src/background/keyring/service.ts @@ -13,6 +13,7 @@ import { TabStore, ResetPasswordError, DeleteAccountError, + UtilityStore, } from "./types"; import { syncTabs, updateTabStorage } from "./utils"; import { ExtensionRequester, getAnomaRouterId } from "extension"; @@ -37,7 +38,7 @@ export class KeyRingService { constructor( protected readonly kvStore: KVStore, protected readonly sdkStore: KVStore>, - protected readonly accountAccountStore: KVStore, + protected readonly utilityStore: KVStore, protected readonly connectedTabsStore: KVStore, protected readonly extensionStore: KVStore, protected readonly chainId: string, @@ -49,7 +50,7 @@ export class KeyRingService { this._keyRing = new KeyRing( kvStore, sdkStore, - accountAccountStore, + utilityStore, extensionStore, chainId, sdk, diff --git a/apps/extension/src/background/keyring/types.ts b/apps/extension/src/background/keyring/types.ts index 4768c786e8..42cf222ffe 100644 --- a/apps/extension/src/background/keyring/types.ts +++ b/apps/extension/src/background/keyring/types.ts @@ -13,6 +13,7 @@ export type Argon2Params = KdfParams & { m_cost: number; t_cost: number; p_cost: number; + salt: string; }; export type ScryptParams = KdfParams & { @@ -21,23 +22,25 @@ export type ScryptParams = KdfParams & { p: number; }; +export type CryptoRecord = { + cipher: { + type: "aes-256-gcm"; + iv: Uint8Array; + text: Uint8Array; + }; + kdf: { + type: KdfType; + params: T; + }; +}; + export interface KeyStore { id: string; alias: string; address: string; owner: string; chainId: string; - crypto: { - cipher: { - type: "aes-256-gcm"; - iv: Uint8Array; - text: Uint8Array; - }; - kdf: { - type: KdfType; - params: T; - }; - }; + crypto: CryptoRecord; meta?: { [key: string]: string; }; @@ -64,6 +67,8 @@ export type TabStore = { timestamp: number; }; +export type UtilityStore = string | { [id: string]: CryptoRecord }; + export enum ResetPasswordError { BadPassword, KeyStoreError, diff --git a/apps/extension/src/background/web-workers/submit-transfer-web-worker.ts b/apps/extension/src/background/web-workers/submit-transfer-web-worker.ts index 2a5b9f5b8a..ac0293b17e 100644 --- a/apps/extension/src/background/web-workers/submit-transfer-web-worker.ts +++ b/apps/extension/src/background/web-workers/submit-transfer-web-worker.ts @@ -14,7 +14,7 @@ import { (async function init() { await initShared(); const sdkStore = new IndexedDBKVStore(KVPrefix.SDK); - const activeAccountStore = new IndexedDBKVStore(KVPrefix.ActiveAccount); + const utilityStore = new IndexedDBKVStore(KVPrefix.Utility); const sdk = new Sdk(chains[defaultChainId].rpc); await sdk.load_masp_params(); @@ -22,9 +22,7 @@ import { const sdkData: Record | undefined = await sdkStore.get( "sdk-store" ); - const activeAccount = await activeAccountStore.get( - "parent-account-id" - ); + const activeAccount = await utilityStore.get("parent-account-id"); if (sdkData && activeAccount) { const data = new TextEncoder().encode(sdkData[activeAccount]); diff --git a/apps/extension/src/provider/Anoma.test.ts b/apps/extension/src/provider/Anoma.test.ts index a272a1a799..7175841b81 100644 --- a/apps/extension/src/provider/Anoma.test.ts +++ b/apps/extension/src/provider/Anoma.test.ts @@ -26,6 +26,7 @@ import { KeyStore, KEYSTORE_KEY, PARENT_ACCOUNT_ID_KEY, + UtilityStore, } from "background/keyring"; import { Sdk } from "@anoma/shared"; import * as utils from "extension/utils"; @@ -36,12 +37,12 @@ jest.mock("webextension-polyfill", () => ({})); describe("Anoma", () => { let anoma: Anoma; let iDBStore: KVStoreMock; - let activeAccountStore: KVStoreMock; + let utilityStore: KVStoreMock; let keyRingService: KeyRingService; beforeAll(async () => { jest.spyOn(utils, "getAnomaRouterId").mockResolvedValue(1); - ({ anoma, iDBStore, activeAccountStore, keyRingService } = await init()); + ({ anoma, iDBStore, utilityStore, keyRingService } = await init()); jest .spyOn(KeyRing.prototype, "checkPassword") @@ -70,7 +71,7 @@ describe("Anoma", () => { it("should return all accounts", async () => { iDBStore.set(KEYSTORE_KEY, keyStore); - activeAccountStore.set(PARENT_ACCOUNT_ID_KEY, ACTIVE_ACCOUNT_ID); + utilityStore.set(PARENT_ACCOUNT_ID_KEY, ACTIVE_ACCOUNT_ID); const storedKeyStore = keyStore.map( ({ crypto: _crypto, owner: _owner, ...account }) => account ); diff --git a/apps/extension/src/router/types/enums.ts b/apps/extension/src/router/types/enums.ts index 81e3f35162..759428b8b6 100644 --- a/apps/extension/src/router/types/enums.ts +++ b/apps/extension/src/router/types/enums.ts @@ -18,7 +18,7 @@ export enum KVPrefix { LocalStorage = "Anoma::LocalStorage", Memory = "Anoma::Memory", SDK = "Anoma::SDK", - ActiveAccount = "Anoma::ActiveAccount", + Utility = "Anoma::Utility", ConnectedTabs = "Anoma::ConnectedTabs", } diff --git a/apps/extension/src/test/init.ts b/apps/extension/src/test/init.ts index d70b7ad798..e1086a33a4 100644 --- a/apps/extension/src/test/init.ts +++ b/apps/extension/src/test/init.ts @@ -14,6 +14,7 @@ import { init as initKeyRing, KeyStore, TabStore, + UtilityStore, } from "../background/keyring"; import { @@ -33,12 +34,15 @@ const chainId = "namada-75a7e12.69483d59a9fb174"; export class KVStoreMock implements KVStore { private storage: { [key: string]: T | null } = {}; - constructor(private readonly _prefix: string) {} + constructor(readonly _prefix: string) {} - get(key: string): Promise { - return Promise.resolve(this.storage[key] || undefined); + get(key: string): Promise { + return new Promise((resolve) => { + const data = this.storage[key]; + return resolve(data ? (data as U) : undefined); + }); } - set(key: string, data: T | null): Promise { + set(key: string, data: U | null): Promise { this.storage[key] = data; return Promise.resolve(); } @@ -51,7 +55,7 @@ export const init = async (): Promise<{ anoma: Anoma; iDBStore: KVStoreMock; extStore: KVStoreMock; - activeAccountStore: KVStoreMock; + utilityStore: KVStoreMock; chainsService: ChainsService; keyRingService: KeyRingService; }> => { @@ -59,7 +63,7 @@ export const init = async (): Promise<{ const iDBStore = new KVStoreMock(KVPrefix.IndexedDB); const sdkStore = new KVStoreMock>(KVPrefix.SDK); const extStore = new KVStoreMock(KVPrefix.IndexedDB); - const activeAccountStore = new KVStoreMock(KVPrefix.ActiveAccount); + const utilityStore = new KVStoreMock(KVPrefix.Utility); const connectedTabsStore = new KVStoreMock( KVPrefix.ConnectedTabs ); @@ -89,7 +93,7 @@ export const init = async (): Promise<{ const keyRingService = new KeyRingService( iDBStore as KVStore, sdkStore, - activeAccountStore, + utilityStore, connectedTabsStore, extStore, chainId, @@ -120,7 +124,7 @@ export const init = async (): Promise<{ anoma, iDBStore, extStore, - activeAccountStore, + utilityStore, chainsService, keyRingService, }; diff --git a/packages/crypto/lib/Cargo.toml b/packages/crypto/lib/Cargo.toml index 86510649b6..c98f49f670 100644 --- a/packages/crypto/lib/Cargo.toml +++ b/packages/crypto/lib/Cargo.toml @@ -40,8 +40,7 @@ lto = true opt-level = "s" [profile.dev] -# We do not wan't to make any optimizations for dev -opt-level = 0 +opt-level = "s" # wasm-pack sepcific configuration [package.metadata.wasm-pack.profile.release] diff --git a/packages/shared/lib/Cargo.toml b/packages/shared/lib/Cargo.toml index 052bb1d186..f730150bb9 100644 --- a/packages/shared/lib/Cargo.toml +++ b/packages/shared/lib/Cargo.toml @@ -62,8 +62,7 @@ lto = true opt-level = "s" [profile.dev] -# We do not wan't to make any optimizations for dev -opt-level = 0 +opt-level = "s" # wasm-pack sepcific configuration [package.metadata.wasm-pack.profile.release] diff --git a/packages/storage/src/store/IndexedDBKVStore.ts b/packages/storage/src/store/IndexedDBKVStore.ts index 33e4371c62..59a568e12a 100644 --- a/packages/storage/src/store/IndexedDBKVStore.ts +++ b/packages/storage/src/store/IndexedDBKVStore.ts @@ -1,11 +1,11 @@ import { KVStore } from "./types"; -export class IndexedDBKVStore implements KVStore { +export class IndexedDBKVStore implements KVStore { protected cachedDB?: IDBDatabase; constructor(protected readonly _prefix: string) {} - public async get(key: string): Promise { + public async get(key: string): Promise { const tx = (await this.getDB()).transaction([this.prefix()], "readonly"); const store = tx.objectStore(this.prefix()); @@ -26,7 +26,7 @@ export class IndexedDBKVStore implements KVStore { }); } - public async set(key: string, data: T | null): Promise { + public async set(key: string, data: U | null): Promise { if (data === null) { const tx = (await this.getDB()).transaction([this.prefix()], "readwrite"); const store = tx.objectStore(this.prefix()); diff --git a/packages/storage/src/store/types.ts b/packages/storage/src/store/types.ts index dde7e6c3a5..d8f3229eb3 100644 --- a/packages/storage/src/store/types.ts +++ b/packages/storage/src/store/types.ts @@ -1,6 +1,6 @@ export interface KVStore { - get(key: string): Promise; - set(key: string, data: T | null): Promise; + get(key: string): Promise; + set(key: string, data: U | null): Promise; prefix(): string; }