1 | import type { Resolvable, VerificationMethod } from 'did-resolver'
|
2 | import type {
|
3 | AnonEncryptParams,
|
4 | AuthEncryptParams,
|
5 | Decrypter,
|
6 | ECDH,
|
7 | Encrypter,
|
8 | KeyWrapper,
|
9 | ProtectedHeader,
|
10 | Recipient,
|
11 | WrappingResult,
|
12 | } from './types.js'
|
13 | import { base64ToBytes, extractPublicKeyBytes, isDefined, toSealed } from '../util.js'
|
14 | import { xc20pDirDecrypter, xc20pDirEncrypter, xc20pEncrypter } from './xc20pDir.js'
|
15 | import { computeX25519Ecdh1PUv3Kek, createX25519Ecdh1PUv3Kek } from './X25519-ECDH-1PU.js'
|
16 | import { computeX25519EcdhEsKek, createX25519EcdhEsKek } from './X25519-ECDH-ES.js'
|
17 | import { createFullEncrypter } from './createEncrypter.js'
|
18 |
|
19 |
|
20 |
|
21 |
|
22 |
|
23 | export function createAuthEncrypter(
|
24 | recipientPublicKey: Uint8Array,
|
25 | senderSecret: Uint8Array | ECDH,
|
26 | options: Partial<AuthEncryptParams> = {}
|
27 | ): Encrypter {
|
28 | return xc20pAuthEncrypterEcdh1PuV3x25519WithXc20PkwV2(recipientPublicKey, senderSecret, options)
|
29 | }
|
30 |
|
31 |
|
32 |
|
33 |
|
34 |
|
35 | export function createAnonEncrypter(publicKey: Uint8Array, options: Partial<AnonEncryptParams> = {}): Encrypter {
|
36 | return xc20pAnonEncrypterEcdhESx25519WithXc20PkwV2(publicKey, options)
|
37 | }
|
38 |
|
39 |
|
40 |
|
41 |
|
42 |
|
43 | export function createAuthDecrypter(recipientSecret: Uint8Array | ECDH, senderPublicKey: Uint8Array): Decrypter {
|
44 | return xc20pAuthDecrypterEcdh1PuV3x25519WithXc20PkwV2(recipientSecret, senderPublicKey)
|
45 | }
|
46 |
|
47 |
|
48 |
|
49 |
|
50 |
|
51 | export function createAnonDecrypter(recipientSecret: Uint8Array | ECDH): Decrypter {
|
52 | return xc20pAnonDecrypterEcdhESx25519WithXc20PkwV2(recipientSecret)
|
53 | }
|
54 |
|
55 | export function validateHeader(header?: ProtectedHeader): Required<Pick<ProtectedHeader, 'epk' | 'iv' | 'tag'>> {
|
56 | if (!(header && header.epk && header.iv && header.tag)) {
|
57 | throw new Error('bad_jwe: malformed header')
|
58 | }
|
59 | return header as Required<Pick<ProtectedHeader, 'epk' | 'iv' | 'tag'>>
|
60 | }
|
61 |
|
62 | export const xc20pKeyWrapper: KeyWrapper = {
|
63 | from: (wrappingKey: Uint8Array) => {
|
64 | const wrap = async (cek: Uint8Array): Promise<WrappingResult> => {
|
65 | return xc20pEncrypter(wrappingKey)(cek)
|
66 | }
|
67 | return { wrap }
|
68 | },
|
69 |
|
70 | alg: 'XC20PKW',
|
71 | }
|
72 |
|
73 |
|
74 |
|
75 |
|
76 |
|
77 | export function x25519Encrypter(publicKey: Uint8Array, kid?: string, apv?: string): Encrypter {
|
78 | return xc20pAnonEncrypterEcdhESx25519WithXc20PkwV2(publicKey, { kid, apv })
|
79 | }
|
80 |
|
81 |
|
82 |
|
83 |
|
84 |
|
85 |
|
86 |
|
87 |
|
88 |
|
89 |
|
90 |
|
91 |
|
92 |
|
93 | export function xc20pAnonEncrypterEcdhESx25519WithXc20PkwV2(
|
94 | recipientPublicKey: Uint8Array,
|
95 | options: Partial<AnonEncryptParams> = {}
|
96 | ): Encrypter {
|
97 | return createFullEncrypter(
|
98 | recipientPublicKey,
|
99 | undefined,
|
100 | options,
|
101 | { createKek: createX25519EcdhEsKek, alg: 'ECDH-ES' },
|
102 | xc20pKeyWrapper,
|
103 | { from: (cek: Uint8Array) => xc20pDirEncrypter(cek), enc: 'XC20P' }
|
104 | )
|
105 | }
|
106 |
|
107 |
|
108 |
|
109 |
|
110 |
|
111 |
|
112 |
|
113 |
|
114 |
|
115 |
|
116 |
|
117 |
|
118 |
|
119 |
|
120 |
|
121 |
|
122 |
|
123 |
|
124 |
|
125 |
|
126 |
|
127 | export function xc20pAuthEncrypterEcdh1PuV3x25519WithXc20PkwV2(
|
128 | recipientPublicKey: Uint8Array,
|
129 | senderSecret: Uint8Array | ECDH,
|
130 | options: Partial<AuthEncryptParams> = {}
|
131 | ): Encrypter {
|
132 | return createFullEncrypter(
|
133 | recipientPublicKey,
|
134 | senderSecret,
|
135 | options,
|
136 | { createKek: createX25519Ecdh1PUv3Kek, alg: 'ECDH-1PU' },
|
137 | xc20pKeyWrapper,
|
138 | { from: (cek: Uint8Array) => xc20pDirEncrypter(cek), enc: 'XC20P' }
|
139 | )
|
140 | }
|
141 |
|
142 | export async function resolveX25519Encrypters(dids: string[], resolver: Resolvable): Promise<Encrypter[]> {
|
143 | const encryptersForDID = async (did: string, resolved: string[] = []): Promise<Encrypter[]> => {
|
144 | const { didResolutionMetadata, didDocument } = await resolver.resolve(did)
|
145 | resolved.push(did)
|
146 | if (didResolutionMetadata?.error || didDocument == null) {
|
147 | throw new Error(
|
148 | `resolver_error: Could not resolve ${did}: ${didResolutionMetadata.error}, ${didResolutionMetadata.message}`
|
149 | )
|
150 | }
|
151 | let controllerEncrypters: Encrypter[] = []
|
152 | if (!didDocument.controller && !didDocument.keyAgreement) {
|
153 | throw new Error(`no_suitable_keys: Could not find x25519 key for ${did}`)
|
154 | }
|
155 | if (didDocument.controller) {
|
156 | let controllers = Array.isArray(didDocument.controller) ? didDocument.controller : [didDocument.controller]
|
157 | controllers = controllers.filter((c) => !resolved.includes(c))
|
158 | const encrypterPromises = controllers.map((did) =>
|
159 | encryptersForDID(did, resolved).catch(() => {
|
160 | return []
|
161 | })
|
162 | )
|
163 | const encrypterArrays = await Promise.all(encrypterPromises)
|
164 | controllerEncrypters = ([] as Encrypter[]).concat(...encrypterArrays)
|
165 | }
|
166 | const agreementKeys: VerificationMethod[] = didDocument.keyAgreement
|
167 | ?.map((key) => {
|
168 | if (typeof key === 'string') {
|
169 | return [...(didDocument.publicKey || []), ...(didDocument.verificationMethod || [])].find(
|
170 | (pk) => pk.id === key
|
171 | )
|
172 | }
|
173 | return key
|
174 | })
|
175 | ?.filter((key) => typeof key !== 'undefined') as VerificationMethod[]
|
176 | const pks =
|
177 | agreementKeys?.filter((key) =>
|
178 | ['X25519KeyAgreementKey2019', 'X25519KeyAgreementKey2020', 'JsonWebKey2020', 'Multikey'].includes(key.type)
|
179 | ) ?? []
|
180 | if (!pks.length && !controllerEncrypters.length)
|
181 | throw new Error(`no_suitable_keys: Could not find X25519 key for ${did}`)
|
182 | return pks
|
183 | .map((pk) => {
|
184 | const { keyBytes, keyType } = extractPublicKeyBytes(pk)
|
185 | if (keyType === 'X25519') {
|
186 | return x25519Encrypter(keyBytes, pk.id)
|
187 | } else {
|
188 | return null
|
189 | }
|
190 | })
|
191 | .filter(isDefined)
|
192 | .concat(...controllerEncrypters)
|
193 | }
|
194 |
|
195 | const encrypterPromises = dids.map((did) => encryptersForDID(did))
|
196 | const encrypterArrays = await Promise.all(encrypterPromises)
|
197 | return ([] as Encrypter[]).concat(...encrypterArrays)
|
198 | }
|
199 |
|
200 |
|
201 |
|
202 |
|
203 |
|
204 | export function x25519Decrypter(receiverSecret: Uint8Array | ECDH): Decrypter {
|
205 | return xc20pAnonDecrypterEcdhESx25519WithXc20PkwV2(receiverSecret)
|
206 | }
|
207 |
|
208 |
|
209 |
|
210 |
|
211 |
|
212 |
|
213 |
|
214 |
|
215 |
|
216 |
|
217 |
|
218 |
|
219 |
|
220 |
|
221 |
|
222 | export function xc20pAnonDecrypterEcdhESx25519WithXc20PkwV2(recipientSecret: Uint8Array | ECDH): Decrypter {
|
223 | const alg = 'ECDH-ES+XC20PKW'
|
224 | const enc = 'XC20P'
|
225 |
|
226 | async function decrypt(
|
227 | sealed: Uint8Array,
|
228 | iv: Uint8Array,
|
229 | aad?: Uint8Array,
|
230 | recipient?: Recipient
|
231 | ): Promise<Uint8Array | null> {
|
232 | recipient = <Recipient>recipient
|
233 | const header = validateHeader(recipient.header)
|
234 |
|
235 | const kek = await computeX25519EcdhEsKek(recipient, recipientSecret, alg)
|
236 | if (!kek) return null
|
237 |
|
238 | const sealedCek = toSealed(recipient.encrypted_key, header.tag)
|
239 | const cek = await xc20pDirDecrypter(kek).decrypt(sealedCek, base64ToBytes(header.iv))
|
240 | if (cek === null) return null
|
241 |
|
242 | return xc20pDirDecrypter(cek).decrypt(sealed, iv, aad)
|
243 | }
|
244 |
|
245 | return { alg, enc, decrypt }
|
246 | }
|
247 |
|
248 |
|
249 |
|
250 |
|
251 |
|
252 |
|
253 |
|
254 |
|
255 |
|
256 |
|
257 |
|
258 |
|
259 |
|
260 |
|
261 |
|
262 |
|
263 |
|
264 |
|
265 |
|
266 |
|
267 |
|
268 |
|
269 | export function xc20pAuthDecrypterEcdh1PuV3x25519WithXc20PkwV2(
|
270 | recipientSecret: Uint8Array | ECDH,
|
271 | senderPublicKey: Uint8Array
|
272 | ): Decrypter {
|
273 | const alg = 'ECDH-1PU+XC20PKW'
|
274 | const enc = 'XC20P'
|
275 |
|
276 | async function decrypt(
|
277 | sealed: Uint8Array,
|
278 | iv: Uint8Array,
|
279 | aad?: Uint8Array,
|
280 | recipient?: Recipient
|
281 | ): Promise<Uint8Array | null> {
|
282 | recipient = <Recipient>recipient
|
283 | const header = validateHeader(recipient.header)
|
284 | const kek = await computeX25519Ecdh1PUv3Kek(recipient, recipientSecret, senderPublicKey, alg)
|
285 | if (!kek) return null
|
286 |
|
287 | const sealedCek = toSealed(recipient.encrypted_key, header.tag)
|
288 | const cek = await xc20pDirDecrypter(kek).decrypt(sealedCek, base64ToBytes(header.iv))
|
289 | if (cek === null) return null
|
290 |
|
291 | return xc20pDirDecrypter(cek).decrypt(sealed, iv, aad)
|
292 | }
|
293 |
|
294 | return { alg, enc, decrypt }
|
295 | }
|