@bsv/sdk 1.9.24 → 1.9.29

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 (59) hide show
  1. package/dist/cjs/package.json +1 -1
  2. package/dist/cjs/src/primitives/AESGCM.js +160 -76
  3. package/dist/cjs/src/primitives/AESGCM.js.map +1 -1
  4. package/dist/cjs/src/primitives/Point.js +41 -18
  5. package/dist/cjs/src/primitives/Point.js.map +1 -1
  6. package/dist/cjs/src/primitives/SymmetricKey.js +20 -19
  7. package/dist/cjs/src/primitives/SymmetricKey.js.map +1 -1
  8. package/dist/cjs/src/primitives/hex.js +1 -3
  9. package/dist/cjs/src/primitives/hex.js.map +1 -1
  10. package/dist/cjs/src/primitives/utils.js +10 -0
  11. package/dist/cjs/src/primitives/utils.js.map +1 -1
  12. package/dist/cjs/src/totp/totp.js +3 -1
  13. package/dist/cjs/src/totp/totp.js.map +1 -1
  14. package/dist/cjs/src/wallet/ProtoWallet.js +4 -2
  15. package/dist/cjs/src/wallet/ProtoWallet.js.map +1 -1
  16. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  17. package/dist/esm/src/primitives/AESGCM.js +158 -75
  18. package/dist/esm/src/primitives/AESGCM.js.map +1 -1
  19. package/dist/esm/src/primitives/Point.js +41 -18
  20. package/dist/esm/src/primitives/Point.js.map +1 -1
  21. package/dist/esm/src/primitives/SymmetricKey.js +20 -19
  22. package/dist/esm/src/primitives/SymmetricKey.js.map +1 -1
  23. package/dist/esm/src/primitives/hex.js +1 -3
  24. package/dist/esm/src/primitives/hex.js.map +1 -1
  25. package/dist/esm/src/primitives/utils.js +9 -0
  26. package/dist/esm/src/primitives/utils.js.map +1 -1
  27. package/dist/esm/src/totp/totp.js +3 -1
  28. package/dist/esm/src/totp/totp.js.map +1 -1
  29. package/dist/esm/src/wallet/ProtoWallet.js +4 -2
  30. package/dist/esm/src/wallet/ProtoWallet.js.map +1 -1
  31. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  32. package/dist/types/src/primitives/AESGCM.d.ts +59 -9
  33. package/dist/types/src/primitives/AESGCM.d.ts.map +1 -1
  34. package/dist/types/src/primitives/Point.d.ts +1 -0
  35. package/dist/types/src/primitives/Point.d.ts.map +1 -1
  36. package/dist/types/src/primitives/SymmetricKey.d.ts.map +1 -1
  37. package/dist/types/src/primitives/hex.d.ts.map +1 -1
  38. package/dist/types/src/primitives/utils.d.ts +1 -0
  39. package/dist/types/src/primitives/utils.d.ts.map +1 -1
  40. package/dist/types/src/totp/totp.d.ts.map +1 -1
  41. package/dist/types/src/wallet/ProtoWallet.d.ts.map +1 -1
  42. package/dist/types/tsconfig.types.tsbuildinfo +1 -1
  43. package/dist/umd/bundle.js +3 -3
  44. package/dist/umd/bundle.js.map +1 -1
  45. package/docs/reference/primitives.md +206 -60
  46. package/package.json +1 -1
  47. package/src/primitives/AESGCM.ts +225 -103
  48. package/src/primitives/Point.ts +67 -20
  49. package/src/primitives/SymmetricKey.ts +28 -20
  50. package/src/primitives/__tests/AESGCM.test.ts +254 -354
  51. package/src/primitives/__tests/ECDSA.test.ts +27 -0
  52. package/src/primitives/__tests/Point.test.ts +52 -0
  53. package/src/primitives/__tests/utils.test.ts +24 -1
  54. package/src/primitives/hex.ts +1 -3
  55. package/src/primitives/utils.ts +10 -0
  56. package/src/totp/__tests/totp.test.ts +21 -0
  57. package/src/totp/totp.ts +9 -1
  58. package/src/wallet/ProtoWallet.ts +8 -3
  59. package/src/wallet/__tests/ProtoWallet.test.ts +55 -34
@@ -2,6 +2,7 @@ import * as ECDSA from '../../primitives/ECDSA'
2
2
  import BigNumber from '../../primitives/BigNumber'
