Merge pull request #796 from cosmos/776-direct-wallet-serialization
Add serialization to DirectSecp256k1HdWallet
This commit is contained in:
commit
d141a2101a
@ -8,6 +8,11 @@ and this project adheres to
|
||||
|
||||
### Added
|
||||
|
||||
- @cosmjs/proto-signing: Add `serialize` and `serializeWithEncryptionKey`
|
||||
methods to `DirectSecp256k1HdWallet`. Also add `deserialize` and
|
||||
`deserializeWithEncryptionKey` static methods.
|
||||
- @cosmjs/proto-signing: Export `extractKdfConfiguration` and `executeKdf`
|
||||
helper functions and `KdfConfiguration` type.
|
||||
- @cosmjs/proto-signing: Export `makeCosmoshubPath` helper.
|
||||
- @cosmjs/stargate: Export `makeCosmoshubPath` helper.
|
||||
|
||||
|
||||
@ -275,7 +275,7 @@ describe("Secp256k1HdWallet", () => {
|
||||
algorithm: "argon2id",
|
||||
params: {
|
||||
outputLength: 32,
|
||||
opsLimit: 20,
|
||||
opsLimit: 24,
|
||||
memLimitKib: 12 * 1024,
|
||||
},
|
||||
},
|
||||
|
||||
@ -36,13 +36,13 @@ const serializationTypeV1 = "secp256k1wallet-v1";
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* It takes about 1 second in Node.js 16.0.0 and should have similar runtimes in other modern Wasm hosts.
|
||||
*/
|
||||
const basicPasswordHashingOptions: KdfConfiguration = {
|
||||
algorithm: "argon2id",
|
||||
params: {
|
||||
outputLength: 32,
|
||||
opsLimit: 20,
|
||||
opsLimit: 24,
|
||||
memLimitKib: 12 * 1024,
|
||||
},
|
||||
};
|
||||
|
||||
@ -2,9 +2,10 @@ import { coins, makeCosmoshubPath } from "@cosmjs/amino";
|
||||
import { Secp256k1, Secp256k1Signature, sha256 } from "@cosmjs/crypto";
|
||||
import { fromBase64, fromHex } from "@cosmjs/encoding";
|
||||
|
||||
import { DirectSecp256k1HdWallet } from "./directsecp256k1hdwallet";
|
||||
import { DirectSecp256k1HdWallet, extractKdfConfiguration } from "./directsecp256k1hdwallet";
|
||||
import { makeAuthInfoBytes, makeSignBytes, makeSignDoc } from "./signing";
|
||||
import { faucet, testVectors } from "./testutils.spec";
|
||||
import { base64Matcher, faucet, testVectors } from "./testutils.spec";
|
||||
import { executeKdf, KdfConfiguration } from "./wallet";
|
||||
|
||||
describe("DirectSecp256k1HdWallet", () => {
|
||||
// m/44'/118'/0'/0/0
|
||||
@ -60,6 +61,175 @@ describe("DirectSecp256k1HdWallet", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("deserialize", () => {
|
||||
it("can restore", async () => {
|
||||
const original = await DirectSecp256k1HdWallet.fromMnemonic(defaultMnemonic);
|
||||
const password = "123";
|
||||
const serialized = await original.serialize(password);
|
||||
const deserialized = await DirectSecp256k1HdWallet.deserialize(serialized, password);
|
||||
const accounts = await deserialized.getAccounts();
|
||||
|
||||
expect(deserialized.mnemonic).toEqual(defaultMnemonic);
|
||||
expect(accounts).toEqual([
|
||||
{
|
||||
algo: "secp256k1",
|
||||
address: defaultAddress,
|
||||
pubkey: defaultPubkey,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("can restore multiple accounts", async () => {
|
||||
const mnemonic =
|
||||
"economy stock theory fatal elder harbor betray wasp final emotion task crumble siren bottom lizard educate guess current outdoor pair theory focus wife stone";
|
||||
const prefix = "wasm";
|
||||
const accountNumbers = [0, 1, 2, 3, 4];
|
||||
const hdPaths = accountNumbers.map(makeCosmoshubPath);
|
||||
const original = await DirectSecp256k1HdWallet.fromMnemonic(mnemonic, {
|
||||
hdPaths: hdPaths,
|
||||
prefix: prefix,
|
||||
});
|
||||
const password = "123";
|
||||
const serialized = await original.serialize(password);
|
||||
const deserialized = await DirectSecp256k1HdWallet.deserialize(serialized, password);
|
||||
const accounts = await deserialized.getAccounts();
|
||||
|
||||
expect(deserialized.mnemonic).toEqual(mnemonic);
|
||||
// These values are taken from the generate_addresses.js script in the scripts/wasmd directory
|
||||
expect(accounts).toEqual([
|
||||
{
|
||||
algo: "secp256k1",
|
||||
pubkey: fromBase64("A08EGB7ro1ORuFhjOnZcSgwYlpe0DSFjVNUIkNNQxwKQ"),
|
||||
address: "wasm1pkptre7fdkl6gfrzlesjjvhxhlc3r4gm32kke3",
|
||||
},
|
||||
{
|
||||
algo: "secp256k1",
|
||||
pubkey: fromBase64("AiDosfIbBi54XJ1QjCeApumcy/FjdtF+YhywPf3DKTx7"),
|
||||
address: "wasm10dyr9899g6t0pelew4nvf4j5c3jcgv0r5d3a5l",
|
||||
},
|
||||
{
|
||||
algo: "secp256k1",
|
||||
pubkey: fromBase64("AzQg33JZqH7vSsm09esZY5bZvmzYwE/SY78cA0iLxpD7"),
|
||||
address: "wasm1xy4yqngt0nlkdcenxymg8tenrghmek4n3u2lwa",
|
||||
},
|
||||
{
|
||||
algo: "secp256k1",
|
||||
pubkey: fromBase64("A3gOAlB6aiRTCPvWMQg2+ZbGYNsLd8qlvV28m8p2UhY2"),
|
||||
address: "wasm142u9fgcjdlycfcez3lw8x6x5h7rfjlnfaallkd",
|
||||
},
|
||||
{
|
||||
algo: "secp256k1",
|
||||
pubkey: fromBase64("Aum2063ub/ErUnIUB36sK55LktGUStgcbSiaAnL1wadu"),
|
||||
address: "wasm1hsm76p4ahyhl5yh3ve9ur49r5kemhp2r93f89d",
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("deserializeWithEncryptionKey", () => {
|
||||
it("can restore", async () => {
|
||||
const password = "123";
|
||||
let serialized: string;
|
||||
{
|
||||
const original = await DirectSecp256k1HdWallet.fromMnemonic(defaultMnemonic);
|
||||
const anyKdfParams: KdfConfiguration = {
|
||||
algorithm: "argon2id",
|
||||
params: {
|
||||
outputLength: 32,
|
||||
opsLimit: 4,
|
||||
memLimitKib: 3 * 1024,
|
||||
},
|
||||
};
|
||||
const encryptionKey = await executeKdf(password, anyKdfParams);
|
||||
serialized = await original.serializeWithEncryptionKey(encryptionKey, anyKdfParams);
|
||||
}
|
||||
|
||||
{
|
||||
const kdfConfiguration = extractKdfConfiguration(serialized);
|
||||
const encryptionKey = await executeKdf(password, kdfConfiguration);
|
||||
const deserialized = await DirectSecp256k1HdWallet.deserializeWithEncryptionKey(
|
||||
serialized,
|
||||
encryptionKey,
|
||||
);
|
||||
expect(deserialized.mnemonic).toEqual(defaultMnemonic);
|
||||
expect(await deserialized.getAccounts()).toEqual([
|
||||
{
|
||||
algo: "secp256k1",
|
||||
address: defaultAddress,
|
||||
pubkey: defaultPubkey,
|
||||
},
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
it("can restore multiple accounts", async () => {
|
||||
const mnemonic =
|
||||
"economy stock theory fatal elder harbor betray wasp final emotion task crumble siren bottom lizard educate guess current outdoor pair theory focus wife stone";
|
||||
const prefix = "wasm";
|
||||
const password = "123";
|
||||
const accountNumbers = [0, 1, 2, 3, 4];
|
||||
const hdPaths = accountNumbers.map(makeCosmoshubPath);
|
||||
let serialized: string;
|
||||
{
|
||||
const original = await DirectSecp256k1HdWallet.fromMnemonic(mnemonic, {
|
||||
prefix: prefix,
|
||||
hdPaths: hdPaths,
|
||||
});
|
||||
const anyKdfParams: KdfConfiguration = {
|
||||
algorithm: "argon2id",
|
||||
params: {
|
||||
outputLength: 32,
|
||||
opsLimit: 4,
|
||||
memLimitKib: 3 * 1024,
|
||||
},
|
||||
};
|
||||
const encryptionKey = await executeKdf(password, anyKdfParams);
|
||||
serialized = await original.serializeWithEncryptionKey(encryptionKey, anyKdfParams);
|
||||
}
|
||||
|
||||
{
|
||||
const kdfConfiguration = extractKdfConfiguration(serialized);
|
||||
const encryptionKey = await executeKdf(password, kdfConfiguration);
|
||||
const deserialized = await DirectSecp256k1HdWallet.deserializeWithEncryptionKey(
|
||||
serialized,
|
||||
encryptionKey,
|
||||
);
|
||||
const accounts = await deserialized.getAccounts();
|
||||
|
||||
expect(deserialized.mnemonic).toEqual(mnemonic);
|
||||
expect(deserialized.mnemonic).toEqual(mnemonic);
|
||||
// These values are taken from the generate_addresses.js script in the scripts/wasmd directory
|
||||
expect(accounts).toEqual([
|
||||
{
|
||||
algo: "secp256k1",
|
||||
pubkey: fromBase64("A08EGB7ro1ORuFhjOnZcSgwYlpe0DSFjVNUIkNNQxwKQ"),
|
||||
address: "wasm1pkptre7fdkl6gfrzlesjjvhxhlc3r4gm32kke3",
|
||||
},
|
||||
{
|
||||
algo: "secp256k1",
|
||||
pubkey: fromBase64("AiDosfIbBi54XJ1QjCeApumcy/FjdtF+YhywPf3DKTx7"),
|
||||
address: "wasm10dyr9899g6t0pelew4nvf4j5c3jcgv0r5d3a5l",
|
||||
},
|
||||
{
|
||||
algo: "secp256k1",
|
||||
pubkey: fromBase64("AzQg33JZqH7vSsm09esZY5bZvmzYwE/SY78cA0iLxpD7"),
|
||||
address: "wasm1xy4yqngt0nlkdcenxymg8tenrghmek4n3u2lwa",
|
||||
},
|
||||
{
|
||||
algo: "secp256k1",
|
||||
pubkey: fromBase64("A3gOAlB6aiRTCPvWMQg2+ZbGYNsLd8qlvV28m8p2UhY2"),
|
||||
address: "wasm142u9fgcjdlycfcez3lw8x6x5h7rfjlnfaallkd",
|
||||
},
|
||||
{
|
||||
algo: "secp256k1",
|
||||
pubkey: fromBase64("Aum2063ub/ErUnIUB36sK55LktGUStgcbSiaAnL1wadu"),
|
||||
address: "wasm1hsm76p4ahyhl5yh3ve9ur49r5kemhp2r93f89d",
|
||||
},
|
||||
]);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("getAccounts", () => {
|
||||
it("resolves to a list of accounts", async () => {
|
||||
const wallet = await DirectSecp256k1HdWallet.fromMnemonic(defaultMnemonic);
|
||||
@ -108,4 +278,51 @@ describe("DirectSecp256k1HdWallet", () => {
|
||||
expect(valid).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("serialize", () => {
|
||||
it("can save with password", async () => {
|
||||
const wallet = await DirectSecp256k1HdWallet.fromMnemonic(defaultMnemonic);
|
||||
const serialized = await wallet.serialize("123");
|
||||
expect(JSON.parse(serialized)).toEqual({
|
||||
type: "directsecp256k1hdwallet-v1",
|
||||
kdf: {
|
||||
algorithm: "argon2id",
|
||||
params: {
|
||||
outputLength: 32,
|
||||
opsLimit: 24,
|
||||
memLimitKib: 12 * 1024,
|
||||
},
|
||||
},
|
||||
encryption: {
|
||||
algorithm: "xchacha20poly1305-ietf",
|
||||
},
|
||||
data: jasmine.stringMatching(base64Matcher),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("serializeWithEncryptionKey", () => {
|
||||
it("can save with password", async () => {
|
||||
const wallet = await DirectSecp256k1HdWallet.fromMnemonic(defaultMnemonic);
|
||||
|
||||
const key = fromHex("aabb221100aabb332211aabb33221100aabb221100aabb332211aabb33221100");
|
||||
const customKdfConfiguration: KdfConfiguration = {
|
||||
algorithm: "argon2id",
|
||||
params: {
|
||||
outputLength: 32,
|
||||
opsLimit: 321,
|
||||
memLimitKib: 11 * 1024,
|
||||
},
|
||||
};
|
||||
const serialized = await wallet.serializeWithEncryptionKey(key, customKdfConfiguration);
|
||||
expect(JSON.parse(serialized)).toEqual({
|
||||
type: "directsecp256k1hdwallet-v1",
|
||||
kdf: customKdfConfiguration,
|
||||
encryption: {
|
||||
algorithm: "xchacha20poly1305-ietf",
|
||||
},
|
||||
data: jasmine.stringMatching(base64Matcher),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -3,23 +3,64 @@ import {
|
||||
Bip39,
|
||||
EnglishMnemonic,
|
||||
HdPath,
|
||||
pathToString,
|
||||
Random,
|
||||
Secp256k1,
|
||||
Secp256k1Keypair,
|
||||
sha256,
|
||||
Slip10,
|
||||
Slip10Curve,
|
||||
stringToPath,
|
||||
} from "@cosmjs/crypto";
|
||||
import { Bech32 } from "@cosmjs/encoding";
|
||||
import { Bech32, fromBase64, fromUtf8, toBase64, toUtf8 } from "@cosmjs/encoding";
|
||||
import { assert, isNonNullObject } from "@cosmjs/utils/build";
|
||||
|
||||
import { SignDoc } from "./codec/cosmos/tx/v1beta1/tx";
|
||||
import { AccountData, DirectSignResponse, OfflineDirectSigner } from "./signer";
|
||||
import { makeSignBytes } from "./signing";
|
||||
import {
|
||||
decrypt,
|
||||
encrypt,
|
||||
EncryptionConfiguration,
|
||||
executeKdf,
|
||||
KdfConfiguration,
|
||||
supportedAlgorithms,
|
||||
} from "./wallet";
|
||||
|
||||
interface AccountDataWithPrivkey extends AccountData {
|
||||
readonly privkey: Uint8Array;
|
||||
}
|
||||
|
||||
const serializationTypeV1 = "directsecp256k1hdwallet-v1";
|
||||
|
||||
/**
|
||||
* A KDF configuration that is not very strong but can be used on the main thread.
|
||||
* It takes about 1 second in Node.js 16.0.0 and should have similar runtimes in other modern Wasm hosts.
|
||||
*/
|
||||
const basicPasswordHashingOptions: KdfConfiguration = {
|
||||
algorithm: "argon2id",
|
||||
params: {
|
||||
outputLength: 32,
|
||||
opsLimit: 24,
|
||||
memLimitKib: 12 * 1024,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* This interface describes a JSON object holding the encrypted wallet and the meta data.
|
||||
* All fields in here must be JSON types.
|
||||
*/
|
||||
export interface DirectSecp256k1HdWalletSerialization {
|
||||
/** A format+version identifier for this serialization format */
|
||||
readonly type: string;
|
||||
/** Information about the key derivation function (i.e. password to encryption key) */
|
||||
readonly kdf: KdfConfiguration;
|
||||
/** Information about the symmetric encryption */
|
||||
readonly encryption: EncryptionConfiguration;
|
||||
/** An instance of Secp256k1HdWalletData, which is stringified, encrypted and base64 encoded. */
|
||||
readonly data: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Derivation information required to derive a keypair and an address from a mnemonic.
|
||||
*/
|
||||
@ -28,6 +69,47 @@ interface Secp256k1Derivation {
|
||||
readonly prefix: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Derivation information required to derive a keypair and an address from a mnemonic.
|
||||
* All fields in here must be JSON types.
|
||||
*/
|
||||
interface DerivationInfoJson {
|
||||
readonly hdPath: string;
|
||||
readonly prefix: string;
|
||||
}
|
||||
|
||||
function isDerivationJson(thing: unknown): thing is DerivationInfoJson {
|
||||
if (!isNonNullObject(thing)) return false;
|
||||
if (typeof (thing as DerivationInfoJson).hdPath !== "string") return false;
|
||||
if (typeof (thing as DerivationInfoJson).prefix !== "string") return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* The data of a wallet serialization that is encrypted.
|
||||
* All fields in here must be JSON types.
|
||||
*/
|
||||
interface DirectSecp256k1HdWalletData {
|
||||
readonly mnemonic: string;
|
||||
readonly accounts: readonly DerivationInfoJson[];
|
||||
}
|
||||
|
||||
function extractKdfConfigurationV1(doc: any): KdfConfiguration {
|
||||
return doc.kdf;
|
||||
}
|
||||
|
||||
export function extractKdfConfiguration(serialization: string): KdfConfiguration {
|
||||
const root = JSON.parse(serialization);
|
||||
if (!isNonNullObject(root)) throw new Error("Root document is not an object.");
|
||||
|
||||
switch ((root as any).type) {
|
||||
case serializationTypeV1:
|
||||
return extractKdfConfigurationV1(root);
|
||||
default:
|
||||
throw new Error("Unsupported serialization type");
|
||||
}
|
||||
}
|
||||
|
||||
export interface DirectSecp256k1HdWalletOptions {
|
||||
/** The password to use when deriving a BIP39 seed from a mnemonic. */
|
||||
readonly bip39Password: string;
|
||||
@ -83,6 +165,77 @@ export class DirectSecp256k1HdWallet implements OfflineDirectSigner {
|
||||
return DirectSecp256k1HdWallet.fromMnemonic(mnemonic.toString(), options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores a wallet from an encrypted serialization.
|
||||
*
|
||||
* @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 static async deserialize(serialization: string, password: string): Promise<DirectSecp256k1HdWallet> {
|
||||
const root = JSON.parse(serialization);
|
||||
if (!isNonNullObject(root)) throw new Error("Root document is not an object.");
|
||||
switch ((root as any).type) {
|
||||
case serializationTypeV1:
|
||||
return DirectSecp256k1HdWallet.deserializeTypeV1(serialization, password);
|
||||
default:
|
||||
throw new Error("Unsupported serialization type");
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Restores a wallet from an encrypted serialization.
|
||||
*
|
||||
* This is an advanced alternative to calling `deserialize(serialization, password)` directly, which allows
|
||||
* you to offload the KDF execution to a non-UI thread (e.g. in a WebWorker).
|
||||
*
|
||||
* The caller is responsible for ensuring the key was derived with the given KDF configuration. This can be
|
||||
* done using `extractKdfConfiguration(serialization)` and `executeKdf(password, kdfConfiguration)` from this package.
|
||||
*/
|
||||
public static async deserializeWithEncryptionKey(
|
||||
serialization: string,
|
||||
encryptionKey: Uint8Array,
|
||||
): Promise<DirectSecp256k1HdWallet> {
|
||||
const root = JSON.parse(serialization);
|
||||
if (!isNonNullObject(root)) throw new Error("Root document is not an object.");
|
||||
const untypedRoot: any = root;
|
||||
switch (untypedRoot.type) {
|
||||
case serializationTypeV1: {
|
||||
const decryptedBytes = await decrypt(
|
||||
fromBase64(untypedRoot.data),
|
||||
encryptionKey,
|
||||
untypedRoot.encryption,
|
||||
);
|
||||
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.every((account) => isDerivationJson(account))) {
|
||||
throw new Error("Account is not in the correct format.");
|
||||
}
|
||||
const firstPrefix = accounts[0].prefix;
|
||||
if (!accounts.every(({ prefix }) => prefix === firstPrefix)) {
|
||||
throw new Error("Accounts do not all have the same prefix");
|
||||
}
|
||||
const hdPaths = accounts.map(({ hdPath }) => stringToPath(hdPath));
|
||||
return DirectSecp256k1HdWallet.fromMnemonic(mnemonic, {
|
||||
hdPaths: hdPaths,
|
||||
prefix: firstPrefix,
|
||||
});
|
||||
}
|
||||
default:
|
||||
throw new Error("Unsupported serialization type");
|
||||
}
|
||||
}
|
||||
|
||||
private static async deserializeTypeV1(
|
||||
serialization: string,
|
||||
password: string,
|
||||
): Promise<DirectSecp256k1HdWallet> {
|
||||
const root = JSON.parse(serialization);
|
||||
if (!isNonNullObject(root)) throw new Error("Root document is not an object.");
|
||||
const encryptionKey = await executeKdf(password, (root as any).kdf);
|
||||
return DirectSecp256k1HdWallet.deserializeWithEncryptionKey(serialization, encryptionKey);
|
||||
}
|
||||
|
||||
/** Base secret */
|
||||
private readonly secret: EnglishMnemonic;
|
||||
/** BIP39 seed */
|
||||
@ -132,6 +285,54 @@ export class DirectSecp256k1HdWallet implements OfflineDirectSigner {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an encrypted serialization of this wallet.
|
||||
*
|
||||
* @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 serialize(password: string): Promise<string> {
|
||||
const kdfConfiguration = basicPasswordHashingOptions;
|
||||
const encryptionKey = await executeKdf(password, kdfConfiguration);
|
||||
return this.serializeWithEncryptionKey(encryptionKey, kdfConfiguration);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an encrypted serialization of this wallet.
|
||||
*
|
||||
* This is an advanced alternative to calling `serialize(password)` directly, which allows you to
|
||||
* offload the KDF execution to a 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 serializeWithEncryptionKey(
|
||||
encryptionKey: Uint8Array,
|
||||
kdfConfiguration: KdfConfiguration,
|
||||
): Promise<string> {
|
||||
const dataToEncrypt: DirectSecp256k1HdWalletData = {
|
||||
mnemonic: this.mnemonic,
|
||||
accounts: this.accounts.map(({ hdPath, prefix }) => ({
|
||||
hdPath: pathToString(hdPath),
|
||||
prefix: prefix,
|
||||
})),
|
||||
};
|
||||
const dataToEncryptRaw = toUtf8(JSON.stringify(dataToEncrypt));
|
||||
|
||||
const encryptionConfiguration: EncryptionConfiguration = {
|
||||
algorithm: supportedAlgorithms.xchacha20poly1305Ietf,
|
||||
};
|
||||
const encryptedData = await encrypt(dataToEncryptRaw, encryptionKey, encryptionConfiguration);
|
||||
|
||||
const out: DirectSecp256k1HdWalletSerialization = {
|
||||
type: serializationTypeV1,
|
||||
kdf: kdfConfiguration,
|
||||
encryption: encryptionConfiguration,
|
||||
data: toBase64(encryptedData),
|
||||
};
|
||||
return JSON.stringify(out);
|
||||
}
|
||||
|
||||
private async getKeyPair(hdPath: HdPath): Promise<Secp256k1Keypair> {
|
||||
const { privkey } = Slip10.derivePath(Slip10Curve.Secp256k1, this.seed, hdPath);
|
||||
const { pubkey } = await Secp256k1.makeKeypair(privkey);
|
||||
|
||||
@ -14,7 +14,11 @@ export {
|
||||
TsProtoGeneratedType,
|
||||
TxBodyEncodeObject,
|
||||
} from "./registry";
|
||||
export { DirectSecp256k1HdWallet, DirectSecp256k1HdWalletOptions } from "./directsecp256k1hdwallet";
|
||||
export {
|
||||
extractKdfConfiguration,
|
||||
DirectSecp256k1HdWallet,
|
||||
DirectSecp256k1HdWalletOptions,
|
||||
} from "./directsecp256k1hdwallet";
|
||||
export { DirectSecp256k1Wallet } from "./directsecp256k1wallet";
|
||||
export { makeCosmoshubPath } from "./paths";
|
||||
export { decodePubkey, encodePubkey } from "./pubkey";
|
||||
@ -27,3 +31,4 @@ export {
|
||||
OfflineSigner,
|
||||
} from "./signer";
|
||||
export { makeAuthInfoBytes, makeSignBytes, makeSignDoc } from "./signing";
|
||||
export { executeKdf, KdfConfiguration } from "./wallet";
|
||||
|
||||
87
packages/proto-signing/src/wallet.ts
Normal file
87
packages/proto-signing/src/wallet.ts
Normal file
@ -0,0 +1,87 @@
|
||||
import {
|
||||
Argon2id,
|
||||
isArgon2idOptions,
|
||||
Random,
|
||||
xchacha20NonceLength,
|
||||
Xchacha20poly1305Ietf,
|
||||
} from "@cosmjs/crypto";
|
||||
import { toAscii } from "@cosmjs/encoding";
|
||||
|
||||
/**
|
||||
* A fixed salt is chosen to archive a deterministic password to key derivation.
|
||||
* This reduces the scope of a potential rainbow attack to all CosmJS users.
|
||||
* Must be 16 bytes due to implementation limitations.
|
||||
*/
|
||||
export const cosmjsSalt = toAscii("The CosmJS salt.");
|
||||
|
||||
export interface KdfConfiguration {
|
||||
/**
|
||||
* An algorithm identifier, such as "argon2id" or "scrypt".
|
||||
*/
|
||||
readonly algorithm: string;
|
||||
/** A map of algorithm-specific parameters */
|
||||
readonly params: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export async function executeKdf(password: string, configuration: KdfConfiguration): Promise<Uint8Array> {
|
||||
switch (configuration.algorithm) {
|
||||
case "argon2id": {
|
||||
const options = configuration.params;
|
||||
if (!isArgon2idOptions(options)) throw new Error("Invalid format of argon2id params");
|
||||
return Argon2id.execute(password, cosmjsSalt, options);
|
||||
}
|
||||
default:
|
||||
throw new Error("Unsupported KDF algorithm");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration how to encrypt data or how data was encrypted.
|
||||
* This is stored as part of the wallet serialization and must only contain JSON types.
|
||||
*/
|
||||
export interface EncryptionConfiguration {
|
||||
/**
|
||||
* An algorithm identifier, such as "xchacha20poly1305-ietf".
|
||||
*/
|
||||
readonly algorithm: string;
|
||||
/** A map of algorithm-specific parameters */
|
||||
readonly params?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export const supportedAlgorithms = {
|
||||
xchacha20poly1305Ietf: "xchacha20poly1305-ietf",
|
||||
};
|
||||
|
||||
export async function encrypt(
|
||||
plaintext: Uint8Array,
|
||||
encryptionKey: Uint8Array,
|
||||
config: EncryptionConfiguration,
|
||||
): Promise<Uint8Array> {
|
||||
switch (config.algorithm) {
|
||||
case supportedAlgorithms.xchacha20poly1305Ietf: {
|
||||
const nonce = Random.getBytes(xchacha20NonceLength);
|
||||
// Prepend fixed-length nonce to ciphertext as suggested in the example from https://github.com/jedisct1/libsodium.js#api
|
||||
return new Uint8Array([
|
||||
...nonce,
|
||||
...(await Xchacha20poly1305Ietf.encrypt(plaintext, encryptionKey, nonce)),
|
||||
]);
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unsupported encryption algorithm: '${config.algorithm}'`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function decrypt(
|
||||
ciphertext: Uint8Array,
|
||||
encryptionKey: Uint8Array,
|
||||
config: EncryptionConfiguration,
|
||||
): Promise<Uint8Array> {
|
||||
switch (config.algorithm) {
|
||||
case supportedAlgorithms.xchacha20poly1305Ietf: {
|
||||
const nonce = ciphertext.slice(0, xchacha20NonceLength);
|
||||
return Xchacha20poly1305Ietf.decrypt(ciphertext.slice(xchacha20NonceLength), encryptionKey, nonce);
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unsupported encryption algorithm: '${config.algorithm}'`);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user