Add Secp256k1Wallet.deserialize and .deserializeWithEncryptionKey

This commit is contained in:
Simon Warta 2020-07-22 15:00:18 +02:00
parent 588b31fed6
commit b6c7b8b1d4
3 changed files with 158 additions and 4 deletions

View File

@ -1,8 +1,8 @@
import { Argon2idOptions, Secp256k1, Secp256k1Signature, Sha256 } from "@cosmjs/crypto";
import { Argon2id, Argon2idOptions, Secp256k1, Secp256k1Signature, Sha256 } from "@cosmjs/crypto";
import { fromBase64, fromHex, toAscii } from "@cosmjs/encoding";
import { base64Matcher, hexMatcher } from "./testutils.spec";
import { Secp256k1Wallet } from "./wallet";
import { extractKdfParams, Secp256k1Wallet, secp256k1WalletSalt } from "./wallet";
describe("Secp256k1Wallet", () => {
// m/44'/118'/0'/0/0
@ -34,6 +34,54 @@ describe("Secp256k1Wallet", () => {
});
});
describe("deserialize", () => {
it("can restore", async () => {
const original = await Secp256k1Wallet.fromMnemonic(defaultMnemonic);
const password = "123";
const serialized = await original.serialize(password);
const deserialized = await Secp256k1Wallet.deserialize(serialized, password);
expect(deserialized.mnemonic).toEqual(defaultMnemonic);
expect(await deserialized.getAccounts()).toEqual([
{
algo: "secp256k1",
address: defaultAddress,
pubkey: defaultPubkey,
},
]);
});
});
describe("deserializeWithEncryptionKey", () => {
it("can restore", async () => {
const password = "123";
let serialized: string;
{
const original = await Secp256k1Wallet.fromMnemonic(defaultMnemonic);
const anyKdfParams: Argon2idOptions = {
outputLength: 32,
opsLimit: 4,
memLimitKib: 3 * 1024,
};
const encryptionKey = await Argon2id.execute(password, secp256k1WalletSalt, anyKdfParams);
serialized = await original.serializeWithEncryptionKey(encryptionKey, anyKdfParams);
}
{
const kdfOptions: any = extractKdfParams(serialized);
const encryptionKey = await Argon2id.execute(password, secp256k1WalletSalt, kdfOptions);
const deserialized = await Secp256k1Wallet.deserializeWithEncryptionKey(serialized, encryptionKey);
expect(deserialized.mnemonic).toEqual(defaultMnemonic);
expect(await deserialized.getAccounts()).toEqual([
{
algo: "secp256k1",
address: defaultAddress,
pubkey: defaultPubkey,
},
]);
}
});
});
describe("getAccounts", () => {
it("resolves to a list of accounts if enabled", async () => {
const wallet = await Secp256k1Wallet.fromMnemonic(defaultMnemonic);

View File

@ -11,10 +11,12 @@ import {
Slip10,
Slip10Curve,
Slip10RawIndex,
stringToPath,
xchacha20NonceLength,
Xchacha20poly1305Ietf,
} from "@cosmjs/crypto";
import { toAscii, toBase64, toHex, toUtf8 } from "@cosmjs/encoding";
import { fromBase64, fromHex, fromUtf8, toAscii, toBase64, toHex, toUtf8 } from "@cosmjs/encoding";
import { assert, isNonNullObject } from "@cosmjs/utils";
import { rawSecp256k1PubkeyToAddress } from "./address";
import { encodeSecp256k1Signature } from "./signature";
@ -77,7 +79,7 @@ const serializationType1 = "v1";
* This reduces the scope of a potential rainbow attack to all Secp256k1Wallet v1 users.
* Must be 16 bytes due to implementation limitations.
*/
const secp256k1WalletSalt = toAscii("Secp256k1Wallet1");
export const secp256k1WalletSalt = toAscii("Secp256k1Wallet1");
/**
* A KDF configuration that is not very strong but can be used on the main thread.
@ -128,6 +130,22 @@ export interface EncryptedSecp256k1WalletData {
}>;
}
function extractKdfParamsV1(document: any): Record<string, any> {
return document.kdf.params;
}
export function extractKdfParams(serialization: string): Record<string, any> {
const root = JSON.parse(serialization);
if (!isNonNullObject(root)) throw new Error("Root document is not an onject.");
switch ((root as any).type) {
case serializationType1:
return extractKdfParamsV1(root);
default:
throw new Error("Unsupported serialization type");
}
}
export class Secp256k1Wallet implements OfflineSigner {
/**
* Restores a wallet from the given BIP39 mnemonic.
@ -172,6 +190,82 @@ export class Secp256k1Wallet implements OfflineSigner {
return Secp256k1Wallet.fromMnemonic(mnemonic.toString(), hdPath, prefix);
}
public static async deserialize(serialization: string, password: string): Promise<Secp256k1Wallet> {
const root = JSON.parse(serialization);
if (!isNonNullObject(root)) throw new Error("Root document is not an onject.");
const untypedRoot: any = root;
switch (untypedRoot.type) {
case serializationType1: {
let encryptionKey: Uint8Array;
switch (untypedRoot.kdf.algorithm) {
case "argon2id": {
const kdfOptions = untypedRoot.kdf.params;
encryptionKey = await Argon2id.execute(password, secp256k1WalletSalt, kdfOptions);
break;
}
default:
throw new Error("Unsupported KDF algorithm");
}
const nonce = fromHex(untypedRoot.encryption.params.nonce);
const decryptedBytes = await Xchacha20poly1305Ietf.decrypt(
fromBase64(untypedRoot.value),
encryptionKey,
nonce,
);
const decryptedDocument = JSON.parse(fromUtf8(decryptedBytes));
const { mnemonic, accounts } = decryptedDocument;
assert(typeof mnemonic === "string");
if (!Array.isArray(accounts)) throw new Error("Property 'accounts' is not an array");
if (accounts.length !== 1) throw new Error("Property 'accounts' only supports one entry");
const account = accounts[0];
if (!isNonNullObject(account)) throw new Error("Account is not an onject.");
const { algo, hdPath, prefix } = account as any;
assert(algo === "secp256k1");
assert(typeof hdPath === "string");
assert(typeof prefix === "string");
return Secp256k1Wallet.fromMnemonic(mnemonic, stringToPath(hdPath), prefix);
}
default:
throw new Error("Unsupported serialization type");
}
}
public static async deserializeWithEncryptionKey(
serialization: string,
encryptionKey: Uint8Array,
): Promise<Secp256k1Wallet> {
const root = JSON.parse(serialization);
if (!isNonNullObject(root)) throw new Error("Root document is not an onject.");
const untypedRoot: any = root;
switch (untypedRoot.type) {
case serializationType1: {
const nonce = fromHex(untypedRoot.encryption.params.nonce);
const decryptedBytes = await Xchacha20poly1305Ietf.decrypt(
fromBase64(untypedRoot.value),
encryptionKey,
nonce,
);
const decryptedDocument = JSON.parse(fromUtf8(decryptedBytes));
const { mnemonic, accounts } = decryptedDocument;
assert(typeof mnemonic === "string");
if (!Array.isArray(accounts)) throw new Error("Property 'accounts' is not an array");
if (accounts.length !== 1) throw new Error("Property 'accounts' only supports one entry");
const account = accounts[0];
if (!isNonNullObject(account)) throw new Error("Account is not an onject.");
const { algo, hdPath, prefix } = account as any;
assert(algo === "secp256k1");
assert(typeof hdPath === "string");
assert(typeof prefix === "string");
return Secp256k1Wallet.fromMnemonic(mnemonic, stringToPath(hdPath), prefix);
}
default:
throw new Error("Unsupported serialization type");
}
}
/** Base secret */
private readonly secret: EnglishMnemonic;
/** Derivation instrations */

View File

@ -22,6 +22,12 @@ 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
*/
@ -57,6 +63,7 @@ export interface EncryptedSecp256k1WalletData {
readonly prefix: string;
}>;
}
export declare function extractKdfParams(serialization: string): Record<string, any>;
export declare class Secp256k1Wallet implements OfflineSigner {
/**
* Restores a wallet from the given BIP39 mnemonic.
@ -82,6 +89,11 @@ export declare class Secp256k1Wallet implements OfflineSigner {
hdPath?: readonly Slip10RawIndex[],
prefix?: string,
): Promise<Secp256k1Wallet>;
static deserialize(serialization: string, password: string): Promise<Secp256k1Wallet>;
static deserializeWithEncryptionKey(
serialization: string,
encryptionKey: Uint8Array,
): Promise<Secp256k1Wallet>;
/** Base secret */
private readonly secret;
/** Derivation instrations */