diff --git a/packages/sdk38/src/testutils.spec.ts b/packages/sdk38/src/testutils.spec.ts index 7845d0b4..5bccb7cc 100644 --- a/packages/sdk38/src/testutils.spec.ts +++ b/packages/sdk38/src/testutils.spec.ts @@ -21,6 +21,7 @@ export const dateTimeStampMatcher = /^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{ export const semverMatcher = /^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$/; /** @see https://rgxdb.com/r/1NUN74O6 */ export const base64Matcher = /^(?:[a-zA-Z0-9+/]{4})*(?:|(?:[a-zA-Z0-9+/]{3}=)|(?:[a-zA-Z0-9+/]{2}==)|(?:[a-zA-Z0-9+/]{1}===))$/; +export const hexMatcher = /^([0-9a-fA-F][0-9a-fA-F])*$/; // https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki#bech32 export const bech32AddressMatcher = /^[\x21-\x7e]{1,83}1[02-9ac-hj-np-z]{38}$/; diff --git a/packages/sdk38/src/wallet.spec.ts b/packages/sdk38/src/wallet.spec.ts index fd42768a..c83b07ac 100644 --- a/packages/sdk38/src/wallet.spec.ts +++ b/packages/sdk38/src/wallet.spec.ts @@ -1,7 +1,7 @@ -import { Secp256k1, Secp256k1Signature, Sha256 } from "@cosmjs/crypto"; +import { Argon2idOptions, Secp256k1, Secp256k1Signature, Sha256 } from "@cosmjs/crypto"; import { fromBase64, fromHex, toAscii } from "@cosmjs/encoding"; -import { base64Matcher } from "./testutils.spec"; +import { base64Matcher, hexMatcher } from "./testutils.spec"; import { Secp256k1Wallet } from "./wallet"; describe("Secp256k1Wallet", () => { @@ -76,6 +76,50 @@ describe("Secp256k1Wallet", () => { expect(JSON.parse(serialized)).toEqual( jasmine.objectContaining({ type: "v1", + kdf: { + algorithm: "argon2id", + params: { + outputLength: 32, + opsLimit: 20, + memLimitKib: 12 * 1024, + }, + }, + encryption: { + algorithm: "xchacha20poly1305-ietf", + params: { + nonce: jasmine.stringMatching(hexMatcher), + }, + }, + value: jasmine.stringMatching(base64Matcher), + }), + ); + }); + }); + + describe("saveWithEncryptionKey", () => { + it("can save with password", async () => { + const wallet = await Secp256k1Wallet.fromMnemonic(defaultMnemonic); + + const key = fromHex("aabb221100aabb332211aabb33221100aabb221100aabb332211aabb33221100"); + const customKdfParams: Argon2idOptions = { + outputLength: 32, + opsLimit: 321, + memLimitKib: 11 * 1024, + }; + const serialized = await wallet.saveWithEncryptionKey(key, customKdfParams); + expect(JSON.parse(serialized)).toEqual( + jasmine.objectContaining({ + type: "v1", + kdf: { + algorithm: "argon2id", + params: customKdfParams, + }, + encryption: { + algorithm: "xchacha20poly1305-ietf", + params: { + nonce: jasmine.stringMatching(hexMatcher), + }, + }, value: jasmine.stringMatching(base64Matcher), }), ); diff --git a/packages/sdk38/src/wallet.ts b/packages/sdk38/src/wallet.ts index 1ca45eb6..602d9484 100644 --- a/packages/sdk38/src/wallet.ts +++ b/packages/sdk38/src/wallet.ts @@ -15,7 +15,6 @@ import { Xchacha20poly1305Ietf, } from "@cosmjs/crypto"; import { toAscii, toBase64, toHex, toUtf8 } from "@cosmjs/encoding"; -import { isUint8Array } from "@cosmjs/utils"; import { rawSecp256k1PubkeyToAddress } from "./address"; import { encodeSecp256k1Signature } from "./signature"; @@ -81,12 +80,13 @@ const serializationType1 = "v1"; const secp256k1WalletSalt = toAscii("Secp256k1Wallet1"); /** - * Not great but can be used on the main thread + * 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 passwordHashingOptions: Argon2idOptions = { +const basicPasswordHashingOptions: Argon2idOptions = { outputLength: 32, - opsLimit: 11, - memLimitKib: 8 * 1024, + opsLimit: 20, + memLimitKib: 12 * 1024, }; const algorithmIdXchacha20poly1305Ietf = "xchacha20poly1305-ietf"; @@ -238,18 +238,28 @@ export class Secp256k1Wallet implements OfflineSigner { /** * Generates an encrypted serialization of this wallet. * - * @param secret If set to a string, a KDF runs internally. If set to an Uin8Array, this is used a the encryption key directly. + * @param password The user provided password used to generate an encryption key via a KDF. + * This is not normalized internally (see "Unicode normalization" to learn more). */ - public async save(secret: string | Uint8Array): Promise { - let encryptionKey: Uint8Array; - if (typeof secret === "string") { - encryptionKey = await Argon2id.execute(secret, secp256k1WalletSalt, passwordHashingOptions); - } else if (isUint8Array(secret)) { - encryptionKey = secret; - } else { - throw new Error("Unsupported type of encryption secret"); - } + public async save(password: string): Promise { + const kdfOption = basicPasswordHashingOptions; + const encryptionKey = await Argon2id.execute(password, secp256k1WalletSalt, kdfOption); + return this.saveWithEncryptionKey(encryptionKey, kdfOption); + } + /** + * Generates an encrypted serialization of this wallet. + * + * This is an advanced alternative of calling `save(password)` directly, which allows you to + * offload the KDF execution to an non-UI thread (e.g. in a WebWorker). + * + * 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. + */ + public async saveWithEncryptionKey( + encryptionKey: Uint8Array, + kdfOptions: Argon2idOptions, + ): Promise { const encrytedData: EncryptedSecp256k1WalletData = { mnemonic: this.mnemonic, accounts: this.accounts.map((account) => ({ @@ -264,7 +274,7 @@ export class Secp256k1Wallet implements OfflineSigner { const out: EncryptedSecp256k1Wallet = { type: serializationType1, - kdf: { algorithm: "scrypt", params: {} }, + kdf: { algorithm: "argon2id", params: { ...kdfOptions } }, encryption: { algorithm: algorithmIdXchacha20poly1305Ietf, params: { diff --git a/packages/sdk38/types/wallet.d.ts b/packages/sdk38/types/wallet.d.ts index 7c8d512c..486513b5 100644 --- a/packages/sdk38/types/wallet.d.ts +++ b/packages/sdk38/types/wallet.d.ts @@ -1,4 +1,4 @@ -import { Slip10RawIndex } from "@cosmjs/crypto"; +import { Argon2idOptions, Slip10RawIndex } from "@cosmjs/crypto"; import { StdSignature } from "./types"; export declare type PrehashType = "sha256" | "sha512" | null; export declare type Algo = "secp256k1" | "ed25519" | "sr25519"; @@ -97,7 +97,18 @@ export declare class Secp256k1Wallet implements OfflineSigner { /** * Generates an encrypted serialization of this wallet. * - * @param secret If set to a string, a KDF runs internally. If set to an Uin8Array, this is used a the encryption key directly. + * @param password The user provided password used to generate an encryption key via a KDF. + * This is not normalized internally (see "Unicode normalization" to learn more). */ - save(secret: string | Uint8Array): Promise; + save(password: string): Promise; + /** + * Generates an encrypted serialization of this wallet. + * + * This is an advanced alternative of calling `save(password)` directly, which allows you to + * offload the KDF execution to an non-UI thread (e.g. in a WebWorker). + * + * 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. + */ + saveWithEncryptionKey(encryptionKey: Uint8Array, kdfOptions: Argon2idOptions): Promise; }