Skip to content

Commit

Permalink
Merge pull request #3947 from BitGo/WP-722-add-keycard-generation-for…
Browse files Browse the repository at this point in the history
…-key-creation

feat(key-card): add keycard generation for cold tss key creation
  • Loading branch information
alebusse authored Oct 3, 2023
2 parents 732584c + 810985e commit 5879c93
Show file tree
Hide file tree
Showing 10 changed files with 204 additions and 75 deletions.
42 changes: 22 additions & 20 deletions modules/key-card/src/drawKeycard.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import { jsPDF } from 'jspdf';
import * as QRCode from 'qrcode';
import { FAQ } from './faq';
import { QrData } from './generateQrData';
import { splitKeys } from './utils';
import { IDrawKeyCard } from './types';

enum KeyCurveName {
ed25519 = 'EDDSA',
secp256k1 = 'ECDSA',
bls = 'BLS',
}

// Max for Binary/Byte Data https://github.com/soldair/node-qrcode#qr-code-capacity
// the largest theoretically possible value is actually 2953 but the QR codes get so dense that scanning them with a
Expand Down Expand Up @@ -69,13 +74,8 @@ export async function drawKeycard({
keyCardImage,
qrData,
walletLabel,
}: {
activationCode?: string;
keyCardImage?: HTMLImageElement;
qrData: QrData;
questions: FAQ[];
walletLabel: string;
}): Promise<jsPDF> {
curve,
}: IDrawKeyCard): Promise<jsPDF> {
// document details
const width = 8.5 * 72;
let y = 0;
Expand All @@ -100,7 +100,7 @@ export async function drawKeycard({
}
doc.setFontSize(font.header).setTextColor(color.black);
y = moveDown(y, 25);
doc.text('KeyCard', left(325), y - 1);
doc.text('KeyCard', left(curve && !activationCode ? 460 : 325), y - 1);
if (activationCode) {
doc.setFontSize(font.header).setTextColor(color.gray);
doc.text(activationCode, left(460), y);
Expand All @@ -111,21 +111,23 @@ export async function drawKeycard({
const date = new Date().toDateString();
y = moveDown(y, margin);
doc.setFontSize(font.body).setTextColor(color.gray);
doc.text('Created on ' + date + ' for wallet named:', left(0), y);
const title = curve ? KeyCurveName[curve] + ' key:' : 'wallet named:';
doc.text('Created on ' + date + ' for ' + title, left(0), y);
// copy
y = moveDown(y, 25);
doc.setFontSize(font.subheader).setTextColor(color.black);
doc.text(walletLabel, left(0), y);
// Red Bar
y = moveDown(y, 20);
doc.setFillColor(255, 230, 230);
doc.rect(left(0), y, width - 2 * margin, 32, 'F');

// warning message
y = moveDown(y, 20);
doc.setFontSize(font.body).setTextColor(color.red);
doc.text('Print this document, or keep it securely offline. See below for FAQ.', left(75), y);
if (!curve) {
// Red Bar
y = moveDown(y, 20);
doc.setFillColor(255, 230, 230);
doc.rect(left(0), y, width - 2 * margin, 32, 'F');

// warning message
y = moveDown(y, 20);
doc.setFontSize(font.body).setTextColor(color.red);
doc.text('Print this document, or keep it securely offline. See below for FAQ.', left(75), y);
}
// Generate the first page's data for the backup PDF
y = moveDown(y, 35);
const qrSize = 130;
Expand Down
6 changes: 1 addition & 5 deletions modules/key-card/src/faq.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
export interface FAQ {
question: string;
// the answer to the question, already split into individual lines of text
answer: string[];
}
import { FAQ } from './types';

export function generateFaq(coinName: string): FAQ[] {
const sectionACoinSpecific = `The KeyCard contains important information which can be used to recover the ${coinName} `;
Expand Down
43 changes: 43 additions & 0 deletions modules/key-card/src/generateParamsForKeyCreation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import * as assert from 'assert';
import { GenerateQrDataBaseParams, GenerateQrDataForKeychainParams, IDrawKeyCard } from './types';

export function generateParamsForKeyCreation({
curve,
bitgoKeychain,
walletLabel,
keyCardImage,
}: GenerateQrDataForKeychainParams & GenerateQrDataBaseParams): IDrawKeyCard {
assert(bitgoKeychain.commonKeychain, 'bitgoKeychain.commonKeychain is required');
return {
walletLabel,
keyCardImage,
curve,
qrData: {
user: {
title: 'A: Common Keychain',
data: bitgoKeychain.commonKeychain,
description: 'This is the common pub which is the equivalent of xpub (public key)\r\nof the key generated',
},
bitgo: {
title: 'B: BitGo Key ID',
data: bitgoKeychain.id,
description:
'This is the identifier assigned to the key generated using which BitGo\r\ncan lookup BitGo key share.',
},
},
questions: [
{
question: 'What is the KeyCard?',
answer: [
'This key card contains information about the key id for the generated key.\r\nThis id can later be used to derive wallets.',
],
},
{
question: 'What should I do with it?',
answer: [
'Store this keycard for later use. The key ID is important to communicate with BitGo\r\nto derive wallets from the key.',
],
},
],
};
}
40 changes: 1 addition & 39 deletions modules/key-card/src/generateQrData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,45 +2,7 @@ import { BaseCoin } from '@bitgo/statics';
import { Keychain } from '@bitgo/sdk-core';
import { encrypt } from '@bitgo/sdk-api';
import * as assert from 'assert';

export interface GenerateQrDataParams {
// 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
backupKeyProvider?: string;
// The key id of the backup key, only used for cold keys
backupMasterKey?: 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<BaseCoin>;
// 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
userMasterKey?: string;
}

interface QrDataEntry {
data: string;
description: string;
title: string;
publicMasterKey?: string;
}

export interface QrData {
backup?: QrDataEntry;
bitgo?: QrDataEntry;
passcode?: QrDataEntry;
user: QrDataEntry;
}
import { GenerateQrDataParams, QrData, QrDataEntry } from './types';

function getPubFromKey(key: Keychain): string | undefined {
switch (key.type) {
Expand Down
27 changes: 17 additions & 10 deletions modules/key-card/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,29 @@
import { generateQrData, GenerateQrDataParams } from './generateQrData';
import jsPDF from 'jspdf';

import { generateQrData } from './generateQrData';
import { generateFaq } from './faq';
import { drawKeycard } from './drawKeycard';
import { generateParamsForKeyCreation } from './generateParamsForKeyCreation';
import { GenerateKeycardParams } from './types';

export * from './drawKeycard';
export * from './faq';
export * from './generateQrData';
export * from './utils';

export interface GenerateKeycardParams extends GenerateQrDataParams {
activationCode?: string;
keyCardImage?: HTMLImageElement;
walletLabel: string;
}
export * from './types';

export async function generateKeycard(params: GenerateKeycardParams): Promise<void> {
const questions = generateFaq(params.coin.fullName);
const qrData = generateQrData(params);
const keycard = await drawKeycard({ ...params, questions, qrData });
let keycard: jsPDF;
if ('coin' in params) {
const questions = generateFaq(params.coin.fullName);
const qrData = generateQrData(params);
keycard = await drawKeycard({ ...params, questions, qrData });
} else if ('curve' in params) {
const data = generateParamsForKeyCreation(params);
keycard = await drawKeycard(data);
} else {
throw new Error('Either curve or coin must be provided');
}
// Save the PDF on the user's browser
keycard.save(`BitGo Keycard for ${params.walletLabel}.pdf`);
}
72 changes: 72 additions & 0 deletions modules/key-card/src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { Keychain } from '@bitgo/sdk-core';
import { BaseCoin, KeyCurve } from '@bitgo/statics';

export interface GenerateQrDataBaseParams {
activationCode?: string;
keyCardImage?: HTMLImageElement;
walletLabel: string;
}

export interface GenerateQrDataForKeychainParams {
// The BitGo keychain as it is returned from the BitGo API upon creation
bitgoKeychain: Keychain;
// The curve used for the key
curve: KeyCurve;
}

export interface GenerateQrDataParams {
// 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
backupKeyProvider?: string;
// The key id of the backup key, only used for cold keys
backupMasterKey?: 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<BaseCoin>;
// 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
userMasterKey?: string;
}

export type GenerateKeycardParams = GenerateQrDataBaseParams & (GenerateQrDataForKeychainParams | GenerateQrDataParams);

export interface IDrawKeyCard {
activationCode?: string;
keyCardImage?: HTMLImageElement;
qrData: QrData;
questions: FAQ[];
walletLabel: string;
curve?: KeyCurve;
}

export interface FAQ {
question: string;
// the answer to the question, already split into individual lines of text
answer: string[];
}

export interface QrDataEntry {
data: string;
description: string;
title: string;
publicMasterKey?: string;
}

export interface QrData {
backup?: QrDataEntry;
bitgo?: QrDataEntry;
passcode?: QrDataEntry;
curve?: KeyCurve;
user: QrDataEntry;
}
27 changes: 27 additions & 0 deletions modules/key-card/test/unit/generateParamsForKeyCreation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Keychain } from '@bitgo/sdk-core';
import * as assert from 'assert';
import { generateParamsForKeyCreation } from '../../src/generateParamsForKeyCreation';
import { KeyCurve } from '@bitgo/statics';

describe('generateParamsForKeyCreation', function () {
it('should return the right params', async function () {
const bitgoKeychain: Keychain = {
id: 'randomId',
commonKeychain: 'random string',
type: 'tss',
};
const curve = KeyCurve.Ed25519;
const walletLabel = 'random key name';
const keyCardImage: HTMLImageElement = 'random image' as unknown as HTMLImageElement;

const result = generateParamsForKeyCreation({ bitgoKeychain, curve, walletLabel, keyCardImage });
assert(result);
assert(result.qrData.user);
assert(result.qrData.user.data === bitgoKeychain.commonKeychain);
assert(result.qrData.bitgo && result.qrData.bitgo.data === bitgoKeychain.id);
assert(result.questions && result.questions.length === 2);
assert(result.walletLabel === walletLabel);
assert(result.curve === curve);
assert(result.keyCardImage === keyCardImage);
});
});
Binary file added modules/web-demo/src/assets/images/bitgo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 17 additions & 1 deletion modules/web-demo/src/components/KeyCard/fixtures.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { generateKeycard } from '@bitgo/key-card';
import { coins } from '@bitgo/statics';
import { KeyCurve, coins } from '@bitgo/statics';
import { Keychain } from '@bitgo/sdk-core';

function downloadKeycardImage(coinFamily: string): Promise<HTMLImageElement> {
Expand Down Expand Up @@ -274,3 +274,19 @@ export async function downloadKeycardForSelfManagedHotAdvancedPolygonWallet() {
walletLabel: 'SMHA Polygon Wallet',
});
}

export async function downloadKeycardForSelfManagedColdEddsaKey() {
const bitgoKeychain: Keychain = {
id: '63e50a158312c00007bd35c89cc5fb1a',
type: 'tss',
commonKeychain:
'021150550261463d753e96647bd5debb05d9ef325f4368cc160bf03365e4dadc756416f1230ccc4a1f24eb4924395bced22400f63d4837d511a28da015728ca049',
};

await generateKeycard({
bitgoKeychain,
curve: KeyCurve.Ed25519,
walletLabel: 'My EdDSA Key',
keyCardImage: await downloadKeycardImage('bitgo'),
});
}
4 changes: 4 additions & 0 deletions modules/web-demo/src/components/KeyCard/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
downloadKeycardForHotEthTSSWallet,
downloadKeycardForHotLtcWallet,
downloadKeycardForSelfManagedHotAdvancedPolygonWallet,
downloadKeycardForSelfManagedColdEddsaKey,
} from '@components/KeyCard/fixtures';

const KeyCard = () => {
Expand All @@ -19,6 +20,9 @@ const KeyCard = () => {
<button onClick={downloadKeycardForSelfManagedHotAdvancedPolygonWallet}>
Download for Self Managed Hot Advanced Polygon Wallet
</button>
<button onClick={downloadKeycardForSelfManagedColdEddsaKey}>
Download for Self Managed Cold Eddsa Key
</button>
</React.Fragment>
);
};
Expand Down

0 comments on commit 5879c93

Please sign in to comment.