Add Secp256k1Wallet.deserialize and .deserializeWithEncryptionKey
This commit is contained in:
parent
588b31fed6
commit
b6c7b8b1d4
@ -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);
|
||||
|
||||
@ -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 */
|
||||
|
||||
12
packages/sdk38/types/wallet.d.ts
vendored
12
packages/sdk38/types/wallet.d.ts
vendored
@ -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 */
|
||||
|
||||
Loading…
Reference in New Issue
Block a user