@enbox/agent 0.3.1 → 0.4.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.
Files changed (57) hide show
  1. package/dist/browser.mjs +12 -30
  2. package/dist/browser.mjs.map +4 -4
  3. package/dist/esm/connect.js +20 -24
  4. package/dist/esm/connect.js.map +1 -1
  5. package/dist/esm/dwn-api.js +149 -22
  6. package/dist/esm/dwn-api.js.map +1 -1
  7. package/dist/esm/dwn-discovery-file.js +1 -1
  8. package/dist/esm/dwn-discovery-payload.js +20 -21
  9. package/dist/esm/dwn-discovery-payload.js.map +1 -1
  10. package/dist/esm/dwn-key-delivery.js.map +1 -1
  11. package/dist/esm/{oidc.js → enbox-connect-protocol.js} +236 -248
  12. package/dist/esm/enbox-connect-protocol.js.map +1 -0
  13. package/dist/esm/enbox-user-agent.js +18 -5
  14. package/dist/esm/enbox-user-agent.js.map +1 -1
  15. package/dist/esm/index.js +4 -2
  16. package/dist/esm/index.js.map +1 -1
  17. package/dist/esm/local-dwn.js +21 -51
  18. package/dist/esm/local-dwn.js.map +1 -1
  19. package/dist/esm/permissions-api.js.map +1 -1
  20. package/dist/esm/store-data.js.map +1 -1
  21. package/dist/esm/sync-engine-level.js +1 -1
  22. package/dist/esm/sync-engine-level.js.map +1 -1
  23. package/dist/esm/sync-messages.js +1 -1
  24. package/dist/esm/sync-messages.js.map +1 -1
  25. package/dist/types/connect.d.ts +15 -19
  26. package/dist/types/connect.d.ts.map +1 -1
  27. package/dist/types/dwn-api.d.ts +46 -6
  28. package/dist/types/dwn-api.d.ts.map +1 -1
  29. package/dist/types/dwn-discovery-file.d.ts +1 -1
  30. package/dist/types/dwn-discovery-payload.d.ts +18 -19
  31. package/dist/types/dwn-discovery-payload.d.ts.map +1 -1
  32. package/dist/types/enbox-connect-protocol.d.ts +220 -0
  33. package/dist/types/enbox-connect-protocol.d.ts.map +1 -0
  34. package/dist/types/enbox-user-agent.d.ts +10 -1
  35. package/dist/types/enbox-user-agent.d.ts.map +1 -1
  36. package/dist/types/index.d.ts +1 -2
  37. package/dist/types/index.d.ts.map +1 -1
  38. package/dist/types/local-dwn.d.ts +16 -32
  39. package/dist/types/local-dwn.d.ts.map +1 -1
  40. package/package.json +9 -11
  41. package/src/connect.ts +38 -47
  42. package/src/dwn-api.ts +175 -29
  43. package/src/dwn-discovery-file.ts +1 -1
  44. package/src/dwn-discovery-payload.ts +23 -24
  45. package/src/dwn-key-delivery.ts +1 -1
  46. package/src/enbox-connect-protocol.ts +778 -0
  47. package/src/enbox-user-agent.ts +27 -4
  48. package/src/index.ts +4 -2
  49. package/src/local-dwn.ts +22 -53
  50. package/src/permissions-api.ts +3 -3
  51. package/src/store-data.ts +1 -1
  52. package/src/sync-engine-level.ts +1 -1
  53. package/src/sync-messages.ts +1 -1
  54. package/dist/esm/oidc.js.map +0 -1
  55. package/dist/types/oidc.d.ts +0 -250
  56. package/dist/types/oidc.d.ts.map +0 -1
  57. package/src/oidc.ts +0 -864