3
3
  import Curve from '../../primitives/Curve'
4
4
  import Signature from '../../primitives/Signature'
5
+ import Point from '../../primitives/Point'
5
6
 
6
7
  const msg = new BigNumber('deadbeef', 16)
7
8
  const key = new BigNumber(
@@ -90,4 +91,30 @@ describe('ECDSA', () => {
90
91
  ECDSA.sign(msg, key, undefined, n)
91
92
  ).toThrow()
92
93
  })
94
+
95
+ it('k·G + (−k·G) results in point at infinity (TOB-25)', () => {
96
+ const k = new BigNumber('123456789abcdef', 16)
97
+
98
+ const P = curve.g.mul(k)
99
+ const negP = P.neg()
100
+ const sum = P.add(negP)
101
+
102
+ expect(sum.isInfinity()).toBe(true)
103
+ })
104
+
105
+ it('scalar multiplication by zero returns point at infinity (TOB-25)', () => {
106
+ const zero = new BigNumber(0)
107
+ const result = curve.g.mul(zero)
108
+
109
+ expect(result.isInfinity()).toBe(true)
110
+ })
111
+
112
+ it('ECDSA verify rejects point-at-infinity public key (TOB-25)', () => {
113
+ const signature = ECDSA.sign(msg, key)
114
+ const infinityPub = new Point(null, null)
115
+
116
+ expect(() =>
117
+ ECDSA.verify(msg, signature, infinityPub)
118
+ ).toThrow()
119
+ })
93
120
  })
@@ -0,0 +1,52 @@
1
+ import Point from '../../primitives/Point'
2
+
3
+ describe('Point.fromJSON / fromDER / fromX curve validation (TOB-24)', () => {
4
+ it('rejects clearly off-curve coordinates', () => {
5
+ expect(() =>
6
+ Point.fromJSON([123, 456], true)
7
+ ).toThrow(/Invalid point/)
8
+ })
9
+
10
+ it('rejects nested off-curve precomputed points', () => {
11
+ const bad = [
12
+ 123,
13
+ 456,
14
+ {
15
+ doubles: {
16
+ step: 2,
17
+ points: [
18
+ [1, 2],
19
+ [3, 4]
20
+ ]
21
+ }
22
+ }
23
+ ]
24
+ expect(() => Point.fromJSON(bad, true)).toThrow(/Invalid point/)
25
+ })
26
+
27
+ it('accepts valid generator point from toJSON → fromJSON roundtrip', () => {
28
+ // Compressed secp256k1 G:
29
+ const G_COMPRESSED =
30
+ '0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'
31
+
32
+ const g = Point.fromString(G_COMPRESSED)
33
+ const serialized = g.toJSON()
34
+ const restored = Point.fromJSON(serialized as any, true)
35
+
36
+ expect(restored.eq(g)).toBe(true)
37
+ })
38
+
39
+ it('rejects invalid compressed points in fromDER', () => {
40
+ // 0x02 is a valid compressed prefix, but x = 0 gives y^2 = 7,
41
+ // which has no square root mod p on secp256k1 → invalid point.
42
+ const der = [0x02, ...Array(32).fill(0x00)]
43
+ expect(() => Point.fromDER(der)).toThrow(/Invalid point/)
44
+ })
45
+
46
+ it('fromX rejects values with no square root mod p', () => {
47
+ // x = 0 ⇒ y^2 = 7, which has no square root mod p on secp256k1.
48
+ // This guarantees that fromX must reject it.
49
+ const badX = '0000000000000000000000000000000000000000000000000000000000000000'
50
+ expect(() => Point.fromX(badX, true)).toThrow(/Invalid point/)
51
+ })
52
+ })
@@ -9,7 +9,8 @@ import {
9
9
  toBase58,
10
10
  fromBase58Check,
11
11
  toBase58Check,
12
- verifyNotNull
12
+ verifyNotNull,
13
+ constantTimeEquals
13
14
  } from '../../primitives/utils'
14
15
  import Point from '../../primitives/Point'
15
16
 
@@ -376,3 +377,25 @@ describe('Point.encode infinity handling', () => {
376
377
  expect(() => p.encode()).not.toThrow()
377
378
  })
378
379
  })
