@sphereon/ssi-sdk-ext.jwt-service 0.24.1-unstable.92 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +73 -374
- package/dist/agent/JwtService.d.ts +3 -0
- package/dist/agent/JwtService.d.ts.map +1 -1
- package/dist/agent/JwtService.js +78 -1
- package/dist/agent/JwtService.js.map +1 -1
- package/dist/functions/JWE.d.ts +75 -0
- package/dist/functions/JWE.d.ts.map +1 -0
- package/dist/functions/JWE.js +280 -0
- package/dist/functions/JWE.js.map +1 -0
- package/dist/functions/index.d.ts +19 -5
- package/dist/functions/index.d.ts.map +1 -1
- package/dist/functions/index.js +155 -19
- package/dist/functions/index.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/tsdoc-metadata.json +1 -1
- package/dist/types/IJwtService.d.ts +149 -20
- package/dist/types/IJwtService.d.ts.map +1 -1
- package/dist/types/IJwtService.js +54 -1
- package/dist/types/IJwtService.js.map +1 -1
- package/package.json +15 -13
- package/plugin.schema.json +4212 -282
- package/src/agent/JwtService.ts +103 -39
- package/src/functions/JWE.ts +360 -0
- package/src/functions/index.ts +184 -26
- package/src/index.ts +4 -0
- package/src/types/IJwtService.ts +272 -55
package/src/agent/JwtService.ts
CHANGED
|
@@ -1,47 +1,111 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {IAgentPlugin} from '@veramo/core'
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
3
|
+
createJwsCompact,
|
|
4
|
+
CreateJwsCompactArgs,
|
|
5
|
+
CreateJwsFlattenedArgs,
|
|
6
|
+
CreateJwsJsonArgs,
|
|
7
|
+
createJwsJsonFlattened,
|
|
8
|
+
createJwsJsonGeneral,
|
|
9
|
+
DecryptJweCompactJwtArgs,
|
|
10
|
+
EncryptJweCompactJwtArgs,
|
|
11
|
+
IJwsValidationResult,
|
|
12
|
+
IJwtService,
|
|
13
|
+
IRequiredContext,
|
|
14
|
+
jweAlg,
|
|
15
|
+
jweEnc,
|
|
16
|
+
JwsJsonFlattened,
|
|
17
|
+
JwsJsonGeneral,
|
|
18
|
+
JwtCompactResult,
|
|
19
|
+
JwtLogger,
|
|
20
|
+
PreparedJwsObject,
|
|
21
|
+
prepareJwsObject,
|
|
22
|
+
schema,
|
|
23
|
+
verifyJws,
|
|
24
|
+
VerifyJwsArgs,
|
|
17
25
|
} from '..'
|
|
26
|
+
import {CompactJwtEncrypter} from "../functions/JWE";
|
|
27
|
+
|
|
28
|
+
import * as u8a from 'uint8arrays'
|
|
18
29
|
|
|
19
30
|
/**
|
|
20
31
|
* @public
|
|
21
32
|
*/
|
|
22
33
|
export class JwtService implements IAgentPlugin {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
34
|
+
readonly schema = schema.IJwtService
|
|
35
|
+
readonly methods: IJwtService = {
|
|
36
|
+
jwtPrepareJws: this.jwtPrepareJws.bind(this),
|
|
37
|
+
jwtCreateJwsJsonGeneralSignature: this.jwtCreateJwsJsonGeneralSignature.bind(this),
|
|
38
|
+
jwtCreateJwsJsonFlattenedSignature: this.jwtCreateJwsJsonFlattenedSignature.bind(this),
|
|
39
|
+
jwtCreateJwsCompactSignature: this.jwtCreateJwsCompactSignature.bind(this),
|
|
40
|
+
jwtVerifyJwsSignature: this.jwtVerifyJwsSignature.bind(this),
|
|
41
|
+
jwtEncryptJweCompactJwt: this.jwtEncryptJweCompactJwt.bind(this),
|
|
42
|
+
jwtDecryptJweCompactJwt: this.jwtDecryptJweCompactJwt.bind(this)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private async jwtPrepareJws(args: CreateJwsJsonArgs, context: IRequiredContext): Promise<PreparedJwsObject> {
|
|
46
|
+
return await prepareJwsObject(args, context)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private async jwtCreateJwsJsonGeneralSignature(args: CreateJwsJsonArgs, context: IRequiredContext): Promise<JwsJsonGeneral> {
|
|
50
|
+
return await createJwsJsonGeneral(args, context)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
private async jwtCreateJwsJsonFlattenedSignature(args: CreateJwsFlattenedArgs, context: IRequiredContext): Promise<JwsJsonFlattened> {
|
|
54
|
+
return await createJwsJsonFlattened(args, context)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private async jwtCreateJwsCompactSignature(args: CreateJwsCompactArgs, context: IRequiredContext): Promise<JwtCompactResult> {
|
|
58
|
+
// We wrap it in a json object for remote REST calls
|
|
59
|
+
return {jwt: await createJwsCompact(args, context)}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private async jwtVerifyJwsSignature(args: VerifyJwsArgs, context: IRequiredContext): Promise<IJwsValidationResult> {
|
|
63
|
+
return await verifyJws(args, context)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private async jwtEncryptJweCompactJwt(args: EncryptJweCompactJwtArgs, context: IRequiredContext): Promise<JwtCompactResult> {
|
|
67
|
+
const {payload, protectedHeader = {alg: args.alg, enc: args.enc}, recipientKey, issuer, expirationTime, audience} = args
|
|
68
|
+
|
|
69
|
+
console.log(JSON.stringify(args, null, 2))
|
|
70
|
+
|
|
71
|
+
const alg = jweAlg(args.alg) ?? jweAlg(protectedHeader.alg) ?? 'ECDH-ES'
|
|
72
|
+
const enc = jweEnc(args.enc) ?? jweEnc(protectedHeader.enc) ?? 'A256GCM'
|
|
73
|
+
const encJwks = recipientKey.jwks.length === 1 ? [recipientKey.jwks[0]] : recipientKey.jwks.filter(jwk => (jwk.kid && (jwk.kid === jwk.jwk.kid || jwk.kid === jwk.jwkThumbprint)) || jwk.jwk.use === 'enc')
|
|
74
|
+
if (encJwks.length === 0) {
|
|
75
|
+
return Promise.reject(Error(`No public JWK found that can be used to encrypt against`))
|
|
76
|
+
}
|
|
77
|
+
const jwkInfo = encJwks[0]
|
|
78
|
+
if (encJwks.length > 0) {
|
|
79
|
+
JwtLogger.warning(`More than one JWK with 'enc' usage found. Selected the first one as no 'kid' was provided`, encJwks)
|
|
80
|
+
}
|
|
81
|
+
if (jwkInfo.jwk.kty?.startsWith('EC') !== true || !alg.startsWith('ECDH')) {
|
|
82
|
+
return Promise.reject(Error(`Currently only ECDH-ES is supported for encryption. JWK alg ${jwkInfo.jwk.kty}, header alg ${alg}`)) // TODO: Probably we support way more already
|
|
83
|
+
}
|
|
84
|
+
const apuVal = protectedHeader.apu ?? args.apu
|
|
85
|
+
const apu = apuVal ? u8a.fromString(apuVal, 'base64url') : undefined
|
|
86
|
+
const apvVal = protectedHeader.apv ?? args.apv
|
|
87
|
+
const apv = apvVal ? u8a.fromString(apvVal, 'base64url') : undefined
|
|
88
|
+
|
|
89
|
+
const pubKey = await crypto.subtle.importKey('jwk', jwkInfo.jwk, {
|
|
90
|
+
name: 'ECDH',
|
|
91
|
+
namedCurve: 'P-256',
|
|
92
|
+
}, true, [])
|
|
93
|
+
const encrypter = new CompactJwtEncrypter({
|
|
94
|
+
enc,
|
|
95
|
+
alg,
|
|
96
|
+
keyManagementParams: {apu, apv},
|
|
97
|
+
key: pubKey,
|
|
98
|
+
issuer,
|
|
99
|
+
expirationTime,
|
|
100
|
+
audience
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
const jwe = await encrypter.encryptCompactJWT(payload, {})
|
|
104
|
+
return {jwt: jwe}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private async jwtDecryptJweCompactJwt(args: DecryptJweCompactJwtArgs, context: IRequiredContext): Promise<JwtCompactResult> {
|
|
108
|
+
|
|
109
|
+
return {jwt: "FIXME"}
|
|
110
|
+
}
|
|
47
111
|
}
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
import {defaultRandomSource, randomBytes, RandomSource} from '@stablelib/random'
|
|
2
|
+
import {base64ToBytes, bytesToBase64url, decodeBase64url} from "@veramo/utils";
|
|
3
|
+
import * as jose from "jose";
|
|
4
|
+
import {JWEKeyManagementHeaderParameters, JWTDecryptOptions} from "jose";
|
|
5
|
+
import type {KeyLike} from "jose/dist/types/types";
|
|
6
|
+
import * as u8a from 'uint8arrays'
|
|
7
|
+
import {
|
|
8
|
+
JweAlg,
|
|
9
|
+
JweAlgs,
|
|
10
|
+
JweEnc,
|
|
11
|
+
JweEncs,
|
|
12
|
+
JweHeader,
|
|
13
|
+
JweJsonGeneral,
|
|
14
|
+
JweProtectedHeader,
|
|
15
|
+
JweRecipient,
|
|
16
|
+
JweRecipientUnprotectedHeader,
|
|
17
|
+
JwsPayload
|
|
18
|
+
} from "../types/IJwtService";
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
export interface EncryptionResult {
|
|
22
|
+
ciphertext: Uint8Array
|
|
23
|
+
tag: Uint8Array
|
|
24
|
+
iv: Uint8Array
|
|
25
|
+
protectedHeader?: string
|
|
26
|
+
recipients?: JweRecipient[]
|
|
27
|
+
cek?: Uint8Array
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
export const generateContentEncryptionKey = async ({alg, randomSource = defaultRandomSource}: {
|
|
32
|
+
alg: JweEnc,
|
|
33
|
+
randomSource?: RandomSource
|
|
34
|
+
}): Promise<Uint8Array> => {
|
|
35
|
+
let length: number
|
|
36
|
+
switch (alg) {
|
|
37
|
+
case "A128GCM":
|
|
38
|
+
length = 16
|
|
39
|
+
break
|
|
40
|
+
case "A192GCM":
|
|
41
|
+
length = 24
|
|
42
|
+
break
|
|
43
|
+
case "A128CBC-HS256":
|
|
44
|
+
case "A256GCM":
|
|
45
|
+
length = 32
|
|
46
|
+
break
|
|
47
|
+
case "A192CBC-HS384":
|
|
48
|
+
length = 48
|
|
49
|
+
break
|
|
50
|
+
case "A256CBC-HS512":
|
|
51
|
+
length = 64
|
|
52
|
+
break
|
|
53
|
+
default:
|
|
54
|
+
length = 32
|
|
55
|
+
}
|
|
56
|
+
return randomBytes(length, randomSource)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/*
|
|
60
|
+
export const generateContentEncryptionKeyfdsdf = async ({type = 'Secp256r1', ...rest}: {
|
|
61
|
+
type?: Extract<TKeyType, 'Secp256r1' | 'RSA'>,
|
|
62
|
+
kms?: string
|
|
63
|
+
}, context: IAgentContext<ISphereonKeyManager>): Promise<EphemeralPublicKey> => {
|
|
64
|
+
|
|
65
|
+
const kms = rest.kms ?? await context.agent.keyManagerGetDefaultKeyManagementSystem()
|
|
66
|
+
const key = await context.agent.keyManagerCreate({kms, type, opts: {ephemeral: true}})
|
|
67
|
+
const jwk = toJwkFromKey(key, {use: JwkKeyUse.Encryption, noKidThumbprint: true})
|
|
68
|
+
}
|
|
69
|
+
*/
|
|
70
|
+
export interface JwtEncrypter {
|
|
71
|
+
alg: string
|
|
72
|
+
enc: string
|
|
73
|
+
encrypt: (payload: JwsPayload, protectedHeader: JweProtectedHeader, aad?: Uint8Array) => Promise<EncryptionResult>
|
|
74
|
+
encryptCek?: (cek: Uint8Array) => Promise<JweRecipient>
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
export interface JweEncrypter {
|
|
79
|
+
alg: string
|
|
80
|
+
enc: string
|
|
81
|
+
encrypt: (payload: Uint8Array, protectedHeader: JweProtectedHeader, aad?: Uint8Array) => Promise<EncryptionResult>
|
|
82
|
+
encryptCek?: (cek: Uint8Array) => Promise<JweRecipient>
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface JweDecrypter {
|
|
86
|
+
alg: string
|
|
87
|
+
enc: string
|
|
88
|
+
decrypt: (sealed: Uint8Array, iv: Uint8Array, aad?: Uint8Array, recipient?: JweRecipient) => Promise<Uint8Array | null>
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function jweAssertValid(jwe: JweJsonGeneral) {
|
|
92
|
+
if (!(jwe.protected && jwe.iv && jwe.ciphertext && jwe.tag)) {
|
|
93
|
+
throw Error('JWE is missing properties: protected, iv, ciphertext and/or tag')
|
|
94
|
+
}
|
|
95
|
+
if (jwe.recipients) {
|
|
96
|
+
jwe.recipients.map((recipient: JweRecipient) => {
|
|
97
|
+
if (!(recipient.header && recipient.encrypted_key)) {
|
|
98
|
+
throw Error('Malformed JWE recipients; no header and encrypted key present')
|
|
99
|
+
}
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function jweEncode({ciphertext, tag, iv, protectedHeader, recipients, aad, unprotected}: EncryptionResult & {
|
|
105
|
+
aad?: Uint8Array,
|
|
106
|
+
unprotected?: JweHeader
|
|
107
|
+
}): JweJsonGeneral {
|
|
108
|
+
if (!recipients || recipients.length === 0) {
|
|
109
|
+
throw Error(`No recipient found`)
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
...(unprotected && {unprotected}),
|
|
113
|
+
protected: <string>protectedHeader,
|
|
114
|
+
iv: bytesToBase64url(iv),
|
|
115
|
+
ciphertext: bytesToBase64url(ciphertext),
|
|
116
|
+
...(tag && {tag: bytesToBase64url(tag)}),
|
|
117
|
+
...(aad && {aad: bytesToBase64url(aad)}),
|
|
118
|
+
recipients
|
|
119
|
+
} satisfies JweJsonGeneral
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export class CompactJwtEncrypter implements JweEncrypter {
|
|
123
|
+
private _alg: JweAlg | undefined;
|
|
124
|
+
private _enc: JweEnc | undefined;
|
|
125
|
+
private _keyManagementParams: JWEKeyManagementHeaderParameters | undefined
|
|
126
|
+
private recipientKey: Uint8Array | jose.KeyLike //,EphemeralPublicKey | BaseJWK;
|
|
127
|
+
private expirationTime
|
|
128
|
+
private issuer: string | undefined
|
|
129
|
+
private audience: string | string[] | undefined
|
|
130
|
+
|
|
131
|
+
constructor(args: {
|
|
132
|
+
key: Uint8Array | jose.KeyLike /*EphemeralPublicKey | BaseJWK*/,
|
|
133
|
+
alg?: JweAlg,
|
|
134
|
+
enc?: JweEnc,
|
|
135
|
+
keyManagementParams?: JWEKeyManagementHeaderParameters,
|
|
136
|
+
expirationTime?: number | string | Date
|
|
137
|
+
issuer?: string
|
|
138
|
+
audience?: string | string[]
|
|
139
|
+
}) {
|
|
140
|
+
if (args?.alg) {
|
|
141
|
+
this._alg = args.alg
|
|
142
|
+
}
|
|
143
|
+
if (args?.enc) {
|
|
144
|
+
this._enc = args.enc
|
|
145
|
+
}
|
|
146
|
+
this._keyManagementParams = args.keyManagementParams
|
|
147
|
+
this.recipientKey = args.key
|
|
148
|
+
this.expirationTime = args.expirationTime
|
|
149
|
+
this.issuer = args.issuer
|
|
150
|
+
this.audience = args.audience
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
get enc(): string {
|
|
154
|
+
if (!this._enc) {
|
|
155
|
+
throw Error(`enc not set`)
|
|
156
|
+
}
|
|
157
|
+
return this._enc;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
set enc(value: JweEnc | string) {
|
|
161
|
+
// @ts-ignore
|
|
162
|
+
if (!JweEncs.includes(value)) {
|
|
163
|
+
throw Error(`invalid JWE enc value ${value}`)
|
|
164
|
+
}
|
|
165
|
+
this._enc = value as JweEnc;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
get alg(): string {
|
|
169
|
+
if (!this._alg) {
|
|
170
|
+
throw Error(`alg not set`)
|
|
171
|
+
}
|
|
172
|
+
return this._alg;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
set alg(value: JweAlg | string) {
|
|
176
|
+
// @ts-ignore
|
|
177
|
+
if (!JweAlgs.includes(value)) {
|
|
178
|
+
throw Error(`invalid JWE alg value ${value}`)
|
|
179
|
+
}
|
|
180
|
+
this._alg = value as JweAlg;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async encryptCompactJWT(
|
|
184
|
+
payload: JwsPayload,
|
|
185
|
+
jweProtectedHeader: JweProtectedHeader,
|
|
186
|
+
aad?: Uint8Array | undefined
|
|
187
|
+
): Promise<string> {
|
|
188
|
+
const protectedHeader = {
|
|
189
|
+
...jweProtectedHeader,
|
|
190
|
+
alg: jweProtectedHeader.alg ?? this._alg,
|
|
191
|
+
enc: jweProtectedHeader.enc ?? this._enc
|
|
192
|
+
}
|
|
193
|
+
if (!protectedHeader.alg || !protectedHeader.enc) {
|
|
194
|
+
return Promise.reject(Error(`no 'alg' or 'enc' value set for the protected JWE header!`))
|
|
195
|
+
}
|
|
196
|
+
this.enc = protectedHeader.enc
|
|
197
|
+
this.alg = protectedHeader.alg
|
|
198
|
+
if (payload.exp) {
|
|
199
|
+
this.expirationTime = payload.exp
|
|
200
|
+
}
|
|
201
|
+
if (payload.iss) {
|
|
202
|
+
this.issuer = payload.iss
|
|
203
|
+
}
|
|
204
|
+
if (payload.aud) {
|
|
205
|
+
this.audience = payload.aud
|
|
206
|
+
}
|
|
207
|
+
const encrypt = new jose.EncryptJWT(payload).setProtectedHeader({
|
|
208
|
+
...protectedHeader,
|
|
209
|
+
alg: this.alg,
|
|
210
|
+
enc: this.enc
|
|
211
|
+
})
|
|
212
|
+
if (this._alg!.startsWith('ECDH')) {
|
|
213
|
+
if (!this._keyManagementParams) {
|
|
214
|
+
return Promise.reject(Error(`ECDH requires key management params`))
|
|
215
|
+
}
|
|
216
|
+
encrypt.setKeyManagementParameters(this._keyManagementParams!)
|
|
217
|
+
}
|
|
218
|
+
if(this.expirationTime !== undefined) {
|
|
219
|
+
encrypt.setExpirationTime(this.expirationTime)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (this.issuer) {
|
|
223
|
+
encrypt.setIssuer(this.issuer)
|
|
224
|
+
}
|
|
225
|
+
if (this.audience) {
|
|
226
|
+
encrypt.setAudience(this.audience)
|
|
227
|
+
}
|
|
228
|
+
return await encrypt.encrypt(this.recipientKey)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
public static async decryptCompactJWT(jwt: string, key: KeyLike | Uint8Array, options?: JWTDecryptOptions) {
|
|
232
|
+
return await jose.jwtDecrypt(jwt, key, options)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async encrypt(
|
|
236
|
+
payload: Uint8Array,
|
|
237
|
+
jweProtectedHeader: JweProtectedHeader,
|
|
238
|
+
aad?: Uint8Array | undefined
|
|
239
|
+
): Promise<EncryptionResult> {
|
|
240
|
+
const jwt = await this.encryptCompactJWT(JSON.parse(u8a.toString(payload)), jweProtectedHeader, aad)
|
|
241
|
+
const [protectedHeader, encryptedKey, ivB64, payloadB64, tagB64,] = jwt.split('.')
|
|
242
|
+
//[jwe.protected, jwe.encrypted_key, jwe.iv, jwe.ciphertext, jwe.tag].join('.');
|
|
243
|
+
console.log(`FIXME: TO EncryptionResult`)
|
|
244
|
+
|
|
245
|
+
return {
|
|
246
|
+
protectedHeader,
|
|
247
|
+
tag: base64ToBytes(tagB64),
|
|
248
|
+
ciphertext: base64ToBytes(payloadB64),
|
|
249
|
+
iv: base64ToBytes(ivB64),
|
|
250
|
+
recipients: [
|
|
251
|
+
|
|
252
|
+
{
|
|
253
|
+
//fixme
|
|
254
|
+
// header: protectedHeader,
|
|
255
|
+
...(encryptedKey && { encrypted_key: encryptedKey})
|
|
256
|
+
|
|
257
|
+
}
|
|
258
|
+
]
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// encryptCek?: ((cek: Uint8Array) => Promise<JweRecipient>) | undefined;
|
|
264
|
+
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export async function createJwe(
|
|
268
|
+
cleartext: Uint8Array,
|
|
269
|
+
encrypters: JweEncrypter[],
|
|
270
|
+
protectedHeader: JweProtectedHeader,
|
|
271
|
+
aad?: Uint8Array
|
|
272
|
+
): Promise<JweJsonGeneral> {
|
|
273
|
+
if (encrypters.length === 0) {
|
|
274
|
+
throw Error('JWE needs at least 1 encryptor')
|
|
275
|
+
}
|
|
276
|
+
if (encrypters.find(enc => enc.alg === 'dir' || enc.alg === 'ECDH-ES')) {
|
|
277
|
+
if (encrypters.length !== 1) {
|
|
278
|
+
throw Error(`JWE can only do "dir" or "ECDH-ES" encryption with one key. ${encrypters.length} supplied`)
|
|
279
|
+
}
|
|
280
|
+
const encryptionResult = await encrypters[0].encrypt(cleartext, protectedHeader, aad)
|
|
281
|
+
return jweEncode({...encryptionResult, aad})
|
|
282
|
+
} else {
|
|
283
|
+
const tmpEnc = encrypters[0].enc
|
|
284
|
+
if (!encrypters.reduce((acc, encrypter) => acc && encrypter.enc === tmpEnc, true)) {
|
|
285
|
+
throw new Error('invalid_argument: Incompatible encrypters passed')
|
|
286
|
+
}
|
|
287
|
+
let cek: Uint8Array | undefined = undefined
|
|
288
|
+
let jwe: JweJsonGeneral | undefined = undefined
|
|
289
|
+
for (const encrypter of encrypters) {
|
|
290
|
+
if (!cek) {
|
|
291
|
+
const encryptionResult = await encrypter.encrypt(cleartext, protectedHeader, aad)
|
|
292
|
+
cek = encryptionResult.cek
|
|
293
|
+
jwe = jweEncode({...encryptionResult, aad})
|
|
294
|
+
} else {
|
|
295
|
+
const recipient = await encrypter.encryptCek?.(cek)
|
|
296
|
+
if (recipient) {
|
|
297
|
+
jwe?.recipients?.push(recipient)
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
if (!jwe) {
|
|
302
|
+
throw Error(`No JWE constructed`)
|
|
303
|
+
}
|
|
304
|
+
return jwe
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Merges all headers, so we get a unified header.
|
|
310
|
+
*
|
|
311
|
+
* @param protectedHeader
|
|
312
|
+
* @param unprotectedHeader
|
|
313
|
+
* @param recipientUnprotectedHeader
|
|
314
|
+
*/
|
|
315
|
+
export function jweMergeHeaders({protectedHeader, unprotectedHeader, recipientUnprotectedHeader}: {
|
|
316
|
+
protectedHeader?: JweProtectedHeader,
|
|
317
|
+
unprotectedHeader?: JweHeader,
|
|
318
|
+
recipientUnprotectedHeader?: JweRecipientUnprotectedHeader
|
|
319
|
+
}): JweHeader {
|
|
320
|
+
// TODO: Check that all headers/params are disjoint!
|
|
321
|
+
const header = {...protectedHeader, ...unprotectedHeader, ...recipientUnprotectedHeader}
|
|
322
|
+
|
|
323
|
+
if (!header.alg || !header.enc) {
|
|
324
|
+
throw Error(`Either 'alg' or 'enc' are missing from the headers`)
|
|
325
|
+
}
|
|
326
|
+
return header as JweHeader
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export async function decryptJwe(jwe: JweJsonGeneral, decrypter: JweDecrypter): Promise<Uint8Array> {
|
|
330
|
+
jweAssertValid(jwe)
|
|
331
|
+
const protectedHeader: JweProtectedHeader = JSON.parse(decodeBase64url(jwe.protected))
|
|
332
|
+
if (protectedHeader?.enc !== decrypter.enc) {
|
|
333
|
+
return Promise.reject(Error(`Decrypter enc '${decrypter.enc}' does not support header enc '${protectedHeader.enc}'`))
|
|
334
|
+
} else if (!jwe.tag) {
|
|
335
|
+
return Promise.reject(Error(`Decrypter enc '${decrypter.enc}' does not support header enc '${protectedHeader.enc}'`))
|
|
336
|
+
}
|
|
337
|
+
const sealed = toWebCryptoCiphertext(jwe.ciphertext, jwe.tag)
|
|
338
|
+
const aad = u8a.fromString(jwe.aad ? `${jwe.protected}.${jwe.aad}` : jwe.protected)
|
|
339
|
+
let cleartext = null
|
|
340
|
+
if (protectedHeader.alg === 'dir' && decrypter.alg === 'dir') {
|
|
341
|
+
cleartext = await decrypter.decrypt(sealed, base64ToBytes(jwe.iv), aad)
|
|
342
|
+
} else if (!jwe.recipients || jwe.recipients.length === 0) {
|
|
343
|
+
throw Error('missing recipients for JWE')
|
|
344
|
+
} else {
|
|
345
|
+
for (let i = 0; !cleartext && i < jwe.recipients.length; i++) {
|
|
346
|
+
const recipient: JweRecipient = jwe.recipients[i]
|
|
347
|
+
recipient.header = {...recipient.header, ...protectedHeader} as JweRecipientUnprotectedHeader
|
|
348
|
+
if (recipient.header.alg === decrypter.alg) {
|
|
349
|
+
cleartext = await decrypter.decrypt(sealed, base64ToBytes(jwe.iv), aad, recipient)
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
if (cleartext === null) throw new Error('failure: Failed to decrypt')
|
|
354
|
+
return cleartext
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
export function toWebCryptoCiphertext(ciphertext: string, tag: string): Uint8Array {
|
|
359
|
+
return u8a.concat([base64ToBytes(ciphertext), base64ToBytes(tag)])
|
|
360
|
+
}
|