Skip to content
Open
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
1 change: 1 addition & 0 deletions modules/abstract-lightning/src/codecs/api/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ export const UpdateLightningWalletClientRequest = t.intersection([
signerMacaroon: t.string,
signerAdminMacaroon: t.string,
signerTlsKey: t.string,
encryptionVersion: t.union([t.literal(1), t.literal(2)]),
}),
]);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,20 +24,23 @@ async function encryptWalletUpdateRequest(
requestWithEncryption.encryptedSignerTlsKey = await wallet.bitgo.encryptAsync({
password: params.passphrase,
input: params.signerTlsKey,
encryptionVersion: params.encryptionVersion,
});
}

if (params.signerAdminMacaroon) {
requestWithEncryption.encryptedSignerAdminMacaroon = await wallet.bitgo.encryptAsync({
password: params.passphrase,
input: params.signerAdminMacaroon,
encryptionVersion: params.encryptionVersion,
});
}

if (params.signerMacaroon) {
requestWithEncryption.encryptedSignerMacaroon = await wallet.bitgo.encryptAsync({
password: deriveLightningServiceSharedSecret(coinName, userAuthXprv).toString('hex'),
input: params.signerMacaroon,
encryptionVersion: params.encryptionVersion,
});
}

Expand Down
7 changes: 6 additions & 1 deletion modules/express/src/fetchEncryptedPrivKeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type Credentials = {
walletId: string; // Id of the BitGo wallet.
walletPassword: string; // Password used for the wallet.
secret: string; // xprv of user key or backup key.
encryptionVersion?: 1 | 2;
};