380
+
381
+ describe('constantTimeEquals', () => {
382
+ it('returns true for identical arrays', () => {
383
+ expect(constantTimeEquals([1, 2, 3], [1, 2, 3])).toBe(true)
384
+ })
385
+
386
+ it('returns false for arrays with different content', () => {
387
+ expect(constantTimeEquals([1, 2, 3], [1, 2, 4])).toBe(false)
388
+ })
389
+
390
+ it('returns false for arrays of different length', () => {
391
+ expect(constantTimeEquals([1, 2], [1, 2, 3])).toBe(false)
392
+ })
393
+
394
+ it('runs through entire array (no early exit)', () => {
395
+ expect(constantTimeEquals([0,0,0,0,9], [0,0,0,0,8])).toBe(false)
396
+ })
397
+
398
+ it('works with Uint8Array', () => {
399
+ expect(constantTimeEquals(new Uint8Array([5,6,7]), new Uint8Array([5,6,7]))).toBe(true)
400
+ })
401
+ })
@@ -5,15 +5,13 @@ const PURE_HEX_REGEX = /^[0-9a-fA-F]*$/
5
5
 
6
6
  export function assertValidHex (msg: string): void {
7
7
  if (typeof msg !== 'string') {
8
- console.error('assertValidHex FAIL (non-string):', msg)
9
- throw new Error('Invalid hex string')
8
+ throw new TypeError('Invalid hex string')
10
9
  }
11
10
 
12
11
  // allow empty
13
12
  if (msg.length === 0) return
14
13
 
15
14
  if (!PURE_HEX_REGEX.test(msg)) {
16
- console.error('assertValidHex FAIL (bad hex):', msg)
17
15
  throw new Error('Invalid hex string')
18
16
  }
19
17
  }
@@ -951,3 +951,13 @@ export function verifyNotNull<T> (value: T | undefined | null, errorMessage: str
951
951
  if (value == null) throw new Error(errorMessage)
952
952
  return value
953
953
  }
954
+
955
+ export function constantTimeEquals (a: Uint8Array | number[], b: Uint8Array | number[]): boolean {
956
+ if (a.length !== b.length) return false
957
+
958
+ let diff = 0
959
+ for (let i = 0; i < a.length; i++) {
960
+ diff |= a[i] ^ b[i]
961
+ }
962
+ return diff === 0
963
+ }
@@ -78,4 +78,25 @@ describe('totp generation and validation', () => {
78
78
  checkAdjacentWindow(time - i * periodMS, false)
79
79
  }
80
80
  })
81
+
82
+ test('should reject wrong passcode with same length', () => {
83
+ jest.setSystemTime(0)
84
+
85
+ const correct = TOTP.generate(secret, options)
86
+
87
+ // Same length but definitely wrong
88
+ const wrong = correct === '123456' ? '654321' : '123456'
89
+
90
+ expect(wrong.length).toBe(correct.length)
91
+ expect(TOTP.validate(secret, wrong, options)).toBe(false)
92
+ })
93
+
94
+ test('should validate correct passcode using constant-time comparison', () => {
95
+ jest.setSystemTime(0)
96
+
97
+ const correct = TOTP.generate(secret, options)
98
+
99
+ // Ensure the code path executes constantTimeEquals and returns true
100
+ expect(TOTP.validate(secret, correct, options)).toBe(true)
101
+ })
81
102
  })
package/src/totp/totp.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { SHA1HMAC, SHA256HMAC, SHA512HMAC } from '../primitives/Hash.js'
2
2
  import BigNumber from '../primitives/BigNumber.js'
3
+ import { constantTimeEquals, toArray } from '../primitives/utils.js'
3
4
 
4
5
  export type TOTPAlgorithm = 'SHA-1' | 'SHA-256' | 'SHA-512'
5
6
 
@@ -68,7 +69,14 @@ export class TOTP {
68
69
  }
69
70
 
