@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.
- package/LICENSE +22 -0
- package/README.md +543 -0
- package/dist/crypto/index.cjs +807 -0
- package/dist/crypto/index.d.cts +641 -0
- package/dist/crypto/index.d.ts +641 -0
- package/dist/crypto/index.js +716 -0
- package/dist/decrypt-eSHlbh1j.d.cts +321 -0
- package/dist/decrypt-eSHlbh1j.d.ts +321 -0
- package/dist/fs/index.cjs +1168 -0
- package/dist/fs/index.d.cts +400 -0
- package/dist/fs/index.d.ts +400 -0
- package/dist/fs/index.js +1091 -0
- package/dist/index.cjs +2160 -0
- package/dist/index.d.cts +282 -0
- package/dist/index.d.ts +282 -0
- package/dist/index.js +2031 -0
- package/dist/integrity-CCYjrap3.d.ts +31 -0
- package/dist/integrity-Dx9jukMH.d.cts +31 -0
- package/dist/types-61c7Q9ri.d.ts +134 -0
- package/dist/types-Ch0y-n7K.d.cts +134 -0
- package/dist/utils/index.cjs +129 -0
- package/dist/utils/index.d.cts +49 -0
- package/dist/utils/index.d.ts +49 -0
- package/dist/utils/index.js +114 -0
- package/dist/vault/index.cjs +713 -0
- package/dist/vault/index.d.cts +237 -0
- package/dist/vault/index.d.ts +237 -0
- package/dist/vault/index.js +677 -0
- package/dist/version-BygzPVGs.d.cts +55 -0
- package/dist/version-BygzPVGs.d.ts +55 -0
- package/package.json +86 -0
- package/src/crypto/dilithium.ts +233 -0
- package/src/crypto/hybrid.ts +358 -0
- package/src/crypto/index.ts +181 -0
- package/src/crypto/kyber.ts +199 -0
- package/src/crypto/nacl.ts +204 -0
- package/src/crypto/primitives/blake3.ts +141 -0
- package/src/crypto/primitives/chacha.ts +211 -0
- package/src/crypto/primitives/hkdf.ts +192 -0
- package/src/crypto/primitives/index.ts +54 -0
- package/src/crypto/primitives.ts +144 -0
- package/src/crypto/x25519.ts +134 -0
- package/src/fs/aes.ts +343 -0
- package/src/fs/argon2.ts +184 -0
- package/src/fs/browser.ts +408 -0
- package/src/fs/decrypt.ts +320 -0
- package/src/fs/encrypt.ts +324 -0
- package/src/fs/format.ts +425 -0
- package/src/fs/index.ts +144 -0
- package/src/fs/types.ts +304 -0
- package/src/index.ts +414 -0
- package/src/kdf/index.ts +311 -0
- package/src/runtime/crypto.ts +16 -0
- package/src/security/index.ts +345 -0
- package/src/tunnel/index.ts +39 -0
- package/src/tunnel/session.ts +229 -0
- package/src/tunnel/types.ts +115 -0
- package/src/utils/entropy.ts +128 -0
- package/src/utils/index.ts +25 -0
- package/src/utils/integrity.ts +95 -0
- package/src/vault/decrypt.ts +167 -0
- package/src/vault/encrypt.ts +207 -0
- package/src/vault/index.ts +71 -0
- package/src/vault/manager.ts +327 -0
- package/src/vault/migrate.ts +190 -0
- package/src/vault/types.ts +177 -0
- package/src/version.ts +304 -0
package/src/kdf/index.ts
ADDED
|
@@ -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';
|