Skip to content
This repository was archived by the owner on Jul 25, 2021. It is now read-only.

Commit e2021fc

Browse files
committed
Add recovery setup
1 parent 35d0d7d commit e2021fc

File tree

6 files changed

+259
-78
lines changed

6 files changed

+259
-78
lines changed

src/background.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,14 @@ const pskSetup = async () => {
9393
}
9494
};
9595

96+
const pskRecovery = async () => {
97+
try {
98+
await PSK.recoverySetup();
99+
} catch (e) {
100+
log.error('failed to setup psk recovery', { errorType: `${(typeof e)}` }, e);
101+
}
102+
}
103+
96104
const pskOptions = async (url) => {
97105
try {
98106
await PSK.setOptions(url);
@@ -112,6 +120,9 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
112120
case 'psk_setup':
113121
pskSetup().then(() => alert('PSK setup was successfully!'), null);
114122
break;
123+
case 'psk_recovery':
124+
pskRecovery().then(() => alert('PSK recovery setup was successfully!'), null);
125+
break;
115126
case 'psk_options':
116127
pskOptions(msg.url).then(() => alert('PSK options was successfully!'), null);
117128
break;

src/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,4 @@ export const PSK_EXTENSION_IDENTIFIER = 'psk';
2626
export const BACKUP_KEY = 'backup_key';
2727
export const BD_ENDPOINT = 'bd_endpoint';
2828
export const DEFAULT_BD_ENDPOINT = 'http://localhost:8005';
29+
export const RECOVERY_KEY = 'recovery_key';

src/webauth_storage.ts

Lines changed: 148 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
11
import {base64ToByteArray, byteArrayToBase64, concatenate} from "./utils";
2-
import {BACKUP_KEY, BD_ENDPOINT, DEFAULT_BD_ENDPOINT, ivLength, keyExportFormat, PIN, saltLength} from "./constants";
2+
import {
3+
BACKUP_KEY,
4+
BD_ENDPOINT,
5+
DEFAULT_BD_ENDPOINT, ES256,
6+
ivLength,
7+
keyExportFormat,
8+
PIN,
9+
RECOVERY_KEY,
10+
saltLength
11+
} from "./constants";
312
import {getLogger} from "./logging";
4-
import {BackupKey} from "./webauthn_psk";
13+
import {BackupKey, RecoveryKey} from "./webauthn_psk";
514

615
const log = getLogger('auth_storage');
716

@@ -100,6 +109,81 @@ export class PSKStorage {
100109
});
101110
});
102111
};
112+
113+
public static async storeRecoveryKeys(recoveryKeys: RecoveryKey[]): Promise<void> {
114+
log.debug('Storing recovery keys');
115+
116+
// Export recoveryKeys
117+
const exportKeys = []
118+
for (let i = 0; i < recoveryKeys.length; i++) {
119+
const recKey = recoveryKeys[i];
120+
const expPrvKey = await exportKey(recKey.privKey);
121+
const expPubKey = await window.crypto.subtle.exportKey('jwk', recKey.pubKey);
122+
123+
const json = {
124+
credentialId: recKey.credentialId,
125+
pubKey: expPubKey,
126+
privKey: expPrvKey,
127+
delegationSignature: recKey.delegationSignature,
128+
}
129+
130+
exportKeys.push(json)
131+
}
132+
133+
let exportJSON = JSON.stringify(exportKeys);
134+
return new Promise<void>(async (res, rej) => {
135+
chrome.storage.local.set({[RECOVERY_KEY]: exportJSON}, () => {
136+
if (!!chrome.runtime.lastError) {
137+
log.error('Could not perform PSKStorage.storeRecoveryKeys', chrome.runtime.lastError.message);
138+
rej(chrome.runtime.lastError);
139+
return;
140+
} else {
141+
res();
142+
}
143+
});
144+
});
145+
}
146+
147+
public static async loadRecoveryKeys(rpId): Promise<RecoveryKey[]> {
148+
log.debug(`Loading recovery keys`);
149+
return new Promise<RecoveryKey[]>(async (res, rej) => {
150+
chrome.storage.local.get({[RECOVERY_KEY]: null}, async (resp) => {
151+
if (!!chrome.runtime.lastError) {
152+
log.error('Could not perform PSKStorage.loadRecoveryKeys', chrome.runtime.lastError.message);
153+
rej(chrome.runtime.lastError);
154+
return;
155+
}
156+
157+
if (resp[RECOVERY_KEY] == null) {
158+
log.warn(`No recovery keys found`);
159+
res([]);
160+
return;
161+
}
162+
163+
const exportJSON = await JSON.parse(resp[RECOVERY_KEY]);
164+
const recKeys = new Array<RecoveryKey>();
165+
for (let i = 0; i < exportJSON.length; ++i) {
166+
const json = exportJSON[i];
167+
const prvKey = await importKey(json.privKey);
168+
const pubKey = await window.crypto.subtle.importKey(
169+
'jwk',
170+
json.pubKey,
171+
{
172+
name: 'ECDSA',
173+
namedCurve: ES256,
174+
},
175+
true,
176+
['sign'],
177+
);
178+
179+
const recKey = new RecoveryKey(json.credId, pubKey, prvKey, json.sign);
180+
recKeys.push(recKey);
181+
}
182+
log.debug('Loaded recovery keys successfully');
183+
res(recKeys);
184+
});
185+
});
186+
}
103187
}
104188

