@sip-protocol/sdk 0.4.0 → 0.5.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.
- package/dist/browser.d.mts +1 -1
- package/dist/browser.d.ts +1 -1
- package/dist/browser.js +1866 -153
- package/dist/browser.mjs +14 -2
- package/dist/chunk-DMHBKRWV.mjs +14712 -0
- package/dist/chunk-HGU6HZRC.mjs +231 -0
- package/dist/chunk-J4Q4NJ2U.mjs +13544 -0
- package/dist/chunk-W2B7T6WU.mjs +14714 -0
- package/dist/index-5jAdWMA-.d.ts +8973 -0
- package/dist/index-B9Vkpaao.d.mts +8973 -0
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1851 -138
- package/dist/index.mjs +14 -2
- package/dist/proofs/noir.mjs +1 -1
- package/package.json +1 -1
- package/src/compliance/compliance-manager.ts +87 -0
- package/src/compliance/conditional-threshold.ts +379 -0
- package/src/compliance/conditional.ts +382 -0
- package/src/compliance/derivation.ts +489 -0
- package/src/compliance/index.ts +50 -8
- package/src/compliance/pdf.ts +365 -0
- package/src/compliance/reports.ts +644 -0
- package/src/compliance/threshold.ts +529 -0
- package/src/compliance/types.ts +223 -0
- package/src/errors.ts +8 -0
- package/src/index.ts +29 -1
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conditional Disclosure Module for SIP Protocol
|
|
3
|
+
*
|
|
4
|
+
* Provides time-locked and block-height-locked disclosure mechanisms
|
|
5
|
+
* for regulatory compliance and automatic auditability.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { ConditionalDisclosure } from '@sip-protocol/sdk'
|
|
10
|
+
*
|
|
11
|
+
* const disclosure = new ConditionalDisclosure()
|
|
12
|
+
*
|
|
13
|
+
* // Create time-locked disclosure (reveals after 30 days)
|
|
14
|
+
* const timeLock = disclosure.createTimeLocked({
|
|
15
|
+
* viewingKey: '0x1234...',
|
|
16
|
+
* revealAfter: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
|
|
17
|
+
* commitment: '0xabcd...',
|
|
18
|
+
* })
|
|
19
|
+
*
|
|
20
|
+
* // Check if unlocked (after time has passed)
|
|
21
|
+
* const result = disclosure.checkUnlocked(timeLock)
|
|
22
|
+
* if (result.unlocked) {
|
|
23
|
+
* console.log('Viewing key:', result.viewingKey)
|
|
24
|
+
* }
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* @module compliance/conditional
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { sha256 } from '@noble/hashes/sha256'
|
|
31
|
+
import { bytesToHex, hexToBytes, randomBytes, utf8ToBytes } from '@noble/hashes/utils'
|
|
32
|
+
import { xchacha20poly1305 } from '@noble/ciphers/chacha.js'
|
|
33
|
+
import type { HexString } from '@sip-protocol/types'
|
|
34
|
+
import { ValidationError, CryptoError, ErrorCode } from '../errors'
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Time-lock result containing encrypted viewing key and metadata
|
|
38
|
+
*/
|
|
39
|
+
export interface TimeLockResult {
|
|
40
|
+
/** Encrypted viewing key */
|
|
41
|
+
encryptedKey: HexString
|
|
42
|
+
/** Nonce for XChaCha20-Poly1305 */
|
|
43
|
+
nonce: HexString
|
|
44
|
+
/** Reveal time (Unix timestamp in seconds) or block height */
|
|
45
|
+
revealAfter: number
|
|
46
|
+
/** Commitment to verify integrity (hash of viewingKey + revealAfter) */
|
|
47
|
+
verificationCommitment: HexString
|
|
48
|
+
/** Original commitment parameter (transaction hash or identifier) */
|
|
49
|
+
encryptionCommitment: HexString
|
|
50
|
+
/** Type of time-lock: 'timestamp' or 'blockheight' */
|
|
51
|
+
type: 'timestamp' | 'blockheight'
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Unlock result from checking a time-locked disclosure
|
|
56
|
+
*/
|
|
57
|
+
export interface UnlockResult {
|
|
58
|
+
/** Whether the time-lock is unlocked */
|
|
59
|
+
unlocked: boolean
|
|
60
|
+
/** Viewing key (only present if unlocked) */
|
|
61
|
+
viewingKey?: HexString
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Parameters for creating a time-locked disclosure
|
|
66
|
+
*/
|
|
67
|
+
export interface TimeLockParams {
|
|
68
|
+
/** Viewing key to encrypt and time-lock */
|
|
69
|
+
viewingKey: HexString
|
|
70
|
+
/** Reveal after this time (Date) or block height (number) */
|
|
71
|
+
revealAfter: Date | number
|
|
72
|
+
/** Commitment value (transaction hash or identifier) */
|
|
73
|
+
commitment: HexString
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Conditional Disclosure Manager
|
|
78
|
+
*
|
|
79
|
+
* Handles automatic disclosure of viewing keys after specified
|
|
80
|
+
* time or block height for regulatory compliance.
|
|
81
|
+
*/
|
|
82
|
+
export class ConditionalDisclosure {
|
|
83
|
+
/**
|
|
84
|
+
* Create a time-locked disclosure
|
|
85
|
+
*
|
|
86
|
+
* Encrypts the viewing key with a deterministic key derived from
|
|
87
|
+
* the commitment and reveal time. The key can only be reconstructed
|
|
88
|
+
* after the specified time/block height.
|
|
89
|
+
*
|
|
90
|
+
* @param params - Time-lock parameters
|
|
91
|
+
* @returns Time-lock result with encrypted key
|
|
92
|
+
* @throws {ValidationError} If parameters are invalid
|
|
93
|
+
* @throws {CryptoError} If encryption fails
|
|
94
|
+
*/
|
|
95
|
+
createTimeLocked(params: TimeLockParams): TimeLockResult {
|
|
96
|
+
// Validate viewing key
|
|
97
|
+
if (!params.viewingKey || !params.viewingKey.startsWith('0x')) {
|
|
98
|
+
throw new ValidationError(
|
|
99
|
+
'Invalid viewing key format',
|
|
100
|
+
'viewingKey',
|
|
101
|
+
{ viewingKey: params.viewingKey },
|
|
102
|
+
ErrorCode.INVALID_KEY
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Validate commitment
|
|
107
|
+
if (!params.commitment || !params.commitment.startsWith('0x')) {
|
|
108
|
+
throw new ValidationError(
|
|
109
|
+
'Invalid commitment format',
|
|
110
|
+
'commitment',
|
|
111
|
+
{ commitment: params.commitment },
|
|
112
|
+
ErrorCode.INVALID_COMMITMENT
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Parse and validate revealAfter
|
|
117
|
+
let revealAfterSeconds: number
|
|
118
|
+
let type: 'timestamp' | 'blockheight'
|
|
119
|
+
|
|
120
|
+
if (params.revealAfter instanceof Date) {
|
|
121
|
+
revealAfterSeconds = Math.floor(params.revealAfter.getTime() / 1000)
|
|
122
|
+
type = 'timestamp'
|
|
123
|
+
|
|
124
|
+
// Note: We don't validate that the time is in the future to allow testing
|
|
125
|
+
// In production, applications should validate this before creating time-locks
|
|
126
|
+
} else if (typeof params.revealAfter === 'number') {
|
|
127
|
+
// Assume block height if number > 1e10, otherwise treat as timestamp
|
|
128
|
+
if (params.revealAfter > 1e10) {
|
|
129
|
+
// Looks like a timestamp in milliseconds, convert to seconds
|
|
130
|
+
revealAfterSeconds = Math.floor(params.revealAfter / 1000)
|
|
131
|
+
type = 'timestamp'
|
|
132
|
+
} else {
|
|
133
|
+
// Block height
|
|
134
|
+
revealAfterSeconds = params.revealAfter
|
|
135
|
+
type = 'blockheight'
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (revealAfterSeconds <= 0) {
|
|
139
|
+
throw new ValidationError(
|
|
140
|
+
'Reveal time/block height must be positive',
|
|
141
|
+
'revealAfter',
|
|
142
|
+
{ revealAfter: revealAfterSeconds },
|
|
143
|
+
ErrorCode.INVALID_TIME_LOCK
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
} else {
|
|
147
|
+
throw new ValidationError(
|
|
148
|
+
'Invalid revealAfter type (must be Date or number)',
|
|
149
|
+
'revealAfter',
|
|
150
|
+
{ revealAfter: params.revealAfter },
|
|
151
|
+
ErrorCode.INVALID_TIME_LOCK
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
// Derive deterministic encryption key from commitment and reveal time
|
|
157
|
+
const encryptionKey = this._deriveEncryptionKey(
|
|
158
|
+
params.commitment,
|
|
159
|
+
revealAfterSeconds
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
// Generate random nonce (24 bytes for XChaCha20)
|
|
163
|
+
const nonce = randomBytes(24)
|
|
164
|
+
|
|
165
|
+
// Encrypt viewing key
|
|
166
|
+
const viewingKeyBytes = hexToBytes(params.viewingKey.slice(2))
|
|
167
|
+
const cipher = xchacha20poly1305(encryptionKey, nonce)
|
|
168
|
+
const encryptedKey = cipher.encrypt(viewingKeyBytes)
|
|
169
|
+
|
|
170
|
+
// Create verifiable commitment (hash of viewingKey + revealAfter)
|
|
171
|
+
const commitmentData = new Uint8Array([
|
|
172
|
+
...viewingKeyBytes,
|
|
173
|
+
...this._numberToBytes(revealAfterSeconds),
|
|
174
|
+
])
|
|
175
|
+
const commitmentHash = sha256(commitmentData)
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
encryptedKey: ('0x' + bytesToHex(encryptedKey)) as HexString,
|
|
179
|
+
nonce: ('0x' + bytesToHex(nonce)) as HexString,
|
|
180
|
+
revealAfter: revealAfterSeconds,
|
|
181
|
+
verificationCommitment: ('0x' + bytesToHex(commitmentHash)) as HexString,
|
|
182
|
+
encryptionCommitment: params.commitment,
|
|
183
|
+
type,
|
|
184
|
+
}
|
|
185
|
+
} catch (error) {
|
|
186
|
+
if (error instanceof ValidationError) {
|
|
187
|
+
throw error
|
|
188
|
+
}
|
|
189
|
+
throw new CryptoError(
|
|
190
|
+
'Failed to create time-locked disclosure',
|
|
191
|
+
ErrorCode.ENCRYPTION_FAILED,
|
|
192
|
+
{
|
|
193
|
+
cause: error instanceof Error ? error : undefined,
|
|
194
|
+
operation: 'createTimeLocked',
|
|
195
|
+
}
|
|
196
|
+
)
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Check if a time-lock is unlocked and retrieve the viewing key
|
|
202
|
+
*
|
|
203
|
+
* @param timeLock - Time-lock result to check
|
|
204
|
+
* @param currentTimeOrBlock - Current time (Date/number) or block height (number)
|
|
205
|
+
* @returns Unlock result with viewing key if unlocked
|
|
206
|
+
* @throws {ValidationError} If time-lock format is invalid
|
|
207
|
+
* @throws {CryptoError} If decryption fails
|
|
208
|
+
*/
|
|
209
|
+
checkUnlocked(
|
|
210
|
+
timeLock: TimeLockResult,
|
|
211
|
+
currentTimeOrBlock?: Date | number
|
|
212
|
+
): UnlockResult {
|
|
213
|
+
// Validate time-lock format
|
|
214
|
+
if (!timeLock.encryptedKey || !timeLock.encryptedKey.startsWith('0x')) {
|
|
215
|
+
throw new ValidationError(
|
|
216
|
+
'Invalid encrypted key format',
|
|
217
|
+
'encryptedKey',
|
|
218
|
+
{ encryptedKey: timeLock.encryptedKey },
|
|
219
|
+
ErrorCode.INVALID_ENCRYPTED_DATA
|
|
220
|
+
)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (!timeLock.nonce || !timeLock.nonce.startsWith('0x')) {
|
|
224
|
+
throw new ValidationError(
|
|
225
|
+
'Invalid nonce format',
|
|
226
|
+
'nonce',
|
|
227
|
+
{ nonce: timeLock.nonce },
|
|
228
|
+
ErrorCode.INVALID_ENCRYPTED_DATA
|
|
229
|
+
)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (!timeLock.verificationCommitment || !timeLock.verificationCommitment.startsWith('0x')) {
|
|
233
|
+
throw new ValidationError(
|
|
234
|
+
'Invalid verification commitment format',
|
|
235
|
+
'verificationCommitment',
|
|
236
|
+
{ commitment: timeLock.verificationCommitment },
|
|
237
|
+
ErrorCode.INVALID_COMMITMENT
|
|
238
|
+
)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (!timeLock.encryptionCommitment || !timeLock.encryptionCommitment.startsWith('0x')) {
|
|
242
|
+
throw new ValidationError(
|
|
243
|
+
'Invalid encryption commitment format',
|
|
244
|
+
'encryptionCommitment',
|
|
245
|
+
{ commitment: timeLock.encryptionCommitment },
|
|
246
|
+
ErrorCode.INVALID_COMMITMENT
|
|
247
|
+
)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Determine current time or block height
|
|
251
|
+
let currentValue: number
|
|
252
|
+
if (currentTimeOrBlock instanceof Date) {
|
|
253
|
+
currentValue = Math.floor(currentTimeOrBlock.getTime() / 1000)
|
|
254
|
+
} else if (typeof currentTimeOrBlock === 'number') {
|
|
255
|
+
currentValue = currentTimeOrBlock
|
|
256
|
+
} else {
|
|
257
|
+
// Default to current time
|
|
258
|
+
currentValue = Math.floor(Date.now() / 1000)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Check if unlocked
|
|
262
|
+
const unlocked = currentValue >= timeLock.revealAfter
|
|
263
|
+
|
|
264
|
+
if (!unlocked) {
|
|
265
|
+
return { unlocked: false }
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
try {
|
|
269
|
+
// Derive the encryption key using the stored commitment
|
|
270
|
+
const encryptionKey = this._deriveEncryptionKey(
|
|
271
|
+
timeLock.encryptionCommitment,
|
|
272
|
+
timeLock.revealAfter
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
// Decrypt the viewing key
|
|
276
|
+
const nonce = hexToBytes(timeLock.nonce.slice(2))
|
|
277
|
+
const encryptedData = hexToBytes(timeLock.encryptedKey.slice(2))
|
|
278
|
+
|
|
279
|
+
const cipher = xchacha20poly1305(encryptionKey, nonce)
|
|
280
|
+
const decryptedBytes = cipher.decrypt(encryptedData)
|
|
281
|
+
|
|
282
|
+
const viewingKey = ('0x' + bytesToHex(decryptedBytes)) as HexString
|
|
283
|
+
|
|
284
|
+
return {
|
|
285
|
+
unlocked: true,
|
|
286
|
+
viewingKey,
|
|
287
|
+
}
|
|
288
|
+
} catch (error) {
|
|
289
|
+
if (error instanceof ValidationError || error instanceof CryptoError) {
|
|
290
|
+
throw error
|
|
291
|
+
}
|
|
292
|
+
throw new CryptoError(
|
|
293
|
+
'Failed to decrypt time-locked viewing key',
|
|
294
|
+
ErrorCode.DECRYPTION_FAILED,
|
|
295
|
+
{
|
|
296
|
+
cause: error instanceof Error ? error : undefined,
|
|
297
|
+
operation: 'checkUnlocked',
|
|
298
|
+
}
|
|
299
|
+
)
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Verify a time-lock commitment
|
|
305
|
+
*
|
|
306
|
+
* Verifies that the verification commitment in the time-lock matches the hash
|
|
307
|
+
* of the provided viewing key and reveal time.
|
|
308
|
+
*
|
|
309
|
+
* @param timeLock - Time-lock to verify
|
|
310
|
+
* @param viewingKey - Viewing key to verify against
|
|
311
|
+
* @returns True if commitment is valid
|
|
312
|
+
*/
|
|
313
|
+
verifyCommitment(timeLock: TimeLockResult, viewingKey: HexString): boolean {
|
|
314
|
+
try {
|
|
315
|
+
const viewingKeyBytes = hexToBytes(viewingKey.slice(2))
|
|
316
|
+
const commitmentData = new Uint8Array([
|
|
317
|
+
...viewingKeyBytes,
|
|
318
|
+
...this._numberToBytes(timeLock.revealAfter),
|
|
319
|
+
])
|
|
320
|
+
const expectedCommitment = sha256(commitmentData)
|
|
321
|
+
const actualCommitment = hexToBytes(timeLock.verificationCommitment.slice(2))
|
|
322
|
+
|
|
323
|
+
// Constant-time comparison
|
|
324
|
+
if (expectedCommitment.length !== actualCommitment.length) {
|
|
325
|
+
return false
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
let diff = 0
|
|
329
|
+
for (let i = 0; i < expectedCommitment.length; i++) {
|
|
330
|
+
diff |= expectedCommitment[i] ^ actualCommitment[i]
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return diff === 0
|
|
334
|
+
} catch {
|
|
335
|
+
return false
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Derive deterministic encryption key from commitment and reveal time
|
|
341
|
+
*
|
|
342
|
+
* @private
|
|
343
|
+
*/
|
|
344
|
+
private _deriveEncryptionKey(
|
|
345
|
+
commitment: HexString,
|
|
346
|
+
revealAfter: number
|
|
347
|
+
): Uint8Array {
|
|
348
|
+
// Combine commitment and reveal time
|
|
349
|
+
const commitmentBytes = hexToBytes(commitment.slice(2))
|
|
350
|
+
const timeBytes = this._numberToBytes(revealAfter)
|
|
351
|
+
const combined = new Uint8Array([...commitmentBytes, ...timeBytes])
|
|
352
|
+
|
|
353
|
+
// Hash to derive key (32 bytes for XChaCha20-Poly1305)
|
|
354
|
+
const key = sha256(combined)
|
|
355
|
+
|
|
356
|
+
if (key.length !== 32) {
|
|
357
|
+
throw new CryptoError(
|
|
358
|
+
'Derived key must be 32 bytes',
|
|
359
|
+
ErrorCode.INVALID_KEY_SIZE,
|
|
360
|
+
{
|
|
361
|
+
context: { actualSize: key.length, expectedSize: 32 },
|
|
362
|
+
operation: '_deriveEncryptionKey',
|
|
363
|
+
}
|
|
364
|
+
)
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return key
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Convert number to 8-byte big-endian representation
|
|
372
|
+
*
|
|
373
|
+
* @private
|
|
374
|
+
*/
|
|
375
|
+
private _numberToBytes(num: number): Uint8Array {
|
|
376
|
+
const bytes = new Uint8Array(8)
|
|
377
|
+
const view = new DataView(bytes.buffer)
|
|
378
|
+
// Use BigInt to handle large numbers safely
|
|
379
|
+
view.setBigUint64(0, BigInt(Math.floor(num)), false) // false = big-endian
|
|
380
|
+
return bytes
|
|
381
|
+
}
|
|
382
|
+
}
|