Add initial attempt of Secp256k1Wallet.save
This commit is contained in:
parent
c42b341fe4
commit
bd6efee4f0
@ -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}$/;
|
||||
|
||||
@ -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),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
49
packages/sdk38/types/wallet.d.ts
vendored
49
packages/sdk38/types/wallet.d.ts
vendored
@ -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>;
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user