From 91aec706507892c9824bf8d9571e5d95f140d0ba Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Wed, 22 Jul 2020 16:15:45 +0200 Subject: [PATCH] Pull out executeKdf --- packages/sdk38/src/wallet.spec.ts | 39 ++++++++++-------- packages/sdk38/src/wallet.ts | 68 ++++++++++++++++++++----------- packages/sdk38/types/wallet.d.ts | 30 ++++++-------- 3 files changed, 78 insertions(+), 59 deletions(-) diff --git a/packages/sdk38/src/wallet.spec.ts b/packages/sdk38/src/wallet.spec.ts index 7e05c296..ebaafc82 100644 --- a/packages/sdk38/src/wallet.spec.ts +++ b/packages/sdk38/src/wallet.spec.ts @@ -1,8 +1,8 @@ -import { Argon2id, Argon2idOptions, Secp256k1, Secp256k1Signature, Sha256 } from "@cosmjs/crypto"; +import { Secp256k1, Secp256k1Signature, Sha256 } from "@cosmjs/crypto"; import { fromBase64, fromHex, toAscii } from "@cosmjs/encoding"; import { base64Matcher, hexMatcher } from "./testutils.spec"; -import { extractKdfParams, Secp256k1Wallet, secp256k1WalletSalt } from "./wallet"; +import { executeKdf, extractKdfConfiguration, KdfConfiguration, Secp256k1Wallet } from "./wallet"; describe("Secp256k1Wallet", () => { // m/44'/118'/0'/0/0 @@ -57,18 +57,21 @@ describe("Secp256k1Wallet", () => { let serialized: string; { const original = await Secp256k1Wallet.fromMnemonic(defaultMnemonic); - const anyKdfParams: Argon2idOptions = { - outputLength: 32, - opsLimit: 4, - memLimitKib: 3 * 1024, + const anyKdfParams: KdfConfiguration = { + algorithm: "argon2id", + params: { + outputLength: 32, + opsLimit: 4, + memLimitKib: 3 * 1024, + }, }; - const encryptionKey = await Argon2id.execute(password, secp256k1WalletSalt, anyKdfParams); + const encryptionKey = await executeKdf(password, anyKdfParams); serialized = await original.serializeWithEncryptionKey(encryptionKey, anyKdfParams); } { - const kdfOptions: any = extractKdfParams(serialized); - const encryptionKey = await Argon2id.execute(password, secp256k1WalletSalt, kdfOptions); + const kdfConfiguration = extractKdfConfiguration(serialized); + const encryptionKey = await executeKdf(password, kdfConfiguration); const deserialized = await Secp256k1Wallet.deserializeWithEncryptionKey(serialized, encryptionKey); expect(deserialized.mnemonic).toEqual(defaultMnemonic); expect(await deserialized.getAccounts()).toEqual([ @@ -149,19 +152,19 @@ describe("Secp256k1Wallet", () => { const wallet = await Secp256k1Wallet.fromMnemonic(defaultMnemonic); const key = fromHex("aabb221100aabb332211aabb33221100aabb221100aabb332211aabb33221100"); - const customKdfParams: Argon2idOptions = { - outputLength: 32, - opsLimit: 321, - memLimitKib: 11 * 1024, + const customKdfConfiguration: KdfConfiguration = { + algorithm: "argon2id", + params: { + outputLength: 32, + opsLimit: 321, + memLimitKib: 11 * 1024, + }, }; - const serialized = await wallet.serializeWithEncryptionKey(key, customKdfParams); + const serialized = await wallet.serializeWithEncryptionKey(key, customKdfConfiguration); expect(JSON.parse(serialized)).toEqual( jasmine.objectContaining({ type: "secp256k1wallet-v1", - kdf: { - algorithm: "argon2id", - params: customKdfParams, - }, + kdf: customKdfConfiguration, encryption: { algorithm: "xchacha20poly1305-ietf", params: { diff --git a/packages/sdk38/src/wallet.ts b/packages/sdk38/src/wallet.ts index b97bd0bc..6930adff 100644 --- a/packages/sdk38/src/wallet.ts +++ b/packages/sdk38/src/wallet.ts @@ -76,19 +76,22 @@ const serializationTypeV1 = "secp256k1wallet-v1"; /** * A fixed salt is chosen to archive a deterministic password to key derivation. - * This reduces the scope of a potential rainbow attack to all Secp256k1Wallet v1 users. + * This reduces the scope of a potential rainbow attack to all CosmJS users. * Must be 16 bytes due to implementation limitations. */ -export const secp256k1WalletSalt = toAscii("Secp256k1Wallet1"); +const cosmjsSalt = toAscii("The CosmJS salt."); /** * A KDF configuration that is not very strong but can be used on the main thread. * It takes about 1 second in Node.js 12.15 and should have similar runtimes in other modern Wasm hosts. */ -const basicPasswordHashingOptions: Argon2idOptions = { - outputLength: 32, - opsLimit: 20, - memLimitKib: 12 * 1024, +const basicPasswordHashingOptions: KdfConfiguration = { + algorithm: "argon2id", + params: { + outputLength: 32, + opsLimit: 20, + memLimitKib: 12 * 1024, + }, }; const algorithmIdXchacha20poly1305Ietf = "xchacha20poly1305-ietf"; @@ -101,14 +104,7 @@ export interface Secp256k1WalletSerialization { /** A format+version identifier for this serialization format */ readonly type: string; /** Information about the key derivation function (i.e. password to encrytion key) */ - readonly kdf: { - /** - * An algorithm identifier, such as "argon2id" or "scrypt". - */ - readonly algorithm: string; - /** A map of algorithm-specific parameters */ - readonly params: Record; - }; + readonly kdf: KdfConfiguration; /** Information about the symmetric encryption */ readonly encryption: { /** @@ -135,22 +131,46 @@ export interface Secp256k1WalletData { }>; } -function extractKdfParamsV1(document: any): Record { - return document.kdf.params; +function extractKdfConfigurationV1(document: any): KdfConfiguration { + return document.kdf; } -export function extractKdfParams(serialization: string): Record { +export function extractKdfConfiguration(serialization: string): KdfConfiguration { const root = JSON.parse(serialization); if (!isNonNullObject(root)) throw new Error("Root document is not an object."); switch ((root as any).type) { case serializationTypeV1: - return extractKdfParamsV1(root); + return extractKdfConfigurationV1(root); default: throw new Error("Unsupported serialization type"); } } +export interface KdfConfiguration { + /** + * An algorithm identifier, such as "argon2id" or "scrypt". + */ + readonly algorithm: string; + /** A map of algorithm-specific parameters */ + readonly params: Record; +} + +export async function executeKdf(password: string, configuration: KdfConfiguration): Promise { + switch (configuration.algorithm) { + case "argon2id": { + const { outputLength, opsLimit, memLimitKib } = configuration.params; + assert(typeof outputLength === "number"); + assert(typeof opsLimit === "number"); + assert(typeof memLimitKib === "number"); + const options: Argon2idOptions = { outputLength, opsLimit, memLimitKib }; + return Argon2id.execute(password, cosmjsSalt, options); + } + default: + throw new Error("Unsupported KDF algorithm"); + } +} + export class Secp256k1Wallet implements OfflineSigner { /** * Restores a wallet from the given BIP39 mnemonic. @@ -250,7 +270,7 @@ export class Secp256k1Wallet implements OfflineSigner { switch (untypedRoot.kdf.algorithm) { case "argon2id": { const kdfOptions = untypedRoot.kdf.params; - encryptionKey = await Argon2id.execute(password, secp256k1WalletSalt, kdfOptions); + encryptionKey = await Argon2id.execute(password, cosmjsSalt, kdfOptions); break; } default: @@ -333,9 +353,9 @@ export class Secp256k1Wallet implements OfflineSigner { * This is not normalized internally (see "Unicode normalization" to learn more). */ public async serialize(password: string): Promise { - const kdfOption = basicPasswordHashingOptions; - const encryptionKey = await Argon2id.execute(password, secp256k1WalletSalt, kdfOption); - return this.serializeWithEncryptionKey(encryptionKey, kdfOption); + const kdfConfiguration = basicPasswordHashingOptions; + const encryptionKey = await executeKdf(password, kdfConfiguration); + return this.serializeWithEncryptionKey(encryptionKey, kdfConfiguration); } /** @@ -349,7 +369,7 @@ export class Secp256k1Wallet implements OfflineSigner { */ public async serializeWithEncryptionKey( encryptionKey: Uint8Array, - kdfOptions: Argon2idOptions, + kdfConfiguration: KdfConfiguration, ): Promise { const encrytedData: Secp256k1WalletData = { mnemonic: this.mnemonic, @@ -365,7 +385,7 @@ export class Secp256k1Wallet implements OfflineSigner { const out: Secp256k1WalletSerialization = { type: serializationTypeV1, - kdf: { algorithm: "argon2id", params: { ...kdfOptions } }, + kdf: kdfConfiguration, encryption: { algorithm: algorithmIdXchacha20poly1305Ietf, params: { diff --git a/packages/sdk38/types/wallet.d.ts b/packages/sdk38/types/wallet.d.ts index 383cf916..0d377686 100644 --- a/packages/sdk38/types/wallet.d.ts +++ b/packages/sdk38/types/wallet.d.ts @@ -1,4 +1,4 @@ -import { Argon2idOptions, Slip10RawIndex } from "@cosmjs/crypto"; +import { Slip10RawIndex } from "@cosmjs/crypto"; import { StdSignature } from "./types"; export declare type PrehashType = "sha256" | "sha512" | null; export declare type Algo = "secp256k1" | "ed25519" | "sr25519"; @@ -22,12 +22,6 @@ export interface OfflineSigner { * with 0-based account index `a`. */ export declare function makeCosmoshubPath(a: number): readonly Slip10RawIndex[]; -/** - * A fixed salt is chosen to archive a deterministic password to key derivation. - * This reduces the scope of a potential rainbow attack to all Secp256k1Wallet v1 users. - * Must be 16 bytes due to implementation limitations. - */ -export declare const secp256k1WalletSalt: Uint8Array; /** * This interface describes a JSON object holding the encrypted wallet and the meta data. * All fields in here must be JSON types. @@ -36,14 +30,7 @@ export interface Secp256k1WalletSerialization { /** A format+version identifier for this serialization format */ readonly type: string; /** Information about the key derivation function (i.e. password to encrytion key) */ - readonly kdf: { - /** - * An algorithm identifier, such as "argon2id" or "scrypt". - */ - readonly algorithm: string; - /** A map of algorithm-specific parameters */ - readonly params: Record; - }; + readonly kdf: KdfConfiguration; /** Information about the symmetric encryption */ readonly encryption: { /** @@ -68,7 +55,16 @@ export interface Secp256k1WalletData { readonly prefix: string; }>; } -export declare function extractKdfParams(serialization: string): Record; +export declare function extractKdfConfiguration(serialization: string): KdfConfiguration; +export interface KdfConfiguration { + /** + * An algorithm identifier, such as "argon2id" or "scrypt". + */ + readonly algorithm: string; + /** A map of algorithm-specific parameters */ + readonly params: Record; +} +export declare function executeKdf(password: string, configuration: KdfConfiguration): Promise; export declare class Secp256k1Wallet implements OfflineSigner { /** * Restores a wallet from the given BIP39 mnemonic. @@ -128,5 +124,5 @@ export declare class Secp256k1Wallet implements OfflineSigner { * The caller is responsible for ensuring the key was derived with the given kdf options. If this * is not the case, the wallet cannot be restored with the original password. */ - serializeWithEncryptionKey(encryptionKey: Uint8Array, kdfOptions: Argon2idOptions): Promise; + serializeWithEncryptionKey(encryptionKey: Uint8Array, kdfConfiguration: KdfConfiguration): Promise; }