diff --git a/packages/hdwallet-native-vault/src/index.ts b/packages/hdwallet-native-vault/src/index.ts index 317ff0ba6..364b7ec35 100644 --- a/packages/hdwallet-native-vault/src/index.ts +++ b/packages/hdwallet-native-vault/src/index.ts @@ -1,9 +1,11 @@ +import { registerKeystoreTransformers } from "./keystore"; import { createMnemonic, crypto, entropyToMnemonic, GENERATE_MNEMONIC } from "./util"; import { Vault } from "./vault"; export type { ISealableVaultFactory, IVault, IVaultFactory } from "./types"; export { GENERATE_MNEMONIC } from "./util"; export { Vault } from "./vault"; +export { type XChainKeystore, decryptFromKeystore } from "./keystore"; Vault.registerValueTransformer("#mnemonic", async (x: unknown) => { if (x !== GENERATE_MNEMONIC) return x; @@ -16,4 +18,6 @@ Vault.registerValueWrapper("#mnemonic", async (x: unknown, addRevoker: (revoke: addRevoker(() => out.revoke?.()); return out; }); + +registerKeystoreTransformers(); Vault.extensionRegistrationComplete(); diff --git a/packages/hdwallet-native-vault/src/keystore.ts b/packages/hdwallet-native-vault/src/keystore.ts new file mode 100644 index 000000000..857200851 --- /dev/null +++ b/packages/hdwallet-native-vault/src/keystore.ts @@ -0,0 +1,115 @@ +import { blake2bFinal, blake2bInit, blake2bUpdate } from "blakejs"; + +import { crypto, encoder } from "./util"; +import { Vault } from "./vault"; + +// https://github.com/thorswap/SwapKit/blob/349a9212d8357cc35a8bab771728bbc8d6900ebc/packages/wallets/keystore/src/helpers.ts#L6 +export interface XChainKeystore { + crypto: { + cipher: string; + ciphertext: string; + cipherparams: { + iv: string; + }; + kdf: string; + kdfparams: { + prf: string; + dklen: number; + salt: string; + c: number; + }; + mac: string; + }; + version: number; + meta: string; +} + +// https://github.com/thorswap/SwapKit/blob/349a9212d8357cc35a8bab771728bbc8d6900ebc/packages/wallets/keystore/src/helpers.ts#L29-L42 +function blake256(data: Uint8Array): string { + const context = blake2bInit(32); + blake2bUpdate(context, data); + return Buffer.from(blake2bFinal(context)).toString("hex"); +} + +// https://github.com/thorswap/SwapKit/blob/349a9212d8357cc35a8bab771728bbc8d6900ebc/packages/wallets/keystore/src/helpers.ts#L102 +export async function decryptFromKeystore(keystore: XChainKeystore, password: string): Promise { + if (keystore.version !== 1 || keystore.meta !== "xchain-keystore") { + throw new Error("Invalid keystore format"); + } + + const { kdfparams } = keystore.crypto; + + // Derive key using PBKDF2 similar to SwapKit's `pbkdf2Async` call + const passwordKey = await ( + await crypto + ).subtle.importKey("raw", encoder.encode(password), "PBKDF2", false, ["deriveBits"]); + + const derivedKey = new Uint8Array( + await ( + await crypto + ).subtle.deriveBits( + { + name: "PBKDF2", + salt: Buffer.from(kdfparams.salt, "hex"), + iterations: kdfparams.c, + hash: "SHA-256", + }, + passwordKey, + kdfparams.dklen * 8 + ) + ); + + const ciphertext = Buffer.from(keystore.crypto.ciphertext, "hex"); + const mac = blake256(Buffer.concat([Buffer.from(derivedKey.subarray(16, 32)), ciphertext])); + + if (mac !== keystore.crypto.mac) { + throw new Error("Invalid password"); + } + + const aesKey = await ( + await crypto + ).subtle.importKey( + "raw", + derivedKey.subarray(0, 16), + { + name: "AES-CTR", + length: 128, + }, + false, + ["decrypt"] + ); + + const iv = Buffer.from(keystore.crypto.cipherparams.iv, "hex"); + const counter = new Uint8Array(16); + counter.set(iv); + + const decrypted = await ( + await crypto + ).subtle.decrypt( + { + name: "AES-CTR", + counter, + length: 128, + }, + aesKey, + ciphertext + ); + + return new TextDecoder().decode(decrypted); +} + +export const registerKeystoreTransformers = () => { + Vault.registerValueTransformer("#keystore", async (value: unknown) => { + if (!value || typeof value !== "string") return value; + + try { + const keystore = JSON.parse(value) as XChainKeystore; + if (keystore.version !== 1 || keystore.meta !== "xchain-keystore") { + throw new Error("Invalid keystore format"); + } + return keystore; + } catch { + return value; + } + }); +}; diff --git a/packages/hdwallet-native-vault/src/vault.ts b/packages/hdwallet-native-vault/src/vault.ts index 443f4b169..fdad4a12d 100644 --- a/packages/hdwallet-native-vault/src/vault.ts +++ b/packages/hdwallet-native-vault/src/vault.ts @@ -2,6 +2,7 @@ import * as core from "@shapeshiftoss/hdwallet-core"; import * as jose from "jose"; import * as ta from "type-assertions"; +import { decryptFromKeystore } from "./keystore"; import { MapVault } from "./mapVault"; import { RawVault } from "./rawVault"; import { ISealableVaultFactory, IVault, VaultPrepareParams } from "./types"; @@ -179,6 +180,13 @@ export class Vault extends MapVault implements IVault { return this; } + async loadFromKeystore(stringifiedKeystore: string, password: string) { + const keystore = JSON.parse(stringifiedKeystore); + const mnemonic = await decryptFromKeystore(keystore, password); + this.set("#mnemonic", mnemonic); + return this; + } + async save() { const unwrappedRevoker = new (Revocable(class {}))(); const unwrapped = this.#unwrap((x) => unwrappedRevoker.addRevoker(x));