@smartledger/bsv 4.6.0 → 5.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.
@@ -3,119 +3,261 @@
3
3
  var BN = require('./bn')
4
4
  var Random = require('./random')
5
5
  var Hash = require('./hash')
6
- var _ = require('../util/_')
7
6
 
8
- /**
9
- * Shamir Secret Sharing implementation for secure secret distribution
10
- * Based on Shamir's Secret Sharing algorithm using finite field arithmetic
11
- *
12
- * Features:
13
- * - Split secrets into N shares with K threshold
14
- * - Reconstruct secret from any K shares
15
- * - Cryptographically secure random polynomial generation
16
- * - Support for arbitrary secret sizes via byte-level processing
17
- * - Compatible with BSV cryptographic primitives
18
- */
7
+ // secrets.js-grempe is loaded lazily and wired to use the library's own vetted
8
+ // Random (Node CSPRNG / window.crypto), so it never depends on the bundler's
9
+ // `crypto` resolution for randomness. Lazy loading also means simply requiring
10
+ // this module (e.g. loading the full bsv bundle) never runs secrets.js's
11
+ // auto-init, which would otherwise throw in a bundle that mocks node `crypto`.
12
+ var _secrets = null
13
+ function secretsLib () {
14
+ if (_secrets) return _secrets
15
+ var s = require('secrets.js-grempe')
16
+ s.setRNG(function (bits) {
17
+ var bytes = Random.getRandomBuffer(Math.ceil(bits / 8))
18
+ var str = ''
19
+ for (var i = 0; i < bytes.length; i++) {
20
+ str += ('00000000' + bytes[i].toString(2)).slice(-8)
21
+ }
22
+ str = str.slice(0, bits)
23
+ // secrets.js rejects an all-zero draw; with a CSPRNG this is astronomically
24
+ // unlikely, but honour the contract by signalling a re-draw.
25
+ return /1/.test(str) ? str : null
26
+ })
27
+ _secrets = s
28
+ return _secrets
29
+ }
19
30
 
20
31
  /**
21
- * Prime number for finite field operations
22
- * Using a large prime that's suitable for byte operations (2^31 - 1)
32
+ * Shamir Secret Sharing.
33
+ *
34
+ * The split/combine math is provided by `secrets.js-grempe`, a vetted GF(2^8)
35
+ * implementation (the same field/approach used by hardware wallets), rather
36
+ * than a hand-rolled finite field. On top of it this module adds:
37
+ *
38
+ * - a per-split nonce (`splitId`) so shares from different splits can't be
39
+ * silently mixed into a wrong-but-plausible reconstruction;
40
+ * - an integrity `checksum` (truncated SHA-256 of the secret) so a tampered
41
+ * share set is detected at combine time instead of returning garbage.
42
+ *
43
+ * Share objects produced here are format v2. Shares produced by <= 4.x (which
44
+ * carried a `bytes` array of {x, y} points over a 31-bit prime field) are still
45
+ * accepted by `combine()` for recovery only — see `_combineLegacy`.
23
46
  */
24
- var PRIME = new BN(2147483647) // Mersenne prime 2^31 - 1
47
+
48
+ // Marker byte prepended to the secret before sharing. Guarantees a non-zero
49
+ // leading byte so leading zero bytes of the secret survive the hex round-trip,
50
+ // and lets combine() recover the exact original length.
51
+ var MARKER = 'ff'
52
+
53
+ var SHARE_VERSION = 2
25
54
 
26
55
  /**
27
- * Shamir Secret Sharing constructor
28
- * @param {Object} options - Configuration options
56
+ * Shamir Secret Sharing constructor (kept for API compatibility).
57
+ * @param {Object} options
29
58
  */
