From 0fb61ebbea832c529f0b7b074b5a2b3df8d5273f Mon Sep 17 00:00:00 2001 From: David Kaplan Date: Tue, 5 May 2026 13:50:45 -0400 Subject: [PATCH] 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 --- commitlint.config.js | 1 + modules/key-card/src/faq.ts | 14 ++++++ modules/key-card/src/generateQrData.ts | 46 +++++++++++++++--- modules/key-card/src/index.ts | 24 ++++++++-- modules/key-card/src/types.ts | 33 ++++++++----- modules/key-card/test/unit/faq.ts | 18 ++++++- modules/key-card/test/unit/generateQrData.ts | 49 +++++++++++++++++++- 7 files changed, 160 insertions(+), 25 deletions(-) diff --git a/commitlint.config.js b/commitlint.config.js index c237343dd7..5dca7b8f13 100644 --- a/commitlint.config.js +++ b/commitlint.config.js @@ -65,6 +65,7 @@ module.exports = { 'STLX-', 'TMS-', 'TRUST-', + 'T1-', 'USDS-', 'VL-', 'WIN-', diff --git a/modules/key-card/src/faq.ts b/modules/key-card/src/faq.ts index ad9c1a5847..f1d582d4c1 100644 --- a/modules/key-card/src/faq.ts +++ b/modules/key-card/src/faq.ts @@ -72,3 +72,17 @@ export function generateFaq(coinName: string): FAQ[] { }, ]; } + +export function generateLightningFaq(coinName: string): FAQ[] { + return [ + ...generateFaq(coinName), + { + question: 'What is the User Auth Key?', + answer: [ + 'The User Auth Key is the private key used to authenticate you for signing lightning payment ', + 'requests and wallet configuration updates. It is encrypted with your wallet password. Without it, ', + `you will not be able to authorize transactions on your ${coinName} lightning wallet.`, + ], + }, + ]; +} diff --git a/modules/key-card/src/generateQrData.ts b/modules/key-card/src/generateQrData.ts index cef41ad99d..bdd9388a1a 100644 --- a/modules/key-card/src/generateQrData.ts +++ b/modules/key-card/src/generateQrData.ts @@ -2,7 +2,13 @@ import { BaseCoin } from '@bitgo/statics'; import { Keychain } from '@bitgo/sdk-core'; import { encrypt } from '@bitgo/sdk-api'; import * as assert from 'assert'; -import { GenerateQrDataParams, MasterPublicKeyQrDataEntry, QrData, QrDataEntry } from './types'; +import { + GenerateLightningQrDataParams, + GenerateQrDataParams, + MasterPublicKeyQrDataEntry, + QrData, + QrDataEntry, +} from './types'; function getPubFromKey(key: Keychain): string | undefined { switch (key.type) { @@ -123,6 +129,15 @@ function generateUserMasterPublicKeyQRData(publicKey: string): MasterPublicKeyQr }; } +function generatePasscodeQrData(passphrase: string, passcodeEncryptionCode: string): QrDataEntry { + const encryptedWalletPasscode = encrypt(passcodeEncryptionCode, passphrase); + return { + title: 'D: Encrypted wallet Password', + description: 'This is the wallet password, encrypted client-side with a key held by BitGo.', + data: encryptedWalletPasscode, + }; +} + function generateBackupMasterPublicKeyQRData(publicKey: string): MasterPublicKeyQrDataEntry { return { title: 'F: Master Backup Public Key', @@ -159,13 +174,30 @@ export function generateQrData({ }; if (passphrase && passcodeEncryptionCode) { - const encryptedWalletPasscode = encrypt(passcodeEncryptionCode, passphrase); + qrData.passcode = generatePasscodeQrData(passphrase, passcodeEncryptionCode); + } - qrData.passcode = { - title: 'D: Encrypted wallet Password', - description: 'This is the wallet password, encrypted client-side with a key held by BitGo.', - data: encryptedWalletPasscode, - }; + return qrData; +} + +export function generateLightningQrData({ + userAuthKeychain, + passcodeEncryptionCode, + passphrase, +}: GenerateLightningQrDataParams): QrData { + assert.ok(userAuthKeychain.encryptedPrv, 'userAuthKeychain must have an encryptedPrv'); + + const qrData: QrData = { + user: { + title: 'A: User Auth Key', + description: + 'This is your user authentication private key, encrypted with your wallet password.\r\nIt is used to authenticate payment and wallet operations.', + data: userAuthKeychain.encryptedPrv, + }, + }; + + if (passphrase && passcodeEncryptionCode) { + qrData.passcode = generatePasscodeQrData(passphrase, passcodeEncryptionCode); } return qrData; diff --git a/modules/key-card/src/index.ts b/modules/key-card/src/index.ts index 36a6cd02b1..5c3fa0145d 100644 --- a/modules/key-card/src/index.ts +++ b/modules/key-card/src/index.ts @@ -1,8 +1,8 @@ -import { generateQrData } from './generateQrData'; -import { generateFaq } from './faq'; +import { generateLightningQrData, generateQrData } from './generateQrData'; +import { generateFaq, generateLightningFaq } from './faq'; import { drawKeycard } from './drawKeycard'; import { generateParamsForKeyCreation } from './generateParamsForKeyCreation'; -import { GenerateKeycardParams } from './types'; +import { GenerateKeycardParams, GenerateLightningQrDataParams, GenerateQrDataBaseParams } from './types'; export * from './drawKeycard'; export * from './faq'; @@ -11,7 +11,13 @@ export * from './utils'; export * from './types'; export async function generateKeycard(params: GenerateKeycardParams): Promise { - if ('coin' in params) { + if ('userAuthKeychain' in params) { + const questions = generateLightningFaq(params.coin.fullName); + const qrData = generateLightningQrData(params); + const keycard = await drawKeycard({ ...params, questions, qrData }); + const label = params.walletLabel || params.coin.fullName; + keycard.save(`BitGo Keycard for ${label}.pdf`); + } else if ('coin' in params) { const questions = generateFaq(params.coin.fullName); const qrData = generateQrData(params); const keycard = await drawKeycard({ ...params, questions, qrData }); @@ -26,3 +32,13 @@ export async function generateKeycard(params: GenerateKeycardParams): Promise { + const questions = generateLightningFaq(params.coin.fullName); + const qrData = generateLightningQrData(params); + const keycard = await drawKeycard({ ...params, questions, qrData }); + const label = params.walletLabel || params.coin.fullName; + keycard.save(`BitGo Keycard for ${label}.pdf`); +} diff --git a/modules/key-card/src/types.ts b/modules/key-card/src/types.ts index 1778a48095..074762b1ca 100644 --- a/modules/key-card/src/types.ts +++ b/modules/key-card/src/types.ts @@ -14,7 +14,20 @@ export interface GenerateQrDataForKeychainParams { curve: KeyCurve; } -export interface GenerateQrDataParams { +export interface GenerateQrDataCoinParams { + // The coin of the wallet that was/ is about to be created + coin: Readonly; + // A code that can be used to encrypt the wallet password to. + // 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. + passcodeEncryptionCode?: string; + // The wallet password + // 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; +} + +export interface GenerateQrDataParams extends GenerateQrDataCoinParams { // The backup keychain as it is returned from the BitGo API upon creation backupKeychain: Keychain; // 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 { backupMasterPublicKey?: string; // The BitGo keychain as it is returned from the BitGo API upon creation bitgoKeychain: Keychain; - // The coin of the wallet that was/ is about to be created - coin: Readonly; - // A code that can be used to encrypt the wallet password to. - // 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. - passcodeEncryptionCode?: string; - // The wallet password - // 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; // The user keychain as it is returned from the BitGo API upon creation userKeychain: Keychain; // The key id of the user key, only used for cold keys @@ -47,7 +50,13 @@ export interface GenerateQrDataParams { userMasterPublicKey?: string; } -export type GenerateKeycardParams = GenerateQrDataBaseParams & (GenerateQrDataForKeychainParams | GenerateQrDataParams); +export interface GenerateLightningQrDataParams extends GenerateQrDataCoinParams { + // The user authentication keychain, used to sign payment requests and wallet configuration updates + userAuthKeychain: Keychain; +} + +export type GenerateKeycardParams = GenerateQrDataBaseParams & + (GenerateQrDataForKeychainParams | GenerateQrDataParams | GenerateLightningQrDataParams); export interface IDrawKeyCard { activationCode?: string; diff --git a/modules/key-card/test/unit/faq.ts b/modules/key-card/test/unit/faq.ts index 97496f88f8..a7dcd1a90e 100644 --- a/modules/key-card/test/unit/faq.ts +++ b/modules/key-card/test/unit/faq.ts @@ -1,4 +1,4 @@ -import { generateFaq } from '../../src/faq'; +import { generateFaq, generateLightningFaq } from '../../src/faq'; describe('generateFaq', function () { it('generates faq with filled in coin name', function () { @@ -12,3 +12,19 @@ describe('generateFaq', function () { questions[6].answer[2].should.match(new RegExp(coinName)); }); }); + +describe('generateLightningFaq', function () { + it('generates base FAQ plus lightning-specific questions', function () { + const coinName = 'Lightning Bitcoin'; + const questions = generateLightningFaq(coinName); + + questions.length.should.equal(8); + + // Base FAQ coin name interpolation still works + questions[0].answer[0].should.match(new RegExp(coinName)); + + // Lightning-specific question + questions[7].question.should.equal('What is the User Auth Key?'); + questions[7].answer[2].should.match(new RegExp(coinName)); + }); +}); diff --git a/modules/key-card/test/unit/generateQrData.ts b/modules/key-card/test/unit/generateQrData.ts index 1191ea0f65..77a75887b8 100644 --- a/modules/key-card/test/unit/generateQrData.ts +++ b/modules/key-card/test/unit/generateQrData.ts @@ -1,6 +1,6 @@ import * as assert from 'assert'; import * as should from 'should'; -import { generateQrData } from '../../src/generateQrData'; +import { generateLightningQrData, generateQrData } from '../../src/generateQrData'; import { decrypt } from '@bitgo/sdk-api'; import { ApiKeyShare, Keychain, KeyType } from '@bitgo/sdk-core'; import { coins } from '@bitgo/statics'; @@ -137,6 +137,53 @@ describe('generateQrData', function () { } }); + describe('generateLightningQrData', function () { + it('lightning wallet with encrypted key and passcode', function () { + const userAuthEncryptedPrv = 'userAuthPrv123encrypted'; + const passphrase = 'testingIsFun'; + const passcodeEncryptionCode = '123456'; + + const qrData = generateLightningQrData({ + userAuthKeychain: createKeychain({ encryptedPrv: userAuthEncryptedPrv }), + coin: coins.get('lnbtc'), + passcodeEncryptionCode, + passphrase, + }); + + qrData.user.title.should.equal('A: User Auth Key'); + qrData.user.description.should.match(/user authentication private key/); + qrData.user.data.should.equal(userAuthEncryptedPrv); + + should.not.exist(qrData.backup); + should.not.exist(qrData.bitgo); + + assert.ok(qrData.passcode); + qrData.passcode.title.should.equal('D: Encrypted wallet Password'); + const decryptedData = decrypt(passcodeEncryptionCode, qrData.passcode.data); + decryptedData.should.equal(passphrase); + }); + + it('lightning wallet without passcode', function () { + const qrData = generateLightningQrData({ + userAuthKeychain: createKeychain({ encryptedPrv: 'userAuthPrv' }), + coin: coins.get('lnbtc'), + }); + + should.not.exist(qrData.passcode); + }); + + it('throws when userAuthKeychain is missing encryptedPrv', function () { + assert.throws( + () => + generateLightningQrData({ + userAuthKeychain: createKeychain({ pub: 'pub123' }), + coin: coins.get('lnbtc'), + }), + /userAuthKeychain must have an encryptedPrv/ + ); + }); + }); + it('backup key from provider', function () { const coin = coins.get('btc'); const userEncryptedPrv = 'prv123encrypted';