@ursalock/crypto 0.2.1
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/dist/index.d.ts +285 -0
- package/dist/index.js +271 -0
- package/package.json +53 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { CipherJWK } from '@z-base/cryptosuite';
|
|
2
|
+
export { CipherJWK } from '@z-base/cryptosuite';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Interfaces for crypto operations (Dependency Inversion Principle)
|
|
6
|
+
* Allows testing and alternative implementations
|
|
7
|
+
*/
|
|
8
|
+
/** Encrypted data structure */
|
|
9
|
+
interface IEncryptedPayload {
|
|
10
|
+
/** Initialization vector */
|
|
11
|
+
iv: Uint8Array;
|
|
12
|
+
/** Ciphertext with auth tag */
|
|
13
|
+
ciphertext: Uint8Array;
|
|
14
|
+
/** Combined iv + ciphertext for storage */
|
|
15
|
+
combined: Uint8Array;
|
|
16
|
+
}
|
|
17
|
+
/** Crypto provider interface for encryption/decryption */
|
|
18
|
+
interface ICryptoProvider {
|
|
19
|
+
/**
|
|
20
|
+
* Encrypt data with AES-GCM
|
|
21
|
+
* @param plaintext Data to encrypt
|
|
22
|
+
* @param key 256-bit encryption key
|
|
23
|
+
* @param iv Optional IV (generated if not provided)
|
|
24
|
+
*/
|
|
25
|
+
encrypt(plaintext: Uint8Array, key: Uint8Array, iv?: Uint8Array): Promise<IEncryptedPayload>;
|
|
26
|
+
/**
|
|
27
|
+
* Decrypt data with AES-GCM
|
|
28
|
+
* @param encrypted Encrypted payload or combined bytes
|
|
29
|
+
* @param key 256-bit encryption key
|
|
30
|
+
*/
|
|
31
|
+
decrypt(encrypted: Uint8Array | IEncryptedPayload, key: Uint8Array): Promise<Uint8Array>;
|
|
32
|
+
}
|
|
33
|
+
/** Key derivation provider interface */
|
|
34
|
+
interface IKeyDerivationProvider {
|
|
35
|
+
/**
|
|
36
|
+
* Derive encryption key from password
|
|
37
|
+
* @param password Password bytes
|
|
38
|
+
* @param salt Salt (generated if not provided)
|
|
39
|
+
* @param options Derivation options
|
|
40
|
+
*/
|
|
41
|
+
deriveKey(password: Uint8Array, salt?: Uint8Array, options?: {
|
|
42
|
+
iterations?: number;
|
|
43
|
+
memorySize?: number;
|
|
44
|
+
parallelism?: number;
|
|
45
|
+
}): Promise<{
|
|
46
|
+
key: Uint8Array;
|
|
47
|
+
salt: Uint8Array;
|
|
48
|
+
}>;
|
|
49
|
+
}
|
|
50
|
+
/** Random number generator interface */
|
|
51
|
+
interface IRandomProvider {
|
|
52
|
+
/**
|
|
53
|
+
* Generate cryptographically secure random bytes
|
|
54
|
+
* @param length Number of bytes to generate
|
|
55
|
+
*/
|
|
56
|
+
randomBytes(length: number): Uint8Array;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Web Crypto API implementation (Concrete provider)
|
|
61
|
+
* Implements ICryptoProvider using browser's native crypto
|
|
62
|
+
*/
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Web Crypto API provider for AES-256-GCM
|
|
66
|
+
*/
|
|
67
|
+
declare class WebCryptoProvider implements ICryptoProvider {
|
|
68
|
+
encrypt(plaintext: Uint8Array, key: Uint8Array, iv?: Uint8Array): Promise<IEncryptedPayload>;
|
|
69
|
+
decrypt(encrypted: Uint8Array | IEncryptedPayload, key: Uint8Array): Promise<Uint8Array>;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Key derivation using Argon2id
|
|
74
|
+
*
|
|
75
|
+
* Parameters based on OWASP 2026 recommendations:
|
|
76
|
+
* - Memory: 64 MiB
|
|
77
|
+
* - Iterations: 3
|
|
78
|
+
* - Parallelism: 4
|
|
79
|
+
*/
|
|
80
|
+
interface DeriveKeyOptions {
|
|
81
|
+
/** Password or recovery key bytes */
|
|
82
|
+
password: Uint8Array;
|
|
83
|
+
/** Salt (32 bytes recommended, auto-generated if not provided) */
|
|
84
|
+
salt?: Uint8Array;
|
|
85
|
+
/** Memory cost in KiB (default: 65536 = 64 MiB) */
|
|
86
|
+
memoryCost?: number;
|
|
87
|
+
/** Time cost / iterations (default: 3) */
|
|
88
|
+
timeCost?: number;
|
|
89
|
+
/** Parallelism (default: 4) */
|
|
90
|
+
parallelism?: number;
|
|
91
|
+
/** Output key length in bytes (default: 32 for AES-256) */
|
|
92
|
+
keyLength?: number;
|
|
93
|
+
}
|
|
94
|
+
interface DerivedKey {
|
|
95
|
+
/** The derived key bytes */
|
|
96
|
+
key: Uint8Array;
|
|
97
|
+
/** The salt used (save this for re-derivation) */
|
|
98
|
+
salt: Uint8Array;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Derive an encryption key from a password/recovery key using Argon2id
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* ```ts
|
|
105
|
+
* const { key, salt } = await deriveKey({
|
|
106
|
+
* password: recoveryKeyBytes,
|
|
107
|
+
* })
|
|
108
|
+
* // Save salt alongside encrypted data
|
|
109
|
+
* // Use key for AES-256-GCM encryption
|
|
110
|
+
* ```
|
|
111
|
+
*/
|
|
112
|
+
declare function deriveKey(options: DeriveKeyOptions): Promise<DerivedKey>;
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* AES-256-GCM encryption/decryption using Web Crypto API
|
|
116
|
+
*
|
|
117
|
+
* - 256-bit key
|
|
118
|
+
* - 96-bit (12 byte) IV (NIST recommended for GCM)
|
|
119
|
+
* - 128-bit auth tag (included in ciphertext by Web Crypto)
|
|
120
|
+
*
|
|
121
|
+
* Refactored to follow Dependency Inversion Principle:
|
|
122
|
+
* - Uses ICryptoProvider interface
|
|
123
|
+
* - Default implementation uses WebCryptoProvider
|
|
124
|
+
* - Can inject alternative implementations for testing
|
|
125
|
+
*/
|
|
126
|
+
|
|
127
|
+
/** Encrypted payload structure (backward compatibility) */
|
|
128
|
+
interface EncryptedPayload extends IEncryptedPayload {
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Set custom crypto provider (for testing or alternative implementations)
|
|
132
|
+
* @param provider Custom crypto provider
|
|
133
|
+
*/
|
|
134
|
+
declare function setCryptoProvider(provider: ICryptoProvider): void;
|
|
135
|
+
/**
|
|
136
|
+
* Get current crypto provider
|
|
137
|
+
*/
|
|
138
|
+
declare function getCryptoProvider(): ICryptoProvider;
|
|
139
|
+
/**
|
|
140
|
+
* Encrypt data using AES-256-GCM
|
|
141
|
+
*
|
|
142
|
+
* @param plaintext - Data to encrypt
|
|
143
|
+
* @param key - 256-bit encryption key
|
|
144
|
+
* @param provider - Optional crypto provider (uses default if not provided)
|
|
145
|
+
* @returns Encrypted payload with IV
|
|
146
|
+
*
|
|
147
|
+
* @example
|
|
148
|
+
* ```ts
|
|
149
|
+
* const data = new TextEncoder().encode(JSON.stringify(state))
|
|
150
|
+
* const encrypted = await encrypt(data, key)
|
|
151
|
+
* // Store encrypted.combined (iv + ciphertext)
|
|
152
|
+
* ```
|
|
153
|
+
*/
|
|
154
|
+
declare function encrypt(plaintext: Uint8Array, key: Uint8Array, provider?: ICryptoProvider): Promise<EncryptedPayload>;
|
|
155
|
+
/**
|
|
156
|
+
* Decrypt data using AES-256-GCM
|
|
157
|
+
*
|
|
158
|
+
* @param encrypted - Combined IV + ciphertext, or separate components
|
|
159
|
+
* @param key - 256-bit encryption key
|
|
160
|
+
* @param provider - Optional crypto provider (uses default if not provided)
|
|
161
|
+
* @returns Decrypted plaintext
|
|
162
|
+
*
|
|
163
|
+
* @example
|
|
164
|
+
* ```ts
|
|
165
|
+
* const plaintext = await decrypt(encrypted.combined, key)
|
|
166
|
+
* const state = JSON.parse(new TextDecoder().decode(plaintext))
|
|
167
|
+
* ```
|
|
168
|
+
*/
|
|
169
|
+
declare function decrypt(encrypted: Uint8Array | EncryptedPayload, key: Uint8Array, provider?: ICryptoProvider): Promise<Uint8Array>;
|
|
170
|
+
/**
|
|
171
|
+
* Encrypt a string (convenience wrapper)
|
|
172
|
+
*/
|
|
173
|
+
declare function encryptString(plaintext: string, key: Uint8Array, provider?: ICryptoProvider): Promise<EncryptedPayload>;
|
|
174
|
+
/**
|
|
175
|
+
* Decrypt to a string (convenience wrapper)
|
|
176
|
+
*/
|
|
177
|
+
declare function decryptString(encrypted: Uint8Array | EncryptedPayload, key: Uint8Array, provider?: ICryptoProvider): Promise<string>;
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Recovery key generation and validation
|
|
181
|
+
*
|
|
182
|
+
* Recovery key format:
|
|
183
|
+
* - 256 bits of entropy (32 bytes)
|
|
184
|
+
* - Encoded as base32 (RFC 4648, no padding)
|
|
185
|
+
* - Split into groups of 4 chars with dashes for readability
|
|
186
|
+
* - Total: 52 characters + 12 dashes = 64 characters
|
|
187
|
+
*
|
|
188
|
+
* Example: ABCD-EFGH-IJKL-MNOP-QRST-UVWX-YZ23-4567-ABCD-EFGH-IJKL-MNOP-Q
|
|
189
|
+
*/
|
|
190
|
+
/** Recovery key with metadata */
|
|
191
|
+
interface RecoveryKey {
|
|
192
|
+
/** Human-readable recovery key with dashes */
|
|
193
|
+
formatted: string;
|
|
194
|
+
/** Raw recovery key without dashes */
|
|
195
|
+
raw: string;
|
|
196
|
+
/** Original bytes (32 bytes = 256 bits) */
|
|
197
|
+
bytes: Uint8Array;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Generate a new recovery key
|
|
201
|
+
*
|
|
202
|
+
* @returns Recovery key in multiple formats
|
|
203
|
+
*
|
|
204
|
+
* @example
|
|
205
|
+
* ```ts
|
|
206
|
+
* const recovery = generateRecoveryKey()
|
|
207
|
+
* console.log(recovery.formatted)
|
|
208
|
+
* // "ABCD-EFGH-IJKL-MNOP-QRST-UVWX-YZ23-4567-ABCD-EFGH-IJKL-MNOP-Q"
|
|
209
|
+
* ```
|
|
210
|
+
*/
|
|
211
|
+
declare function generateRecoveryKey(): RecoveryKey;
|
|
212
|
+
/**
|
|
213
|
+
* Validate a recovery key format
|
|
214
|
+
*
|
|
215
|
+
* @param key - Recovery key (with or without dashes)
|
|
216
|
+
* @returns True if valid format
|
|
217
|
+
*/
|
|
218
|
+
declare function validateRecoveryKey(key: string): boolean;
|
|
219
|
+
/**
|
|
220
|
+
* Convert recovery key string to bytes
|
|
221
|
+
*
|
|
222
|
+
* @param key - Recovery key (with or without dashes)
|
|
223
|
+
* @returns 32 bytes
|
|
224
|
+
*/
|
|
225
|
+
declare function recoveryKeyToBytes(key: string): Uint8Array;
|
|
226
|
+
/**
|
|
227
|
+
* Convert bytes to recovery key string
|
|
228
|
+
*
|
|
229
|
+
* @param bytes - 32 bytes
|
|
230
|
+
* @returns Raw base32 string (no dashes)
|
|
231
|
+
*/
|
|
232
|
+
declare function bytesToRecoveryKey(bytes: Uint8Array): string;
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Crypto utilities
|
|
236
|
+
*/
|
|
237
|
+
/**
|
|
238
|
+
* Generate cryptographically secure random bytes
|
|
239
|
+
*/
|
|
240
|
+
declare function randomBytes(length: number): Uint8Array;
|
|
241
|
+
/**
|
|
242
|
+
* Constant-time comparison to prevent timing attacks
|
|
243
|
+
*/
|
|
244
|
+
declare function constantTimeEqual(a: Uint8Array, b: Uint8Array): boolean;
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* JWK-based encryption using @z-base/cryptosuite
|
|
248
|
+
* For use with ZKCredentials PRF-derived keys
|
|
249
|
+
*/
|
|
250
|
+
|
|
251
|
+
/** Encrypted payload with IV and ciphertext */
|
|
252
|
+
interface JwkEncryptedPayload {
|
|
253
|
+
/** Initialization vector (12 bytes for AES-GCM) */
|
|
254
|
+
iv: Uint8Array;
|
|
255
|
+
/** Encrypted data */
|
|
256
|
+
ciphertext: Uint8Array;
|
|
257
|
+
/** Combined iv + ciphertext for easy storage */
|
|
258
|
+
combined: Uint8Array;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Encrypt data using AES-256-GCM with a CipherJWK
|
|
262
|
+
*
|
|
263
|
+
* @param plaintext - Data to encrypt
|
|
264
|
+
* @param cipherJwk - JWK key from ZKCredentials
|
|
265
|
+
* @returns Encrypted payload with IV
|
|
266
|
+
*/
|
|
267
|
+
declare function encryptWithJwk(plaintext: Uint8Array, cipherJwk: CipherJWK): Promise<JwkEncryptedPayload>;
|
|
268
|
+
/**
|
|
269
|
+
* Decrypt data using AES-256-GCM with a CipherJWK
|
|
270
|
+
*
|
|
271
|
+
* @param encrypted - Combined IV + ciphertext, or separate components
|
|
272
|
+
* @param cipherJwk - JWK key from ZKCredentials
|
|
273
|
+
* @returns Decrypted plaintext
|
|
274
|
+
*/
|
|
275
|
+
declare function decryptWithJwk(encrypted: Uint8Array | JwkEncryptedPayload, cipherJwk: CipherJWK): Promise<Uint8Array>;
|
|
276
|
+
/**
|
|
277
|
+
* Encrypt a string using CipherJWK
|
|
278
|
+
*/
|
|
279
|
+
declare function encryptStringWithJwk(plaintext: string, cipherJwk: CipherJWK): Promise<JwkEncryptedPayload>;
|
|
280
|
+
/**
|
|
281
|
+
* Decrypt to a string using CipherJWK
|
|
282
|
+
*/
|
|
283
|
+
declare function decryptStringWithJwk(encrypted: Uint8Array | JwkEncryptedPayload, cipherJwk: CipherJWK): Promise<string>;
|
|
284
|
+
|
|
285
|
+
export { type DeriveKeyOptions, type EncryptedPayload, type ICryptoProvider, type IEncryptedPayload, type IKeyDerivationProvider, type IRandomProvider, type JwkEncryptedPayload, type RecoveryKey, WebCryptoProvider, bytesToRecoveryKey, constantTimeEqual, decrypt, decryptString, decryptStringWithJwk, decryptWithJwk, deriveKey, encrypt, encryptString, encryptStringWithJwk, encryptWithJwk, generateRecoveryKey, getCryptoProvider, randomBytes, recoveryKeyToBytes, setCryptoProvider, validateRecoveryKey };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
// src/utils.ts
|
|
2
|
+
function randomBytes(length) {
|
|
3
|
+
const bytes = new Uint8Array(length);
|
|
4
|
+
crypto.getRandomValues(bytes);
|
|
5
|
+
return bytes;
|
|
6
|
+
}
|
|
7
|
+
function constantTimeEqual(a, b) {
|
|
8
|
+
if (a.length !== b.length) return false;
|
|
9
|
+
let result = 0;
|
|
10
|
+
for (let i = 0; i < a.length; i++) {
|
|
11
|
+
result |= a[i] ^ b[i];
|
|
12
|
+
}
|
|
13
|
+
return result === 0;
|
|
14
|
+
}
|
|
15
|
+
function concatBytes(...arrays) {
|
|
16
|
+
const totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0);
|
|
17
|
+
const result = new Uint8Array(totalLength);
|
|
18
|
+
let offset = 0;
|
|
19
|
+
for (const arr of arrays) {
|
|
20
|
+
result.set(arr, offset);
|
|
21
|
+
offset += arr.length;
|
|
22
|
+
}
|
|
23
|
+
return result;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// src/providers/web-crypto.ts
|
|
27
|
+
var IV_LENGTH = 12;
|
|
28
|
+
var WebCryptoProvider = class {
|
|
29
|
+
async encrypt(plaintext, key, iv) {
|
|
30
|
+
if (key.length !== 32) {
|
|
31
|
+
throw new Error(`Invalid key length: expected 32 bytes, got ${key.length}`);
|
|
32
|
+
}
|
|
33
|
+
const actualIv = iv ?? randomBytes(IV_LENGTH);
|
|
34
|
+
const cryptoKey = await crypto.subtle.importKey(
|
|
35
|
+
"raw",
|
|
36
|
+
key.buffer,
|
|
37
|
+
{ name: "AES-GCM" },
|
|
38
|
+
false,
|
|
39
|
+
["encrypt"]
|
|
40
|
+
);
|
|
41
|
+
const ciphertext = new Uint8Array(
|
|
42
|
+
await crypto.subtle.encrypt(
|
|
43
|
+
{ name: "AES-GCM", iv: actualIv },
|
|
44
|
+
cryptoKey,
|
|
45
|
+
plaintext.buffer
|
|
46
|
+
)
|
|
47
|
+
);
|
|
48
|
+
const combined = concatBytes(actualIv, ciphertext);
|
|
49
|
+
return { iv: actualIv, ciphertext, combined };
|
|
50
|
+
}
|
|
51
|
+
async decrypt(encrypted, key) {
|
|
52
|
+
if (key.length !== 32) {
|
|
53
|
+
throw new Error(`Invalid key length: expected 32 bytes, got ${key.length}`);
|
|
54
|
+
}
|
|
55
|
+
let iv;
|
|
56
|
+
let ciphertext;
|
|
57
|
+
if (encrypted instanceof Uint8Array) {
|
|
58
|
+
if (encrypted.length < IV_LENGTH + 16) {
|
|
59
|
+
throw new Error("Invalid encrypted data: too short");
|
|
60
|
+
}
|
|
61
|
+
iv = encrypted.slice(0, IV_LENGTH);
|
|
62
|
+
ciphertext = encrypted.slice(IV_LENGTH);
|
|
63
|
+
} else {
|
|
64
|
+
iv = encrypted.iv;
|
|
65
|
+
ciphertext = encrypted.ciphertext;
|
|
66
|
+
}
|
|
67
|
+
const cryptoKey = await crypto.subtle.importKey(
|
|
68
|
+
"raw",
|
|
69
|
+
key.buffer,
|
|
70
|
+
{ name: "AES-GCM" },
|
|
71
|
+
false,
|
|
72
|
+
["decrypt"]
|
|
73
|
+
);
|
|
74
|
+
try {
|
|
75
|
+
const plaintext = await crypto.subtle.decrypt(
|
|
76
|
+
{ name: "AES-GCM", iv },
|
|
77
|
+
cryptoKey,
|
|
78
|
+
ciphertext.buffer
|
|
79
|
+
);
|
|
80
|
+
return new Uint8Array(plaintext);
|
|
81
|
+
} catch (error) {
|
|
82
|
+
throw new Error("Decryption failed: invalid key or corrupted data");
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// src/derive.ts
|
|
88
|
+
import { argon2id } from "hash-wasm";
|
|
89
|
+
async function deriveKey(options) {
|
|
90
|
+
const {
|
|
91
|
+
password,
|
|
92
|
+
salt = randomBytes(32),
|
|
93
|
+
memoryCost = 65536,
|
|
94
|
+
// 64 MiB
|
|
95
|
+
timeCost = 3,
|
|
96
|
+
parallelism = 4,
|
|
97
|
+
keyLength = 32
|
|
98
|
+
} = options;
|
|
99
|
+
const hash = await argon2id({
|
|
100
|
+
password,
|
|
101
|
+
salt,
|
|
102
|
+
memorySize: memoryCost,
|
|
103
|
+
iterations: timeCost,
|
|
104
|
+
parallelism,
|
|
105
|
+
hashLength: keyLength,
|
|
106
|
+
outputType: "binary"
|
|
107
|
+
});
|
|
108
|
+
return {
|
|
109
|
+
key: new Uint8Array(hash),
|
|
110
|
+
salt
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// src/aes.ts
|
|
115
|
+
var defaultProvider = new WebCryptoProvider();
|
|
116
|
+
function setCryptoProvider(provider) {
|
|
117
|
+
defaultProvider = provider;
|
|
118
|
+
}
|
|
119
|
+
function getCryptoProvider() {
|
|
120
|
+
return defaultProvider;
|
|
121
|
+
}
|
|
122
|
+
async function encrypt(plaintext, key, provider = defaultProvider) {
|
|
123
|
+
return provider.encrypt(plaintext, key);
|
|
124
|
+
}
|
|
125
|
+
async function decrypt(encrypted, key, provider = defaultProvider) {
|
|
126
|
+
return provider.decrypt(encrypted, key);
|
|
127
|
+
}
|
|
128
|
+
async function encryptString(plaintext, key, provider) {
|
|
129
|
+
const data = new TextEncoder().encode(plaintext);
|
|
130
|
+
return encrypt(data, key, provider);
|
|
131
|
+
}
|
|
132
|
+
async function decryptString(encrypted, key, provider) {
|
|
133
|
+
const plaintext = await decrypt(encrypted, key, provider);
|
|
134
|
+
return new TextDecoder().decode(plaintext);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// src/recovery.ts
|
|
138
|
+
var BASE32_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
|
139
|
+
function generateRecoveryKey() {
|
|
140
|
+
const bytes = randomBytes(32);
|
|
141
|
+
const raw = bytesToRecoveryKey(bytes);
|
|
142
|
+
const formatted = formatRecoveryKey(raw);
|
|
143
|
+
return { formatted, raw, bytes };
|
|
144
|
+
}
|
|
145
|
+
function validateRecoveryKey(key) {
|
|
146
|
+
const clean = key.replace(/[-\s]/g, "").toUpperCase();
|
|
147
|
+
if (clean.length !== 52) return false;
|
|
148
|
+
for (const char of clean) {
|
|
149
|
+
if (!BASE32_ALPHABET.includes(char)) return false;
|
|
150
|
+
}
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
function recoveryKeyToBytes(key) {
|
|
154
|
+
const clean = key.replace(/[-\s]/g, "").toUpperCase();
|
|
155
|
+
if (!validateRecoveryKey(clean)) {
|
|
156
|
+
throw new Error("Invalid recovery key format");
|
|
157
|
+
}
|
|
158
|
+
return base32Decode(clean);
|
|
159
|
+
}
|
|
160
|
+
function bytesToRecoveryKey(bytes) {
|
|
161
|
+
if (bytes.length !== 32) {
|
|
162
|
+
throw new Error(`Invalid bytes length: expected 32, got ${bytes.length}`);
|
|
163
|
+
}
|
|
164
|
+
return base32Encode(bytes);
|
|
165
|
+
}
|
|
166
|
+
function formatRecoveryKey(raw) {
|
|
167
|
+
const chunks = [];
|
|
168
|
+
for (let i = 0; i < raw.length; i += 4) {
|
|
169
|
+
chunks.push(raw.slice(i, i + 4));
|
|
170
|
+
}
|
|
171
|
+
return chunks.join("-");
|
|
172
|
+
}
|
|
173
|
+
function base32Encode(bytes) {
|
|
174
|
+
let result = "";
|
|
175
|
+
let bits = 0;
|
|
176
|
+
let value = 0;
|
|
177
|
+
for (const byte of bytes) {
|
|
178
|
+
value = value << 8 | byte;
|
|
179
|
+
bits += 8;
|
|
180
|
+
while (bits >= 5) {
|
|
181
|
+
bits -= 5;
|
|
182
|
+
result += BASE32_ALPHABET[value >> bits & 31];
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (bits > 0) {
|
|
186
|
+
result += BASE32_ALPHABET[value << 5 - bits & 31];
|
|
187
|
+
}
|
|
188
|
+
return result;
|
|
189
|
+
}
|
|
190
|
+
function base32Decode(str) {
|
|
191
|
+
const output = [];
|
|
192
|
+
let bits = 0;
|
|
193
|
+
let value = 0;
|
|
194
|
+
for (const char of str) {
|
|
195
|
+
const index = BASE32_ALPHABET.indexOf(char);
|
|
196
|
+
if (index === -1) {
|
|
197
|
+
throw new Error(`Invalid base32 character: ${char}`);
|
|
198
|
+
}
|
|
199
|
+
value = value << 5 | index;
|
|
200
|
+
bits += 5;
|
|
201
|
+
while (bits >= 8) {
|
|
202
|
+
bits -= 8;
|
|
203
|
+
output.push(value >> bits & 255);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return new Uint8Array(output);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// src/jwk.ts
|
|
210
|
+
import { CipherCluster } from "@z-base/cryptosuite";
|
|
211
|
+
async function encryptWithJwk(plaintext, cipherJwk) {
|
|
212
|
+
const result = await CipherCluster.encrypt(cipherJwk, plaintext);
|
|
213
|
+
const iv = result.iv;
|
|
214
|
+
const ciphertext = new Uint8Array(result.ciphertext);
|
|
215
|
+
const combined = new Uint8Array(iv.length + ciphertext.length);
|
|
216
|
+
combined.set(iv, 0);
|
|
217
|
+
combined.set(ciphertext, iv.length);
|
|
218
|
+
return { iv, ciphertext, combined };
|
|
219
|
+
}
|
|
220
|
+
async function decryptWithJwk(encrypted, cipherJwk) {
|
|
221
|
+
let iv;
|
|
222
|
+
let ciphertext;
|
|
223
|
+
if (encrypted instanceof Uint8Array) {
|
|
224
|
+
if (encrypted.length < 12 + 16) {
|
|
225
|
+
throw new Error("Invalid encrypted data: too short");
|
|
226
|
+
}
|
|
227
|
+
iv = encrypted.slice(0, 12);
|
|
228
|
+
ciphertext = encrypted.slice(12);
|
|
229
|
+
} else {
|
|
230
|
+
iv = encrypted.iv;
|
|
231
|
+
ciphertext = encrypted.ciphertext;
|
|
232
|
+
}
|
|
233
|
+
try {
|
|
234
|
+
const ciphertextBuffer = new ArrayBuffer(ciphertext.length);
|
|
235
|
+
new Uint8Array(ciphertextBuffer).set(ciphertext);
|
|
236
|
+
return await CipherCluster.decrypt(cipherJwk, {
|
|
237
|
+
iv,
|
|
238
|
+
ciphertext: ciphertextBuffer
|
|
239
|
+
});
|
|
240
|
+
} catch {
|
|
241
|
+
throw new Error("Decryption failed: invalid key or corrupted data");
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
async function encryptStringWithJwk(plaintext, cipherJwk) {
|
|
245
|
+
const data = new TextEncoder().encode(plaintext);
|
|
246
|
+
return encryptWithJwk(data, cipherJwk);
|
|
247
|
+
}
|
|
248
|
+
async function decryptStringWithJwk(encrypted, cipherJwk) {
|
|
249
|
+
const plaintext = await decryptWithJwk(encrypted, cipherJwk);
|
|
250
|
+
return new TextDecoder().decode(plaintext);
|
|
251
|
+
}
|
|
252
|
+
export {
|
|
253
|
+
WebCryptoProvider,
|
|
254
|
+
bytesToRecoveryKey,
|
|
255
|
+
constantTimeEqual,
|
|
256
|
+
decrypt,
|
|
257
|
+
decryptString,
|
|
258
|
+
decryptStringWithJwk,
|
|
259
|
+
decryptWithJwk,
|
|
260
|
+
deriveKey,
|
|
261
|
+
encrypt,
|
|
262
|
+
encryptString,
|
|
263
|
+
encryptStringWithJwk,
|
|
264
|
+
encryptWithJwk,
|
|
265
|
+
generateRecoveryKey,
|
|
266
|
+
getCryptoProvider,
|
|
267
|
+
randomBytes,
|
|
268
|
+
recoveryKeyToBytes,
|
|
269
|
+
setCryptoProvider,
|
|
270
|
+
validateRecoveryKey
|
|
271
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ursalock/crypto",
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "E2EE crypto primitives for ursalock",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsup src/index.ts --format esm --dts",
|
|
20
|
+
"test": "vitest run",
|
|
21
|
+
"test:watch": "vitest",
|
|
22
|
+
"typecheck": "tsc --noEmit"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@z-base/cryptosuite": "^1.0.1",
|
|
26
|
+
"hash-wasm": "^4.11.0"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"tsup": "^8.0.0",
|
|
30
|
+
"typescript": "^5.4.0",
|
|
31
|
+
"vitest": "^1.6.0"
|
|
32
|
+
},
|
|
33
|
+
"keywords": [
|
|
34
|
+
"encryption",
|
|
35
|
+
"e2ee",
|
|
36
|
+
"aes-256-gcm",
|
|
37
|
+
"argon2id",
|
|
38
|
+
"crypto",
|
|
39
|
+
"zero-knowledge",
|
|
40
|
+
"jwk"
|
|
41
|
+
],
|
|
42
|
+
"license": "MIT",
|
|
43
|
+
"author": "Nicolas de Luz <ndlz@pm.me>",
|
|
44
|
+
"homepage": "https://github.com/nicodlz/ursalock#readme",
|
|
45
|
+
"bugs": {
|
|
46
|
+
"url": "https://github.com/nicodlz/ursalock/issues"
|
|
47
|
+
},
|
|
48
|
+
"repository": {
|
|
49
|
+
"type": "git",
|
|
50
|
+
"url": "https://github.com/nicodlz/ursalock.git",
|
|
51
|
+
"directory": "packages/crypto"
|
|
52
|
+
}
|
|
53
|
+
}
|