30
- function Shamir(options) {
59
+ function Shamir (options) {
31
60
  if (!(this instanceof Shamir)) {
32
61
  return new Shamir(options)
33
62
  }
34
-
35
63
  this.options = options || {}
36
64
  return this
37
65
  }
38
66
 
39
67
  /**
40
- * Split a secret into shares using Shamir's Secret Sharing
68
+ * Split a secret into shares.
41
69
  * @param {Buffer|String} secret - The secret to split
42
- * @param {Number} threshold - Minimum number of shares needed to reconstruct
43
- * @param {Number} shares - Total number of shares to generate
44
- * @param {Object} options - Additional options
45
- * @returns {Array} Array of share objects with {x, y} coordinates
70
+ * @param {Number} threshold - Minimum shares needed to reconstruct (>= 2)
71
+ * @param {Number} shares - Total shares to generate (>= threshold, <= 255)
72
+ * @param {Object} [options]
73
+ * @param {Boolean} [options.checksum=true] - embed a truncated SHA-256 of the
74
+ * secret in each share so combine() can detect a wrong/tampered share set.
75
+ * Set false to avoid disclosing any hash of the secret.
76
+ * @returns {Array} Array of v2 share objects
46
77
  */
47
- Shamir.split = function(secret, threshold, shares, options) {
78
+ Shamir.split = function (secret, threshold, shares, options) {
48
79
  options = options || {}
49
-
50
- if (!secret) {
80
+
81
+ if (secret === undefined || secret === null) {
51
82
  throw new Error('Secret is required')
52
83
  }
53
-
54
84
  if (threshold < 2) {
55
85
  throw new Error('Threshold must be at least 2')
56
86
  }
57
-
58
87
  if (shares < threshold) {
59
88
  throw new Error('Number of shares must be at least threshold')
60
89
  }
61
-
62
90
  if (threshold > 255 || shares > 255) {
63
91
  throw new Error('Threshold and shares must be <= 255')
64
92
  }
65
-
66
- // Convert secret to buffer if string
93
+
67
94
  var secretBuffer = Buffer.isBuffer(secret) ? secret : Buffer.from(secret, 'utf8')
68
-
69
- // Process each byte of the secret
70
- var allShares = []
71
-
72
- for (var i = 0; i < secretBuffer.length; i++) {
73
- var byte = secretBuffer[i]
74
- var byteShares = Shamir._splitByte(byte, threshold, shares)
75
- allShares.push(byteShares)
76
- }
77
-
78
- // Combine shares across bytes
95
+
96
+ var markedHex = MARKER + secretBuffer.toString('hex')
97
+ var rawShares = secretsLib().share(markedHex, shares, threshold)
98
+
99
+ var splitId = Random.getRandomBuffer(8).toString('hex')
100
+ var useChecksum = options.checksum !== false
101
+ var checksum = useChecksum
102
+ ? Hash.sha256(secretBuffer).slice(0, 4).toString('hex')
103
+ : null
104
+
79
105
  var result = []
80
- for (var j = 0; j < shares; j++) {
81
- var shareData = {
82
- id: j + 1,
106
+ for (var i = 0; i < rawShares.length; i++) {
107
+ result.push({
108
+ v: SHARE_VERSION,
109
+ id: i + 1,
83
110
  threshold: threshold,
84
111
  shares: shares,
85
112
  length: secretBuffer.length,
86
- bytes: []
87
- }
88
-
89
- for (var k = 0; k < allShares.length; k++) {
90
- shareData.bytes.push(allShares[k][j])
91
- }
92
-
93
- result.push(shareData)
113
+ splitId: splitId,
114
+ share: rawShares[i],
115
+ checksum: checksum
116
+ })
94
117
  }
95
-
96
118
  return result
97
119
  }
98
120
 
99
121
  /**
100
- * Combine shares to reconstruct the original secret
101
- * @param {Array} shares - Array of share objects
122
+ * Combine shares to reconstruct the original secret.
123
+ * @param {Array} shares - Array of share objects (v2 or legacy)
102
124
  * @returns {Buffer} The reconstructed secret
103
125
  */
104
- Shamir.combine = function(shares) {
126
+ Shamir.combine = function (shares) {
105
127
  if (!shares || shares.length === 0) {
106
128
  throw new Error('Shares array is required')
107
129
  }
108
-
109
- // Validate shares
130
+
131
+ // Route legacy (<= 4.x) shares to the retained recovery-only combiner.
132
+ if (shares[0] && Array.isArray(shares[0].bytes) && shares[0].share === undefined) {
133
+ return Shamir._combineLegacy(shares)
134
+ }
135
+
136
+ var threshold = shares[0].threshold
137
+ var splitId = shares[0].splitId
138
+
139
+ if (shares.length < threshold) {
140
+ throw new Error('Insufficient shares: need ' + threshold + ', got ' + shares.length)
141
+ }
142
+
143
+ var rawShares = []
144
+ for (var i = 0; i < shares.length; i++) {
145
+ var s = shares[i]
146
+ if (!s || typeof s.share !== 'string') {
147
+ throw new Error('Invalid share at index ' + i)
148
+ }
149
+ // All shares must come from the same split.
150
+ if (splitId !== undefined && s.splitId !== splitId) {
151
+ throw new Error('Shares are from different splits (splitId mismatch)')
152
+ }
153
+ if (s.threshold !== threshold) {
154
+ throw new Error('Shares have inconsistent threshold')
155
+ }
156
+ rawShares.push(s.share)
157
+ }
158
+
159
+ var markedHex = secretsLib().combine(rawShares)
160
+ if (markedHex.slice(0, MARKER.length) !== MARKER) {
161
+ // Wrong/insufficient/corrupt shares produced an unmarked result.
162
+ throw new Error('Reconstruction failed: invalid or insufficient shares')
163
+ }
164
+ var secretBuffer = Buffer.from(markedHex.slice(MARKER.length), 'hex')
165
+
166
+ // Integrity check: a tampered share or a mismatched set reconstructs to the
167
+ // wrong secret; the embedded checksum catches it instead of silently
168
+ // returning garbage.
169
+ var expected = shares[0].checksum
170
+ if (expected) {
171
+ var actual = Hash.sha256(secretBuffer).slice(0, 4).toString('hex')
172
+ if (actual !== expected) {
173
+ throw new Error('Integrity check failed: shares are inconsistent or tampered')
174
+ }
175
+ }
176
+
177
+ return secretBuffer
178
+ }
179
+
180
+ /**
181
+ * Verify a share is structurally valid.
182
+ * @param {Object} share
183
+ * @returns {Boolean}
184
+ */
185
+ Shamir.verifyShare = function (share) {
186
+ try {
187
+ if (!share || typeof share !== 'object') {
188
+ return false
189
+ }
190
+
191
+ // Legacy share shape (recovery only).
192
+ if (Array.isArray(share.bytes) && share.share === undefined) {
193
+ return Shamir._verifyLegacyShare(share)
194
+ }
195
+
196
+ if (share.v !== SHARE_VERSION) {
197
+ return false
198
+ }
199
+ if (typeof share.share !== 'string' || !/^[0-9a-fA-F]+$/.test(share.share)) {
200
+ return false
201
+ }
202
+ if (typeof share.threshold !== 'number' || typeof share.shares !== 'number') {
203
+ return false
204
+ }
205
+ if (share.threshold < 2 || share.shares < share.threshold || share.shares > 255) {
206
+ return false
207
+ }
208
+ if (share.id < 1 || share.id > share.shares) {
209
+ return false
210
+ }
211
+ if (share.checksum !== null && share.checksum !== undefined &&
212
+ !/^[0-9a-fA-F]{8}$/.test(share.checksum)) {
213
+ return false
214
+ }
215
+ // secrets.js validates the internal share structure (throws on garbage).
216
+ secretsLib().extractShareComponents(share.share)
217
+ return true
218
+ } catch (e) {
219
+ return false
220
+ }
221
+ }
222
+
223
+ /**
224
+ * Generate test vectors for validation.
225
+ * @returns {Object}
226
+ */
227
+ Shamir.generateTestVectors = function () {
228
+ var secret = 'Hello, Bitcoin SV!'
229
+ var threshold = 3
230
+ var shares = 5
231
+
232
+ var splitShares = Shamir.split(secret, threshold, shares)
233
+ var reconstructed = Shamir.combine(splitShares.slice(0, threshold))
234
+
235
+ return {
236
+ secret: secret,
237
+ threshold: threshold,
238
+ totalShares: shares,
239
+ shares: splitShares,
240
+ reconstructed: reconstructed.toString('utf8'),
241
+ valid: reconstructed.toString('utf8') === secret
242
+ }
243
+ }
244
+
245
+ // ---------------------------------------------------------------------------
246
+ // Legacy recovery (shares produced by <= 4.x). Read-only; do NOT use to split.
247
+ // These reconstruct over the old 31-bit prime field. Retained so secrets split
248
+ // with older versions remain recoverable.
249
+ // ---------------------------------------------------------------------------
250
+
251
+ var LEGACY_PRIME = new BN(2147483647) // 2^31 - 1
252
+
253
+ Shamir._combineLegacy = function (shares) {
110
254
  var threshold = shares[0].threshold
111
255
  var totalShares = shares[0].shares
112
256
  var secretLength = shares[0].length
113
-
257
+
114
258
  if (shares.length < threshold) {
115
259
  throw new Error('Insufficient shares: need ' + threshold + ', got ' + shares.length)
116
260
  }
117
-
118
- // Verify all shares have same parameters
119
261
  for (var i = 0; i < shares.length; i++) {
120
262
  if (shares[i].threshold !== threshold || shares[i].shares !== totalShares) {
121
263
  throw new Error('Shares have inconsistent parameters')
@@ -124,158 +266,66 @@ Shamir.combine = function(shares) {
124
266
  throw new Error('Shares have different secret lengths')
125
267
  }
126
268
  }
127
-
269
+
128
270
  var reconstructedBytes = []
129
-
130
- // Reconstruct each byte
131
271
  for (var j = 0; j < secretLength; j++) {
132
272
  var byteShares = []
133
273
  for (var k = 0; k < Math.min(shares.length, threshold); k++) {
134
274
  byteShares.push(shares[k].bytes[j])
135
275
  }
136
-
137
- var byte = Shamir._combineByte(byteShares)
138
- reconstructedBytes.push(byte)
276
+ reconstructedBytes.push(Shamir._combineByteLegacy(byteShares))
139
277
  }
140
-
141
- // Create buffer from reconstructed bytes
142
278
  return Buffer.from(reconstructedBytes)
143
279
  }
144
280
 
145
- /**
146
- * Split a single byte using polynomial interpolation
147
- * @private
148
- */
149
- Shamir._splitByte = function(secretByte, threshold, shares) {
150
- // Convert byte to big number
151
- var secret = new BN(secretByte)
152
-
153
- // Generate random polynomial coefficients
154
- var coefficients = [secret] // a0 = secret
155
-
156
- for (var i = 1; i < threshold; i++) {
157
- var coeff = Shamir._randomFieldElement()
158
- coefficients.push(coeff)
159
- }
160
-
161
- // Generate shares by evaluating polynomial at different points
162
- var result = []
163
- for (var x = 1; x <= shares; x++) {
164
- var y = Shamir._evaluatePolynomial(coefficients, new BN(x))
165
- result.push({
166
- x: x,
167
- y: y.toString(16)
168
- })
169
- }
170
-
171
- return result
172
- }
173
-
174
- /**
175
- * Combine shares for a single byte using Lagrange interpolation
176
- * @private
177
- */
178
- Shamir._combineByte = function(shares) {
179
- if (shares.length === 0) {
180
- throw new Error('No shares provided')
181
- }
182
-
281
+ Shamir._combineByteLegacy = function (shares) {
183
282
  var result = new BN(0)
184
-
185
283
  for (var i = 0; i < shares.length; i++) {
186
284
  var xi = new BN(shares[i].x)
187
285
  var yi = new BN(shares[i].y, 16)
188
-
189
- // Calculate Lagrange basis polynomial
190
286
  var numerator = new BN(1)
191
287
  var denominator = new BN(1)
192
-
288
+
193
289
  for (var j = 0; j < shares.length; j++) {
194
290
  if (i !== j) {
195
291
  var xj = new BN(shares[j].x)
196
- // For Lagrange interpolation at x=0 (to get the constant term)
197
292
  var numFactor = new BN(0).sub(xj)
198
293
  if (numFactor.lt(new BN(0))) {
199
- numFactor = numFactor.add(PRIME)
294
+ numFactor = numFactor.add(LEGACY_PRIME)
200
295
  }
201
- numerator = numerator.mul(numFactor).mod(PRIME)
202
-
296
+ numerator = numerator.mul(numFactor).mod(LEGACY_PRIME)
297
+
203
298
  var denFactor = xi.sub(xj)
204
299
  if (denFactor.lt(new BN(0))) {
205
- denFactor = denFactor.add(PRIME)
300
+ denFactor = denFactor.add(LEGACY_PRIME)
206
301
  }
207
- denominator = denominator.mul(denFactor).mod(PRIME)
302
+ denominator = denominator.mul(denFactor).mod(LEGACY_PRIME)
208
303
  }
209
304
  }
210
-
211
- // Calculate modular inverse of denominator
212
- var inverse = Shamir._modInverse(denominator, PRIME)
213
- var lagrange = numerator.mul(inverse).mod(PRIME)
214
-
215
- // Add to result
216
- result = result.add(yi.mul(lagrange)).mod(PRIME)
217
- }
218
-
219
- // Convert back to byte (0-255) - ensure positive result
220
- var finalResult = result.mod(PRIME).mod(new BN(256))
221
- return finalResult.toNumber()
222
- }
223
305
 
224
- /**
225
- * Evaluate polynomial at given point
226
- * @private
227
- */
228
- Shamir._evaluatePolynomial = function(coefficients, x) {
229
- var result = new BN(0)
230
- var xPower = new BN(1)
231
-
232
- for (var i = 0; i < coefficients.length; i++) {
233
- result = result.add(coefficients[i].mul(xPower)).mod(PRIME)
234
- xPower = xPower.mul(x).mod(PRIME)
306
+ var inverse = Shamir._modInverseLegacy(denominator, LEGACY_PRIME)
307
+ var lagrange = numerator.mul(inverse).mod(LEGACY_PRIME)
308
+ result = result.add(yi.mul(lagrange)).mod(LEGACY_PRIME)
235
309
  }
236
-
237
- return result
310
+ return result.mod(LEGACY_PRIME).mod(new BN(256)).toNumber()
238
311
  }
239
312
 
240
- /**
241
- * Generate random field element
242
- * @private
243
- */
244
- Shamir._randomFieldElement = function() {
245
- var bytes = Random.getRandomBuffer(32)
246
- var num = new BN(bytes)
247
- return num.mod(PRIME.sub(new BN(1))).add(new BN(1))
248
- }
249
-
250
- /**
251
- * Calculate modular inverse using extended Euclidean algorithm
252
- * @private
253
- */
254
- Shamir._modInverse = function(a, m) {
313
+ Shamir._modInverseLegacy = function (a, m) {
255
314
  if (a.lt(new BN(0))) {
256
315
  a = a.mod(m).add(m)
257
316
  }
258
-
259
- var g = Shamir._extendedGCD(a, m)
260
-
317
+ var g = Shamir._extendedGCDLegacy(a, m)
261
318
  if (!g.gcd.eq(new BN(1))) {
262
319
  throw new Error('Modular inverse does not exist')
263
320
  }
264
-
265
321
  return g.x.mod(m).add(m).mod(m)
266
322
  }
267
323
 
268
- /**
269
- * Extended Euclidean algorithm
270
- * @private
271
- */
272
- Shamir._extendedGCD = function(a, b) {
324
+ Shamir._extendedGCDLegacy = function (a, b) {
273
325
  if (a.eq(new BN(0))) {
274
326
  return { gcd: b, x: new BN(0), y: new BN(1) }
275
327
  }
276
-
277
- var g = Shamir._extendedGCD(b.mod(a), a)
278
-
328
+ var g = Shamir._extendedGCDLegacy(b.mod(a), a)
279
329
  return {
280
330
  gcd: g.gcd,
281
331
  x: g.y.sub(b.div(a).mul(g.x)),
@@ -283,78 +333,29 @@ Shamir._extendedGCD = function(a, b) {
283
333
  }
284
334
  }
285
335
 
286
- /**
287
- * Verify share integrity
288
- * @param {Object} share - Share to verify
289
- * @returns {Boolean} True if share is valid
290
- */
291
- Shamir.verifyShare = function(share) {
292
- try {
293
- if (!share || typeof share !== 'object') {
294
- return false
295
- }
296
-
297
- if (!share.id || !share.threshold || !share.shares || !share.bytes || typeof share.length !== 'number') {
336
+ Shamir._verifyLegacyShare = function (share) {
337
+ if (!share.id || !share.threshold || !share.shares || !share.bytes || typeof share.length !== 'number') {
338
+ return false
339
+ }
340
+ if (share.threshold < 2 || share.shares < share.threshold) {
341
+ return false
342
+ }
343
+ if (!Array.isArray(share.bytes) || share.bytes.length !== share.length) {
344
+ return false
345
+ }
346
+ for (var i = 0; i < share.bytes.length; i++) {
347
+ var byteShare = share.bytes[i]
348
+ if (!byteShare || !byteShare.x || !byteShare.y) {
298
349
  return false
299
350
  }
300
-
301
- if (share.threshold < 2 || share.shares < share.threshold) {
351
+ if (byteShare.x < 1 || byteShare.x > share.shares) {
302
352
  return false
303
353
  }
304
-
305
- if (!Array.isArray(share.bytes) || share.bytes.length !== share.length) {
354
+ if (!/^[0-9a-fA-F]+$/.test(byteShare.y)) {
306
355
  return false
307
356
  }
308
-
309
- // Verify each byte share
310
- for (var i = 0; i < share.bytes.length; i++) {
311
- var byteShare = share.bytes[i]
312
- if (!byteShare.x || !byteShare.y) {
313
- return false
314
- }
315
-
316
- if (byteShare.x < 1 || byteShare.x > share.shares) {
317
- return false
318
- }
319
-
320
- // Verify y is valid hex
321
- try {
322
- var testHex = byteShare.y
323
- if (!/^[0-9a-fA-F]+$/.test(testHex)) {
324
- return false
325
- }
326
- new BN(testHex, 16)
327
- } catch (e) {
328
- return false
329
- }
330
- }
331
-
332
- return true
333
- } catch (e) {
334
- return false
335
- }
336
- }
337
-
338
- /**
339
- * Generate test vectors for validation
340
- * @returns {Object} Test data
341
- */
342
- Shamir.generateTestVectors = function() {
343
- var secret = 'Hello, Bitcoin SV!'
344
- var threshold = 3
345
- var shares = 5
346
-
347
- var splitShares = Shamir.split(secret, threshold, shares)
348
- var reconstructed = Shamir.combine(splitShares.slice(0, threshold))
349
-
350
- return {
351
- secret: secret,
352
- threshold: threshold,
353
- totalShares: shares,
354
- shares: splitShares,
355
- reconstructed: reconstructed.toString('utf8'),
356
- valid: reconstructed.toString('utf8') === secret
357
357
  }
358
+ return true
358
359
  }
359
360
 
360
- module.exports = Shamir
361
+ module.exports = Shamir
@@ -9,6 +9,20 @@ var aesjs = require('aes-js')
9
9
  var CBC = aesjs.ModeOfOperation.cbc
10
10
  var Random = bsv.crypto.Random
11
11
 
12
+ // Constant-time buffer comparison. Works in Node and browser bundles alike
13
+ // (crypto.timingSafeEqual is not provided by crypto-browserify). Compares the
14
+ // full length unconditionally so timing does not leak how many bytes matched.
15
+ function constantTimeEqual (a, b) {
16
+ if (a.length !== b.length) {
17
+ return false
18
+ }
19
+ var diff = 0
20
+ for (var i = 0; i < a.length; i++) {
21
+ diff |= a[i] ^ b[i]
22
+ }
23
+ return diff === 0
24
+ }
25
+
12
26
  var AESCBC = function AESCBC () {}
13
27
 
14
28
  AESCBC.encrypt = function (messagebuf, cipherkeybuf, ivbuf) {
@@ -149,11 +163,8 @@ ECIES.prototype.decrypt = function (encbuf) {
149
163
  var d2 = Hash.sha256hmac(c, this.kM)
150
164
  if (this.opts.shortTag) d2 = d2.slice(0, 4)
151
165
 
152
- var equal = true
153
- for (var i = 0; i < d.length; i++) {
154
- equal &= (d[i] === d2[i])
155
- }
156
- if (!equal) {
166
+ // Constant-time MAC comparison (encrypt-then-MAC verification).
167
+ if (!constantTimeEqual(d, d2)) {
157
168
  throw new Error('Invalid checksum')
158
169
  }
159
170