@bsv/wallet-toolbox 1.7.22 → 1.8.2
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/CHANGELOG.md +8 -0
- package/docs/README.md +1 -0
- package/docs/client.md +135 -0
- package/docs/wab-shamir.md +311 -0
- package/docs/wallet.md +135 -0
- package/out/src/ShamirWalletManager.d.ts +213 -0
- package/out/src/ShamirWalletManager.d.ts.map +1 -0
- package/out/src/ShamirWalletManager.js +363 -0
- package/out/src/ShamirWalletManager.js.map +1 -0
- package/out/src/WalletPermissionsManager.d.ts +27 -0
- package/out/src/WalletPermissionsManager.d.ts.map +1 -1
- package/out/src/WalletPermissionsManager.js +308 -147
- package/out/src/WalletPermissionsManager.js.map +1 -1
- package/out/src/__tests/ShamirWalletManager.test.d.ts +2 -0
- package/out/src/__tests/ShamirWalletManager.test.d.ts.map +1 -0
- package/out/src/__tests/ShamirWalletManager.test.js +298 -0
- package/out/src/__tests/ShamirWalletManager.test.js.map +1 -0
- package/out/src/__tests/WalletPermissionsManager.callbacks.test.js +116 -0
- package/out/src/__tests/WalletPermissionsManager.callbacks.test.js.map +1 -1
- package/out/src/__tests/WalletPermissionsManager.pmodules.test.js +111 -0
- package/out/src/__tests/WalletPermissionsManager.pmodules.test.js.map +1 -1
- package/out/src/entropy/EntropyCollector.d.ts +89 -0
- package/out/src/entropy/EntropyCollector.d.ts.map +1 -0
- package/out/src/entropy/EntropyCollector.js +176 -0
- package/out/src/entropy/EntropyCollector.js.map +1 -0
- package/out/src/entropy/__tests/EntropyCollector.test.d.ts +2 -0
- package/out/src/entropy/__tests/EntropyCollector.test.d.ts.map +1 -0
- package/out/src/entropy/__tests/EntropyCollector.test.js +137 -0
- package/out/src/entropy/__tests/EntropyCollector.test.js.map +1 -0
- package/out/src/index.all.d.ts +2 -0
- package/out/src/index.all.d.ts.map +1 -1
- package/out/src/index.all.js +2 -0
- package/out/src/index.all.js.map +1 -1
- package/out/src/sdk/WalletServices.interfaces.d.ts.map +1 -1
- package/out/src/services/chaintracker/chaintracks/Storage/ChaintracksStorageBase.d.ts.map +1 -1
- package/out/src/services/chaintracker/chaintracks/Storage/ChaintracksStorageBase.js +4 -1
- package/out/src/services/chaintracker/chaintracks/Storage/ChaintracksStorageBase.js.map +1 -1
- package/out/src/wab-client/WABClient.d.ts +65 -0
- package/out/src/wab-client/WABClient.d.ts.map +1 -1
- package/out/src/wab-client/WABClient.js +107 -0
- package/out/src/wab-client/WABClient.js.map +1 -1
- package/out/tsconfig.all.tsbuildinfo +1 -1
- package/package.json +5 -1
- package/src/ShamirWalletManager.ts +499 -0
- package/src/WalletPermissionsManager.ts +368 -181
- package/src/__tests/ShamirWalletManager.test.ts +369 -0
- package/src/__tests/WalletPermissionsManager.callbacks.test.ts +140 -1
- package/src/__tests/WalletPermissionsManager.pmodules.test.ts +152 -0
- package/src/entropy/EntropyCollector.ts +228 -0
- package/src/entropy/__tests/EntropyCollector.test.ts +182 -0
- package/src/index.all.ts +2 -0
- package/src/sdk/WalletServices.interfaces.ts +0 -1
- package/src/services/chaintracker/chaintracks/Storage/ChaintracksStorageBase.ts +4 -1
- package/src/wab-client/WABClient.ts +135 -0
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EntropyCollector
|
|
3
|
+
*
|
|
4
|
+
* Collects entropy from user interactions (mouse movements) and mixes it with
|
|
5
|
+
* cryptographically secure random numbers to generate high-quality random seeds
|
|
6
|
+
* for key generation.
|
|
7
|
+
*
|
|
8
|
+
* This provides defense-in-depth: even if the system's CSPRNG is compromised,
|
|
9
|
+
* the user-provided entropy adds unpredictability. Conversely, if the user's
|
|
10
|
+
* mouse movements are predictable, the CSPRNG ensures security.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { Random, Hash } from '@bsv/sdk'
|
|
14
|
+
|
|
15
|
+
export interface EntropyCollectorConfig {
|
|
16
|
+
/** Target number of mouse samples to collect (default: 256) */
|
|
17
|
+
targetSamples?: number
|
|
18
|
+
/** Minimum time between samples in ms (default: 10) */
|
|
19
|
+
minSampleInterval?: number
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface EntropyProgress {
|
|
23
|
+
/** Number of samples collected so far */
|
|
24
|
+
collected: number
|
|
25
|
+
/** Target number of samples */
|
|
26
|
+
target: number
|
|
27
|
+
/** Percentage complete (0-100) */
|
|
28
|
+
percent: number
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Callback type for entropy collection progress updates
|
|
33
|
+
*/
|
|
34
|
+
export type EntropyProgressCallback = (progress: EntropyProgress) => void
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Collected entropy data before mixing
|
|
38
|
+
*/
|
|
39
|
+
interface RawEntropyData {
|
|
40
|
+
mousePositions: Array<{ x: number; y: number; time: number }>
|
|
41
|
+
timingDeltas: number[]
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export class EntropyCollector {
|
|
45
|
+
private config: Required<EntropyCollectorConfig>
|
|
46
|
+
private rawEntropy: RawEntropyData = {
|
|
47
|
+
mousePositions: [],
|
|
48
|
+
timingDeltas: []
|
|
49
|
+
}
|
|
50
|
+
private lastSampleTime: number = 0
|
|
51
|
+
|
|
52
|
+
constructor(config: EntropyCollectorConfig = {}) {
|
|
53
|
+
this.config = {
|
|
54
|
+
targetSamples: config.targetSamples ?? 256,
|
|
55
|
+
minSampleInterval: config.minSampleInterval ?? 10
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Reset collected entropy data
|
|
61
|
+
*/
|
|
62
|
+
reset(): void {
|
|
63
|
+
this.rawEntropy = {
|
|
64
|
+
mousePositions: [],
|
|
65
|
+
timingDeltas: []
|
|
66
|
+
}
|
|
67
|
+
this.lastSampleTime = 0
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Add a mouse movement sample
|
|
72
|
+
* Call this from a mousemove event handler
|
|
73
|
+
*
|
|
74
|
+
* @returns Progress information, or null if sample was rejected (too soon)
|
|
75
|
+
*/
|
|
76
|
+
addMouseSample(x: number, y: number): EntropyProgress | null {
|
|
77
|
+
const now = performance.now()
|
|
78
|
+
|
|
79
|
+
// Rate limit samples to reduce correlation
|
|
80
|
+
if (now - this.lastSampleTime < this.config.minSampleInterval) {
|
|
81
|
+
return null
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Record timing delta (high precision timing adds entropy)
|
|
85
|
+
if (this.lastSampleTime > 0) {
|
|
86
|
+
this.rawEntropy.timingDeltas.push(now - this.lastSampleTime)
|
|
87
|
+
}
|
|
88
|
+
this.lastSampleTime = now
|
|
89
|
+
|
|
90
|
+
// Record position
|
|
91
|
+
this.rawEntropy.mousePositions.push({ x, y, time: now })
|
|
92
|
+
|
|
93
|
+
return this.getProgress()
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Get current collection progress
|
|
98
|
+
*/
|
|
99
|
+
getProgress(): EntropyProgress {
|
|
100
|
+
const collected = this.rawEntropy.mousePositions.length
|
|
101
|
+
return {
|
|
102
|
+
collected,
|
|
103
|
+
target: this.config.targetSamples,
|
|
104
|
+
percent: Math.min(100, Math.floor((collected / this.config.targetSamples) * 100))
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Check if enough entropy has been collected
|
|
110
|
+
*/
|
|
111
|
+
isComplete(): boolean {
|
|
112
|
+
return this.rawEntropy.mousePositions.length >= this.config.targetSamples
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Extract entropy bytes from collected mouse data
|
|
117
|
+
* Uses SHA-256 to compress and whiten the data
|
|
118
|
+
*/
|
|
119
|
+
private extractRawEntropy(): Uint8Array {
|
|
120
|
+
// Serialize all collected data
|
|
121
|
+
const dataPoints: number[] = []
|
|
122
|
+
|
|
123
|
+
// Add mouse positions
|
|
124
|
+
for (const pos of this.rawEntropy.mousePositions) {
|
|
125
|
+
// Use lower bits of coordinates (more random due to jitter)
|
|
126
|
+
dataPoints.push(pos.x & 0xff)
|
|
127
|
+
dataPoints.push(pos.y & 0xff)
|
|
128
|
+
// Include fractional timing (microsecond precision)
|
|
129
|
+
dataPoints.push(Math.floor(pos.time * 1000) & 0xff)
|
|
130
|
+
dataPoints.push(Math.floor(pos.time * 1000000) & 0xff)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Add timing deltas (high entropy source)
|
|
134
|
+
for (const delta of this.rawEntropy.timingDeltas) {
|
|
135
|
+
// Timing deltas converted to bytes
|
|
136
|
+
dataPoints.push(Math.floor(delta * 1000) & 0xff)
|
|
137
|
+
dataPoints.push(Math.floor(delta * 1000000) & 0xff)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Hash to compress and whiten the entropy
|
|
141
|
+
const hash = Hash.sha256(dataPoints)
|
|
142
|
+
return new Uint8Array(hash)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Mix user entropy with system CSPRNG for defense-in-depth
|
|
147
|
+
*
|
|
148
|
+
* @returns 32 bytes of high-quality random data suitable for key generation
|
|
149
|
+
*/
|
|
150
|
+
mixWithCSPRNG(): Uint8Array {
|
|
151
|
+
const userEntropy = this.extractRawEntropy()
|
|
152
|
+
const systemEntropy = new Uint8Array(Random(32))
|
|
153
|
+
|
|
154
|
+
// XOR user entropy with system entropy
|
|
155
|
+
const mixed = new Uint8Array(32)
|
|
156
|
+
for (let i = 0; i < 32; i++) {
|
|
157
|
+
mixed[i] = userEntropy[i] ^ systemEntropy[i]
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Final hash to ensure uniform distribution
|
|
161
|
+
const final = Hash.sha256(Array.from(mixed))
|
|
162
|
+
return new Uint8Array(final)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Generate entropy with automatic CSPRNG fallback
|
|
167
|
+
*
|
|
168
|
+
* If not enough user entropy has been collected, supplements with CSPRNG.
|
|
169
|
+
* Always mixes with CSPRNG regardless.
|
|
170
|
+
*
|
|
171
|
+
* @returns 32 bytes suitable for private key generation
|
|
172
|
+
*/
|
|
173
|
+
generateEntropy(): Uint8Array {
|
|
174
|
+
if (!this.isComplete()) {
|
|
175
|
+
console.warn(
|
|
176
|
+
`EntropyCollector: Only ${this.rawEntropy.mousePositions.length}/${this.config.targetSamples} ` +
|
|
177
|
+
`samples collected. Supplementing with additional CSPRNG entropy.`
|
|
178
|
+
)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return this.mixWithCSPRNG()
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Convenience method for browser environments.
|
|
186
|
+
* Creates event listeners and resolves when enough entropy is collected.
|
|
187
|
+
*
|
|
188
|
+
* @param element The HTML element to listen on (default: document)
|
|
189
|
+
* @param onProgress Optional callback for progress updates
|
|
190
|
+
* @returns Promise that resolves with 32 bytes of entropy
|
|
191
|
+
*/
|
|
192
|
+
async collectFromBrowser(element: EventTarget = document, onProgress?: EntropyProgressCallback): Promise<Uint8Array> {
|
|
193
|
+
return new Promise(resolve => {
|
|
194
|
+
const handler = (event: Event) => {
|
|
195
|
+
if (event instanceof MouseEvent) {
|
|
196
|
+
const progress = this.addMouseSample(event.clientX, event.clientY)
|
|
197
|
+
if (progress) {
|
|
198
|
+
onProgress?.(progress)
|
|
199
|
+
|
|
200
|
+
if (this.isComplete()) {
|
|
201
|
+
element.removeEventListener('mousemove', handler)
|
|
202
|
+
resolve(this.generateEntropy())
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
element.addEventListener('mousemove', handler)
|
|
209
|
+
})
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Estimate the quality of collected entropy in bits
|
|
214
|
+
* This is a rough heuristic, not a cryptographic guarantee
|
|
215
|
+
*/
|
|
216
|
+
estimateEntropyBits(): number {
|
|
217
|
+
const samples = this.rawEntropy.mousePositions.length
|
|
218
|
+
|
|
219
|
+
if (samples === 0) return 0
|
|
220
|
+
|
|
221
|
+
// Assume roughly 4-6 bits per mouse position (jitter in lower bits)
|
|
222
|
+
// Plus 2-4 bits from timing
|
|
223
|
+
const bitsPerSample = 6
|
|
224
|
+
|
|
225
|
+
// Cap at 256 bits (32 bytes) since that's our output size
|
|
226
|
+
return Math.min(256, samples * bitsPerSample)
|
|
227
|
+
}
|
|
228
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { EntropyCollector } from '../EntropyCollector'
|
|
2
|
+
|
|
3
|
+
describe('EntropyCollector', () => {
|
|
4
|
+
describe('Configuration', () => {
|
|
5
|
+
it('should use default configuration', () => {
|
|
6
|
+
const collector = new EntropyCollector()
|
|
7
|
+
const progress = collector.getProgress()
|
|
8
|
+
|
|
9
|
+
expect(progress.target).toBe(256)
|
|
10
|
+
expect(progress.collected).toBe(0)
|
|
11
|
+
expect(progress.percent).toBe(0)
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it('should accept custom configuration', () => {
|
|
15
|
+
const collector = new EntropyCollector({
|
|
16
|
+
targetSamples: 100,
|
|
17
|
+
minSampleInterval: 20
|
|
18
|
+
})
|
|
19
|
+
const progress = collector.getProgress()
|
|
20
|
+
|
|
21
|
+
expect(progress.target).toBe(100)
|
|
22
|
+
})
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
describe('Sample Collection', () => {
|
|
26
|
+
it('should collect mouse samples', () => {
|
|
27
|
+
const collector = new EntropyCollector({ targetSamples: 10, minSampleInterval: 0 })
|
|
28
|
+
|
|
29
|
+
const progress = collector.addMouseSample(100, 200)
|
|
30
|
+
|
|
31
|
+
expect(progress).not.toBeNull()
|
|
32
|
+
expect(progress!.collected).toBe(1)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('should reject samples that are too close together', () => {
|
|
36
|
+
const collector = new EntropyCollector({ targetSamples: 10, minSampleInterval: 1000 })
|
|
37
|
+
|
|
38
|
+
collector.addMouseSample(100, 200)
|
|
39
|
+
const secondProgress = collector.addMouseSample(101, 201)
|
|
40
|
+
|
|
41
|
+
expect(secondProgress).toBeNull()
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('should track progress percentage', () => {
|
|
45
|
+
const collector = new EntropyCollector({ targetSamples: 10, minSampleInterval: 0 })
|
|
46
|
+
|
|
47
|
+
for (let i = 0; i < 5; i++) {
|
|
48
|
+
collector.addMouseSample(i * 10, i * 20)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const progress = collector.getProgress()
|
|
52
|
+
expect(progress.percent).toBe(50)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('should cap percent at 100', () => {
|
|
56
|
+
const collector = new EntropyCollector({ targetSamples: 5, minSampleInterval: 0 })
|
|
57
|
+
|
|
58
|
+
for (let i = 0; i < 10; i++) {
|
|
59
|
+
collector.addMouseSample(i * 10, i * 20)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const progress = collector.getProgress()
|
|
63
|
+
expect(progress.percent).toBe(100)
|
|
64
|
+
})
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
describe('Completion', () => {
|
|
68
|
+
it('should report incomplete when not enough samples', () => {
|
|
69
|
+
const collector = new EntropyCollector({ targetSamples: 100, minSampleInterval: 0 })
|
|
70
|
+
|
|
71
|
+
for (let i = 0; i < 50; i++) {
|
|
72
|
+
collector.addMouseSample(i, i)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
expect(collector.isComplete()).toBe(false)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('should report complete when enough samples collected', () => {
|
|
79
|
+
const collector = new EntropyCollector({ targetSamples: 10, minSampleInterval: 0 })
|
|
80
|
+
|
|
81
|
+
for (let i = 0; i < 10; i++) {
|
|
82
|
+
collector.addMouseSample(i * 5, i * 10)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
expect(collector.isComplete()).toBe(true)
|
|
86
|
+
})
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
describe('Reset', () => {
|
|
90
|
+
it('should reset collected samples', () => {
|
|
91
|
+
const collector = new EntropyCollector({ targetSamples: 10, minSampleInterval: 0 })
|
|
92
|
+
|
|
93
|
+
for (let i = 0; i < 5; i++) {
|
|
94
|
+
collector.addMouseSample(i, i)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
expect(collector.getProgress().collected).toBe(5)
|
|
98
|
+
|
|
99
|
+
collector.reset()
|
|
100
|
+
|
|
101
|
+
expect(collector.getProgress().collected).toBe(0)
|
|
102
|
+
expect(collector.isComplete()).toBe(false)
|
|
103
|
+
})
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
describe('Entropy Generation', () => {
|
|
107
|
+
it('should generate 32 bytes of entropy', () => {
|
|
108
|
+
const collector = new EntropyCollector({ targetSamples: 10, minSampleInterval: 0 })
|
|
109
|
+
|
|
110
|
+
for (let i = 0; i < 10; i++) {
|
|
111
|
+
collector.addMouseSample(Math.random() * 1000, Math.random() * 1000)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const entropy = collector.generateEntropy()
|
|
115
|
+
|
|
116
|
+
expect(entropy).toBeInstanceOf(Uint8Array)
|
|
117
|
+
expect(entropy.length).toBe(32)
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('should generate entropy even with insufficient samples (with warning)', () => {
|
|
121
|
+
const collector = new EntropyCollector({ targetSamples: 100, minSampleInterval: 0 })
|
|
122
|
+
|
|
123
|
+
// Only add a few samples
|
|
124
|
+
for (let i = 0; i < 5; i++) {
|
|
125
|
+
collector.addMouseSample(i, i)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation()
|
|
129
|
+
|
|
130
|
+
const entropy = collector.generateEntropy()
|
|
131
|
+
|
|
132
|
+
expect(entropy.length).toBe(32)
|
|
133
|
+
expect(consoleSpy).toHaveBeenCalled()
|
|
134
|
+
|
|
135
|
+
consoleSpy.mockRestore()
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it('should generate different entropy on each call (due to CSPRNG mixing)', () => {
|
|
139
|
+
const collector = new EntropyCollector({ targetSamples: 10, minSampleInterval: 0 })
|
|
140
|
+
|
|
141
|
+
for (let i = 0; i < 10; i++) {
|
|
142
|
+
collector.addMouseSample(i, i)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const entropy1 = collector.generateEntropy()
|
|
146
|
+
const entropy2 = collector.generateEntropy()
|
|
147
|
+
|
|
148
|
+
// They should be different due to CSPRNG mixing
|
|
149
|
+
expect(Buffer.from(entropy1).toString('hex')).not.toBe(Buffer.from(entropy2).toString('hex'))
|
|
150
|
+
})
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
describe('Entropy Estimation', () => {
|
|
154
|
+
it('should estimate 0 bits with no samples', () => {
|
|
155
|
+
const collector = new EntropyCollector()
|
|
156
|
+
expect(collector.estimateEntropyBits()).toBe(0)
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it('should estimate entropy based on sample count', () => {
|
|
160
|
+
const collector = new EntropyCollector({ targetSamples: 100, minSampleInterval: 0 })
|
|
161
|
+
|
|
162
|
+
for (let i = 0; i < 20; i++) {
|
|
163
|
+
collector.addMouseSample(i, i)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const bits = collector.estimateEntropyBits()
|
|
167
|
+
expect(bits).toBeGreaterThan(0)
|
|
168
|
+
expect(bits).toBeLessThanOrEqual(256) // Capped at 256
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it('should cap entropy estimate at 256 bits', () => {
|
|
172
|
+
const collector = new EntropyCollector({ targetSamples: 1000, minSampleInterval: 0 })
|
|
173
|
+
|
|
174
|
+
for (let i = 0; i < 500; i++) {
|
|
175
|
+
collector.addMouseSample(i, i)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const bits = collector.estimateEntropyBits()
|
|
179
|
+
expect(bits).toBe(256)
|
|
180
|
+
})
|
|
181
|
+
})
|
|
182
|
+
})
|
package/src/index.all.ts
CHANGED
|
@@ -9,7 +9,6 @@ import {
|
|
|
9
9
|
import { Chain, ReqHistoryNote } from './types'
|
|
10
10
|
import { WalletError } from './WalletError'
|
|
11
11
|
import { TableOutput } from '../storage/schema/tables/TableOutput'
|
|
12
|
-
import { ChaintracksServiceClient } from '../services/chaintracker/chaintracks/ChaintracksServiceClient'
|
|
13
12
|
import { ChaintracksClientApi } from '../services/chaintracker/chaintracks/Api/ChaintracksClientApi'
|
|
14
13
|
/**
|
|
15
14
|
* Defines standard interfaces to access functionality implemented by external transaction processing services.
|
|
@@ -365,7 +365,10 @@ export abstract class ChaintracksStorageBase implements ChaintracksStorageQueryA
|
|
|
365
365
|
private async addLiveHeadersToBulk(liveHeaders: LiveBlockHeader[]) {
|
|
366
366
|
if (liveHeaders.length === 0) return
|
|
367
367
|
const lastChainWork = liveHeaders.slice(-1)[0].chainWork
|
|
368
|
-
|
|
368
|
+
const firstHeader = liveHeaders[0]
|
|
369
|
+
const previousWork = subWork(firstHeader.chainWork, convertBitsToWork(liveHeaders[0].bits))
|
|
370
|
+
const incrementalWork = subWork(lastChainWork, previousWork)
|
|
371
|
+
await this.bulkManager.mergeIncrementalBlockHeaders(liveHeaders, incrementalWork)
|
|
369
372
|
}
|
|
370
373
|
}
|
|
371
374
|
|
|
@@ -91,4 +91,139 @@ export class WABClient {
|
|
|
91
91
|
})
|
|
92
92
|
return res.json()
|
|
93
93
|
}
|
|
94
|
+
|
|
95
|
+
// ============================================================
|
|
96
|
+
// Shamir Share Management (2-of-3 Key Recovery System)
|
|
97
|
+
// ============================================================
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Start OTP verification for share operations
|
|
101
|
+
* This initiates the auth flow (e.g., sends SMS code via Twilio)
|
|
102
|
+
*
|
|
103
|
+
* @param methodType The auth method type (e.g., "TwilioPhone", "DevConsole")
|
|
104
|
+
* @param userIdHash SHA256 hash of the user's identity key
|
|
105
|
+
* @param payload Auth method specific data (e.g., { phoneNumber: "+1..." })
|
|
106
|
+
*/
|
|
107
|
+
public async startShareAuth(
|
|
108
|
+
methodType: string,
|
|
109
|
+
userIdHash: string,
|
|
110
|
+
payload: any
|
|
111
|
+
): Promise<{ success: boolean; message: string }> {
|
|
112
|
+
const res = await fetch(`${this.serverUrl}/auth/start`, {
|
|
113
|
+
method: 'POST',
|
|
114
|
+
headers: { 'Content-Type': 'application/json' },
|
|
115
|
+
body: JSON.stringify({
|
|
116
|
+
methodType,
|
|
117
|
+
presentationKey: userIdHash, // Reuse existing auth flow with userIdHash
|
|
118
|
+
payload
|
|
119
|
+
})
|
|
120
|
+
})
|
|
121
|
+
return res.json()
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Store a Shamir share (Share B) on the server
|
|
126
|
+
* Requires prior OTP verification via startShareAuth
|
|
127
|
+
*
|
|
128
|
+
* @param methodType The auth method type used for verification
|
|
129
|
+
* @param payload Contains the OTP code and auth method specific data
|
|
130
|
+
* @param shareB The Shamir share to store (format: x.y.threshold.integrity)
|
|
131
|
+
* @param userIdHash SHA256 hash of the user's identity key
|
|
132
|
+
*/
|
|
133
|
+
public async storeShare(
|
|
134
|
+
methodType: string,
|
|
135
|
+
payload: any,
|
|
136
|
+
shareB: string,
|
|
137
|
+
userIdHash: string
|
|
138
|
+
): Promise<{ success: boolean; message: string; userId?: number }> {
|
|
139
|
+
const res = await fetch(`${this.serverUrl}/share/store`, {
|
|
140
|
+
method: 'POST',
|
|
141
|
+
headers: { 'Content-Type': 'application/json' },
|
|
142
|
+
body: JSON.stringify({
|
|
143
|
+
methodType,
|
|
144
|
+
payload,
|
|
145
|
+
shareB,
|
|
146
|
+
userIdHash
|
|
147
|
+
})
|
|
148
|
+
})
|
|
149
|
+
return res.json()
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Retrieve a Shamir share (Share B) from the server
|
|
154
|
+
* Requires OTP verification
|
|
155
|
+
*
|
|
156
|
+
* @param methodType The auth method type used for verification
|
|
157
|
+
* @param payload Contains the OTP code and auth method specific data
|
|
158
|
+
* @param userIdHash SHA256 hash of the user's identity key
|
|
159
|
+
*/
|
|
160
|
+
public async retrieveShare(
|
|
161
|
+
methodType: string,
|
|
162
|
+
payload: any,
|
|
163
|
+
userIdHash: string
|
|
164
|
+
): Promise<{ success: boolean; shareB?: string; message: string }> {
|
|
165
|
+
const res = await fetch(`${this.serverUrl}/share/retrieve`, {
|
|
166
|
+
method: 'POST',
|
|
167
|
+
headers: { 'Content-Type': 'application/json' },
|
|
168
|
+
body: JSON.stringify({
|
|
169
|
+
methodType,
|
|
170
|
+
payload,
|
|
171
|
+
userIdHash
|
|
172
|
+
})
|
|
173
|
+
})
|
|
174
|
+
return res.json()
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Update a Shamir share (for key rotation)
|
|
179
|
+
* Requires OTP verification
|
|
180
|
+
*
|
|
181
|
+
* @param methodType The auth method type used for verification
|
|
182
|
+
* @param payload Contains the OTP code and auth method specific data
|
|
183
|
+
* @param userIdHash SHA256 hash of the user's identity key
|
|
184
|
+
* @param newShareB The new Shamir share to store
|
|
185
|
+
*/
|
|
186
|
+
public async updateShare(
|
|
187
|
+
methodType: string,
|
|
188
|
+
payload: any,
|
|
189
|
+
userIdHash: string,
|
|
190
|
+
newShareB: string
|
|
191
|
+
): Promise<{ success: boolean; message: string; shareVersion?: number }> {
|
|
192
|
+
const res = await fetch(`${this.serverUrl}/share/update`, {
|
|
193
|
+
method: 'POST',
|
|
194
|
+
headers: { 'Content-Type': 'application/json' },
|
|
195
|
+
body: JSON.stringify({
|
|
196
|
+
methodType,
|
|
197
|
+
payload,
|
|
198
|
+
userIdHash,
|
|
199
|
+
newShareB
|
|
200
|
+
})
|
|
201
|
+
})
|
|
202
|
+
return res.json()
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Delete a Shamir user's account and stored share
|
|
207
|
+
* Requires OTP verification
|
|
208
|
+
*
|
|
209
|
+
* @param methodType The auth method type used for verification
|
|
210
|
+
* @param payload Contains the OTP code and auth method specific data
|
|
211
|
+
* @param userIdHash SHA256 hash of the user's identity key
|
|
212
|
+
*/
|
|
213
|
+
public async deleteShamirUser(
|
|
214
|
+
methodType: string,
|
|
215
|
+
payload: any,
|
|
216
|
+
userIdHash: string
|
|
217
|
+
): Promise<{ success: boolean; message: string }> {
|
|
218
|
+
const res = await fetch(`${this.serverUrl}/share/delete`, {
|
|
219
|
+
method: 'POST',
|
|
220
|
+
headers: { 'Content-Type': 'application/json' },
|
|
221
|
+
body: JSON.stringify({
|
|
222
|
+
methodType,
|
|
223
|
+
payload,
|
|
224
|
+
userIdHash
|
|
225
|
+
})
|
|
226
|
+
})
|
|
227
|
+
return res.json()
|
|
228
|
+
}
|
|
94
229
|
}
|