1 | import { concat, fromString, toString } from 'uint8arrays'
|
2 | import { x25519 } from '@noble/curves/ed25519'
|
3 | import type { EphemeralKeyPair } from './encryption/types.js'
|
4 | import { varint } from 'multiformats'
|
5 | import { BaseName, decode, encode } from 'multibase'
|
6 | import type { VerificationMethod } from 'did-resolver'
|
7 | import { secp256k1 } from '@noble/curves/secp256k1'
|
8 | import { p256 } from '@noble/curves/p256'
|
9 |
|
10 | const u8a = { toString, fromString, concat }
|
11 |
|
12 |
|
13 |
|
14 |
|
15 | export interface EcdsaSignature {
|
16 | r: string
|
17 | s: string
|
18 | recoveryParam?: number
|
19 | }
|
20 |
|
21 |
|
22 |
|
23 |
|
24 | export type ECDSASignature = {
|
25 | compact: Uint8Array
|
26 | recovery?: number
|
27 | }
|
28 |
|
29 | export type JsonWebKey = {
|
30 | crv: string
|
31 | kty: string
|
32 | x?: string
|
33 | y?: string
|
34 |
|
35 | [key: string]: any
|
36 | }
|
37 |
|
38 | export function bytesToBase64url(b: Uint8Array): string {
|
39 | return u8a.toString(b, 'base64url')
|
40 | }
|
41 |
|
42 | export function base64ToBytes(s: string): Uint8Array {
|
43 | const inputBase64Url = s.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '')
|
44 | return u8a.fromString(inputBase64Url, 'base64url')
|
45 | }
|
46 |
|
47 | export function bytesToBase64(b: Uint8Array): string {
|
48 | return u8a.toString(b, 'base64pad')
|
49 | }
|
50 |
|
51 | export function base58ToBytes(s: string): Uint8Array {
|
52 | return u8a.fromString(s, 'base58btc')
|
53 | }
|
54 |
|
55 | export function bytesToBase58(b: Uint8Array): string {
|
56 | return u8a.toString(b, 'base58btc')
|
57 | }
|
58 |
|
59 | export type KNOWN_JWA = 'ES256' | 'ES256K' | 'ES256K-R' | 'Ed25519' | 'EdDSA'
|
60 |
|
61 | export type KNOWN_VERIFICATION_METHOD =
|
62 | | 'JsonWebKey2020'
|
63 | | 'Multikey'
|
64 | | 'Secp256k1SignatureVerificationKey2018'
|
65 | | 'Secp256k1VerificationKey2018'
|
66 | | 'EcdsaSecp256k1VerificationKey2019'
|
67 | | 'EcdsaPublicKeySecp256k1'
|
68 | | 'EcdsaSecp256k1RecoveryMethod2020'
|
69 | | 'EcdsaSecp256r1VerificationKey2019'
|
70 | | 'Ed25519VerificationKey2018'
|
71 | | 'Ed25519VerificationKey2020'
|
72 | | 'ED25519SignatureVerification'
|
73 | | 'ConditionalProof2022'
|
74 | | 'X25519KeyAgreementKey2019'
|
75 | | 'X25519KeyAgreementKey2020'
|
76 |
|
77 | export type KNOWN_KEY_TYPE = 'Secp256k1' | 'Ed25519' | 'X25519' | 'Bls12381G1' | 'Bls12381G2' | 'P-256'
|
78 |
|
79 | export type PublicKeyTypes = Record<KNOWN_JWA, KNOWN_VERIFICATION_METHOD[]>
|
80 |
|
81 | export const SUPPORTED_PUBLIC_KEY_TYPES: PublicKeyTypes = {
|
82 | ES256: ['JsonWebKey2020', 'Multikey', 'EcdsaSecp256r1VerificationKey2019'],
|
83 | ES256K: [
|
84 | 'EcdsaSecp256k1VerificationKey2019',
|
85 | |
86 |
|
87 |
|
88 | 'EcdsaSecp256k1RecoveryMethod2020',
|
89 | |
90 |
|
91 |
|
92 |
|
93 | 'Secp256k1VerificationKey2018',
|
94 | |
95 |
|
96 |
|
97 |
|
98 | 'Secp256k1SignatureVerificationKey2018',
|
99 | |
100 |
|
101 |
|
102 |
|
103 | 'EcdsaPublicKeySecp256k1',
|
104 | |
105 |
|
106 |
|
107 |
|
108 | 'JsonWebKey2020',
|
109 | 'Multikey',
|
110 | ],
|
111 | 'ES256K-R': [
|
112 | 'EcdsaSecp256k1VerificationKey2019',
|
113 | |
114 |
|
115 |
|
116 | 'EcdsaSecp256k1RecoveryMethod2020',
|
117 | |
118 |
|
119 |
|
120 |
|
121 | 'Secp256k1VerificationKey2018',
|
122 | |
123 |
|
124 |
|
125 |
|
126 | 'Secp256k1SignatureVerificationKey2018',
|
127 | |
128 |
|
129 |
|
130 |
|
131 | 'EcdsaPublicKeySecp256k1',
|
132 | 'ConditionalProof2022',
|
133 | 'JsonWebKey2020',
|
134 | 'Multikey',
|
135 | ],
|
136 | Ed25519: [
|
137 | 'ED25519SignatureVerification',
|
138 | 'Ed25519VerificationKey2018',
|
139 | 'Ed25519VerificationKey2020',
|
140 | 'JsonWebKey2020',
|
141 | 'Multikey',
|
142 | ],
|
143 | EdDSA: [
|
144 | 'ED25519SignatureVerification',
|
145 | 'Ed25519VerificationKey2018',
|
146 | 'Ed25519VerificationKey2020',
|
147 | 'JsonWebKey2020',
|
148 | 'Multikey',
|
149 | ],
|
150 | }
|
151 |
|
152 | export const VM_TO_KEY_TYPE: Record<KNOWN_VERIFICATION_METHOD, KNOWN_KEY_TYPE | undefined> = {
|
153 | Secp256k1SignatureVerificationKey2018: 'Secp256k1',
|
154 | Secp256k1VerificationKey2018: 'Secp256k1',
|
155 | EcdsaSecp256k1VerificationKey2019: 'Secp256k1',
|
156 | EcdsaPublicKeySecp256k1: 'Secp256k1',
|
157 | EcdsaSecp256k1RecoveryMethod2020: 'Secp256k1',
|
158 | EcdsaSecp256r1VerificationKey2019: 'P-256',
|
159 | Ed25519VerificationKey2018: 'Ed25519',
|
160 | Ed25519VerificationKey2020: 'Ed25519',
|
161 | ED25519SignatureVerification: 'Ed25519',
|
162 | X25519KeyAgreementKey2019: 'X25519',
|
163 | X25519KeyAgreementKey2020: 'X25519',
|
164 | ConditionalProof2022: undefined,
|
165 | JsonWebKey2020: undefined,
|
166 | Multikey: undefined,
|
167 | }
|
168 |
|
169 | export type KNOWN_CODECS =
|
170 | | 'ed25519-pub'
|
171 | | 'x25519-pub'
|
172 | | 'secp256k1-pub'
|
173 | | 'bls12_381-g1-pub'
|
174 | | 'bls12_381-g2-pub'
|
175 | | 'p256-pub'
|
176 |
|
177 |
|
178 | export const supportedCodecs: Record<KNOWN_CODECS, number> = {
|
179 | 'ed25519-pub': 0xed,
|
180 | 'x25519-pub': 0xec,
|
181 | 'secp256k1-pub': 0xe7,
|
182 | 'bls12_381-g1-pub': 0xea,
|
183 | 'bls12_381-g2-pub': 0xeb,
|
184 | 'p256-pub': 0x1200,
|
185 | }
|
186 |
|
187 | export const CODEC_TO_KEY_TYPE: Record<KNOWN_CODECS, KNOWN_KEY_TYPE> = {
|
188 | 'bls12_381-g1-pub': 'Bls12381G1',
|
189 | 'bls12_381-g2-pub': 'Bls12381G2',
|
190 | 'ed25519-pub': 'Ed25519',
|
191 | 'p256-pub': 'P-256',
|
192 | 'secp256k1-pub': 'Secp256k1',
|
193 | 'x25519-pub': 'X25519',
|
194 | }
|
195 |
|
196 |
|
197 |
|
198 |
|
199 |
|
200 |
|
201 | export function extractPublicKeyBytes(pk: VerificationMethod): { keyBytes: Uint8Array; keyType?: KNOWN_KEY_TYPE } {
|
202 | if (pk.publicKeyBase58) {
|
203 | return {
|
204 | keyBytes: base58ToBytes(pk.publicKeyBase58),
|
205 | keyType: VM_TO_KEY_TYPE[pk.type as KNOWN_VERIFICATION_METHOD],
|
206 | }
|
207 | } else if (pk.publicKeyBase64) {
|
208 | return {
|
209 | keyBytes: base64ToBytes(pk.publicKeyBase64),
|
210 | keyType: VM_TO_KEY_TYPE[pk.type as KNOWN_VERIFICATION_METHOD],
|
211 | }
|
212 | } else if (pk.publicKeyHex) {
|
213 | return { keyBytes: hexToBytes(pk.publicKeyHex), keyType: VM_TO_KEY_TYPE[pk.type as KNOWN_VERIFICATION_METHOD] }
|
214 | } else if (pk.publicKeyJwk && pk.publicKeyJwk.crv === 'secp256k1' && pk.publicKeyJwk.x && pk.publicKeyJwk.y) {
|
215 | return {
|
216 | keyBytes: secp256k1.ProjectivePoint.fromAffine({
|
217 | x: bytesToBigInt(base64ToBytes(pk.publicKeyJwk.x)),
|
218 | y: bytesToBigInt(base64ToBytes(pk.publicKeyJwk.y)),
|
219 | }).toRawBytes(false),
|
220 | keyType: 'Secp256k1',
|
221 | }
|
222 | } else if (pk.publicKeyJwk && pk.publicKeyJwk.crv === 'P-256' && pk.publicKeyJwk.x && pk.publicKeyJwk.y) {
|
223 | return {
|
224 | keyBytes: p256.ProjectivePoint.fromAffine({
|
225 | x: bytesToBigInt(base64ToBytes(pk.publicKeyJwk.x)),
|
226 | y: bytesToBigInt(base64ToBytes(pk.publicKeyJwk.y)),
|
227 | }).toRawBytes(false),
|
228 | keyType: 'P-256',
|
229 | }
|
230 | } else if (
|
231 | pk.publicKeyJwk &&
|
232 | pk.publicKeyJwk.kty === 'OKP' &&
|
233 | ['Ed25519', 'X25519'].includes(pk.publicKeyJwk.crv ?? '') &&
|
234 | pk.publicKeyJwk.x
|
235 | ) {
|
236 | return { keyBytes: base64ToBytes(pk.publicKeyJwk.x), keyType: pk.publicKeyJwk.crv as KNOWN_KEY_TYPE }
|
237 | } else if (pk.publicKeyMultibase) {
|
238 | const { keyBytes, keyType } = multibaseToBytes(pk.publicKeyMultibase)
|
239 | return { keyBytes, keyType: keyType ?? VM_TO_KEY_TYPE[pk.type as KNOWN_VERIFICATION_METHOD] }
|
240 | }
|
241 | return { keyBytes: new Uint8Array() }
|
242 | }
|
243 |
|
244 |
|
245 |
|
246 |
|
247 |
|
248 |
|
249 |
|
250 |
|
251 |
|
252 |
|
253 |
|
254 |
|
255 |
|
256 | export function bytesToMultibase(
|
257 | b: Uint8Array,
|
258 | base: BaseName = 'base58btc',
|
259 | codec?: keyof typeof supportedCodecs | number
|
260 | ): string {
|
261 | if (!codec) {
|
262 | return u8a.toString(encode(base, b), 'utf-8')
|
263 | } else {
|
264 | const codecCode = typeof codec === 'string' ? supportedCodecs[codec] : codec
|
265 | const prefixLength = varint.encodingLength(codecCode)
|
266 | const multicodecEncoding = new Uint8Array(prefixLength + b.length)
|
267 | varint.encodeTo(codecCode, multicodecEncoding)
|
268 | multicodecEncoding.set(b, prefixLength)
|
269 | return u8a.toString(encode(base, multicodecEncoding), 'utf-8')
|
270 | }
|
271 | }
|
272 |
|
273 |
|
274 |
|
275 |
|
276 |
|
277 |
|
278 |
|
279 |
|
280 |
|
281 |
|
282 |
|
283 | export function multibaseToBytes(s: string): { keyBytes: Uint8Array; keyType?: KNOWN_KEY_TYPE } {
|
284 | const bytes = decode(s)
|
285 |
|
286 |
|
287 |
|
288 | if ([32, 33, 48, 64, 65, 96].includes(bytes.length)) {
|
289 | return { keyBytes: bytes }
|
290 | }
|
291 |
|
292 |
|
293 | try {
|
294 |
|
295 | const [codec, length] = varint.decode(bytes)
|
296 | const possibleCodec: string | undefined =
|
297 | Object.entries(supportedCodecs).filter(([, code]) => code === codec)?.[0][0] ?? ''
|
298 | return { keyBytes: bytes.slice(length), keyType: CODEC_TO_KEY_TYPE[possibleCodec as KNOWN_CODECS] }
|
299 | } catch (e) {
|
300 |
|
301 | return { keyBytes: bytes }
|
302 | }
|
303 | }
|
304 |
|
305 | export function hexToBytes(s: string, minLength?: number): Uint8Array {
|
306 | let input = s.startsWith('0x') ? s.substring(2) : s
|
307 |
|
308 | if (input.length % 2 !== 0) {
|
309 | input = `0${input}`
|
310 | }
|
311 |
|
312 | if (minLength) {
|
313 | const paddedLength = Math.max(input.length, minLength * 2)
|
314 | input = input.padStart(paddedLength, '00')
|
315 | }
|
316 |
|
317 | return u8a.fromString(input.toLowerCase(), 'base16')
|
318 | }
|
319 |
|
320 | export function encodeBase64url(s: string): string {
|
321 | return bytesToBase64url(u8a.fromString(s))
|
322 | }
|
323 |
|
324 | export function decodeBase64url(s: string): string {
|
325 | return u8a.toString(base64ToBytes(s))
|
326 | }
|
327 |
|
328 | export function bytesToHex(b: Uint8Array): string {
|
329 | return u8a.toString(b, 'base16')
|
330 | }
|
331 |
|
332 | export function bytesToBigInt(b: Uint8Array): bigint {
|
333 | return BigInt(`0x` + u8a.toString(b, 'base16'))
|
334 | }
|
335 |
|
336 | export function bigintToBytes(n: bigint, minLength?: number): Uint8Array {
|
337 | return hexToBytes(n.toString(16), minLength)
|
338 | }
|
339 |
|
340 | export function stringToBytes(s: string): Uint8Array {
|
341 | return u8a.fromString(s, 'utf-8')
|
342 | }
|
343 |
|
344 | export function toJose({ r, s, recoveryParam }: EcdsaSignature, recoverable?: boolean): string {
|
345 | const jose = new Uint8Array(recoverable ? 65 : 64)
|
346 | jose.set(u8a.fromString(r, 'base16'), 0)
|
347 | jose.set(u8a.fromString(s, 'base16'), 32)
|
348 | if (recoverable) {
|
349 | if (typeof recoveryParam === 'undefined') {
|
350 | throw new Error('Signer did not return a recoveryParam')
|
351 | }
|
352 | jose[64] = <number>recoveryParam
|
353 | }
|
354 | return bytesToBase64url(jose)
|
355 | }
|
356 |
|
357 | export function fromJose(signature: string): { r: string; s: string; recoveryParam?: number } {
|
358 | const signatureBytes: Uint8Array = base64ToBytes(signature)
|
359 | if (signatureBytes.length < 64 || signatureBytes.length > 65) {
|
360 | throw new TypeError(`Wrong size for signature. Expected 64 or 65 bytes, but got ${signatureBytes.length}`)
|
361 | }
|
362 | const r = bytesToHex(signatureBytes.slice(0, 32))
|
363 | const s = bytesToHex(signatureBytes.slice(32, 64))
|
364 | const recoveryParam = signatureBytes.length === 65 ? signatureBytes[64] : undefined
|
365 | return { r, s, recoveryParam }
|
366 | }
|
367 |
|
368 | export function toSealed(ciphertext: string, tag?: string): Uint8Array {
|
369 | return u8a.concat([base64ToBytes(ciphertext), tag ? base64ToBytes(tag) : new Uint8Array(0)])
|
370 | }
|
371 |
|
372 | export function leftpad(data: string, size = 64): string {
|
373 | if (data.length === size) return data
|
374 | return '0'.repeat(size - data.length) + data
|
375 | }
|
376 |
|
377 |
|
378 |
|
379 |
|
380 | export function generateKeyPair(): { secretKey: Uint8Array; publicKey: Uint8Array } {
|
381 | const secretKey = x25519.utils.randomPrivateKey()
|
382 | const publicKey = x25519.getPublicKey(secretKey)
|
383 | return {
|
384 | secretKey: secretKey,
|
385 | publicKey: publicKey,
|
386 | }
|
387 | }
|
388 |
|
389 |
|
390 |
|
391 |
|
392 | export function generateKeyPairFromSeed(seed: Uint8Array): { secretKey: Uint8Array; publicKey: Uint8Array } {
|
393 | if (seed.length !== 32) {
|
394 | throw new Error(`x25519: seed must be ${32} bytes`)
|
395 | }
|
396 | return {
|
397 | publicKey: x25519.getPublicKey(seed),
|
398 | secretKey: seed,
|
399 | }
|
400 | }
|
401 |
|
402 | export function genX25519EphemeralKeyPair(): EphemeralKeyPair {
|
403 | const epk = generateKeyPair()
|
404 | return {
|
405 | publicKeyJWK: { kty: 'OKP', crv: 'X25519', x: bytesToBase64url(epk.publicKey) },
|
406 | secretKey: epk.secretKey,
|
407 | }
|
408 | }
|
409 |
|
410 |
|
411 |
|
412 |
|
413 |
|
414 |
|
415 |
|
416 |
|
417 |
|
418 | export function isDefined<T>(arg: T): arg is Exclude<T, null | undefined> {
|
419 | return arg !== null && typeof arg !== 'undefined'
|
420 | }
|