package/src/oidc.ts DELETED
@@ -1,864 +0,0 @@
1
- import type { ConnectPermissionRequest } from './connect.js';
2
- import type { EnboxAgent } from './types/agent.js';
3
- import type { RequireOnly } from '@enbox/common';
4
- import type { DidDocument, PortableDid } from '@enbox/dids';
5
- import type { DwnDataEncodedRecordsWriteMessage, DwnPermissionScope, DwnProtocolDefinition } from './types/dwn.js';
6
- import type {
7
- JoseHeaderParams,
8
- Jwk } from '@enbox/crypto';
9
-
10
- import { type BearerDid, DidJwk } from '@enbox/dids';
11
- import { Convert, logger } from '@enbox/common';
12
- import {
13
- CryptoUtils,
14
- Ed25519,
15
- EdDsaAlgorithm,
16
- Hkdf,
17
- Sha256,
18
- X25519,
19
- XChaCha20Poly1305,
20
- } from '@enbox/crypto';
21
- import { DwnInterfaceName, DwnMethodName } from '@enbox/dwn-sdk-js';
22
-
23
- import { AgentPermissionsApi } from './permissions-api.js';
24
- import { concatenateUrl } from './utils.js';
25
- import { DwnInterface } from './types/dwn.js';
26
- import { isRecordPermissionScope } from './dwn-api.js';
27
-
28
- /**
29
- * Sent to an OIDC server to authorize a client. Allows clients
30
- * to securely send authorization request parameters directly to
31
- * the server via POST. This avoids exposing sensitive data in URLs
32
- * and ensures the server validates the request before user interaction.
33
- *
34
- * @see {@link https://www.rfc-editor.org/rfc/rfc9126.html | OAuth 2.0 Pushed Authorization Requests}
35
- */
36
- export type PushedAuthRequest = {
37
- /** The JWT which contains the {@link EnboxConnectAuthRequest} */
38
- request: string;
39
- };
40
-
41
- /**
42
- * Sent back by OIDC server in response to {@link PushedAuthRequest}
43
- * The server generates a TTL and a unique request_uri. The request_uri can be shared
44
- * with the Provider using a link or a QR code along with additional params
45
- * to access the url and decrypt the payload.
46
- */
47
- export type PushedAuthResponse = {
48
- request_uri: string;
49
- expires_in: number;
50
- };
51
-
52
- /**
53
- * Used in decentralized apps. The SIOPv2 Auth Request is created by a client relying party (RP)
54
- * often a web service or an app who wants to obtain information from a provider
55
- * The contents of this are inserted into a JWT inside of the {@link PushedAuthRequest}.
56
- * @see {@link https://github.com/enboxorg/enbox | Enbox OIDC Documentation for SIOPv2 }
57
- */
58
- export type SIOPv2AuthRequest = {
59
- /** The DID of the client (RP) */
60
- client_id: string;
61
-
62
- /** The scope of the access request (e.g., `openid profile`). */
63
- scope: string;
64
-
65
- /** The type of response desired (e.g. `id_token`) */
66
- response_type: string;
67
-
68
- /** the URL to which the Identity Provider will post the Authorization Response */
69
- redirect_uri: string;
70
-
71
- /** The URI to which the SIOPv2 Authorization Response will be sent (Tim's note: not used with encrypted request JWT)*/
72
- response_uri?: string;
73
-
74
- /**
75
- * An opaque value used to maintain state between the request and the callback.
76
- * Recommended for security to prevent CSRF attacks.
77
- */
78
- state: string;
79
-
80
- /**
81
- * A string value used to associate a client session with an ID token to mitigate replay attacks.
82
- * Recommended when requesting ID tokens.
83
- */
84
- nonce: string;
85
-
86
- /**
87
- * The PKCE code challenge.
88
- * Required if `code_challenge_method` is used. Enhances security for public clients (e.g., single-page apps,
89
- * mobile apps) by requiring an additional verification step during token exchange.
90
- */
91
- code_challenge?: string;
92
-
93
- /** The method used for the PKCE challenge (typically `S256`). Must be present if `code_challenge` is included. */
94
- code_challenge_method?: 'S256';
95
-
96
- /**
97
- * An ID token previously issued to the client, passed as a hint about the end-user’s current or past authenticated
98
- * session with the client. Can streamline user experience if already logged in.
99
- */
100
- id_token_hint?: string;
101
-
102
- /** A hint to the authorization server about the login identifier the user might use. Useful for pre-filling login information. */
103
- login_hint?: string;
104
-
105
- /** Requested Authentication Context Class Reference values. Specifies the authentication context requirements. */
106
- acr_values?: string;
107
-
108
- /** When using a PAR for secure cross device flows we use a "form_post" rather than a "direct_post" */
109
- response_mode: 'direct_post';
110
-
111
- /** Used by PFI to request VCs as input to IDV process. If present, `response_type: "vp_token""` MUST also be present */
112
- presentation_definition?: any;
113
-
114
- /** A JSON object containing the Verifier metadata values (Tim's note: from TBD KCC Repo) */
115
- client_metadata?: {
116
- /** Array of strings, each a DID method supported for the subject of ID Token */
117
- subject_syntax_types_supported: string[];
118
- /** Human-readable string name of the client to be presented to the end-user during authorization */
119
- client_name?: string;
120
- /** URI of a web page providing information about the client */
121
- client_uri?: string;
122
- /** URI of an image logo for the client */
123
- logo_uri?: string;
124
- /** Array of strings representing ways to contact people responsible for this client, typically email addresses */
125
- contacts?: string[];
126
- /** URI that points to a terms of service document for the client */
127
- tos_uri?: string;
128
- /** URI that points to a privacy policy document */
129
- policy_uri?: string;
130
- };
131
- };
132
-
133
- /**
134
- * An auth request that is compatible with both Web5 Connect and (hopefully, WIP) OIDC SIOPv2
135
- * The contents of this are inserted into a JWT inside of the {@link PushedAuthRequest}.
136
- */
137
- export type EnboxConnectAuthRequest = {
138
- /** The user friendly name of the client/app to be displayed when prompting end-user with permission requests. */
139
- displayName: string;
140
-
141
- /** PermissionGrants that are to be sent to the provider */
142
- permissionRequests: ConnectPermissionRequest[];
143
- } & SIOPv2AuthRequest;
144
-
145
- /** The fields for an OIDC SIOPv2 Auth Repsonse */
146
- export type SIOPv2AuthResponse = {
147
- /** Issuer MUST match the value of sub (Applicant's DID) */
148
- iss: string;
149
- /** Subject Identifier. A locally unique and never reassigned identifier
150
- * within the Issuer for the End-User, which is intended to be consumed
151
- * by the Client. */
152
- sub: string;
153
- /** Audience(s) that this ID Token is intended for. It MUST contain the
154
- * OAuth 2.0 client_id of the Relying Party as an audience value. */
155
- aud: string;
156
- /** Time at which the JWT was issued. */
157
- iat: number;
158
- /** Expiration time on or after which the ID Token MUST NOT be accepted
159
- * for processing. */
160
- exp: number;
161
- /** Time when the End-User authentication occurred. */
162
- auth_time?: number;
163
- /** b64url encoded nonce used to associate a Client session with an ID Token, and to
164
- * mitigate replay attacks. */
165
- nonce?: string;
166
- /** Custom claims. */
167
- [key: string]: any;
168
- };
169
-
170
- /** An auth response that is compatible with both Web5 Connect and (hopefully, WIP) OIDC SIOPv2 */
171
- export type EnboxConnectAuthResponse = {
172
- delegateGrants: DwnDataEncodedRecordsWriteMessage[];
173
- delegatePortableDid: PortableDid;
174
- } & SIOPv2AuthResponse;
175
-
176
- /** Represents the different OIDC endpoint types.
177
- * 1. `pushedAuthorizationRequest`: client sends {@link PushedAuthRequest} receives {@link PushedAuthResponse}
178
- * 2. `authorize`: provider gets the {@link EnboxConnectAuthRequest} JWT that was stored by the PAR
179
- * 3. `callback`: provider sends {@link EnboxConnectAuthResponse} to this endpoint
180
- * 4. `token`: client gets {@link EnboxConnectAuthResponse} from this endpoint
181
- */
182
- type OidcEndpoint =
183
- | 'pushedAuthorizationRequest'
184
- | 'authorize'
185
- | 'callback'
186
- | 'token';
187
-
188
- /**
189
- * Gets the correct OIDC endpoint out of the {@link OidcEndpoint} options provided.
190
- * Handles a trailing slash on baseURL
191
- *
192
- * @param {Object} options the options object
193
- * @param {string} options.baseURL for example `http://foo.com/connect/
194
- * @param {OidcEndpoint} options.endpoint the OIDC endpoint desired
195
- * @param {string} options.authParam this is the unique id which must be provided when getting the `authorize` endpoint
196
- * @param {string} options.tokenParam this is the random state as b64url which must be provided with the `token` endpoint
197
- */
198
- function buildOidcUrl({
199
- baseURL,
200
- endpoint,
201
- authParam,
202
- tokenParam,
203
- }: {
204
- baseURL: string;
205
- endpoint: OidcEndpoint;
206
- authParam?: string;
207
- tokenParam?: string;
208
- }): string {
209
- switch (endpoint) {
210
- /** 1. client sends {@link PushedAuthRequest} & client receives {@link PushedAuthResponse} */
211
- case 'pushedAuthorizationRequest':
212
- return concatenateUrl(baseURL, 'par');
213
- /** 2. provider gets {@link EnboxConnectAuthRequest} */
214
- case 'authorize':
215
- if (!authParam)
216
- {throw new Error(
217
- `authParam must be providied when building a token URL`
218
- );}
219
- return concatenateUrl(baseURL, `authorize/${authParam}.jwt`);
220
- /** 3. provider sends {@link EnboxConnectAuthResponse} */
221
- case 'callback':
222
- return concatenateUrl(baseURL, `callback`);
223
- /** 4. client gets {@link EnboxConnectAuthResponse */
224
- case 'token':
225
- if (!tokenParam)
226
- {throw new Error(
227
- `tokenParam must be providied when building a token URL`
228
- );}
229
- return concatenateUrl(baseURL, `token/${tokenParam}.jwt`);
230
- // TODO: metadata endpoints?
231
- default:
232
- throw new Error(`No matches for endpoint specified: ${endpoint}`);
233
- }
234
- }
235
-
236
- /**
237
- * Generates a cryptographically random "code challenge" in
238
- * accordance with the RFC 7636 PKCE specification.
239
- *
240
- * @see {@link https://datatracker.ietf.org/doc/html/rfc7636#section-4.2 | RFC 7636 }
241
- */
242
- async function generateCodeChallenge(): Promise<{ codeChallengeBytes: Uint8Array; codeChallengeBase64Url: string }> {
243
- const codeVerifierBytes = CryptoUtils.randomBytes(32);
244
- const codeChallengeBytes = await Sha256.digest({ data: codeVerifierBytes });
245
- const codeChallengeBase64Url =
246
- Convert.uint8Array(codeChallengeBytes).toBase64Url();
247
-
248
- return { codeChallengeBytes, codeChallengeBase64Url };
249
- }
250
-
251
- /** Client creates the {@link EnboxConnectAuthRequest} */
252
- async function createAuthRequest(
253
- options: RequireOnly<
254
- EnboxConnectAuthRequest,
255
- 'client_id' | 'scope' | 'redirect_uri' | 'permissionRequests' | 'displayName'
256
- >
257
- ): Promise<EnboxConnectAuthRequest> {
258
- // Generate a random state value to associate the authorization request with the response.
259
- const stateBytes = CryptoUtils.randomBytes(16);
260
-
261
- // Generate a random nonce value to associate the ID Token with the authorization request.
262
- const nonceBytes = CryptoUtils.randomBytes(16);
263
-
264
- const requestObject: EnboxConnectAuthRequest = {
265
- ...options,
266
- nonce : Convert.uint8Array(nonceBytes).toBase64Url(),
267
- response_type : 'id_token',
268
- response_mode : 'direct_post',
269
- state : Convert.uint8Array(stateBytes).toBase64Url(),
270
- client_metadata : {
271
- subject_syntax_types_supported: ['did:dht', 'did:jwk'],
272
- },
273
- };
274
-
275
- return requestObject;
276
- }
277
-
278
- /** Encrypts the auth request with the key which will be passed through QR code */
279
- async function encryptAuthRequest({
280
- jwt,
281
- encryptionKey,
282
- }: {
283
- jwt: string;
284
- encryptionKey: Uint8Array;
285
- }): Promise<string> {
286
- const protectedHeader = {
287
- alg : 'dir',
288
- cty : 'JWT',
289
- enc : 'XC20P',
290
- typ : 'JWT',
291
- };
292
- const nonce = CryptoUtils.randomBytes(24);
293
- const additionalData = Convert.object(protectedHeader).toUint8Array();
294
- const jwtBytes = Convert.string(jwt).toUint8Array();
295
- const ciphertextAndTag = await XChaCha20Poly1305.encryptRaw({ data: jwtBytes, keyBytes: encryptionKey, nonce, additionalData });
296
-
297
- /** The cipher output concatenates the encrypted data and tag
298
- * so we need to extract the values for use in the JWE. */
299
- const ciphertext = ciphertextAndTag.subarray(0, -16);
300
- const authenticationTag = ciphertextAndTag.subarray(-16);
301
-
302
- const compactJwe = [
303
- Convert.object(protectedHeader).toBase64Url(),
304
- '', // Empty string since there is no wrapped key.
305
- Convert.uint8Array(nonce).toBase64Url(),
306
- Convert.uint8Array(ciphertext).toBase64Url(),
307
- Convert.uint8Array(authenticationTag).toBase64Url(),
308
- ].join('.');
309
-
310
- return compactJwe;
311
- }
312
-
313
- /** Create a response object compatible with Web5 Connect and OIDC SIOPv2 */
314
- async function createResponseObject(
315
- options: RequireOnly<
316
- EnboxConnectAuthResponse,
317
- 'iss' | 'sub' | 'aud' | 'delegateGrants' | 'delegatePortableDid'
318
- >
319
- ): Promise<EnboxConnectAuthResponse> {
320
- const currentTimeInSeconds = Math.floor(Date.now() / 1000);
321
-
322
- const responseObject: EnboxConnectAuthResponse = {
323
- ...options,
324
- iat : currentTimeInSeconds,
325
- exp : currentTimeInSeconds + 600, // Expires in 10 minutes.
326
- };
327
-
328
- return responseObject;
329
- }
330
-
331
- /** sign an object and transform it into a jwt using a did */
332
- async function signJwt({
333
- did,
334
- data,
335
- }: {
336
- did: BearerDid;
337
- data: Record<string, unknown>;
338
- }): Promise<string> {
339
- const header = Convert.object({
340
- alg : 'EdDSA',
341
- kid : did.document.verificationMethod![0].id,
342
- typ : 'JWT',
343
- }).toBase64Url();
344
-
345
- const payload = Convert.object(data).toBase64Url();
346
-
347
- // signs using ed25519 EdDSA
348
- const signer = await did.getSigner();
349
- const signature = await signer.sign({
350
- data: Convert.string(`${header}.${payload}`).toUint8Array(),
351
- });
352
-
353
- const signatureBase64Url = Convert.uint8Array(signature).toBase64Url();
354
-
355
- const jwt = `${header}.${payload}.${signatureBase64Url}`;
356
-
357
- return jwt;
358
- }
359
-
360
- /** Take the decrypted JWT and verify it was signed by its public DID. Return parsed object. */
361
- async function verifyJwt({ jwt }: { jwt: string }): Promise<Record<string, unknown>> {
362
- const [headerB64U, payloadB64U, signatureB64U] = jwt.split('.');
363
-
364
- // Convert the header back to a JOSE object and verify that the 'kid' header value is present.
365
- const header: JoseHeaderParams = Convert.base64Url(headerB64U).toObject();
366
-
367
- if (!header.kid)
368
- {throw new Error(
369
- `OIDC: Object could not be verified due to missing 'kid' header value.`
370
- );}
371
-
372
- // Resolve the Client DID document.
373
- const { didDocument } = await DidJwk.resolve(header.kid.split('#')[0]);
374
-
375
- if (!didDocument)
376
- {throw new Error(
377
- 'OIDC: Object could not be verified due to Client DID resolution issue.'
378
- );}
379
-
380
- // Get the public key used to sign the Object from the DID document.
381
- const { publicKeyJwk } =
382
- didDocument.verificationMethod?.find((method: any) => {
383
- return method.id === header.kid;
384
- }) ?? {};
385
-
386
- if (!publicKeyJwk)
387
- {throw new Error(
388
- 'OIDC: Object could not be verified due to missing public key in DID document.'
389
- );}
390
-
391
- const EdDsa = new EdDsaAlgorithm();
392
- const isValid = await EdDsa.verify({
393
- key : publicKeyJwk,
394
- signature : Convert.base64Url(signatureB64U).toUint8Array(),
395
- data : Convert.string(`${headerB64U}.${payloadB64U}`).toUint8Array(),
396
- });
397
-
398
- if (!isValid)
399
- {throw new Error(
400
- 'OIDC: Object failed verification due to invalid signature.'
401
- );}
402
-
403
- const object = Convert.base64Url(payloadB64U).toObject() as Record<string, unknown>;
404
-
405
- return object;
406
- }
407
-
408
- /**
409
- * Fetches the {@EnboxConnectAuthRequest} from the authorize endpoint and decrypts it
410
- * using the encryption key passed via QR code.
411
- */
412
- const getAuthRequest = async (request_uri: string, encryption_key: string): Promise<EnboxConnectAuthRequest> => {
413
- const authRequest = await fetch(request_uri, { signal: AbortSignal.timeout(30_000) });
414
- const jwe = await authRequest.text();
415
- const jwt = await decryptAuthRequest({
416
- jwe,
417
- encryption_key,
418
- });
419
- const web5ConnectAuthRequest = (await verifyJwt({
420
- jwt,
421
- })) as EnboxConnectAuthRequest;
422
-
423
- return web5ConnectAuthRequest;
424
- };
425
-
426
- /** Take the encrypted JWE, decrypt using the code challenge and return a JWT string which will need to be verified */
427
- async function decryptAuthRequest({
428
- jwe,
429
- encryption_key,
430
- }: {
431
- jwe: string;
432
- encryption_key: string;
433
- }): Promise<string> {
434
- const [
435
- protectedHeaderB64U,
436
- ,
437
- nonceB64U,
438
- ciphertextB64U,
439
- authenticationTagB64U,
440
- ] = jwe.split('.');
441
-
442
- const encryptionKeyBytes = Convert.base64Url(encryption_key).toUint8Array();
443
- const protectedHeader = Convert.base64Url(protectedHeaderB64U).toUint8Array();
444
- const additionalData = protectedHeader;
445
- const nonce = Convert.base64Url(nonceB64U).toUint8Array();
446
- const ciphertext = Convert.base64Url(ciphertextB64U).toUint8Array();
447
- const authenticationTag = Convert.base64Url(
448
- authenticationTagB64U
449
- ).toUint8Array();
450
-
451
- // The cipher expects the encrypted data and tag to be concatenated.
452
- const ciphertextAndTag = new Uint8Array([
453
- ...ciphertext,
454
- ...authenticationTag,
455
- ]);
456
- const decryptedJwtBytes = await XChaCha20Poly1305.decryptRaw({ data: ciphertextAndTag, keyBytes: encryptionKeyBytes, nonce, additionalData });
457
- const jwt = Convert.uint8Array(decryptedJwtBytes).toString();
458
-
459
- return jwt;
460
- }
461
-
462
- /**
463
- * The client uses to decrypt the jwe obtained from the auth server which contains
464
- * the {@link EnboxConnectAuthResponse} that was sent by the provider to the auth server.
465
- *
466
- * @async
467
- * @param {BearerDid} clientDid - The did that was initially used by the client for ECDH at connect init.
468
- * @param {string} jwe - The encrypted data as a jwe.
469
- * @param {string} pin - The pin that was obtained from the user.
470
- */
471
- async function decryptAuthResponse(
472
- clientDid: BearerDid,
473
- jwe: string,
474
- pin: string
475
- ): Promise<string> {
476
- const [
477
- protectedHeaderB64U,
478
- ,
479
- nonceB64U,
480
- ciphertextB64U,
481
- authenticationTagB64U,
482
- ] = jwe.split('.');
483
-
484
- // get the delegatedid public key from the header
485
- const header = Convert.base64Url(protectedHeaderB64U).toObject() as Jwk;
486
- if (!header.kid) {
487
- throw new Error('JWE protected header is missing required "kid" property');
488
- }
489
- const delegateResolvedDid = await DidJwk.resolve(header.kid.split('#')[0]);
490
-
491
- // derive ECDH shared key using the provider's public key and our clientDid private key
492
- const sharedKey = await Oidc.deriveSharedKey(
493
- clientDid,
494
- delegateResolvedDid.didDocument!
495
- );
496
-
497
- // add the pin to the AAD
498
- const additionalData = { ...header, pin: pin };
499
- const AAD = Convert.object(additionalData).toUint8Array();
500
-
501
- const nonce = Convert.base64Url(nonceB64U).toUint8Array();
502
- const ciphertext = Convert.base64Url(ciphertextB64U).toUint8Array();
503
- const authenticationTag = Convert.base64Url(
504
- authenticationTagB64U
505
- ).toUint8Array();
506
-
507
- // The cipher expects the encrypted data and tag to be concatenated.
508
- const ciphertextAndTag = new Uint8Array([
509
- ...ciphertext,
510
- ...authenticationTag,
511
- ]);
512
-
513
- // decrypt using the sharedKey
514
- const decryptedJwtBytes = await XChaCha20Poly1305.decryptRaw({ data: ciphertextAndTag, keyBytes: sharedKey, nonce, additionalData: AAD });
515
- const jwt = Convert.uint8Array(decryptedJwtBytes).toString();
516
-
517
- return jwt;
518
- }
519
-
520
- /** Derives a shared ECDH private key in order to encrypt the {@link EnboxConnectAuthResponse} */
521
- async function deriveSharedKey(
522
- privateKeyDid: BearerDid,
523
- publicKeyDid: DidDocument
524
- ): Promise<Uint8Array> {
525
- const privatePortableDid = await privateKeyDid.export();
526
-
527
- const publicJwk = publicKeyDid.verificationMethod?.[0].publicKeyJwk!;
528
- const privateJwk = privatePortableDid.privateKeys?.[0]!;
529
- publicJwk.alg = 'EdDSA';
530
-
531
- const publicX25519 = await Ed25519.convertPublicKeyToX25519({
532
- publicKey: publicJwk,
533
- });
534
- const privateX25519 = await Ed25519.convertPrivateKeyToX25519({
535
- privateKey: privateJwk,
536
- });
537
-
538
- const sharedKey = await X25519.sharedSecret({
539
- privateKeyA : privateX25519,
540
- publicKeyB : publicX25519,
541
- });
542
-
543
- const sharedEncryptionKey = await Hkdf.deriveKeyBytes({
544
- baseKeyBytes : new Uint8Array(sharedKey),
545
- hash : 'SHA-256',
546
- salt : new Uint8Array(),
547
- info : new Uint8Array(),
548
- length : 256,
549
- });
550
-
551
- return sharedEncryptionKey;
552
- }
553
-
554
- /**
555
- * Encrypts the auth response jwt. Requires a randomPin is added to the AAD of the
556
- * encryption algorithm in order to prevent man in the middle and eavesdropping attacks.
557
- * The keyid of the delegate did is used to pass the public key to the client in order
558
- * for the client to derive the shared ECDH private key.
559
- */
560
- async function encryptAuthResponse({
561
- jwt,
562
- encryptionKey,
563
- delegateDidKeyId,
564
- randomPin,
565
- }: {
566
- jwt: string;
567
- encryptionKey: Uint8Array;
568
- delegateDidKeyId: string;
569
- randomPin: string;
570
- }): Promise<string> {
571
- const protectedHeader = {
572
- alg : 'dir',
573
- cty : 'JWT',
574
- enc : 'XC20P',
575
- typ : 'JWT',
576
- kid : delegateDidKeyId,
577
- };
578
- const nonce = CryptoUtils.randomBytes(24);
579
- const additionalData = Convert.object({
580
- ...protectedHeader,
581
- pin: randomPin,
582
- }).toUint8Array();
583
-
584
- const jwtBytes = Convert.string(jwt).toUint8Array();
585
- const ciphertextAndTag = await XChaCha20Poly1305.encryptRaw({ data: jwtBytes, keyBytes: encryptionKey, nonce, additionalData });
586
-
587
- /** The cipher output concatenates the encrypted data and tag
588
- * so we need to extract the values for use in the JWE. */
589
- const ciphertext = ciphertextAndTag.subarray(0, -16);
590
- const authenticationTag = ciphertextAndTag.subarray(-16);
591
-
592
- const compactJwe = [
593
- Convert.object(protectedHeader).toBase64Url(),
594
- '', // Empty string since there is no wrapped key.
595
- Convert.uint8Array(nonce).toBase64Url(),
596
- Convert.uint8Array(ciphertext).toBase64Url(),
597
- Convert.uint8Array(authenticationTag).toBase64Url(),
598
- ].join('.');
599
-
600
- return compactJwe;
601
- }
602
-
603
- function shouldUseDelegatePermission(scope: DwnPermissionScope): boolean {
604
- // Currently all record permissions are treated as delegated permissions
605
- // In the future only methods that modify state will be delegated and the rest will be normal permissions
606
- if (isRecordPermissionScope(scope)) {
607
- return true;
608
- } else if (scope.interface === DwnInterfaceName.Protocols && scope.method === DwnMethodName.Configure) {
609
- // ProtocolConfigure messages are also delegated, as they modify state
610
- return true;
611
- }
612
-
613
- // All other permissions are not treated as delegated
614
- return false;
615
- }
616
-
617
- /**
618
- * Creates the permission grants that assign to the selectedDid the level of
619
- * permissions that the web app requested in the {@link EnboxConnectAuthRequest}
620
- */
621
- async function createPermissionGrants(
622
- selectedDid: string,
623
- delegateBearerDid: BearerDid,
624
- agent: EnboxAgent,
625
- scopes: DwnPermissionScope[],
626
- ): Promise<DwnDataEncodedRecordsWriteMessage[]> {
627
- const permissionsApi = new AgentPermissionsApi({ agent });
628
-
629
- // TODO: cleanup all grants if one fails by deleting them from the DWN: https://github.com/enboxorg/enbox/issues/849
630
- logger.log(`Creating permission grants for ${scopes.length} scopes given...`);
631
- const permissionGrants = await Promise.all(
632
- scopes.map((scope) => {
633
- // check if the scope is a records permission scope, or a protocol configure scope, if so it should use a delegated permission.
634
- const delegated = shouldUseDelegatePermission(scope);
635
- return permissionsApi.createGrant({
636
- delegated,
637
- store : true,
638
- grantedTo : delegateBearerDid.uri,
639
- scope,
640
- dateExpires : '2040-06-25T16:09:16.693356Z', // TODO: make dateExpires optional
641
- author : selectedDid,
642
- });
643
- })
644
- );
645
-
646
- logger.log(`Sending ${permissionGrants.length} permission grants to remote DWN...`);
647
- const messagePromises = permissionGrants.map(async (grant) => {
648
- // Quirk: we have to pull out encodedData out of the message the schema validator doesn't want it there
649
- const { encodedData, ...rawMessage } = grant.message;
650
-
651
- const data = Convert.base64Url(encodedData).toUint8Array();
652
- const { reply } = await agent.sendDwnRequest({
653
- author : selectedDid,
654
- target : selectedDid,
655
- messageType : DwnInterface.RecordsWrite,
656
- dataStream : new Blob([data]),
657
- rawMessage,
658
- });
659
-
660
- // check if the message was sent successfully, if the remote returns 409 the message may have come through already via sync
661
- if (reply.status.code !== 202 && reply.status.code !== 409) {
662
- logger.error(`Error sending RecordsWrite: ${reply.status.detail}`);
663
- logger.error(`RecordsWrite message: ${rawMessage}`);
664
- throw new Error(
665
- `Could not send the message. Error details: ${reply.status.detail}`
666
- );
667
- }
668
-
669
- return grant.message;
670
- });
671
-
672
- try {
673
- const messages = await Promise.all(messagePromises);
674
- return messages;
675
- } catch (error) {
676
- logger.error(`Error during batch-send of permission grants: ${error}`);
677
- throw error;
678
- }
679
- }
680
-
681
- /**
682
- * Installs the protocol required by the Client on the Provider if it doesn't already exist.
683
- */
684
- async function prepareProtocol(
685
- selectedDid: string,
686
- agent: EnboxAgent,
687
- protocolDefinition: DwnProtocolDefinition
688
- ): Promise<void> {
689
-
690
- const queryMessage = await agent.processDwnRequest({
691
- author : selectedDid,
692
- messageType : DwnInterface.ProtocolsQuery,
693
- target : selectedDid,
694
- messageParams : { filter: { protocol: protocolDefinition.protocol } },
695
- });
696
-
697
- if ( queryMessage.reply.status.code !== 200) {
698
- // if the query failed, throw an error
699
- throw new Error(
700
- `Could not fetch protocol: ${queryMessage.reply.status.detail}`
701
- );
702
- } else if (queryMessage.reply.entries === undefined || queryMessage.reply.entries.length === 0) {
703
- logger.log(`Protocol does not exist, creating: ${protocolDefinition.protocol}`);
704
-
705
- // send the protocol definition to the remote DWN first, if it passes we can process it locally
706
- const { reply: sendReply, message: configureMessage } = await agent.sendDwnRequest({
707
- author : selectedDid,
708
- target : selectedDid,
709
- messageType : DwnInterface.ProtocolsConfigure,
710
- messageParams : { definition: protocolDefinition },
711
- });
712
-
713
- // check if the message was sent successfully, if the remote returns 409 the message may have come through already via sync
714
- if (sendReply.status.code !== 202 && sendReply.status.code !== 409) {
715
- throw new Error(`Could not send protocol: ${sendReply.status.detail}`);
716
- }
717
-
718
- // process the protocol locally, we don't have to check if it exists as this is just a convenience over waiting for sync.
719
- await agent.processDwnRequest({
720
- author : selectedDid,
721
- target : selectedDid,
722
- messageType : DwnInterface.ProtocolsConfigure,
723
- rawMessage : configureMessage
724
- });
725
-
726
- } else {
727
- logger.log(`Protocol already exists: ${protocolDefinition.protocol}`);
728
-
729
- // the protocol already exists, let's make sure it exists on the remote DWN as the requesting app will need it
730
- const configureMessage = queryMessage.reply.entries![0];
731
- const { reply: sendReply } = await agent.sendDwnRequest({
732
- author : selectedDid,
733
- target : selectedDid,
734
- messageType : DwnInterface.ProtocolsConfigure,
735
- rawMessage : configureMessage,
736
- });
737
-
738
- if (sendReply.status.code !== 202 && sendReply.status.code !== 409) {
739
- throw new Error(`Could not send protocol: ${sendReply.status.detail}`);
740
- }
741
- }
742
- }
743
-
744
- /**
745
- * Creates a delegate did which the web app will use as its future indentity.
746
- * Assigns to that DID the level of permissions that the web app requested in
747
- * the {@link EnboxConnectAuthRequest}. Encrypts via ECDH key that the web app
748
- * will have access to because the web app has the public key which it provided
749
- * in the {@link EnboxConnectAuthRequest}. Then sends the ciphertext of this
750
- * {@link EnboxConnectAuthResponse} to the callback endpoint. Which the
751
- * web app will need to retrieve from the token endpoint and decrypt with the pin to access.
752
- */
753
- async function submitAuthResponse(
754
- selectedDid: string,
755
- authRequest: EnboxConnectAuthRequest,
756
- randomPin: string,
757
- agent: EnboxAgent
758
- ): Promise<void> {
759
- const delegateBearerDid = await DidJwk.create();
760
- const delegatePortableDid = await delegateBearerDid.export();
761
-
762
- // TODO: roll back permissions and protocol configurations if an error occurs. Need a way to delete protocols to achieve this.
763
- const delegateGrantPromises = authRequest.permissionRequests.map(
764
- async (permissionRequest) => {
765
- const { protocolDefinition, permissionScopes } = permissionRequest;
766
-
767
- // We validate that all permission scopes match the protocol uri of the protocol definition they are provided with.
768
- const grantsMatchProtocolUri = permissionScopes.every(scope => 'protocol' in scope && scope.protocol === protocolDefinition.protocol);
769
- if (!grantsMatchProtocolUri) {
770
- throw new Error('All permission scopes must match the protocol uri they are provided with.');
771
- }
772
-
773
- await prepareProtocol(selectedDid, agent, protocolDefinition);
774
-
775
- const permissionGrants = await Oidc.createPermissionGrants(
776
- selectedDid,
777
- delegateBearerDid,
778
- agent,
779
- permissionScopes
780
- );
781
-
782
- return permissionGrants;
783
- }
784
- );
785
-
786
- const delegateGrants = (await Promise.all(delegateGrantPromises)).flat();
787
-
788
- logger.log('Generating auth response object...');
789
- const responseObject = await Oidc.createResponseObject({
790
- //* the IDP's did that was selected to be connected
791
- iss : selectedDid,
792
- //* the client's new identity
793
- sub : delegateBearerDid.uri,
794
- //* the client's temporary ephemeral did used for connect
795
- aud : authRequest.client_id,
796
- //* the nonce of the original auth request
797
- nonce : authRequest.nonce,
798
- delegateGrants,
799
- delegatePortableDid,
800
- });
801
-
802
- // Sign the Response Object using the ephemeral DID's signing key.
803
- logger.log('Signing auth response object...');
804
- const responseObjectJwt = await Oidc.signJwt({
805
- did : delegateBearerDid,
806
- data : responseObject,
807
- });
808
- const clientDid = await DidJwk.resolve(authRequest.client_id);
809
-
810
- const sharedKey = await Oidc.deriveSharedKey(
811
- delegateBearerDid,
812
- clientDid?.didDocument!
813
- );
814
-
815
- logger.log('Encrypting auth response object...');
816
- const encryptedResponse = await Oidc.encryptAuthResponse({
817
- jwt : responseObjectJwt!,
818
- encryptionKey : sharedKey,
819
- delegateDidKeyId : delegateBearerDid.document.verificationMethod![0].id,
820
- randomPin,
821
- });
822
-
823
- const formEncodedRequest = new URLSearchParams({
824
- id_token : encryptedResponse,
825
- state : authRequest.state,
826
- }).toString();
827
-
828
- logger.log(`Sending auth response object to Web5 Connect server: ${authRequest.redirect_uri}`);
829
- await fetch(authRequest.redirect_uri, {
830
- body : formEncodedRequest,
831
- method : 'POST',
832
- headers : {
833
- 'Content-Type': 'application/x-www-form-urlencoded',
834
- },
835
- signal: AbortSignal.timeout(30_000),
836
- });
837
- }
838
-
839
- export const Oidc = {
840
- createAuthRequest,
841
- encryptAuthRequest,
842
- getAuthRequest,
843
- decryptAuthRequest,
844
- createPermissionGrants,
845
- createResponseObject,
846
- encryptAuthResponse,
847
- decryptAuthResponse,
848
- deriveSharedKey,
849
- signJwt,
850
- verifyJwt,
851
- buildOidcUrl,
852
- generateCodeChallenge,
853
- submitAuthResponse,
854
- };
855
-
856
- // ---------------------------------------------------------------------------
857
- // Deprecated aliases — migration aid
858
- // ---------------------------------------------------------------------------
859
-
860
- /** @deprecated Use {@link EnboxConnectAuthRequest} instead. */
861
- export type Web5ConnectAuthRequest = EnboxConnectAuthRequest;
862
-
863
- /** @deprecated Use {@link EnboxConnectAuthResponse} instead. */
864
- export type Web5ConnectAuthResponse = EnboxConnectAuthResponse;