diff --git a/packages/crypto/src/libsodium.ts b/packages/crypto/src/libsodium.ts index 0776a0c68a..947ce7f91d 100644 --- a/packages/crypto/src/libsodium.ts +++ b/packages/crypto/src/libsodium.ts @@ -33,6 +33,9 @@ export function isArgon2idOptions(thing: unknown): thing is Argon2idOptions { return true; } +// Equal to sodium.crypto_pwhash_SALTBYTES (16) +export const sodiumSaltBytes = 16; + export class Argon2id { public static async execute( password: string, diff --git a/packages/proto-signing/package.json b/packages/proto-signing/package.json index 477191ac0d..c3c1faf40a 100644 --- a/packages/proto-signing/package.json +++ b/packages/proto-signing/package.json @@ -84,6 +84,7 @@ "typedoc": "^0.21", "typescript": "~4.3", "webpack": "^5.32.0", - "webpack-cli": "^4.6.0" + "webpack-cli": "^4.6.0", + "proto-signing-v0-26-2": "npm:@cosmjs/proto-signing@0.26.2" } } diff --git a/packages/proto-signing/src/directsecp256k1hdwallet.spec.ts b/packages/proto-signing/src/directsecp256k1hdwallet.spec.ts index 0b9122f33b..c5657cbb82 100644 --- a/packages/proto-signing/src/directsecp256k1hdwallet.spec.ts +++ b/packages/proto-signing/src/directsecp256k1hdwallet.spec.ts @@ -3,6 +3,7 @@ import { Secp256k1, Secp256k1Signature, sha256 } from "@cosmjs/crypto"; import { fromBase64, fromHex } from "@cosmjs/encoding"; import { DirectSecp256k1HdWallet, extractKdfConfiguration } from "./directsecp256k1hdwallet"; +import { DirectSecp256k1HdWallet as DirectSecp256k1HdWallet_v0_26_2 } from "proto-signing-v0-26-2" import { makeAuthInfoBytes, makeSignBytes, makeSignDoc } from "./signing"; import { base64Matcher, faucet, testVectors } from "./testutils.spec"; import { executeKdf, KdfConfiguration } from "./wallet"; @@ -124,6 +125,23 @@ describe("DirectSecp256k1HdWallet", () => { }, ]); }); + + it("can restore: backward compatibility test", async () => { + const original = await DirectSecp256k1HdWallet_v0_26_2.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, + }, + ]); + }); }); describe("deserializeWithEncryptionKey", () => { @@ -299,6 +317,15 @@ describe("DirectSecp256k1HdWallet", () => { data: jasmine.stringMatching(base64Matcher), }); }); + + it("Store a salt next to the serialization options", async () => { + const original = await DirectSecp256k1HdWallet.fromMnemonic(defaultMnemonic); + const password = "123"; + const serialized = await original.serialize(password); + const parsed = JSON.parse(serialized); + + expect(Object.keys(parsed.kdf.params)).toContain('salt'); + }); }); describe("serializeWithEncryptionKey", () => { diff --git a/packages/proto-signing/src/wallet.ts b/packages/proto-signing/src/wallet.ts index dc85068a1d..d5dc9ea603 100644 --- a/packages/proto-signing/src/wallet.ts +++ b/packages/proto-signing/src/wallet.ts @@ -4,16 +4,10 @@ import { Random, xchacha20NonceLength, Xchacha20poly1305Ietf, + sodiumSaltBytes, } 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". @@ -28,7 +22,8 @@ export async function executeKdf(password: string, configuration: KdfConfigurati case "argon2id": { const options = configuration.params; if (!isArgon2idOptions(options)) throw new Error("Invalid format of argon2id params"); - return Argon2id.execute(password, cosmjsSalt, options); + const salt: Uint8Array = Random.getBytes(sodiumSaltBytes); + return Argon2id.execute(password, salt, options); } default: throw new Error("Unsupported KDF algorithm"); @@ -59,7 +54,7 @@ export async function encrypt( ): Promise { switch (config.algorithm) { case supportedAlgorithms.xchacha20poly1305Ietf: { - const nonce = Random.getBytes(xchacha20NonceLength); + const nonce: Uint8Array = 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,