105189
export class CredentialsMap {
@@ -194,35 +278,7 @@ export class PublicKeyCredentialSource {
194278
const _id = json.id;
195279
const _rpId = json.rpId;
196280
const _userHandle = json.userHandle;
197-
198-
const keyPayload = base64ToByteArray(json.privateKey);
199-
const saltByteLength = keyPayload[0];
200-
const ivByteLength = keyPayload[1];
201-
const keyAlgorithmByteLength = keyPayload[2];
202-
let offset = 3;
203-
const salt = keyPayload.subarray(offset, offset + saltByteLength);
204-
offset += saltByteLength;
205-
const iv = keyPayload.subarray(offset, offset + ivByteLength);
206-
offset += ivByteLength;
207-
const keyAlgorithmBytes = keyPayload.subarray(offset, offset + keyAlgorithmByteLength);
208-
offset += keyAlgorithmByteLength;
209-
const keyBytes = keyPayload.subarray(offset);
210-
211-
const wrappingKey = await getWrappingKey(PIN, salt);
212-
const wrapAlgorithm: AesGcmParams = {
213-
iv,
214-
name: 'AES-GCM',
215-
};
216-
const unwrappingKeyAlgorithm = JSON.parse(new TextDecoder().decode(keyAlgorithmBytes));
217-
const _privateKey = await window.crypto.subtle.unwrapKey(
218-
keyExportFormat,
219-
keyBytes,
220-
wrappingKey,
221-
wrapAlgorithm,
222-
unwrappingKeyAlgorithm,
223-
true,
224-
['sign'],
225-
);
281+
const _privateKey = await importKey(json.privateKey);
226282

227283
return new PublicKeyCredentialSource(_id, _privateKey, _rpId, _userHandle);
228284
}
@@ -246,41 +302,74 @@ export class PublicKeyCredentialSource {
246302
}
247303

248304
public async export(): Promise<any> {
249-
const salt = window.crypto.getRandomValues(new Uint8Array(saltLength));
250-
const wrappingKey = await getWrappingKey(PIN, salt);
251-
const iv = window.crypto.getRandomValues(new Uint8Array(ivLength));
252-
const wrapAlgorithm: AesGcmParams = {
253-
iv,
254-
name: 'AES-GCM',
255-
};
256-
257-
const wrappedKeyBuffer = await window.crypto.subtle.wrapKey(
258-
keyExportFormat,
259-
this.privateKey,
260-
wrappingKey,
261-
wrapAlgorithm,
262-
);
263-
const wrappedKey = new Uint8Array(wrappedKeyBuffer);
264-
const keyAlgorithm = new TextEncoder().encode(JSON.stringify(this.privateKey.algorithm));
265-
const payload = concatenate(
266-
Uint8Array.of(saltLength, ivLength, keyAlgorithm.length),
267-
salt,
268-
iv,
269-
keyAlgorithm,
270-
wrappedKey);
271-
272-
const json = {
305+
return {
273306
id: this.id,
274-
privateKey: byteArrayToBase64(payload),
307+
privateKey: await exportKey(this.privateKey),
275308
rpId: this.rpId,
276309
userHandle: this.userHandle,
277310
type: this.type
278-
}
279-
280-
return json;
311+
};
281312
}
282313
}
283314

