Skip to content

Commit

Permalink
restore recovery key from human-readable form
Browse files Browse the repository at this point in the history
  • Loading branch information
overheadhunter committed May 10, 2024
1 parent c229fdd commit b46f55e
Show file tree
Hide file tree
Showing 2 changed files with 61 additions and 6 deletions.
32 changes: 26 additions & 6 deletions frontend/src/common/universalVaultFormat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,12 +98,12 @@ export class RecoveryKey {
}

/**
* Loads the public key of the recovery key pair.
* Imports the public key of the recovery key pair.
* @param publicKey the DER-encoded public key
* @param publicKey the PKCS8-encoded private key
* @returns recovery key for encrypting vault metadata
*/
public static async load(publicKey: CryptoKey | Uint8Array, privateKey?: CryptoKey | Uint8Array): Promise<RecoveryKey> {
public static async import(publicKey: CryptoKey | Uint8Array, privateKey?: CryptoKey | Uint8Array): Promise<RecoveryKey> {
if (publicKey instanceof Uint8Array) {
publicKey = await crypto.subtle.importKey('spki', publicKey, RecoveryKey.KEY_DESIGNATION, true, []);
}
Expand All @@ -118,8 +118,28 @@ export class RecoveryKey {
* @param recoveryKey the encoded recovery key
* @returns complete recovery key for decrypting vault metadata
*/
public static recover(recoveryKey: string) {
// TODO
public static async recover(recoveryKey: string): Promise<RecoveryKey> {
// decode and check recovery key:
const decoded = wordEncoder.decode(recoveryKey);
const paddingLength = decoded[decoded.length - 1];
if (paddingLength > 0x03) {
throw new Error('Invalid padding');
}
const unpadded = decoded.subarray(0, -paddingLength);
const checksum = unpadded.subarray(-2);
const rawkey = unpadded.subarray(0, -2);
const crc32 = CRC32.compute(rawkey);
if (checksum[0] !== (crc32 & 0xFF)
|| checksum[1] !== (crc32 >> 8 & 0xFF)) {
throw new Error('Invalid recovery key checksum.');
}

// construct new RecoveryKey from recovered key
const privateKey = await crypto.subtle.importKey('pkcs8', rawkey, RecoveryKey.KEY_DESIGNATION, true, RecoveryKey.KEY_USAGES);
const jwk = await crypto.subtle.exportKey('jwk', privateKey);
delete jwk.d; // remove private part
const publicKey = await crypto.subtle.importKey('jwk', jwk, RecoveryKey.KEY_DESIGNATION, true, []);
return new RecoveryKey(publicKey, privateKey);
}

/**
Expand Down Expand Up @@ -292,9 +312,9 @@ export class UniversalVaultFormat implements AccessTokenProducing, VaultTemplate
const metadata = await VaultMetadata.decryptWithMemberKey(vault.uvfMetadataFile, memberKey);
let recoveryKey: RecoveryKey;
if (payload.recoveryKey) {
recoveryKey = await RecoveryKey.load(base64.parse(vault.uvfRecoveryPublicKey), base64.parse(payload.recoveryKey));
recoveryKey = await RecoveryKey.import(base64.parse(vault.uvfRecoveryPublicKey), base64.parse(payload.recoveryKey));
} else {
recoveryKey = await RecoveryKey.load(base64.parse(vault.uvfRecoveryPublicKey));
recoveryKey = await RecoveryKey.import(base64.parse(vault.uvfRecoveryPublicKey));
}
return new UniversalVaultFormat(metadata, memberKey, recoveryKey);
}
Expand Down
35 changes: 35 additions & 0 deletions frontend/test/common/universalVaultFormat.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,41 @@ describe('UVF', () => {
expect(recoveryKey).to.be.not.null;
});

it('recover() succeeds for valid recovery key', async () => {
const serialized = `cult hold all away buck do law relaxed other stimulus all bank fit indulge dad any ear grey cult golf
all baby dig war linear tour sleep humanity threat question neglect stance radar bank coup misery painter tragedy buddy
compare winter national approval budget deep screen outdoor audience tear stream cure type ugly chamber supporter franchise
accept sexy ad imply being drug doctor regime where thick dam training grass chamber domestic dictator educate sigh music spoken
connected measure voice lemon pig comprise disturb appear greatly satisfied heat news curiosity top impress nor method reflect
lesson recommend dual revenge thorough bus count broadband living riot prejudice target blonde excess company thereby tribe
respond horror mere way proud shopping wise liver mortgage plastic gentleman eighteen terms worry melt`;

const recoveryKey = await RecoveryKey.recover(serialized);

return Promise.all([
expect(recoveryKey.serializePublicKey()).to.eventually.eq('MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAESzrRXmyI8VWFJg1dPUNbFcc9jZvjZEfH7ulKI1UkXAltd7RGWrcfFxqyGPcwu6AQhHUag3OvDzEr0uUQND4PXHQTXP5IDGdYhJhL+WLKjnGjQAw0rNGy5V29+aV+yseW'),
expect(recoveryKey.serializePrivateKey()).to.eventually.eq('MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDDCi4K1Ts3DgTz/ufkLX7EGMHjGpJv+WJmFgyzLwwaDFSfLpDw0Kgf3FKK+LAsV8r+hZANiAARLOtFebIjxVYUmDV09Q1sVxz2Nm+NkR8fu6UojVSRcCW13tEZatx8XGrIY9zC7oBCEdRqDc68PMSvS5RA0Pg9cdBNc/kgMZ1iEmEv5YsqOcaNADDSs0bLlXb35pX7Kx5Y=')
]);
});

it('recover() fails for invalid recovery key', async () => {
const notInDict = RecoveryKey.recover('hallo bonjour');
const invalidPadding = RecoveryKey.recover('cult hold all away buck do law relaxed other stimulus');
const invalidCrc = RecoveryKey.recover(`wrong hold all away buck do law relaxed other stimulus all bank fit indulge dad any ear grey cult golf
all baby dig war linear tour sleep humanity threat question neglect stance radar bank coup misery painter tragedy buddy
compare winter national approval budget deep screen outdoor audience tear stream cure type ugly chamber supporter franchise
accept sexy ad imply being drug doctor regime where thick dam training grass chamber domestic dictator educate sigh music spoken
connected measure voice lemon pig comprise disturb appear greatly satisfied heat news curiosity top impress nor method reflect
lesson recommend dual revenge thorough bus count broadband living riot prejudice target blonde excess company thereby tribe
respond horror mere way proud shopping wise liver mortgage plastic gentleman eighteen terms worry melt`);

return Promise.all([
expect(notInDict).to.be.rejectedWith(Error, /Word not in dictionary/),
expect(invalidPadding).to.be.rejectedWith(Error, /Invalid padding/),
expect(invalidCrc).to.be.rejectedWith(Error, /Invalid recovery key checksum/),
]);
});

describe('instance methods', () => {
let recoveryKey: RecoveryKey;

Expand Down

0 comments on commit b46f55e

Please sign in to comment.