Split into save/saveWithEncryptionKey
This commit is contained in:
parent
bd6efee4f0
commit
ed8497b005
@ -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}$/;
|
||||
|
||||
@ -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),
|
||||
}),
|
||||
);
|
||||
|
||||
@ -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: {
|
||||
|
||||
17
packages/sdk38/types/wallet.d.ts
vendored
17
packages/sdk38/types/wallet.d.ts
vendored
@ -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>;
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user