@bitgo-beta/key-card 0.0.1-beta.19 → 0.0.1-beta.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -5
- package/src/drawKeycard.ts +68 -50
- package/src/generateQrData.ts +8 -28
- package/test/unit/generateQrData.ts +8 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bitgo-beta/key-card",
|
|
3
|
-
"version": "0.0.1-beta.
|
|
3
|
+
"version": "0.0.1-beta.20",
|
|
4
4
|
"description": "key card generator for BitGo wallets",
|
|
5
5
|
"main": "./dist/src/index.js",
|
|
6
6
|
"types": "./dist/src/index.d.ts",
|
|
@@ -33,14 +33,14 @@
|
|
|
33
33
|
]
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@bitgo-beta/sdk-api": "1.1.1-beta.
|
|
37
|
-
"@bitgo-beta/sdk-core": "1.1.1-beta.
|
|
38
|
-
"@bitgo-beta/statics": "7.0.1-beta.
|
|
36
|
+
"@bitgo-beta/sdk-api": "1.1.1-beta.145",
|
|
37
|
+
"@bitgo-beta/sdk-core": "1.1.1-beta.145",
|
|
38
|
+
"@bitgo-beta/statics": "7.0.1-beta.145",
|
|
39
39
|
"jspdf": "^2.5.1",
|
|
40
40
|
"qrcode": "^1.5.1"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@types/qrcode": "1.5.0"
|
|
44
44
|
},
|
|
45
|
-
"gitHead": "
|
|
45
|
+
"gitHead": "66b7845703411ad8d39b044d9b6048af0499398c"
|
|
46
46
|
}
|
package/src/drawKeycard.ts
CHANGED
|
@@ -4,9 +4,11 @@ import { FAQ } from './faq';
|
|
|
4
4
|
import { QrData } from './generateQrData';
|
|
5
5
|
import { splitKeys } from './utils';
|
|
6
6
|
|
|
7
|
-
// Max for Binary/Byte Data https://
|
|
8
|
-
//
|
|
9
|
-
|
|
7
|
+
// Max for Binary/Byte Data https://github.com/soldair/node-qrcode#qr-code-capacity
|
|
8
|
+
// the largest theoretically possible value is actually 2953 but the QR codes get so dense that scanning them with a
|
|
9
|
+
// phone (off of a printed page) doesn't work anymore
|
|
10
|
+
// this limitation was chosen by trial and error
|
|
11
|
+
export const QRBinaryMaxLength = 1500;
|
|
10
12
|
|
|
11
13
|
const font = {
|
|
12
14
|
header: 24,
|
|
@@ -23,6 +25,38 @@ const color = {
|
|
|
23
25
|
|
|
24
26
|
const margin = 30;
|
|
25
27
|
|
|
28
|
+
// Helpers for data formatting / positioning on the paper
|
|
29
|
+
function left(x: number): number {
|
|
30
|
+
return margin + x;
|
|
31
|
+
}
|
|
32
|
+
function moveDown(y: number, ydelta: number): number {
|
|
33
|
+
return y + ydelta;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function drawOnePageOfQrCodes(qrImages: HTMLCanvasElement[], doc: jsPDF, y: number, qrSize: number, startIndex): number {
|
|
37
|
+
doc.setFont('helvetica');
|
|
38
|
+
let qrIndex: number = startIndex;
|
|
39
|
+
for (; qrIndex < qrImages.length; qrIndex++) {
|
|
40
|
+
const image = qrImages[qrIndex];
|
|
41
|
+
const textBuffer = 15;
|
|
42
|
+
if (y + qrSize + textBuffer >= doc.internal.pageSize.getHeight()) {
|
|
43
|
+
return qrIndex;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
doc.addImage(image, left(0), y, qrSize, qrSize);
|
|
47
|
+
|
|
48
|
+
if (qrImages.length === 1) {
|
|
49
|
+
return qrIndex + 1;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
y = moveDown(y, qrSize + textBuffer);
|
|
53
|
+
doc.setFontSize(font.body).setTextColor(color.black);
|
|
54
|
+
doc.text('Part ' + (qrIndex + 1).toString(), left(0), y);
|
|
55
|
+
y = moveDown(y, 20);
|
|
56
|
+
}
|
|
57
|
+
return qrIndex + 1;
|
|
58
|
+
}
|
|
59
|
+
|
|
26
60
|
export async function drawKeycard({ activationCode, questions, keyCardImage, qrData, walletLabel }: {
|
|
27
61
|
activationCode?: string;
|
|
28
62
|
keyCardImage?: HTMLImageElement;
|
|
@@ -34,18 +68,13 @@ export async function drawKeycard({ activationCode, questions, keyCardImage, qrD
|
|
|
34
68
|
const width = 8.5 * 72;
|
|
35
69
|
let y = 0;
|
|
36
70
|
|
|
37
|
-
// Helpers for data formatting / positioning on the paper
|
|
38
|
-
const left = (x: number) => margin + x;
|
|
39
|
-
const moveDown = (ydelta: number) => (y += ydelta);
|
|
40
|
-
|
|
41
71
|
// Create the PDF instance
|
|
42
|
-
|
|
43
72
|
const doc = new jsPDF('portrait', 'pt', 'letter'); // jshint ignore:line
|
|
44
73
|
doc.setFont('helvetica');
|
|
45
74
|
|
|
46
75
|
// PDF Header Area - includes the logo and company name
|
|
47
76
|
// This is data for the BitGo logo in the top left of the PDF
|
|
48
|
-
moveDown(30);
|
|
77
|
+
y = moveDown(y, 30);
|
|
49
78
|
|
|
50
79
|
if (keyCardImage) {
|
|
51
80
|
doc.addImage(keyCardImage, left(0), y, 303, 40);
|
|
@@ -53,12 +82,12 @@ export async function drawKeycard({ activationCode, questions, keyCardImage, qrD
|
|
|
53
82
|
|
|
54
83
|
// Activation Code
|
|
55
84
|
if (activationCode) {
|
|
56
|
-
moveDown(8);
|
|
85
|
+
y = moveDown(y, 8);
|
|
57
86
|
doc.setFontSize(font.body).setTextColor(color.gray);
|
|
58
87
|
doc.text('Activation Code', left(460), y);
|
|
59
88
|
}
|
|
60
89
|
doc.setFontSize(font.header).setTextColor(color.black);
|
|
61
|
-
moveDown(25);
|
|
90
|
+
y = moveDown(y, 25);
|
|
62
91
|
doc.text('KeyCard', left(325), y - 1);
|
|
63
92
|
if (activationCode) {
|
|
64
93
|
doc.setFontSize(font.header).setTextColor(color.gray);
|
|
@@ -68,25 +97,25 @@ export async function drawKeycard({ activationCode, questions, keyCardImage, qrD
|
|
|
68
97
|
// Subheader
|
|
69
98
|
// titles
|
|
70
99
|
const date = new Date().toDateString();
|
|
71
|
-
moveDown(margin);
|
|
100
|
+
y = moveDown(y, margin);
|
|
72
101
|
doc.setFontSize(font.body).setTextColor(color.gray);
|
|
73
102
|
doc.text('Created on ' + date + ' for wallet named:', left(0), y);
|
|
74
103
|
// copy
|
|
75
|
-
moveDown(25);
|
|
104
|
+
y = moveDown(y, 25);
|
|
76
105
|
doc.setFontSize(font.subheader).setTextColor(color.black);
|
|
77
106
|
doc.text(walletLabel, left(0), y);
|
|
78
107
|
// Red Bar
|
|
79
|
-
moveDown(20);
|
|
108
|
+
y = moveDown(y, 20);
|
|
80
109
|
doc.setFillColor(255, 230, 230);
|
|
81
110
|
doc.rect(left(0), y, width - 2 * margin, 32, 'F');
|
|
82
111
|
|
|
83
112
|
// warning message
|
|
84
|
-
moveDown(20);
|
|
113
|
+
y = moveDown(y, 20);
|
|
85
114
|
doc.setFontSize(font.body).setTextColor(color.red);
|
|
86
115
|
doc.text('Print this document, or keep it securely offline. See below for FAQ.', left(75), y);
|
|
87
116
|
|
|
88
117
|
// Generate the first page's data for the backup PDF
|
|
89
|
-
moveDown(35);
|
|
118
|
+
y = moveDown(y, 35);
|
|
90
119
|
const qrSize = 130;
|
|
91
120
|
|
|
92
121
|
const qrKeys = ['user', 'passcode', 'backup', 'bitgo'];
|
|
@@ -105,45 +134,35 @@ export async function drawKeycard({ activationCode, questions, keyCardImage, qrD
|
|
|
105
134
|
const textLeft = left(qrSize + 15);
|
|
106
135
|
let textHeight = 0;
|
|
107
136
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
// key is too long for one QR code
|
|
113
|
-
const keys = splitKeys(qr.data, QRBinaryMaxLength);
|
|
114
|
-
for (let keyIndex = 0; keyIndex < keys.length; keyIndex++) {
|
|
115
|
-
const image = await QRCode.toCanvas(keys[keyIndex], { errorCorrectionLevel: 'L' });
|
|
116
|
-
doc.addImage(image, left(0), y, qrSize, qrSize);
|
|
117
|
-
const textBuffer = 15;
|
|
118
|
-
moveDown(qrSize + textBuffer);
|
|
119
|
-
doc.setFontSize(font.body).setTextColor(color.black);
|
|
120
|
-
doc.text('Part ' + (keyIndex + 1).toString(), left(0), y);
|
|
121
|
-
moveDown(20);
|
|
122
|
-
}
|
|
123
|
-
y = topY;
|
|
137
|
+
const qrImages: HTMLCanvasElement[] = [];
|
|
138
|
+
const keys = splitKeys(qr.data, QRBinaryMaxLength);
|
|
139
|
+
for (const key of keys) {
|
|
140
|
+
qrImages.push(await QRCode.toCanvas(key, { errorCorrectionLevel: 'L' }));
|
|
124
141
|
}
|
|
125
142
|
|
|
143
|
+
let nextQrIndex = drawOnePageOfQrCodes(qrImages, doc, y, qrSize, 0);
|
|
144
|
+
|
|
126
145
|
doc.setFontSize(font.subheader).setTextColor(color.black);
|
|
127
|
-
moveDown(10);
|
|
146
|
+
y = moveDown(y, 10);
|
|
128
147
|
textHeight += 10;
|
|
129
148
|
doc.text(qr.title, textLeft, y);
|
|
130
149
|
textHeight += doc.getLineHeight();
|
|
131
|
-
moveDown(15);
|
|
150
|
+
y = moveDown(y, 15);
|
|
132
151
|
textHeight += 15;
|
|
133
152
|
doc.setFontSize(font.body).setTextColor(color.darkgray);
|
|
134
153
|
doc.text(qr.description, textLeft, y);
|
|
135
154
|
textHeight += doc.getLineHeight();
|
|
136
155
|
doc.setFontSize(font.body - 2);
|
|
137
156
|
if (qr?.data?.length > QRBinaryMaxLength) {
|
|
138
|
-
moveDown(30);
|
|
157
|
+
y = moveDown(y, 30);
|
|
139
158
|
textHeight += 30;
|
|
140
|
-
doc.text('Note: you will need to put
|
|
159
|
+
doc.text('Note: you will need to put all Parts together for the full key', textLeft, y);
|
|
141
160
|
}
|
|
142
|
-
moveDown(30);
|
|
161
|
+
y = moveDown(y, 30);
|
|
143
162
|
textHeight += 30;
|
|
144
163
|
doc.text('Data:', textLeft, y);
|
|
145
164
|
textHeight += doc.getLineHeight();
|
|
146
|
-
moveDown(15);
|
|
165
|
+
y = moveDown(y, 15);
|
|
147
166
|
textHeight += 15;
|
|
148
167
|
const width = 72 * 8.5 - textLeft - 30;
|
|
149
168
|
doc.setFont('courier').setFontSize(9).setTextColor(color.black);
|
|
@@ -156,10 +175,12 @@ export async function drawKeycard({ activationCode, questions, keyCardImage, qrD
|
|
|
156
175
|
textHeight = 0;
|
|
157
176
|
y = 30;
|
|
158
177
|
topY = y;
|
|
178
|
+
nextQrIndex = drawOnePageOfQrCodes(qrImages, doc, y, qrSize, nextQrIndex);
|
|
179
|
+
doc.setFont('courier').setFontSize(9).setTextColor(color.black);
|
|
159
180
|
}
|
|
160
181
|
doc.text(lines[line], textLeft, y);
|
|
161
182
|
if (line !== lines.length - 1) {
|
|
162
|
-
moveDown(buffer);
|
|
183
|
+
y = moveDown(y, buffer);
|
|
163
184
|
textHeight += buffer;
|
|
164
185
|
}
|
|
165
186
|
}
|
|
@@ -169,7 +190,7 @@ export async function drawKeycard({ activationCode, questions, keyCardImage, qrD
|
|
|
169
190
|
const text = 'Key Id: ' + qr.publicMasterKey;
|
|
170
191
|
|
|
171
192
|
// Gray bar
|
|
172
|
-
moveDown(20);
|
|
193
|
+
y = moveDown(y, 20);
|
|
173
194
|
textHeight += 20;
|
|
174
195
|
doc.setFillColor(247, 249, 249); // Gray background
|
|
175
196
|
doc.setDrawColor(0, 0, 0); // Border
|
|
@@ -187,12 +208,9 @@ export async function drawKeycard({ activationCode, questions, keyCardImage, qrD
|
|
|
187
208
|
doc.setFont('helvetica');
|
|
188
209
|
// Move down the size of the QR code minus accumulated height on the right side, plus margin
|
|
189
210
|
// if we have a key that spans multiple pages, then exclude QR code size
|
|
190
|
-
|
|
191
|
-
if (qr?.data?.length > QRBinaryMaxLength) {
|
|
192
|
-
rowHeight = textHeight;
|
|
193
|
-
}
|
|
211
|
+
const rowHeight = Math.max(qr.data.length > QRBinaryMaxLength ? qrSize + 20 : qrSize, textHeight);
|
|
194
212
|
const marginBottom = 15;
|
|
195
|
-
moveDown(rowHeight - (y - topY) + marginBottom);
|
|
213
|
+
y = moveDown(y, rowHeight - (y - topY) + marginBottom);
|
|
196
214
|
}
|
|
197
215
|
|
|
198
216
|
// Add next Page
|
|
@@ -200,22 +218,22 @@ export async function drawKeycard({ activationCode, questions, keyCardImage, qrD
|
|
|
200
218
|
|
|
201
219
|
// next pages title
|
|
202
220
|
y = 0;
|
|
203
|
-
moveDown(55);
|
|
221
|
+
y = moveDown(y, 55);
|
|
204
222
|
doc.setFontSize(font.header).setTextColor(color.black);
|
|
205
223
|
doc.text('BitGo KeyCard FAQ', left(0), y);
|
|
206
224
|
|
|
207
225
|
// Generate the second page's data for the backup PDF
|
|
208
|
-
moveDown(30);
|
|
226
|
+
y = moveDown(y, 30);
|
|
209
227
|
questions.forEach(function (q) {
|
|
210
228
|
doc.setFontSize(font.subheader).setTextColor(color.black);
|
|
211
229
|
doc.text(q.question, left(0), y);
|
|
212
|
-
moveDown(20);
|
|
230
|
+
y = moveDown(y, 20);
|
|
213
231
|
doc.setFontSize(font.body).setTextColor(color.darkgray);
|
|
214
232
|
q.answer.forEach(function (line) {
|
|
215
233
|
doc.text(line, left(0), y);
|
|
216
|
-
moveDown(font.body + 3);
|
|
234
|
+
y = moveDown(y, font.body + 3);
|
|
217
235
|
});
|
|
218
|
-
moveDown(22);
|
|
236
|
+
y = moveDown(y, 22);
|
|
219
237
|
});
|
|
220
238
|
|
|
221
239
|
return doc;
|
package/src/generateQrData.ts
CHANGED
|
@@ -90,13 +90,13 @@ function generateBackupQrData(coin: Readonly<BaseCoin>, backupKeychain: Keychain
|
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
if (backupKeyProvider === 'BitGo Trust' && backupKeychain.type === 'tss') {
|
|
93
|
-
const
|
|
94
|
-
assert(
|
|
93
|
+
const keyShares = backupKeychain.keyShares?.filter((keyShare) => keyShare.to === 'backup');
|
|
94
|
+
assert(keyShares?.length === 2);
|
|
95
95
|
return {
|
|
96
|
-
title: 'B:
|
|
97
|
-
description: `
|
|
98
|
-
`
|
|
99
|
-
data: JSON.stringify(
|
|
96
|
+
title: 'B: Backup Key Shares',
|
|
97
|
+
description: `These are the key shares for ${backupKeyProvider}. If BitGo Inc. goes out of business,\r\n` +
|
|
98
|
+
`contact ${backupKeyProvider} and they will help you recover your funds.`,
|
|
99
|
+
data: JSON.stringify(keyShares),
|
|
100
100
|
};
|
|
101
101
|
}
|
|
102
102
|
|
|
@@ -120,24 +120,7 @@ function generateBackupQrData(coin: Readonly<BaseCoin>, backupKeychain: Keychain
|
|
|
120
120
|
};
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
-
function generateBitGoQrData(bitgoKeychain: Keychain
|
|
124
|
-
backupKeychain,
|
|
125
|
-
backupKeyProvider,
|
|
126
|
-
}: {
|
|
127
|
-
backupKeychain?: Keychain,
|
|
128
|
-
backupKeyProvider?: string;
|
|
129
|
-
}): QrDataEntry {
|
|
130
|
-
if (backupKeyProvider === 'BitGo Trust' && backupKeychain?.type === 'tss') {
|
|
131
|
-
const bitgoToBackupShare = backupKeychain.keyShares?.find((keyShare) => keyShare.from === 'bitgo' && keyShare.to === 'backup');
|
|
132
|
-
assert(bitgoToBackupShare);
|
|
133
|
-
return {
|
|
134
|
-
title: 'C: BitGo To Backup Key Share',
|
|
135
|
-
description: `This is the key share from BitGo Inc for ${backupKeyProvider}. If BitGo Inc goes out of\r\nbusiness,` +
|
|
136
|
-
` contact ${backupKeyProvider} and they will help you recover your funds.`,
|
|
137
|
-
data: JSON.stringify(bitgoToBackupShare),
|
|
138
|
-
};
|
|
139
|
-
}
|
|
140
|
-
|
|
123
|
+
function generateBitGoQrData(bitgoKeychain: Keychain): QrDataEntry {
|
|
141
124
|
const bitgoData = getPubFromKey(bitgoKeychain);
|
|
142
125
|
assert(bitgoData);
|
|
143
126
|
|
|
@@ -167,10 +150,7 @@ export function generateQrData({
|
|
|
167
150
|
backupKeyProvider,
|
|
168
151
|
backupMasterKey,
|
|
169
152
|
}),
|
|
170
|
-
bitgo: generateBitGoQrData(bitgoKeychain,
|
|
171
|
-
backupKeychain,
|
|
172
|
-
backupKeyProvider,
|
|
173
|
-
}),
|
|
153
|
+
bitgo: generateBitGoQrData(bitgoKeychain),
|
|
174
154
|
};
|
|
175
155
|
|
|
176
156
|
if (passphrase && passcodeEncryptionCode) {
|
|
@@ -179,6 +179,7 @@ describe('generateQrData', function () {
|
|
|
179
179
|
const backupKeyProvider = 'BitGo Trust';
|
|
180
180
|
const userToBackupKeyShare: ApiKeyShare = { from: 'user', to: 'backup', publicShare: 'userToBackupPublic', privateShare: 'userToBackupPrivate' };
|
|
181
181
|
const bitgoToBackupKeyShare: ApiKeyShare = { from: 'bitgo', to: 'backup', publicShare: 'bitgoToBackupPublic', privateShare: 'bitgoToBackupPrivate' };
|
|
182
|
+
const bitgoCommonKeychain = 'commonCommonBitGo';
|
|
182
183
|
const qrData = generateQrData({
|
|
183
184
|
backupKeychain: createKeychain({
|
|
184
185
|
provider,
|
|
@@ -188,6 +189,7 @@ describe('generateQrData', function () {
|
|
|
188
189
|
backupKeyProvider,
|
|
189
190
|
bitgoKeychain: createKeychain({
|
|
190
191
|
type: 'tss',
|
|
192
|
+
commonKeychain: bitgoCommonKeychain,
|
|
191
193
|
}),
|
|
192
194
|
coin,
|
|
193
195
|
userKeychain: createKeychain({
|
|
@@ -199,13 +201,13 @@ describe('generateQrData', function () {
|
|
|
199
201
|
qrData.user.data.should.equal(userEncryptedPrv);
|
|
200
202
|
|
|
201
203
|
assert(qrData.backup);
|
|
202
|
-
qrData.backup.title.should.equal('B:
|
|
203
|
-
qrData.backup.description.should.equal(`
|
|
204
|
-
qrData.backup.data.should.equal(JSON.stringify(userToBackupKeyShare));
|
|
204
|
+
qrData.backup.title.should.equal('B: Backup Key Shares');
|
|
205
|
+
qrData.backup.description.should.equal(`These are the key shares for ${backupKeyProvider}. If BitGo Inc. goes out of business,\r\ncontact ${backupKeyProvider} and they will help you recover your funds.`);
|
|
206
|
+
qrData.backup.data.should.equal(JSON.stringify([userToBackupKeyShare, bitgoToBackupKeyShare]));
|
|
205
207
|
|
|
206
208
|
assert(qrData.bitgo);
|
|
207
|
-
qrData.bitgo.title.should.equal('C: BitGo
|
|
208
|
-
qrData.bitgo.description.should.equal(
|
|
209
|
-
qrData.bitgo.data.should.equal(
|
|
209
|
+
qrData.bitgo.title.should.equal('C: BitGo Public Key');
|
|
210
|
+
qrData.bitgo.description.should.equal('This is the public part of the key that BitGo will use to co-sign transactions\r\nwith you on your wallet.');
|
|
211
|
+
qrData.bitgo.data.should.equal(bitgoCommonKeychain);
|
|
210
212
|
});
|
|
211
213
|
});
|