diff --git a/packages/sdk38/src/cosmosclient.searchtx.spec.ts b/packages/sdk38/src/cosmosclient.searchtx.spec.ts index 342db6e7..e86b8534 100644 --- a/packages/sdk38/src/cosmosclient.searchtx.spec.ts +++ b/packages/sdk38/src/cosmosclient.searchtx.spec.ts @@ -6,6 +6,7 @@ import { CosmosClient, isPostTxFailure } from "./cosmosclient"; import { makeSignBytes } from "./encoding"; import { LcdClient } from "./lcdapi"; import { isMsgSend, MsgSend } from "./msgs"; +import { Secp256k1Wallet } from "./secp256k1wallet"; import { SigningCosmosClient } from "./signingcosmosclient"; import { faucet, @@ -16,7 +17,6 @@ import { wasmdEnabled, } from "./testutils.spec"; import { CosmosSdkTx } from "./types"; -import { Secp256k1Wallet } from "./wallet"; interface TestTxSend { readonly sender: string; diff --git a/packages/sdk38/src/cosmosclient.spec.ts b/packages/sdk38/src/cosmosclient.spec.ts index 6087e967..34124d71 100644 --- a/packages/sdk38/src/cosmosclient.spec.ts +++ b/packages/sdk38/src/cosmosclient.spec.ts @@ -6,6 +6,7 @@ import { assertIsPostTxSuccess, CosmosClient, PrivateCosmWasmClient } from "./co import { makeSignBytes } from "./encoding"; import { findAttribute } from "./logs"; import { MsgSend } from "./msgs"; +import { Secp256k1Wallet } from "./secp256k1wallet"; import cosmoshub from "./testdata/cosmoshub.json"; import { faucet, @@ -16,7 +17,6 @@ import { wasmd, } from "./testutils.spec"; import { StdFee } from "./types"; -import { Secp256k1Wallet } from "./wallet"; const blockTime = 1_000; // ms diff --git a/packages/sdk38/src/index.ts b/packages/sdk38/src/index.ts index 4afe6c26..78cbf494 100644 --- a/packages/sdk38/src/index.ts +++ b/packages/sdk38/src/index.ts @@ -85,4 +85,5 @@ export { findSequenceForSignedTx } from "./sequence"; export { encodeSecp256k1Signature, decodeSignature } from "./signature"; export { FeeTable, SigningCosmosClient } from "./signingcosmosclient"; export { isStdTx, pubkeyType, CosmosSdkTx, PubKey, StdFee, StdSignature, StdTx } from "./types"; -export { OfflineSigner, Secp256k1Wallet, makeCosmoshubPath } from "./wallet"; +export { OfflineSigner, makeCosmoshubPath } from "./wallet"; +export { Secp256k1Wallet } from "./secp256k1wallet"; diff --git a/packages/sdk38/src/lcdapi/distribution.spec.ts b/packages/sdk38/src/lcdapi/distribution.spec.ts index af63253f..4d2dd774 100644 --- a/packages/sdk38/src/lcdapi/distribution.spec.ts +++ b/packages/sdk38/src/lcdapi/distribution.spec.ts @@ -6,6 +6,7 @@ import { coin, coins } from "../coins"; import { assertIsPostTxSuccess } from "../cosmosclient"; import { makeSignBytes } from "../encoding"; import { MsgDelegate } from "../msgs"; +import { Secp256k1Wallet } from "../secp256k1wallet"; import { SigningCosmosClient } from "../signingcosmosclient"; import { bigDecimalMatcher, @@ -16,7 +17,6 @@ import { wasmd, wasmdEnabled, } from "../testutils.spec"; -import { Secp256k1Wallet } from "../wallet"; import { DistributionExtension, setupDistributionExtension } from "./distribution"; import { LcdClient } from "./lcdclient"; diff --git a/packages/sdk38/src/lcdapi/gov.spec.ts b/packages/sdk38/src/lcdapi/gov.spec.ts index 3cb35162..17dc66d1 100644 --- a/packages/sdk38/src/lcdapi/gov.spec.ts +++ b/packages/sdk38/src/lcdapi/gov.spec.ts @@ -4,6 +4,7 @@ import { sleep } from "@cosmjs/utils"; import { coins } from "../coins"; import { assertIsPostTxSuccess } from "../cosmosclient"; import { makeSignBytes } from "../encoding"; +import { Secp256k1Wallet } from "../secp256k1wallet"; import { SigningCosmosClient } from "../signingcosmosclient"; import { dateTimeStampMatcher, @@ -13,7 +14,6 @@ import { wasmd, wasmdEnabled, } from "../testutils.spec"; -import { Secp256k1Wallet } from "../wallet"; import { GovExtension, GovParametersType, setupGovExtension } from "./gov"; import { LcdClient } from "./lcdclient"; diff --git a/packages/sdk38/src/lcdapi/lcdclient.spec.ts b/packages/sdk38/src/lcdapi/lcdclient.spec.ts index 2bc4f263..074e842d 100644 --- a/packages/sdk38/src/lcdapi/lcdclient.spec.ts +++ b/packages/sdk38/src/lcdapi/lcdclient.spec.ts @@ -6,6 +6,7 @@ import { isPostTxFailure } from "../cosmosclient"; import { makeSignBytes } from "../encoding"; import { parseLogs } from "../logs"; import { MsgSend } from "../msgs"; +import { Secp256k1Wallet } from "../secp256k1wallet"; import { SigningCosmosClient } from "../signingcosmosclient"; import cosmoshub from "../testdata/cosmoshub.json"; import { @@ -19,7 +20,7 @@ import { wasmdEnabled, } from "../testutils.spec"; import { StdFee } from "../types"; -import { makeCosmoshubPath, Secp256k1Wallet } from "../wallet"; +import { makeCosmoshubPath } from "../wallet"; import { setupAuthExtension } from "./auth"; import { TxsResponse } from "./base"; import { LcdApiArray, LcdClient, normalizeLcdApiArray } from "./lcdclient"; diff --git a/packages/sdk38/src/lcdapi/staking.spec.ts b/packages/sdk38/src/lcdapi/staking.spec.ts index a3c28ace..5575ea6a 100644 --- a/packages/sdk38/src/lcdapi/staking.spec.ts +++ b/packages/sdk38/src/lcdapi/staking.spec.ts @@ -5,6 +5,7 @@ import { coin, coins } from "../coins"; import { assertIsPostTxSuccess } from "../cosmosclient"; import { makeSignBytes } from "../encoding"; import { MsgDelegate, MsgUndelegate } from "../msgs"; +import { Secp256k1Wallet } from "../secp256k1wallet"; import { SigningCosmosClient } from "../signingcosmosclient"; import { bigDecimalMatcher, @@ -16,7 +17,6 @@ import { wasmd, wasmdEnabled, } from "../testutils.spec"; -import { Secp256k1Wallet } from "../wallet"; import { LcdClient } from "./lcdclient"; import { BondStatus, setupStakingExtension, StakingExtension } from "./staking"; diff --git a/packages/sdk38/src/wallet.spec.ts b/packages/sdk38/src/secp256k1wallet.spec.ts similarity index 97% rename from packages/sdk38/src/wallet.spec.ts rename to packages/sdk38/src/secp256k1wallet.spec.ts index ebaafc82..feb4de62 100644 --- a/packages/sdk38/src/wallet.spec.ts +++ b/packages/sdk38/src/secp256k1wallet.spec.ts @@ -1,8 +1,9 @@ import { Secp256k1, Secp256k1Signature, Sha256 } from "@cosmjs/crypto"; import { fromBase64, fromHex, toAscii } from "@cosmjs/encoding"; +import { extractKdfConfiguration, Secp256k1Wallet } from "./secp256k1wallet"; import { base64Matcher, hexMatcher } from "./testutils.spec"; -import { executeKdf, extractKdfConfiguration, KdfConfiguration, Secp256k1Wallet } from "./wallet"; +import { executeKdf, KdfConfiguration } from "./wallet"; describe("Secp256k1Wallet", () => { // m/44'/118'/0'/0/0 diff --git a/packages/sdk38/src/secp256k1wallet.ts b/packages/sdk38/src/secp256k1wallet.ts new file mode 100644 index 00000000..c12382c8 --- /dev/null +++ b/packages/sdk38/src/secp256k1wallet.ts @@ -0,0 +1,332 @@ +import { + Argon2id, + Bip39, + EnglishMnemonic, + pathToString, + Random, + Secp256k1, + Slip10, + Slip10Curve, + Slip10RawIndex, + stringToPath, + xchacha20NonceLength, + Xchacha20poly1305Ietf, +} from "@cosmjs/crypto"; +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"; +import { StdSignature } from "./types"; +import { + AccountData, + Algo, + executeKdf, + KdfConfiguration, + makeCosmoshubPath, + OfflineSigner, + prehash, + PrehashType, +} from "./wallet"; + +const serializationTypeV1 = "secp256k1wallet-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 CosmJS users. + * Must be 16 bytes due to implementation limitations. + */ +const cosmjsSalt = toAscii("The CosmJS salt."); + +/** + * 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 basicPasswordHashingOptions: KdfConfiguration = { + algorithm: "argon2id", + params: { + outputLength: 32, + opsLimit: 20, + memLimitKib: 12 * 1024, + }, +}; + +const algorithmIdXchacha20poly1305Ietf = "xchacha20poly1305-ietf"; + +/** + * This interface describes a JSON object holding the encrypted wallet and the meta data. + * All fields in here must be JSON types. + */ +export interface Secp256k1WalletSerialization { + /** 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: KdfConfiguration; + /** 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; + }; + /** An instance of Secp256k1WalletData, which is stringified, encrypted and base64 encoded. */ + readonly data: string; +} + +/** + * The data of a wallet serialization that is encrypted. + * All fields in here must be JSON types. + */ +export interface Secp256k1WalletData { + readonly mnemonic: string; + readonly accounts: ReadonlyArray<{ + readonly algo: string; + readonly hdPath: string; + readonly prefix: string; + }>; +} + +function extractKdfConfigurationV1(document: any): KdfConfiguration { + return document.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 class Secp256k1Wallet implements OfflineSigner { + /** + * Restores a wallet from the given BIP39 mnemonic. + * + * @param mnemonic Any valid English mnemonic. + * @param hdPath The BIP-32/SLIP-10 derivation path. Defaults to the Cosmos Hub/ATOM path `m/44'/118'/0'/0/0`. + * @param prefix The bech32 address prefix (human readable part). Defaults to "cosmos". + */ + public static async fromMnemonic( + mnemonic: string, + hdPath: readonly Slip10RawIndex[] = makeCosmoshubPath(0), + prefix = "cosmos", + ): Promise { + const mnemonicChecked = new EnglishMnemonic(mnemonic); + 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, + hdPath, + privkey, + Secp256k1.compressPubkey(uncompressed), + prefix, + ); + } + + /** + * Generates a new wallet with a BIP39 mnemonic of the given length. + * + * @param length The number of words in the mnemonic (12, 15, 18, 21 or 24). + * @param hdPath The BIP-32/SLIP-10 derivation path. Defaults to the Cosmos Hub/ATOM path `m/44'/118'/0'/0/0`. + * @param prefix The bech32 address prefix (human readable part). Defaults to "cosmos". + */ + public static async generate( + length: 12 | 15 | 18 | 21 | 24 = 12, + hdPath: readonly Slip10RawIndex[] = makeCosmoshubPath(0), + prefix = "cosmos", + ): Promise { + const entropyLength = 4 * Math.floor((11 * length) / 33); + const entropy = Random.getBytes(entropyLength); + const mnemonic = Bip39.encode(entropy); + return Secp256k1Wallet.fromMnemonic(mnemonic.toString(), hdPath, prefix); + } + + public static async deserialize(serialization: string, password: string): Promise { + 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 Secp256k1Wallet.deserializeType1(serialization, password); + default: + throw new Error("Unsupported serialization type"); + } + } + + public static async deserializeWithEncryptionKey( + serialization: string, + encryptionKey: Uint8Array, + ): Promise { + 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 nonce = fromHex(untypedRoot.encryption.params.nonce); + const decryptedBytes = await Xchacha20poly1305Ietf.decrypt( + fromBase64(untypedRoot.data), + 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 object."); + 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"); + } + } + + private static async deserializeType1(serialization: string, password: string): Promise { + 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: { + let encryptionKey: Uint8Array; + switch (untypedRoot.kdf.algorithm) { + case "argon2id": { + const kdfOptions = untypedRoot.kdf.params; + encryptionKey = await Argon2id.execute(password, cosmjsSalt, kdfOptions); + break; + } + default: + throw new Error("Unsupported KDF algorithm"); + } + return Secp256k1Wallet.deserializeWithEncryptionKey(serialization, encryptionKey); + } + default: + throw new Error("Unsupported serialization type"); + } + } + + /** 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 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; + } + + public get mnemonic(): string { + return this.secret.toString(); + } + + private get address(): string { + return rawSecp256k1PubkeyToAddress(this.pubkey, this.accounts[0].prefix); + } + + public async getAccounts(): Promise { + return [ + { + address: this.address, + algo: this.accounts[0].algo, + pubkey: this.pubkey, + }, + ]; + } + + public async sign( + address: string, + message: Uint8Array, + prehashType: PrehashType = "sha256", + ): Promise { + if (address !== this.address) { + throw new Error(`Address ${address} not found in wallet`); + } + const hashedMessage = prehash(message, prehashType); + const signature = await Secp256k1.createSignature(hashedMessage, this.privkey); + const signatureBytes = new Uint8Array([...signature.r(32), ...signature.s(32)]); + return encodeSecp256k1Signature(this.pubkey, signatureBytes); + } + + /** + * 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 { + 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 of calling `serialize(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 serializeWithEncryptionKey( + encryptionKey: Uint8Array, + kdfConfiguration: KdfConfiguration, + ): Promise { + const encrytedData: Secp256k1WalletData = { + 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: Secp256k1WalletSerialization = { + type: serializationTypeV1, + kdf: kdfConfiguration, + encryption: { + algorithm: algorithmIdXchacha20poly1305Ietf, + params: { + nonce: toHex(nonce), + }, + }, + data: toBase64(encrypted), + }; + return JSON.stringify(out); + } +} diff --git a/packages/sdk38/src/signingcosmosclient.spec.ts b/packages/sdk38/src/signingcosmosclient.spec.ts index 63952110..425458df 100644 --- a/packages/sdk38/src/signingcosmosclient.spec.ts +++ b/packages/sdk38/src/signingcosmosclient.spec.ts @@ -4,9 +4,9 @@ import { assert } from "@cosmjs/utils"; import { Coin, coin, coins } from "./coins"; import { assertIsPostTxSuccess, PrivateCosmWasmClient } from "./cosmosclient"; import { MsgDelegate } from "./msgs"; +import { Secp256k1Wallet } from "./secp256k1wallet"; import { SigningCosmosClient } from "./signingcosmosclient"; import { makeRandomAddress, pendingWithoutWasmd, validatorAddress } from "./testutils.spec"; -import { Secp256k1Wallet } from "./wallet"; const httpUrl = "http://localhost:1317"; diff --git a/packages/sdk38/src/wallet.ts b/packages/sdk38/src/wallet.ts index 6930adff..d7b43262 100644 --- a/packages/sdk38/src/wallet.ts +++ b/packages/sdk38/src/wallet.ts @@ -1,25 +1,7 @@ -import { - Argon2id, - Argon2idOptions, - Bip39, - EnglishMnemonic, - pathToString, - Random, - Secp256k1, - Sha256, - Sha512, - Slip10, - Slip10Curve, - Slip10RawIndex, - stringToPath, - xchacha20NonceLength, - Xchacha20poly1305Ietf, -} from "@cosmjs/crypto"; -import { fromBase64, fromHex, fromUtf8, toAscii, toBase64, toHex, toUtf8 } from "@cosmjs/encoding"; -import { assert, isNonNullObject } from "@cosmjs/utils"; +import { Argon2id, Argon2idOptions, Sha256, Sha512, Slip10RawIndex } from "@cosmjs/crypto"; +import { toAscii } from "@cosmjs/encoding"; +import { assert } from "@cosmjs/utils"; -import { rawSecp256k1PubkeyToAddress } from "./address"; -import { encodeSecp256k1Signature } from "./signature"; import { StdSignature } from "./types"; export type PrehashType = "sha256" | "sha512" | null; @@ -45,7 +27,7 @@ export interface OfflineSigner { readonly sign: (address: string, message: Uint8Array, prehashType?: PrehashType) => Promise; } -function prehash(bytes: Uint8Array, type: PrehashType): Uint8Array { +export function prehash(bytes: Uint8Array, type: PrehashType): Uint8Array { switch (type) { case null: return new Uint8Array([...bytes]); @@ -72,8 +54,6 @@ export function makeCosmoshubPath(a: number): readonly Slip10RawIndex[] { ]; } -const serializationTypeV1 = "secp256k1wallet-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 CosmJS users. @@ -81,72 +61,6 @@ const serializationTypeV1 = "secp256k1wallet-v1"; */ const cosmjsSalt = toAscii("The CosmJS salt."); -/** - * 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 basicPasswordHashingOptions: KdfConfiguration = { - algorithm: "argon2id", - params: { - outputLength: 32, - opsLimit: 20, - memLimitKib: 12 * 1024, - }, -}; - -const algorithmIdXchacha20poly1305Ietf = "xchacha20poly1305-ietf"; - -/** - * This interface describes a JSON object holding the encrypted wallet and the meta data. - * All fields in here must be JSON types. - */ -export interface Secp256k1WalletSerialization { - /** 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: KdfConfiguration; - /** 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; - }; - /** An instance of Secp256k1WalletData, which is stringified, encrypted and base64 encoded. */ - readonly data: string; -} - -/** - * The data of a wallet serialization that is encrypted. - * All fields in here must be JSON types. - */ -export interface Secp256k1WalletData { - readonly mnemonic: string; - readonly accounts: ReadonlyArray<{ - readonly algo: string; - readonly hdPath: string; - readonly prefix: string; - }>; -} - -function extractKdfConfigurationV1(document: any): KdfConfiguration { - return document.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 KdfConfiguration { /** * An algorithm identifier, such as "argon2id" or "scrypt". @@ -170,230 +84,3 @@ export async function executeKdf(password: string, configuration: KdfConfigurati throw new Error("Unsupported KDF algorithm"); } } - -export class Secp256k1Wallet implements OfflineSigner { - /** - * Restores a wallet from the given BIP39 mnemonic. - * - * @param mnemonic Any valid English mnemonic. - * @param hdPath The BIP-32/SLIP-10 derivation path. Defaults to the Cosmos Hub/ATOM path `m/44'/118'/0'/0/0`. - * @param prefix The bech32 address prefix (human readable part). Defaults to "cosmos". - */ - public static async fromMnemonic( - mnemonic: string, - hdPath: readonly Slip10RawIndex[] = makeCosmoshubPath(0), - prefix = "cosmos", - ): Promise { - const mnemonicChecked = new EnglishMnemonic(mnemonic); - 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, - hdPath, - privkey, - Secp256k1.compressPubkey(uncompressed), - prefix, - ); - } - - /** - * Generates a new wallet with a BIP39 mnemonic of the given length. - * - * @param length The number of words in the mnemonic (12, 15, 18, 21 or 24). - * @param hdPath The BIP-32/SLIP-10 derivation path. Defaults to the Cosmos Hub/ATOM path `m/44'/118'/0'/0/0`. - * @param prefix The bech32 address prefix (human readable part). Defaults to "cosmos". - */ - public static async generate( - length: 12 | 15 | 18 | 21 | 24 = 12, - hdPath: readonly Slip10RawIndex[] = makeCosmoshubPath(0), - prefix = "cosmos", - ): Promise { - const entropyLength = 4 * Math.floor((11 * length) / 33); - const entropy = Random.getBytes(entropyLength); - const mnemonic = Bip39.encode(entropy); - return Secp256k1Wallet.fromMnemonic(mnemonic.toString(), hdPath, prefix); - } - - public static async deserialize(serialization: string, password: string): Promise { - 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 Secp256k1Wallet.deserializeType1(serialization, password); - default: - throw new Error("Unsupported serialization type"); - } - } - - public static async deserializeWithEncryptionKey( - serialization: string, - encryptionKey: Uint8Array, - ): Promise { - 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 nonce = fromHex(untypedRoot.encryption.params.nonce); - const decryptedBytes = await Xchacha20poly1305Ietf.decrypt( - fromBase64(untypedRoot.data), - 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 object."); - 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"); - } - } - - private static async deserializeType1(serialization: string, password: string): Promise { - 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: { - let encryptionKey: Uint8Array; - switch (untypedRoot.kdf.algorithm) { - case "argon2id": { - const kdfOptions = untypedRoot.kdf.params; - encryptionKey = await Argon2id.execute(password, cosmjsSalt, kdfOptions); - break; - } - default: - throw new Error("Unsupported KDF algorithm"); - } - return Secp256k1Wallet.deserializeWithEncryptionKey(serialization, encryptionKey); - } - default: - throw new Error("Unsupported serialization type"); - } - } - - /** 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 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; - } - - public get mnemonic(): string { - return this.secret.toString(); - } - - private get address(): string { - return rawSecp256k1PubkeyToAddress(this.pubkey, this.accounts[0].prefix); - } - - public async getAccounts(): Promise { - return [ - { - address: this.address, - algo: this.accounts[0].algo, - pubkey: this.pubkey, - }, - ]; - } - - public async sign( - address: string, - message: Uint8Array, - prehashType: PrehashType = "sha256", - ): Promise { - if (address !== this.address) { - throw new Error(`Address ${address} not found in wallet`); - } - const hashedMessage = prehash(message, prehashType); - const signature = await Secp256k1.createSignature(hashedMessage, this.privkey); - const signatureBytes = new Uint8Array([...signature.r(32), ...signature.s(32)]); - return encodeSecp256k1Signature(this.pubkey, signatureBytes); - } - - /** - * 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 { - 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 of calling `serialize(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 serializeWithEncryptionKey( - encryptionKey: Uint8Array, - kdfConfiguration: KdfConfiguration, - ): Promise { - const encrytedData: Secp256k1WalletData = { - 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: Secp256k1WalletSerialization = { - type: serializationTypeV1, - kdf: kdfConfiguration, - encryption: { - algorithm: algorithmIdXchacha20poly1305Ietf, - params: { - nonce: toHex(nonce), - }, - }, - data: toBase64(encrypted), - }; - return JSON.stringify(out); - } -} diff --git a/packages/sdk38/types/index.d.ts b/packages/sdk38/types/index.d.ts index d84be9f2..5e37414e 100644 --- a/packages/sdk38/types/index.d.ts +++ b/packages/sdk38/types/index.d.ts @@ -83,4 +83,5 @@ export { findSequenceForSignedTx } from "./sequence"; export { encodeSecp256k1Signature, decodeSignature } from "./signature"; export { FeeTable, SigningCosmosClient } from "./signingcosmosclient"; export { isStdTx, pubkeyType, CosmosSdkTx, PubKey, StdFee, StdSignature, StdTx } from "./types"; -export { OfflineSigner, Secp256k1Wallet, makeCosmoshubPath } from "./wallet"; +export { OfflineSigner, makeCosmoshubPath } from "./wallet"; +export { Secp256k1Wallet } from "./secp256k1wallet"; diff --git a/packages/sdk38/types/secp256k1wallet.d.ts b/packages/sdk38/types/secp256k1wallet.d.ts new file mode 100644 index 00000000..e0020657 --- /dev/null +++ b/packages/sdk38/types/secp256k1wallet.d.ts @@ -0,0 +1,98 @@ +import { Slip10RawIndex } from "@cosmjs/crypto"; +import { StdSignature } from "./types"; +import { AccountData, KdfConfiguration, OfflineSigner, PrehashType } from "./wallet"; +/** + * This interface describes a JSON object holding the encrypted wallet and the meta data. + * All fields in here must be JSON types. + */ +export interface Secp256k1WalletSerialization { + /** 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: KdfConfiguration; + /** 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; + }; + /** An instance of Secp256k1WalletData, which is stringified, encrypted and base64 encoded. */ + readonly data: string; +} +/** + * The data of a wallet serialization that is encrypted. + * All fields in here must be JSON types. + */ +export interface Secp256k1WalletData { + readonly mnemonic: string; + readonly accounts: ReadonlyArray<{ + readonly algo: string; + readonly hdPath: string; + readonly prefix: string; + }>; +} +export declare function extractKdfConfiguration(serialization: string): KdfConfiguration; +export declare class Secp256k1Wallet implements OfflineSigner { + /** + * Restores a wallet from the given BIP39 mnemonic. + * + * @param mnemonic Any valid English mnemonic. + * @param hdPath The BIP-32/SLIP-10 derivation path. Defaults to the Cosmos Hub/ATOM path `m/44'/118'/0'/0/0`. + * @param prefix The bech32 address prefix (human readable part). Defaults to "cosmos". + */ + static fromMnemonic( + mnemonic: string, + hdPath?: readonly Slip10RawIndex[], + prefix?: string, + ): Promise; + /** + * Generates a new wallet with a BIP39 mnemonic of the given length. + * + * @param length The number of words in the mnemonic (12, 15, 18, 21 or 24). + * @param hdPath The BIP-32/SLIP-10 derivation path. Defaults to the Cosmos Hub/ATOM path `m/44'/118'/0'/0/0`. + * @param prefix The bech32 address prefix (human readable part). Defaults to "cosmos". + */ + static generate( + length?: 12 | 15 | 18 | 21 | 24, + hdPath?: readonly Slip10RawIndex[], + prefix?: string, + ): Promise; + static deserialize(serialization: string, password: string): Promise; + static deserializeWithEncryptionKey( + serialization: string, + encryptionKey: Uint8Array, + ): Promise; + private static deserializeType1; + /** Base secret */ + private readonly secret; + /** Derivation instrations */ + private readonly accounts; + /** Derived data */ + private readonly pubkey; + private readonly privkey; + private constructor(); + get mnemonic(): string; + private get address(); + getAccounts(): Promise; + sign(address: string, message: Uint8Array, prehashType?: PrehashType): Promise; + /** + * 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). + */ + serialize(password: string): Promise; + /** + * Generates an encrypted serialization of this wallet. + * + * This is an advanced alternative of calling `serialize(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. + */ + serializeWithEncryptionKey(encryptionKey: Uint8Array, kdfConfiguration: KdfConfiguration): Promise; +} diff --git a/packages/sdk38/types/wallet.d.ts b/packages/sdk38/types/wallet.d.ts index 0d377686..19fe5696 100644 --- a/packages/sdk38/types/wallet.d.ts +++ b/packages/sdk38/types/wallet.d.ts @@ -17,45 +17,12 @@ export interface OfflineSigner { */ readonly sign: (address: string, message: Uint8Array, prehashType?: PrehashType) => Promise; } +export declare function prehash(bytes: Uint8Array, type: PrehashType): Uint8Array; /** * The Cosmoshub derivation path in the form `m/44'/118'/0'/0/a` * 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. - * All fields in here must be JSON types. - */ -export interface Secp256k1WalletSerialization { - /** 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: KdfConfiguration; - /** 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; - }; - /** An instance of Secp256k1WalletData, which is stringified, encrypted and base64 encoded. */ - readonly data: string; -} -/** - * The data of a wallet serialization that is encrypted. - * All fields in here must be JSON types. - */ -export interface Secp256k1WalletData { - readonly mnemonic: string; - readonly accounts: ReadonlyArray<{ - readonly algo: string; - readonly hdPath: string; - readonly prefix: string; - }>; -} -export declare function extractKdfConfiguration(serialization: string): KdfConfiguration; export interface KdfConfiguration { /** * An algorithm identifier, such as "argon2id" or "scrypt". @@ -65,64 +32,3 @@ export interface KdfConfiguration { readonly params: Record; } export declare function executeKdf(password: string, configuration: KdfConfiguration): Promise; -export declare class Secp256k1Wallet implements OfflineSigner { - /** - * Restores a wallet from the given BIP39 mnemonic. - * - * @param mnemonic Any valid English mnemonic. - * @param hdPath The BIP-32/SLIP-10 derivation path. Defaults to the Cosmos Hub/ATOM path `m/44'/118'/0'/0/0`. - * @param prefix The bech32 address prefix (human readable part). Defaults to "cosmos". - */ - static fromMnemonic( - mnemonic: string, - hdPath?: readonly Slip10RawIndex[], - prefix?: string, - ): Promise; - /** - * Generates a new wallet with a BIP39 mnemonic of the given length. - * - * @param length The number of words in the mnemonic (12, 15, 18, 21 or 24). - * @param hdPath The BIP-32/SLIP-10 derivation path. Defaults to the Cosmos Hub/ATOM path `m/44'/118'/0'/0/0`. - * @param prefix The bech32 address prefix (human readable part). Defaults to "cosmos". - */ - static generate( - length?: 12 | 15 | 18 | 21 | 24, - hdPath?: readonly Slip10RawIndex[], - prefix?: string, - ): Promise; - static deserialize(serialization: string, password: string): Promise; - static deserializeWithEncryptionKey( - serialization: string, - encryptionKey: Uint8Array, - ): Promise; - private static deserializeType1; - /** Base secret */ - private readonly secret; - /** Derivation instrations */ - private readonly accounts; - /** Derived data */ - private readonly pubkey; - private readonly privkey; - private constructor(); - get mnemonic(): string; - private get address(); - getAccounts(): Promise; - sign(address: string, message: Uint8Array, prehashType?: PrehashType): Promise; - /** - * 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). - */ - serialize(password: string): Promise; - /** - * Generates an encrypted serialization of this wallet. - * - * This is an advanced alternative of calling `serialize(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. - */ - serializeWithEncryptionKey(encryptionKey: Uint8Array, kdfConfiguration: KdfConfiguration): Promise; -}