Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.getUserPrvAsync({ keychain: userKeychain, walletPassphrase });
const userPrvBuffer = bip32.fromBase58(userPrv).privateKey;
if (!userPrvBuffer) {
throw new Error('invalid userPrv');
Expand Down
4 changes: 2 additions & 2 deletions modules/abstract-utxo/src/impl/btc/inscriptionBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ export class InscriptionBuilder implements IInscriptionBuilder {
inscriptionData: Buffer
): Promise<SubmitTransactionResponse> {
const userKeychain = await this.wallet.baseCoin.keychains().get({ id: this.wallet.keyIds()[KeyIndices.USER] });
const xprv = await this.wallet.getUserPrv({ keychain: userKeychain, walletPassphrase });
const xprv = await this.wallet.getUserPrvAsync({ keychain: userKeychain, walletPassphrase });

const halfSignedCommitTransaction = (await this.wallet.signTransaction({
prv: xprv,
Expand Down 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.getUserPrvAsync({ 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 = verifyUserPublicKey(bitgo, {
userKeychain: keychains.user,
disableNetworking,
txParams,
});
} catch (e) {
debug('failed to verify user public key!', e);
}
Expand Down
56 changes: 55 additions & 1 deletion modules/bitgo/test/unit/decryptKeychain.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import 'should';
import { decryptKeychainPrivateKey, OptionalKeychainEncryptedKey } from '@bitgo/sdk-core';
import {
decryptKeychainPrivateKey,
decryptKeychainPrivateKeyAsync,
OptionalKeychainEncryptedKey,
} from '@bitgo/sdk-core';
import { BitGoAPI } from '@bitgo/sdk-api';

describe('decryptKeychainPrivateKey', () => {
Expand Down Expand Up @@ -78,3 +82,53 @@ describe('decryptKeychainPrivateKey', () => {
(decryptKeychainPrivateKey(bitgo, {}, 'password') === undefined).should.be.true();
});
});

describe('decryptKeychainPrivateKeyAsync', () => {
const bitgo = new BitGoAPI();

const prv1 = Math.random().toString();
const password1 = Math.random().toString();

const prv2 = Math.random().toString();
const password2 = Math.random().toString();

it('should decrypt encryptedPrv (v1)', async () => {
const keychain: OptionalKeychainEncryptedKey = {
encryptedPrv: bitgo.encrypt({ input: prv1, password: password1 }),
};
const result = await decryptKeychainPrivateKeyAsync(bitgo, keychain, password1);
result!.should.equal(prv1);
});

it('should decrypt webauthnDevices encryptedPrv (v1)', async () => {
const keychain: OptionalKeychainEncryptedKey = {
webauthnDevices: [
{
otpDeviceId: '123',
authenticatorInfo: {
credID: 'credID',
fmt: 'packed',
publicKey: 'some value',
},
prfSalt: '456',
encryptedPrv: bitgo.encrypt({ input: prv2, password: password2 }),
},
],
};
const result = await decryptKeychainPrivateKeyAsync(bitgo, keychain, password2);
result!.should.equal(prv2);
});

it('should return undefined if no encryptedPrv can be decrypted', async () => {
const keychain: OptionalKeychainEncryptedKey = {
encryptedPrv: bitgo.encrypt({ input: prv1, password: password1 }),
};
const result = await decryptKeychainPrivateKeyAsync(bitgo, keychain, Math.random().toString());
(result === undefined).should.equal(true);
});

it('should return undefined if no encryptedPrv is present', async () => {
const result = await decryptKeychainPrivateKeyAsync(bitgo, {}, 'password');
(result === undefined).should.be.true();
});
});
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
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,128 @@ describe('signTxRequest:', function () {
nockPromises[2].isDone().should.be.true();
});

describe('v2 encryption (offline rounds with adata)', function () {
it('e2e: 3-round offline signing with v2 encrypted keys preserves adata context binding', async function () {
const walletPassphrase = 'testpassphrase';
const userShare = fs.readFileSync(shareFiles[vector.party1]);
const userPrvBase64 = Buffer.from(userShare).toString('base64');

const encryptedPrv = await bitgo.encryptAsync({
input: userPrvBase64,
password: walletPassphrase,
encryptionVersion: 2,
});
JSON.parse(encryptedPrv).v.should.equal(2);

const round1Result = await tssUtils.createOfflineRound1Share({
txRequest,
prv: userPrvBase64,
walletPassphrase,
encryptedPrv,
});

const r1SessionEnvelope = JSON.parse(round1Result.encryptedRound1Session);
r1SessionEnvelope.v.should.equal(2);
r1SessionEnvelope.should.have.property('adata');
r1SessionEnvelope.should.have.property('hkdfSalt');
r1SessionEnvelope.adata.should.containEql('DKLS23_SIGNING_ROUND1_STATE');

const r1GpgEnvelope = JSON.parse(round1Result.encryptedUserGpgPrvKey);
r1GpgEnvelope.v.should.equal(2);
r1GpgEnvelope.should.have.property('adata');
r1GpgEnvelope.adata.should.containEql('DKLS23_SIGNING_USER_GPG_KEY');

await nockTxRequestResponseSignatureShareRoundOne(bitgoParty, txRequest, bitgoGpgKey);
const transactions = getRoute('ecdsa');
const round1TxRequestResponse = await bitgo
.post(bitgo.url(`/wallet/${txRequest.walletId}/txrequests/${txRequest.txRequestId + transactions}/sign`, 2))
.send({
signatureShares: [round1Result.signatureShareRound1],
signerGpgPublicKey: round1Result.userGpgPubKey,
})
.result();

const round1TxReq: TxRequest = {
...txRequest,
transactions: [
{
...txRequest.transactions![0],
signatureShares: round1TxRequestResponse.transactions[0].signatureShares,
},
],
};

const round2Result = await tssUtils.createOfflineRound2Share({
txRequest: round1TxReq,
prv: userPrvBase64,
walletPassphrase,
bitgoPublicGpgKey: bitgoGpgKey.publicKey,
encryptedUserGpgPrvKey: round1Result.encryptedUserGpgPrvKey,
encryptedRound1Session: round1Result.encryptedRound1Session,
});

const r2Envelope = JSON.parse(round2Result.encryptedRound2Session);
r2Envelope.v.should.equal(2);
r2Envelope.should.have.property('adata');
r2Envelope.adata.should.containEql('DKLS23_SIGNING_ROUND2_STATE');

await nockTxRequestResponseSignatureShareRoundTwo(bitgoParty, txRequest, bitgoGpgKey);
const round2TxRequestResponse = await bitgo
.post(bitgo.url(`/wallet/${txRequest.walletId}/txrequests/${txRequest.txRequestId + transactions}/sign`, 2))
.send({
signatureShares: [round2Result.signatureShareRound2],
signerGpgPublicKey: round1Result.userGpgPubKey,
})
.result();

const round2TxReq: TxRequest = {
...txRequest,
transactions: [
{
...txRequest.transactions![0],
signatureShares: round2TxRequestResponse.transactions[0].signatureShares,
},
],
};

const round3Result = await tssUtils.createOfflineRound3Share({
txRequest: round2TxReq,
prv: userPrvBase64,
walletPassphrase,
bitgoPublicGpgKey: bitgoGpgKey.publicKey,
encryptedUserGpgPrvKey: round1Result.encryptedUserGpgPrvKey,
encryptedRound2Session: round2Result.encryptedRound2Session,
});

round3Result.should.have.property('signatureShareRound3');
});

it('validateAdata accepts v2 envelopes with matching adata and domain separator', async function () {
const adata = 'txhash:m/0/1';
const domainSep = 'DKLS23_SIGNING_ROUND1_STATE';
const ct = await bitgo.encryptAsync({
input: 'test-data',
password: 'testpass',
encryptionVersion: 2,
adata: `${domainSep}:${adata}`,
});

(tssUtils as any).validateAdata(adata, ct, domainSep);
});

it('validateAdata rejects v2 envelopes with mismatched adata', async function () {
const domainSep = 'DKLS23_SIGNING_ROUND1_STATE';
const ct = await bitgo.encryptAsync({
input: 'test-data',
password: 'testpass',
encryptionVersion: 2,
adata: `${domainSep}:context-A`,
});

(() => (tssUtils as any).validateAdata('context-B', ct, domainSep)).should.throw(/Adata does not match/);
});
});

it('fails to signs a txRequest for a dkls hot wallet after receiving over 3 429 errors', async function () {
const nockPromises = [
await nockTxRequestResponseSignatureShareRoundOne(bitgoParty, txRequest, bitgoGpgKey),
Expand Down
101 changes: 101 additions & 0 deletions modules/bitgo/test/v2/unit/internal/tssUtils/eddsa.ts
Original file line number Diff line number Diff line change
Expand Up @@ -547,6 +547,107 @@ 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('v2 encryption (EdDSA signing dispatch)', function () {
const signingTxRequest: TxRequest = {
txRequestId: 'v2-signing-test',
unsignedTxs: [
{
serializedTxHex: 'test-payload',
signableHex: 'deadbeef',
derivationPath: 'm/0',
},
],
date: new Date().toISOString(),
intent: { intentType: 'payment' },
latest: true,
state: 'pendingUserSignature',
walletType: 'hot',
walletId: 'walletId',
policiesChecked: true,
version: 1,
userId: 'userId',
};

it('v2 R-share round-trip: encrypt via commitment, verify envelope, decrypt via createRShare', async function () {
const passphrase = 'test-passphrase';
const prv = JSON.stringify(validUserSigningMaterial);

// 1. Encrypt prv with v2
const encryptedPrv = await bitgo.encryptAsync({
input: prv,
password: passphrase,
encryptionVersion: 2,
});
JSON.parse(encryptedPrv).v.should.equal(2);

// 2. Create commitment -- R-share should be a v2 envelope
const commitResult = await tssUtils.createCommitmentShareFromTxRequest({
txRequest: signingTxRequest,
prv,
walletPassphrase: passphrase,
bitgoGpgPubKey: bitgoGpgKey.publicKey,
encryptedPrv,
});

const rShareEnvelope = JSON.parse(commitResult.encryptedUserToBitgoRShare.share);
rShareEnvelope.v.should.equal(2);
rShareEnvelope.should.have.property('hkdfSalt');

// 3. Round-trip: decrypt the v2 R-share
const { rShare } = await tssUtils.createRShareFromTxRequest({
txRequest: signingTxRequest,
walletPassphrase: passphrase,
encryptedUserToBitgoRShare: commitResult.encryptedUserToBitgoRShare,
});

should.exist(rShare.xShare);
should.exist(rShare.rShares);
});
});

describe('signTxRequest:', function () {
Expand Down
Loading
Loading