315+
async function exportKey(key: CryptoKey): Promise<string> {
316+
const salt = window.crypto.getRandomValues(new Uint8Array(saltLength));
317+
const wrappingKey = await getWrappingKey(PIN, salt);
318+
const iv = window.crypto.getRandomValues(new Uint8Array(ivLength));
319+
const wrapAlgorithm: AesGcmParams = {
320+
iv,
321+
name: 'AES-GCM',
322+
};
323+
324+
const wrappedKeyBuffer = await window.crypto.subtle.wrapKey(
325+
keyExportFormat,
326+
key,
327+
wrappingKey,
328+
wrapAlgorithm,
329+
);
330+
const wrappedKey = new Uint8Array(wrappedKeyBuffer);
331+
const keyAlgorithm = new TextEncoder().encode(JSON.stringify(key.algorithm));
332+
const payload = concatenate(
333+
Uint8Array.of(saltLength, ivLength, keyAlgorithm.length),
334+
salt,
335+
iv,
336+
keyAlgorithm,
337+
wrappedKey);
338+
339+
return byteArrayToBase64(payload)
340+
}
341+
342+
async function importKey(rawKey: string): Promise<CryptoKey> {
343+
const keyPayload = base64ToByteArray(rawKey);
344+
const saltByteLength = keyPayload[0];
345+
const ivByteLength = keyPayload[1];
346+
const keyAlgorithmByteLength = keyPayload[2];
347+
let offset = 3;
348+
const salt = keyPayload.subarray(offset, offset + saltByteLength);
349+
offset += saltByteLength;
350+
const iv = keyPayload.subarray(offset, offset + ivByteLength);
351+
offset += ivByteLength;
352+
const keyAlgorithmBytes = keyPayload.subarray(offset, offset + keyAlgorithmByteLength);
353+
offset += keyAlgorithmByteLength;
354+
const keyBytes = keyPayload.subarray(offset);
355+
356+
const wrappingKey = await getWrappingKey(PIN, salt);
357+
const wrapAlgorithm: AesGcmParams = {
358+
iv,
359+
name: 'AES-GCM',
360+
};
361+
const unwrappingKeyAlgorithm = JSON.parse(new TextDecoder().decode(keyAlgorithmBytes));
362+
return await window.crypto.subtle.unwrapKey(
363+
keyExportFormat,
364+
keyBytes,
365+
wrappingKey,
366+
wrapAlgorithm,
367+
unwrappingKeyAlgorithm,
368+
true,
369+
['sign'],
370+
);
371+
}
372+
284373
const getWrappingKey = async (pin: string, salt: Uint8Array): Promise<CryptoKey> => {
285374
const enc = new TextEncoder();
286375
const derivationKey = await window.crypto.subtle.importKey(

src/webauthn_authenticator.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -200,12 +200,12 @@ export class Authenticator {
200200

201201
}
202202

203-
private static async generateAttestedCredentialData(credentialId: Uint8Array, keyPair: ICOSECompatibleKey): Promise<Uint8Array> {
203+
private static async generateAttestedCredentialData(credentialId: Uint8Array, publicKey: ICOSECompatibleKey): Promise<Uint8Array> {
204204
const aaguid = this.AAGUID.slice(0, 16);
205205
const credIdLen = new Uint8Array(2);
206206
credIdLen[0] = (credentialId.length >> 8) & 0xff;
207207
credIdLen[1] = credentialId.length & 0xff;
208-
const coseKey = await keyPair.toCOSE(keyPair.publicKey);
208+
const coseKey = await publicKey.toCOSE(publicKey.publicKey);
209209
const encodedKey = new Uint8Array(CBOR.encodeCanonical(coseKey));
210210

211211
const attestedCredentialDataLength = aaguid.length + credIdLen.length + credentialId.length + encodedKey.length;

src/webauthn_crypto.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export interface ICOSECompatibleKey {
1313

1414
export class ECDSA implements ICOSECompatibleKey {
1515
public algorithm: number
16-
public privateKey: CryptoKey
16+
public privateKey?: CryptoKey
1717
public publicKey?: CryptoKey
1818

1919
public static async fromKey(key: CryptoKey): Promise<ECDSA> {

0 commit comments

Comments
 (0)