@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
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Omnituum FS - File Decryption
|
|
3
|
+
*
|
|
4
|
+
* Decrypt .oqe (Omnituum Quantum Encrypted) files using hybrid PQC or password.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import nacl from 'tweetnacl';
|
|
8
|
+
import {
|
|
9
|
+
fromHex,
|
|
10
|
+
hkdfSha256,
|
|
11
|
+
toB64,
|
|
12
|
+
u8,
|
|
13
|
+
} from '../crypto/primitives';
|
|
14
|
+
import { kyberDecapsulate, isKyberAvailable } from '../crypto/kyber';
|
|
15
|
+
import {
|
|
16
|
+
ALGORITHM_SUITES,
|
|
17
|
+
OQEMetadata,
|
|
18
|
+
HybridDecryptOptions,
|
|
19
|
+
PasswordDecryptOptions,
|
|
20
|
+
DecryptOptions,
|
|
21
|
+
OQEDecryptResult,
|
|
22
|
+
OQEError,
|
|
23
|
+
FileInput,
|
|
24
|
+
toUint8Array,
|
|
25
|
+
} from './types';
|
|
26
|
+
import { deriveKeyFromPassword, isArgon2Available } from './argon2';
|
|
27
|
+
import { aesDecrypt } from './aes';
|
|
28
|
+
import {
|
|
29
|
+
parseOQEFile,
|
|
30
|
+
parseHybridKeyMaterial,
|
|
31
|
+
parsePasswordKeyMaterial,
|
|
32
|
+
parseMetadata,
|
|
33
|
+
} from './format';
|
|
34
|
+
|
|
35
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
36
|
+
// HELPERS
|
|
37
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
38
|
+
|
|
39
|
+
function hkdfFlex(ikm: Uint8Array, salt: string, info: string): Uint8Array {
|
|
40
|
+
return hkdfSha256(ikm, { salt: u8(salt), info: u8(info), length: 32 });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
44
|
+
// HYBRID MODE DECRYPTION
|
|
45
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Decrypt an OQE file using hybrid X25519 + Kyber768.
|
|
49
|
+
* Tries Kyber first (post-quantum), falls back to X25519 (classical).
|
|
50
|
+
*/
|
|
51
|
+
async function decryptHybrid(
|
|
52
|
+
encryptedData: Uint8Array,
|
|
53
|
+
options: HybridDecryptOptions
|
|
54
|
+
): Promise<OQEDecryptResult> {
|
|
55
|
+
// Parse file structure
|
|
56
|
+
const { header, keyMaterial, encryptedMetadata, encryptedContent } = parseOQEFile(encryptedData);
|
|
57
|
+
|
|
58
|
+
// Verify algorithm
|
|
59
|
+
if (header.algorithmSuite !== ALGORITHM_SUITES.HYBRID_X25519_KYBER768_AES256GCM) {
|
|
60
|
+
throw new OQEError(
|
|
61
|
+
'UNSUPPORTED_ALGORITHM',
|
|
62
|
+
'This file was not encrypted with hybrid mode. Use password decryption.'
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Parse key material
|
|
67
|
+
const km = parseHybridKeyMaterial(keyMaterial);
|
|
68
|
+
|
|
69
|
+
let contentKey: Uint8Array | null = null;
|
|
70
|
+
|
|
71
|
+
// Try Kyber first (post-quantum path)
|
|
72
|
+
if (await isKyberAvailable()) {
|
|
73
|
+
try {
|
|
74
|
+
// Kyber decapsulate expects base64 ciphertext
|
|
75
|
+
const kyberShared = await kyberDecapsulate(
|
|
76
|
+
toB64(km.kyberCiphertext),
|
|
77
|
+
options.recipientSecretKeys.kyberSecB64
|
|
78
|
+
);
|
|
79
|
+
const kyberKek = hkdfFlex(kyberShared, 'omnituum/fs/kyber', 'wrap-content-key');
|
|
80
|
+
const unwrapped = nacl.secretbox.open(km.kyberWrappedKey, km.kyberNonce, kyberKek);
|
|
81
|
+
|
|
82
|
+
if (unwrapped) {
|
|
83
|
+
contentKey = unwrapped;
|
|
84
|
+
console.log('[OQE] Decrypted content key via Kyber (post-quantum secure)');
|
|
85
|
+
}
|
|
86
|
+
} catch (e) {
|
|
87
|
+
console.warn('[OQE] Kyber decapsulation failed, trying X25519:', e);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Fall back to X25519 (classical path)
|
|
92
|
+
if (!contentKey) {
|
|
93
|
+
try {
|
|
94
|
+
const ephPk = km.x25519EphemeralPk;
|
|
95
|
+
const sk = fromHex(options.recipientSecretKeys.x25519SecHex);
|
|
96
|
+
const x25519Shared = nacl.scalarMult(sk, ephPk);
|
|
97
|
+
const x25519Kek = hkdfFlex(x25519Shared, 'omnituum/fs/x25519', 'wrap-content-key');
|
|
98
|
+
const unwrapped = nacl.secretbox.open(km.x25519WrappedKey, km.x25519Nonce, x25519Kek);
|
|
99
|
+
|
|
100
|
+
if (unwrapped) {
|
|
101
|
+
contentKey = unwrapped;
|
|
102
|
+
console.log('[OQE] Decrypted content key via X25519 (classical)');
|
|
103
|
+
}
|
|
104
|
+
} catch (e) {
|
|
105
|
+
console.warn('[OQE] X25519 decryption failed:', e);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (!contentKey) {
|
|
110
|
+
throw new OQEError('KEY_UNWRAP_FAILED', 'Could not unwrap content key with provided keys');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Decrypt metadata
|
|
114
|
+
let metadata: OQEMetadata;
|
|
115
|
+
try {
|
|
116
|
+
const metadataBytes = await aesDecrypt(encryptedMetadata, contentKey, header.iv);
|
|
117
|
+
metadata = parseMetadata(metadataBytes);
|
|
118
|
+
} catch (e) {
|
|
119
|
+
throw new OQEError('DECRYPTION_FAILED', 'Failed to decrypt file metadata');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Decrypt file content
|
|
123
|
+
let plaintext: Uint8Array;
|
|
124
|
+
try {
|
|
125
|
+
plaintext = await aesDecrypt(encryptedContent, contentKey, header.iv);
|
|
126
|
+
} catch (e) {
|
|
127
|
+
throw new OQEError('DECRYPTION_FAILED', 'Failed to decrypt file content');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
data: plaintext,
|
|
132
|
+
filename: metadata.filename,
|
|
133
|
+
mimeType: metadata.mimeType,
|
|
134
|
+
originalSize: metadata.originalSize,
|
|
135
|
+
metadata,
|
|
136
|
+
mode: 'hybrid',
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
141
|
+
// PASSWORD MODE DECRYPTION
|
|
142
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Decrypt an OQE file using password (Argon2id + AES-256-GCM).
|
|
146
|
+
*/
|
|
147
|
+
async function decryptPassword(
|
|
148
|
+
encryptedData: Uint8Array,
|
|
149
|
+
options: PasswordDecryptOptions
|
|
150
|
+
): Promise<OQEDecryptResult> {
|
|
151
|
+
// Verify Argon2 is available
|
|
152
|
+
if (!(await isArgon2Available())) {
|
|
153
|
+
throw new OQEError('ARGON2_UNAVAILABLE', 'Argon2 library not available in this environment');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Parse file structure
|
|
157
|
+
const { header, keyMaterial, encryptedMetadata, encryptedContent } = parseOQEFile(encryptedData);
|
|
158
|
+
|
|
159
|
+
// Verify algorithm
|
|
160
|
+
if (header.algorithmSuite !== ALGORITHM_SUITES.PASSWORD_ARGON2ID_AES256GCM) {
|
|
161
|
+
throw new OQEError(
|
|
162
|
+
'UNSUPPORTED_ALGORITHM',
|
|
163
|
+
'This file was not encrypted with password mode. Use hybrid decryption.'
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Parse key material (Argon2 parameters)
|
|
168
|
+
const km = parsePasswordKeyMaterial(keyMaterial);
|
|
169
|
+
|
|
170
|
+
// Derive content key from password
|
|
171
|
+
const contentKey = await deriveKeyFromPassword(options.password, km.salt, {
|
|
172
|
+
memoryCost: km.memoryCost,
|
|
173
|
+
timeCost: km.timeCost,
|
|
174
|
+
parallelism: km.parallelism,
|
|
175
|
+
hashLength: 32,
|
|
176
|
+
saltLength: km.salt.length,
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// Decrypt metadata (also verifies password)
|
|
180
|
+
let metadata: OQEMetadata;
|
|
181
|
+
try {
|
|
182
|
+
const metadataBytes = await aesDecrypt(encryptedMetadata, contentKey, header.iv);
|
|
183
|
+
metadata = parseMetadata(metadataBytes);
|
|
184
|
+
} catch (e) {
|
|
185
|
+
throw new OQEError('PASSWORD_WRONG', 'Incorrect password or corrupted file');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Decrypt file content
|
|
189
|
+
let plaintext: Uint8Array;
|
|
190
|
+
try {
|
|
191
|
+
plaintext = await aesDecrypt(encryptedContent, contentKey, header.iv);
|
|
192
|
+
} catch (e) {
|
|
193
|
+
throw new OQEError('DECRYPTION_FAILED', 'Failed to decrypt file content');
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
data: plaintext,
|
|
198
|
+
filename: metadata.filename,
|
|
199
|
+
mimeType: metadata.mimeType,
|
|
200
|
+
originalSize: metadata.originalSize,
|
|
201
|
+
metadata,
|
|
202
|
+
mode: 'password',
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
207
|
+
// MAIN DECRYPTION API
|
|
208
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Decrypt an OQE file.
|
|
212
|
+
*
|
|
213
|
+
* @param encryptedData - Encrypted .oqe file data
|
|
214
|
+
* @param options - Decryption options (hybrid or password mode)
|
|
215
|
+
* @returns Decrypted file result
|
|
216
|
+
*
|
|
217
|
+
* @example
|
|
218
|
+
* // Hybrid mode (with identity)
|
|
219
|
+
* const result = await decryptFile(oqeData, {
|
|
220
|
+
* mode: 'hybrid',
|
|
221
|
+
* recipientSecretKeys: identity.getSecretKeys(),
|
|
222
|
+
* });
|
|
223
|
+
*
|
|
224
|
+
* @example
|
|
225
|
+
* // Password mode
|
|
226
|
+
* const result = await decryptFile(oqeData, {
|
|
227
|
+
* mode: 'password',
|
|
228
|
+
* password: 'my-secure-password',
|
|
229
|
+
* });
|
|
230
|
+
*/
|
|
231
|
+
export async function decryptFile(
|
|
232
|
+
encryptedData: FileInput,
|
|
233
|
+
options: DecryptOptions
|
|
234
|
+
): Promise<OQEDecryptResult> {
|
|
235
|
+
const data = await toUint8Array(encryptedData);
|
|
236
|
+
|
|
237
|
+
if (options.mode === 'hybrid') {
|
|
238
|
+
return decryptHybrid(data, options);
|
|
239
|
+
} else {
|
|
240
|
+
return decryptPassword(data, options);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Decrypt a file encrypted for self.
|
|
246
|
+
* Convenience method for personal file decryption.
|
|
247
|
+
*/
|
|
248
|
+
export async function decryptFileForSelf(
|
|
249
|
+
encryptedData: FileInput,
|
|
250
|
+
identity: {
|
|
251
|
+
x25519SecHex: string;
|
|
252
|
+
kyberSecB64: string;
|
|
253
|
+
}
|
|
254
|
+
): Promise<OQEDecryptResult> {
|
|
255
|
+
return decryptFile(encryptedData, {
|
|
256
|
+
mode: 'hybrid',
|
|
257
|
+
recipientSecretKeys: {
|
|
258
|
+
x25519SecHex: identity.x25519SecHex,
|
|
259
|
+
kyberSecB64: identity.kyberSecB64,
|
|
260
|
+
},
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Quick decrypt with password (simple API).
|
|
266
|
+
*/
|
|
267
|
+
export async function decryptFileWithPassword(
|
|
268
|
+
encryptedData: FileInput,
|
|
269
|
+
password: string
|
|
270
|
+
): Promise<OQEDecryptResult> {
|
|
271
|
+
return decryptFile(encryptedData, {
|
|
272
|
+
mode: 'password',
|
|
273
|
+
password,
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
278
|
+
// FILE INSPECTION (without decryption)
|
|
279
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
280
|
+
|
|
281
|
+
export interface OQEFileInfo {
|
|
282
|
+
/** Format version */
|
|
283
|
+
version: number;
|
|
284
|
+
/** Encryption mode */
|
|
285
|
+
mode: 'hybrid' | 'password';
|
|
286
|
+
/** Algorithm name */
|
|
287
|
+
algorithm: string;
|
|
288
|
+
/** Can decrypt with Kyber (for hybrid mode) */
|
|
289
|
+
supportsKyber: boolean;
|
|
290
|
+
/** File size */
|
|
291
|
+
fileSize: number;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Inspect an OQE file without decrypting it.
|
|
296
|
+
* Useful for determining what credentials are needed.
|
|
297
|
+
*/
|
|
298
|
+
export async function inspectOQEFile(data: FileInput): Promise<OQEFileInfo> {
|
|
299
|
+
const bytes = await toUint8Array(data);
|
|
300
|
+
const { header } = parseOQEFile(bytes);
|
|
301
|
+
|
|
302
|
+
let mode: 'hybrid' | 'password';
|
|
303
|
+
let algorithm: string;
|
|
304
|
+
|
|
305
|
+
if (header.algorithmSuite === ALGORITHM_SUITES.HYBRID_X25519_KYBER768_AES256GCM) {
|
|
306
|
+
mode = 'hybrid';
|
|
307
|
+
algorithm = 'X25519 + Kyber768 + AES-256-GCM';
|
|
308
|
+
} else {
|
|
309
|
+
mode = 'password';
|
|
310
|
+
algorithm = 'Argon2id + AES-256-GCM';
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return {
|
|
314
|
+
version: header.version,
|
|
315
|
+
mode,
|
|
316
|
+
algorithm,
|
|
317
|
+
supportsKyber: mode === 'hybrid' && (await isKyberAvailable()),
|
|
318
|
+
fileSize: bytes.length,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Omnituum FS - File Encryption
|
|
3
|
+
*
|
|
4
|
+
* Encrypt files using hybrid post-quantum cryptography or password-based encryption.
|
|
5
|
+
* Outputs .oqe (Omnituum Quantum Encrypted) files.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import nacl from 'tweetnacl';
|
|
9
|
+
import {
|
|
10
|
+
rand32,
|
|
11
|
+
rand24,
|
|
12
|
+
rand12,
|
|
13
|
+
toHex,
|
|
14
|
+
fromHex,
|
|
15
|
+
hkdfSha256,
|
|
16
|
+
sha256,
|
|
17
|
+
u8,
|
|
18
|
+
} from '../crypto/primitives';
|
|
19
|
+
import { kyberEncapsulate, isKyberAvailable } from '../crypto/kyber';
|
|
20
|
+
import {
|
|
21
|
+
OQE_FORMAT_VERSION,
|
|
22
|
+
ALGORITHM_SUITES,
|
|
23
|
+
OQEMetadata,
|
|
24
|
+
OQEHeader,
|
|
25
|
+
HybridKeyMaterial,
|
|
26
|
+
PasswordKeyMaterial,
|
|
27
|
+
HybridEncryptOptions,
|
|
28
|
+
PasswordEncryptOptions,
|
|
29
|
+
EncryptOptions,
|
|
30
|
+
OQEEncryptResult,
|
|
31
|
+
OQEError,
|
|
32
|
+
FileInput,
|
|
33
|
+
toUint8Array,
|
|
34
|
+
DEFAULT_ARGON2ID_PARAMS,
|
|
35
|
+
} from './types';
|
|
36
|
+
import { deriveKeyFromPassword, generateArgon2Salt, isArgon2Available } from './argon2';
|
|
37
|
+
import { aesEncrypt } from './aes';
|
|
38
|
+
import {
|
|
39
|
+
serializeHybridKeyMaterial,
|
|
40
|
+
serializePasswordKeyMaterial,
|
|
41
|
+
serializeMetadata,
|
|
42
|
+
assembleOQEFile,
|
|
43
|
+
addOQEExtension,
|
|
44
|
+
} from './format';
|
|
45
|
+
|
|
46
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
47
|
+
// HELPERS
|
|
48
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
49
|
+
|
|
50
|
+
function hkdfFlex(ikm: Uint8Array, salt: string, info: string): Uint8Array {
|
|
51
|
+
return hkdfSha256(ikm, { salt: u8(salt), info: u8(info), length: 32 });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function computeIdentityHash(publicKeyHex: string): string {
|
|
55
|
+
const hash = sha256(fromHex(publicKeyHex));
|
|
56
|
+
return toHex(hash).slice(0, 16); // First 8 bytes = 16 hex chars
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
60
|
+
// HYBRID MODE ENCRYPTION
|
|
61
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Encrypt a file using hybrid X25519 + Kyber768 encryption.
|
|
65
|
+
* Provides post-quantum security through dual-algorithm key wrapping.
|
|
66
|
+
*/
|
|
67
|
+
async function encryptHybrid(
|
|
68
|
+
plaintext: Uint8Array,
|
|
69
|
+
metadata: OQEMetadata,
|
|
70
|
+
options: HybridEncryptOptions
|
|
71
|
+
): Promise<OQEEncryptResult> {
|
|
72
|
+
// Verify Kyber is available
|
|
73
|
+
if (!(await isKyberAvailable())) {
|
|
74
|
+
throw new OQEError('KYBER_UNAVAILABLE', 'Kyber library not available in this environment');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 1. Generate random content key (32 bytes for AES-256)
|
|
78
|
+
const contentKey = rand32();
|
|
79
|
+
|
|
80
|
+
// 2. Generate IV for AES-GCM
|
|
81
|
+
const iv = rand12();
|
|
82
|
+
|
|
83
|
+
// 3. Wrap content key with X25519 ECDH
|
|
84
|
+
const x25519EphKp = nacl.box.keyPair();
|
|
85
|
+
const recipientX25519Pk = fromHex(options.recipientPublicKeys.x25519PubHex);
|
|
86
|
+
const x25519Shared = nacl.scalarMult(x25519EphKp.secretKey, recipientX25519Pk);
|
|
87
|
+
const x25519Kek = hkdfFlex(x25519Shared, 'omnituum/fs/x25519', 'wrap-content-key');
|
|
88
|
+
const x25519Nonce = rand24();
|
|
89
|
+
const x25519WrappedKey = nacl.secretbox(contentKey, x25519Nonce, x25519Kek);
|
|
90
|
+
|
|
91
|
+
// 4. Wrap content key with Kyber KEM
|
|
92
|
+
const kyberResult = await kyberEncapsulate(options.recipientPublicKeys.kyberPubB64);
|
|
93
|
+
const kyberKek = hkdfFlex(kyberResult.sharedSecret, 'omnituum/fs/kyber', 'wrap-content-key');
|
|
94
|
+
const kyberNonce = rand24();
|
|
95
|
+
const kyberWrappedKey = nacl.secretbox(contentKey, kyberNonce, kyberKek);
|
|
96
|
+
|
|
97
|
+
// 5. Serialize key material
|
|
98
|
+
const keyMaterial: HybridKeyMaterial = {
|
|
99
|
+
x25519EphemeralPk: x25519EphKp.publicKey,
|
|
100
|
+
x25519Nonce,
|
|
101
|
+
x25519WrappedKey,
|
|
102
|
+
kyberCiphertext: kyberResult.ciphertext,
|
|
103
|
+
kyberNonce,
|
|
104
|
+
kyberWrappedKey,
|
|
105
|
+
};
|
|
106
|
+
const keyMaterialBytes = serializeHybridKeyMaterial(keyMaterial);
|
|
107
|
+
|
|
108
|
+
// 6. Encrypt metadata
|
|
109
|
+
const metadataBytes = serializeMetadata(metadata);
|
|
110
|
+
const { ciphertext: encryptedMetadata } = await aesEncrypt(metadataBytes, contentKey, iv);
|
|
111
|
+
|
|
112
|
+
// 7. Encrypt file content
|
|
113
|
+
const { ciphertext: encryptedContent } = await aesEncrypt(plaintext, contentKey, iv);
|
|
114
|
+
|
|
115
|
+
// 8. Build header
|
|
116
|
+
const header: OQEHeader = {
|
|
117
|
+
version: OQE_FORMAT_VERSION,
|
|
118
|
+
algorithmSuite: ALGORITHM_SUITES.HYBRID_X25519_KYBER768_AES256GCM,
|
|
119
|
+
flags: 0,
|
|
120
|
+
metadataLength: encryptedMetadata.length,
|
|
121
|
+
keyMaterialLength: keyMaterialBytes.length,
|
|
122
|
+
iv,
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// 9. Assemble complete file
|
|
126
|
+
const fileData = assembleOQEFile({
|
|
127
|
+
header,
|
|
128
|
+
keyMaterial: keyMaterialBytes,
|
|
129
|
+
encryptedMetadata,
|
|
130
|
+
encryptedContent,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
data: fileData,
|
|
135
|
+
filename: addOQEExtension(metadata.filename),
|
|
136
|
+
metadata,
|
|
137
|
+
mode: 'hybrid',
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
142
|
+
// PASSWORD MODE ENCRYPTION
|
|
143
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Encrypt a file using password-based encryption (Argon2id + AES-256-GCM).
|
|
147
|
+
* Suitable for standalone file protection without identity system.
|
|
148
|
+
*/
|
|
149
|
+
async function encryptPassword(
|
|
150
|
+
plaintext: Uint8Array,
|
|
151
|
+
metadata: OQEMetadata,
|
|
152
|
+
options: PasswordEncryptOptions
|
|
153
|
+
): Promise<OQEEncryptResult> {
|
|
154
|
+
// Verify Argon2 is available
|
|
155
|
+
if (!(await isArgon2Available())) {
|
|
156
|
+
throw new OQEError('ARGON2_UNAVAILABLE', 'Argon2 library not available in this environment');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Merge user params with defaults
|
|
160
|
+
const params = {
|
|
161
|
+
...DEFAULT_ARGON2ID_PARAMS,
|
|
162
|
+
...options.argon2Params,
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
// 1. Generate salt
|
|
166
|
+
const salt = generateArgon2Salt(params.saltLength);
|
|
167
|
+
|
|
168
|
+
// 2. Derive content key from password
|
|
169
|
+
const contentKey = await deriveKeyFromPassword(options.password, salt, params);
|
|
170
|
+
|
|
171
|
+
// 3. Generate IV for AES-GCM
|
|
172
|
+
const iv = rand12();
|
|
173
|
+
|
|
174
|
+
// 4. Serialize key material (Argon2 params + salt)
|
|
175
|
+
const keyMaterial: PasswordKeyMaterial = {
|
|
176
|
+
salt,
|
|
177
|
+
memoryCost: params.memoryCost,
|
|
178
|
+
timeCost: params.timeCost,
|
|
179
|
+
parallelism: params.parallelism,
|
|
180
|
+
};
|
|
181
|
+
const keyMaterialBytes = serializePasswordKeyMaterial(keyMaterial);
|
|
182
|
+
|
|
183
|
+
// 5. Encrypt metadata
|
|
184
|
+
const metadataBytes = serializeMetadata(metadata);
|
|
185
|
+
const { ciphertext: encryptedMetadata } = await aesEncrypt(metadataBytes, contentKey, iv);
|
|
186
|
+
|
|
187
|
+
// 6. Encrypt file content
|
|
188
|
+
const { ciphertext: encryptedContent } = await aesEncrypt(plaintext, contentKey, iv);
|
|
189
|
+
|
|
190
|
+
// 7. Build header
|
|
191
|
+
const header: OQEHeader = {
|
|
192
|
+
version: OQE_FORMAT_VERSION,
|
|
193
|
+
algorithmSuite: ALGORITHM_SUITES.PASSWORD_ARGON2ID_AES256GCM,
|
|
194
|
+
flags: 0,
|
|
195
|
+
metadataLength: encryptedMetadata.length,
|
|
196
|
+
keyMaterialLength: keyMaterialBytes.length,
|
|
197
|
+
iv,
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
// 8. Assemble complete file
|
|
201
|
+
const fileData = assembleOQEFile({
|
|
202
|
+
header,
|
|
203
|
+
keyMaterial: keyMaterialBytes,
|
|
204
|
+
encryptedMetadata,
|
|
205
|
+
encryptedContent,
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
data: fileData,
|
|
210
|
+
filename: addOQEExtension(metadata.filename),
|
|
211
|
+
metadata,
|
|
212
|
+
mode: 'password',
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
217
|
+
// MAIN ENCRYPTION API
|
|
218
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
219
|
+
|
|
220
|
+
export interface EncryptFileInput {
|
|
221
|
+
/** File data */
|
|
222
|
+
data: FileInput;
|
|
223
|
+
/** Original filename */
|
|
224
|
+
filename: string;
|
|
225
|
+
/** Optional MIME type */
|
|
226
|
+
mimeType?: string;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Encrypt a file using hybrid PQC or password-based encryption.
|
|
231
|
+
*
|
|
232
|
+
* @param input - File data and metadata
|
|
233
|
+
* @param options - Encryption options (hybrid or password mode)
|
|
234
|
+
* @returns Encrypted .oqe file result
|
|
235
|
+
*
|
|
236
|
+
* @example
|
|
237
|
+
* // Hybrid mode (with identity)
|
|
238
|
+
* const result = await encryptFile(
|
|
239
|
+
* { data: fileBytes, filename: 'secret.pdf' },
|
|
240
|
+
* {
|
|
241
|
+
* mode: 'hybrid',
|
|
242
|
+
* recipientPublicKeys: identity.getPublicKeys(),
|
|
243
|
+
* }
|
|
244
|
+
* );
|
|
245
|
+
*
|
|
246
|
+
* @example
|
|
247
|
+
* // Password mode
|
|
248
|
+
* const result = await encryptFile(
|
|
249
|
+
* { data: fileBytes, filename: 'secret.pdf' },
|
|
250
|
+
* {
|
|
251
|
+
* mode: 'password',
|
|
252
|
+
* password: 'my-secure-password',
|
|
253
|
+
* }
|
|
254
|
+
* );
|
|
255
|
+
*/
|
|
256
|
+
export async function encryptFile(
|
|
257
|
+
input: EncryptFileInput,
|
|
258
|
+
options: EncryptOptions
|
|
259
|
+
): Promise<OQEEncryptResult> {
|
|
260
|
+
// Convert input to Uint8Array
|
|
261
|
+
const plaintext = await toUint8Array(input.data);
|
|
262
|
+
|
|
263
|
+
// Build metadata
|
|
264
|
+
const metadata: OQEMetadata = {
|
|
265
|
+
filename: input.filename,
|
|
266
|
+
originalSize: plaintext.length,
|
|
267
|
+
mimeType: input.mimeType,
|
|
268
|
+
encryptedAt: new Date().toISOString(),
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
// Add identity hashes for hybrid mode
|
|
272
|
+
if (options.mode === 'hybrid') {
|
|
273
|
+
metadata.recipientIdHash = computeIdentityHash(options.recipientPublicKeys.x25519PubHex);
|
|
274
|
+
if (options.sender) {
|
|
275
|
+
metadata.encryptorIdHash = options.sender.id;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Encrypt based on mode
|
|
280
|
+
if (options.mode === 'hybrid') {
|
|
281
|
+
return encryptHybrid(plaintext, metadata, options);
|
|
282
|
+
} else {
|
|
283
|
+
return encryptPassword(plaintext, metadata, options);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Encrypt a file for self (encrypt and decrypt with same identity).
|
|
289
|
+
* Convenience method for personal file encryption.
|
|
290
|
+
*/
|
|
291
|
+
export async function encryptFileForSelf(
|
|
292
|
+
input: EncryptFileInput,
|
|
293
|
+
identity: {
|
|
294
|
+
id: string;
|
|
295
|
+
name?: string;
|
|
296
|
+
x25519PubHex: string;
|
|
297
|
+
kyberPubB64: string;
|
|
298
|
+
}
|
|
299
|
+
): Promise<OQEEncryptResult> {
|
|
300
|
+
return encryptFile(input, {
|
|
301
|
+
mode: 'hybrid',
|
|
302
|
+
recipientPublicKeys: {
|
|
303
|
+
x25519PubHex: identity.x25519PubHex,
|
|
304
|
+
kyberPubB64: identity.kyberPubB64,
|
|
305
|
+
},
|
|
306
|
+
sender: {
|
|
307
|
+
id: identity.id,
|
|
308
|
+
name: identity.name,
|
|
309
|
+
},
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Quick encrypt with password (simple API).
|
|
315
|
+
*/
|
|
316
|
+
export async function encryptFileWithPassword(
|
|
317
|
+
input: EncryptFileInput,
|
|
318
|
+
password: string
|
|
319
|
+
): Promise<OQEEncryptResult> {
|
|
320
|
+
return encryptFile(input, {
|
|
321
|
+
mode: 'password',
|
|
322
|
+
password,
|
|
323
|
+
});
|
|
324
|
+
}
|