UNPKG

11.8 kBPlain TextView Raw
1import type { Resolvable, VerificationMethod } from 'did-resolver'
2import type {
3 AnonEncryptParams,
4 AuthEncryptParams,
5 Decrypter,
6 ECDH,
7 Encrypter,
8 KeyWrapper,
9 ProtectedHeader,
10 Recipient,
11 WrappingResult,
12} from './types.js'
13import { base64ToBytes, extractPublicKeyBytes, isDefined, toSealed } from '../util.js'
14import { xc20pDirDecrypter, xc20pDirEncrypter, xc20pEncrypter } from './xc20pDir.js'
15import { computeX25519Ecdh1PUv3Kek, createX25519Ecdh1PUv3Kek } from './X25519-ECDH-1PU.js'
16import { computeX25519EcdhEsKek, createX25519EcdhEsKek } from './X25519-ECDH-ES.js'
17import { createFullEncrypter } from './createEncrypter.js'
18
19/**
20 * @deprecated Use
21 * {@link xc20pAuthEncrypterEcdh1PuV3x25519WithXc20PkwV2 | xc20pAuthEncrypterEcdh1PuV3x25519WithXc20PkwV2() } instead
22 */
23export 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 * @deprecated Use {@link xc20pAnonEncrypterEcdhESx25519WithXc20PkwV2 | xc20pAnonEncrypterEcdhESx25519WithXc20PkwV2() }
33 * instead
34 */
35export function createAnonEncrypter(publicKey: Uint8Array, options: Partial<AnonEncryptParams> = {}): Encrypter {
36 return xc20pAnonEncrypterEcdhESx25519WithXc20PkwV2(publicKey, options)
37}
38
39/**
40 * @deprecated Use
41 * {@link xc20pAuthDecrypterEcdh1PuV3x25519WithXc20PkwV2 | xc20pAuthDecrypterEcdh1PuV3x25519WithXc20PkwV2() } instead
42 */
43export function createAuthDecrypter(recipientSecret: Uint8Array | ECDH, senderPublicKey: Uint8Array): Decrypter {
44 return xc20pAuthDecrypterEcdh1PuV3x25519WithXc20PkwV2(recipientSecret, senderPublicKey)
45}
46
47/**
48 * @deprecated Use {@link xc20pAnonDecrypterEcdhESx25519WithXc20PkwV2 | xc20pAnonDecrypterEcdhESx25519WithXc20PkwV2() }
49 * instead
50 */
51export function createAnonDecrypter(recipientSecret: Uint8Array | ECDH): Decrypter {
52 return xc20pAnonDecrypterEcdhESx25519WithXc20PkwV2(recipientSecret)
53}
54
55export 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
62export 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 * @deprecated Use {@link xc20pAnonEncrypterEcdhESx25519WithXc20PkwV2 | xc20pAnonEncrypterEcdhESx25519WithXc20PkwV2() }
75 * instead
76 */
77export function x25519Encrypter(publicKey: Uint8Array, kid?: string, apv?: string): Encrypter {
78 return xc20pAnonEncrypterEcdhESx25519WithXc20PkwV2(publicKey, { kid, apv })
79}
80
81/**
82 * Recommended encrypter for anonymous encryption (i.e. no sender authentication).
83 * Uses {@link https://tools.ietf.org/html/draft-amringer-jose-chacha-02 | ECDH-ES+XC20PKW v2}.
84 *
85 * @param recipientPublicKey - the byte array representing the recipient public key
86 * @param options - {@link AnonEncryptParams} used to specify the recipient key ID (`kid`)
87 *
88 * @returns an {@link Encrypter} instance usable with {@link createJWE}
89 *
90 * NOTE: ECDH-ES+XC20PKW is a proposed draft in IETF and not a standard yet and
91 * is subject to change as new revisions or until the official CFRG specification is released.
92 */
93export 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 * Recommended encrypter for authenticated encryption (i.e. sender authentication and requires
109 * sender private key to encrypt the data).
110 * Uses {@link https://tools.ietf.org/html/draft-madden-jose-ecdh-1pu-03 | ECDH-1PU v3 } and
111 * {@link https://tools.ietf.org/html/draft-amringer-jose-chacha-02 | XC20PKW v2 }.
112 *
113 * @param recipientPublicKey - the byte array representing the recipient public key
114 * @param senderSecret - either a Uint8Array representing the sender secret key or
115 * an ECDH function that wraps the key and can promise a shared secret given a public key
116 * @param options - {@link AuthEncryptParams} used to specify extra header parameters
117 *
118 * @returns an {@link Encrypter} instance usable with {@link createJWE}
119 *
120 * NOTE: ECDH-1PU and XC20PKW are proposed drafts in IETF and not a standard yet and
121 * are subject to change as new revisions or until the official CFRG specification are released.
122 *
123 * Implements ECDH-1PU+XC20PKW with XChaCha20Poly1305 based on the following specs:
124 * - {@link https://tools.ietf.org/html/draft-amringer-jose-chacha-02 | XC20PKW}
125 * - {@link https://tools.ietf.org/html/draft-madden-jose-ecdh-1pu-03 | ECDH-1PU}
126 */
127export 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
142export 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 * @deprecated Use {@link xc20pAnonDecrypterEcdhESx25519WithXc20PkwV2 | xc20pAnonDecrypterEcdhESx25519WithXc20PkwV2() }
202 * instead
203 */
204export function x25519Decrypter(receiverSecret: Uint8Array | ECDH): Decrypter {
205 return xc20pAnonDecrypterEcdhESx25519WithXc20PkwV2(receiverSecret)
206}
207
208/**
209 * Recommended decrypter for anonymous encryption (i.e. no sender authentication).
210 * Uses {@link https://tools.ietf.org/html/draft-amringer-jose-chacha-02 | ECDH-ES+XC20PKW v2 }.
211 *
212 * @param recipientSecret - either a Uint8Array representing the recipient secret key or
213 * an ECDH function that wraps the key and can promise a shared secret given a public key
214 *
215 * @returns a {@link Decrypter} instance usable with {@link decryptJWE}
216 *
217 * NOTE: ECDH-ES+XC20PKW is a proposed draft in IETF and not a standard yet and
218 * is subject to change as new revisions or until the official CFRG specification is released.
219 *
220 * @beta
221 */
222export 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 // Content Encryption Key
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 * Recommended decrypter for authenticated encryption (i.e. sender authentication and requires
250 * sender public key to decrypt the data).
251 * Uses {@link https://tools.ietf.org/html/draft-madden-jose-ecdh-1pu-03 | ECDH-1PU v3 } and
252 * {@link https://tools.ietf.org/html/draft-amringer-jose-chacha-02 | XC20PKW v2 }.
253 *
254 * @param recipientSecret - either a Uint8Array representing the recipient secret key or
255 * an ECDH function that wraps the key and can promise a shared secret given a public key
256 * @param senderPublicKey - the byte array representing the sender public key
257 *
258 * @returns a {@link Decrypter} instance usable with {@link decryptJWE}
259 *
260 * NOTE: ECDH-1PU and XC20PKW are proposed drafts in IETF and not a standard yet and
261 * are subject to change as new revisions or until the official CFRG specification are released.
262 *
263 * @beta
264 *
265 * Implements ECDH-1PU+XC20PKW with XChaCha20Poly1305 based on the following specs:
266 * - {@link https://tools.ietf.org/html/draft-amringer-jose-chacha-02 | XC20PKW}
267 * - {@link https://tools.ietf.org/html/draft-madden-jose-ecdh-1pu-03 | ECDH-1PU}
268 */
269export 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 // Content Encryption Key
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}