@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,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
+ }