Skip to content
Draft
2 changes: 1 addition & 1 deletion modules/abstract-eth/src/abstractEthLikeNewCoins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2554,7 +2554,7 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin {
const walletPassphrase = buildParams.walletPassphrase;

const userKeychain = await this.keychains().get({ id: wallet.keyIds()[0] });
const userPrv = wallet.getUserPrv({ keychain: userKeychain, walletPassphrase });
const userPrv = await wallet.getUserPrv({ keychain: userKeychain, walletPassphrase });
const userPrvBuffer = bip32.fromBase58(userPrv).privateKey;
if (!userPrvBuffer) {
throw new Error('invalid userPrv');
Expand Down
2 changes: 1 addition & 1 deletion modules/abstract-utxo/src/abstractUtxoCoin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -677,7 +677,7 @@ export abstract class AbstractUtxoCoin
/**
* @deprecated - use function verifyUserPublicKey instead
*/
protected verifyUserPublicKey(params: VerifyUserPublicKeyOptions): boolean {
protected async verifyUserPublicKey(params: VerifyUserPublicKeyOptions): Promise<boolean> {
return verifyUserPublicKey(this.bitgo, params);
}

Expand Down
2 changes: 1 addition & 1 deletion modules/abstract-utxo/src/impl/btc/inscriptionBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,7 @@ export class InscriptionBuilder implements IInscriptionBuilder {
txPrebuild: PrebuildTransactionResult
): Promise<SubmitTransactionResponse> {
const userKeychain = await this.wallet.baseCoin.keychains().get({ id: this.wallet.keyIds()[KeyIndices.USER] });
const prv = this.wallet.getUserPrv({ keychain: userKeychain, walletPassphrase });
const prv = await this.wallet.getUserPrv({ keychain: userKeychain, walletPassphrase });

const halfSigned = (await this.wallet.signTransaction({ prv, txPrebuild })) as HalfSignedUtxoTransaction;
return this.wallet.submitTransaction({ halfSigned });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,11 @@ export async function verifyTransaction<TNumber extends bigint | number>(
let userPublicKeyVerified = false;
try {
// verify the user public key matches the private key - this will throw if there is no match
userPublicKeyVerified = verifyUserPublicKey(bitgo, { userKeychain: keychains.user, disableNetworking, txParams });
userPublicKeyVerified = await verifyUserPublicKey(bitgo, {
userKeychain: keychains.user,
disableNetworking,
txParams,
});
} catch (e) {
debug('failed to verify user public key!', e);
}
Expand Down
4 changes: 2 additions & 2 deletions modules/abstract-utxo/src/verifyKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export function verifyCustomChangeKeySignatures<TNumber extends number | bigint>
/**
* Decrypt the wallet's user private key and verify that the claimed public key matches
*/
export function verifyUserPublicKey(bitgo: BitGoBase, params: VerifyUserPublicKeyOptions): boolean {
export async function verifyUserPublicKey(bitgo: BitGoBase, params: VerifyUserPublicKeyOptions): Promise<boolean> {
const { userKeychain, txParams, disableNetworking } = params;
if (!userKeychain) {
throw new Error('user keychain is required');
Expand All @@ -94,7 +94,7 @@ export function verifyUserPublicKey(bitgo: BitGoBase, params: VerifyUserPublicKe

let userPrv = userKeychain.prv;
if (!userPrv && txParams.walletPassphrase) {
userPrv = decryptKeychainPrivateKey(bitgo, userKeychain, txParams.walletPassphrase);
userPrv = await decryptKeychainPrivateKey(bitgo, userKeychain, txParams.walletPassphrase);
}

if (!userPrv) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,55 @@ describe('TSS Ecdsa MPCv2 Utils:', async function () {
assert.equal(bitgoKeychain.source, 'bitgo');
});

it('should generate TSS MPCv2 keys with v2 encryption envelopes', async function () {
const bitgoSession = new DklsDkg.Dkg(3, 2, 2);

const round1Nock = await nockKeyGenRound1(bitgoSession, 1);
const round2Nock = await nockKeyGenRound2(bitgoSession, 1);
const round3Nock = await nockKeyGenRound3(bitgoSession, 1);
const addKeyNock = await nockAddKeyChain(coinName, 3);
const params = {
passphrase: 'test',
enterprise: enterpriseId,
originalPasscodeEncryptionCode: '123456',
encryptionVersion: 2 as const,
};
const { userKeychain, backupKeychain, bitgoKeychain } = await tssUtils.createKeychains(params);
assert.ok(round1Nock.isDone());
assert.ok(round2Nock.isDone());
assert.ok(round3Nock.isDone());
assert.ok(addKeyNock.isDone());

assert.ok(userKeychain);
assert.equal(userKeychain.source, 'user');
assert.ok(userKeychain.commonKeychain);
assert.ok(ECDSAUtils.EcdsaMPCv2Utils.validateCommonKeychainPublicKey(userKeychain.commonKeychain));

// Verify v2 envelopes for encryptedPrv
assert.ok(userKeychain.encryptedPrv);
const encryptedPrvParsed: { v: number } = JSON.parse(userKeychain.encryptedPrv);
assert.equal(encryptedPrvParsed.v, 2, 'encryptedPrv should be a v2 envelope');

// Verify v2 envelopes for reducedEncryptedPrv
assert.ok(userKeychain.reducedEncryptedPrv);
const reducedEncryptedPrvParsed: { v: number } = JSON.parse(userKeychain.reducedEncryptedPrv);
assert.equal(reducedEncryptedPrvParsed.v, 2, 'reducedEncryptedPrv should be a v2 envelope');

// Verify v2 envelope is decryptable via decryptAsync
const decrypted = await bitgo.decryptAsync({ input: userKeychain.encryptedPrv, password: params.passphrase });
assert.ok(decrypted, 'decryptAsync should successfully decrypt v2 envelope');

// Verify backup keychain also uses v2 envelopes
assert.ok(backupKeychain);
assert.equal(backupKeychain.source, 'backup');
assert.ok(backupKeychain.encryptedPrv);
const backupEncryptedPrvParsed: { v: number } = JSON.parse(backupKeychain.encryptedPrv);
assert.equal(backupEncryptedPrvParsed.v, 2, 'backup encryptedPrv should be a v2 envelope');

assert.ok(bitgoKeychain);
assert.equal(bitgoKeychain.source, 'bitgo');
});

it('should generate TSS MPCv2 keys for retrofit', async function () {
const xiList = [
Array.from(bigIntToBufferBE(BigInt(1), 32)),
Expand Down
43 changes: 43 additions & 0 deletions modules/bitgo/test/v2/unit/internal/tssUtils/eddsa.ts
Original file line number Diff line number Diff line change
Expand Up @@ -546,6 +546,49 @@ describe('TSS Utils:', async function () {
})
.should.be.rejectedWith('Failed to create backup keychain - commonKeychains do not match.');
});

it('should generate TSS key chains with v2 encryption envelopes', async function () {
const passphrase = 'passphrase';
const userKeyShare = MPC.keyShare(1, 2, 3);
const backupKeyShare = MPC.keyShare(2, 2, 3);

await nockBitgoKeychain({
coin: coinName,
userKeyShare,
backupKeyShare,
bitgoKeyShare,
userGpgKey,
backupGpgKey,
bitgoGpgKey,
});
await nockUserKeychain({ coin: coinName });
await nockBackupKeychain({ coin: coinName });

const bitgoKeychain = await tssUtils.createBitgoKeychain({
userGpgKey,
backupGpgKey,
userKeyShare,
backupKeyShare,
});
const userKeychain = await tssUtils.createUserKeychain({
userGpgKey,
backupGpgKey,
userKeyShare,
backupKeyShare,
bitgoKeychain,
passphrase,
encryptionVersion: 2,
});

should.exist(userKeychain.encryptedPrv);
const envelope = JSON.parse(userKeychain.encryptedPrv!);
envelope.v.should.equal(2);

const decrypted = await bitgo.decryptAsync({ input: userKeychain.encryptedPrv!, password: passphrase });
should.exist(decrypted);
const parsed: Record<string, unknown> = JSON.parse(decrypted);
should.exist(parsed.uShare);
});
});

describe('signTxRequest:', function () {
Expand Down
8 changes: 4 additions & 4 deletions modules/bitgo/test/v2/unit/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,7 @@ describe('V2 Wallet:', function () {
prv,
coldDerivationSeed: '123',
};
wallet.getUserPrv(userPrvOptions).should.eql(derivedPrv);
(await wallet.getUserPrv(userPrvOptions)).should.eql(derivedPrv);
});

it('should use the user keychain derivedFromParentWithSeed as the cold derivation seed if none is provided', async () => {
Expand All @@ -365,7 +365,7 @@ describe('V2 Wallet:', function () {
type: 'independent',
},
};
wallet.getUserPrv(userPrvOptions).should.eql(derivedPrv);
(await wallet.getUserPrv(userPrvOptions)).should.eql(derivedPrv);
});

it('should prefer the explicit cold derivation seed to the user keychain derivedFromParentWithSeed', async () => {
Expand All @@ -379,7 +379,7 @@ describe('V2 Wallet:', function () {
type: 'independent',
},
};
wallet.getUserPrv(userPrvOptions).should.eql(derivedPrv);
(await wallet.getUserPrv(userPrvOptions)).should.eql(derivedPrv);
});

it('should return the prv provided for TSS SMC', async () => {
Expand Down Expand Up @@ -407,7 +407,7 @@ describe('V2 Wallet:', function () {
prv,
keychain,
};
wallet.getUserPrv(userPrvOptions).should.eql(prv);
(await wallet.getUserPrv(userPrvOptions)).should.eql(prv);
});
});

Expand Down
12 changes: 6 additions & 6 deletions modules/bitgo/test/v2/unit/wallets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2440,7 +2440,7 @@ describe('V2 Wallets:', function () {
const keychainTest: OptionalKeychainEncryptedKey = {
encryptedPrv: bitgo.encrypt({ input: fromUserPrv.toString(), password: walletPassphrase }),
};
const userPrv = decryptKeychainPrivateKey(bitgo, keychainTest, walletPassphrase);
const userPrv = await decryptKeychainPrivateKey(bitgo, keychainTest, walletPassphrase);
if (!userPrv) {
throw new Error('Unable to decrypt user keychain');
}
Expand Down Expand Up @@ -2512,7 +2512,7 @@ describe('V2 Wallets:', function () {
const keychainTest: OptionalKeychainEncryptedKey = {
encryptedPrv: bitgo.encrypt({ input: fromUserPrv.toString(), password: walletPassphrase }),
};
const userPrv = decryptKeychainPrivateKey(bitgo, keychainTest, walletPassphrase);
const userPrv = await decryptKeychainPrivateKey(bitgo, keychainTest, walletPassphrase);
if (!userPrv) {
throw new Error('Unable to decrypt user keychain');
}
Expand Down Expand Up @@ -2591,7 +2591,7 @@ describe('V2 Wallets:', function () {
const keychainTest: OptionalKeychainEncryptedKey = {
encryptedPrv: bitgo.encrypt({ input: fromUserPrv.toString(), password: walletPassphrase }),
};
const userPrv = decryptKeychainPrivateKey(bitgo, keychainTest, walletPassphrase);
const userPrv = await decryptKeychainPrivateKey(bitgo, keychainTest, walletPassphrase);
if (!userPrv) {
throw new Error('Unable to decrypt user keychain');
}
Expand Down Expand Up @@ -2659,7 +2659,7 @@ describe('V2 Wallets:', function () {
const keychainTest: OptionalKeychainEncryptedKey = {
encryptedPrv: bitgo.encrypt({ input: fromUserPrv.toString(), password: walletPassphrase }),
};
const userPrv = decryptKeychainPrivateKey(bitgo, keychainTest, walletPassphrase);
const userPrv = await decryptKeychainPrivateKey(bitgo, keychainTest, walletPassphrase);
if (!userPrv) {
throw new Error('Unable to decrypt user keychain');
}
Expand Down Expand Up @@ -2760,7 +2760,7 @@ describe('V2 Wallets:', function () {
const keychainTest: OptionalKeychainEncryptedKey = {
encryptedPrv: bitgo.encrypt({ input: fromUserPrv.toString(), password: walletPassphrase }),
};
const userPrv = decryptKeychainPrivateKey(bitgo, keychainTest, walletPassphrase);
const userPrv = await decryptKeychainPrivateKey(bitgo, keychainTest, walletPassphrase);
if (!userPrv) {
throw new Error('Unable to decrypt user keychain');
}
Expand Down Expand Up @@ -2862,7 +2862,7 @@ describe('V2 Wallets:', function () {
const keychainTest: OptionalKeychainEncryptedKey = {
encryptedPrv: bitgo.encrypt({ input: fromUserPrv.toString(), password: walletPassphrase }),
};
const userPrv = decryptKeychainPrivateKey(bitgo, keychainTest, walletPassphrase);
const userPrv = await decryptKeychainPrivateKey(bitgo, keychainTest, walletPassphrase);
if (!userPrv) {
throw new Error('Unable to decrypt user keychain');
}
Expand Down
25 changes: 25 additions & 0 deletions modules/sdk-api/src/bitgoAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ import {
verifyResponseAsync,
} from './api';
import { decrypt, decryptAsync, encrypt } from './encrypt';
import { createEncryptionSession } from './encryptionSession';
import { encryptV2 } from './encryptV2';
import { verifyAddress } from './v1/verifyAddress';
import {
AccessTokenOptions,
Expand Down Expand Up @@ -715,6 +717,29 @@ export class BitGoAPI implements BitGoBase {
return encrypt(params.password, params.input, { adata: params.adata });
}

/**
* Async encrypt that dispatches to v1 (SJCL) or v2 (Argon2id + AES-256-GCM)
* based on `encryptionVersion`.
*/
async encryptAsync(params: EncryptOptions): Promise<string> {
common.validateParams(params, ['input', 'password'], []);
if (!params.password) {
throw new Error('cannot encrypt without password');
}
if (params.encryptionVersion === 2) {
return encryptV2(params.password, params.input);
}
return encrypt(params.password, params.input, { adata: params.adata });
}

/**
* Create an encryption session for multi-call operations.
* Runs Argon2id once; all subsequent calls derive keys via HKDF.
*/
async createEncryptionSession(password: string) {
return createEncryptionSession(password);
}

/**
* Decrypt an encrypted string locally.
*/
Expand Down
8 changes: 6 additions & 2 deletions modules/sdk-api/src/encryptV2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,12 +101,16 @@ export function hkdfDeriveAesKey(hkdfKey: CryptoKey, hkdfSalt: Uint8Array, usage
}

export async function aesGcmEncrypt(key: CryptoKey, iv: Uint8Array, plaintext: string): Promise<Uint8Array> {
const ct = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, new TextEncoder().encode(plaintext));
const ct = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv, tagLength: 128 },
key,
new TextEncoder().encode(plaintext)
);
return new Uint8Array(ct);
}

export async function aesGcmDecrypt(key: CryptoKey, iv: Uint8Array, ct: Uint8Array): Promise<string> {
const plaintext = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, key, ct);
const plaintext = await crypto.subtle.decrypt({ name: 'AES-GCM', iv, tagLength: 128 }, key, ct);
return new TextDecoder().decode(plaintext);
}

Expand Down
16 changes: 12 additions & 4 deletions modules/sdk-api/src/encryptionSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {
*/
export class EncryptionSession {
private hkdfKey: CryptoKey | null;
private readonly argon2SaltB64: string;
private argon2SaltB64: string | null;
private readonly memorySize: number;
private readonly iterations: number;
private readonly parallelism: number;
Expand Down Expand Up @@ -53,7 +53,7 @@ export class EncryptionSession {
if (!envelope.hkdfSalt) {
throw new Error('envelope was not encrypted with a session; use decryptV2 instead');
}
if (envelope.salt !== this.argon2SaltB64) {
if (envelope.salt !== this.getSaltOrThrow()) {
throw new Error('envelope was not encrypted with this session');
}
const iv = new Uint8Array(Buffer.from(envelope.iv, 'base64'));
Expand All @@ -65,22 +65,30 @@ export class EncryptionSession {

destroy(): void {
this.hkdfKey = null;
this.argon2SaltB64 = null;
}

private getKeyOrThrow(): CryptoKey {
if (this.hkdfKey === null) {
if (this.hkdfKey === null || this.argon2SaltB64 === null) {
throw new Error('EncryptionSession has been destroyed');
}
return this.hkdfKey;
}

private getSaltOrThrow(): string {
if (this.argon2SaltB64 === null) {
throw new Error('EncryptionSession has been destroyed');
}
return this.argon2SaltB64;
}

private buildEnvelope(hkdfSalt: Uint8Array, iv: Uint8Array, ct: Uint8Array): V2Envelope {
return {
v: 2,
m: this.memorySize,
t: this.iterations,
p: this.parallelism,
salt: this.argon2SaltB64,
salt: this.getSaltOrThrow(),
hkdfSalt: Buffer.from(hkdfSalt).toString('base64'),
iv: Buffer.from(iv).toString('base64'),
ct: Buffer.from(ct).toString('base64'),
Expand Down
Loading