type WalletIds = {
Expand Down Expand Up @@ -77,7 +78,11 @@ export async function fetchKeys(ids: WalletIds, token: string, accessToken?: str

if (keychain.encryptedPrv === undefined) {
if (typeof credential === 'object') {
const encryptedPrv = await bg.encryptAsync({ password: credential.walletPassword, input: credential.secret });
const encryptedPrv = await bg.encryptAsync({
password: credential.walletPassword,
input: credential.secret,
encryptionVersion: credential.encryptionVersion,
});
output[id] = encryptedPrv;
} else {
console.warn(`could not find a ${coinName} encrypted user private key for wallet id ${id}, skipping`);
Expand Down
20 changes: 16 additions & 4 deletions modules/key-card/src/generateQrData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,12 @@ function generatePasscodeQrData(passphrase: string, passcodeEncryptionCode: stri
};
}

async function generatePasscodeQrDataAsync(passphrase: string, passcodeEncryptionCode: string): Promise<QrDataEntry> {
const encryptedWalletPasscode = await encryptAsync(passcodeEncryptionCode, passphrase);
async function generatePasscodeQrDataAsync(
passphrase: string,
passcodeEncryptionCode: string,
encryptionVersion?: 1 | 2
): Promise<QrDataEntry> {
const encryptedWalletPasscode = await encryptAsync(passcodeEncryptionCode, passphrase, { encryptionVersion });
return {
title: 'D: Encrypted wallet Password',
description: 'This is the wallet password, encrypted client-side with a key held by BitGo.',
Expand Down Expand Up @@ -211,7 +215,11 @@ export async function generateQrDataAsync(params: GenerateQrDataParams): Promise
const qrData = buildWalletQrData(params);

if (params.passphrase && params.passcodeEncryptionCode) {
qrData.passcode = await generatePasscodeQrDataAsync(params.passphrase, params.passcodeEncryptionCode);
qrData.passcode = await generatePasscodeQrDataAsync(
params.passphrase,
params.passcodeEncryptionCode,
params.encryptionVersion
);
}

return qrData;
Expand All @@ -234,7 +242,11 @@ export async function generateLightningQrDataAsync(params: GenerateLightningQrDa
const qrData = buildLightningQrData(params);

if (params.passphrase && params.passcodeEncryptionCode) {
qrData.passcode = await generatePasscodeQrDataAsync(params.passphrase, params.passcodeEncryptionCode);
qrData.passcode = await generatePasscodeQrDataAsync(
params.passphrase,
params.passcodeEncryptionCode,
params.encryptionVersion
);
}

return qrData;
Expand Down
3 changes: 2 additions & 1 deletion modules/key-card/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Keychain } from '@bitgo/sdk-core';
import { EncryptionVersion, Keychain } from '@bitgo/sdk-core';
import { BaseCoin, KeyCurve } from '@bitgo/statics';

export interface GenerateQrDataBaseParams {
Expand All @@ -25,6 +25,7 @@ export interface GenerateQrDataCoinParams {
// If both the passphrase and passcodeEncryptionCode are passed, then this code encrypts the passphrase with the
// passcodeEncryptionCode and puts the result into Box D. Allows recoveries of the wallet password.
passphrase?: string;
encryptionVersion?: EncryptionVersion;
}

export interface GenerateQrDataParams extends GenerateQrDataCoinParams {
Expand Down
84 changes: 84 additions & 0 deletions modules/key-card/test/unit/generateQrData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,72 @@ describe('generateQrDataAsync', function () {
const decryptedData = await decryptAsync(passcodeEncryptionCode, qrData.passcode.data);
decryptedData.should.equal(passphrase);
});

it('produces a v1 Box D when encryptionVersion is not set', async function () {
const passphrase = 'testingIsFun';
const passcodeEncryptionCode = '123456';
const qrData = await generateQrDataAsync({
backupKeychain: createKeychain({ encryptedPrv: 'backupPrv' }),
bitgoKeychain: createKeychain({ pub: 'bitgoPub' }),
coin: coins.get('btc'),
passcodeEncryptionCode,
passphrase,
userKeychain: createKeychain({ encryptedPrv: 'userPrv' }),
});

assert.ok(qrData.passcode);
const envelope = JSON.parse(qrData.passcode.data);
assert.notStrictEqual(envelope.v, 2, 'should default to v1 envelope');
});

it('produces a v2 Box D when encryptionVersion: 2', async function () {
const passphrase = 'testingIsFun';
const passcodeEncryptionCode = '123456';
const qrData = await generateQrDataAsync({
backupKeychain: createKeychain({ encryptedPrv: 'backupPrv' }),
bitgoKeychain: createKeychain({ pub: 'bitgoPub' }),
coin: coins.get('btc'),
passcodeEncryptionCode,
passphrase,
userKeychain: createKeychain({ encryptedPrv: 'userPrv' }),
encryptionVersion: 2,
});

assert.ok(qrData.passcode);
const envelope = JSON.parse(qrData.passcode.data);
assert.strictEqual(envelope.v, 2, 'should produce v2 envelope');
const decryptedData = await decryptAsync(passcodeEncryptionCode, qrData.passcode.data);
decryptedData.should.equal(passphrase);
});

it('produces a v1 Box D when encryptionVersion: 1 is explicit', async function () {
const passphrase = 'testingIsFun';
const passcodeEncryptionCode = '123456';
const qrData = await generateQrDataAsync({
backupKeychain: createKeychain({ encryptedPrv: 'backupPrv' }),
bitgoKeychain: createKeychain({ pub: 'bitgoPub' }),
coin: coins.get('btc'),
passcodeEncryptionCode,
passphrase,
userKeychain: createKeychain({ encryptedPrv: 'userPrv' }),
encryptionVersion: 1,
});

assert.ok(qrData.passcode);
const envelope = JSON.parse(qrData.passcode.data);
assert.notStrictEqual(envelope.v, 2, 'should produce v1 envelope');
});

it('omits Box D when passphrase or passcodeEncryptionCode is missing', async function () {
const qrData = await generateQrDataAsync({
backupKeychain: createKeychain({ encryptedPrv: 'backupPrv' }),
bitgoKeychain: createKeychain({ pub: 'bitgoPub' }),
coin: coins.get('btc'),
userKeychain: createKeychain({ encryptedPrv: 'userPrv' }),
encryptionVersion: 2,
});
assert.strictEqual(qrData.passcode, undefined);
});
});

describe('generateLightningQrDataAsync', function () {
Expand All @@ -264,4 +330,22 @@ describe('generateLightningQrDataAsync', function () {
const decryptedData = await decryptAsync(passcodeEncryptionCode, qrData.passcode.data);
decryptedData.should.equal(passphrase);
});

it('produces a v2 Box D when encryptionVersion: 2', async function () {
const passphrase = 'testingIsFun';
const passcodeEncryptionCode = '123456';
const qrData = await generateLightningQrDataAsync({
userAuthKeychain: createKeychain({ encryptedPrv: 'userAuthPrv' }),
coin: coins.get('lnbtc'),
passcodeEncryptionCode,
passphrase,
encryptionVersion: 2,
});

assert.ok(qrData.passcode);
const envelope = JSON.parse(qrData.passcode.data);
assert.strictEqual(envelope.v, 2);
const decryptedData = await decryptAsync(passcodeEncryptionCode, qrData.passcode.data);
decryptedData.should.equal(passphrase);
});
});
7 changes: 4 additions & 3 deletions modules/passkey-crypto/src/attachPasskeyToWallet.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { BitGoBase, Keychain } from '@bitgo/sdk-core';
import { BitGoBase, EncryptionVersion, Keychain } from '@bitgo/sdk-core';
import { base64UrlToBuffer } from './base64url';
import { deriveEnterpriseSalt } from './deriveEnterpriseSalt';
import { derivePassword } from './derivePassword';
Expand All @@ -11,8 +11,9 @@ export async function attachPasskeyToWallet(params: {
device: WebAuthnOtpDevice;
existingPassphrase: string;
provider: WebAuthnProvider;
encryptionVersion?: EncryptionVersion;
}): Promise<Keychain> {
const { bitgo, coin, walletId, device, existingPassphrase, provider } = params;
const { bitgo, coin, walletId, device, existingPassphrase, provider, encryptionVersion } = params;

// Throw early if PRF extension is not supported
if (!device.prfSalt) {
Expand Down Expand Up @@ -66,7 +67,7 @@ export async function attachPasskeyToWallet(params: {
}

const prfPassword = derivePassword(authResult.prfResult);
const encryptedPrv = await bitgo.encryptAsync({ password: prfPassword, input: privateKey, encryptionVersion: 2 });
const encryptedPrv = await bitgo.encryptAsync({ password: prfPassword, input: privateKey, encryptionVersion });
Comment on lines -69 to +70
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there are no callers of this methods after a github search

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm you wanna check with @mohammadalfaiyazbitgo on this one?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure can do that


const updatedKeychain = await bitgo
.put(bitgo.url(`/${coin}/key/${keychainId}`, 2))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ describe('attachPasskeyToWallet', function () {
device,
existingPassphrase,
provider: mockProvider as unknown as WebAuthnProvider,
encryptionVersion: 2,
...overrides,
});
}
Expand Down
19 changes: 13 additions & 6 deletions modules/sdk-api/src/bitgoAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
DecryptOptions,
defaultConstants,
EcdhDerivedKeypair,
EncryptionVersion,
EncryptOptions,
EnvironmentName,
generateRandomPassword,
Expand Down Expand Up @@ -1125,7 +1126,7 @@ export class BitGoAPI implements BitGoBase {
* @returns {Promise<any>} - A promise that resolves with the new ECDH keychain data.
* @throws {Error} - Throws an error if there is an issue creating the keychain.
*/
public async createUserEcdhKeychain(loginPassword: string): Promise<any> {
public async createUserEcdhKeychain(loginPassword: string, encryptionVersion?: EncryptionVersion): Promise<any> {
const keyData = this.keychains().create();
const hdNode = bitcoin.HDNode.fromBase58(keyData.xprv);

Expand All @@ -1139,6 +1140,7 @@ export class BitGoAPI implements BitGoBase {
encryptedXprv: await this.encryptAsync({
password: loginPassword,
input: hdNode.toBase58(),
encryptionVersion,
}),
});
}
Expand All @@ -1160,7 +1162,10 @@ export class BitGoAPI implements BitGoBase {
* @returns {Promise<any>} - A promise that resolves with the user's settings ensuring we have the ecdhKeychain in there.
* @throws {Error} - Throws an error if there is an issue creating the keychain or updating the user's settings.
*/
private async ensureUserEcdhKeychainIsCreated(loginPassword: string): Promise<any> {
private async ensureUserEcdhKeychainIsCreated(
loginPassword: string,
encryptionVersion?: EncryptionVersion
): Promise<any> {
/**
* Get the user's current settings.
*/
Expand All @@ -1169,7 +1174,7 @@ export class BitGoAPI implements BitGoBase {
* If the user's ECDH keychain does not exist, create a new keychain and update the user's settings.
*/
if (!userSettings.settings.ecdhKeychain) {
const newKeychain = await this.createUserEcdhKeychain(loginPassword);
const newKeychain = await this.createUserEcdhKeychain(loginPassword, encryptionVersion);
await this.updateUserSettings({
settings: {
ecdhKeychain: newKeychain.xpub,
Expand Down Expand Up @@ -1251,7 +1256,9 @@ export class BitGoAPI implements BitGoBase {
await this._hmacAuthStrategy.setToken?.(this._token);
}

const userSettings = params.ensureEcdhKeychain ? await this.ensureUserEcdhKeychainIsCreated(password) : undefined;
const userSettings = params.ensureEcdhKeychain
? await this.ensureUserEcdhKeychainIsCreated(password, params.encryptionVersion)
: undefined;
if (userSettings?.ecdhKeychain) {
response.body.user.ecdhKeychain = userSettings.ecdhKeychain;
}
Expand Down Expand Up @@ -1932,11 +1939,11 @@ export class BitGoAPI implements BitGoBase {
* @param passwords
* @param m
*/
async splitSecretAsync({ seed, passwords, m }: SplitSecretOptions): Promise<SplitSecret> {
async splitSecretAsync({ seed, passwords, m, encryptionVersion }: SplitSecretOptions): Promise<SplitSecret> {
const n = validateSplitSecretInputs({ seed, passwords, m });
const secrets: string[] = shamir.share(seed, n, m);
const shards = await Promise.all(
secrets.map((shard, i) => this.encryptAsync({ input: shard, password: passwords[i] }))
secrets.map((shard, i) => this.encryptAsync({ input: shard, password: passwords[i], encryptionVersion }))
);
return buildSplitSecretResult(seed, shards, m, n);
}
Expand Down
8 changes: 7 additions & 1 deletion modules/sdk-api/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { EnvironmentName, IRequestTracer, V1Network } from '@bitgo/sdk-core';
import { EncryptionVersion, EnvironmentName, IRequestTracer, V1Network } from '@bitgo/sdk-core';
import { ECPairInterface } from '@bitgo/utxo-lib';
import { type Agent } from 'http';

Expand Down Expand Up @@ -104,6 +104,11 @@ export interface AuthenticateOptions {
* It is highly recommended that this is always set to avoid any issues when using a BitGo wallet
*/
ensureEcdhKeychain?: boolean;
/**
* Encryption version to use when creating the ECDH keychain if it does not already exist.
* Only applies when `ensureEcdhKeychain` is true and no ECDH keychain exists for the user yet.
*/
encryptionVersion?: EncryptionVersion;
forReset2FA?: boolean;
/**
* The initial stage fingerprint hash used for device identification and verification.
Expand Down Expand Up @@ -226,6 +231,7 @@ export interface SplitSecretOptions {
seed: string;
passwords: string[];
m: number;
encryptionVersion?: EncryptionVersion;
}

export interface SplitSecret {
Expand Down
9 changes: 7 additions & 2 deletions modules/sdk-api/src/v1/keychains.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

import { bip32 } from '@bitgo/utxo-lib';
import { randomBytes } from 'crypto';
import { common, Util, sanitizeLegacyPath } from '@bitgo/sdk-core';
import { common, isV2Envelope, Util, sanitizeLegacyPath } from '@bitgo/sdk-core';
const _ = require('lodash');

//
Expand Down Expand Up @@ -194,7 +194,12 @@ Keychains.prototype.updatePassword = function (params, callback) {
input: oldEncryptedXprv as string,
password: params.oldPassword,
});
const newEncryptedPrv = await self.bitgo.encryptAsync({ input: decryptedPrv, password: params.newPassword });
const encryptionVersion = isV2Envelope(oldEncryptedXprv as string) ? 2 : 1;
const newEncryptedPrv = await self.bitgo.encryptAsync({
input: decryptedPrv,
password: params.newPassword,
encryptionVersion,
});
newKeychains[xpub] = newEncryptedPrv;
} catch (e) {
// decrypting the keychain with the old password didn't work so we just keep it the way it is
Expand Down
1 change: 1 addition & 0 deletions modules/sdk-api/src/v1/travelRule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,7 @@ TravelRule.prototype.prepareParamsAsync = async function (params) {
const encryptedTravelInfo = await this.bitgo.encryptAsync({
input: prepared.travelInfoJSON,
password: prepared.sharedSecret,
encryptionVersion: params.encryptionVersion,
});

return buildTravelRuleSendParams(prepared, encryptedTravelInfo);
Expand Down
6 changes: 5 additions & 1 deletion modules/sdk-api/src/v1/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2318,7 +2318,11 @@ Wallet.prototype.shareWallet = function (params, callback) {

const eckey = makeRandomKey();
const secret = getSharedSecret(eckey, Buffer.from(sharing.pubkey, 'hex')).toString('hex');
const newEncryptedXprv = await self.bitgo.encryptAsync({ password: secret, input: keychain.xprv });
const newEncryptedXprv = await self.bitgo.encryptAsync({
password: secret,
input: keychain.xprv,
encryptionVersion: params.encryptionVersion,
});

sharedKeychain = {
xpub: keychain.xpub,
Expand Down
Loading
Loading