Skip to content

Commit

Permalink
add additional verification to vaultformat 8 module
Browse files Browse the repository at this point in the history
  • Loading branch information
infeo committed May 29, 2024
1 parent 3368768 commit b43a946
Show file tree
Hide file tree
Showing 3 changed files with 64 additions and 2 deletions.
22 changes: 22 additions & 0 deletions frontend/src/common/jwt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@ export type JWTHeader = {
}

export class JWT {
public header: any;
public payload: any;
public signature: Uint8Array;

private constructor(header: any, payload: any, signature: Uint8Array) {
this.header = header;
this.payload = payload;
this.signature = signature;
}

/**
* Creates a ES384 JWT (signed with ECDSA using P-384 and SHA-384).
*
Expand Down Expand Up @@ -37,4 +47,16 @@ export class JWT {
);
return base64url.stringify(new Uint8Array(signature), { pad: false });
}

public static async parse(token: string): Promise<JWT> {
const jwtSections = token.split('.');
if (jwtSections.length != 3 || !jwtSections[0] || !jwtSections[1] || !jwtSections[2]) {
throw new Error('Invalid JWT');
}

const header = JSON.parse(new TextDecoder().decode(base64url.parse(jwtSections[0], { loose: true })));
const payload = JSON.parse(new TextDecoder().decode(base64url.parse(jwtSections[1], { loose: true })));
const signature = base64url.parse(jwtSections[2], { loose: true });
return new JWT(header, payload, signature);
}
}
31 changes: 29 additions & 2 deletions frontend/src/common/vaultFormat8.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { VaultDto } from './backend';
import config, { absFrontendBaseURL } from './config';
import { AccessTokenProducing, GCM_NONCE_LEN, OtherVaultMember, UnwrapKeyError, UserKeys, VaultTemplateProducing } from './crypto';
import { CRC32, wordEncoder } from './util';
import { JWT } from './jwt';

interface VaultConfigPayload {
jti: string
Expand Down Expand Up @@ -138,13 +139,40 @@ export class VaultFormat8 implements AccessTokenProducing, VaultTemplateProducin
}
}

public static async verifyAndRecover(vaultMetadataToken: string, recoveryKey: string) {
//basic validation
const vaultMetadata = JWT.parse(vaultMetadataToken);

const sigSeparatorIndex = vaultMetadataToken.lastIndexOf('.');
const headerPlusPayload = vaultMetadataToken.slice(0,sigSeparatorIndex);
const signature = vaultMetadataToken.slice(sigSeparatorIndex + 1,vaultMetadataToken.length);

const message = new TextEncoder().encode(headerPlusPayload);
const key = await this.transcodeKey(recoveryKey);
var digest = await crypto.subtle.sign(
VaultFormat8.MASTERKEY_KEY_DESIGNATION,
key,
message
);
const base64urlDigest = base64url.stringify(new Uint8Array(digest), { pad: false });
if (!(signature === base64urlDigest)) {
throw new Error('Verification failed.');
}

return new VaultFormat8(key);
}

/**
* Restore the master key from a given recovery key, create a new admin signature key pair.
* @param recoveryKey The recovery key
* @returns The recovered master key
* @throws Error, if passing a malformed recovery key
*/
public static async recover(recoveryKey: string): Promise<VaultFormat8> {
return new VaultFormat8(await this.transcodeKey(recoveryKey));
}

public static async transcodeKey(recoveryKey: string): Promise<CryptoKey> {
// decode and check recovery key:
const decoded = wordEncoder.decode(recoveryKey);
if (decoded.length !== 66) {
Expand All @@ -158,14 +186,13 @@ export class VaultFormat8 implements AccessTokenProducing, VaultTemplateProducin
}

// construct new VaultKeys from recovered key
const key = crypto.subtle.importKey(
return crypto.subtle.importKey(
'raw',
decodedKey,
VaultFormat8.MASTERKEY_KEY_DESIGNATION,
true,
['sign']
);
return new VaultFormat8(await key);
}

/** @inheritdoc */
Expand Down
13 changes: 13 additions & 0 deletions frontend/test/common/vaultFormat8.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,19 @@ describe('Vault Format 8', () => {
]);
});

it('verifyAndRecover() succeeds for key and corresponding metadata', async () => {
const recoveryKey = `
exotic ghost cooperate rain writing purple bicycle fixed first elite treaty friendly screen pull middle seventeen passport
correctly bored remains give profound ultimate charm haunt retired viable ray delegate indicator race cause aluminium
obesity site tactical root rumour theology glory consist comic terribly substance
`;
const vaultMetadata = 'eyJraWQiOiJtYXN0ZXJrZXlmaWxlOm1hc3RlcmtleS5jcnlwdG9tYXRvciIsInR5cCI6IkpXVCIsImFsZyI6IkhTMjU2In0.eyJmb3JtYXQiOjgsInNob3J0ZW5pbmdUaHJlc2hvbGQiOjIyMCwianRpIjoiZmI0N2IyMDYtM2FjMS00Y2RkLThkNTMtYWE0OWM4NjY4Nzk5IiwiY2lwaGVyQ29tYm8iOiJTSVZfQ1RSTUFDIn0.oSMdTtcC6LtoC37knQpNoPo3biUNFCRfxownXIFf_GM';

const recovered = await VaultFormat8.verifyAndRecover(vaultMetadata, recoveryKey);
const recoveredKey = await crypto.subtle.exportKey('jwk', recovered.masterKey);
//TODO: expect(recoveredKey.k).to.eq('VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3dw');
});

it('encryptForUser()', async () => {
const encrypted = await testVault.encryptForUser(alice.keyPair.publicKey);

Expand Down

0 comments on commit b43a946

Please sign in to comment.