Split into save/saveWithEncryptionKey

This commit is contained in:
Simon Warta 2020-07-22 13:14:38 +02:00
parent bd6efee4f0
commit ed8497b005
4 changed files with 87 additions and 21 deletions

View File

@ -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}$/;

View File

@ -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),
}),
);

View File

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

View File

@ -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<string>;
save(password: string): Promise<string>;
/**
* 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<string>;
}