Skip to content

Commit

Permalink
feat: auth-key storage
Browse files Browse the repository at this point in the history
  • Loading branch information
mateuszjasiuk committed Jul 3, 2023
1 parent 6fe3564 commit 5fd78bf
Show file tree
Hide file tree
Showing 6 changed files with 135 additions and 52 deletions.
2 changes: 2 additions & 0 deletions apps/extension/src/background/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
const store = new IndexedDBKVStore(KVPrefix.IndexedDB);

const activeAccountStore = new IndexedDBKVStore(KVPrefix.ActiveAccount);
const authkeyStore = new IndexedDBKVStore(KVPrefix.ActiveAccount);
// TODO: For now we will be running two stores side by side
const sdkStore = new IndexedDBKVStore(KVPrefix.SDK);
const extensionStore = new ExtensionKVStore(KVPrefix.LocalStorage, {
Expand Down Expand Up @@ -85,6 +86,7 @@ const { REACT_APP_NAMADA_URL = DEFAULT_URL } = process.env;
store,
sdkStore,
activeAccountStore,
authkeyStore,
connectedTabsStore,
extensionStore,
defaultChainId,
Expand Down
112 changes: 79 additions & 33 deletions apps/extension/src/background/keyring/crypto.ts
Original file line number Diff line number Diff line change
@@ -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 = {
Expand Down Expand Up @@ -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,
Expand All @@ -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;

Expand All @@ -98,4 +109,39 @@ export class Crypto {

return phrase;
}

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;
}
}
40 changes: 32 additions & 8 deletions apps/extension/src/background/keyring/keyring.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { v5 as uuid } from "uuid";
import { v5 as uuid, v4 as uuidV4 } from "uuid";

import {
HDWallet,
Expand Down Expand Up @@ -31,6 +31,7 @@ import {
KeyStore,
ResetPasswordError,
DeleteAccountError,
CryptoRecord,
} from "./types";
import {
readVecStringPointer,
Expand Down Expand Up @@ -60,6 +61,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();

Expand All @@ -83,6 +85,7 @@ export class KeyRing {
protected readonly kvStore: KVStore<KeyStore[]>,
protected readonly sdkStore: KVStore<Record<string, string>>,
protected readonly activeAccountStore: KVStore<string>,
protected readonly authkeyStore: KVStore<{ [id: string]: CryptoRecord }>,
protected readonly extensionStore: KVStore<number>,
protected readonly chainId: string,
protected readonly sdk: Sdk,
Expand Down Expand Up @@ -133,12 +136,15 @@ export class KeyRing {
): Promise<boolean> {
// 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);
const authKeys = await this.authkeyStore.get(AUTHKEY_KEY);
// TODO: Generate arbitrary data to check decryption against
if (mnemonic) {
if (authKeys) {
try {
crypto.decrypt(mnemonic, password, this._cryptoMemory);
crypto.decrypt(authKeys[idToCheck], password, this._cryptoMemory);
return true;
} catch (error) {
console.warn(error);
Expand Down Expand Up @@ -171,7 +177,7 @@ export class KeyRing {

for (const account of allAccounts) {
const decryptedSecret = crypto.decrypt(
account,
account.crypto,
currentPassword,
this._cryptoMemory
);
Expand Down Expand Up @@ -263,6 +269,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();
Expand All @@ -277,6 +284,19 @@ export class KeyRing {
return false;
}

public async generateAuthKey(
accountId: string,
password: string
): Promise<void> {
const id = uuidV4();
const authKey = crypto.encryptAuthKey(password, id);
const entries = await this.authkeyStore.get(AUTHKEY_KEY);
await this.authkeyStore.set(AUTHKEY_KEY, {
...entries,
[accountId]: authKey,
});
}

public deriveTransparentAccount(
seed: VecU8Pointer,
path: Bip44Path,
Expand Down Expand Up @@ -462,7 +482,7 @@ export class KeyRing {
const parentId = storedMnemonic.id;
try {
const phrase = crypto.decrypt(
storedMnemonic,
storedMnemonic.crypto,
password,
this._cryptoMemory
);
Expand Down Expand Up @@ -613,7 +633,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}`);
}
Expand Down Expand Up @@ -670,7 +690,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 =
Expand Down
3 changes: 3 additions & 0 deletions apps/extension/src/background/keyring/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
TabStore,
ResetPasswordError,
DeleteAccountError,
CryptoRecord,
} from "./types";
import { syncTabs, updateTabStorage } from "./utils";
import { ExtensionRequester, getAnomaRouterId } from "extension";
Expand All @@ -38,6 +39,7 @@ export class KeyRingService {
protected readonly kvStore: KVStore<KeyStore[]>,
protected readonly sdkStore: KVStore<Record<string, string>>,
protected readonly accountAccountStore: KVStore<string>,
protected readonly authkeyStore: KVStore<{ [id: string]: CryptoRecord }>,
protected readonly connectedTabsStore: KVStore<TabStore[]>,
protected readonly extensionStore: KVStore<number>,
protected readonly chainId: string,
Expand All @@ -50,6 +52,7 @@ export class KeyRingService {
kvStore,
sdkStore,
accountAccountStore,
authkeyStore,
extensionStore,
chainId,
sdk,
Expand Down
25 changes: 14 additions & 11 deletions apps/extension/src/background/keyring/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export type Argon2Params = KdfParams & {
m_cost: number;
t_cost: number;
p_cost: number;
salt: string;
};

export type ScryptParams = KdfParams & {
Expand All @@ -21,23 +22,25 @@ export type ScryptParams = KdfParams & {
p: number;
};

export type CryptoRecord<T = Argon2Params> = {
cipher: {
type: "aes-256-gcm";
iv: Uint8Array;
text: Uint8Array;
};
kdf: {
type: KdfType;
params: T;
};
};

export interface KeyStore<T = Argon2Params> {
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<T>;
meta?: {
[key: string]: string;
};
Expand Down
5 changes: 5 additions & 0 deletions apps/extension/src/test/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
init as initKeyRing,
KeyStore,
TabStore,
CryptoRecord,
} from "../background/keyring";

import {
Expand Down Expand Up @@ -60,6 +61,9 @@ export const init = async (): Promise<{
const sdkStore = new KVStoreMock<Record<string, string>>(KVPrefix.SDK);
const extStore = new KVStoreMock<number>(KVPrefix.IndexedDB);
const activeAccountStore = new KVStoreMock<string>(KVPrefix.ActiveAccount);
const authKeyStore = new KVStoreMock<{ [id: string]: CryptoRecord }>(
KVPrefix.ActiveAccount
);
const connectedTabsStore = new KVStoreMock<TabStore[]>(
KVPrefix.ConnectedTabs
);
Expand Down Expand Up @@ -90,6 +94,7 @@ export const init = async (): Promise<{
iDBStore as KVStore<KeyStore[]>,
sdkStore,
activeAccountStore,
authKeyStore,
connectedTabsStore,
extStore,
chainId,
Expand Down

0 comments on commit 5fd78bf

Please sign in to comment.