@omnituum/pqc-shared 0.2.6

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/LICENSE +22 -0
  2. package/README.md +543 -0
  3. package/dist/crypto/index.cjs +807 -0
  4. package/dist/crypto/index.d.cts +641 -0
  5. package/dist/crypto/index.d.ts +641 -0
  6. package/dist/crypto/index.js +716 -0
  7. package/dist/decrypt-eSHlbh1j.d.cts +321 -0
  8. package/dist/decrypt-eSHlbh1j.d.ts +321 -0
  9. package/dist/fs/index.cjs +1168 -0
  10. package/dist/fs/index.d.cts +400 -0
  11. package/dist/fs/index.d.ts +400 -0
  12. package/dist/fs/index.js +1091 -0
  13. package/dist/index.cjs +2160 -0
  14. package/dist/index.d.cts +282 -0
  15. package/dist/index.d.ts +282 -0
  16. package/dist/index.js +2031 -0
  17. package/dist/integrity-CCYjrap3.d.ts +31 -0
  18. package/dist/integrity-Dx9jukMH.d.cts +31 -0
  19. package/dist/types-61c7Q9ri.d.ts +134 -0
  20. package/dist/types-Ch0y-n7K.d.cts +134 -0
  21. package/dist/utils/index.cjs +129 -0
  22. package/dist/utils/index.d.cts +49 -0
  23. package/dist/utils/index.d.ts +49 -0
  24. package/dist/utils/index.js +114 -0
  25. package/dist/vault/index.cjs +713 -0
  26. package/dist/vault/index.d.cts +237 -0
  27. package/dist/vault/index.d.ts +237 -0
  28. package/dist/vault/index.js +677 -0
  29. package/dist/version-BygzPVGs.d.cts +55 -0
  30. package/dist/version-BygzPVGs.d.ts +55 -0
  31. package/package.json +86 -0
  32. package/src/crypto/dilithium.ts +233 -0
  33. package/src/crypto/hybrid.ts +358 -0
  34. package/src/crypto/index.ts +181 -0
  35. package/src/crypto/kyber.ts +199 -0
  36. package/src/crypto/nacl.ts +204 -0
  37. package/src/crypto/primitives/blake3.ts +141 -0
  38. package/src/crypto/primitives/chacha.ts +211 -0
  39. package/src/crypto/primitives/hkdf.ts +192 -0
  40. package/src/crypto/primitives/index.ts +54 -0
  41. package/src/crypto/primitives.ts +144 -0
  42. package/src/crypto/x25519.ts +134 -0
  43. package/src/fs/aes.ts +343 -0
  44. package/src/fs/argon2.ts +184 -0
  45. package/src/fs/browser.ts +408 -0
  46. package/src/fs/decrypt.ts +320 -0
  47. package/src/fs/encrypt.ts +324 -0
  48. package/src/fs/format.ts +425 -0
  49. package/src/fs/index.ts +144 -0
  50. package/src/fs/types.ts +304 -0
  51. package/src/index.ts +414 -0
  52. package/src/kdf/index.ts +311 -0
  53. package/src/runtime/crypto.ts +16 -0
  54. package/src/security/index.ts +345 -0
  55. package/src/tunnel/index.ts +39 -0
  56. package/src/tunnel/session.ts +229 -0
  57. package/src/tunnel/types.ts +115 -0
  58. package/src/utils/entropy.ts +128 -0
  59. package/src/utils/index.ts +25 -0
  60. package/src/utils/integrity.ts +95 -0
  61. package/src/vault/decrypt.ts +167 -0
  62. package/src/vault/encrypt.ts +207 -0
  63. package/src/vault/index.ts +71 -0
  64. package/src/vault/manager.ts +327 -0
  65. package/src/vault/migrate.ts +190 -0
  66. package/src/vault/types.ts +177 -0
  67. package/src/version.ts +304 -0
