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
27 changes: 8 additions & 19 deletions modules/key-card/src/generateQrData.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { BaseCoin } from '@bitgo/statics';
import { Keychain, KeychainsTriplet } from '@bitgo/sdk-core';
import { HIGH_ENTROPY_ENCRYPTION_VERSION, Keychain, KeychainsTriplet } from '@bitgo/sdk-core';
import { encrypt } from '@bitgo/sdk-api';
import * as assert from 'assert';
import {
Expand Down Expand Up @@ -136,10 +136,12 @@ function generateUserMasterPublicKeyQRData(publicKey: string): MasterPublicKeyQr
async function generatePasscodeQrData(
passphrase: string,
passcodeEncryptionCode: string,
encryptionVersion?: 1 | 2,
entityNoun: KeycardEntity = 'wallet'
): Promise<QrDataEntry> {
const encryptedPasscode = await encrypt(passcodeEncryptionCode, passphrase, { encryptionVersion });
// Box D uses the fixed version for its generated encryption key.
const encryptedPasscode = await encrypt(passcodeEncryptionCode, passphrase, {
encryptionVersion: HIGH_ENTROPY_ENCRYPTION_VERSION,
});
const titleNoun = entityNoun === 'safe' ? 'Safe' : 'Wallet';
return {
title: `D: Encrypted ${titleNoun} Password`,
Expand Down Expand Up @@ -199,11 +201,7 @@ export async function generateQrData(params: GenerateQrDataParams): Promise<QrDa
const qrData = buildWalletQrData(params);

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

return qrData;
Expand All @@ -213,11 +211,7 @@ export async function generateLightningQrData(params: GenerateLightningQrDataPar
const qrData = buildLightningQrData(params);

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

return qrData;
Expand Down Expand Up @@ -282,12 +276,7 @@ export async function generateSafeQrData(params: GenerateSafeQrDataParams): Prom
};

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

return qrData;
Expand Down
1 change: 1 addition & 0 deletions modules/key-card/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
/** @deprecated Ignored because Box D uses a high-entropy key. */
encryptionVersion?: EncryptionVersion;
}

Expand Down
1 change: 0 additions & 1 deletion modules/key-card/src/upgradeWalletEncryption.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,6 @@ export function createKeycardPdfGenerator(options: KeycardPdfGeneratorOptions =
bitgoKeychain,
passphrase,
passcodeEncryptionCode,
encryptionVersion: 2,
});
const questions = generateFaq(staticsCoin.fullName);
return drawKeycard({ qrData, questions, walletLabel, keyCardImage });
Expand Down
18 changes: 9 additions & 9 deletions modules/key-card/test/unit/generateQrData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as assert from 'assert';
import * as should from 'should';
import { decrypt } from '@bitgo/sdk-api';
import { generateLightningQrData, generateQrData } from '../../src/generateQrData';
import { ApiKeyShare, Keychain, KeyType } from '@bitgo/sdk-core';
import { ApiKeyShare, HIGH_ENTROPY_ENCRYPTION_VERSION, Keychain, KeyType } from '@bitgo/sdk-core';
import { coins } from '@bitgo/statics';

function createKeychain({
Expand Down Expand Up @@ -241,7 +241,7 @@ describe('generateQrData', function () {
decryptedData.should.equal(passphrase);
});

it('produces a v2 Box D when encryptionVersion is not set', async function () {
it('pins Box D to the high-entropy version when encryptionVersion is not set', async function () {
const passphrase = 'testingIsFun';
const passcodeEncryptionCode = '123456';
const qrData = await generateQrData({
Expand All @@ -255,10 +255,10 @@ describe('generateQrData', function () {

assert.ok(qrData.passcode);
const envelope = JSON.parse(qrData.passcode.data);
assert.strictEqual(envelope.v, 2, 'should default to v2 envelope');
assert.strictEqual(envelope.v, HIGH_ENTROPY_ENCRYPTION_VERSION, 'should use the high-entropy envelope version');
});

it('produces a v2 Box D when encryptionVersion: 2', async function () {
it('pins Box D to the high-entropy version when encryptionVersion: 2', async function () {
const passphrase = 'testingIsFun';
const passcodeEncryptionCode = '123456';
const qrData = await generateQrData({
Expand All @@ -273,12 +273,12 @@ describe('generateQrData', function () {

assert.ok(qrData.passcode);
const envelope = JSON.parse(qrData.passcode.data);
assert.strictEqual(envelope.v, 2, 'should produce v2 envelope');
assert.strictEqual(envelope.v, HIGH_ENTROPY_ENCRYPTION_VERSION, 'should ignore the caller-selected version');
const decryptedData = await decrypt(passcodeEncryptionCode, qrData.passcode.data);
decryptedData.should.equal(passphrase);
});

it('produces a v1 Box D when encryptionVersion: 1 is explicit', async function () {
it('pins Box D to the high-entropy version when encryptionVersion: 1', async function () {
const passphrase = 'testingIsFun';
const passcodeEncryptionCode = '123456';
const qrData = await generateQrData({
Expand All @@ -293,7 +293,7 @@ describe('generateQrData', function () {

assert.ok(qrData.passcode);
const envelope = JSON.parse(qrData.passcode.data);
assert.notStrictEqual(envelope.v, 2, 'should produce v1 envelope');
assert.strictEqual(envelope.v, HIGH_ENTROPY_ENCRYPTION_VERSION, 'should use the high-entropy envelope version');
});

it('omits Box D when passphrase or passcodeEncryptionCode is missing', async function () {
Expand Down Expand Up @@ -324,7 +324,7 @@ describe('generateLightningQrData', function () {
decryptedData.should.equal(passphrase);
});

it('produces a v2 Box D when encryptionVersion: 2', async function () {
it('pins Lightning Box D to the high-entropy version when encryptionVersion: 2', async function () {
const passphrase = 'testingIsFun';
const passcodeEncryptionCode = '123456';
const qrData = await generateLightningQrData({
Expand All @@ -337,7 +337,7 @@ describe('generateLightningQrData', function () {

assert.ok(qrData.passcode);
const envelope = JSON.parse(qrData.passcode.data);
assert.strictEqual(envelope.v, 2);
assert.strictEqual(envelope.v, HIGH_ENTROPY_ENCRYPTION_VERSION, 'should ignore the caller-selected version');
const decryptedData = await decrypt(passcodeEncryptionCode, qrData.passcode.data);
decryptedData.should.equal(passphrase);
});
Expand Down
7 changes: 7 additions & 0 deletions modules/sdk-core/src/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@ export interface DecryptKeysOptions {

export type EncryptionVersion = 1 | 2;

/**
* Encryption version for values protected by high-entropy keys, including ECDH shared secrets and
* passcode encryption codes. It is independent of the caller-selected version for password-based
* encryption.
*/
export const HIGH_ENTROPY_ENCRYPTION_VERSION: EncryptionVersion = 1;

/**
* Return type for encryption session operations.
* Runs the expensive KDF once; all subsequent calls derive keys via HKDF.
Expand Down
8 changes: 5 additions & 3 deletions modules/sdk-core/src/bitgo/internal/keycard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/
import { isUndefined } from 'lodash';
import { Keychain } from '../keychain';
import { EncryptFnAsync, EncryptionVersion } from '../../api';
import { EncryptFnAsync, EncryptionVersion, HIGH_ENTROPY_ENCRYPTION_VERSION } from '../../api';

/**
* Return the list of questions that will appear on the second page of the keycard
Expand Down Expand Up @@ -194,10 +194,11 @@ async function getKeyData(options: GetKeyDataOptions): Promise<any> {

let encryptedWalletPasscode: string | undefined;
if (passphrase && passcodeEncryptionCode) {
// Box D uses its fixed version; the backup key follows encryptionVersion.
encryptedWalletPasscode = await encrypt({
input: passphrase,
password: passcodeEncryptionCode,
encryptionVersion,
encryptionVersion: HIGH_ENTROPY_ENCRYPTION_VERSION,
});
}

Expand Down Expand Up @@ -373,7 +374,8 @@ function renderKeycardPdf(options: DrawKeycardLayoutOptions, keyData: any): any

/**
* Draw a keycard into a new pdf document object.
* Defaults to v2 (Argon2id) encryption for Box D; pass `encryptionVersion: 1` for legacy v1.
*
* `encryptionVersion` applies to the backup key. Box D uses its fixed version.
* @param options
*/
export async function drawKeycard(options: DrawKeycardOptions): Promise<any> {
Expand Down
2 changes: 2 additions & 0 deletions modules/sdk-core/src/bitgo/wallet/iWallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -832,6 +832,7 @@ export interface ShareWalletOptions {
*/
skipKeychain?: boolean;
disableEmail?: boolean;
/** @deprecated Ignored because the shared key is high entropy. */
encryptionVersion?: EncryptionVersion;
/**
* Pre-decrypted wallet keychain. When supplied, shareWallet skips its internal
Expand All @@ -856,6 +857,7 @@ export interface BulkWalletShareOptions {
path: string;
permissions: string[];
}>;
/** @deprecated Ignored because the shared key is high entropy. */
encryptionVersion?: EncryptionVersion;
}

Expand Down
18 changes: 14 additions & 4 deletions modules/sdk-core/src/bitgo/wallet/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import BigNumber from 'bignumber.js';
import * as t from 'io-ts';
import { BigIntFromString } from 'io-ts-types';
import * as _ from 'lodash';
import { EncryptionVersion, IRequestTracer } from '../../api';
import { EncryptionVersion, HIGH_ENTROPY_ENCRYPTION_VERSION, IRequestTracer } from '../../api';
import * as common from '../../common';
import { AddressBook, IAddressBook } from '../address-book';
import {
Expand Down Expand Up @@ -2043,19 +2043,26 @@ export class Wallet implements IWallet {
* @param pub - The wallet's public key
* @param userPubkey - The recipient user's public key
* @param path - The key path
* @param encryptionVersion - Optional encryption version (defaults to v2)
* @param encryptionVersion - Deprecated and ignored because the shared key is high entropy.
* @returns The encrypted keychain for the recipient with all required fields
*/
async encryptPrvForUser(
decryptedPrv: string,
pub: string,
userPubkey: string,
path: string,
/** @deprecated Ignored because the shared key is high entropy. */
encryptionVersion?: EncryptionVersion
): Promise<BulkWalletShareKeychain> {
void encryptionVersion;
const eckey = makeRandomKey();
const secret = getSharedSecret(eckey, Buffer.from(userPubkey, 'hex')).toString('hex');
const newEncryptedPrv = await this.bitgo.encrypt({ password: secret, input: decryptedPrv, encryptionVersion });
// Use the fixed version for encryption to the recipient.
const newEncryptedPrv = await this.bitgo.encrypt({
password: secret,
input: decryptedPrv,
encryptionVersion: HIGH_ENTROPY_ENCRYPTION_VERSION,
});

const keychain: BulkWalletShareKeychain = {
pub,
Expand Down Expand Up @@ -2093,12 +2100,15 @@ export class Wallet implements IWallet {
* @param walletPassphrase - The passphrase to decrypt the keychain
* @param pubkey - The recipient's public key
* @param path - The key path
* @param encryptionVersion - Deprecated and ignored because the shared key is high entropy.
* @param decryptedKeychain - Pre-decrypted keychain for bulk sharing
* @returns The encrypted keychain for the recipient
*/
async prepareSharedKeychain(
walletPassphrase: string | undefined,
pubkey: string,
path: string,
/** @deprecated Ignored because the shared key is high entropy. */
encryptionVersion?: EncryptionVersion,
decryptedKeychain?: DecryptedKeychainData
): Promise<SharedKeyChain> {
Expand Down Expand Up @@ -3453,7 +3463,7 @@ export class Wallet implements IWallet {

/**
* Creates and downloads PDF keycard for wallet (requires response from wallets.generateWallet).
* Defaults to v2 encryption for Box D; pass `encryptionVersion: 1` for legacy v1.
* `encryptionVersion` applies to the backup key. Box D uses its fixed version.
*
* Note: this is example code and is not the version used on bitgo.com
*
Expand Down
51 changes: 30 additions & 21 deletions modules/sdk-core/src/bitgo/wallet/wallets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { bip32 } from '@bitgo/utxo-lib';
import * as _ from 'lodash';
import { CoinFeature } from '@bitgo/statics';

import { EncryptionVersion, IEncryptionSession, sanitizeLegacyPath } from '../../api';
import { EncryptionVersion, HIGH_ENTROPY_ENCRYPTION_VERSION, IEncryptionSession, sanitizeLegacyPath } from '../../api';
import * as common from '../../common';
import { IBaseCoin, KeychainsTriplet, SupplementGenerateWalletOptions } from '../baseCoin';
import { BitGoBase } from '../bitgoBase';
Expand Down Expand Up @@ -173,6 +173,19 @@ export class Wallets implements IWallets {
};
}

/**
* Encrypt the wallet passphrase for recovery and Box D.
*
* Box D uses a fixed version independent of the caller's keychain encryption version.
*/
private async encryptPassphraseForRecovery(passphrase: string, passcodeEncryptionCode: string): Promise<string> {
return await this.bitgo.encrypt({
input: passphrase,
password: passcodeEncryptionCode,
encryptionVersion: HIGH_ENTROPY_ENCRYPTION_VERSION,
});
}

private async generateLightningWallet(params: GenerateLightningWalletOptions): Promise<LightningWalletWithKeychains> {
const reqId = new RequestTracer();
this.bitgo.setRequestTracer(reqId);
Expand Down Expand Up @@ -341,11 +354,10 @@ export class Wallets implements IWallets {
);

const walletData = await this.generateLightningWallet(options);
walletData.encryptedWalletPassphrase = await this.bitgo.encrypt({
input: options.passphrase,
password: options.passcodeEncryptionCode,
encryptionVersion: options.encryptionVersion,
});
walletData.encryptedWalletPassphrase = await this.encryptPassphraseForRecovery(
options.passphrase,
options.passcodeEncryptionCode
);
return walletData;
}

Expand All @@ -362,11 +374,10 @@ export class Wallets implements IWallets {

const walletData = await this.generateGoAccountWallet(options);
if (options.passphrase !== undefined && options.passcodeEncryptionCode !== undefined) {
walletData.encryptedWalletPassphrase = await this.bitgo.encrypt({
input: options.passphrase,
password: options.passcodeEncryptionCode,
encryptionVersion: options.encryptionVersion,
});
walletData.encryptedWalletPassphrase = await this.encryptPassphraseForRecovery(
options.passphrase,
options.passcodeEncryptionCode
);
}
return walletData;
}
Expand Down Expand Up @@ -472,11 +483,10 @@ export class Wallets implements IWallets {
encryptionVersion: params.encryptionVersion,
});
if (params.passcodeEncryptionCode) {
walletData.encryptedWalletPassphrase = await this.bitgo.encrypt({
input: passphrase,
password: params.passcodeEncryptionCode,
encryptionVersion: params.encryptionVersion,
});
walletData.encryptedWalletPassphrase = await this.encryptPassphraseForRecovery(
passphrase,
params.passcodeEncryptionCode
);
}
return walletData;
}
Expand Down Expand Up @@ -745,11 +755,10 @@ export class Wallets implements IWallets {
}

if (canEncrypt && params.passcodeEncryptionCode) {
result.encryptedWalletPassphrase = await this.bitgo.encrypt({
input: passphrase,
password: params.passcodeEncryptionCode,
encryptionVersion: params.encryptionVersion,
});
result.encryptedWalletPassphrase = await this.encryptPassphraseForRecovery(
passphrase,
params.passcodeEncryptionCode
);
}

return result;
Expand Down
Loading
Loading