Skip to content

Commit 016da33

Browse files
overheadhunterchenkins
authored andcommitted
add "alg": "A256KW" support for JWEs
1 parent 6e2e09b commit 016da33

2 files changed

Lines changed: 60 additions & 3 deletions

File tree

frontend/src/common/jwe.ts

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ export class ConcatKDF {
3636
}
3737

3838
export type JWEHeader = {
39-
readonly alg: 'ECDH-ES' | 'PBES2-HS512+A256KW',
40-
readonly enc: 'A256GCM' | 'A128GCM',
39+
readonly alg: 'ECDH-ES' | 'PBES2-HS512+A256KW' | 'A256KW',
40+
readonly enc: 'A256GCM' | 'A128GCM', // A128GCM for testing only, as we use test vectors with 128 bit keys
4141
readonly apu?: string,
4242
readonly apv?: string,
4343
readonly epk?: JsonWebKey,
@@ -97,7 +97,7 @@ export class JWEParser {
9797
* @throws {UnwrapKeyError} if decryption failed (wrong password?)
9898
*/
9999
public async decryptPbes2(password: string): Promise<any> {
100-
if (this.header.alg != 'PBES2-HS512+A256KW' || /* this.header.enc != 'A256GCM' || */ !this.header.p2s || !this.header.p2c) {
100+
if (this.header.alg != 'PBES2-HS512+A256KW' || this.header.enc != 'A256GCM' || !this.header.p2s || !this.header.p2c) {
101101
throw new Error('unsupported alg or enc');
102102
}
103103
const saltInput = base64url.parse(this.header.p2s, { loose: true });
@@ -110,6 +110,24 @@ export class JWEParser {
110110
}
111111
}
112112

113+
/**
114+
* Decrypts the JWE, assuming alg == A256KW and enc == A256GCM.
115+
* @param kek The key used to wrap the CEK
116+
* @returns Decrypted payload
117+
* @throws {UnwrapKeyError} if decryption failed (wrong kek?)
118+
*/
119+
public async decryptA256kw(kek: CryptoKey): Promise<any> {
120+
if (this.header.alg != 'A256KW' || this.header.enc != 'A256GCM') {
121+
throw new Error('unsupported alg or enc');
122+
}
123+
try {
124+
const cek = crypto.subtle.unwrapKey('raw', this.encryptedKey, kek, 'AES-KW', { name: 'AES-GCM', length: 256 }, false, ['decrypt']);
125+
return this.decrypt(await cek);
126+
} catch (error) {
127+
throw new UnwrapKeyError(error);
128+
}
129+
}
130+
113131
private async decrypt(cek: CryptoKey): Promise<any> {
114132
const utf8enc = new TextEncoder();
115133
const m = new Uint8Array(this.ciphertext.length + this.tag.length);
@@ -180,6 +198,22 @@ export class JWEBuilder {
180198
return new JWEBuilder(header, encryptedKey, cek);
181199
}
182200

201+
/**
202+
* Prepares a new JWE using alg: A256KW and enc: A256GCM.
203+
*
204+
* @param kek The key used to wrap the CEK
205+
* @returns A new JWEBuilder ready to encrypt the payload
206+
*/
207+
public static a256kw(kek: CryptoKey): JWEBuilder {
208+
const header = (async () => <JWEHeader>{
209+
alg: 'A256KW',
210+
enc: 'A256GCM'
211+
})();
212+
const cek = crypto.subtle.generateKey({ name: 'AES-GCM', length: 256 }, true, ['encrypt']);
213+
const encryptedKey = (async () => new Uint8Array(await crypto.subtle.wrapKey('raw', await cek, kek, 'AES-KW')))();
214+
return new JWEBuilder(header, encryptedKey, cek);
215+
}
216+
183217
/**
184218
* Builds the JWE.
185219
* @param payload Payload to be encrypted

frontend/test/common/jwe.spec.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,29 @@ describe('JWE', () => {
7474
// TODO: add some more decrypt-only tests with JWE from 3rd party
7575
});
7676

77+
describe('JWE using alg: A256KW', () => {
78+
it('x = decrypt(encrypt(x, kek), kek)', async () => {
79+
const kek = await crypto.subtle.generateKey({ name: 'AES-KW', length: 256 }, false, ['wrapKey', 'unwrapKey']);
80+
const orig = { hello: 'world' };
81+
82+
const jwe = await JWEBuilder.a256kw(kek).encrypt(orig);
83+
84+
const decrypted = await JWEParser.parse(jwe).decryptA256kw(kek);
85+
expect(decrypted).to.deep.eq(orig);
86+
});
87+
88+
it('decrypt', async () => {
89+
// JWE generated by https://dinochiesa.github.io/jwt/
90+
const jwe = 'eyJhbGciOiJBMjU2S1ciLCJlbmMiOiJBMjU2R0NNIn0.JTSrGbw4XEKXYFTC7siTT7DIZUX2SogThcLKXgxe0FPK3Fi8ckjr9A.zQx0t4qoTVIc-h5f.cmqzZ-md3cvdTNH9FWbKOsw.DCdGhmdwjoYKIuNC5zgQJQ';
91+
const rawKek = base64url.parse('y_uxz8iAtcOXlqMYpm2jASvDWokpCYMtwkthFSK6IF0', { loose: true });
92+
const kek = await crypto.subtle.importKey('raw', rawKek, 'AES-KW', false, ['unwrapKey']);
93+
const orig = { hello: 'world' };
94+
95+
const decrypted = await JWEParser.parse(jwe).decryptA256kw(kek);
96+
expect(decrypted).to.deep.eq(orig);
97+
});
98+
});
99+
77100
describe('PBES2', () => {
78101
/**
79102
* Test vectors from https://www.rfc-editor.org/rfc/rfc7517#appendix-C.4

0 commit comments

Comments
 (0)