70
71
  for (const c of counters) {
71
- if (passcode === generateHOTP(secret, c, _options)) {
72
+ const expected = generateHOTP(secret, c, _options)
73
+
74
+ if (
75
+ constantTimeEquals(
76
+ toArray(passcode, 'utf8'),
77
+ toArray(expected, 'utf8')
78
+ )
79
+ ) {
72
80
  return true
73
81
  }
74
82
  }
@@ -30,6 +30,7 @@ import {
30
30
  WalletEncryptArgs,
31
31
  WalletEncryptResult
32
32
  } from './Wallet.interfaces.js'
33
+ import { constantTimeEquals, toArray } from '../primitives/utils.js'
33
34
 
34
35
  /**
35
36
  * A ProtoWallet is precursor to a full wallet, capable of performing all foundational cryptographic operations.
@@ -222,9 +223,13 @@ export class ProtoWallet {
222
223
  args.keyID,
223
224
  args.counterparty ?? 'self'
224
225
  )
225
- const valid =
226
- Hash.sha256hmac(key.toArray(), args.data).toString() ===
227
- args.hmac.toString()
226
+ const computed = Hash.sha256hmac(key.toArray(), args.data)
227
+ const provided = args.hmac
228
+
229
+ const valid = constantTimeEquals(
230
+ toArray(computed),
231
+ toArray(provided)
232
+ )
228
233
  if (!valid) {
229
234
  const e = new Error('HMAC is not valid') as Error & { code: string }
230
235
  e.code = 'ERR_INVALID_HMAC'
@@ -4,6 +4,19 @@ import { createNonce, verifyNonce } from '../../auth/utils'
4
4
 
5
5
  const sampleData = [3, 1, 4, 1, 5, 9]
6
6
 
7
+ let userKey: PrivateKey
8
+ let counterpartyKey: PrivateKey
9
+ let user: ProtoWallet
10
+ let counterparty: ProtoWallet
11
+
12
+ beforeEach(() => {
13
+ userKey = PrivateKey.fromRandom()
14
+ counterpartyKey = PrivateKey.fromRandom()
15
+ user = new ProtoWallet(userKey)
16
+ counterparty = new ProtoWallet(counterpartyKey)
17
+ })
18
+
19
+
7
20
  describe('ProtoWallet', () => {
8
21
  it('Throws when unsupported functions are called', async () => {
9
22
  const wallet = new ProtoWallet('anyone')
@@ -80,10 +93,6 @@ describe('ProtoWallet', () => {
80
93
  )
81
94
  })
82
95
  it('Encrypts messages decryptable by the counterparty', async () => {
83
- const userKey = PrivateKey.fromRandom()
84
- const counterpartyKey = PrivateKey.fromRandom()
85
- const user = new ProtoWallet(userKey)
86
- const counterparty = new ProtoWallet(counterpartyKey)
87
96
  const { ciphertext } = await user.encrypt({
88
97
  plaintext: sampleData,
89
98
  protocolID: [2, 'tests'],
@@ -100,10 +109,6 @@ describe('ProtoWallet', () => {
100
109
  expect(ciphertext).not.toEqual(plaintext)
101
110
  })
102
111
  it('Fails to decryupt messages for the wrong protocol, key, and counterparty', async () => {
103
- const userKey = PrivateKey.fromRandom()
104
- const counterpartyKey = PrivateKey.fromRandom()
105
- const user = new ProtoWallet(userKey)
106
- const counterparty = new ProtoWallet(counterpartyKey)
107
112
  const { ciphertext } = await user.encrypt({
108
113
  plaintext: sampleData,
109
114
  protocolID: [2, 'tests'],
@@ -139,10 +144,6 @@ describe('ProtoWallet', () => {
139
144
  ).rejects.toThrow()
140
145
  })
141
146
  it('Correctly derives keys for a counterparty', async () => {
142
- const userKey = PrivateKey.fromRandom()
143
- const counterpartyKey = PrivateKey.fromRandom()
144
- const user = new ProtoWallet(userKey)
145
- const counterparty = new ProtoWallet(counterpartyKey)
146
147
  const { publicKey: identityKey } = await user.getPublicKey({
147
148
  identityKey: true
148
149
  })
@@ -162,10 +163,6 @@ describe('ProtoWallet', () => {
162
163
  expect(derivedForCounterparty).toEqual(derivedByCounterparty)
163
164
  })
164
165
  it('Signs messages verifiable by the counterparty', async () => {
165
- const userKey = PrivateKey.fromRandom()
166
- const counterpartyKey = PrivateKey.fromRandom()
167
- const user = new ProtoWallet(userKey)
168
- const counterparty = new ProtoWallet(counterpartyKey)
169
166
  const { signature } = await user.createSignature({
170
167
  data: sampleData,
171
168
  protocolID: [2, 'tests'],
@@ -183,10 +180,6 @@ describe('ProtoWallet', () => {
183
180
  expect(signature.length).not.toEqual(0)
184
181
  })
185
182
  it('Directly signs hash of message verifiable by the counterparty', async () => {
186
- const userKey = PrivateKey.fromRandom()
187
- const counterpartyKey = PrivateKey.fromRandom()
188
- const user = new ProtoWallet(userKey)
189
- const counterparty = new ProtoWallet(counterpartyKey)
190
183
  const { signature } = await user.createSignature({
191
184
  hashToDirectlySign: Hash.sha256(sampleData),
192
185
  protocolID: [2, 'tests'],
@@ -212,10 +205,6 @@ describe('ProtoWallet', () => {
212
205
  expect(signature.length).not.toEqual(0)
213
206
  })
214
207
  it('Fails to verify signature for the wrong data, protocol, key, and counterparty', async () => {
215
- const userKey = PrivateKey.fromRandom()
216
- const counterpartyKey = PrivateKey.fromRandom()
217
- const user = new ProtoWallet(userKey)
218
- const counterparty = new ProtoWallet(counterpartyKey)
219
208
  const { signature } = await user.createSignature({
220
209
  data: sampleData,
221
210
  protocolID: [2, 'tests'],
@@ -264,10 +253,6 @@ describe('ProtoWallet', () => {
264
253
  ).rejects.toThrow()
265
254
  })
266
255
  it('Computes HMAC over messages verifiable by the counterparty', async () => {
267
- const userKey = PrivateKey.fromRandom()
268
- const counterpartyKey = PrivateKey.fromRandom()
269
- const user = new ProtoWallet(userKey)
270
- const counterparty = new ProtoWallet(counterpartyKey)
271
256
  const { hmac } = await user.createHmac({
272
257
  data: sampleData,
273
258
  protocolID: [2, 'tests'],
@@ -285,10 +270,6 @@ describe('ProtoWallet', () => {
285
270
  expect(hmac.length).toEqual(32)
286
271
  })
287
272
  it('Fails to verify HMAC for the wrong data, protocol, key, and counterparty', async () => {
288
- const userKey = PrivateKey.fromRandom()
289
- const counterpartyKey = PrivateKey.fromRandom()
290
- const user = new ProtoWallet(userKey)
291
- const counterparty = new ProtoWallet(counterpartyKey)
292
273
  const { hmac } = await user.createHmac({
293
274
  data: sampleData,
294
275
  protocolID: [2, 'tests'],
@@ -337,8 +318,6 @@ describe('ProtoWallet', () => {
337
318
  ).rejects.toThrow()
338
319
  })
339
320
  it('Uses anyone for creating signatures and self for other operations if no counterparty is provided', async () => {
340
- const userKey = PrivateKey.fromRandom()
341
- const user = new ProtoWallet(userKey)
342
321
  const { hmac } = await user.createHmac({
343
322
  data: sampleData,
344
323
  protocolID: [2, 'tests'],
@@ -589,4 +568,46 @@ describe('ProtoWallet', () => {
589
568
  expect(linkage).toEqual(expectedLinkage)
590
569
  })
591
570
  })
571
+
572
+ it('Fails constant-time HMAC validation for wrong-but-same-length HMAC', async () => {
573
+ const { hmac: correctHmac } = await user.createHmac({
574
+ data: sampleData,
575
+ protocolID: [2, 'tests'],
576
+ keyID: '4',
577
+ counterparty: counterpartyKey.toPublicKey().toString()
578
+ })
579
+
580
+ // Create a different HMAC with same length
581
+ const wrong = correctHmac.slice()
582
+ wrong[0] = (wrong[0] + 1) & 0xff // minimally alter 1 byte
583
+
584
+ await expect(async () =>
585
+ await counterparty.verifyHmac({
586
+ hmac: wrong,
587
+ data: sampleData,
588
+ protocolID: [2, 'tests'],
589
+ keyID: '4',
590
+ counterparty: userKey.toPublicKey().toString()
591
+ })
592
+ ).rejects.toThrow('HMAC is not valid')
593
+ })
594
+
595
+ it('Validates correct HMAC using the constant-time comparison path', async () => {
596
+ const { hmac } = await user.createHmac({
597
+ data: sampleData,
598
+ protocolID: [2, 'tests'],
599
+ keyID: '4',
600
+ counterparty: counterpartyKey.toPublicKey().toString()
601
+ })
602
+
603
+ const { valid } = await counterparty.verifyHmac({
604
+ hmac,
605
+ data: sampleData,
606
+ protocolID: [2, 'tests'],
607
+ keyID: '4',
608
+ counterparty: userKey.toPublicKey().toString()
609
+ })
610
+
611
+ expect(valid).toBe(true)
612
+ })
592
613
  })