Skip to content

Commit f4f05d1

Browse files
feat: add lightning keycard generation to @bitgo/key-card
Add generateLightningKeycard function for single-sig lightning wallets. Lightning keycards include the encrypted user auth key (box A) and encrypted wallet password (box D), without backup or BitGo keys. Extract shared coin/passphrase params into GenerateQrDataCoinParams interface and add lightning-specific FAQ question. T1-3214
1 parent 7e6aa37 commit f4f05d1

7 files changed

Lines changed: 153 additions & 24 deletions

File tree

commitlint.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ module.exports = {
6565
'STLX-',
6666
'TMS-',
6767
'TRUST-',
68+
'T1-',
6869
'USDS-',
6970
'VL-',
7071
'WIN-',

modules/key-card/src/faq.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,3 +72,17 @@ export function generateFaq(coinName: string): FAQ[] {
7272
},
7373
];
7474
}
75+
76+
export function generateLightningFaq(coinName: string): FAQ[] {
77+
return [
78+
...generateFaq(coinName),
79+
{
80+
question: 'What is the User Auth Key?',
81+
answer: [
82+
'The User Auth Key is the private key used to authenticate you for signing lightning payment ',
83+
'requests and wallet configuration updates. It is encrypted with your wallet password. Without it, ',
84+
`you will not be able to authorize transactions on your ${coinName} lightning wallet.`,
85+
],
86+
},
87+
];
88+
}

modules/key-card/src/generateQrData.ts

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@ import { BaseCoin } from '@bitgo/statics';
22
import { Keychain } from '@bitgo/sdk-core';
33
import { encrypt } from '@bitgo/sdk-api';
44
import * as assert from 'assert';
5-
import { GenerateQrDataParams, MasterPublicKeyQrDataEntry, QrData, QrDataEntry } from './types';
5+
import {
6+
GenerateLightningQrDataParams,
7+
GenerateQrDataParams,
8+
MasterPublicKeyQrDataEntry,
9+
QrData,
10+
QrDataEntry,
11+
} from './types';
612

713
function getPubFromKey(key: Keychain): string | undefined {
814
switch (key.type) {
@@ -123,6 +129,15 @@ function generateUserMasterPublicKeyQRData(publicKey: string): MasterPublicKeyQr
123129
};
124130
}
125131

