Skip to content

Commit 54bf80a

Browse files
committed
chore: rename wallet vault keycard references to safe + update keycard copy/formatting
WCN-1193
1 parent fca07ce commit 54bf80a

6 files changed

Lines changed: 139 additions & 112 deletions

File tree

modules/key-card/src/drawKeycard.ts

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ enum KeyCurveName {
3030
export const QRBinaryMaxLength = 1500;
3131

3232
// Default page-break layout: start a new page before the 3rd box (index 2), matching the
33-
// historical wallet keycard. Callers (e.g. the vault keycard) may override with their own indices.
33+
// historical wallet keycard. Callers (e.g. the safe keycard) may override with their own indices.
3434
const DEFAULT_PAGE_BREAK_INDICES = [2];
3535

3636
const font = {
@@ -56,34 +56,37 @@ function moveDown(y: number, ydelta: number): number {
5656
return y + ydelta;
5757
}
5858

59+
// Draws QR codes down the left column, returning the index of the next QR still to draw (for
60+
// continuation on a later page) and the y-offset just below the drawn QR column (so callers
61+
// can place content, e.g. a note, under the QR codes).
5962
function drawOnePageOfQrCodes(
6063
qrImages: HTMLCanvasElement[],
6164
doc: jsPDF,
6265
y: number,
6366
qrSize: number,
64-
startIndex
65-
): number {
67+
startIndex: number
68+
): { nextIndex: number; endY: number } {
6669
doc.setFont('helvetica');
6770
let qrIndex: number = startIndex;
6871
for (; qrIndex < qrImages.length; qrIndex++) {
6972
const image = qrImages[qrIndex];
7073
const textBuffer = 15;
7174
if (y + qrSize + textBuffer >= doc.internal.pageSize.getHeight()) {
72-
return qrIndex;
75+
return { nextIndex: qrIndex, endY: y };
7376
}
7477

7578
doc.addImage(image, left(0), y, qrSize, qrSize);
7679

7780
if (qrImages.length === 1) {
78-
return qrIndex + 1;
81+
return { nextIndex: qrIndex + 1, endY: y + qrSize };
7982
}
8083

8184
y = moveDown(y, qrSize + textBuffer);
8285
doc.setFontSize(font.body).setTextColor(color.black);
8386
doc.text('Part ' + (qrIndex + 1).toString(), left(0), y);
8487
y = moveDown(y, 20);
8588
}
86-
return qrIndex + 1;
89+
return { nextIndex: qrIndex, endY: y };
8790
}
8891

8992
function computeKeyCardImageDimensions(keyCardImage: HTMLImageElement) {
@@ -207,7 +210,21 @@ export async function drawKeycard({
207210
qrImages.push(await QRCode.toCanvas(key, { errorCorrectionLevel: 'L' }));
208211
}
209212

210-
let nextQrIndex = drawOnePageOfQrCodes(qrImages, doc, y, qrSize, 0);
213+
const isMultiPart = qr?.data?.length > QRBinaryMaxLength;
214+
const { nextIndex, endY: qrColumnEndY } = drawOnePageOfQrCodes(qrImages, doc, y, qrSize, 0);
215+
let nextQrIndex = nextIndex;
216+
217+
// For a split key, place the "put all Parts together" note directly under the QR codes,
218+
// wrapped to the QR column width so it stays in the left column and never overlaps the
219+
// Data payload rendered on the right.
220+
let qrColumnBottom = qrColumnEndY;
221+
if (isMultiPart) {
222+
doc.setFontSize(font.body - 2).setTextColor(color.darkgray);
223+
const noteLines = doc.splitTextToSize('Note: you will need to put all Parts together for the full key', qrSize);
224+
const noteY = moveDown(qrColumnEndY, 15);
225+
doc.text(noteLines, left(0), noteY);
226+
qrColumnBottom = noteY + noteLines.length * doc.getLineHeight();
227+
}
211228

212229
doc.setFontSize(font.subheader).setTextColor(color.black);
213230
y = moveDown(y, 10);
@@ -220,11 +237,6 @@ export async function drawKeycard({
220237
doc.text(qr.description, textLeft, y);
221238
textHeight += doc.getLineHeight();
222239
doc.setFontSize(font.body - 2);
223-
if (qr?.data?.length > QRBinaryMaxLength) {
224-
y = moveDown(y, 30);
225-
textHeight += 30;
226-
doc.text('Note: you will need to put all Parts together for the full key', textLeft, y);
227-
}
228240
y = moveDown(y, 30);
229241
textHeight += 30;
230242
doc.text('Data:', textLeft, y);
@@ -242,7 +254,11 @@ export async function drawKeycard({
242254
textHeight = 0;
243255
y = 30;
244256
topY = y;
245-
nextQrIndex = drawOnePageOfQrCodes(qrImages, doc, y, qrSize, nextQrIndex);
257+
// Redraw remaining QRs on the new page and update the column bottom to THIS page, so
258+
// the rowHeight math below stays same-page (avoids a stale page-1 coordinate).
259+
const continued = drawOnePageOfQrCodes(qrImages, doc, y, qrSize, nextQrIndex);
260+
nextQrIndex = continued.nextIndex;
261+
qrColumnBottom = continued.endY;
246262
doc.setFont('courier').setFontSize(9).setTextColor(color.black);
247263
}
248264
doc.text(lines[line], textLeft, y);
@@ -273,9 +289,10 @@ export async function drawKeycard({
273289
}
274290

275291
doc.setFont('helvetica');
276-
// Move down the size of the QR code minus accumulated height on the right side, plus margin
277-
// if we have a key that spans multiple pages, then exclude QR code size
278-
const rowHeight = Math.max(qr.data.length > QRBinaryMaxLength ? qrSize + 20 : qrSize, textHeight);
292+
// Move down past the taller of the two columns: the right-side text (textHeight) or the
293+
// left-side QR column (plus the note printed under it for split keys), then add a margin.
294+
const qrColumnHeight = isMultiPart ? qrColumnBottom - topY : qrSize;
295+
const rowHeight = Math.max(qrColumnHeight, textHeight);
279296
const marginBottom = 15;
280297
y = moveDown(y, rowHeight - (y - topY) + marginBottom);
281298
}

modules/key-card/src/generateQrData.ts

Lines changed: 34 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,14 @@ import * as assert from 'assert';
55
import {
66
GenerateLightningQrDataParams,
77
GenerateQrDataParams,
8-
GenerateVaultQrDataParams,
8+
GenerateSafeQrDataParams,
9+
KeycardEntity,
910
MasterPublicKeyQrDataEntry,
1011
QrData,
1112
QrDataEntry,
12-
VaultKeycardRoots,
13-
VaultRootKeyType,
14-
VAULT_ROOT_ORDER,
13+
SafeKeycardRoots,
14+
SafeRootKeyType,
15+
SAFE_ROOT_ORDER,
1516
} from './types';
1617

1718
function getPubFromKey(key: Keychain): string | undefined {
@@ -135,13 +136,15 @@ function generateUserMasterPublicKeyQRData(publicKey: string): MasterPublicKeyQr
135136
async function generatePasscodeQrData(
136137
passphrase: string,
137138
passcodeEncryptionCode: string,
138-
encryptionVersion?: 1 | 2
139+
encryptionVersion?: 1 | 2,
140+
entityNoun: KeycardEntity = 'wallet'
139141
): Promise<QrDataEntry> {
140-
const encryptedWalletPasscode = await encrypt(passcodeEncryptionCode, passphrase, { encryptionVersion });
142+
const encryptedPasscode = await encrypt(passcodeEncryptionCode, passphrase, { encryptionVersion });
143+
const titleNoun = entityNoun === 'safe' ? 'Safe' : 'Wallet';
141144
return {
142-
title: 'D: Encrypted Wallet Password',
143-
description: 'This is the wallet password, encrypted client-side with a key held by BitGo.',
144-
data: encryptedWalletPasscode,
145+
title: `D: Encrypted ${titleNoun} Password`,
146+
description: `This is the ${entityNoun} password, encrypted client-side with a key held by BitGo.`,
147+
data: encryptedPasscode,
145148
};
146149
}
147150

@@ -220,69 +223,70 @@ export async function generateLightningQrData(params: GenerateLightningQrDataPar
220223
return qrData;
221224
}
222225

223-
function selectRootPrivateKey(keychain: Keychain, slot: VaultRootKeyType, role: 'user' | 'backup'): string {
226+
function selectRootPrivateKey(keychain: Keychain, slot: SafeRootKeyType, role: 'user' | 'backup'): string {
224227
// Prefer the compact MPCv2 reduced share; fall back to encryptedPrv (e.g. multisig roots).
225228
const data = keychain.reducedEncryptedPrv ?? keychain.encryptedPrv;
226-
assert.ok(data, `Vault ${role} root ${slot} is missing encrypted private key material`);
229+
assert.ok(data, `Safe ${role} root ${slot} is missing encrypted private key material`);
227230
return data;
228231
}
229232

230-
function selectRootPublicKey(keychain: Keychain, slot: VaultRootKeyType): string {
233+
function selectRootPublicKey(keychain: Keychain, slot: SafeRootKeyType): string {
231234
const pub = getPubFromKey(keychain);
232-
assert.ok(pub, `Vault BitGo root ${slot} is missing a public key`);
235+
assert.ok(pub, `Safe BitGo root ${slot} is missing a public key`);
233236
return pub;
234237
}
235238

236239
/**
237240
* Serializes one box's four roots into a single JSON object keyed by rootKeyType, e.g.
238-
* `{"secp256k1Multisig":"<encPrv>","ecdsaMpc":"<reducedEncPrv>",...}`. This keeps the vault
241+
* `{"secp256k1Multisig":"<encPrv>","ecdsaMpc":"<reducedEncPrv>",...}`. This keeps the safe
239242
* keycard on the existing 4-box layout — one box (= one QR/data block) per role — with the
240243
* four roots packed into each box's data. `splitKeys` fragments the box if it exceeds the QR
241-
* threshold, exactly as for a normal wallet keycard. Reversed by {@link parseVaultKeycardBox}.
244+
* threshold, exactly as for a normal wallet keycard. Reversed by {@link parseSafeKeycardBox}.
242245
*/
243-
function buildVaultBoxData(
244-
roots: Record<VaultRootKeyType, KeychainsTriplet>,
245-
select: (root: KeychainsTriplet, slot: VaultRootKeyType) => string
246+
function buildSafeBoxData(
247+
roots: Record<SafeRootKeyType, KeychainsTriplet>,
248+
select: (root: KeychainsTriplet, slot: SafeRootKeyType) => string
246249
): string {
247-
const box: VaultKeycardRoots = {} as VaultKeycardRoots;
248-
for (const slot of VAULT_ROOT_ORDER) {
250+
const box: SafeKeycardRoots = {} as SafeKeycardRoots;
251+
for (const slot of SAFE_ROOT_ORDER) {
249252
const root = roots[slot];
250-
assert.ok(root, `Vault is missing the ${slot} root`);
253+
assert.ok(root, `Safe is missing the ${slot} root`);
251254
box[slot] = select(root, slot);
252255
}
253256
return JSON.stringify(box);
254257
}
255258

256259
/**
257-
* Builds vault keycard QR data in the existing wallet {@link QrData} shape: boxes A (user),
260+
* Builds safe keycard QR data in the existing wallet {@link QrData} shape: boxes A (user),
258261
* B (backup), C (BitGo), D (passcode). Each of A/B/C carries a JSON object of the four roots,
259-
* so the vault renders and parses through the same {@link drawKeycard} path as a wallet.
262+
* so the safe renders and parses through the same {@link drawKeycard} path as a wallet.
260263
*/
261-
export async function generateVaultQrData(params: GenerateVaultQrDataParams): Promise<QrData> {
264+
export async function generateSafeQrData(params: GenerateSafeQrDataParams): Promise<QrData> {
262265
const { roots } = params;
263266
const qrData: QrData = {
264267
user: {
265268
title: 'A: User Key',
266-
description: 'Your 4 root private keys, encrypted with your wallet password.',
267-
data: buildVaultBoxData(roots, (root, slot) => selectRootPrivateKey(root.userKeychain, slot, 'user')),
269+
description: 'Your 4 root private keys, encrypted with your safe password.',
270+
data: buildSafeBoxData(roots, (root, slot) => selectRootPrivateKey(root.userKeychain, slot, 'user')),
268271
},
269272
backup: {
270273
title: 'B: Backup Key',
271-
description: 'Your 4 root backup keys, encrypted with your wallet password.',
272-
data: buildVaultBoxData(roots, (root, slot) => selectRootPrivateKey(root.backupKeychain, slot, 'backup')),
274+
description: 'Your 4 root backup keys, encrypted with your safe password.',
275+
data: buildSafeBoxData(roots, (root, slot) => selectRootPrivateKey(root.backupKeychain, slot, 'backup')),
273276
},
274277
bitgo: {
275278
title: 'C: BitGo Key',
276279
description: 'The public parts of the 4 root keys held by BitGo.',
277-
data: buildVaultBoxData(roots, (root, slot) => selectRootPublicKey(root.bitgoKeychain, slot)),
280+
data: buildSafeBoxData(roots, (root, slot) => selectRootPublicKey(root.bitgoKeychain, slot)),
278281
},
279282
};
280283

281284
if (params.passphrase && params.passcodeEncryptionCode) {
282285
qrData.passcode = await generatePasscodeQrData(
283286
params.passphrase,
284287
params.passcodeEncryptionCode,
285-
params.encryptionVersion
288+
params.encryptionVersion,
289+
'safe'
286290
);
287291
}
288292

modules/key-card/src/index.ts

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
import { generateLightningQrData, generateQrData, generateVaultQrData } from './generateQrData';
1+
import { generateLightningQrData, generateQrData, generateSafeQrData } from './generateQrData';
22
import { generateFaq, generateLightningFaq } from './faq';
33
import { drawKeycard } from './drawKeycard';
44
import { generateParamsForKeyCreation } from './generateParamsForKeyCreation';
55
import {
66
GenerateKeycardParams,
77
GenerateLightningQrDataParams,
88
GenerateQrDataBaseParams,
9-
GenerateVaultQrDataParams,
9+
GenerateSafeQrDataParams,
1010
} from './types';
1111

1212
export * from './drawKeycard';
@@ -51,16 +51,14 @@ export async function generateLightningKeycard(
5151
}
5252

5353
/**
54-
* Generates a vault keycard using the existing 4-box layout: boxes A/B/C each carry a JSON
55-
* object of the four roots (see {@link generateVaultQrData}), rendered through the same
56-
* {@link drawKeycard} path as a wallet. Generated as part of vault creation, once all four
54+
* Generates a safe keycard using the existing 4-box layout: boxes A/B/C each carry a JSON
55+
* object of the four roots (see {@link generateSafeQrData}), rendered through the same
56+
* {@link drawKeycard} path as a wallet. Generated as part of safe creation, once all four
5757
* root triplets exist.
5858
*/
59-
export async function generateVaultKeycard(
60-
params: GenerateQrDataBaseParams & GenerateVaultQrDataParams
61-
): Promise<void> {
59+
export async function generateSafeKeycard(params: GenerateQrDataBaseParams & GenerateSafeQrDataParams): Promise<void> {
6260
const questions = generateFaq(params.coin.fullName);
63-
const qrData = await generateVaultQrData(params);
61+
const qrData = await generateSafeQrData(params);
6462
const keycard = await drawKeycard({ ...params, questions, qrData, pageBreakBeforeIndices: [1, 2] });
6563
const label = params.walletLabel || params.coin.fullName;
6664
keycard.save(`BitGo Keycard for ${label}.pdf`);

modules/key-card/src/parseKeycard.ts

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { VaultKeycardRoots, VAULT_ROOT_ORDER } from './types';
1+
import { SafeKeycardRoots, SAFE_ROOT_ORDER } from './types';
22

33
export type PDFTextNode = {
44
text: string;
@@ -14,28 +14,28 @@ export type KeycardEntry = {
1414
};
1515

1616
/**
17-
* Parses a vault keycard box value — the JSON packed by `generateVaultQrData`, e.g.
17+
* Parses a safe keycard box value — the JSON packed by `generateSafeQrData`, e.g.
1818
* `{"secp256k1Multisig":"…","ecdsaMpc":"…",…}` — into its four roots. Validates that all four
1919
* roots are present. Recovery tooling calls this on the A/B/C box value returned by
2020
* {@link parseKeycardFromLines}, then decrypts each root value with the wallet password.
2121
*/
22-
export function parseVaultKeycardBox(data: string): VaultKeycardRoots {
22+
export function parseSafeKeycardBox(data: string): SafeKeycardRoots {
2323
let parsed: unknown;
2424
try {
2525
parsed = JSON.parse(data);
2626
} catch {
27-
throw new Error('parseVaultKeycardBox: value is not valid JSON');
27+
throw new Error('parseSafeKeycardBox: value is not valid JSON');
2828
}
2929
if (typeof parsed !== 'object' || parsed === null) {
30-
throw new Error('parseVaultKeycardBox: value is not an object');
30+
throw new Error('parseSafeKeycardBox: value is not an object');
3131
}
3232
const roots = parsed as Record<string, unknown>;
33-
for (const slot of VAULT_ROOT_ORDER) {
33+
for (const slot of SAFE_ROOT_ORDER) {
3434
if (typeof roots[slot] !== 'string') {
35-
throw new Error(`parseVaultKeycardBox: missing or invalid root ${slot}`);
35+
throw new Error(`parseSafeKeycardBox: missing or invalid root ${slot}`);
3636
}
3737
}
38-
return parsed as VaultKeycardRoots;
38+
return parsed as SafeKeycardRoots;
3939
}
4040

4141
const sectionHeaderRegex = /^([A-D])\s*[:.)-]\s*(.+?)\s*$/i;
@@ -73,7 +73,9 @@ function countChar(input: string, char: string): number {
7373
}
7474

7575
function isEncryptedWalletPasswordSectionTitle(title: string): boolean {
76-
return title.toLowerCase().includes('encrypted wallet password');
76+
const normalized = title.toLowerCase();
77+
// Box D is titled "Encrypted Wallet Password" (wallet) or "Encrypted Safe Password" (safe).
78+
return normalized.includes('encrypted wallet password') || normalized.includes('encrypted safe password');
7779
}
7880

7981
/**

modules/key-card/src/types.ts

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -57,28 +57,31 @@ export interface GenerateLightningQrDataParams extends GenerateQrDataCoinParams
5757
}
5858

5959
/**
60-
* Identifier for one of a vault's four roots (one per signing scheme). Each value is the
60+
* Identifier for one of a safe's four roots (one per signing scheme). Each value is the
6161
* root's `rootKeyType`, which is also the key used in the keycard's per-box JSON.
6262
*/
63-
export type VaultRootKeyType = 'secp256k1Multisig' | 'ecdsaMpc' | 'eddsaMpc' | 'ed25519Multisig';
63+
export type SafeRootKeyType = 'secp256k1Multisig' | 'ecdsaMpc' | 'eddsaMpc' | 'ed25519Multisig';
6464

6565
/**
66-
* Fixed render/scan order of the four roots on the vault keycard. Kept stable so a
66+
* Fixed render/scan order of the four roots on the safe keycard. Kept stable so a
6767
* generated keycard and a re-scanned one line up slot-for-slot.
6868
*/
69-
export const VAULT_ROOT_ORDER: VaultRootKeyType[] = ['secp256k1Multisig', 'ecdsaMpc', 'eddsaMpc', 'ed25519Multisig'];
69+
export const SAFE_ROOT_ORDER: SafeRootKeyType[] = ['secp256k1Multisig', 'ecdsaMpc', 'eddsaMpc', 'ed25519Multisig'];
7070

7171
/**
72-
* The JSON object encoded in a vault keycard box (A/B/C): the four roots keyed by
73-
* {@link VaultRootKeyType}. Values are per-root ciphertext for A/B (encryptedPrv or
72+
* The JSON object encoded in a safe keycard box (A/B/C): the four roots keyed by
73+
* {@link SafeRootKeyType}. Values are per-root ciphertext for A/B (encryptedPrv or
7474
* reducedEncryptedPrv) or public keys for C. The root-key-type keys are self-identifying, so a
7575
* consumer parses by key rather than by size/offset.
7676
*/
77-
export type VaultKeycardRoots = Record<VaultRootKeyType, string>;
77+
export type SafeKeycardRoots = Record<SafeRootKeyType, string>;
7878

79-
export interface GenerateVaultQrDataParams extends GenerateQrDataCoinParams {
79+
/** The product a keycard belongs to, used for user-facing wording (e.g. Box D copy). */
80+
export type KeycardEntity = 'wallet' | 'safe';
81+
82+
export interface GenerateSafeQrDataParams extends GenerateQrDataCoinParams {
8083
// The four root triplets (user/backup/bitgo keychains), keyed by rootKeyType.
81-
roots: Record<VaultRootKeyType, KeychainsTriplet>;
84+
roots: Record<SafeRootKeyType, KeychainsTriplet>;
8285
}
8386

8487
export type GenerateKeycardParams = GenerateQrDataBaseParams &

0 commit comments

Comments
 (0)