@bsv/sdk 1.9.23 → 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 (67) 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/Hash.js +70 -5
  5. package/dist/cjs/src/primitives/Hash.js.map +1 -1
  6. package/dist/cjs/src/primitives/Point.js +41 -18
  7. package/dist/cjs/src/primitives/Point.js.map +1 -1
  8. package/dist/cjs/src/primitives/SymmetricKey.js +20 -19
  9. package/dist/cjs/src/primitives/SymmetricKey.js.map +1 -1
  10. package/dist/cjs/src/primitives/hex.js +1 -3
  11. package/dist/cjs/src/primitives/hex.js.map +1 -1
  12. package/dist/cjs/src/primitives/utils.js +10 -0
  13. package/dist/cjs/src/primitives/utils.js.map +1 -1
  14. package/dist/cjs/src/totp/totp.js +3 -1
  15. package/dist/cjs/src/totp/totp.js.map +1 -1
  16. package/dist/cjs/src/wallet/ProtoWallet.js +4 -2
  17. package/dist/cjs/src/wallet/ProtoWallet.js.map +1 -1
  18. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  19. package/dist/esm/src/primitives/AESGCM.js +158 -75
  20. package/dist/esm/src/primitives/AESGCM.js.map +1 -1
  21. package/dist/esm/src/primitives/Hash.js +68 -6
  22. package/dist/esm/src/primitives/Hash.js.map +1 -1
  23. package/dist/esm/src/primitives/Point.js +41 -18
  24. package/dist/esm/src/primitives/Point.js.map +1 -1
  25. package/dist/esm/src/primitives/SymmetricKey.js +20 -19
  26. package/dist/esm/src/primitives/SymmetricKey.js.map +1 -1
  27. package/dist/esm/src/primitives/hex.js +1 -3
  28. package/dist/esm/src/primitives/hex.js.map +1 -1
  29. package/dist/esm/src/primitives/utils.js +9 -0
  30. package/dist/esm/src/primitives/utils.js.map +1 -1
  31. package/dist/esm/src/totp/totp.js +3 -1
  32. package/dist/esm/src/totp/totp.js.map +1 -1
  33. package/dist/esm/src/wallet/ProtoWallet.js +4 -2
  34. package/dist/esm/src/wallet/ProtoWallet.js.map +1 -1
  35. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  36. package/dist/types/src/primitives/AESGCM.d.ts +59 -9
  37. package/dist/types/src/primitives/AESGCM.d.ts.map +1 -1
  38. package/dist/types/src/primitives/Hash.d.ts +47 -0
  39. package/dist/types/src/primitives/Hash.d.ts.map +1 -1
  40. package/dist/types/src/primitives/Point.d.ts +1 -0
  41. package/dist/types/src/primitives/Point.d.ts.map +1 -1
  42. package/dist/types/src/primitives/SymmetricKey.d.ts.map +1 -1
  43. package/dist/types/src/primitives/hex.d.ts.map +1 -1
  44. package/dist/types/src/primitives/utils.d.ts +1 -0
  45. package/dist/types/src/primitives/utils.d.ts.map +1 -1
  46. package/dist/types/src/totp/totp.d.ts.map +1 -1
  47. package/dist/types/src/wallet/ProtoWallet.d.ts.map +1 -1
  48. package/dist/types/tsconfig.types.tsbuildinfo +1 -1
  49. package/dist/umd/bundle.js +3 -3
  50. package/dist/umd/bundle.js.map +1 -1
  51. package/docs/reference/primitives.md +206 -60
  52. package/package.json +1 -1
  53. package/src/primitives/AESGCM.ts +225 -103
  54. package/src/primitives/Hash.ts +72 -7
  55. package/src/primitives/Point.ts +67 -20
  56. package/src/primitives/SymmetricKey.ts +28 -20
  57. package/src/primitives/__tests/AESGCM.test.ts +254 -354
  58. package/src/primitives/__tests/ECDSA.test.ts +27 -0
  59. package/src/primitives/__tests/Hash.test.ts +62 -10
  60. package/src/primitives/__tests/Point.test.ts +52 -0
  61. package/src/primitives/__tests/utils.test.ts +24 -1
  62. package/src/primitives/hex.ts +1 -3
  63. package/src/primitives/utils.ts +10 -0
  64. package/src/totp/__tests/totp.test.ts +21 -0
  65. package/src/totp/totp.ts +9 -1
  66. package/src/wallet/ProtoWallet.ts +8 -3
  67. 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
  })