132+
function generatePasscodeQrData(passphrase: string, passcodeEncryptionCode: string): QrDataEntry {
133+
const encryptedWalletPasscode = encrypt(passcodeEncryptionCode, passphrase);
134+
return {
135+
title: 'D: Encrypted wallet Password',
136+
description: 'This is the wallet password, encrypted client-side with a key held by BitGo.',
137+
data: encryptedWalletPasscode,
138+
};
139+
}
140+
126141
function generateBackupMasterPublicKeyQRData(publicKey: string): MasterPublicKeyQrDataEntry {
127142
return {
128143
title: 'F: Master Backup Public Key',
@@ -159,13 +174,30 @@ export function generateQrData({
159174
};
160175

161176
if (passphrase && passcodeEncryptionCode) {
162-
const encryptedWalletPasscode = encrypt(passcodeEncryptionCode, passphrase);
177+
qrData.passcode = generatePasscodeQrData(passphrase, passcodeEncryptionCode);
178+
}
163179

164-
qrData.passcode = {
165-
title: 'D: Encrypted wallet Password',
166-
description: 'This is the wallet password, encrypted client-side with a key held by BitGo.',
167-
data: encryptedWalletPasscode,
168-
};
180+
return qrData;
181+
}
182+
183+
export function generateLightningQrData({
184+
userAuthKeychain,
185+
passcodeEncryptionCode,
186+
passphrase,
187+
}: GenerateLightningQrDataParams): QrData {
188+
assert.ok(userAuthKeychain.encryptedPrv, 'userAuthKeychain must have an encryptedPrv');
189+
190+
const qrData: QrData = {
191+
user: {
192+
title: 'A: User Auth Key',
193+
description:
194+
'This is your user authentication private key, encrypted with your wallet password.\r\nIt is used to authenticate payment and wallet operations.',
195+
data: userAuthKeychain.encryptedPrv,
196+
},
197+
};
198+
199+
if (passphrase && passcodeEncryptionCode) {
200+
qrData.passcode = generatePasscodeQrData(passphrase, passcodeEncryptionCode);
169201
}
170202

171203
return qrData;

modules/key-card/src/index.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import { generateQrData } from './generateQrData';
2-
import { generateFaq } from './faq';
1+
import { generateLightningQrData, generateQrData } from './generateQrData';
2+
import { generateFaq, generateLightningFaq } from './faq';
33
import { drawKeycard } from './drawKeycard';
44
import { generateParamsForKeyCreation } from './generateParamsForKeyCreation';
5-
import { GenerateKeycardParams } from './types';
5+
import { GenerateKeycardParams, GenerateLightningQrDataParams, GenerateQrDataBaseParams } from './types';
66

77
export * from './drawKeycard';
88
export * from './faq';
@@ -26,3 +26,13 @@ export async function generateKeycard(params: GenerateKeycardParams): Promise<vo
2626
throw new Error('Either curve or coin must be provided');
2727
}
2828
}
29+
30+
export async function generateLightningKeycard(
31+
params: GenerateQrDataBaseParams & GenerateLightningQrDataParams
32+
): Promise<void> {
33+
const questions = generateLightningFaq(params.coin.fullName);
34+
const qrData = generateLightningQrData(params);
35+
const keycard = await drawKeycard({ ...params, questions, qrData });
36+
const label = params.walletLabel || params.coin.fullName;
37+
keycard.save(`BitGo Keycard for ${label}.pdf`);
38+
}

modules/key-card/src/types.ts

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,20 @@ export interface GenerateQrDataForKeychainParams {
1414
curve: KeyCurve;
1515
}
1616

17-
export interface GenerateQrDataParams {
17+
export interface GenerateQrDataCoinParams {
18+
// The coin of the wallet that was/ is about to be created
19+
coin: Readonly<BaseCoin>;
20+
// A code that can be used to encrypt the wallet password to.
21+
// If both the passphrase and passcodeEncryptionCode are passed, then this code encrypts the passphrase with the
22+
// passcodeEncryptionCode and puts the result into Box D. Allows recoveries of the wallet password.
23+
passcodeEncryptionCode?: string;
24+
// The wallet password
25+
// If both the passphrase and passcodeEncryptionCode are passed, then this code encrypts the passphrase with the
26+
// passcodeEncryptionCode and puts the result into Box D. Allows recoveries of the wallet password.
27+
passphrase?: string;
28+
}
29+
30+
export interface GenerateQrDataParams extends GenerateQrDataCoinParams {
1831
// The backup keychain as it is returned from the BitGo API upon creation
1932
backupKeychain: Keychain;
2033
// The name of the 3rd party provider of the backup key if neither the user nor BitGo stores it
@@ -27,16 +40,6 @@ export interface GenerateQrDataParams {
2740
backupMasterPublicKey?: string;
2841
// The BitGo keychain as it is returned from the BitGo API upon creation
2942
bitgoKeychain: Keychain;
30-
// The coin of the wallet that was/ is about to be created
31-
coin: Readonly<BaseCoin>;
32-
// A code that can be used to encrypt the wallet password to.
33-
// If both the passphrase and passcodeEncryptionCode are passed, then this code encrypts the passphrase with the
34-
// passcodeEncryptionCode and puts the result into Box D. Allows recoveries of the wallet password.
35-
passcodeEncryptionCode?: string;
36-
// The wallet password
37-
// If both the passphrase and passcodeEncryptionCode are passed, then this code encrypts the passphrase with the
38-
// passcodeEncryptionCode and puts the result into Box D. Allows recoveries of the wallet password.
39-
passphrase?: string;
4043
// The user keychain as it is returned from the BitGo API upon creation
4144
userKeychain: Keychain;
4245
// The key id of the user key, only used for cold keys
@@ -47,7 +50,13 @@ export interface GenerateQrDataParams {
4750
userMasterPublicKey?: string;
4851
}
4952

50-
export type GenerateKeycardParams = GenerateQrDataBaseParams & (GenerateQrDataForKeychainParams | GenerateQrDataParams);
53+
export interface GenerateLightningQrDataParams extends GenerateQrDataCoinParams {
54+
// The user authentication keychain, used to sign payment requests and wallet configuration updates
55+
userAuthKeychain: Keychain;
56+
}
57+
58+
export type GenerateKeycardParams = GenerateQrDataBaseParams &
59+
(GenerateQrDataForKeychainParams | GenerateQrDataParams | GenerateLightningQrDataParams);
5160

5261
export interface IDrawKeyCard {
5362
activationCode?: string;

modules/key-card/test/unit/faq.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { generateFaq } from '../../src/faq';
1+
import { generateFaq, generateLightningFaq } from '../../src/faq';
22

33
describe('generateFaq', function () {
44
it('generates faq with filled in coin name', function () {
@@ -12,3 +12,19 @@ describe('generateFaq', function () {
1212
questions[6].answer[2].should.match(new RegExp(coinName));
1313
});
1414
});
15+
16+
describe('generateLightningFaq', function () {
17+
it('generates base FAQ plus lightning-specific questions', function () {
18+
const coinName = 'Lightning Bitcoin';
19+
const questions = generateLightningFaq(coinName);
20+
21+
questions.length.should.equal(8);
22+
23+
// Base FAQ coin name interpolation still works
24+
questions[0].answer[0].should.match(new RegExp(coinName));
25+
26+
// Lightning-specific question
27+
questions[7].question.should.equal('What is the User Auth Key?');
28+
questions[7].answer[2].should.match(new RegExp(coinName));
29+
});
30+
});

modules/key-card/test/unit/generateQrData.ts

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as assert from 'assert';
22
import * as should from 'should';
3-
import { generateQrData } from '../../src/generateQrData';
3+
import { generateLightningQrData, generateQrData } from '../../src/generateQrData';
44
import { decrypt } from '@bitgo/sdk-api';
55
import { ApiKeyShare, Keychain, KeyType } from '@bitgo/sdk-core';
66
import { coins } from '@bitgo/statics';
@@ -137,6 +137,53 @@ describe('generateQrData', function () {
137137
}
138138
});
139139

140+
describe('generateLightningQrData', function () {
141+
it('lightning wallet with encrypted key and passcode', function () {
142+
const userAuthEncryptedPrv = 'userAuthPrv123encrypted';
143+
const passphrase = 'testingIsFun';
144+
const passcodeEncryptionCode = '123456';
145+
146+
const qrData = generateLightningQrData({
147+
userAuthKeychain: createKeychain({ encryptedPrv: userAuthEncryptedPrv }),
148+
coin: coins.get('lnbtc'),
149+
passcodeEncryptionCode,
150+
passphrase,
151+
});
152+
153+
qrData.user.title.should.equal('A: User Auth Key');
154+
qrData.user.description.should.match(/user authentication private key/);
155+
qrData.user.data.should.equal(userAuthEncryptedPrv);
156+
157+
should.not.exist(qrData.backup);
158+
should.not.exist(qrData.bitgo);
159+
160+
assert.ok(qrData.passcode);
161+
qrData.passcode.title.should.equal('D: Encrypted wallet Password');
162+
const decryptedData = decrypt(passcodeEncryptionCode, qrData.passcode.data);
163+
decryptedData.should.equal(passphrase);
164+
});
165+
166+
it('lightning wallet without passcode', function () {
167+
const qrData = generateLightningQrData({
168+
userAuthKeychain: createKeychain({ encryptedPrv: 'userAuthPrv' }),
169+
coin: coins.get('lnbtc'),
170+
});
171+
172+
should.not.exist(qrData.passcode);
173+
});
174+
175+
it('throws when userAuthKeychain is missing encryptedPrv', function () {
176+
assert.throws(
177+
() =>
178+
generateLightningQrData({
179+
userAuthKeychain: createKeychain({ pub: 'pub123' }),
180+
coin: coins.get('lnbtc'),
181+
}),
182+
/userAuthKeychain must have an encryptedPrv/
183+
);
184+
});
185+
});
186+
140187
it('backup key from provider', function () {
141188
const coin = coins.get('btc');
142189
const userEncryptedPrv = 'prv123encrypted';

0 commit comments

Comments
 (0)