@bsv/sdk 1.9.19 → 1.9.22

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 (55) hide show
  1. package/dist/cjs/package.json +1 -1
  2. package/dist/cjs/src/auth/certificates/MasterCertificate.js +9 -2
  3. package/dist/cjs/src/auth/certificates/MasterCertificate.js.map +1 -1
  4. package/dist/cjs/src/primitives/DRBG.js +12 -1
  5. package/dist/cjs/src/primitives/DRBG.js.map +1 -1
  6. package/dist/cjs/src/primitives/Hash.js +6 -5
  7. package/dist/cjs/src/primitives/Hash.js.map +1 -1
  8. package/dist/cjs/src/primitives/hex.js +33 -0
  9. package/dist/cjs/src/primitives/hex.js.map +1 -0
  10. package/dist/cjs/src/primitives/index.js +1 -3
  11. package/dist/cjs/src/primitives/index.js.map +1 -1
  12. package/dist/cjs/src/primitives/utils.js +69 -59
  13. package/dist/cjs/src/primitives/utils.js.map +1 -1
  14. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  15. package/dist/esm/src/auth/certificates/MasterCertificate.js +9 -2
  16. package/dist/esm/src/auth/certificates/MasterCertificate.js.map +1 -1
  17. package/dist/esm/src/primitives/DRBG.js +12 -1
  18. package/dist/esm/src/primitives/DRBG.js.map +1 -1
  19. package/dist/esm/src/primitives/Hash.js +6 -5
  20. package/dist/esm/src/primitives/Hash.js.map +1 -1
  21. package/dist/esm/src/primitives/hex.js +29 -0
  22. package/dist/esm/src/primitives/hex.js.map +1 -0
  23. package/dist/esm/src/primitives/index.js +0 -1
  24. package/dist/esm/src/primitives/index.js.map +1 -1
  25. package/dist/esm/src/primitives/utils.js +69 -59
  26. package/dist/esm/src/primitives/utils.js.map +1 -1
  27. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  28. package/dist/types/src/auth/certificates/MasterCertificate.d.ts.map +1 -1
  29. package/dist/types/src/primitives/DRBG.d.ts +12 -1
  30. package/dist/types/src/primitives/DRBG.d.ts.map +1 -1
  31. package/dist/types/src/primitives/Hash.d.ts.map +1 -1
  32. package/dist/types/src/primitives/hex.d.ts +3 -0
  33. package/dist/types/src/primitives/hex.d.ts.map +1 -0
  34. package/dist/types/src/primitives/index.d.ts +0 -1
  35. package/dist/types/src/primitives/index.d.ts.map +1 -1
  36. package/dist/types/src/primitives/utils.d.ts.map +1 -1
  37. package/dist/types/tsconfig.types.tsbuildinfo +1 -1
  38. package/dist/umd/bundle.js +3 -3
  39. package/dist/umd/bundle.js.map +1 -1
  40. package/docs/reference/auth.md +2 -2
  41. package/docs/reference/primitives.md +90 -31
  42. package/package.json +1 -1
  43. package/src/auth/__tests/Peer.test.ts +2 -1
  44. package/src/auth/certificates/MasterCertificate.ts +9 -2
  45. package/src/auth/certificates/__tests/MasterCertificate.test.ts +46 -9
  46. package/src/primitives/DRBG.ts +12 -1
  47. package/src/primitives/Hash.ts +9 -6
  48. package/src/primitives/__tests/HMAC.test.ts +13 -2
  49. package/src/primitives/__tests/Hash.test.ts +24 -0
  50. package/src/primitives/__tests/hex.test.ts +57 -0
  51. package/src/primitives/__tests/utils.test.ts +39 -0
  52. package/src/primitives/hex.ts +35 -0
  53. package/src/primitives/index.ts +0 -1
  54. package/src/primitives/utils.ts +71 -65
  55. package/src/script/__tests/Script.test.ts +1 -1
