@interop/bnid 6.0.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.
package/src/baseX.ts ADDED
@@ -0,0 +1,3 @@
1
+ import { base58 } from '@scure/base'
2
+
3
+ export const base58btc = base58
package/src/index.ts ADDED
@@ -0,0 +1,560 @@
1
+ /*!
2
+ * Copyright (c) 2020 Digital Bazaar, Inc. All rights reserved.
3
+ */
4
+ import { base58btc } from './baseX.js'
5
+ import { getRandomBytes, bytesToHex, bytesFromHex } from './util.js'
6
+
7
+ // multihash identity function code
8
+ const MULTIHASH_IDENTITY_FUNCTION_CODE = 0x00
9
+
10
+ function _calcOptionsBitLength({
11
+ defaultLength,
12
+ // TODO: allow any bit length
13
+ minLength = 8,
14
+ // TODO: support maxLength
15
+ // maxLength = Infinity,
16
+ bitLength
17
+ }: {
18
+ defaultLength: number
19
+ minLength?: number
20
+ bitLength?: number
21
+ }): number {
22
+ if (bitLength === undefined) {
23
+ return defaultLength
24
+ }
25
+ // TODO: allow any bit length
26
+ if (bitLength % 8 !== 0) {
27
+ throw new Error('Bit length must be a multiple of 8.')
28
+ }
29
+ if (bitLength < minLength) {
30
+ throw new Error(`Minimum bit length is ${minLength}.`)
31
+ }
32
+ // TODO: support maxLength
33
+ // if(bitLength > maxLength) {
34
+ // throw new Error(`Maximum bit length is ${maxLength}.`);
35
+ // }
36
+ return bitLength
37
+ }
38
+
39
+ function _calcDataBitLength({
40
+ bitLength,
41
+ maxLength
42
+ }: {
43
+ bitLength: number
44
+ maxLength?: number
45
+ }): number {
46
+ if (maxLength === 0) {
47
+ return bitLength
48
+ }
49
+
50
+ if (maxLength && bitLength > maxLength) {
51
+ throw new Error(`Input length greater than ${maxLength} bits.`)
52
+ }
53
+ // @ts-expect-error - maxLength may be undefined when no fixed length is set
54
+ return maxLength
55
+ }
56
+
57
+ function _bytesWithBitLength({
58
+ bytes,
59
+ bitLength
60
+ }: {
61
+ bytes: Uint8Array
62
+ bitLength: number
63
+ }): Uint8Array {
64
+ const length = bytes.length * 8
65
+ if (length === bitLength) {
66
+ return bytes
67
+ }
68
+ if (length < bitLength) {
69
+ // pad start
70
+ const data = new Uint8Array(bitLength / 8)
71
+ data.set(bytes, data.length - bytes.length)
72
+ return data
73
+ }
74
+ // trim start, ensure trimmed data is zero
75
+ const start = (length - bitLength) / 8
76
+ if (bytes.subarray(0, start).some(d => d !== 0)) {
77
+ throw new Error(`Data length greater than ${bitLength} bits.`)
78
+ }
79
+ return bytes.subarray(start)
80
+ }
81
+
82
+ export interface IEncoder {
83
+ bytes: Uint8Array
84
+ idEncoder: IdEncoder
85
+ }
86
+
87
+ const _log2_16 = 4
88
+
89
+ function _base16Encoder({ bytes, idEncoder }: IEncoder): string {
90
+ let encoded = bytesToHex(bytes)
91
+ if (idEncoder.encoding === 'base16upper') {
92
+ encoded = encoded.toUpperCase()
93
+ }
94
+ if (idEncoder.fixedLength && idEncoder.fixedLength !== undefined) {
95
+ const fixedBitLength = _calcDataBitLength({
96
+ bitLength: bytes.length * 8,
97
+ maxLength: idEncoder.fixedBitLength
98
+ })
99
+ const wantLength = Math.ceil(fixedBitLength / _log2_16)
100
+ // pad start with 0s
101
+ return encoded.padStart(wantLength, '0')
102
+ }
103
+ return encoded
104
+ }
105
+
106
+ const _log2_58 = Math.log2(58)
107
+
108
+ function _base58Encoder({ bytes, idEncoder }: IEncoder): string {
109
+ const encoded = base58btc.encode(bytes)
110
+ if (idEncoder.fixedLength) {
111
+ const fixedBitLength = _calcDataBitLength({
112
+ bitLength: bytes.length * 8,
113
+ maxLength: idEncoder.fixedBitLength
114
+ })
115
+ const wantLength = Math.ceil(fixedBitLength / _log2_58)
116
+ // pad start with 0s (encoded as '1's)
117
+ return encoded.padStart(wantLength, '1')
118
+ }
119
+ return encoded
120
+ }
121
+
122
+ export class IdGenerator {
123
+ public bitLength: number
124
+
125
+ /**
126
+ * Creates a new IdGenerator instance.
127
+ *
128
+ * An IdGenerator generates an array of id bytes.
129
+ *
130
+ * @param {object} [options] - The options to use.
131
+ * @param {number} [options.bitLength=128] - Number of bits to generate.
132
+ *
133
+ * @returns {IdGenerator} - New IdGenerator.
134
+ */
135
+ constructor({ bitLength }: { bitLength?: number } = {}) {
136
+ this.bitLength = _calcOptionsBitLength({
137
+ // default to 128 bits / 16 bytes
138
+ defaultLength: 128,
139
+ // TODO: allow any bit length
140
+ minLength: 8,
141
+ bitLength
142
+ })
143
+ }
144
+
145
+ /**
146
+ * Generate random id bytes.
147
+ *
148
+ * @returns {Uint8Array} - Array of random id bytes.
149
+ */
150
+ async generate(): Promise<Uint8Array> {
151
+ const buf = new Uint8Array(this.bitLength / 8)
152
+ await getRandomBytes(buf)
153
+ return buf
154
+ }
155
+ }
156
+
157
+ export interface IIdEncoder {
158
+ encoding?: string
159
+ fixedLength?: boolean
160
+ fixedBitLength?: number
161
+ bitLength?: number
162
+ multibase?: boolean
163
+ multihash?: boolean
164
+ }
165
+
166
+ export class IdEncoder {
167
+ public encoder: ({ bytes, idEncoder }: IEncoder) => string
168
+ public encoding: string
169
+ public multibasePrefix: string
170
+ public fixedLength: boolean
171
+ public fixedBitLength?: number
172
+ public multibase: boolean = true
173
+ public multihash: boolean = false
174
+
175
+ /**
176
+ * Creates a new IdEncoder instance.
177
+ *
178
+ * An IdEncoder encodes an array of id bytes into a specific encoding.
179
+ *
180
+ * @param {object} [options] - The options to use.
181
+ * @param {string} [options.encoding='base58'] - Encoding format.
182
+ * @param {boolean} [options.fixedLength=false] - `true` to ensure fixed
183
+ * output length.
184
+ * @param {number} [options.fixedBitLength] - Fixed output bit length or 0 to
185
+ * base on input byte size.
186
+ * @param {boolean} [options.multibase=true] - Use multibase encoding.
187
+ * @param {boolean} [options.multihash=false] - Use multihash encoding.
188
+ *
189
+ * @returns {IdEncoder} - New IdEncoder.
190
+ */
191
+ constructor({
192
+ encoding = 'base58',
193
+ fixedLength = false,
194
+ fixedBitLength,
195
+ multibase = true,
196
+ multihash = false
197
+ }: IIdEncoder = {}) {
198
+ switch (encoding) {
199
+ case 'hex':
200
+ case 'base16':
201
+ this.encoder = _base16Encoder
202
+ this.multibasePrefix = 'f'
203
+ break
204
+ case 'base16upper':
205
+ this.encoder = _base16Encoder
206
+ this.multibasePrefix = 'F'
207
+ break
208
+ case 'base58':
209
+ case 'base58btc':
210
+ this.encoder = _base58Encoder
211
+ this.multibasePrefix = 'z'
212
+ break
213
+ default:
214
+ throw new Error(`Unknown encoding type: "${encoding}".`)
215
+ }
216
+ this.fixedLength = fixedLength || fixedBitLength !== undefined
217
+ if (this.fixedLength) {
218
+ this.fixedBitLength = _calcOptionsBitLength({
219
+ // default of 0 calculates from input size
220
+ defaultLength: 0,
221
+ bitLength: fixedBitLength
222
+ })
223
+ }
224
+ this.encoding = encoding
225
+ this.multibase = multibase
226
+ this.multihash = multihash
227
+ }
228
+
229
+ /**
230
+ * Encode id bytes into a string.
231
+ *
232
+ * @param {Uint8Array} bytes - Bytes to encode.
233
+ *
234
+ * @returns {string} - Encoded string.
235
+ */
236
+ encode(bytes: Uint8Array): string {
237
+ if (this.multihash) {
238
+ const byteSize = bytes.length
239
+
240
+ if (byteSize > 127) {
241
+ throw new RangeError('Identifier size too large.')
242
+ }
243
+ // <varint hash fn code> <varint digest size in bytes> <hash fn output>
244
+ // <identity function> <byte size> <raw bytes>
245
+ const multihash = new Uint8Array(2 + byteSize)
246
+ // <varint hash fn code>: identity function
247
+ multihash.set([MULTIHASH_IDENTITY_FUNCTION_CODE])
248
+ // <varint digest size in bytes>
249
+ multihash.set([byteSize], 1)
250
+ // <hash fn output>: identifier bytes
251
+ multihash.set(bytes, 2)
252
+ bytes = multihash
253
+ }
254
+ const encoded = this.encoder({ bytes, idEncoder: this })
255
+ if (this.multibase) {
256
+ return this.multibasePrefix + encoded
257
+ }
258
+ return encoded
259
+ }
260
+ }
261
+
262
+ export interface IIdDecoder {
263
+ encoding?: string
264
+ fixedBitLength?: number
265
+ multibase?: boolean
266
+ multihash?: boolean
267
+ expectedSize?: number
268
+ }
269
+
270
+ export class IdDecoder {
271
+ public encoding: string
272
+ public fixedBitLength?: number = 0
273
+ public multibase: boolean
274
+ public multihash: boolean
275
+ public expectedSize: number
276
+
277
+ /**
278
+ * Creates a new IdDecoder instance.
279
+ *
280
+ * An IdDecoder decodes an id string into a byte array. It is recommended to
281
+ * use the fixedBitLength option to avoid padding ids resulting in a larger
282
+ * than expected byte length.
283
+ *
284
+ * @param {object} [options] - The options to use.
285
+ * @param {string} [options.encoding='base58'] - Encoding format. Ignored if
286
+ * multibase is true.
287
+ * @param {number} [options.fixedBitLength] - Fixed output bit length. Values
288
+ * with leading non-zero data will error.
289
+ * @param {boolean} [options.multibase=true] - Use multibase encoding to
290
+ * detect the id format.
291
+ * @param {boolean} [options.multihash=false] - Use multihash encoding to
292
+ * detect the id format.
293
+ * @param {number} [options.expectedSize=32] - Optional expected identifier
294
+ * size in bytes (only for multihash encoding). Use `0` to disable size
295
+ * check.
296
+ * @returns {IdDecoder} - New IdDecoder.
297
+ */
298
+ constructor({
299
+ encoding = 'base58',
300
+ fixedBitLength = 0,
301
+ multibase = true,
302
+ multihash = false,
303
+ expectedSize = 32
304
+ }: IIdDecoder = {}) {
305
+ this.encoding = encoding
306
+ this.fixedBitLength = fixedBitLength
307
+ this.multibase = multibase
308
+ this.multihash = multihash
309
+ this.expectedSize = expectedSize
310
+ }
311
+
312
+ /**
313
+ * Decode id string into bytes.
314
+ *
315
+ * @param {string} id - Id to decode.
316
+ *
317
+ * @returns {Uint8Array} - Array of decoded id bytes.
318
+ */
319
+ decode(id: string): Uint8Array {
320
+ let encoding
321
+ let data
322
+ if (this.multibase) {
323
+ if (id.length < 1) {
324
+ throw new Error('Multibase encoding not found.')
325
+ }
326
+ const prefix = id[0]
327
+ data = id.substring(1)
328
+ switch (id[0]) {
329
+ case 'f':
330
+ encoding = 'base16'
331
+ break
332
+ case 'F':
333
+ encoding = 'base16upper'
334
+ break
335
+ case 'z':
336
+ encoding = 'base58'
337
+ break
338
+ default:
339
+ throw new Error(`Unknown multibase prefix "${prefix}".`)
340
+ }
341
+ } else {
342
+ encoding = this.encoding
343
+ data = id
344
+ }
345
+ let decoded
346
+ switch (encoding) {
347
+ case 'hex':
348
+ case 'base16':
349
+ case 'base16upper':
350
+ if (data.length % 2 !== 0) {
351
+ throw new Error('Invalid base16 data length.')
352
+ }
353
+ decoded = bytesFromHex(data)
354
+ break
355
+ case 'base58':
356
+ decoded = base58btc.decode(data)
357
+ break
358
+ default:
359
+ throw new Error(`Unknown encoding "${encoding}".`)
360
+ }
361
+
362
+ if (!decoded) {
363
+ throw new Error(`Invalid encoded data "${data}".`)
364
+ }
365
+
366
+ if (this.fixedBitLength) {
367
+ return _bytesWithBitLength({
368
+ bytes: decoded,
369
+ bitLength: this.fixedBitLength ?? 0
370
+ })
371
+ }
372
+ if (this.multihash) {
373
+ // <varint hash fn code>: identity function
374
+ const [hashFnCode] = decoded
375
+
376
+ if (hashFnCode !== MULTIHASH_IDENTITY_FUNCTION_CODE) {
377
+ throw new Error('Invalid multihash function code.')
378
+ }
379
+ // <varint digest size in bytes>
380
+ const digestSize = decoded[1]
381
+
382
+ if (digestSize > 127) {
383
+ throw new RangeError('Decoded identifier size too large.')
384
+ }
385
+
386
+ const bytes = decoded.subarray(2)
387
+
388
+ if (bytes.byteLength !== digestSize) {
389
+ throw new RangeError('Unexpected identifier size.')
390
+ }
391
+
392
+ if (this.expectedSize && bytes.byteLength !== this.expectedSize) {
393
+ throw new RangeError(
394
+ 'Invalid decoded identifier size. Identifier must be ' +
395
+ `"${this.expectedSize}" bytes.`
396
+ )
397
+ }
398
+
399
+ decoded = bytes
400
+ }
401
+ return decoded
402
+ }
403
+ }
404
+
405
+ /**
406
+ * Generates an encoded id string from random bits.
407
+ *
408
+ * @param {object} [options] - The options to use. See `IdEncoder` and
409
+ * `IdGenerator` for available options.
410
+ *
411
+ * @returns {string} - Encoded string id.
412
+ */
413
+ export async function generateId(options: IIdEncoder): Promise<string> {
414
+ return new IdEncoder(options).encode(
415
+ await new IdGenerator(options).generate()
416
+ )
417
+ }
418
+
419
+ /**
420
+ * Decodes an encoded id string to an array of bytes.
421
+ *
422
+ * @param {object} options - The options to use. See `IdDecoder` for available
423
+ * options.
424
+ * @param {string} options.id - Id to decode.
425
+ *
426
+ * @returns {Uint8Array} - Decoded array of id bytes.
427
+ */
428
+ export function decodeId(options: IIdDecoder & { id: string }): Uint8Array {
429
+ return new IdDecoder(options).decode(options.id)
430
+ }
431
+
432
+ /**
433
+ * Minimum number of bytes needed to encode an id of a given bit length.
434
+ *
435
+ * @param {object} options - The options to use.
436
+ * @param {string} [options.encoding='base58'] - Encoding format.
437
+ * @param {number} [options.bitLength=128] - Number of id bits.
438
+ * @param {boolean} [options.multibase=true] - Account for multibase encoding.
439
+ *
440
+ * @returns {number} - The minimum number of encoded bytes.
441
+ */
442
+ export function minEncodedIdBytes({
443
+ encoding = 'base58',
444
+ bitLength = 128,
445
+ multibase = true
446
+ }: IIdEncoder = {}): number {
447
+ let plainBytes
448
+ switch (encoding) {
449
+ case 'hex':
450
+ case 'base16':
451
+ case 'base16upper':
452
+ plainBytes = bitLength / 4
453
+ break
454
+ case 'base58':
455
+ case 'base58btc':
456
+ plainBytes = bitLength / 8
457
+ break
458
+ default:
459
+ throw new Error(`Unknown encoding type: "${encoding}".`)
460
+ }
461
+ return plainBytes + (multibase ? 1 : 0)
462
+ }
463
+
464
+ /**
465
+ * Maximum number of bytes needed to encode an id of a given bit length.
466
+ *
467
+ * @param {object} options - The options to use.
468
+ * @param {string} [options.encoding='base58'] - Encoding format.
469
+ * @param {number} [options.bitLength=128] - Number of id bits.
470
+ * @param {boolean} [options.multibase=true] - Account for multibase encoding.
471
+ *
472
+ * @returns {number} - The maximum number of encoded bytes.
473
+ */
474
+ export function maxEncodedIdBytes({
475
+ encoding = 'base58',
476
+ bitLength = 128,
477
+ multibase = true
478
+ }: IIdEncoder = {}): number {
479
+ let plainBytes
480
+ switch (encoding) {
481
+ case 'hex':
482
+ case 'base16':
483
+ case 'base16upper':
484
+ plainBytes = bitLength / 4
485
+ break
486
+ case 'base58':
487
+ case 'base58btc':
488
+ plainBytes = Math.ceil(bitLength / Math.log2(58))
489
+ break
490
+ default:
491
+ throw new Error(`Unknown encoding type: "${encoding}".`)
492
+ }
493
+ return plainBytes + (multibase ? 1 : 0)
494
+ }
495
+
496
+ /**
497
+ * Generates a secret key seed encoded as a string that can be stored and later
498
+ * used to generate a key pair. The public key from the key pair can be used as
499
+ * an identifier. The key seed (both raw and encoded form) MUST be kept secret.
500
+ *
501
+ * @param {object} [options] - The options to use.
502
+ * @param {string} [options.encoding='base58'] - Encoding format.
503
+ * @param {number} [options.bitLength=32 * 8] - Number of bits to generate.
504
+ * @param {boolean} [options.multibase=true] - Use multibase encoding.
505
+ * @param {boolean} [options.multihash=true] - Use multihash encoding.
506
+
507
+ * @returns {string} - Secret key seed encoded as a string.
508
+ */
509
+ export async function generateSecretKeySeed({
510
+ bitLength = 32 * 8,
511
+ encoding = 'base58',
512
+ multibase = true,
513
+ multihash = true
514
+ }: IIdEncoder = {}): Promise<string> {
515
+ // reuse `generateId` for convenience, but a key seed is *SECRET* and
516
+ // not an identifier itself, rather it is used to generate an identifier via
517
+ // a public key
518
+ // Note: Setting fixedLength to false even though that's the (current)
519
+ // default as not using a fixed length of false for a seed is a security
520
+ // problem
521
+ return await generateId({
522
+ bitLength,
523
+ encoding,
524
+ fixedLength: false,
525
+ multibase,
526
+ multihash
527
+ })
528
+ }
529
+
530
+ /**
531
+ * Decodes an encoded secret key seed into an array of secret key seed bytes.
532
+ * The key seed bytes MUST be kept secret.
533
+ *
534
+ * @param {object} options - The options to use.
535
+ * @param {boolean} [options.multibase=true] - Use multibase encoding to detect
536
+ * the id format.
537
+ * @param {boolean} [options.multihash=true] - Use multihash encoding to detect
538
+ * the id format.
539
+ * @param {number} [options.expectedSize] - Optional expected identifier size
540
+ * in bytes (only for multihash encoding). Use `0` to disable size check.
541
+ * @param {string} options.secretKeySeed - The secret key seed to be decoded.
542
+ *
543
+ * @returns {Uint8Array} - An array of secret key seed bytes (default size:
544
+ * 32 bytes).
545
+ */
546
+ export function decodeSecretKeySeed({
547
+ multibase = true,
548
+ multihash = true,
549
+ expectedSize = 32,
550
+ secretKeySeed
551
+ }: {
552
+ multibase?: boolean
553
+ multihash?: boolean
554
+ expectedSize?: number
555
+ secretKeySeed: string
556
+ }): Uint8Array {
557
+ // reuse `decodeId` for convenience, but key seed bytes are *SECRET* and
558
+ // are NOT identifiers, they are used to generate identifiers from public keys
559
+ return decodeId({ multihash, multibase, expectedSize, id: secretKeySeed })
560
+ }
@@ -0,0 +1,23 @@
1
+ // browser support
2
+ const crypto = globalThis.crypto
3
+
4
+ export async function getRandomBytes(buf: Uint8Array): Promise<Uint8Array> {
5
+ return crypto.getRandomValues(buf)
6
+ }
7
+
8
+ export function bytesToHex(bytes: Uint8Array): string {
9
+ return Array.from(bytes)
10
+ .map(d => d.toString(16).padStart(2, '0'))
11
+ .join('')
12
+ }
13
+
14
+ // adapted from:
15
+
16
+ // https://stackoverflow.com/questions/43131242/how-to-convert-a-hexadecimal-string-of-data-to-an-arraybuffer-in-javascript
17
+ export function bytesFromHex(hex: string): Uint8Array {
18
+ if (hex.length === 0) {
19
+ return new Uint8Array()
20
+ }
21
+ // @ts-expect-error - match() is non-null for validated hex input
22
+ return new Uint8Array(hex.match(/[\da-f]{2}/gi).map(h => parseInt(h, 16)))
23
+ }
@@ -0,0 +1,27 @@
1
+ // React Native support
2
+ //
3
+ // Requires the `react-native-get-random-values` polyfill, imported once at the
4
+ // top of the app entry, so that `globalThis.crypto.getRandomValues()` is
5
+ // available. See the README "React Native" section.
6
+ const crypto = globalThis.crypto
7
+
8
+ export async function getRandomBytes(buf: Uint8Array): Promise<Uint8Array> {
9
+ return crypto.getRandomValues(buf)
10
+ }
11
+
12
+ export function bytesToHex(bytes: Uint8Array): string {
13
+ return Array.from(bytes)
14
+ .map(d => d.toString(16).padStart(2, '0'))
15
+ .join('')
16
+ }
17
+
18
+ // adapted from:
19
+
20
+ // https://stackoverflow.com/questions/43131242/how-to-convert-a-hexadecimal-string-of-data-to-an-arraybuffer-in-javascript
21
+ export function bytesFromHex(hex: string): Uint8Array {
22
+ if (hex.length === 0) {
23
+ return new Uint8Array()
24
+ }
25
+ // @ts-expect-error - match() is non-null for validated hex input
26
+ return new Uint8Array(hex.match(/[\da-f]{2}/gi).map(h => parseInt(h, 16)))
27
+ }
package/src/util.ts ADDED
@@ -0,0 +1,17 @@
1
+ // Node.js support
2
+ import * as crypto from 'crypto'
3
+ import { promisify } from 'util'
4
+
5
+ const randomFill = promisify(crypto.randomFill)
6
+
7
+ export async function getRandomBytes(buf: Uint8Array): Promise<unknown> {
8
+ return await randomFill(buf)
9
+ }
10
+
11
+ export function bytesToHex(bytes: Uint8Array): string {
12
+ return Buffer.from(bytes).toString('hex')
13
+ }
14
+
15
+ export function bytesFromHex(hex: string): Uint8Array {
16
+ return new Uint8Array(Buffer.from(hex, 'hex'))
17
+ }