@@ -105,9 +105,9 @@ describe('Hash', function () {
105
105
  describe('BaseHash padding and endianness', () => {
106
106
  it('encodes length in big-endian for SHA1', () => {
107
107
  const sha1 = new (hash as any).SHA1()
108
- ;(sha1 as any).pendingTotal = 12345
109
- const pad = (sha1 as any)._pad() as number[]
110
- const padLength = (sha1 as any).padLength as number
108
+ ;(sha1).pendingTotal = 12345
109
+ const pad = (sha1)._pad() as number[]
110
+ const padLength = (sha1).padLength as number
111
111
  const lengthBytes = pad.slice(-padLength)
112
112
 
113
113
  const totalBits = BigInt(12345) * 8n
@@ -123,9 +123,9 @@ describe('Hash', function () {
123
123
 
124
124
  it('encodes length in little-endian for RIPEMD160', () => {
125
125
  const ripemd = new (hash as any).RIPEMD160()
126
- ;(ripemd as any).pendingTotal = 12345
127
- const pad = (ripemd as any)._pad() as number[]
128
- const padLength = (ripemd as any).padLength as number
126
+ ;(ripemd).pendingTotal = 12345
127
+ const pad = (ripemd)._pad() as number[]
128
+ const padLength = (ripemd).padLength as number
129
129
  const lengthBytes = pad.slice(-padLength)
130
130
 
131
131
  const totalBits = BigInt(12345) * 8n
@@ -141,11 +141,11 @@ describe('Hash', function () {
141
141
 
142
142
  it('throws when message length exceeds maximum encodable bits', () => {
143
143
  const sha1 = new (hash as any).SHA1()
144
- ;(sha1 as any).padLength = 1
145
- ;(sha1 as any).pendingTotal = 40
144
+ ;(sha1).padLength = 1
145
+ ;(sha1).pendingTotal = 40
146
146
 
147
147
  expect(() => {
148
- ;(sha1 as any)._pad()
148
+ ;(sha1)._pad()
149
149
  }).toThrow(new Error('Message too long for this hash function'))
150
150
  })
151
151
  })
@@ -180,7 +180,6 @@ describe('Hash', function () {
180
180
  })
181
181
 
182
182
  describe('Hash strict length validation (TOB-21)', () => {
183
-
184
183
  it('throws when pendingTotal is not a safe integer', () => {
185
184
  const h = new SHA1()
186
185
 
@@ -201,4 +200,57 @@ describe('Hash', function () {
201
200
  }).toThrow('Message too long for this hash function')
202
201
  })
203
202
  })
203
+
204
+ describe('TOB-20 byte-order helper functions', () => {
205
+ const { htonl, swapBytes32, realHtonl } = hash
206
+
207
+ it('swapBytes32 performs a strict 32-bit byte swap', () => {
208
+ expect(swapBytes32(0x11223344)).toBe(0x44332211)
209
+ expect(swapBytes32(0xaabbccdd)).toBe(0xddccbbaa)
210
+ expect(swapBytes32(0x00000000)).toBe(0x00000000)
211
+ expect(swapBytes32(0xffffffff)).toBe(0xffffffff)
212
+ })
213
+
214
+ it('swapBytes32 always returns an unsigned 32-bit integer', () => {
215
+ expect(swapBytes32(-1)).toBe(0xffffffff) // wraps to unsigned
216
+ expect(swapBytes32(0x80000000)).toBe(0x00000080) // MSB becomes LSB
217
+ })
218
+
219
+ it('htonl is now an alias for swapBytes32 (deprecated)', () => {
220
+ expect(htonl(0x11223344)).toBe(swapBytes32(0x11223344))
221
+ expect(htonl(0xaabbccdd)).toBe(swapBytes32(0xaabbccdd))
222
+ })
223
+
224
+ it('realHtonl matches swapBytes32 on little-endian systems', () => {
225
+ // All JS engines used for Node/Jest are little-endian
226
+ expect(realHtonl(0x11223344)).toBe(0x44332211)
227
+ expect(realHtonl(0xaabbccdd)).toBe(0xddccbbaa)
228
+ })
229
+
230
+ it('realHtonl preserves value when system is big-endian (forced simulation)', () => {
231
+ // We simulate the big-endian branch of realHtonl by calling
232
+ // the fallback path directly.
233
+ const forceBigEndianRealHtonl = (w: number) => (w >>> 0)
234
+
235
+ expect(forceBigEndianRealHtonl(0x11223344)).toBe(0x11223344)
236
+ expect(forceBigEndianRealHtonl(0xaabbccdd)).toBe(0xaabbccdd)
237
+ expect(forceBigEndianRealHtonl(0xffffffff)).toBe(0xffffffff >>> 0)
238
+ })
239
+
240
+ it('htonl, swapBytes32, realHtonl never throw for any 32-bit input', () => {
241
+ const inputs = [
242
+ 0, 1, -1,
243
+ 0x7fffffff,
244
+ 0x80000000,
245
+ 0xffffffff,
246
+ 0x12345678
247
+ ]
248
+
249
+ for (const n of inputs) {
250
+ expect(() => htonl(n)).not.toThrow()
251
+ expect(() => swapBytes32(n)).not.toThrow()
252
+ expect(() => realHtonl(n)).not.toThrow()
253
+ }
254
+ })
255
+ })
204
256
  })
@@ -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
  })