Skip to content

Commit

Permalink
feat: added JWE General JSON Serialization decryption
Browse files Browse the repository at this point in the history
  • Loading branch information
panva committed Dec 17, 2020
1 parent f511889 commit 16dea9e
Show file tree
Hide file tree
Showing 5 changed files with 231 additions and 0 deletions.
9 changes: 9 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,15 @@
"import": "./dist/node/webcrypto/esm/jwe/flattened/encrypt.js",
"require": "./dist/node/webcrypto/cjs/jwe/flattened/encrypt.js"
},
"./jwe/general/decrypt": {
"browser": "./dist/browser/jwe/general/decrypt.js",
"import": "./dist/node/esm/jwe/general/decrypt.js",
"require": "./dist/node/cjs/jwe/general/decrypt.js"
},
"./webcrypto/jwe/general/decrypt": {
"import": "./dist/node/webcrypto/esm/jwe/general/decrypt.js",
"require": "./dist/node/webcrypto/cjs/jwe/general/decrypt.js"
},
"./jwk/embedded": {
"browser": "./dist/browser/jwk/embedded.js",
"import": "./dist/node/esm/jwk/embedded.js",
Expand Down
118 changes: 118 additions & 0 deletions src/jwe/general/decrypt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import decrypt from '../flattened/decrypt.js'
import { JWEDecryptionFailed, JWEInvalid } from '../../util/errors.js'
import type {
KeyLike,
DecryptOptions,
JWEHeaderParameters,
GetKeyFunction,
FlattenedJWE,
GeneralJWE,
GeneralDecryptResult,
} from '../../types.d'
import isObject from '../../lib/is_object.js'

/**
* Interface for General JWE Decryption dynamic key resolution.
* No token components have been verified at the time of this function call.
*/
export interface GeneralDecryptGetKey extends GetKeyFunction<JWEHeaderParameters, FlattenedJWE> {}

/**
* Decrypts a General JWE.
*
* @param jwe General JWE.
* @param key Private Key or Secret, or a function resolving one, to decrypt the JWE with.
* @param options JWE Decryption options.
*
* @example
* ```
* // ESM import
* import generalDecrypt from 'jose/jwe/general/decrypt'
* ```
*
* @example
* ```
* // CJS import
* const { default: generalDecrypt } = require('jose/jwe/general/decrypt')
* ```
*
* @example
* ```
* // usage
* import parseJwk from 'jose/jwk/parse'
*
* const decoder = new TextDecoder()
* const jwe = {
* ciphertext: '9EzjFISUyoG-ifC2mSihfP0DPC80yeyrxhTzKt1C_VJBkxeBG0MI4Te61Pk45RAGubUvBpU9jm4',
* iv: '8Fy7A_IuoX5VXG9s',
* tag: 'W76IYV6arGRuDSaSyWrQNg',
* aad: 'VGhlIEZlbGxvd3NoaXAgb2YgdGhlIFJpbmc',
* protected: 'eyJhbGciOiJSU0EtT0FFUC0yNTYiLCJlbmMiOiJBMjU2R0NNIn0',
* recipients: [
* {
* encrypted_key: 'Z6eD4UK_yFb5ZoKvKkGAdqywEG_m0e4IYo0x8Vf30LAMJcsc-_zSgIeiF82teZyYi2YYduHKoqImk7MRnoPZOlEs0Q5BNK1OgBmSOhCE8DFyqh9Zh48TCTP6lmBQ52naqoUJFMtHzu-0LwZH26hxos0GP3Dt19O379MJB837TdKKa87skq0zHaVLAquRHOBF77GI54Bc7O49d8aOrSu1VEFGMThlW2caspPRiTSePDMDPq7_WGk50izRhB3Asl9wmP9wEeaTrkJKRnQj5ips1SAZ1hDBsqEQKKukxP1HtdcopHV5_qgwU8Hjm5EwSLMluMQuiE6hwlkXGOujZLVizA'
* }
* ]
* }
* const privateKey = await parseJwk({
* e: 'AQAB',
* n: 'qpzYkTGRKSUcd12hZaJnYEKVLfdEsqu6HBAxZgRSvzLFj_zTSAEXjbf3fX47MPEHRw8NDcEXPjVOz84t4FTXYF2w2_LGWfp_myjV8pR6oUUncJjS7DhnUmTG5bpuK2HFXRMRJYz_iNR48xRJPMoY84jrnhdIFx8Tqv6w4ZHVyEvcvloPgwG3UjLidP6jmqbTiJtidVLnpQJRuFNFQJiluQXBZ1nOLC7raQshu7L9y0IatVU7vf0BPnmuSkcNNvmQkSta6ODQBPaL5-o5SW8H37vQjPDkrlJpreViNa3jqP5DB5HYUO-DMh4FegRv9gZWLDEvXpSd9A13YXCa9Q8K_w',
* d: 'YAfYfiEAK8CPvUAeUC6RMUVI4o6DRG4UWydiJqHYUXYqbVlJMwYqU8Jws1oRxwJjrkNyfYNpqcInkh_jApm-gKc7nRGRQ6QTnynlAp1ASPW7tUzPq9YzkdTXfwboa9KkXDcXN6OdUU8GpQuODYFTegBfXqSMFzeOwniI5u5G_m2I6YU1zU4x7dxaKhPSK2mJ1v-tJu88j855DYIY0AiX5uf_oa0CgaqyOOY3LaxGjV0FxrkAzYluHfQef7ux-1ocXD1aUrdj3owk48ZVEb2o-V1bMLtk415ngS-u89bABHuJ50-gIwpO-y7ofe6ik4fAd9NfD8PVKHHsrNYbC5FdAQ',
* p: '4WlvPw4Vf-mHzoqem_2VUf7hMiLEM5sl_th-CZyA0dowhEnNBJPtaqCz2k_6_ECKZ5C-KoT-EmQOBILQFJtR9SOs6fI9yZGL1OpbjGNKpWzym8iQrFcKAhFvQ_hG7Fkwz6_yRV5fKnOWSD78Rk6wuOTaXqwJS7uljvrn7SmRFpE',
* q: 'wcO_PHrkHazbqDgBVvTDaMXJ7W5l0RTxhrOsU6qGCLp367Zc2F9BwPAlMy9KKMhf9RLxgv32lGqWxVh3WQ1GSJqswSIKhfAOzmuTDjlYxqrte_TMcaVDxtRuO8Bxp5A8Y7i3VxQ_Rjfa04QLxJfiRdap4UamYWco25WKH4rkcI8',
* dp: 'rWynEIZPeEg-GmSAP1fMqHdG34HsHiBCDV6XKeHlIo-SQFVfjSQax6y4c0CRw74MPj4YcTI9H_0m48WZPiF53vcBtESR0SFPyhI9OTezWK8HwV-AH3gf1ROA3XSJbJH6ge_GoCRJZ6nid9ct1RH52WcJs0j9Je1LJURZaBhQ7mE',
* dq: 'tYrMc0ME1dTuHQcUIj_Dkje2gLGtzZ6cyMMw01byq9zhnMRI6yUcu0OE5xcImXtbhIfSJhQCYn4XcyD2-UWZs07QS0e0qlcH2Fkr9-i9B66AQWJT5qqb_P9tpKgjFIbsPdaEWJ8MxaJxcTnHuNNBWoPMuNfz7VC1FD9goTsF23s',
* qi: 'qAZmEWhWcDgW_pQZA5e7r185-sOnNPAW53y16QKh5wNThGjpUl7OvePZWY59ekd6PYwvkloNIRki6mLskP9NZ73CsAdZknSAPaAmBuNGYDabtObcigQDPFQ5DeqyAdRUrim66eN7whE5mf_XgOwVAx3-9PtfHvvmTTNezHfoZdo',
* kty: 'RSA'
* }, 'RSA-OAEP-256')
*
* const {
* plaintext,
* protectedHeader,
* additionalAuthenticatedData
* } = await generalDecrypt(jwe, privateKey)
*
* console.log(protectedHeader)
* console.log(decoder.decode(plaintext))
* console.log(decoder.decode(additionalAuthenticatedData))
* ```
*/
export default async function generalDecrypt(
jwe: GeneralJWE,
key: KeyLike | GeneralDecryptGetKey,
options?: DecryptOptions,
): Promise<GeneralDecryptResult> {
if (!isObject(jwe)) {
throw new JWEInvalid('General JWE must be an object')
}

if (!Array.isArray(jwe.recipients) || !jwe.recipients.every(isObject)) {
throw new JWEInvalid('JWE Recipients missing or incorrect type')
}

// eslint-disable-next-line no-restricted-syntax
for (const recipient of jwe.recipients) {
try {
// eslint-disable-next-line no-await-in-loop
return await decrypt(
{
aad: jwe.aad,
ciphertext: jwe.ciphertext,
encrypted_key: recipient.encrypted_key,
header: recipient.header,
iv: jwe.iv,
protected: jwe.protected,
tag: jwe.tag,
unprotected: jwe.unprotected,
},
<Parameters<typeof decrypt>[1]>key,
options,
)
} catch {
//
}
}
throw new JWEDecryptionFailed()
}

export type { KeyLike, GeneralJWE, DecryptOptions }
6 changes: 6 additions & 0 deletions src/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,10 @@ export interface FlattenedJWE {
unprotected?: JWEHeaderParameters
}

export interface GeneralJWE extends Omit<FlattenedJWE, 'encrypted_key' | 'header'> {
recipients: Pick<FlattenedJWE, 'encrypted_key' | 'header'>[]
}

/**
* Recognized JWE Header Parameters, any other Header members
* may also be present.
Expand Down Expand Up @@ -552,6 +556,8 @@ export interface FlattenedDecryptResult {
unprotectedHeader?: JWEHeaderParameters
}

export interface GeneralDecryptResult extends FlattenedDecryptResult {}

export interface CompactDecryptResult {
/**
* Plaintext.
Expand Down
97 changes: 97 additions & 0 deletions test/jwe/general.decrypt.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/* eslint-disable no-param-reassign */
import test from 'ava';

const root = !('WEBCRYPTO' in process.env) ? '#dist' : '#dist/webcrypto';
Promise.all([
import(`${root}/jwe/flattened/encrypt`),
import(`${root}/jwe/general/decrypt`),
import(`${root}/util/random`),
]).then(
([{ default: FlattenedEncrypt }, { default: generalDecrypt }, { default: random }]) => {
test.before(async (t) => {
const encode = TextEncoder.prototype.encode.bind(new TextEncoder());
t.context.plaintext = encode('It’s a dangerous business, Frodo, going out your door.');
t.context.additionalAuthenticatedData = encode('The Fellowship of the Ring');
t.context.initializationVector = random(new Uint8Array(12));
t.context.secret = random(new Uint8Array(16));
});

test('JWS format validation', async (t) => {
const flattenedJwe = await new FlattenedEncrypt(t.context.plaintext)
.setProtectedHeader({ bar: 'baz' })
.setUnprotectedHeader({ foo: 'bar' })
.setSharedUnprotectedHeader({ alg: 'A128GCMKW', enc: 'A128GCM' })
.setAdditionalAuthenticatedData(t.context.additionalAuthenticatedData)
.encrypt(t.context.secret);

const generalJwe = {
aad: flattenedJwe.aad,
ciphertext: flattenedJwe.ciphertext,
iv: flattenedJwe.iv,
protected: flattenedJwe.protected,
tag: flattenedJwe.tag,
unprotected: flattenedJwe.unprotected,
recipients: [
{
encrypted_key: flattenedJwe.encrypted_key,
header: flattenedJwe.header,
},
],
};

{
await t.throwsAsync(generalDecrypt(null, t.context.secret), {
message: 'General JWE must be an object',
code: 'ERR_JWE_INVALID',
});
}

{
await t.throwsAsync(generalDecrypt({ recipients: null }, t.context.secret), {
message: 'JWE Recipients missing or incorrect type',
code: 'ERR_JWE_INVALID',
});
}

{
await t.throwsAsync(generalDecrypt({ recipients: [null] }, t.context.secret), {
message: 'JWE Recipients missing or incorrect type',
code: 'ERR_JWE_INVALID',
});
}

{
const jwe = { ...generalJwe, recipients: [] };

await t.throwsAsync(generalDecrypt(jwe, t.context.secret), {
message: 'decryption operation failed',
code: 'ERR_JWE_DECRYPTION_FAILED',
});
}

{
const jwe = { ...generalJwe, recipients: [generalJwe.recipients[0]] };

await t.notThrowsAsync(generalDecrypt(jwe, t.context.secret));
}

{
const jwe = { ...generalJwe, recipients: [generalJwe.recipients[0], {}] };

await t.notThrowsAsync(generalDecrypt(jwe, t.context.secret));
}

{
const jwe = { ...generalJwe, recipients: [{}, generalJwe.recipients[0]] };

await t.notThrowsAsync(generalDecrypt(jwe, t.context.secret));
}
});
},
(err) => {
test('failed to import', (t) => {
console.error(err);
t.fail();
});
},
);
1 change: 1 addition & 0 deletions tsconfig/base.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"../src/jwe/compact/decrypt.ts",
"../src/jwe/flattened/encrypt.ts",
"../src/jwe/flattened/decrypt.ts",
"../src/jwe/general/decrypt.ts",

"../src/jws/compact/sign.ts",
"../src/jws/compact/verify.ts",
Expand Down

0 comments on commit 16dea9e

Please sign in to comment.