@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,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Omnituum PQC Shared - X25519 Key Exchange
|
|
3
|
+
*
|
|
4
|
+
* Browser-compatible X25519 operations using tweetnacl.
|
|
5
|
+
* Provides key generation, ECDH, and NaCl box operations.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import nacl from 'tweetnacl';
|
|
9
|
+
import { rand24, assertLen, toHex, fromHex, b64, ub64, sha256, hkdfSha256, u8 } from './primitives';
|
|
10
|
+
|
|
11
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
12
|
+
// TYPES
|
|
13
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
14
|
+
|
|
15
|
+
export interface X25519Keypair {
|
|
16
|
+
publicKey: Uint8Array;
|
|
17
|
+
secretKey: Uint8Array;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface X25519KeypairHex {
|
|
21
|
+
publicHex: string;
|
|
22
|
+
secretHex: string;
|
|
23
|
+
publicBytes: Uint8Array;
|
|
24
|
+
secretBytes: Uint8Array;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ClassicalWrap {
|
|
28
|
+
ephPubKey: string;
|
|
29
|
+
nonce: string;
|
|
30
|
+
boxed: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
34
|
+
// KEY GENERATION
|
|
35
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Generate a new random X25519 keypair.
|
|
39
|
+
*/
|
|
40
|
+
export function generateX25519Keypair(): X25519KeypairHex {
|
|
41
|
+
const kp = nacl.box.keyPair();
|
|
42
|
+
return {
|
|
43
|
+
publicHex: '0x' + toHex(kp.publicKey),
|
|
44
|
+
secretHex: '0x' + toHex(kp.secretKey),
|
|
45
|
+
publicBytes: kp.publicKey,
|
|
46
|
+
secretBytes: kp.secretKey,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Generate a deterministic X25519 keypair from a 32-byte seed.
|
|
52
|
+
*/
|
|
53
|
+
export function generateX25519KeypairFromSeed(seed: Uint8Array): X25519Keypair {
|
|
54
|
+
if (seed.length !== 32) {
|
|
55
|
+
throw new Error('X25519 seed must be 32 bytes');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Hash seed for uniformity
|
|
59
|
+
const uniformSeed = sha256(seed);
|
|
60
|
+
|
|
61
|
+
// Generate keypair from secret key
|
|
62
|
+
const kp = nacl.box.keyPair.fromSecretKey(uniformSeed);
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
publicKey: kp.publicKey,
|
|
66
|
+
secretKey: uniformSeed,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
71
|
+
// KEY WRAPPING
|
|
72
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Wrap a symmetric key for a recipient using X25519 ECDH.
|
|
76
|
+
* Creates an ephemeral keypair and encrypts the key using NaCl box.
|
|
77
|
+
*/
|
|
78
|
+
export function boxWrapWithX25519(symKey32: Uint8Array, recipientPubKeyHex: string): ClassicalWrap {
|
|
79
|
+
assertLen('sym key', symKey32, 32);
|
|
80
|
+
const pk = fromHex(recipientPubKeyHex);
|
|
81
|
+
assertLen('recipient pubKey', pk, 32);
|
|
82
|
+
|
|
83
|
+
// Generate ephemeral keypair
|
|
84
|
+
const eph = nacl.box.keyPair();
|
|
85
|
+
const nonce = rand24();
|
|
86
|
+
|
|
87
|
+
// Encrypt the symmetric key
|
|
88
|
+
const boxed = nacl.box(symKey32, nonce, pk, eph.secretKey);
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
ephPubKey: toHex(eph.publicKey),
|
|
92
|
+
nonce: b64(nonce),
|
|
93
|
+
boxed: b64(boxed),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Unwrap a symmetric key using X25519 ECDH.
|
|
99
|
+
*/
|
|
100
|
+
export function boxUnwrapWithX25519(
|
|
101
|
+
wrap: ClassicalWrap,
|
|
102
|
+
recipientSecretKey32: Uint8Array
|
|
103
|
+
): Uint8Array | null {
|
|
104
|
+
assertLen('recipient secretKey', recipientSecretKey32, 32);
|
|
105
|
+
const ephPk = fromHex(wrap.ephPubKey);
|
|
106
|
+
assertLen('ephemeral pubKey', ephPk, 32);
|
|
107
|
+
|
|
108
|
+
return nacl.box.open(
|
|
109
|
+
ub64(wrap.boxed),
|
|
110
|
+
ub64(wrap.nonce),
|
|
111
|
+
ephPk,
|
|
112
|
+
recipientSecretKey32
|
|
113
|
+
) || null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Perform raw X25519 ECDH to derive a shared secret.
|
|
118
|
+
*/
|
|
119
|
+
export function x25519SharedSecret(
|
|
120
|
+
ourSecretKey: Uint8Array,
|
|
121
|
+
theirPublicKey: Uint8Array
|
|
122
|
+
): Uint8Array {
|
|
123
|
+
assertLen('secret key', ourSecretKey, 32);
|
|
124
|
+
assertLen('public key', theirPublicKey, 32);
|
|
125
|
+
return nacl.scalarMult(ourSecretKey, theirPublicKey);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
129
|
+
// HKDF KEY DERIVATION HELPER
|
|
130
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
131
|
+
|
|
132
|
+
export function deriveKeyFromShared(shared: Uint8Array, salt: string, info: string): Uint8Array {
|
|
133
|
+
return hkdfSha256(shared, { salt: u8(salt), info: u8(info), length: 32 });
|
|
134
|
+
}
|
package/src/fs/aes.ts
ADDED
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Omnituum FS - AES-256-GCM Encryption
|
|
3
|
+
*
|
|
4
|
+
* File encryption using AES-256-GCM via Web Crypto API.
|
|
5
|
+
* Provides authenticated encryption with associated data (AEAD).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { rand12 } from '../crypto/primitives';
|
|
9
|
+
|
|
10
|
+
// Helper to safely extract ArrayBuffer from Uint8Array (handles SharedArrayBuffer)
|
|
11
|
+
function toArrayBuffer(arr: Uint8Array): ArrayBuffer {
|
|
12
|
+
// If buffer is SharedArrayBuffer or view is offset, copy to new ArrayBuffer
|
|
13
|
+
if (arr.buffer instanceof SharedArrayBuffer || arr.byteOffset !== 0 || arr.byteLength !== arr.buffer.byteLength) {
|
|
14
|
+
const copy = new ArrayBuffer(arr.byteLength);
|
|
15
|
+
new Uint8Array(copy).set(arr);
|
|
16
|
+
return copy;
|
|
17
|
+
}
|
|
18
|
+
return arr.buffer as ArrayBuffer;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
22
|
+
// CONSTANTS
|
|
23
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
24
|
+
|
|
25
|
+
/** AES-256 key size in bytes */
|
|
26
|
+
export const AES_KEY_SIZE = 32;
|
|
27
|
+
|
|
28
|
+
/** AES-GCM IV size in bytes (96 bits recommended by NIST) */
|
|
29
|
+
export const AES_GCM_IV_SIZE = 12;
|
|
30
|
+
|
|
31
|
+
/** AES-GCM auth tag size in bytes (128 bits) */
|
|
32
|
+
export const AES_GCM_TAG_SIZE = 16;
|
|
33
|
+
|
|
34
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
35
|
+
// KEY IMPORT
|
|
36
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Import a raw 256-bit key for AES-GCM operations.
|
|
40
|
+
*
|
|
41
|
+
* @param keyBytes - 32-byte raw key material
|
|
42
|
+
* @returns CryptoKey suitable for AES-GCM encryption/decryption
|
|
43
|
+
*/
|
|
44
|
+
export async function importAesKey(keyBytes: Uint8Array): Promise<CryptoKey> {
|
|
45
|
+
if (keyBytes.length !== AES_KEY_SIZE) {
|
|
46
|
+
throw new Error(`AES key must be ${AES_KEY_SIZE} bytes, got ${keyBytes.length}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return globalThis.crypto.subtle.importKey(
|
|
50
|
+
'raw',
|
|
51
|
+
toArrayBuffer(keyBytes),
|
|
52
|
+
{ name: 'AES-GCM', length: 256 },
|
|
53
|
+
false, // not extractable
|
|
54
|
+
['encrypt', 'decrypt']
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Generate a random 256-bit AES key.
|
|
60
|
+
*
|
|
61
|
+
* @returns CryptoKey for AES-GCM
|
|
62
|
+
*/
|
|
63
|
+
export async function generateAesKey(): Promise<CryptoKey> {
|
|
64
|
+
return globalThis.crypto.subtle.generateKey(
|
|
65
|
+
{ name: 'AES-GCM', length: 256 },
|
|
66
|
+
true, // extractable for wrapping
|
|
67
|
+
['encrypt', 'decrypt']
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Export a CryptoKey to raw bytes.
|
|
73
|
+
*
|
|
74
|
+
* @param key - CryptoKey to export
|
|
75
|
+
* @returns 32-byte raw key
|
|
76
|
+
*/
|
|
77
|
+
export async function exportAesKey(key: CryptoKey): Promise<Uint8Array> {
|
|
78
|
+
const exported = await globalThis.crypto.subtle.exportKey('raw', key);
|
|
79
|
+
return new Uint8Array(exported);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
83
|
+
// ENCRYPTION
|
|
84
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Encrypt data using AES-256-GCM.
|
|
88
|
+
*
|
|
89
|
+
* @param plaintext - Data to encrypt
|
|
90
|
+
* @param key - AES-256 key (CryptoKey or 32-byte Uint8Array)
|
|
91
|
+
* @param iv - Optional 12-byte IV (generated if not provided)
|
|
92
|
+
* @param additionalData - Optional additional authenticated data (AAD)
|
|
93
|
+
* @returns Object containing IV and ciphertext (with auth tag appended)
|
|
94
|
+
*/
|
|
95
|
+
export async function aesEncrypt(
|
|
96
|
+
plaintext: Uint8Array,
|
|
97
|
+
key: CryptoKey | Uint8Array,
|
|
98
|
+
iv?: Uint8Array,
|
|
99
|
+
additionalData?: Uint8Array
|
|
100
|
+
): Promise<{ iv: Uint8Array; ciphertext: Uint8Array }> {
|
|
101
|
+
// Import key if raw bytes provided
|
|
102
|
+
const cryptoKey = key instanceof CryptoKey ? key : await importAesKey(key);
|
|
103
|
+
|
|
104
|
+
// Generate IV if not provided
|
|
105
|
+
const ivBytes = iv ?? rand12();
|
|
106
|
+
|
|
107
|
+
if (ivBytes.length !== AES_GCM_IV_SIZE) {
|
|
108
|
+
throw new Error(`IV must be ${AES_GCM_IV_SIZE} bytes, got ${ivBytes.length}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Encrypt with AES-GCM (convert Uint8Arrays to ArrayBuffer for Web Crypto compatibility)
|
|
112
|
+
const encrypted = await globalThis.crypto.subtle.encrypt(
|
|
113
|
+
{
|
|
114
|
+
name: 'AES-GCM',
|
|
115
|
+
iv: toArrayBuffer(ivBytes),
|
|
116
|
+
additionalData: additionalData ? toArrayBuffer(additionalData) : undefined,
|
|
117
|
+
tagLength: 128, // 16 bytes
|
|
118
|
+
},
|
|
119
|
+
cryptoKey,
|
|
120
|
+
toArrayBuffer(plaintext)
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
iv: ivBytes,
|
|
125
|
+
ciphertext: new Uint8Array(encrypted), // Includes auth tag
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Encrypt data and return combined IV + ciphertext.
|
|
131
|
+
* Convenience method for simple encryption.
|
|
132
|
+
*
|
|
133
|
+
* @param plaintext - Data to encrypt
|
|
134
|
+
* @param key - AES-256 key
|
|
135
|
+
* @param additionalData - Optional AAD
|
|
136
|
+
* @returns Combined bytes: [IV (12 bytes)] [ciphertext + tag]
|
|
137
|
+
*/
|
|
138
|
+
export async function aesEncryptCombined(
|
|
139
|
+
plaintext: Uint8Array,
|
|
140
|
+
key: CryptoKey | Uint8Array,
|
|
141
|
+
additionalData?: Uint8Array
|
|
142
|
+
): Promise<Uint8Array> {
|
|
143
|
+
const { iv, ciphertext } = await aesEncrypt(plaintext, key, undefined, additionalData);
|
|
144
|
+
|
|
145
|
+
// Combine IV + ciphertext
|
|
146
|
+
const combined = new Uint8Array(iv.length + ciphertext.length);
|
|
147
|
+
combined.set(iv, 0);
|
|
148
|
+
combined.set(ciphertext, iv.length);
|
|
149
|
+
|
|
150
|
+
return combined;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
154
|
+
// DECRYPTION
|
|
155
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Decrypt data using AES-256-GCM.
|
|
159
|
+
*
|
|
160
|
+
* @param ciphertext - Encrypted data (with auth tag appended)
|
|
161
|
+
* @param key - AES-256 key (CryptoKey or 32-byte Uint8Array)
|
|
162
|
+
* @param iv - 12-byte IV used for encryption
|
|
163
|
+
* @param additionalData - Optional additional authenticated data (must match encryption)
|
|
164
|
+
* @returns Decrypted plaintext
|
|
165
|
+
* @throws Error if authentication fails (wrong key or tampered data)
|
|
166
|
+
*/
|
|
167
|
+
export async function aesDecrypt(
|
|
168
|
+
ciphertext: Uint8Array,
|
|
169
|
+
key: CryptoKey | Uint8Array,
|
|
170
|
+
iv: Uint8Array,
|
|
171
|
+
additionalData?: Uint8Array
|
|
172
|
+
): Promise<Uint8Array> {
|
|
173
|
+
// Import key if raw bytes provided
|
|
174
|
+
const cryptoKey = key instanceof CryptoKey ? key : await importAesKey(key);
|
|
175
|
+
|
|
176
|
+
if (iv.length !== AES_GCM_IV_SIZE) {
|
|
177
|
+
throw new Error(`IV must be ${AES_GCM_IV_SIZE} bytes, got ${iv.length}`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
// Convert Uint8Arrays to ArrayBuffer for Web Crypto compatibility
|
|
182
|
+
const decrypted = await globalThis.crypto.subtle.decrypt(
|
|
183
|
+
{
|
|
184
|
+
name: 'AES-GCM',
|
|
185
|
+
iv: toArrayBuffer(iv),
|
|
186
|
+
additionalData: additionalData ? toArrayBuffer(additionalData) : undefined,
|
|
187
|
+
tagLength: 128,
|
|
188
|
+
},
|
|
189
|
+
cryptoKey,
|
|
190
|
+
toArrayBuffer(ciphertext)
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
return new Uint8Array(decrypted);
|
|
194
|
+
} catch (error) {
|
|
195
|
+
// Web Crypto throws a generic error for auth failures
|
|
196
|
+
throw new Error('Decryption failed: authentication tag mismatch (wrong key or corrupted data)');
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Decrypt combined IV + ciphertext.
|
|
202
|
+
* Convenience method for simple decryption.
|
|
203
|
+
*
|
|
204
|
+
* @param combined - Combined bytes: [IV (12 bytes)] [ciphertext + tag]
|
|
205
|
+
* @param key - AES-256 key
|
|
206
|
+
* @param additionalData - Optional AAD
|
|
207
|
+
* @returns Decrypted plaintext
|
|
208
|
+
*/
|
|
209
|
+
export async function aesDecryptCombined(
|
|
210
|
+
combined: Uint8Array,
|
|
211
|
+
key: CryptoKey | Uint8Array,
|
|
212
|
+
additionalData?: Uint8Array
|
|
213
|
+
): Promise<Uint8Array> {
|
|
214
|
+
if (combined.length < AES_GCM_IV_SIZE + AES_GCM_TAG_SIZE) {
|
|
215
|
+
throw new Error(`Combined data too short: need at least ${AES_GCM_IV_SIZE + AES_GCM_TAG_SIZE} bytes`);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const iv = combined.slice(0, AES_GCM_IV_SIZE);
|
|
219
|
+
const ciphertext = combined.slice(AES_GCM_IV_SIZE);
|
|
220
|
+
|
|
221
|
+
return aesDecrypt(ciphertext, key, iv, additionalData);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
225
|
+
// STREAMING ENCRYPTION (for large files)
|
|
226
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
227
|
+
|
|
228
|
+
/** Chunk size for streaming operations (1 MB) */
|
|
229
|
+
export const STREAM_CHUNK_SIZE = 1024 * 1024;
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Encrypt large data in chunks.
|
|
233
|
+
* Each chunk is encrypted with a unique IV derived from base IV + counter.
|
|
234
|
+
*
|
|
235
|
+
* Note: For files < 100MB, use regular aesEncrypt for simplicity.
|
|
236
|
+
* This is primarily for very large files where memory is a concern.
|
|
237
|
+
*
|
|
238
|
+
* @param plaintext - Data to encrypt
|
|
239
|
+
* @param key - AES-256 key
|
|
240
|
+
* @param onProgress - Optional progress callback
|
|
241
|
+
* @returns Encrypted data with format: [chunk count (4 bytes)] [IV] [chunks...]
|
|
242
|
+
*/
|
|
243
|
+
export async function aesEncryptStreaming(
|
|
244
|
+
plaintext: Uint8Array,
|
|
245
|
+
key: CryptoKey | Uint8Array,
|
|
246
|
+
onProgress?: (percent: number) => void
|
|
247
|
+
): Promise<Uint8Array> {
|
|
248
|
+
const cryptoKey = key instanceof CryptoKey ? key : await importAesKey(key);
|
|
249
|
+
|
|
250
|
+
const chunks: Uint8Array[] = [];
|
|
251
|
+
const totalChunks = Math.ceil(plaintext.length / STREAM_CHUNK_SIZE);
|
|
252
|
+
|
|
253
|
+
// Store chunk count (4 bytes, big-endian)
|
|
254
|
+
const header = new Uint8Array(4);
|
|
255
|
+
new DataView(header.buffer).setUint32(0, totalChunks, false);
|
|
256
|
+
chunks.push(header);
|
|
257
|
+
|
|
258
|
+
for (let i = 0; i < totalChunks; i++) {
|
|
259
|
+
const start = i * STREAM_CHUNK_SIZE;
|
|
260
|
+
const end = Math.min(start + STREAM_CHUNK_SIZE, plaintext.length);
|
|
261
|
+
const chunk = plaintext.slice(start, end);
|
|
262
|
+
|
|
263
|
+
// Encrypt chunk (IV is generated fresh for each chunk)
|
|
264
|
+
const { iv, ciphertext } = await aesEncrypt(chunk, cryptoKey);
|
|
265
|
+
|
|
266
|
+
// Store: [IV (12 bytes)] [ciphertext length (4 bytes)] [ciphertext]
|
|
267
|
+
const chunkData = new Uint8Array(12 + 4 + ciphertext.length);
|
|
268
|
+
chunkData.set(iv, 0);
|
|
269
|
+
new DataView(chunkData.buffer).setUint32(12, ciphertext.length, false);
|
|
270
|
+
chunkData.set(ciphertext, 16);
|
|
271
|
+
chunks.push(chunkData);
|
|
272
|
+
|
|
273
|
+
if (onProgress) {
|
|
274
|
+
onProgress(Math.round(((i + 1) / totalChunks) * 100));
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Combine all chunks
|
|
279
|
+
const totalLength = chunks.reduce((sum, c) => sum + c.length, 0);
|
|
280
|
+
const result = new Uint8Array(totalLength);
|
|
281
|
+
let offset = 0;
|
|
282
|
+
for (const chunk of chunks) {
|
|
283
|
+
result.set(chunk, offset);
|
|
284
|
+
offset += chunk.length;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return result;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Decrypt large data encrypted with aesEncryptStreaming.
|
|
292
|
+
*
|
|
293
|
+
* @param encrypted - Encrypted data from aesEncryptStreaming
|
|
294
|
+
* @param key - AES-256 key
|
|
295
|
+
* @param onProgress - Optional progress callback
|
|
296
|
+
* @returns Decrypted plaintext
|
|
297
|
+
*/
|
|
298
|
+
export async function aesDecryptStreaming(
|
|
299
|
+
encrypted: Uint8Array,
|
|
300
|
+
key: CryptoKey | Uint8Array,
|
|
301
|
+
onProgress?: (percent: number) => void
|
|
302
|
+
): Promise<Uint8Array> {
|
|
303
|
+
const cryptoKey = key instanceof CryptoKey ? key : await importAesKey(key);
|
|
304
|
+
|
|
305
|
+
// Read chunk count
|
|
306
|
+
const totalChunks = new DataView(encrypted.buffer, encrypted.byteOffset).getUint32(0, false);
|
|
307
|
+
const chunks: Uint8Array[] = [];
|
|
308
|
+
|
|
309
|
+
let offset = 4; // Skip header
|
|
310
|
+
|
|
311
|
+
for (let i = 0; i < totalChunks; i++) {
|
|
312
|
+
// Read IV
|
|
313
|
+
const iv = encrypted.slice(offset, offset + 12);
|
|
314
|
+
offset += 12;
|
|
315
|
+
|
|
316
|
+
// Read ciphertext length
|
|
317
|
+
const ctLength = new DataView(encrypted.buffer, encrypted.byteOffset + offset).getUint32(0, false);
|
|
318
|
+
offset += 4;
|
|
319
|
+
|
|
320
|
+
// Read ciphertext
|
|
321
|
+
const ciphertext = encrypted.slice(offset, offset + ctLength);
|
|
322
|
+
offset += ctLength;
|
|
323
|
+
|
|
324
|
+
// Decrypt chunk
|
|
325
|
+
const plaintext = await aesDecrypt(ciphertext, cryptoKey, iv);
|
|
326
|
+
chunks.push(plaintext);
|
|
327
|
+
|
|
328
|
+
if (onProgress) {
|
|
329
|
+
onProgress(Math.round(((i + 1) / totalChunks) * 100));
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Combine all chunks
|
|
334
|
+
const totalLength = chunks.reduce((sum, c) => sum + c.length, 0);
|
|
335
|
+
const result = new Uint8Array(totalLength);
|
|
336
|
+
let resultOffset = 0;
|
|
337
|
+
for (const chunk of chunks) {
|
|
338
|
+
result.set(chunk, resultOffset);
|
|
339
|
+
resultOffset += chunk.length;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return result;
|
|
343
|
+
}
|
package/src/fs/argon2.ts
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Omnituum FS - Argon2id Key Derivation
|
|
3
|
+
*
|
|
4
|
+
* Memory-hard password hashing using Argon2id (winner of PHC).
|
|
5
|
+
* Uses hash-wasm for browser-compatible WASM implementation.
|
|
6
|
+
*
|
|
7
|
+
* Security: OWASP 2024 recommended parameters.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { argon2id } from 'hash-wasm';
|
|
11
|
+
import { randN } from '../crypto/primitives';
|
|
12
|
+
import {
|
|
13
|
+
Argon2idParams,
|
|
14
|
+
DEFAULT_ARGON2ID_PARAMS,
|
|
15
|
+
MIN_ARGON2ID_PARAMS,
|
|
16
|
+
} from './types';
|
|
17
|
+
|
|
18
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
19
|
+
// ARGON2ID KEY DERIVATION
|
|
20
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Derive an encryption key from a password using Argon2id.
|
|
24
|
+
*
|
|
25
|
+
* @param password - User password
|
|
26
|
+
* @param salt - 32-byte random salt (generate with generateArgon2Salt())
|
|
27
|
+
* @param params - Argon2id parameters (uses OWASP defaults if not specified)
|
|
28
|
+
* @returns 32-byte derived key suitable for AES-256
|
|
29
|
+
*/
|
|
30
|
+
export async function deriveKeyFromPassword(
|
|
31
|
+
password: string,
|
|
32
|
+
salt: Uint8Array,
|
|
33
|
+
params: Argon2idParams = DEFAULT_ARGON2ID_PARAMS
|
|
34
|
+
): Promise<Uint8Array> {
|
|
35
|
+
if (salt.length !== params.saltLength) {
|
|
36
|
+
throw new Error(`Salt must be ${params.saltLength} bytes, got ${salt.length}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const hash = await argon2id({
|
|
40
|
+
password,
|
|
41
|
+
salt,
|
|
42
|
+
parallelism: params.parallelism,
|
|
43
|
+
iterations: params.timeCost,
|
|
44
|
+
memorySize: params.memoryCost,
|
|
45
|
+
hashLength: params.hashLength,
|
|
46
|
+
outputType: 'binary',
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
return new Uint8Array(hash);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Generate a random salt for Argon2id.
|
|
54
|
+
*
|
|
55
|
+
* @param length - Salt length in bytes (default: 32)
|
|
56
|
+
* @returns Random salt
|
|
57
|
+
*/
|
|
58
|
+
export function generateArgon2Salt(length: number = 32): Uint8Array {
|
|
59
|
+
return randN(length);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Verify a password against a stored hash.
|
|
64
|
+
* Useful for vault unlocking or file password verification.
|
|
65
|
+
*
|
|
66
|
+
* @param password - Password to verify
|
|
67
|
+
* @param salt - Original salt used
|
|
68
|
+
* @param expectedKey - Expected derived key
|
|
69
|
+
* @param params - Argon2id parameters
|
|
70
|
+
* @returns true if password is correct
|
|
71
|
+
*/
|
|
72
|
+
export async function verifyPassword(
|
|
73
|
+
password: string,
|
|
74
|
+
salt: Uint8Array,
|
|
75
|
+
expectedKey: Uint8Array,
|
|
76
|
+
params: Argon2idParams = DEFAULT_ARGON2ID_PARAMS
|
|
77
|
+
): Promise<boolean> {
|
|
78
|
+
const derivedKey = await deriveKeyFromPassword(password, salt, params);
|
|
79
|
+
|
|
80
|
+
// Constant-time comparison to prevent timing attacks
|
|
81
|
+
if (derivedKey.length !== expectedKey.length) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
let result = 0;
|
|
86
|
+
for (let i = 0; i < derivedKey.length; i++) {
|
|
87
|
+
result |= derivedKey[i] ^ expectedKey[i];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return result === 0;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
94
|
+
// PARAMETER ESTIMATION
|
|
95
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Estimate Argon2id parameters based on available memory and target time.
|
|
99
|
+
* Useful for adapting to low-memory environments.
|
|
100
|
+
*
|
|
101
|
+
* @param targetTimeMs - Target key derivation time in milliseconds
|
|
102
|
+
* @param availableMemoryMB - Available memory in megabytes
|
|
103
|
+
* @returns Estimated Argon2id parameters
|
|
104
|
+
*/
|
|
105
|
+
export function estimateArgon2Params(
|
|
106
|
+
targetTimeMs: number = 1000,
|
|
107
|
+
availableMemoryMB: number = 64
|
|
108
|
+
): Argon2idParams {
|
|
109
|
+
// Start with minimum parameters
|
|
110
|
+
const params = { ...MIN_ARGON2ID_PARAMS };
|
|
111
|
+
|
|
112
|
+
// Scale memory based on availability (cap at 64MB for browser compatibility)
|
|
113
|
+
const maxMemoryKB = Math.min(availableMemoryMB * 1024, 65536);
|
|
114
|
+
params.memoryCost = Math.max(MIN_ARGON2ID_PARAMS.memoryCost, maxMemoryKB);
|
|
115
|
+
|
|
116
|
+
// Use 4 parallelism for modern multi-core CPUs
|
|
117
|
+
params.parallelism = Math.min(4, navigator.hardwareConcurrency || 1);
|
|
118
|
+
|
|
119
|
+
// Estimate time cost (rough heuristic: 1 iteration ≈ 300ms at 64MB)
|
|
120
|
+
const estimatedIterations = Math.max(2, Math.floor(targetTimeMs / 300));
|
|
121
|
+
params.timeCost = Math.min(estimatedIterations, 10); // Cap at 10 iterations
|
|
122
|
+
|
|
123
|
+
return params;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Benchmark Argon2id on current device.
|
|
128
|
+
* Returns the time in milliseconds for one derivation.
|
|
129
|
+
*
|
|
130
|
+
* @param params - Parameters to benchmark
|
|
131
|
+
* @returns Time in milliseconds
|
|
132
|
+
*/
|
|
133
|
+
export async function benchmarkArgon2(
|
|
134
|
+
params: Argon2idParams = DEFAULT_ARGON2ID_PARAMS
|
|
135
|
+
): Promise<number> {
|
|
136
|
+
const testPassword = 'benchmark-test-password';
|
|
137
|
+
const testSalt = generateArgon2Salt(params.saltLength);
|
|
138
|
+
|
|
139
|
+
const start = performance.now();
|
|
140
|
+
await deriveKeyFromPassword(testPassword, testSalt, params);
|
|
141
|
+
const end = performance.now();
|
|
142
|
+
|
|
143
|
+
return end - start;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
147
|
+
// AVAILABILITY CHECK
|
|
148
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
149
|
+
|
|
150
|
+
let _argon2Available: boolean | null = null;
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Check if Argon2id is available in current environment.
|
|
154
|
+
* Caches result after first check.
|
|
155
|
+
*/
|
|
156
|
+
export async function isArgon2Available(): Promise<boolean> {
|
|
157
|
+
if (_argon2Available !== null) {
|
|
158
|
+
return _argon2Available;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
// Quick test with minimal parameters
|
|
163
|
+
await argon2id({
|
|
164
|
+
password: 'test',
|
|
165
|
+
salt: new Uint8Array(16),
|
|
166
|
+
parallelism: 1,
|
|
167
|
+
iterations: 1,
|
|
168
|
+
memorySize: 1024, // 1 MB
|
|
169
|
+
hashLength: 32,
|
|
170
|
+
outputType: 'binary',
|
|
171
|
+
});
|
|
172
|
+
_argon2Available = true;
|
|
173
|
+
} catch {
|
|
174
|
+
_argon2Available = false;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return _argon2Available;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
181
|
+
// EXPORTS
|
|
182
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
183
|
+
|
|
184
|
+
export { DEFAULT_ARGON2ID_PARAMS, MIN_ARGON2ID_PARAMS };
|