@@ -0,0 +1,311 @@
1
+ /**
2
+ * Omnituum PQC Shared - Unified Key Derivation
3
+ *
4
+ * Single source of truth for password-based key derivation.
5
+ * Supports both legacy PBKDF2 (for backwards compatibility) and
6
+ * Argon2id (recommended for new implementations).
7
+ *
8
+ * Security Levels:
9
+ * - PBKDF2-SHA256: 600K iterations (OWASP 2023)
10
+ * - Argon2id: 64MB memory, 3 iterations, 4 parallelism (OWASP 2024)
11
+ */
12
+
13
+ import { argon2id } from 'hash-wasm';
14
+ import { randN } from '../crypto/primitives';
15
+
16
+ // ═══════════════════════════════════════════════════════════════════════════
17
+ // KDF TYPES
18
+ // ═══════════════════════════════════════════════════════════════════════════
19
+
20
+ export type KDFAlgorithm = 'PBKDF2-SHA256' | 'Argon2id';
21
+
22
+ export interface KDFConfig {
23
+ algorithm: KDFAlgorithm;
24
+ // PBKDF2 params
25
+ pbkdf2Iterations?: number;
26
+ // Argon2id params
27
+ argon2MemoryCost?: number; // KiB
28
+ argon2TimeCost?: number;
29
+ argon2Parallelism?: number;
30
+ // Common
31
+ saltLength: number;
32
+ hashLength: number;
33
+ }
34
+
35
+ export interface KDFResult {
36
+ key: Uint8Array;
37
+ salt: Uint8Array;
38
+ algorithm: KDFAlgorithm;
39
+ params: Record<string, number>;
40
+ }
41
+
42
+ // ═══════════════════════════════════════════════════════════════════════════
43
+ // DEFAULT CONFIGURATIONS
44
+ // ═══════════════════════════════════════════════════════════════════════════
45
+
46
+ /** Legacy PBKDF2 config (for existing vaults) */
47
+ export const KDF_CONFIG_PBKDF2: KDFConfig = {
48
+ algorithm: 'PBKDF2-SHA256',
49
+ pbkdf2Iterations: 600000, // OWASP 2023
50
+ saltLength: 32,
51
+ hashLength: 32,
52
+ };
53
+
54
+ /** Modern Argon2id config (recommended for new vaults) */
55
+ export const KDF_CONFIG_ARGON2ID: KDFConfig = {
56
+ algorithm: 'Argon2id',
57
+ argon2MemoryCost: 65536, // 64 MB
58
+ argon2TimeCost: 3,
59
+ argon2Parallelism: 4,
60
+ saltLength: 32,
61
+ hashLength: 32,
62
+ };
63
+
64
+ /** Low-memory Argon2id config (for constrained environments) */
65
+ export const KDF_CONFIG_ARGON2ID_LOW_MEMORY: KDFConfig = {
66
+ algorithm: 'Argon2id',
67
+ argon2MemoryCost: 19456, // 19 MB
68
+ argon2TimeCost: 2,
69
+ argon2Parallelism: 1,
70
+ saltLength: 32,
71
+ hashLength: 32,
72
+ };
73
+
74
+ /** Current default - can be changed for migration */
75
+ export const KDF_CONFIG_DEFAULT = KDF_CONFIG_PBKDF2;
76
+
77
+ // ═══════════════════════════════════════════════════════════════════════════
78
+ // PBKDF2 IMPLEMENTATION
79
+ // ═══════════════════════════════════════════════════════════════════════════
80
+
81
+ const textEncoder = new TextEncoder();
82
+
83
+ async function derivePBKDF2(
84
+ password: string,
85
+ salt: Uint8Array,
86
+ iterations: number,
87
+ hashLength: number
88
+ ): Promise<Uint8Array> {
89
+ const passwordKey = await globalThis.crypto.subtle.importKey(
90
+ 'raw',
91
+ textEncoder.encode(password),
92
+ 'PBKDF2',
93
+ false,
94
+ ['deriveBits']
95
+ );
96
+
97
+ const saltBuffer = new ArrayBuffer(salt.length);
98
+ new Uint8Array(saltBuffer).set(salt);
99
+
100
+ const bits = await globalThis.crypto.subtle.deriveBits(
101
+ {
102
+ name: 'PBKDF2',
103
+ salt: saltBuffer,
104
+ iterations,
105
+ hash: 'SHA-256',
106
+ },
107
+ passwordKey,
108
+ hashLength * 8
109
+ );
110
+
111
+ return new Uint8Array(bits);
112
+ }
113
+
114
+ // ═══════════════════════════════════════════════════════════════════════════
115
+ // ARGON2ID IMPLEMENTATION
116
+ // ═══════════════════════════════════════════════════════════════════════════
117
+
118
+ async function deriveArgon2id(
119
+ password: string,
120
+ salt: Uint8Array,
121
+ memoryCost: number,
122
+ timeCost: number,
123
+ parallelism: number,
124
+ hashLength: number
125
+ ): Promise<Uint8Array> {
126
+ const hash = await argon2id({
127
+ password,
128
+ salt,
129
+ parallelism,
130
+ iterations: timeCost,
131
+ memorySize: memoryCost,
132
+ hashLength,
133
+ outputType: 'binary',
134
+ });
135
+
136
+ return new Uint8Array(hash);
137
+ }
138
+
139
+ // ═══════════════════════════════════════════════════════════════════════════
140
+ // UNIFIED API
141
+ // ═══════════════════════════════════════════════════════════════════════════
142
+
143
+ /**
144
+ * Generate a cryptographically secure salt.
145
+ */
146
+ export function generateSalt(length: number = 32): Uint8Array {
147
+ return randN(length);
148
+ }
149
+
150
+ /**
151
+ * Derive a key from a password using the specified KDF configuration.
152
+ *
153
+ * @param password - User password
154
+ * @param salt - Random salt (use generateSalt())
155
+ * @param config - KDF configuration
156
+ * @returns Derived key as Uint8Array
157
+ */
158
+ export async function kdfDeriveKey(
159
+ password: string,
160
+ salt: Uint8Array,
161
+ config: KDFConfig = KDF_CONFIG_DEFAULT
162
+ ): Promise<Uint8Array> {
163
+ if (salt.length !== config.saltLength) {
164
+ throw new Error(`Salt must be ${config.saltLength} bytes, got ${salt.length}`);
165
+ }
166
+
167
+ if (config.algorithm === 'PBKDF2-SHA256') {
168
+ return derivePBKDF2(
169
+ password,
170
+ salt,
171
+ config.pbkdf2Iterations ?? 600000,
172
+ config.hashLength
173
+ );
174
+ } else if (config.algorithm === 'Argon2id') {
175
+ return deriveArgon2id(
176
+ password,
177
+ salt,
178
+ config.argon2MemoryCost ?? 65536,
179
+ config.argon2TimeCost ?? 3,
180
+ config.argon2Parallelism ?? 4,
181
+ config.hashLength
182
+ );
183
+ } else {
184
+ throw new Error(`Unsupported KDF algorithm: ${config.algorithm}`);
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Derive a key and return full result with params.
190
+ * Useful for storing KDF metadata alongside encrypted data.
191
+ */
192
+ export async function kdfDeriveKeyWithParams(
193
+ password: string,
194
+ config: KDFConfig = KDF_CONFIG_DEFAULT
195
+ ): Promise<KDFResult> {
196
+ const salt = generateSalt(config.saltLength);
197
+ const key = await kdfDeriveKey(password, salt, config);
198
+
199
+ const params: Record<string, number> = {};
200
+
201
+ if (config.algorithm === 'PBKDF2-SHA256') {
202
+ params.iterations = config.pbkdf2Iterations ?? 600000;
203
+ } else if (config.algorithm === 'Argon2id') {
204
+ params.memoryCost = config.argon2MemoryCost ?? 65536;
205
+ params.timeCost = config.argon2TimeCost ?? 3;
206
+ params.parallelism = config.argon2Parallelism ?? 4;
207
+ }
208
+
209
+ return {
210
+ key,
211
+ salt,
212
+ algorithm: config.algorithm,
213
+ params,
214
+ };
215
+ }
216
+
217
+ /**
218
+ * Reconstruct KDF config from stored parameters.
219
+ */
220
+ export function configFromParams(
221
+ algorithm: KDFAlgorithm,
222
+ params: Record<string, number>
223
+ ): KDFConfig {
224
+ if (algorithm === 'PBKDF2-SHA256') {
225
+ return {
226
+ algorithm,
227
+ pbkdf2Iterations: params.iterations,
228
+ saltLength: 32,
229
+ hashLength: 32,
230
+ };
231
+ } else if (algorithm === 'Argon2id') {
232
+ return {
233
+ algorithm,
234
+ argon2MemoryCost: params.memoryCost,
235
+ argon2TimeCost: params.timeCost,
236
+ argon2Parallelism: params.parallelism,
237
+ saltLength: 32,
238
+ hashLength: 32,
239
+ };
240
+ } else {
241
+ throw new Error(`Unsupported KDF algorithm: ${algorithm}`);
242
+ }
243
+ }
244
+
245
+ // ═══════════════════════════════════════════════════════════════════════════
246
+ // AVAILABILITY CHECKS
247
+ // ═══════════════════════════════════════════════════════════════════════════
248
+
249
+ let _argon2Available: boolean | null = null;
250
+
251
+ /**
252
+ * Check if Argon2id is available in current environment.
253
+ */
254
+ export async function isArgon2idAvailable(): Promise<boolean> {
255
+ if (_argon2Available !== null) {
256
+ return _argon2Available;
257
+ }
258
+
259
+ try {
260
+ await argon2id({
261
+ password: 'test',
262
+ salt: new Uint8Array(16),
263
+ parallelism: 1,
264
+ iterations: 1,
265
+ memorySize: 1024,
266
+ hashLength: 32,
267
+ outputType: 'binary',
268
+ });
269
+ _argon2Available = true;
270
+ } catch {
271
+ _argon2Available = false;
272
+ }
273
+
274
+ return _argon2Available;
275
+ }
276
+
277
+ /**
278
+ * Check if PBKDF2 is available (always true in Web Crypto environments).
279
+ */
280
+ export function isPBKDF2Available(): boolean {
281
+ return typeof globalThis.crypto !== 'undefined' && typeof globalThis.crypto.subtle !== 'undefined';
282
+ }
283
+
284
+ /**
285
+ * Get the recommended KDF config based on environment capabilities.
286
+ */
287
+ export async function getRecommendedConfig(): Promise<KDFConfig> {
288
+ if (await isArgon2idAvailable()) {
289
+ return KDF_CONFIG_ARGON2ID;
290
+ }
291
+ return KDF_CONFIG_PBKDF2;
292
+ }
293
+
294
+ // ═══════════════════════════════════════════════════════════════════════════
295
+ // BENCHMARKING
296
+ // ═══════════════════════════════════════════════════════════════════════════
297
+
298
+ /**
299
+ * Benchmark a KDF configuration.
300
+ * @returns Time in milliseconds
301
+ */
302
+ export async function benchmarkKDF(config: KDFConfig = KDF_CONFIG_DEFAULT): Promise<number> {
303
+ const salt = generateSalt(config.saltLength);
304
+ const testPassword = 'benchmark-test-password';
305
+
306
+ const start = performance.now();
307
+ await kdfDeriveKey(testPassword, salt, config);
308
+ const end = performance.now();
309
+
310
+ return end - start;
311
+ }
@@ -0,0 +1,16 @@
1
+ // src/runtime/crypto.ts
2
+ // Ensure globalThis.crypto exists across Node + browsers.
3
+ // Uses dynamic import so bundlers don't try to resolve Node built-ins.
4
+
5
+ export async function ensureCrypto(): Promise<void> {
6
+ if (typeof globalThis.crypto !== 'undefined') return;
7
+
8
+ // Node: attach WebCrypto at runtime
9
+ const mod: any = await import('node:crypto');
10
+ const webcrypto = mod.webcrypto ?? mod.default?.webcrypto;
11
+ if (!webcrypto) throw new Error('WebCrypto not available in this Node runtime');
12
+ (globalThis as any).crypto = webcrypto as unknown as Crypto;
13
+ }
14
+
15
+ // fire-and-forget (works for your library init pattern)
16
+ void ensureCrypto();
@@ -0,0 +1,345 @@
1
+ /**
2
+ * Omnituum PQC Shared - Security Utilities
3
+ *
4
+ * Memory hygiene, secure comparison, and session management utilities.
5
+ * These are critical for enterprise credibility and threat model legitimacy.
6
+ */
7
+
8
+ // ═══════════════════════════════════════════════════════════════════════════
9
+ // MEMORY ZEROING
10
+ // ═══════════════════════════════════════════════════════════════════════════
11
+
12
+ /**
13
+ * Securely zero out a Uint8Array to prevent sensitive data from lingering in memory.
14
+ *
15
+ * Note: JavaScript garbage collection may still leave copies. This is a best-effort
16
+ * approach for browser environments. For maximum security, use Web Assembly or
17
+ * native code.
18
+ *
19
+ * @param arr - Array to zero
20
+ */
21
+ export function zeroMemory(arr: Uint8Array): void {
22
+ if (!arr || arr.length === 0) return;
23
+
24
+ // Fill with zeros
25
+ arr.fill(0);
26
+
27
+ // Try to prevent optimizer from removing the fill
28
+ // This is a best-effort approach
29
+ if (arr[0] !== 0) {
30
+ throw new Error('Memory zeroing failed');
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Zero multiple arrays at once.
36
+ *
37
+ * @param arrays - Arrays to zero
38
+ */
39
+ export function zeroAll(...arrays: (Uint8Array | null | undefined)[]): void {
40
+ for (const arr of arrays) {
41
+ if (arr) {
42
+ zeroMemory(arr);
43
+ }
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Execute a function and zero the result after a callback processes it.
49
+ * Ensures sensitive data is cleared even if callback throws.
50
+ *
51
+ * @param getData - Function that returns sensitive data
52
+ * @param process - Function to process the data
53
+ * @returns Result of process function
54
+ */
55
+ export async function withSecureData<T, R>(
56
+ getData: () => Promise<Uint8Array>,
57
+ process: (data: Uint8Array) => Promise<R>
58
+ ): Promise<R> {
59
+ let data: Uint8Array | null = null;
60
+ try {
61
+ data = await getData();
62
+ return await process(data);
63
+ } finally {
64
+ if (data) {
65
+ zeroMemory(data);
66
+ }
67
+ }
68
+ }
69
+
70
+ // ═══════════════════════════════════════════════════════════════════════════
71
+ // CONSTANT-TIME COMPARISON
72
+ // ═══════════════════════════════════════════════════════════════════════════
73
+
74
+ /**
75
+ * Compare two byte arrays in constant time to prevent timing attacks.
76
+ *
77
+ * @param a - First array
78
+ * @param b - Second array
79
+ * @returns true if arrays are equal
80
+ */
81
+ export function constantTimeEqual(a: Uint8Array, b: Uint8Array): boolean {
82
+ if (a.length !== b.length) {
83
+ return false;
84
+ }
85
+
86
+ let result = 0;
87
+ for (let i = 0; i < a.length; i++) {
88
+ result |= a[i] ^ b[i];
89
+ }
90
+
91
+ return result === 0;
92
+ }
93
+
94
+ /**
95
+ * Compare two strings in constant time.
96
+ *
97
+ * @param a - First string
98
+ * @param b - Second string
99
+ * @returns true if strings are equal
100
+ */
101
+ export function constantTimeStringEqual(a: string, b: string): boolean {
102
+ const encoder = new TextEncoder();
103
+ return constantTimeEqual(encoder.encode(a), encoder.encode(b));
104
+ }
105
+
106
+ // ═══════════════════════════════════════════════════════════════════════════
107
+ // SESSION MANAGEMENT
108
+ // ═══════════════════════════════════════════════════════════════════════════
109
+
110
+ export type UnlockReason =
111
+ | 'password' // User entered password
112
+ | 'biometric' // Biometric authentication (future)
113
+ | 'hardware_key' // Hardware security key (future)
114
+ | 'session_restore' // Restored from saved session
115
+ | 'api_token' // API token authentication
116
+ | 'unknown';
117
+
118
+ export interface SecureSession {
119
+ /** Session is currently unlocked */
120
+ unlocked: boolean;
121
+
122
+ /** Timestamp when session was unlocked (ms since epoch) */
123
+ unlockedAt: number | null;
124
+
125
+ /** Session timeout in milliseconds (0 = never) */
126
+ timeoutMs: number;
127
+
128
+ /** How the session was unlocked */
129
+ unlockReason: UnlockReason | null;
130
+
131
+ /** Optional session identifier */
132
+ sessionId: string | null;
133
+
134
+ /** Last activity timestamp (ms since epoch) */
135
+ lastActivityAt: number | null;
136
+
137
+ /** Number of failed unlock attempts */
138
+ failedAttempts: number;
139
+
140
+ /** Lockout until timestamp (ms since epoch) if too many failed attempts */
141
+ lockedOutUntil: number | null;
142
+ }
143
+
144
+ /**
145
+ * Create a new locked session.
146
+ */
147
+ export function createSession(timeoutMs: number = 15 * 60 * 1000): SecureSession {
148
+ return {
149
+ unlocked: false,
150
+ unlockedAt: null,
151
+ timeoutMs,
152
+ unlockReason: null,
153
+ sessionId: null,
154
+ lastActivityAt: null,
155
+ failedAttempts: 0,
156
+ lockedOutUntil: null,
157
+ };
158
+ }
159
+
160
+ /**
161
+ * Unlock a secure session.
162
+ */
163
+ export function unlockSecureSession(
164
+ session: SecureSession,
165
+ reason: UnlockReason = 'password'
166
+ ): SecureSession {
167
+ const now = Date.now();
168
+ return {
169
+ ...session,
170
+ unlocked: true,
171
+ unlockedAt: now,
172
+ unlockReason: reason,
173
+ sessionId: generateSessionId(),
174
+ lastActivityAt: now,
175
+ failedAttempts: 0,
176
+ lockedOutUntil: null,
177
+ };
178
+ }
179
+
180
+ /**
181
+ * Lock a secure session and clear sensitive state.
182
+ */
183
+ export function lockSecureSession(session: SecureSession): SecureSession {
184
+ return {
185
+ ...session,
186
+ unlocked: false,
187
+ unlockedAt: null,
188
+ unlockReason: null,
189
+ sessionId: null,
190
+ lastActivityAt: null,
191
+ };
192
+ }
193
+
194
+ /**
195
+ * Record session activity (resets timeout).
196
+ */
197
+ export function touchSession(session: SecureSession): SecureSession {
198
+ if (!session.unlocked) {
199
+ return session;
200
+ }
201
+ return {
202
+ ...session,
203
+ lastActivityAt: Date.now(),
204
+ };
205
+ }
206
+
207
+ /**
208
+ * Check if session has timed out.
209
+ */
210
+ export function isSessionTimedOut(session: SecureSession): boolean {
211
+ if (!session.unlocked || session.timeoutMs === 0) {
212
+ return false;
213
+ }
214
+
215
+ const lastActivity = session.lastActivityAt ?? session.unlockedAt ?? 0;
216
+ return Date.now() - lastActivity > session.timeoutMs;
217
+ }
218
+
219
+ /**
220
+ * Check if session should be auto-locked.
221
+ */
222
+ export function shouldAutoLock(session: SecureSession): boolean {
223
+ return session.unlocked && isSessionTimedOut(session);
224
+ }
225
+
226
+ /**
227
+ * Record a failed unlock attempt.
228
+ */
229
+ export function recordFailedAttempt(
230
+ session: SecureSession,
231
+ lockoutThreshold: number = 5,
232
+ lockoutDurationMs: number = 5 * 60 * 1000
233
+ ): SecureSession {
234
+ const newAttempts = session.failedAttempts + 1;
235
+ const isLockedOut = newAttempts >= lockoutThreshold;
236
+
237
+ return {
238
+ ...session,
239
+ failedAttempts: newAttempts,
240
+ lockedOutUntil: isLockedOut ? Date.now() + lockoutDurationMs : null,
241
+ };
242
+ }
243
+
244
+ /**
245
+ * Check if session is in lockout state.
246
+ */
247
+ export function isLockedOut(session: SecureSession): boolean {
248
+ if (!session.lockedOutUntil) {
249
+ return false;
250
+ }
251
+ return Date.now() < session.lockedOutUntil;
252
+ }
253
+
254
+ /**
255
+ * Get remaining lockout time in milliseconds.
256
+ */
257
+ export function getLockoutRemaining(session: SecureSession): number {
258
+ if (!session.lockedOutUntil) {
259
+ return 0;
260
+ }
261
+ return Math.max(0, session.lockedOutUntil - Date.now());
262
+ }
263
+
264
+ // ═══════════════════════════════════════════════════════════════════════════
265
+ // HELPERS
266
+ // ═══════════════════════════════════════════════════════════════════════════
267
+
268
+ function generateSessionId(): string {
269
+ const bytes = globalThis.crypto.getRandomValues(new Uint8Array(16));
270
+ return Array.from(bytes)
271
+ .map(b => b.toString(16).padStart(2, '0'))
272
+ .join('');
273
+ }
274
+
275
+ // ═══════════════════════════════════════════════════════════════════════════
276
+ // SENSITIVE DATA WRAPPER
277
+ // ═══════════════════════════════════════════════════════════════════════════
278
+
279
+ /**
280
+ * Wrapper for sensitive data that auto-zeros on disposal.
281
+ */
282
+ export class SecureBuffer {
283
+ private _data: Uint8Array;
284
+ private _disposed: boolean = false;
285
+
286
+ constructor(data: Uint8Array) {
287
+ // Copy data to prevent external references
288
+ this._data = new Uint8Array(data.length);
289
+ this._data.set(data);
290
+ }
291
+
292
+ /**
293
+ * Get a copy of the data (original stays protected).
294
+ */
295
+ get data(): Uint8Array {
296
+ if (this._disposed) {
297
+ throw new Error('SecureBuffer has been disposed');
298
+ }
299
+ const copy = new Uint8Array(this._data.length);
300
+ copy.set(this._data);
301
+ return copy;
302
+ }
303
+
304
+ /**
305
+ * Get data length without exposing contents.
306
+ */
307
+ get length(): number {
308
+ return this._data.length;
309
+ }
310
+
311
+ /**
312
+ * Check if buffer has been disposed.
313
+ */
314
+ get isDisposed(): boolean {
315
+ return this._disposed;
316
+ }
317
+
318
+ /**
319
+ * Zero and dispose the buffer.
320
+ */
321
+ dispose(): void {
322
+ if (!this._disposed) {
323
+ zeroMemory(this._data);
324
+ this._disposed = true;
325
+ }
326
+ }
327
+
328
+ /**
329
+ * Execute a function with the data, then dispose.
330
+ */
331
+ async useAndDispose<T>(fn: (data: Uint8Array) => Promise<T>): Promise<T> {
332
+ try {
333
+ return await fn(this._data);
334
+ } finally {
335
+ this.dispose();
336
+ }
337
+ }
338
+ }
339
+
340
+ /**
341
+ * Create a SecureBuffer from data.
342
+ */
343
+ export function secureBuffer(data: Uint8Array): SecureBuffer {
344
+ return new SecureBuffer(data);
345
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Omnituum Tunnel v1
3
+ *
4
+ * Post-handshake encrypted tunnel abstraction.
5
+ * Handshake-agnostic: any key agreement protocol can feed into this.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * import { createTunnelSession, TunnelKeyMaterial } from '@omnituum/pqc-shared';
10
+ *
11
+ * // From Noise handshake
12
+ * const keys: TunnelKeyMaterial = toTunnelKeyMaterial(noiseState);
13
+ * const tunnel = createTunnelSession(keys);
14
+ *
15
+ * // Encrypt
16
+ * const ciphertext = tunnel.encrypt(plaintext);
17
+ *
18
+ * // Decrypt
19
+ * const plaintext = tunnel.decrypt(ciphertext);
20
+ *
21
+ * // Clean up
22
+ * tunnel.close();
23
+ * ```
24
+ *
25
+ * @see pqc-docs/specs/tunnel.v1.md
26
+ */
27
+
28
+ // Types
29
+ export type { TunnelKeyMaterial, PQCTunnelSession } from './types';
30
+
31
+ export {
32
+ TUNNEL_VERSION,
33
+ TUNNEL_KEY_SIZE,
34
+ TUNNEL_NONCE_SIZE,
35
+ TUNNEL_TAG_SIZE,
36
+ } from './types';
37
+
38
+ // Session factory
39
+ export { createTunnelSession, createTestKeyMaterial } from './session';