@sphereon/ssi-sdk-ext.jwt-service 0.24.1-unstable.93 → 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.
@@ -1,47 +1,111 @@
1
- import { IAgentPlugin } from '@veramo/core'
1
+ import {IAgentPlugin} from '@veramo/core'
2
2
  import {
3
- createJwsCompact,
4
- CreateJwsCompactArgs,
5
- CreateJwsFlattenedArgs,
6
- CreateJwsJsonArgs,
7
- createJwsJsonFlattened,
8
- createJwsJsonGeneral,
9
- IJwtService,
10
- IRequiredContext,
11
- JwsCompactResult,
12
- JwsJsonFlattened,
13
- JwsJsonGeneral,
14
- PreparedJwsObject,
15
- prepareJwsObject,
16
- schema,
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
- readonly schema = schema.IMnemonicInfoGenerator
24
- readonly methods: IJwtService = {
25
- jwtPrepareJws: this.jwtPrepareJws.bind(this),
26
- jwtCreateJwsJsonGeneralSignature: this.jwtCreateJwsJsonGeneralSignature.bind(this),
27
- jwtCreateJwsJsonFlattenedSignature: this.jwtCreateJwsJsonFlattenedSignature.bind(this),
28
- jwtCreateJwsCompactSignature: this.jwtCreateJwsCompactSignature.bind(this),
29
- }
30
-
31
- private async jwtPrepareJws(args: CreateJwsJsonArgs, context: IRequiredContext): Promise<PreparedJwsObject> {
32
- return await prepareJwsObject(args, context)
33
- }
34
-
35
- private async jwtCreateJwsJsonGeneralSignature(args: CreateJwsJsonArgs, context: IRequiredContext): Promise<JwsJsonGeneral> {
36
- return await createJwsJsonGeneral(args, context)
37
- }
38
-
39
- private async jwtCreateJwsJsonFlattenedSignature(args: CreateJwsFlattenedArgs, context: IRequiredContext): Promise<JwsJsonFlattened> {
40
- return await createJwsJsonFlattened(args, context)
41
- }
42
-
43
- private async jwtCreateJwsCompactSignature(args: CreateJwsCompactArgs, context: IRequiredContext): Promise<JwsCompactResult> {
44
- // We wrap it in a json object for remote REST calls
45
- return { jwt: await createJwsCompact(args, context) }
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
+ }