@@ -502,7 +502,7 @@ export class MasterCertificate extends Certificate {
502
502
  static async createKeyringForVerifier(subjectWallet: ProtoWallet, certifier: WalletCounterparty, verifier: WalletCounterparty, fields: Record<CertificateFieldNameUnder50Bytes, Base64String>, fieldsToReveal: string[], masterKeyring: Record<CertificateFieldNameUnder50Bytes, Base64String>, serialNumber: Base64String, privileged?: boolean, privilegedReason?: string): Promise<Record<CertificateFieldNameUnder50Bytes, string>>
503
503
  static async issueCertificateForSubject(certifierWallet: ProtoWallet, subject: WalletCounterparty, fields: Record<CertificateFieldNameUnder50Bytes, string>, certificateType: string, getRevocationOutpoint = async (_serial: string): Promise<string> => {
504
504
  void _serial;
505
- return "Certificate revocation not tracked.";
505
+ return "00".repeat(32);
506
506
  }, serialNumber?: string): Promise<MasterCertificate>
507
507
  static async decryptFields(subjectOrCertifierWallet: ProtoWallet, masterKeyring: Record<CertificateFieldNameUnder50Bytes, Base64String>, fields: Record<CertificateFieldNameUnder50Bytes, Base64String>, counterparty: WalletCounterparty, privileged?: boolean, privilegedReason?: string): Promise<Record<CertificateFieldNameUnder50Bytes, string>>
508
508
  static async decryptField(subjectOrCertifierWallet: ProtoWallet, masterKeyring: Record<CertificateFieldNameUnder50Bytes, Base64String>, fieldName: Base64String, fieldValue: Base64String, counterparty: WalletCounterparty, privileged?: boolean, privilegedReason?: string): Promise<{
@@ -634,7 +634,7 @@ can also includes a revocation outpoint to manage potential revocation.
634
634
  ```ts
635
635
  static async issueCertificateForSubject(certifierWallet: ProtoWallet, subject: WalletCounterparty, fields: Record<CertificateFieldNameUnder50Bytes, string>, certificateType: string, getRevocationOutpoint = async (_serial: string): Promise<string> => {
636
636
  void _serial;
637
- return "Certificate revocation not tracked.";
637
+ return "00".repeat(32);
638
638
  }, serialNumber?: string): Promise<MasterCertificate>
639
639
  ```
640
640
  See also: [CertificateFieldNameUnder50Bytes](./wallet.md#type-certificatefieldnameunder50bytes), [MasterCertificate](./auth.md#class-mastercertificate), [ProtoWallet](./wallet.md#class-protowallet), [WalletCounterparty](./wallet.md#type-walletcounterparty)
@@ -829,7 +829,17 @@ Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](
829
829
  ---
830
830
  ### Class: DRBG
831
831
 
832
- This class behaves as a HMAC-based deterministic random bit generator (DRBG). It implements a deterministic random number generator using SHA256HMAC HASH function. It takes an initial entropy and nonce when instantiated for seeding purpose.
832
+ HMAC-DRBG used **only** for deterministic ECDSA nonce generation.
833
+
834
+ This implementation follows the RFC 6979-style HMAC-DRBG construction for secp256k1
835
+ and is wired internally into the ECDSA signing code. It is **not forward-secure**
836
+ and MUST NOT be used as a general-purpose DRBG, key generator, or randomness source.
837
+
838
+ Security note:
839
+ - Intended scope: internal ECDSA nonce generation with fixed-size inputs.
840
+ - Out-of-scope: generic randomness, long-lived session keys, or any context
841
+ where forward secrecy is required.
842
+ - API stability: this class is internal.
833
843
 
834
844
  Example
835
845
 
@@ -4953,8 +4963,10 @@ Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](
4953
4963
  | [AES](#function-aes) |
4954
4964
  | [AESGCM](#function-aesgcm) |
4955
4965
  | [AESGCMDecrypt](#function-aesgcmdecrypt) |
4966
+ | [assertValidHex](#function-assertvalidhex) |
4956
4967
  | [base64ToArray](#function-base64toarray) |
4957
4968
  | [ghash](#function-ghash) |
4969
+ | [normalizeHex](#function-normalizehex) |
4958
4970
  | [pbkdf2](#function-pbkdf2) |
4959
4971
  | [red](#function-red) |
4960
4972
  | [toArray](#function-toarray) |
@@ -4994,6 +5006,15 @@ export function AESGCMDecrypt(cipherText: number[], additionalAuthenticatedData:
4994
5006
 
4995
5007
  Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](#functions), [Types](#types), [Enums](#enums), [Variables](#variables)
4996
5008
 
5009
+ ---
5010
+ ### Function: assertValidHex
5011
+
5012
+ ```ts
5013
+ export function assertValidHex(msg: string): void
5014
+ ```
5015
+
5016
+ Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](#functions), [Types](#types), [Enums](#enums), [Variables](#variables)
5017
+
4997
5018
  ---
4998
5019
  ### Function: base64ToArray
4999
5020
 
@@ -5012,6 +5033,15 @@ export function ghash(input: number[], hashSubKey: number[]): number[]
5012
5033
 
5013
5034
  Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](#functions), [Types](#types), [Enums](#enums), [Variables](#variables)
5014
5035
 
5036
+ ---
5037
+ ### Function: normalizeHex
5038
+
5039
+ ```ts
5040
+ export function normalizeHex(msg: string): string
5041
+ ```
5042
+
5043
+ Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](#functions), [Types](#types), [Enums](#enums), [Variables](#variables)
5044
+
5015
5045
  ---
5016
5046
  ### Function: pbkdf2
5017
5047
 
@@ -5939,7 +5969,7 @@ sign = (msg: BigNumber, key: BigNumber, forceLowS: boolean = false, customK?: Bi
5939
5969
  if (kBN == null)
5940
5970
  throw new Error("k is undefined");
5941
5971
  kBN = truncateToN(kBN, true);
5942
- if (kBN.cmpn(1) <= 0 || kBN.cmp(ns1) >= 0) {
5972
+ if (kBN.cmpn(1) < 0 || kBN.cmp(ns1) > 0) {
5943
5973
  if (BigNumber.isBN(customK)) {
5944
5974
  throw new Error("Invalid fixed custom K value (must be >1 and <N\u20111)");
5945
5975
  }
@@ -6090,49 +6120,78 @@ Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](
6090
6120
  ```ts
6091
6121
  toUTF8 = (arr: number[]): string => {
6092
6122
  let result = "";
6093
- let skip = 0;
6123
+ const replacementChar = "\uFFFD";
6094
6124
  for (let i = 0; i < arr.length; i++) {
6095
- const byte = arr[i];
6096
- if (skip > 0) {
6097
- skip--;
6125
+ const byte1 = arr[i];
6126
+ if (byte1 <= 127) {
6127
+ result += String.fromCharCode(byte1);
6098
6128
  continue;
6099
6129
  }
6100
- if (byte <= 127) {
6101
- result += String.fromCharCode(byte);
6102
- continue;
6103
- }
6104
- if (byte >= 192 && byte <= 223) {
6105
- const avail = arr.length - (i + 1);
6106
- const byte2 = avail >= 1 ? arr[i + 1] : 0;
6107
- skip = Math.min(1, avail);
6108
- const codePoint = ((byte & 31) << 6) | (byte2 & 63);
6130
+ const emitReplacement = (): void => {
6131
+ result += replacementChar;
6132
+ };
6133
+ if (byte1 >= 192 && byte1 <= 223) {
6134
+ if (i + 1 >= arr.length) {
6135
+ emitReplacement();
6136
+ continue;
6137
+ }
6138
+ const byte2 = arr[i + 1];
6139
+ if ((byte2 & 192) !== 128) {
6140
+ emitReplacement();
6141
+ i += 1;
6142
+ continue;
6143
+ }
6144
+ const codePoint = ((byte1 & 31) << 6) | (byte2 & 63);
6109
6145
  result += String.fromCharCode(codePoint);
6146
+ i += 1;
6110
6147
  continue;
6111
6148
  }
6112
- if (byte >= 224 && byte <= 239) {
6113
- const avail = arr.length - (i + 1);
6114
- const byte2 = avail >= 1 ? arr[i + 1] : 0;
6115
- const byte3 = avail >= 2 ? arr[i + 2] : 0;
6116
- skip = Math.min(2, avail);
6117
- const codePoint = ((byte & 15) << 12) | ((byte2 & 63) << 6) | (byte3 & 63);
6149
+ if (byte1 >= 224 && byte1 <= 239) {
6150
+ if (i + 2 >= arr.length) {
6151
+ emitReplacement();
6152
+ continue;
6153
+ }
6154
+ const byte2 = arr[i + 1];
6155
+ const byte3 = arr[i + 2];
6156
+ if ((byte2 & 192) !== 128 || (byte3 & 192) !== 128) {
6157
+ emitReplacement();
6158
+ i += 2;
6159
+ continue;
6160
+ }
6161
+ const codePoint = ((byte1 & 15) << 12) |
6162
+ ((byte2 & 63) << 6) |
6163
+ (byte3 & 63);
6118
6164
  result += String.fromCharCode(codePoint);
6165
+ i += 2;
6119
6166
  continue;
6120
6167
  }
6121
- if (byte >= 240 && byte <= 247) {
6122
- const avail = arr.length - (i + 1);
6123
- const byte2 = avail >= 1 ? arr[i + 1] : 0;
6124
- const byte3 = avail >= 2 ? arr[i + 2] : 0;
6125
- const byte4 = avail >= 3 ? arr[i + 3] : 0;
6126
- skip = Math.min(3, avail);
6127
- const codePoint = ((byte & 7) << 18) |
6168
+ if (byte1 >= 240 && byte1 <= 247) {
6169
+ if (i + 3 >= arr.length) {
6170
+ emitReplacement();
6171
+ continue;
6172
+ }
6173
+ const byte2 = arr[i + 1];
6174
+ const byte3 = arr[i + 2];
6175
+ const byte4 = arr[i + 3];
6176
+ if ((byte2 & 192) !== 128 ||
6177
+ (byte3 & 192) !== 128 ||
6178
+ (byte4 & 192) !== 128) {
6179
+ emitReplacement();
6180
+ i += 3;
6181
+ continue;
6182
+ }
6183
+ const codePoint = ((byte1 & 7) << 18) |
6128
6184
  ((byte2 & 63) << 12) |
6129
6185
  ((byte3 & 63) << 6) |
6130
6186
  (byte4 & 63);
6131
- const surrogate1 = 55296 + ((codePoint - 65536) >> 10);
6132
- const surrogate2 = 56320 + ((codePoint - 65536) & 1023);
6133
- result += String.fromCharCode(surrogate1, surrogate2);
6187
+ const offset = codePoint - 65536;
6188
+ const highSurrogate = 55296 + (offset >> 10);
6189
+ const lowSurrogate = 56320 + (offset & 1023);
6190
+ result += String.fromCharCode(highSurrogate, lowSurrogate);
6191
+ i += 3;
6134
6192
  continue;
6135
6193
  }
6194
+ emitReplacement();
6136
6195
  }
6137
6196
  return result;
6138
6197
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bsv/sdk",
3
- "version": "1.9.19",
3
+ "version": "1.9.22",
4
4
  "type": "module",
5
5
  "description": "BSV Blockchain Software Development Kit",
6
6
  "main": "dist/cjs/mod.js",
@@ -12,6 +12,7 @@ import { SimplifiedFetchTransport } from '../../auth/transports/SimplifiedFetchT
12
12
  const certifierPrivKey = new PrivateKey(21)
13
13
  const alicePrivKey = new PrivateKey(22)
14
14
  const bobPrivKey = new PrivateKey(23)
15
+ const DUMMY_REVOCATION_OUTPOINT_HEX = '00'.repeat(36)
15
16
 
16
17
  jest.mock('../../auth/utils/getVerifiableCertificates')
17
18
 
@@ -101,7 +102,7 @@ describe('Peer class mutual authentication and certificate exchange', () => {
101
102
  subjectPubKey,
102
103
  fields,
103
104
  certificateType,
104
- async () => 'revocationOutpoint' // or any revocation outpoint logic you want
105
+ async () => DUMMY_REVOCATION_OUTPOINT_HEX
105
106
  )
106
107
 
107
108
  // For test consistency, you could override the auto-generated serialNumber:
@@ -232,7 +232,7 @@ export class MasterCertificate extends Certificate {
232
232
  certificateType: string,
233
233
  getRevocationOutpoint = async (_serial: string): Promise<string> => {
234
234
  void _serial // Explicitly acknowledge unused parameter
235
- return 'Certificate revocation not tracked.'
235
+ return '00'.repeat(32)
236
236
  },
237
237
  serialNumber?: string
238
238
  ): Promise<MasterCertificate> {
@@ -246,11 +246,18 @@ export class MasterCertificate extends Certificate {
246
246
  // 3. Obtain a revocation outpoint
247
247
  const revocationOutpoint = await getRevocationOutpoint(finalSerialNumber)
248
248
 
249
+ let subjectIdentityKey: string
250
+ if (subject === 'self') {
251
+ subjectIdentityKey = (await certifierWallet.getPublicKey({ identityKey: true })).publicKey
252
+ } else {
253
+ subjectIdentityKey = subject
254
+ }
255
+
249
256
  // 4. Create new MasterCertificate instance
250
257
  const certificate = new MasterCertificate(
251
258
  certificateType,
252
259
  finalSerialNumber,
253
- subject,
260
+ subjectIdentityKey,
254
261
  (await certifierWallet.getPublicKey({ identityKey: true })).publicKey,
255
262
  revocationOutpoint,
256
263
  certificateFields,
@@ -14,7 +14,8 @@ const verifierKey2 = new PrivateKey(81)
14
14
 
15
15
  // A mock revocation outpoint for testing
16
16
  const mockRevocationOutpoint =
17
- 'deadbeefdeadbeefdeadbeefdeadbeef00000000000000000000000000000000.1'
17
+ 'deadbeefdeadbeefdeadbeefdeadbeef00000001'
18
+
18
19
 
19
20
  // Arbitrary certificate data (in plaintext)
20
21
  const plaintextFields = {
@@ -355,33 +356,69 @@ describe('MasterCertificate', () => {
355
356
  expect(newCert.fields[fieldName]).toMatch(/^[A-Za-z0-9+/]+=*$/)
356
357
  }
357
358
  })
359
+
358
360
  it('should allow issuing a self-signed certificate and decrypt it with the same wallet', async () => {
359
- // In a self-signed scenario, the subject and certifier are the same
360
361
  const subjectWallet = new CompletedProtoWallet(subjectKey2)
361
362
 
362
- // Some sample fields
363
363
  const selfSignedFields = {
364
364
  owner: 'Bob',
365
365
  organization: 'SelfCo'
366
366
  }
367
367
 
368
- // Issue the certificate for "self"
368
+ const subjectIdentityKey = (
369
+ await subjectWallet.getPublicKey({ identityKey: true })
370
+ ).publicKey
371
+
372
+ // Issue the certificate: subject = actual identity key (valid hex)
369
373
  const selfSignedCert = await MasterCertificate.issueCertificateForSubject(
370
- subjectWallet, // act as certifier
371
- 'self',
374
+ subjectWallet, // acts as certifier
375
+ subjectIdentityKey, // <-- was 'self', now real hex
372
376
  selfSignedFields,
373
377
  'SELF_SIGNED_TEST'
374
378
  )
375
379
 
376
- // Now we attempt to decrypt the fields with the same wallet
380
+ // Decrypt with the same wallet
377
381
  const decrypted = await MasterCertificate.decryptFields(
378
382
  subjectWallet,
379
383
  selfSignedCert.masterKeyring,
380
384
  selfSignedCert.fields,
381
- 'self'
385
+ 'self' // still fine here if decryptFields treats 'self' specially
382
386
  )
383
387
 
384
388
  expect(decrypted).toEqual(selfSignedFields)
385
389
  })
390
+
391
+ it('resolves subject === "self" to the certifier wallet identity key', async () => {
392
+ const certifierWallet = new CompletedProtoWallet(new PrivateKey(99))
393
+
394
+ const certifierIdentityKey = (
395
+ await certifierWallet.getPublicKey({ identityKey: true })
396
+ ).publicKey
397
+
398
+ const cert = await MasterCertificate.issueCertificateForSubject(
399
+ certifierWallet,
400
+ 'self',
401
+ { name: 'Alice' },
402
+ 'TEST_CERT'
403
+ )
404
+
405
+ expect(cert.subject).toBe(certifierIdentityKey)
406
+ })
407
+
408
+ it('uses provided subjectIdentityKey when subject is a valid hex string', async () => {
409
+ const certifierWallet = new CompletedProtoWallet(new PrivateKey(42))
410
+
411
+ const validPubkey =
412
+ '0279BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798'
413
+
414
+ const cert = await MasterCertificate.issueCertificateForSubject(
415
+ certifierWallet,
416
+ validPubkey,
417
+ { name: 'Alice' },
418
+ 'TEST_CERT'
419
+ )
420
+
421
+ expect(cert.subject).toBe(validPubkey)
422
+ })
386
423
  })
387
- })
424
+ })
@@ -2,7 +2,18 @@ import { SHA256HMAC } from './Hash.js'
2
2
  import { toHex, toArray } from './utils.js'
3
3
 
4
4
  /**
5
- * This class behaves as a HMAC-based deterministic random bit generator (DRBG). It implements a deterministic random number generator using SHA256HMAC HASH function. It takes an initial entropy and nonce when instantiated for seeding purpose.
5
+ * HMAC-DRBG used **only** for deterministic ECDSA nonce generation.
6
+ *
7
+ * This implementation follows the RFC 6979-style HMAC-DRBG construction for secp256k1
8
+ * and is wired internally into the ECDSA signing code. It is **not forward-secure**
9
+ * and MUST NOT be used as a general-purpose DRBG, key generator, or randomness source.
10
+ *
11
+ * Security note:
12
+ * - Intended scope: internal ECDSA nonce generation with fixed-size inputs.
13
+ * - Out-of-scope: generic randomness, long-lived session keys, or any context
14
+ * where forward secrecy is required.
15
+ * - API stability: this class is internal.
16
+ *
6
17
  * @class DRBG
7
18
  *
8
19
  * @constructor
@@ -1,6 +1,8 @@
1
1
 
2
2
  // @ts-nocheck
3
3
  /* eslint-disable @typescript-eslint/naming-convention */
4
+ import { assertValidHex, normalizeHex } from './hex.js'
5
+
4
6
  const assert = (
5
7
  expression: unknown,
6
8
  message: string = 'Hash assertion failed'
@@ -169,6 +171,10 @@ abstract class BaseHash {
169
171
  */
170
172
  private _pad (): number[] {
171
173
  const len = this.pendingTotal
174
+ if (!Number.isSafeInteger(len) || len < 0) {
175
+ throw new Error('Message too long for this hash function')
176
+ }
177
+
172
178
  const bytes = this._delta8
173
179
  const k = bytes - ((len + this.padLength) % bytes)
174
180
  const res = new Array(k + this.padLength)
@@ -177,8 +183,6 @@ abstract class BaseHash {
177
183
  for (i = 1; i < k; i++) {
178
184
  res[i] = 0
179
185
  }
180
-
181
- // Append length
182
186
  const lengthBytes = this.padLength
183
187
  const maxBits = 1n << BigInt(lengthBytes * 8)
184
188
  let totalBits = BigInt(len) * 8n
@@ -204,6 +208,7 @@ abstract class BaseHash {
204
208
  totalBits >>= 8n
205
209
  }
206
210
  }
211
+
207
212
  return res
208
213
  }
209
214
  }
@@ -262,10 +267,8 @@ export function toArray (
262
267
  }
263
268
  }
264
269
  } else {
265
- msg = msg.replace(/[^a-z0-9]+/gi, '')
266
- if (msg.length % 2 !== 0) {
267
- msg = '0' + msg
268
- }
270
+ assertValidHex(msg)
271
+ msg = normalizeHex(msg)
269
272
  for (let i = 0; i < msg.length; i += 2) {
270
273
  res.push(parseInt(msg[i] + msg[i + 1], 16))
271
274
  }
@@ -48,11 +48,22 @@ describe('HMAC', function () {
48
48
  res: 'cf5ad5984f9e43917aa9087380dac46e410ddc8a7731859c84e9d0f31bd43655'
49
49
  })
50
50
 
51
+ function normalizeKey (key: string | number[]): string | number[] {
52
+ if (typeof key === 'string') {
53
+ // test-only helper: remove whitespace between hex groups
54
+ return key.replace(/\s+/g, '')
55
+ }
56
+ return key
57
+ }
58
+
51
59
  function test (opt): void {
52
60
  it(`should not fail at ${opt.name as string}`, function (): void {
53
- let h = new SHA256HMAC(opt.key)
61
+ const key = normalizeKey(opt.key)
62
+
63
+ let h = new SHA256HMAC(key as any)
54
64
  expect(h.update(opt.msg, opt.msgEnc).digestHex()).toEqual(opt.res)
55
- h = h = new SHA256HMAC(opt.key)
65
+
66
+ h = new SHA256HMAC(key as any)
56
67
  expect(
57
68
  h
58
69
  .update(opt.msg.slice(0, 10), opt.msgEnc)
@@ -3,6 +3,7 @@ import * as hash from '../../primitives/Hash'
3
3
  import * as crypto from 'crypto'
4
4
  import PBKDF2Vectors from './PBKDF2.vectors'
5
5
  import { toArray, toHex } from '../../primitives/utils'
6
+ import { SHA1 } from '../..//primitives/Hash'
6
7
 
7
8
  describe('Hash', function () {
8
9
  function test (Hash, cases): void {
@@ -177,4 +178,27 @@ describe('Hash', function () {
177
178
  })
178
179
  }
179
180
  })
181
+
182
+ describe('Hash strict length validation (TOB-21)', () => {
183
+
184
+ it('throws when pendingTotal is not a safe integer', () => {
185
+ const h = new SHA1()
186
+
187
+ h.pendingTotal = Number.MAX_SAFE_INTEGER + 10
188
+
189
+ expect(() => {
190
+ h.digest()
191
+ }).toThrow('Message too long for this hash function')
192
+ })
193
+
194
+ it('throws when pendingTotal is negative', () => {
195
+ const h = new SHA1()
196
+
197
+ h.pendingTotal = -5
198
+
199
+ expect(() => {
200
+ h.digest()
201
+ }).toThrow('Message too long for this hash function')
202
+ })
203
+ })
180
204
  })
@@ -0,0 +1,57 @@
1
+ /* eslint-env jest */
2
+
3
+ import { assertValidHex, normalizeHex } from '../../primitives/hex'
4
+
5
+ describe('hex utils', () => {
6
+ describe('assertValidHex', () => {
7
+ it('should not throw on valid hex strings', () => {
8
+ expect(() => assertValidHex('')).not.toThrow() // empty is allowed
9
+ expect(() => assertValidHex('00')).not.toThrow()
10
+ expect(() => assertValidHex('abcdef')).not.toThrow()
11
+ expect(() => assertValidHex('ABCDEF')).not.toThrow()
12
+ expect(() => assertValidHex('1234567890')).not.toThrow()
13
+ })
14
+
15
+ it('should throw on non-hex characters', () => {
16
+ expect(() => assertValidHex('zz')).toThrow('Invalid hex string')
17
+ expect(() => assertValidHex('0x1234')).toThrow('Invalid hex string')
18
+ expect(() => assertValidHex('12 34')).toThrow('Invalid hex string')
19
+ expect(() => assertValidHex('g1')).toThrow('Invalid hex string')
20
+ })
21
+
22
+ // ❌ old behavior: empty string was considered invalid
23
+ // it('should throw on empty string', () => {
24
+ // expect(() => assertValidHex('')).toThrow('Invalid hex string')
25
+ // })
26
+
27
+ it('should throw on undefined or null', () => {
28
+ expect(() => assertValidHex(undefined as any)).toThrow('Invalid hex string')
29
+ expect(() => assertValidHex(null as any)).toThrow('Invalid hex string')
30
+ })
31
+ })
32
+
33
+ describe('normalizeHex', () => {
34
+ it('should return lowercase hex', () => {
35
+ expect(normalizeHex('ABCD')).toBe('abcd')
36
+ })
37
+
38
+ it('should prepend 0 to odd-length hex strings', () => {
39
+ expect(normalizeHex('abc')).toBe('0abc')
40
+ expect(normalizeHex('f')).toBe('0f')
41
+ })
42
+
43
+ it('should leave even-length hex strings untouched (except lowercase)', () => {
44
+ expect(normalizeHex('AABB')).toBe('aabb')
45
+ expect(normalizeHex('001122')).toBe('001122')
46
+ })
47
+
48
+ it('should return empty string unchanged', () => {
49
+ expect(normalizeHex('')).toBe('')
50
+ })
51
+
52
+ it('should throw on invalid hex', () => {
53
+ expect(() => normalizeHex('xyz')).toThrow('Invalid hex string')
54
+ expect(() => normalizeHex('12 34')).toThrow('Invalid hex string')
55
+ })
56
+ })
57
+ })
@@ -320,3 +320,42 @@ describe('verifyNotNull', () => {
320
320
  expect(() => verifyNotNull(undefined, 'Another custom error')).toThrow('Another custom error')
321
321
  })
322
322
  })
323
+
324
+ describe('toUTF8 strict UTF-8 decoding (TOB-21)', () => {
325
+
326
+ it('replaces invalid 2-byte sequences with U+FFFD', () => {
327
+ // 0xC2 should expect a continuation byte 0x80–0xBF
328
+ const arr = [0xC2, 0x20] // 0x20 is INVALID continuation
329
+ const str = toUTF8(arr)
330
+ expect(str).toBe('\uFFFD')
331
+ })
332
+
333
+ it('decodes valid 3-byte sequences', () => {
334
+ const euro = [0xE2, 0x82, 0xAC]
335
+ expect(toUTF8(euro)).toBe('€')
336
+ })
337
+
338
+ it('replaces invalid 3-byte sequences', () => {
339
+ // Middle byte invalid
340
+ const arr = [0xE2, 0x20, 0xAC]
341
+ expect(toUTF8(arr)).toBe('\uFFFD')
342
+ })
343
+
344
+ it('decodes valid 4-byte sequences into surrogate pairs', () => {
345
+ const smile = [0xF0, 0x9F, 0x98, 0x80] // 😀
346
+ expect(toUTF8(smile)).toBe('😀')
347
+ })
348
+
349
+ it('replaces invalid 4-byte sequences with U+FFFD', () => {
350
+ // 0x9F is valid, 0x20 is INVALID continuation for byte 3
351
+ const arr = [0xF0, 0x9F, 0x20, 0x80]
352
+ expect(toUTF8(arr)).toBe('\uFFFD')
353
+ })
354
+
355
+ it('replaces incomplete UTF-8 sequence at end', () => {
356
+ const arr = [0xE2] // incomplete 3-byte seq
357
+ expect(toUTF8(arr)).toBe('\uFFFD')
358
+ })
359
+
360
+ })
361
+
@@ -0,0 +1,35 @@
1
+ // src/primitives/hex.ts
2
+
3
+ // Accepts empty string because empty byte arrays are valid in Bitcoin.
4
+ const PURE_HEX_REGEX = /^[0-9a-fA-F]*$/
5
+
6
+ export function assertValidHex (msg: string): void {
7
+ if (typeof msg !== 'string') {
8
+ console.error('assertValidHex FAIL (non-string):', msg)
9
+ throw new Error('Invalid hex string')
10
+ }
11
+
12
+ // allow empty
13
+ if (msg.length === 0) return
14
+
15
+ if (!PURE_HEX_REGEX.test(msg)) {
16
+ console.error('assertValidHex FAIL (bad hex):', msg)
17
+ throw new Error('Invalid hex string')
18
+ }
19
+ }
20
+
21
+ export function normalizeHex (msg: string): string {
22
+ assertValidHex(msg)
23
+
24
+ // If empty, return empty — never force to "00"
25
+ if (msg.length === 0) return ''
26
+
27
+ let normalized = msg.toLowerCase()
28
+
29
+ // Pad odd-length hex
30
+ if (normalized.length % 2 !== 0) {
31
+ normalized = '0' + normalized
32
+ }
33
+
34
+ return normalized
35
+ }
@@ -5,7 +5,6 @@ export { default as PublicKey } from './PublicKey.js'
5
5
  export { default as Signature } from './Signature.js'
6
6
  export { default as PrivateKey, KeyShares } from './PrivateKey.js'
7
7
  export { default as SymmetricKey } from './SymmetricKey.js'
8
- export { default as DRBG } from './DRBG.js'
9
8
  export * as ECDSA from './ECDSA.js'
10
9
  export * as Utils from './utils.js'
11
10
  export * as Hash from './Hash.js'