Skip to content

Commit

Permalink
feat: add GeneralSign signature and GeneralEncrypt recipient builder …
Browse files Browse the repository at this point in the history
…chaining
  • Loading branch information
panva committed Nov 11, 2021
1 parent 62e5428 commit cfc93f5
Show file tree
Hide file tree
Showing 13 changed files with 130 additions and 71 deletions.
3 changes: 1 addition & 2 deletions src/jwe/compact/decrypt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,12 @@ export interface CompactDecryptGetKey extends GetKeyFunction<JWEHeaderParameters
*
* @example Usage
* ```js
* const decoder = new TextDecoder()
* const jwe = 'eyJhbGciOiJSU0EtT0FFUC0yNTYiLCJlbmMiOiJBMjU2R0NNIn0.nyQ19eq9ogh9wA7fFtnI2oouzy5_8b5DeLkoRMfi2yijgfTs2zEnayCEofz_qhnL-nwszabd9qUeHv0-IwvhhJJS7GUJOU3ikiIe42qcIAFme1A_Fo9CTxw4XTOy-I5qanl8So91u6hwfyN1VxAqVLsSE7_23EC-gfGEg_5znew9PyXXsOIE-K_HH7IQowRrlZ1X_bM_Liu53RzDpLDvRz59mp3S8L56YqpM8FexFGTGpEaoTcEIst375qncYt3-79IVR7gZN1RWsWgjPatfvVbnh74PglQcATSf3UUhaW0OAKn6q7r3PDx6DIKQ35bgHQg5QopuN00eIfLQL2trGw.W3grIVj5HVuAb76X.6PcuDe5D6ttWFYyv0oqqdDXfI2R8wBg1F2Q80UUA_Gv8eEimNWfxIWdLxrjzgQGSvIhxmFKuLM0.a93_Ug3uZHuczj70Zavx8Q'
*
* const { plaintext, protectedHeader } = await jose.compactDecrypt(jwe, privateKey)
*
* console.log(protectedHeader)
* console.log(decoder.decode(plaintext))
* console.log(new TextDecoder().decode(plaintext))
* ```
*/
export async function compactDecrypt(
Expand Down
8 changes: 5 additions & 3 deletions src/jwe/compact/encrypt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@ import type {
*
* @example Usage
* ```js
* const encoder = new TextEncoder()
*
* const jwe = await new jose.CompactEncrypt(encoder.encode('It’s a dangerous business, Frodo, going out your door.'))
* const jwe = await new jose.CompactEncrypt(
* new TextEncoder().encode(
* 'It’s a dangerous business, Frodo, going out your door.'
* )
* )
* .setProtectedHeader({ alg: 'RSA-OAEP-256', enc: 'A256GCM' })
* .encrypt(publicKey)
*
Expand Down
2 changes: 1 addition & 1 deletion src/jwe/flattened/decrypt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ export interface FlattenedDecryptGetKey
*
* @example Usage
* ```js
* const decoder = new TextDecoder()
* const jwe = {
* ciphertext: '9EzjFISUyoG-ifC2mSihfP0DPC80yeyrxhTzKt1C_VJBkxeBG0MI4Te61Pk45RAGubUvBpU9jm4',
* iv: '8Fy7A_IuoX5VXG9s',
Expand All @@ -53,6 +52,7 @@ export interface FlattenedDecryptGetKey
* } = await jose.flattenedDecrypt(jwe, privateKey)
*
* console.log(protectedHeader)
* const decoder = new TextDecoder()
* console.log(decoder.decode(plaintext))
* console.log(decoder.decode(additionalAuthenticatedData))
* ```
Expand Down
8 changes: 5 additions & 3 deletions src/jwe/flattened/encrypt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,11 @@ export const unprotected = Symbol()
*
* @example Usage
* ```js
* const encoder = new TextEncoder()
*
* const jwe = await new jose.FlattenedEncrypt(encoder.encode('It’s a dangerous business, Frodo, going out your door.'))
* const jwe = await new jose.FlattenedEncrypt(
* new TextEncoder().encode(
* 'It’s a dangerous business, Frodo, going out your door.'
* )
* )
* .setProtectedHeader({ alg: 'RSA-OAEP-256', enc: 'A256GCM' })
* .setAdditionalAuthenticatedData(encoder.encode('The Fellowship of the Ring'))
* .encrypt(publicKey)
Expand Down
2 changes: 1 addition & 1 deletion src/jwe/general/decrypt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ export interface GeneralDecryptGetKey extends GetKeyFunction<JWEHeaderParameters
*
* @example Usage
* ```js
* const decoder = new TextDecoder()
* const jwe = {
* ciphertext: '9EzjFISUyoG-ifC2mSihfP0DPC80yeyrxhTzKt1C_VJBkxeBG0MI4Te61Pk45RAGubUvBpU9jm4',
* iv: '8Fy7A_IuoX5VXG9s',
Expand All @@ -48,6 +47,7 @@ export interface GeneralDecryptGetKey extends GetKeyFunction<JWEHeaderParameters
* } = await jose.generalDecrypt(jwe, privateKey)
*
* console.log(protectedHeader)
* const decoder = new TextDecoder()
* console.log(decoder.decode(plaintext))
* console.log(decoder.decode(additionalAuthenticatedData))
* ```
Expand Down
58 changes: 47 additions & 11 deletions src/jwe/general/encrypt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,21 @@ export interface Recipient {
* @param unprotectedHeader JWE Per-Recipient Unprotected Header.
*/
setUnprotectedHeader(unprotectedHeader: JWEHeaderParameters): Recipient

/**
* A shorthand for calling addRecipient() on the enclosing GeneralEncrypt instance
*/
addRecipient(...args: Parameters<GeneralEncrypt['addRecipient']>): Recipient

/**
* A shorthand for calling encrypt() on the enclosing GeneralEncrypt instance
*/
encrypt(...args: Parameters<GeneralEncrypt['encrypt']>): Promise<GeneralJWE>

/**
* Returns the enclosing GeneralEncrypt
*/
done(): GeneralEncrypt
}

interface RecipientReference {
Expand All @@ -32,6 +47,12 @@ interface RecipientReference {
const recipientRef: WeakMap<IndividualRecipient, RecipientReference> = new WeakMap()

class IndividualRecipient implements Recipient {
private _back: GeneralEncrypt

constructor(enc: GeneralEncrypt) {
this._back = enc
}

setUnprotectedHeader(unprotectedHeader: JWEHeaderParameters) {
const ref = recipientRef.get(this)!
if (ref.unprotectedHeader) {
Expand All @@ -40,27 +61,38 @@ class IndividualRecipient implements Recipient {
ref.unprotectedHeader = unprotectedHeader
return this
}

addRecipient(...args: Parameters<GeneralEncrypt['addRecipient']>) {
return this._back.addRecipient(...args)
}

encrypt(...args: Parameters<GeneralEncrypt['encrypt']>) {
return this._back.encrypt(...args)
}

done() {
return this._back
}
}

/**
* The GeneralEncrypt class is a utility for creating General JWE objects.
*
* @example Usage
* ```js
* const encoder = new TextEncoder()
*
* const encrypt = new jose.GeneralEncrypt(encoder.encode('It’s a dangerous business, Frodo, going out your door.'))
* const jwe = await new jose.GeneralEncrypt(
* new TextEncoder().encode(
* 'It’s a dangerous business, Frodo, going out your door.'
* )
* )
* .setProtectedHeader({ enc: 'A256GCM' })
*
* encrypt
* .addRecipient(ecPrivateKey)
* .setUnprotectedHeader({ alg: 'ECDH-ES+A256KW' })
*
* encrypt
* .addRecipient(rsaPrivateKey)
* .setUnprotectedHeader({ alg: 'RSA-OAEP-384' })
* .encrypt()
*
* const jwe = await encrypt.encrypt()
* console.log(jwe)
* ```
*/
export class GeneralEncrypt {
Expand Down Expand Up @@ -88,7 +120,7 @@ export class GeneralEncrypt {
* @param options JWE Encryption options.
*/
addRecipient(key: KeyLike | Uint8Array, options?: CritOption): Recipient {
const recipient = new IndividualRecipient()
const recipient = new IndividualRecipient(this)
recipientRef.set(recipient, { key, options: { crit: options?.crit } })
this._recipients.push(recipient)
return recipient
Expand Down Expand Up @@ -242,8 +274,12 @@ export class GeneralEncrypt {
.setProtectedHeader(this._protectedHeader)
.setSharedUnprotectedHeader(this._unprotectedHeader)
.setUnprotectedHeader(unprotectedHeader!)
// @ts-expect-error
.encrypt(key, { ...recipientOpts, ...options, [unprotected]: true })
.encrypt(key, {
...recipientOpts,
...options,
// @ts-expect-error
[unprotected]: true,
})

jwe.ciphertext = flattened.ciphertext
jwe.iv = flattened.iv
Expand Down
8 changes: 5 additions & 3 deletions src/jws/compact/sign.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ import type { JWSHeaderParameters, KeyLike, SignOptions } from '../../types.d'
*
* @example Usage
* ```js
* const encoder = new TextEncoder()
*
* const jws = await new jose.CompactSign(encoder.encode('It’s a dangerous business, Frodo, going out your door.'))
* const jws = await new jose.CompactSign(
* new TextEncoder().encode(
* 'It’s a dangerous business, Frodo, going out your door.'
* )
* )
* .setProtectedHeader({ alg: 'ES256' })
* .sign(privateKey)
*
Expand Down
3 changes: 1 addition & 2 deletions src/jws/compact/verify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,12 @@ export interface CompactVerifyGetKey
*
* @example Usage
* ```js
* const decoder = new TextDecoder()
* const jws = 'eyJhbGciOiJFUzI1NiJ9.SXTigJlzIGEgZGFuZ2Vyb3VzIGJ1c2luZXNzLCBGcm9kbywgZ29pbmcgb3V0IHlvdXIgZG9vci4.kkAs_gPPxWMI3rHuVlxHaTPfDWDoqdI8jSvuSmqV-8IHIWXg9mcAeC9ggV-45ZHRbiRJ3obUIFo1rHphPA5URg'
*
* const { payload, protectedHeader } = await jose.compactVerify(jws, publicKey)
*
* console.log(protectedHeader)
* console.log(decoder.decode(payload))
* console.log(new TextDecoder().decode(payload))
* ```
*/
export function compactVerify(
Expand Down
9 changes: 6 additions & 3 deletions src/jws/flattened/sign.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,14 @@ import validateCrit from '../../lib/validate_crit.js'
*
* @example Usage
* ```js
* const encoder = new TextEncoder()
*
* const jws = await new jose.FlattenedSign(encoder.encode('It’s a dangerous business, Frodo, going out your door.'))
* const jws = await new jose.FlattenedSign(
* new TextEncoder().encode(
* 'It’s a dangerous business, Frodo, going out your door.'
* )
* )
* .setProtectedHeader({ alg: 'ES256' })
* .sign(privateKey)
*
* console.log(jws)
* ```
*/
Expand Down
50 changes: 41 additions & 9 deletions src/jws/general/sign.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,21 @@ export interface Signature {
* @param unprotectedHeader JWS Unprotected Header.
*/
setUnprotectedHeader(unprotectedHeader: JWSHeaderParameters): Signature

/**
* A shorthand for calling addSignature() on the enclosing GeneralSign instance
*/
addSignature(...args: Parameters<GeneralSign['addSignature']>): Signature

/**
* A shorthand for calling encrypt() on the enclosing GeneralSign instance
*/
sign(...args: Parameters<GeneralSign['sign']>): Promise<GeneralJWS>

/**
* Returns the enclosing GeneralSign
*/
done(): GeneralSign
}

interface SignatureReference {
Expand All @@ -29,6 +44,12 @@ interface SignatureReference {
const signatureRef: WeakMap<IndividualSignature, SignatureReference> = new WeakMap()

class IndividualSignature implements Signature {
private _back: GeneralSign

constructor(sig: GeneralSign) {
this._back = sig
}

setProtectedHeader(protectedHeader: JWSHeaderParameters) {
const ref = signatureRef.get(this)!
if (ref.protectedHeader) {
Expand All @@ -46,26 +67,37 @@ class IndividualSignature implements Signature {
ref.unprotectedHeader = unprotectedHeader
return this
}

addSignature(...args: Parameters<GeneralSign['addSignature']>) {
return this._back.addSignature(...args)
}

sign(...args: Parameters<GeneralSign['sign']>) {
return this._back.sign(...args)
}

done() {
return this._back
}
}

/**
* The GeneralSign class is a utility for creating General JWS objects.
*
* @example Usage
* ```js
* const encoder = new TextEncoder()
*
* const sign = new jose.GeneralSign(encoder.encode('It’s a dangerous business, Frodo, going out your door.'))
*
* sign
* const jws = await new jose.GeneralSign(
* new TextEncoder().encode(
* 'It’s a dangerous business, Frodo, going out your door.'
* )
* )
* .addSignature(ecPrivateKey)
* .setProtectedHeader({ alg: 'ES256' })
*
* sign
* .addSignature(rsaPrivateKey)
* .setProtectedHeader({ alg: 'PS256' })
* .sign()
*
* const jws = await sign.sign()
* console.log(jws)
* ```
*/
export class GeneralSign {
Expand All @@ -87,7 +119,7 @@ export class GeneralSign {
* @param options JWS Sign options.
*/
addSignature(key: KeyLike | Uint8Array, options?: SignOptions): Signature {
const signature = new IndividualSignature()
const signature = new IndividualSignature(this)
signatureRef.set(signature, { key, options })
this._signatures.push(signature)
return signature
Expand Down
3 changes: 1 addition & 2 deletions src/jws/general/verify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ export interface GeneralVerifyGetKey
*
* @example Usage
* ```js
* const decoder = new TextDecoder()
* const jws = {
* payload: 'SXTigJlzIGEgZGFuZ2Vyb3VzIGJ1c2luZXNzLCBGcm9kbywgZ29pbmcgb3V0IHlvdXIgZG9vci4',
* signatures: [
Expand All @@ -45,7 +44,7 @@ export interface GeneralVerifyGetKey
* const { payload, protectedHeader } = await jose.generalVerify(jws, publicKey)
*
* console.log(protectedHeader)
* console.log(decoder.decode(payload))
* console.log(new TextDecoder().decode(payload))
* ```
*/
export function generalVerify(
Expand Down
29 changes: 12 additions & 17 deletions test/jwe/general.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,15 @@ test.before(async (t) => {
})

test('General JWE encryption', async (t) => {
const enc = new GeneralEncrypt(t.context.plaintext)
const generalJwe = await new GeneralEncrypt(t.context.plaintext)
.setAdditionalAuthenticatedData(t.context.additionalAuthenticatedData)
.setProtectedHeader({ enc: 'A256GCM' })
.setSharedUnprotectedHeader({ foo: 'bar' })

enc.addRecipient(t.context.secret).setUnprotectedHeader({ alg: 'A256GCMKW' })

enc.addRecipient(t.context.secret2).setUnprotectedHeader({ alg: 'A128GCMKW' })

const generalJwe = await enc.encrypt()
.addRecipient(t.context.secret)
.setUnprotectedHeader({ alg: 'A256GCMKW' })
.addRecipient(t.context.secret2)
.setUnprotectedHeader({ alg: 'A128GCMKW' })
.encrypt()

t.true(generalJwe.aad && typeof generalJwe.aad === 'string')
t.true(generalJwe.ciphertext && typeof generalJwe.ciphertext === 'string')
Expand Down Expand Up @@ -52,14 +51,12 @@ test('General JWE encryption', async (t) => {
})

test('General JWE encryption (single recipient dir)', async (t) => {
const enc = new GeneralEncrypt(t.context.plaintext)
const generalJwe = await new GeneralEncrypt(t.context.plaintext)
.setAdditionalAuthenticatedData(t.context.additionalAuthenticatedData)
.setProtectedHeader({ enc: 'A256GCM' })
.setSharedUnprotectedHeader({ alg: 'A256GCMKW' })

enc.addRecipient(t.context.secret)

const generalJwe = await enc.encrypt()
.addRecipient(t.context.secret)
.encrypt()

t.true(generalJwe.aad && typeof generalJwe.aad === 'string')
t.true(generalJwe.ciphertext && typeof generalJwe.ciphertext === 'string')
Expand All @@ -84,14 +81,12 @@ test('General JWE encryption (single recipient dir)', async (t) => {

test('General JWE encryption (single recipient ECDH-ES)', async (t) => {
const kp = await generateKeyPair('ECDH-ES')
const enc = new GeneralEncrypt(t.context.plaintext)
const generalJwe = await new GeneralEncrypt(t.context.plaintext)
.setAdditionalAuthenticatedData(t.context.additionalAuthenticatedData)
.setProtectedHeader({ enc: 'A256GCM' })
.setSharedUnprotectedHeader({ alg: 'ECDH-ES' })

enc.addRecipient(kp.publicKey)

const generalJwe = await enc.encrypt()
.addRecipient(kp.publicKey)
.encrypt()

t.true(generalJwe.aad && typeof generalJwe.aad === 'string')
t.true(generalJwe.ciphertext && typeof generalJwe.ciphertext === 'string')
Expand Down
Loading

0 comments on commit cfc93f5

Please sign in to comment.