Pull out executeKdf

This commit is contained in:
Simon Warta 2020-07-22 16:15:45 +02:00
parent 21202e821d
commit 91aec70650
3 changed files with 78 additions and 59 deletions

View File

@ -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: {

View File

@ -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<string, unknown>;
};
readonly kdf: KdfConfiguration;
/** Information about the symmetric encryption */
readonly encryption: {
/**
@ -135,22 +131,46 @@ export interface Secp256k1WalletData {
}>;
}
function extractKdfParamsV1(document: any): Record<string, any> {
return document.kdf.params;
function extractKdfConfigurationV1(document: any): KdfConfiguration {
return document.kdf;
}
export function extractKdfParams(serialization: string): Record<string, any> {
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<string, unknown>;
}
export async function executeKdf(password: string, configuration: KdfConfiguration): Promise<Uint8Array> {
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<string> {
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<string> {
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: {

View File

@ -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<string, unknown>;
};
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<string, any>;
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<string, unknown>;
}
export declare function executeKdf(password: string, configuration: KdfConfiguration): Promise<Uint8Array>;
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<string>;
serializeWithEncryptionKey(encryptionKey: Uint8Array, kdfConfiguration: KdfConfiguration): Promise<string>;
}