From 54bf80aa45ed7586b5872e5d18d4a1afe58d5fa6 Mon Sep 17 00:00:00 2001 From: Sibi Krishnan Date: Mon, 13 Jul 2026 16:21:38 -0400 Subject: [PATCH] chore: rename wallet vault keycard references to safe + update keycard copy/formatting WCN-1193 --- modules/key-card/src/drawKeycard.ts | 49 +++++++---- modules/key-card/src/generateQrData.ts | 64 ++++++++------- modules/key-card/src/index.ts | 16 ++-- modules/key-card/src/parseKeycard.ts | 20 ++--- modules/key-card/src/types.ts | 21 ++--- .../unit/{vaultQrData.ts => safeQrData.ts} | 81 ++++++++++--------- 6 files changed, 139 insertions(+), 112 deletions(-) rename modules/key-card/test/unit/{vaultQrData.ts => safeQrData.ts} (68%) diff --git a/modules/key-card/src/drawKeycard.ts b/modules/key-card/src/drawKeycard.ts index 4fb7488cf7..f60322b0ee 100644 --- a/modules/key-card/src/drawKeycard.ts +++ b/modules/key-card/src/drawKeycard.ts @@ -30,7 +30,7 @@ enum KeyCurveName { export const QRBinaryMaxLength = 1500; // Default page-break layout: start a new page before the 3rd box (index 2), matching the -// historical wallet keycard. Callers (e.g. the vault keycard) may override with their own indices. +// historical wallet keycard. Callers (e.g. the safe keycard) may override with their own indices. const DEFAULT_PAGE_BREAK_INDICES = [2]; const font = { @@ -56,26 +56,29 @@ function moveDown(y: number, ydelta: number): number { return y + ydelta; } +// Draws QR codes down the left column, returning the index of the next QR still to draw (for +// continuation on a later page) and the y-offset just below the drawn QR column (so callers +// can place content, e.g. a note, under the QR codes). function drawOnePageOfQrCodes( qrImages: HTMLCanvasElement[], doc: jsPDF, y: number, qrSize: number, - startIndex -): number { + startIndex: number +): { nextIndex: number; endY: number } { doc.setFont('helvetica'); let qrIndex: number = startIndex; for (; qrIndex < qrImages.length; qrIndex++) { const image = qrImages[qrIndex]; const textBuffer = 15; if (y + qrSize + textBuffer >= doc.internal.pageSize.getHeight()) { - return qrIndex; + return { nextIndex: qrIndex, endY: y }; } doc.addImage(image, left(0), y, qrSize, qrSize); if (qrImages.length === 1) { - return qrIndex + 1; + return { nextIndex: qrIndex + 1, endY: y + qrSize }; } y = moveDown(y, qrSize + textBuffer); @@ -83,7 +86,7 @@ function drawOnePageOfQrCodes( doc.text('Part ' + (qrIndex + 1).toString(), left(0), y); y = moveDown(y, 20); } - return qrIndex + 1; + return { nextIndex: qrIndex, endY: y }; } function computeKeyCardImageDimensions(keyCardImage: HTMLImageElement) { @@ -207,7 +210,21 @@ export async function drawKeycard({ qrImages.push(await QRCode.toCanvas(key, { errorCorrectionLevel: 'L' })); } - let nextQrIndex = drawOnePageOfQrCodes(qrImages, doc, y, qrSize, 0); + const isMultiPart = qr?.data?.length > QRBinaryMaxLength; + const { nextIndex, endY: qrColumnEndY } = drawOnePageOfQrCodes(qrImages, doc, y, qrSize, 0); + let nextQrIndex = nextIndex; + + // For a split key, place the "put all Parts together" note directly under the QR codes, + // wrapped to the QR column width so it stays in the left column and never overlaps the + // Data payload rendered on the right. + let qrColumnBottom = qrColumnEndY; + if (isMultiPart) { + doc.setFontSize(font.body - 2).setTextColor(color.darkgray); + const noteLines = doc.splitTextToSize('Note: you will need to put all Parts together for the full key', qrSize); + const noteY = moveDown(qrColumnEndY, 15); + doc.text(noteLines, left(0), noteY); + qrColumnBottom = noteY + noteLines.length * doc.getLineHeight(); + } doc.setFontSize(font.subheader).setTextColor(color.black); y = moveDown(y, 10); @@ -220,11 +237,6 @@ export async function drawKeycard({ doc.text(qr.description, textLeft, y); textHeight += doc.getLineHeight(); doc.setFontSize(font.body - 2); - if (qr?.data?.length > QRBinaryMaxLength) { - y = moveDown(y, 30); - textHeight += 30; - doc.text('Note: you will need to put all Parts together for the full key', textLeft, y); - } y = moveDown(y, 30); textHeight += 30; doc.text('Data:', textLeft, y); @@ -242,7 +254,11 @@ export async function drawKeycard({ textHeight = 0; y = 30; topY = y; - nextQrIndex = drawOnePageOfQrCodes(qrImages, doc, y, qrSize, nextQrIndex); + // Redraw remaining QRs on the new page and update the column bottom to THIS page, so + // the rowHeight math below stays same-page (avoids a stale page-1 coordinate). + const continued = drawOnePageOfQrCodes(qrImages, doc, y, qrSize, nextQrIndex); + nextQrIndex = continued.nextIndex; + qrColumnBottom = continued.endY; doc.setFont('courier').setFontSize(9).setTextColor(color.black); } doc.text(lines[line], textLeft, y); @@ -273,9 +289,10 @@ export async function drawKeycard({ } doc.setFont('helvetica'); - // Move down the size of the QR code minus accumulated height on the right side, plus margin - // if we have a key that spans multiple pages, then exclude QR code size - const rowHeight = Math.max(qr.data.length > QRBinaryMaxLength ? qrSize + 20 : qrSize, textHeight); + // Move down past the taller of the two columns: the right-side text (textHeight) or the + // left-side QR column (plus the note printed under it for split keys), then add a margin. + const qrColumnHeight = isMultiPart ? qrColumnBottom - topY : qrSize; + const rowHeight = Math.max(qrColumnHeight, textHeight); const marginBottom = 15; y = moveDown(y, rowHeight - (y - topY) + marginBottom); } diff --git a/modules/key-card/src/generateQrData.ts b/modules/key-card/src/generateQrData.ts index 5bf437770b..4258126357 100644 --- a/modules/key-card/src/generateQrData.ts +++ b/modules/key-card/src/generateQrData.ts @@ -5,13 +5,14 @@ import * as assert from 'assert'; import { GenerateLightningQrDataParams, GenerateQrDataParams, - GenerateVaultQrDataParams, + GenerateSafeQrDataParams, + KeycardEntity, MasterPublicKeyQrDataEntry, QrData, QrDataEntry, - VaultKeycardRoots, - VaultRootKeyType, - VAULT_ROOT_ORDER, + SafeKeycardRoots, + SafeRootKeyType, + SAFE_ROOT_ORDER, } from './types'; function getPubFromKey(key: Keychain): string | undefined { @@ -135,13 +136,15 @@ function generateUserMasterPublicKeyQRData(publicKey: string): MasterPublicKeyQr async function generatePasscodeQrData( passphrase: string, passcodeEncryptionCode: string, - encryptionVersion?: 1 | 2 + encryptionVersion?: 1 | 2, + entityNoun: KeycardEntity = 'wallet' ): Promise { - const encryptedWalletPasscode = await encrypt(passcodeEncryptionCode, passphrase, { encryptionVersion }); + const encryptedPasscode = await encrypt(passcodeEncryptionCode, passphrase, { encryptionVersion }); + const titleNoun = entityNoun === 'safe' ? 'Safe' : 'Wallet'; return { - title: 'D: Encrypted Wallet Password', - description: 'This is the wallet password, encrypted client-side with a key held by BitGo.', - data: encryptedWalletPasscode, + title: `D: Encrypted ${titleNoun} Password`, + description: `This is the ${entityNoun} password, encrypted client-side with a key held by BitGo.`, + data: encryptedPasscode, }; } @@ -220,61 +223,61 @@ export async function generateLightningQrData(params: GenerateLightningQrDataPar return qrData; } -function selectRootPrivateKey(keychain: Keychain, slot: VaultRootKeyType, role: 'user' | 'backup'): string { +function selectRootPrivateKey(keychain: Keychain, slot: SafeRootKeyType, role: 'user' | 'backup'): string { // Prefer the compact MPCv2 reduced share; fall back to encryptedPrv (e.g. multisig roots). const data = keychain.reducedEncryptedPrv ?? keychain.encryptedPrv; - assert.ok(data, `Vault ${role} root ${slot} is missing encrypted private key material`); + assert.ok(data, `Safe ${role} root ${slot} is missing encrypted private key material`); return data; } -function selectRootPublicKey(keychain: Keychain, slot: VaultRootKeyType): string { +function selectRootPublicKey(keychain: Keychain, slot: SafeRootKeyType): string { const pub = getPubFromKey(keychain); - assert.ok(pub, `Vault BitGo root ${slot} is missing a public key`); + assert.ok(pub, `Safe BitGo root ${slot} is missing a public key`); return pub; } /** * Serializes one box's four roots into a single JSON object keyed by rootKeyType, e.g. - * `{"secp256k1Multisig":"","ecdsaMpc":"",...}`. This keeps the vault + * `{"secp256k1Multisig":"","ecdsaMpc":"",...}`. This keeps the safe * keycard on the existing 4-box layout — one box (= one QR/data block) per role — with the * four roots packed into each box's data. `splitKeys` fragments the box if it exceeds the QR - * threshold, exactly as for a normal wallet keycard. Reversed by {@link parseVaultKeycardBox}. + * threshold, exactly as for a normal wallet keycard. Reversed by {@link parseSafeKeycardBox}. */ -function buildVaultBoxData( - roots: Record, - select: (root: KeychainsTriplet, slot: VaultRootKeyType) => string +function buildSafeBoxData( + roots: Record, + select: (root: KeychainsTriplet, slot: SafeRootKeyType) => string ): string { - const box: VaultKeycardRoots = {} as VaultKeycardRoots; - for (const slot of VAULT_ROOT_ORDER) { + const box: SafeKeycardRoots = {} as SafeKeycardRoots; + for (const slot of SAFE_ROOT_ORDER) { const root = roots[slot]; - assert.ok(root, `Vault is missing the ${slot} root`); + assert.ok(root, `Safe is missing the ${slot} root`); box[slot] = select(root, slot); } return JSON.stringify(box); } /** - * Builds vault keycard QR data in the existing wallet {@link QrData} shape: boxes A (user), + * Builds safe keycard QR data in the existing wallet {@link QrData} shape: boxes A (user), * B (backup), C (BitGo), D (passcode). Each of A/B/C carries a JSON object of the four roots, - * so the vault renders and parses through the same {@link drawKeycard} path as a wallet. + * so the safe renders and parses through the same {@link drawKeycard} path as a wallet. */ -export async function generateVaultQrData(params: GenerateVaultQrDataParams): Promise { +export async function generateSafeQrData(params: GenerateSafeQrDataParams): Promise { const { roots } = params; const qrData: QrData = { user: { title: 'A: User Key', - description: 'Your 4 root private keys, encrypted with your wallet password.', - data: buildVaultBoxData(roots, (root, slot) => selectRootPrivateKey(root.userKeychain, slot, 'user')), + description: 'Your 4 root private keys, encrypted with your safe password.', + data: buildSafeBoxData(roots, (root, slot) => selectRootPrivateKey(root.userKeychain, slot, 'user')), }, backup: { title: 'B: Backup Key', - description: 'Your 4 root backup keys, encrypted with your wallet password.', - data: buildVaultBoxData(roots, (root, slot) => selectRootPrivateKey(root.backupKeychain, slot, 'backup')), + description: 'Your 4 root backup keys, encrypted with your safe password.', + data: buildSafeBoxData(roots, (root, slot) => selectRootPrivateKey(root.backupKeychain, slot, 'backup')), }, bitgo: { title: 'C: BitGo Key', description: 'The public parts of the 4 root keys held by BitGo.', - data: buildVaultBoxData(roots, (root, slot) => selectRootPublicKey(root.bitgoKeychain, slot)), + data: buildSafeBoxData(roots, (root, slot) => selectRootPublicKey(root.bitgoKeychain, slot)), }, }; @@ -282,7 +285,8 @@ export async function generateVaultQrData(params: GenerateVaultQrDataParams): Pr qrData.passcode = await generatePasscodeQrData( params.passphrase, params.passcodeEncryptionCode, - params.encryptionVersion + params.encryptionVersion, + 'safe' ); } diff --git a/modules/key-card/src/index.ts b/modules/key-card/src/index.ts index 617a8c43b3..7c98d4c9f4 100644 --- a/modules/key-card/src/index.ts +++ b/modules/key-card/src/index.ts @@ -1,4 +1,4 @@ -import { generateLightningQrData, generateQrData, generateVaultQrData } from './generateQrData'; +import { generateLightningQrData, generateQrData, generateSafeQrData } from './generateQrData'; import { generateFaq, generateLightningFaq } from './faq'; import { drawKeycard } from './drawKeycard'; import { generateParamsForKeyCreation } from './generateParamsForKeyCreation'; @@ -6,7 +6,7 @@ import { GenerateKeycardParams, GenerateLightningQrDataParams, GenerateQrDataBaseParams, - GenerateVaultQrDataParams, + GenerateSafeQrDataParams, } from './types'; export * from './drawKeycard'; @@ -51,16 +51,14 @@ export async function generateLightningKeycard( } /** - * Generates a vault keycard using the existing 4-box layout: boxes A/B/C each carry a JSON - * object of the four roots (see {@link generateVaultQrData}), rendered through the same - * {@link drawKeycard} path as a wallet. Generated as part of vault creation, once all four + * Generates a safe keycard using the existing 4-box layout: boxes A/B/C each carry a JSON + * object of the four roots (see {@link generateSafeQrData}), rendered through the same + * {@link drawKeycard} path as a wallet. Generated as part of safe creation, once all four * root triplets exist. */ -export async function generateVaultKeycard( - params: GenerateQrDataBaseParams & GenerateVaultQrDataParams -): Promise { +export async function generateSafeKeycard(params: GenerateQrDataBaseParams & GenerateSafeQrDataParams): Promise { const questions = generateFaq(params.coin.fullName); - const qrData = await generateVaultQrData(params); + const qrData = await generateSafeQrData(params); const keycard = await drawKeycard({ ...params, questions, qrData, pageBreakBeforeIndices: [1, 2] }); const label = params.walletLabel || params.coin.fullName; keycard.save(`BitGo Keycard for ${label}.pdf`); diff --git a/modules/key-card/src/parseKeycard.ts b/modules/key-card/src/parseKeycard.ts index 98a972acfb..df2a9ad416 100644 --- a/modules/key-card/src/parseKeycard.ts +++ b/modules/key-card/src/parseKeycard.ts @@ -1,4 +1,4 @@ -import { VaultKeycardRoots, VAULT_ROOT_ORDER } from './types'; +import { SafeKeycardRoots, SAFE_ROOT_ORDER } from './types'; export type PDFTextNode = { text: string; @@ -14,28 +14,28 @@ export type KeycardEntry = { }; /** - * Parses a vault keycard box value — the JSON packed by `generateVaultQrData`, e.g. + * Parses a safe keycard box value — the JSON packed by `generateSafeQrData`, e.g. * `{"secp256k1Multisig":"…","ecdsaMpc":"…",…}` — into its four roots. Validates that all four * roots are present. Recovery tooling calls this on the A/B/C box value returned by * {@link parseKeycardFromLines}, then decrypts each root value with the wallet password. */ -export function parseVaultKeycardBox(data: string): VaultKeycardRoots { +export function parseSafeKeycardBox(data: string): SafeKeycardRoots { let parsed: unknown; try { parsed = JSON.parse(data); } catch { - throw new Error('parseVaultKeycardBox: value is not valid JSON'); + throw new Error('parseSafeKeycardBox: value is not valid JSON'); } if (typeof parsed !== 'object' || parsed === null) { - throw new Error('parseVaultKeycardBox: value is not an object'); + throw new Error('parseSafeKeycardBox: value is not an object'); } const roots = parsed as Record; - for (const slot of VAULT_ROOT_ORDER) { + for (const slot of SAFE_ROOT_ORDER) { if (typeof roots[slot] !== 'string') { - throw new Error(`parseVaultKeycardBox: missing or invalid root ${slot}`); + throw new Error(`parseSafeKeycardBox: missing or invalid root ${slot}`); } } - return parsed as VaultKeycardRoots; + return parsed as SafeKeycardRoots; } const sectionHeaderRegex = /^([A-D])\s*[:.)-]\s*(.+?)\s*$/i; @@ -73,7 +73,9 @@ function countChar(input: string, char: string): number { } function isEncryptedWalletPasswordSectionTitle(title: string): boolean { - return title.toLowerCase().includes('encrypted wallet password'); + const normalized = title.toLowerCase(); + // Box D is titled "Encrypted Wallet Password" (wallet) or "Encrypted Safe Password" (safe). + return normalized.includes('encrypted wallet password') || normalized.includes('encrypted safe password'); } /** diff --git a/modules/key-card/src/types.ts b/modules/key-card/src/types.ts index 79d2092eee..544a19780e 100644 --- a/modules/key-card/src/types.ts +++ b/modules/key-card/src/types.ts @@ -57,28 +57,31 @@ export interface GenerateLightningQrDataParams extends GenerateQrDataCoinParams } /** - * Identifier for one of a vault's four roots (one per signing scheme). Each value is the + * Identifier for one of a safe's four roots (one per signing scheme). Each value is the * root's `rootKeyType`, which is also the key used in the keycard's per-box JSON. */ -export type VaultRootKeyType = 'secp256k1Multisig' | 'ecdsaMpc' | 'eddsaMpc' | 'ed25519Multisig'; +export type SafeRootKeyType = 'secp256k1Multisig' | 'ecdsaMpc' | 'eddsaMpc' | 'ed25519Multisig'; /** - * Fixed render/scan order of the four roots on the vault keycard. Kept stable so a + * Fixed render/scan order of the four roots on the safe keycard. Kept stable so a * generated keycard and a re-scanned one line up slot-for-slot. */ -export const VAULT_ROOT_ORDER: VaultRootKeyType[] = ['secp256k1Multisig', 'ecdsaMpc', 'eddsaMpc', 'ed25519Multisig']; +export const SAFE_ROOT_ORDER: SafeRootKeyType[] = ['secp256k1Multisig', 'ecdsaMpc', 'eddsaMpc', 'ed25519Multisig']; /** - * The JSON object encoded in a vault keycard box (A/B/C): the four roots keyed by - * {@link VaultRootKeyType}. Values are per-root ciphertext for A/B (encryptedPrv or + * The JSON object encoded in a safe keycard box (A/B/C): the four roots keyed by + * {@link SafeRootKeyType}. Values are per-root ciphertext for A/B (encryptedPrv or * reducedEncryptedPrv) or public keys for C. The root-key-type keys are self-identifying, so a * consumer parses by key rather than by size/offset. */ -export type VaultKeycardRoots = Record; +export type SafeKeycardRoots = Record; -export interface GenerateVaultQrDataParams extends GenerateQrDataCoinParams { +/** The product a keycard belongs to, used for user-facing wording (e.g. Box D copy). */ +export type KeycardEntity = 'wallet' | 'safe'; + +export interface GenerateSafeQrDataParams extends GenerateQrDataCoinParams { // The four root triplets (user/backup/bitgo keychains), keyed by rootKeyType. - roots: Record; + roots: Record; } export type GenerateKeycardParams = GenerateQrDataBaseParams & diff --git a/modules/key-card/test/unit/vaultQrData.ts b/modules/key-card/test/unit/safeQrData.ts similarity index 68% rename from modules/key-card/test/unit/vaultQrData.ts rename to modules/key-card/test/unit/safeQrData.ts index 8375f53457..3d124060cd 100644 --- a/modules/key-card/test/unit/vaultQrData.ts +++ b/modules/key-card/test/unit/safeQrData.ts @@ -3,13 +3,13 @@ import * as assert from 'assert'; import { decrypt, encrypt } from '@bitgo/sdk-api'; import { coins } from '@bitgo/statics'; import { Keychain, KeychainsTriplet, KeyType } from '@bitgo/sdk-core'; -import { generateVaultQrData } from '../../src/generateQrData'; +import { generateSafeQrData } from '../../src/generateQrData'; import { splitKeys } from '../../src/utils'; import { QRBinaryMaxLength } from '../../src/drawKeycard'; -import { parseKeycardFromLines, parseVaultKeycardBox } from '../../src/parseKeycard'; -import { VaultRootKeyType, VAULT_ROOT_ORDER } from '../../src/types'; +import { parseKeycardFromLines, parseSafeKeycardBox } from '../../src/parseKeycard'; +import { SafeRootKeyType, SAFE_ROOT_ORDER } from '../../src/types'; -const passphrase = 'vault-keycard-round-trip'; +const passphrase = 'safe-keycard-round-trip'; // Courier chars per line when synthesizing the "Data:" block of a scanned PDF (see the parser // round-trip test); any value works — it only exercises line reassembly in parseKeycardFromLines. @@ -29,7 +29,7 @@ function makeKeychain(overrides: Partial): Keychain { * (`MPSUtil.generateEdDsaDKGKeyShares()[i].getReducedKeyShare()`) * Using the real lengths keeps the round-trip and QR-split assertions representative. */ -const ROOT_PLAINTEXT_LENGTHS: Record = { +const ROOT_PLAINTEXT_LENGTHS: Record = { secp256k1Multisig: 111, ecdsaMpc: 808, eddsaMpc: 548, @@ -38,20 +38,20 @@ const ROOT_PLAINTEXT_LENGTHS: Record = { // Distinct, correctly-sized plaintext per (slot, role) — the prefix makes each value unique; // padEnd brings it to the real length above. -function plaintextFor(slot: VaultRootKeyType, role: string): string { +function plaintextFor(slot: SafeRootKeyType, role: string): string { return `${slot}:${role}:`.padEnd(ROOT_PLAINTEXT_LENGTHS[slot], 'x'); } async function buildRoots(): Promise<{ - roots: Record; - plaintexts: Record; - bitgoPubs: Record; + roots: Record; + plaintexts: Record; + bitgoPubs: Record; }> { - const roots = {} as Record; - const plaintexts = {} as Record; - const bitgoPubs = {} as Record; + const roots = {} as Record; + const plaintexts = {} as Record; + const bitgoPubs = {} as Record; - for (const slot of VAULT_ROOT_ORDER) { + for (const slot of SAFE_ROOT_ORDER) { const isMpc = slot === 'ecdsaMpc' || slot === 'eddsaMpc'; const keyType: KeyType = isMpc ? 'tss' : 'independent'; const userPrv = plaintextFor(slot, 'user'); @@ -84,35 +84,35 @@ function reassemble(data: string): string { return splitKeys(data, QRBinaryMaxLength).join(''); } -describe('generateVaultQrData', function () { +describe('generateSafeQrData', function () { // Argon2id encryption of several root payloads is CPU-heavy. this.timeout(30000); it('produces the existing 4-box QrData shape, each box a JSON object of the 4 roots', async function () { const { roots } = await buildRoots(); - const qrData = await generateVaultQrData({ coin: coins.get('btc'), roots }); + const qrData = await generateSafeQrData({ coin: coins.get('btc'), roots }); qrData.user.title.should.equal('A: User Key'); qrData.backup?.title.should.equal('B: Backup Key'); qrData.bitgo?.title.should.equal('C: BitGo Key'); - // Each box parses via parseVaultKeycardBox into the four root slots, in order. + // Each box parses via parseSafeKeycardBox into the four root slots, in order. for (const box of [qrData.user, qrData.backup, qrData.bitgo]) { assert.ok(box); - Object.keys(parseVaultKeycardBox(box.data)).should.deepEqual([...VAULT_ROOT_ORDER]); + Object.keys(parseSafeKeycardBox(box.data)).should.deepEqual([...SAFE_ROOT_ORDER]); } }); it('round-trips: reassemble box -> JSON.parse -> decrypt all 4 user + backup roots', async function () { const { roots, plaintexts, bitgoPubs } = await buildRoots(); - const qrData = await generateVaultQrData({ coin: coins.get('btc'), roots }); + const qrData = await generateSafeQrData({ coin: coins.get('btc'), roots }); assert.ok(qrData.backup && qrData.bitgo); - const userBox = parseVaultKeycardBox(reassemble(qrData.user.data)); - const backupBox = parseVaultKeycardBox(reassemble(qrData.backup.data)); - const bitgoBox = parseVaultKeycardBox(reassemble(qrData.bitgo.data)); + const userBox = parseSafeKeycardBox(reassemble(qrData.user.data)); + const backupBox = parseSafeKeycardBox(reassemble(qrData.backup.data)); + const bitgoBox = parseSafeKeycardBox(reassemble(qrData.bitgo.data)); - for (const slot of VAULT_ROOT_ORDER) { + for (const slot of SAFE_ROOT_ORDER) { (await decrypt(passphrase, userBox[slot])).should.equal(plaintexts[slot].user); (await decrypt(passphrase, backupBox[slot])).should.equal(plaintexts[slot].backup); bitgoBox[slot].should.equal(bitgoPubs[slot]); @@ -121,8 +121,8 @@ describe('generateVaultQrData', function () { it('uses reducedEncryptedPrv for MPC roots and encryptedPrv for multisig roots', async function () { const { roots } = await buildRoots(); - const qrData = await generateVaultQrData({ coin: coins.get('btc'), roots }); - const userBox = parseVaultKeycardBox(qrData.user.data); + const qrData = await generateSafeQrData({ coin: coins.get('btc'), roots }); + const userBox = parseSafeKeycardBox(qrData.user.data); // The reduced (MPC) value must equal the keychain's reducedEncryptedPrv; multisig the encryptedPrv. userBox['ecdsaMpc'].should.equal(roots.ecdsaMpc.userKeychain.reducedEncryptedPrv); @@ -131,34 +131,37 @@ describe('generateVaultQrData', function () { userBox['ed25519Multisig'].should.equal(roots.ed25519Multisig.userKeychain.encryptedPrv); }); - it('includes box D when passphrase + passcodeEncryptionCode are provided', async function () { + it('includes box D with safe wording when passphrase + passcodeEncryptionCode are provided', async function () { const { roots } = await buildRoots(); - const qrData = await generateVaultQrData({ + const qrData = await generateSafeQrData({ coin: coins.get('btc'), roots, - passphrase: 'wallet-pw', + passphrase: 'safe-pw', passcodeEncryptionCode: '123456', }); assert.ok(qrData.passcode); - qrData.passcode.title.should.equal('D: Encrypted Wallet Password'); - (await decrypt('123456', qrData.passcode.data)).should.equal('wallet-pw'); + qrData.passcode.title.should.equal('D: Encrypted Safe Password'); + qrData.passcode.description.should.equal( + 'This is the safe password, encrypted client-side with a key held by BitGo.' + ); + (await decrypt('123456', qrData.passcode.data)).should.equal('safe-pw'); }); it('throws when a user root is missing private key material', async function () { const { roots } = await buildRoots(); roots.eddsaMpc.userKeychain = makeKeychain({ type: 'tss' }); - await assert.rejects(() => generateVaultQrData({ coin: coins.get('btc'), roots }), /missing encrypted private key/); + await assert.rejects(() => generateSafeQrData({ coin: coins.get('btc'), roots }), /missing encrypted private key/); }); it('throws a clear error when an entire root triplet is missing', async function () { const { roots } = await buildRoots(); - delete (roots as Partial>).ecdsaMpc; - await assert.rejects(() => generateVaultQrData({ coin: coins.get('btc'), roots }), /missing the ecdsaMpc root/); + delete (roots as Partial>).ecdsaMpc; + await assert.rejects(() => generateSafeQrData({ coin: coins.get('btc'), roots }), /missing the ecdsaMpc root/); }); it('recovers all 4 roots through the existing PDF-line parser + JSON.parse', async function () { const { roots, plaintexts } = await buildRoots(); - const qrData = await generateVaultQrData({ coin: coins.get('btc'), roots }); + const qrData = await generateSafeQrData({ coin: coins.get('btc'), roots }); assert.ok(qrData.backup && qrData.bitgo); // Synthesize the text lines a PDF scan of a wallet-format keycard produces. @@ -180,15 +183,15 @@ describe('generateVaultQrData', function () { const entries = parseKeycardFromLines(lines); const userEntry = entries.find((e) => e.label.startsWith('A')); assert.ok(userEntry); - const userBox = parseVaultKeycardBox(userEntry.value); - for (const slot of VAULT_ROOT_ORDER) { + const userBox = parseSafeKeycardBox(userEntry.value); + for (const slot of SAFE_ROOT_ORDER) { (await decrypt(passphrase, userBox[slot])).should.equal(plaintexts[slot].user); } }); - it('parseVaultKeycardBox rejects malformed / incomplete box data', function () { - assert.throws(() => parseVaultKeycardBox('not json'), /not valid JSON/); - assert.throws(() => parseVaultKeycardBox('"a string"'), /not an object/); - assert.throws(() => parseVaultKeycardBox('{"secp256k1Multisig":"x"}'), /missing or invalid root/); + it('parseSafeKeycardBox rejects malformed / incomplete box data', function () { + assert.throws(() => parseSafeKeycardBox('not json'), /not valid JSON/); + assert.throws(() => parseSafeKeycardBox('"a string"'), /not an object/); + assert.throws(() => parseSafeKeycardBox('{"secp256k1Multisig":"x"}'), /missing or invalid root/); }); });