Add initial attempt of Secp256k1Wallet.save

This commit is contained in:
Simon Warta 2020-07-22 10:39:44 +02:00
parent c42b341fe4
commit bd6efee4f0
4 changed files with 201 additions and 13 deletions

View File

@ -19,6 +19,8 @@ export const tendermintAddressMatcher = /^[0-9A-F]{40}$/;
export const tendermintShortHashMatcher = /^[0-9a-f]{40}$/;
export const dateTimeStampMatcher = /^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\.[0-9]+)?Z$/;
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}===))$/;
// 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,6 +1,7 @@
import { Secp256k1, Secp256k1Signature, Sha256 } from "@cosmjs/crypto";
import { fromBase64, fromHex, toAscii } from "@cosmjs/encoding";
import { base64Matcher } from "./testutils.spec";
import { Secp256k1Wallet } from "./wallet";
describe("Secp256k1Wallet", () => {
@ -67,4 +68,17 @@ describe("Secp256k1Wallet", () => {
expect(valid).toEqual(true);
});
});
describe("save", () => {
it("can save with password", async () => {
const wallet = await Secp256k1Wallet.fromMnemonic(defaultMnemonic);
const serialized = await wallet.save("123");
expect(JSON.parse(serialized)).toEqual(
jasmine.objectContaining({
type: "v1",
value: jasmine.stringMatching(base64Matcher),
}),
);
});
});
});

View File

@ -1,6 +1,9 @@
import {
Argon2id,
Argon2idOptions,
Bip39,
EnglishMnemonic,
pathToString,
Random,
Secp256k1,
Sha256,
@ -8,7 +11,11 @@ import {
Slip10,
Slip10Curve,
Slip10RawIndex,
xchacha20NonceLength,
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";
@ -64,6 +71,63 @@ export function makeCosmoshubPath(a: number): readonly Slip10RawIndex[] {
];
}
const serializationType1 = "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.
* Must be 16 bytes due to implementation limitations.
*/
const secp256k1WalletSalt = toAscii("Secp256k1Wallet1");
/**
* Not great but can be used on the main thread
*/
const passwordHashingOptions: Argon2idOptions = {
outputLength: 32,
opsLimit: 11,
memLimitKib: 8 * 1024,
};
const algorithmIdXchacha20poly1305Ietf = "xchacha20poly1305-ietf";
/**
* This interface describes a JSON object holding the encrypted wallet and the meta data
*/
export interface EncryptedSecp256k1Wallet {
/** 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>;
};
/** Information about the symmetric encryption */
readonly encryption: {
/**
* An algorithm identifier, such as "xchacha20poly1305-ietf".
*/
readonly algorithm: string;
/** A map of algorithm-specific parameters */
readonly params: Record<string, unknown>;
};
/** base64 encoded enccrypted value */
readonly value: string;
}
export interface EncryptedSecp256k1WalletData {
readonly mnemonic: string;
readonly accounts: ReadonlyArray<{
readonly algo: string;
readonly hdPath: string;
readonly prefix: string;
}>;
}
export class Secp256k1Wallet implements OfflineSigner {
/**
* Restores a wallet from the given BIP39 mnemonic.
@ -81,7 +145,13 @@ export class Secp256k1Wallet implements OfflineSigner {
const seed = await Bip39.mnemonicToSeed(mnemonicChecked);
const { privkey } = Slip10.derivePath(Slip10Curve.Secp256k1, seed, hdPath);
const uncompressed = (await Secp256k1.makeKeypair(privkey)).pubkey;
return new Secp256k1Wallet(mnemonicChecked, privkey, Secp256k1.compressPubkey(uncompressed), prefix);
return new Secp256k1Wallet(
mnemonicChecked,
hdPath,
privkey,
Secp256k1.compressPubkey(uncompressed),
prefix,
);
}
/**
@ -102,32 +172,50 @@ export class Secp256k1Wallet implements OfflineSigner {
return Secp256k1Wallet.fromMnemonic(mnemonic.toString(), hdPath, prefix);
}
private readonly mnemonicData: EnglishMnemonic;
/** Base secret */
private readonly secret: EnglishMnemonic;
/** Derivation instrations */
private readonly accounts: ReadonlyArray<{
readonly algo: Algo;
readonly hdPath: readonly Slip10RawIndex[];
readonly prefix: string;
}>;
/** Derived data */
private readonly pubkey: Uint8Array;
private readonly privkey: Uint8Array;
private readonly prefix: string;
private readonly algo: Algo = "secp256k1";
private constructor(mnemonic: EnglishMnemonic, privkey: Uint8Array, pubkey: Uint8Array, prefix: string) {
this.mnemonicData = mnemonic;
private constructor(
mnemonic: EnglishMnemonic,
hdPath: readonly Slip10RawIndex[],
privkey: Uint8Array,
pubkey: Uint8Array,
prefix: string,
) {
this.secret = mnemonic;
this.accounts = [
{
algo: "secp256k1",
hdPath: hdPath,
prefix: prefix,
},
];
this.privkey = privkey;
this.pubkey = pubkey;
this.prefix = prefix;
}
public get mnemonic(): string {
return this.mnemonicData.toString();
return this.secret.toString();
}
private get address(): string {
return rawSecp256k1PubkeyToAddress(this.pubkey, this.prefix);
return rawSecp256k1PubkeyToAddress(this.pubkey, this.accounts[0].prefix);
}
public async getAccounts(): Promise<readonly AccountData[]> {
return [
{
address: this.address,
algo: this.algo,
algo: this.accounts[0].algo,
pubkey: this.pubkey,
},
];
@ -146,4 +234,45 @@ export class Secp256k1Wallet implements OfflineSigner {
const signatureBytes = new Uint8Array([...signature.r(32), ...signature.s(32)]);
return encodeSecp256k1Signature(this.pubkey, signatureBytes);
}
/**
* 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.
*/
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");
}
const encrytedData: EncryptedSecp256k1WalletData = {
mnemonic: this.mnemonic,
accounts: this.accounts.map((account) => ({
algo: account.algo,
hdPath: pathToString(account.hdPath),
prefix: account.prefix,
})),
};
const message = toUtf8(JSON.stringify(encrytedData));
const nonce = Random.getBytes(xchacha20NonceLength);
const encrypted = await Xchacha20poly1305Ietf.encrypt(message, encryptionKey, nonce);
const out: EncryptedSecp256k1Wallet = {
type: serializationType1,
kdf: { algorithm: "scrypt", params: {} },
encryption: {
algorithm: algorithmIdXchacha20poly1305Ietf,
params: {
nonce: toHex(nonce),
},
},
value: toBase64(encrypted),
};
return JSON.stringify(out);
}
}

View File

@ -22,6 +22,41 @@ export interface OfflineSigner {
* with 0-based account index `a`.
*/
export declare function makeCosmoshubPath(a: number): readonly Slip10RawIndex[];
/**
* This interface describes a JSON object holding the encrypted wallet and the meta data
*/
export interface EncryptedSecp256k1Wallet {
/** 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>;
};
/** Information about the symmetric encryption */
readonly encryption: {
/**
* An algorithm identifier, such as "xchacha20poly1305-ietf".
*/
readonly algorithm: string;
/** A map of algorithm-specific parameters */
readonly params: Record<string, unknown>;
};
/** base64 encoded enccrypted value */
readonly value: string;
}
export interface EncryptedSecp256k1WalletData {
readonly mnemonic: string;
readonly accounts: ReadonlyArray<{
readonly algo: string;
readonly hdPath: string;
readonly prefix: string;
}>;
}
export declare class Secp256k1Wallet implements OfflineSigner {
/**
* Restores a wallet from the given BIP39 mnemonic.
@ -47,14 +82,22 @@ export declare class Secp256k1Wallet implements OfflineSigner {
hdPath?: readonly Slip10RawIndex[],
prefix?: string,
): Promise<Secp256k1Wallet>;
private readonly mnemonicData;
/** Base secret */
private readonly secret;
/** Derivation instrations */
private readonly accounts;
/** Derived data */
private readonly pubkey;
private readonly privkey;
private readonly prefix;
private readonly algo;
private constructor();
get mnemonic(): string;
private get address();
getAccounts(): Promise<readonly AccountData[]>;
sign(address: string, message: Uint8Array, prehashType?: PrehashType): Promise<StdSignature>;
/**
* 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.
*/
save(secret: string | Uint8Array): Promise<string>;
}