@cloudpss/crypto 0.5.24 → 0.5.26

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 (43) hide show
  1. package/benchmark.js +44 -0
  2. package/dist/encryption/browser.d.ts +3 -3
  3. package/dist/encryption/browser.js +1 -2
  4. package/dist/encryption/browser.js.map +1 -1
  5. package/dist/encryption/common.d.ts +45 -16
  6. package/dist/encryption/common.js +59 -9
  7. package/dist/encryption/common.js.map +1 -1
  8. package/dist/encryption/index.d.ts +4 -21
  9. package/dist/encryption/index.js +11 -63
  10. package/dist/encryption/index.js.map +1 -1
  11. package/dist/encryption/module.d.ts +22 -0
  12. package/dist/encryption/module.js +62 -0
  13. package/dist/encryption/module.js.map +1 -0
  14. package/dist/encryption/node.d.ts +3 -3
  15. package/dist/encryption/node.js +19 -15
  16. package/dist/encryption/node.js.map +1 -1
  17. package/dist/encryption/wasm.d.ts +5 -0
  18. package/dist/encryption/wasm.js +21 -0
  19. package/dist/encryption/wasm.js.map +1 -0
  20. package/dist/encryption/web.d.ts +3 -3
  21. package/dist/encryption/web.js +17 -15
  22. package/dist/encryption/web.js.map +1 -1
  23. package/dist/index.d.ts +1 -1
  24. package/dist/index.js +1 -1
  25. package/dist/index.js.map +1 -1
  26. package/lib/wasm.d.ts +26 -0
  27. package/lib/wasm.js +149 -0
  28. package/package.json +11 -10
  29. package/src/encryption/browser.ts +4 -5
  30. package/src/encryption/common.ts +83 -16
  31. package/src/encryption/index.ts +12 -71
  32. package/src/encryption/module.ts +94 -0
  33. package/src/encryption/node.ts +24 -15
  34. package/src/encryption/wasm.ts +46 -0
  35. package/src/encryption/web.ts +24 -15
  36. package/src/index.ts +1 -1
  37. package/tests/encryption.js +151 -55
  38. package/tsconfig.json +2 -1
  39. package/wasm-build.js +30 -0
  40. package/dist/encryption/pure-js.d.ts +0 -5
  41. package/dist/encryption/pure-js.js +0 -54
  42. package/dist/encryption/pure-js.js.map +0 -1
  43. package/src/encryption/pure-js.ts +0 -62
@@ -0,0 +1,94 @@
1
+ import { toUint8Array } from '../utils.js';
2
+ import {
3
+ AAD_LEN_SIZE,
4
+ AAD_MAX_SIZE,
5
+ AAD_PADDING,
6
+ MAGIC_NUMBER,
7
+ NONCE_SIZE,
8
+ padding,
9
+ parseEncrypted,
10
+ type PlainData,
11
+ } from './common.js';
12
+
13
+ /** 检查密码 */
14
+ function assertPassphrase(passphrase: string): void {
15
+ if (typeof passphrase !== 'string') {
16
+ throw new TypeError('Invalid passphrase, must be a string');
17
+ }
18
+ if (passphrase.length === 0) {
19
+ throw new TypeError('Invalid passphrase, must not be empty');
20
+ }
21
+ }
22
+
23
+ /** 模块 */
24
+ interface Module {
25
+ /**
26
+ * 加密数据
27
+ * @throws {TypeError} 如果密码无效
28
+ */
29
+ encrypt(data: BinaryData, passphrase: string): Promise<Uint8Array>;
30
+ /**
31
+ * 加密数据,包含不加密的附加数据
32
+ * @throws {TypeError} 如果密码无效
33
+ */
34
+ encryptAad(data: BinaryData, aad: BinaryData | undefined, passphrase: string): Promise<Uint8Array>;
35
+ /**
36
+ * 解密数据
37
+ * @throws {TypeError} 如果数据不是有效的加密数据
38
+ * @throws {TypeError} 如果密码无效
39
+ */
40
+ decrypt(data: BinaryData, passphrase: string): Promise<Uint8Array>;
41
+ }
42
+
43
+ /** 创建模块 */
44
+ export function createModule(impl: typeof import('#encryption') | typeof import('./wasm.js')): Module {
45
+ const encryptAad: Module['encryptAad'] = async (data, aad, passphrase) => {
46
+ assertPassphrase(passphrase);
47
+ const aadSize = aad?.byteLength ?? 0;
48
+ if (aadSize > AAD_MAX_SIZE) {
49
+ throw new TypeError('Invalid AAD size');
50
+ }
51
+ const paddedAddSize = padding(aadSize, AAD_PADDING);
52
+ const plain: PlainData = {
53
+ aad: aadSize ? toUint8Array(aad!) : undefined,
54
+ data: toUint8Array(data),
55
+ };
56
+ const encrypted = await impl.encrypt(plain, passphrase);
57
+ const result = new Uint8Array(
58
+ MAGIC_NUMBER.length + NONCE_SIZE + AAD_LEN_SIZE + paddedAddSize + encrypted.data.length,
59
+ );
60
+ result.set(MAGIC_NUMBER);
61
+ result.set(encrypted.nonce, MAGIC_NUMBER.length);
62
+ if (aadSize) {
63
+ result[MAGIC_NUMBER.length + NONCE_SIZE] = aadSize >>> 24;
64
+ result[MAGIC_NUMBER.length + NONCE_SIZE + 1] = aadSize >>> 16;
65
+ result[MAGIC_NUMBER.length + NONCE_SIZE + 2] = aadSize >>> 8;
66
+ result[MAGIC_NUMBER.length + NONCE_SIZE + 3] = aadSize;
67
+ result.set(plain.aad!, MAGIC_NUMBER.length + NONCE_SIZE + AAD_LEN_SIZE);
68
+ }
69
+ result.set(encrypted.data, MAGIC_NUMBER.length + NONCE_SIZE + AAD_LEN_SIZE + paddedAddSize);
70
+ return result;
71
+ };
72
+ const encrypt: Module['encrypt'] = async (data, passphrase) => {
73
+ return await encryptAad(data, undefined, passphrase);
74
+ };
75
+
76
+ const decrypt: Module['decrypt'] = async (data, passphrase) => {
77
+ assertPassphrase(passphrase);
78
+ const encrypted = parseEncrypted(data);
79
+ if (encrypted == null) {
80
+ throw new TypeError('Invalid encrypted data');
81
+ }
82
+ try {
83
+ const result = await impl.decrypt(encrypted, passphrase);
84
+ return result.data;
85
+ } catch (ex) {
86
+ throw new Error('Wrong passphrase', { cause: ex });
87
+ }
88
+ };
89
+ return {
90
+ encrypt,
91
+ encryptAad,
92
+ decrypt,
93
+ };
94
+ }
@@ -1,30 +1,39 @@
1
1
  import { pbkdf2 as _pbkdf2, createCipheriv, createDecipheriv, randomBytes } from 'node:crypto';
2
- import { PBKDF2_ITERATIONS, PBKDF2_SALT_SIZE, type EncryptionResult, AES_IV_SIZE, AES_KEY_SIZE } from './common.js';
2
+ import {
3
+ PBKDF2_ITERATIONS,
4
+ NONCE_SIZE,
5
+ AES_TAG_SIZE,
6
+ AES_KEY_SIZE,
7
+ type EncryptedData,
8
+ type PlainData,
9
+ } from './common.js';
3
10
  import { promisify } from 'node:util';
4
11
  import { toUint8Array } from '../utils.js';
5
12
 
13
+ const pbkdf2 = promisify(_pbkdf2);
6
14
  const aesKdf = (passphrase: string, salt: Uint8Array): Promise<Buffer> => {
7
- return promisify(_pbkdf2)(passphrase, salt, PBKDF2_ITERATIONS, AES_KEY_SIZE, 'sha256');
15
+ return pbkdf2(passphrase, salt, PBKDF2_ITERATIONS, AES_KEY_SIZE, 'sha256');
8
16
  };
9
17
 
10
18
  /** nodejs encrypt */
11
- export async function encrypt(data: Uint8Array, passphrase: string): Promise<EncryptionResult> {
12
- const salt = randomBytes(PBKDF2_SALT_SIZE);
13
- const key = await aesKdf(passphrase, salt);
14
- const iv = randomBytes(AES_IV_SIZE);
15
- const cipher = createCipheriv('aes-256-cbc', key, iv);
16
- const encrypted = Buffer.concat([cipher.update(data), cipher.final()]);
19
+ export async function encrypt({ data, aad }: PlainData, passphrase: string): Promise<EncryptedData> {
20
+ const nonce = randomBytes(NONCE_SIZE);
21
+ const key = await aesKdf(passphrase, nonce);
22
+ const cipher = createCipheriv('aes-256-gcm', key, nonce, { authTagLength: AES_TAG_SIZE });
23
+ if (aad) cipher.setAAD(aad);
24
+ const encrypted = Buffer.concat([cipher.update(data), cipher.final(), cipher.getAuthTag()]);
17
25
  return {
18
- salt: toUint8Array(salt),
19
- iv: toUint8Array(iv),
26
+ nonce: toUint8Array(nonce),
20
27
  data: toUint8Array(encrypted),
21
28
  };
22
29
  }
23
30
 
24
31
  /** nodejs decrypt */
25
- export async function decrypt({ data, iv, salt }: EncryptionResult, passphrase: string): Promise<Uint8Array> {
26
- const key = await aesKdf(passphrase, salt);
27
- const decipher = createDecipheriv('aes-256-cbc', key, iv);
28
- const decrypted = Buffer.concat([decipher.update(data), decipher.final()]);
29
- return toUint8Array(decrypted);
32
+ export async function decrypt({ nonce, aad, data }: EncryptedData, passphrase: string): Promise<PlainData> {
33
+ const key = await aesKdf(passphrase, nonce);
34
+ const decipher = createDecipheriv('aes-256-gcm', key, nonce, { authTagLength: AES_TAG_SIZE });
35
+ decipher.setAuthTag(data.subarray(data.length - AES_TAG_SIZE));
36
+ if (aad) decipher.setAAD(aad);
37
+ const decrypted = Buffer.concat([decipher.update(data.subarray(0, data.length - AES_TAG_SIZE)), decipher.final()]);
38
+ return { data: toUint8Array(decrypted) };
30
39
  }
@@ -0,0 +1,46 @@
1
+ import {
2
+ NONCE_SIZE,
3
+ AES_KEY_SIZE,
4
+ AES_TAG_SIZE,
5
+ type EncryptedData,
6
+ PBKDF2_ITERATIONS,
7
+ type PlainData,
8
+ } from './common.js';
9
+ import * as mod from '#lib-wasm';
10
+
11
+ const EMPTY = new Uint8Array(0);
12
+ const encoder = new TextEncoder();
13
+
14
+ /** crypto-js encrypt */
15
+ export function encrypt({ data, aad }: PlainData, passphrase: string): EncryptedData {
16
+ const nonce = crypto.getRandomValues(new Uint8Array(NONCE_SIZE));
17
+ const result = mod.encrypt(
18
+ encoder.encode(passphrase),
19
+ data,
20
+ aad ?? EMPTY,
21
+ nonce,
22
+ PBKDF2_ITERATIONS,
23
+ AES_KEY_SIZE,
24
+ AES_TAG_SIZE,
25
+ );
26
+ return {
27
+ nonce,
28
+ data: result,
29
+ };
30
+ }
31
+
32
+ /** crypto-js decrypt */
33
+ export function decrypt({ data, aad, nonce }: EncryptedData, passphrase: string): PlainData {
34
+ const decrypted = mod.decrypt(
35
+ encoder.encode(passphrase),
36
+ data,
37
+ aad ?? EMPTY,
38
+ nonce,
39
+ PBKDF2_ITERATIONS,
40
+ AES_KEY_SIZE,
41
+ AES_TAG_SIZE,
42
+ );
43
+ return {
44
+ data: decrypted,
45
+ };
46
+ }
@@ -1,4 +1,11 @@
1
- import { AES_IV_SIZE, AES_KEY_SIZE, PBKDF2_SALT_SIZE, type EncryptionResult, PBKDF2_ITERATIONS } from './common.js';
1
+ import {
2
+ NONCE_SIZE,
3
+ AES_TAG_SIZE,
4
+ AES_KEY_SIZE,
5
+ type EncryptedData,
6
+ type PlainData,
7
+ PBKDF2_ITERATIONS,
8
+ } from './common.js';
2
9
 
3
10
  const encoder = new TextEncoder();
4
11
 
@@ -11,42 +18,44 @@ async function aesKdfWeb(passphrase: string, salt: Uint8Array): Promise<CryptoKe
11
18
  iterations: PBKDF2_ITERATIONS,
12
19
  hash: 'SHA-256',
13
20
  };
14
- return await crypto.subtle.deriveKey(pbkdf2Params, pass, { name: 'AES-CBC', length: AES_KEY_SIZE * 8 }, false, [
21
+ return await crypto.subtle.deriveKey(pbkdf2Params, pass, { name: 'AES-GCM', length: AES_KEY_SIZE * 8 }, false, [
15
22
  'encrypt',
16
23
  'decrypt',
17
24
  ]);
18
25
  }
19
26
 
20
27
  /** webcrypto encrypt */
21
- export async function encrypt(data: Uint8Array, passphrase: string): Promise<EncryptionResult> {
22
- const salt = crypto.getRandomValues(new Uint8Array(PBKDF2_SALT_SIZE));
23
- const key = await aesKdfWeb(passphrase, salt);
24
- const iv = crypto.getRandomValues(new Uint8Array(AES_IV_SIZE));
28
+ export async function encrypt({ data, aad }: PlainData, passphrase: string): Promise<EncryptedData> {
29
+ const nonce = crypto.getRandomValues(new Uint8Array(NONCE_SIZE));
30
+ const key = await aesKdfWeb(passphrase, nonce);
25
31
  const encrypted = await crypto.subtle.encrypt(
26
32
  {
27
- name: 'AES-CBC',
28
- iv,
33
+ name: 'AES-GCM',
34
+ iv: nonce,
35
+ tagLength: AES_TAG_SIZE * 8,
36
+ additionalData: aad,
29
37
  },
30
38
  key,
31
39
  data,
32
40
  );
33
41
  return {
34
- salt: salt,
35
- iv: iv,
42
+ nonce,
36
43
  data: new Uint8Array(encrypted),
37
44
  };
38
45
  }
39
46
 
40
47
  /** webcrypto decrypt */
41
- export async function decrypt({ data, salt, iv }: EncryptionResult, passphrase: string): Promise<Uint8Array> {
42
- const key = await aesKdfWeb(passphrase, salt);
48
+ export async function decrypt({ data, nonce, aad }: EncryptedData, passphrase: string): Promise<PlainData> {
49
+ const key = await aesKdfWeb(passphrase, nonce);
43
50
  const decrypted = await crypto.subtle.decrypt(
44
51
  {
45
- name: 'AES-CBC',
46
- iv,
52
+ name: 'AES-GCM',
53
+ iv: nonce,
54
+ tagLength: AES_TAG_SIZE * 8,
55
+ additionalData: aad,
47
56
  },
48
57
  key,
49
58
  data,
50
59
  );
51
- return new Uint8Array(decrypted);
60
+ return { data: new Uint8Array(decrypted) };
52
61
  }
package/src/index.ts CHANGED
@@ -1 +1 @@
1
- export { isEncrypted, encrypt, decrypt } from './encryption/index.js';
1
+ export { isEncrypted, encrypt, decrypt, encryptAad, extractAad } from './encryption/index.js';
@@ -1,10 +1,20 @@
1
1
  import { MAGIC_NUMBER } from '../dist/encryption/index.js';
2
- import { isEncrypted, encrypt, decrypt } from '../dist/index.js';
2
+ import { isEncrypted, encrypt, decrypt, encryptAad, extractAad } from '../dist/index.js';
3
3
  import { toUint8Array } from '../dist/utils.js';
4
+ import { createModule } from '../dist/encryption/module.js';
4
5
  import * as nodeImpl from '../dist/encryption/node.js';
5
6
  import * as browserImpl from '../dist/encryption/browser.js';
6
7
  import * as webImpl from '../dist/encryption/web.js';
7
- import * as jsImpl from '../dist/encryption/pure-js.js';
8
+ import * as wasmImpl from '../dist/encryption/wasm.js';
9
+
10
+ const data = [
11
+ Buffer.from(''),
12
+ Buffer.from('Hello, World!'),
13
+ Buffer.from('Hello, World!'.repeat(100)),
14
+ new Uint8Array(100),
15
+ Buffer.from('Hello, World!'.repeat(1000)).buffer,
16
+ ].map((d) => ({ raw: d, length: d.byteLength, type: d.constructor.name }));
17
+ const passphrase = 'test';
8
18
 
9
19
  describe('Encryption root export', () => {
10
20
  it('has MAGIC_NUMBER', () => {
@@ -13,13 +23,26 @@ describe('Encryption root export', () => {
13
23
  });
14
24
 
15
25
  it('check is encrypted', () => {
26
+ const nonce = Buffer.alloc(12);
27
+ const tag = Buffer.alloc(16);
28
+ const aadLength = Buffer.alloc(4);
29
+
16
30
  // @ts-expect-error bad type
17
31
  expect(() => isEncrypted({})).toThrow('Invalid data');
18
- expect(isEncrypted(Buffer.from(MAGIC_NUMBER))).toBe(false);
19
- expect(isEncrypted(Buffer.concat([MAGIC_NUMBER, Buffer.alloc(32)]))).toBe(false);
20
- expect(isEncrypted(Buffer.concat([MAGIC_NUMBER, Buffer.alloc(33)]))).toBe(true);
21
32
  expect(isEncrypted(Buffer.alloc(40))).toBe(false);
22
33
  expect(isEncrypted(Buffer.alloc(41))).toBe(false);
34
+
35
+ expect(isEncrypted(Buffer.from(MAGIC_NUMBER))).toBe(false);
36
+
37
+ expect(isEncrypted(Buffer.concat([MAGIC_NUMBER, nonce, aadLength, tag]))).toBe(true);
38
+ expect(isEncrypted(Buffer.concat([MAGIC_NUMBER, nonce, aadLength, tag.subarray(1)]))).toBe(false);
39
+
40
+ aadLength.writeUInt32BE(100);
41
+ expect(isEncrypted(Buffer.concat([MAGIC_NUMBER, nonce, aadLength, Buffer.alloc(111), tag]))).toBe(false);
42
+ expect(isEncrypted(Buffer.concat([MAGIC_NUMBER, nonce, aadLength, Buffer.alloc(112), tag]))).toBe(true);
43
+
44
+ aadLength.writeUInt32BE(0xffff_fffe);
45
+ expect(isEncrypted(Buffer.concat([MAGIC_NUMBER, nonce, aadLength, Buffer.alloc(112), tag]))).toBe(false);
23
46
  });
24
47
 
25
48
  it('encrypt check', async () => {
@@ -40,71 +63,144 @@ describe('Encryption root export', () => {
40
63
  await expect(() => decrypt(Buffer.alloc(100), 'xx')).rejects.toThrow('Invalid encrypted data');
41
64
  });
42
65
 
43
- it('encrypt/decrypt', async () => {
44
- const data = [
45
- Buffer.from(''),
46
- Buffer.from('Hello, World!'),
47
- Buffer.from('Hello, World!'.repeat(100)),
48
- new Uint8Array(100),
49
- Buffer.from('Hello, World!'.repeat(1000)).buffer,
50
- ];
51
- const passphrase = 'test';
52
- for (const raw of data) {
53
- const encrypted = await encrypt(raw, passphrase);
66
+ describe('aad', () => {
67
+ it('accepts empty aad', async () => {
68
+ const encrypted = await encryptAad(Buffer.alloc(0), Buffer.alloc(0), passphrase);
54
69
  expect(encrypted).toBeInstanceOf(Uint8Array);
55
- expect(encrypted.byteLength).toBeGreaterThan(raw.byteLength);
56
- expect(isEncrypted(encrypted)).toBe(true);
70
+ const extractedAad = extractAad(encrypted);
71
+ expect(extractedAad).toBeUndefined();
72
+ });
57
73
 
74
+ it('accepts undefined aad', async () => {
75
+ const encrypted = await encryptAad(Buffer.alloc(0), undefined, passphrase);
76
+ expect(encrypted).toBeInstanceOf(Uint8Array);
77
+ const extractedAad = extractAad(encrypted);
78
+ expect(extractedAad).toBeUndefined();
79
+ });
80
+
81
+ it('rejects invalid aad size', async () => {
82
+ const aad2 = Buffer.alloc(1024 * 1024 * 1024 + 1);
58
83
  await expect(async () => {
59
- await decrypt(encrypted, 'xx');
60
- throw new Error('This may not be thrown since cipher may not report error');
61
- }).rejects.toThrow();
84
+ await encryptAad(Buffer.alloc(0), aad2, passphrase);
85
+ }).rejects.toThrow('Invalid AAD size');
86
+ });
62
87
 
63
- const decrypted = await decrypt(encrypted, passphrase);
64
- expect(decrypted).toBeInstanceOf(Uint8Array);
65
- expect(decrypted).toEqual(toUint8Array(raw));
66
- }
88
+ it('rejects invalid data', () => {
89
+ const data = Buffer.alloc(10);
90
+ expect(() => extractAad(data)).toThrow('Invalid encrypted data');
91
+ });
67
92
  });
93
+
94
+ checkModule({ encrypt, decrypt, encryptAad });
68
95
  });
69
96
 
70
97
  /**
71
- * 检查实现
72
- * @param {any} impl impl module
98
+ * 检查实现模块
99
+ * @param {any} module wrapped module
73
100
  */
74
- function checkImpl(impl) {
75
- expect(impl).toMatchObject({
76
- encrypt: expect.any(Function),
77
- decrypt: expect.any(Function),
101
+ function checkModule(module) {
102
+ it('has correct exports', () => {
103
+ expect(module).toMatchObject({
104
+ encrypt: expect.any(Function),
105
+ decrypt: expect.any(Function),
106
+ encryptAad: expect.any(Function),
107
+ });
108
+ });
109
+
110
+ // eslint-disable-next-line @typescript-eslint/unbound-method
111
+ const { encrypt, decrypt, encryptAad } = module;
112
+
113
+ it.each(data)('encrypt/decrypt $type[$length]', async ({ raw }) => {
114
+ const encrypted = await encrypt(raw, passphrase);
115
+ expect(encrypted).toBeInstanceOf(Uint8Array);
116
+ expect(encrypted.byteLength).toBeGreaterThan(raw.byteLength);
117
+ expect(isEncrypted(encrypted)).toBe(true);
118
+
119
+ await expect(async () => {
120
+ await decrypt(encrypted, 'xx');
121
+ }).rejects.toThrow();
122
+
123
+ const decrypted = await decrypt(encrypted, passphrase);
124
+ expect(decrypted).toBeInstanceOf(Uint8Array);
125
+ expect(decrypted).toEqual(toUint8Array(raw));
126
+ });
127
+
128
+ it.each(data)('encrypt/decrypt $type[$length] with aad', async ({ raw }) => {
129
+ const aad = Buffer.from('Hello, AAD!');
130
+ const encrypted = await encryptAad(raw, aad, passphrase);
131
+ expect(encrypted).toBeInstanceOf(Uint8Array);
132
+ expect(encrypted.byteLength).toBeGreaterThan(raw.byteLength);
133
+ expect(isEncrypted(encrypted)).toBe(true);
134
+
135
+ const extractedAad = extractAad(encrypted);
136
+ expect(extractedAad).toBeInstanceOf(Uint8Array);
137
+ expect(extractedAad).toEqual(toUint8Array(aad));
138
+
139
+ await expect(async () => {
140
+ await decrypt(encrypted, 'xx');
141
+ }).rejects.toThrow();
142
+
143
+ const decrypted = await decrypt(encrypted, passphrase);
144
+ expect(decrypted).toBeInstanceOf(Uint8Array);
145
+ expect(decrypted).toEqual(toUint8Array(raw));
78
146
  });
79
- checkImplEncryption(impl.encrypt, impl.decrypt);
80
147
  }
81
148
 
82
149
  /**
83
150
  * 检查实现
84
- * @param {(arg0: Buffer, arg1: string) => any} encrypt encrypt
85
- * @param {(arg0: any, arg1: string) => any} decrypt decrypt
151
+ * @param {Function} encrypt encrypt
152
+ * @param {Function} decrypt decrypt
86
153
  */
87
154
  function checkImplEncryption(encrypt, decrypt) {
88
- const data = [Buffer.alloc(0), Buffer.from('Hello, World!'), Buffer.from('Hello, World!'.repeat(100))];
89
- const passphrase = 'test';
90
- it.each(data.map((v) => ({ length: v.byteLength, buffer: v })))(
91
- `len=$length`,
92
- async ({ buffer: raw }) => {
93
- const encrypted = await encrypt(raw, passphrase);
94
- expect(encrypted.salt).toBeInstanceOf(Uint8Array);
95
- expect(encrypted.salt.byteLength).toBe(16);
96
- expect(encrypted.iv).toBeInstanceOf(Uint8Array);
97
- expect(encrypted.iv.byteLength).toBe(16);
155
+ it.each(data)(
156
+ `$type[$length]`,
157
+ async ({ raw }) => {
158
+ const encrypted = await encrypt({ data: toUint8Array(raw) }, passphrase);
159
+ expect(encrypted.nonce).toBeInstanceOf(Uint8Array);
160
+ expect(encrypted.nonce.byteLength).toBe(12);
161
+ expect(encrypted.aad).toBeUndefined();
98
162
  expect(encrypted.data).toBeInstanceOf(Uint8Array);
99
163
 
100
164
  await expect(async () => {
101
165
  await decrypt(encrypted, 'xx');
102
- throw new Error('This may not be thrown since cipher may not report error');
103
166
  }).rejects.toThrow();
104
167
 
105
168
  const decrypted = await decrypt(encrypted, passphrase);
106
- expect(decrypted).toBeInstanceOf(Uint8Array);
107
- expect(decrypted).toEqual(toUint8Array(raw));
169
+ expect(decrypted.data).toBeInstanceOf(Uint8Array);
170
+ expect(decrypted.data).toEqual(toUint8Array(raw));
171
+ expect(decrypted.aad).toBeUndefined();
172
+ },
173
+ 100_000,
174
+ );
175
+ it.each(data)(
176
+ `(aad) $type[$length]`,
177
+ async ({ raw }) => {
178
+ const aad = Buffer.from('Hello, AAD!');
179
+ const encrypted = await encrypt({ data: toUint8Array(raw), aad }, passphrase);
180
+ expect(encrypted.nonce).toBeInstanceOf(Uint8Array);
181
+ expect(encrypted.nonce.byteLength).toBe(12);
182
+ expect(encrypted.data).toBeInstanceOf(Uint8Array);
183
+
184
+ await expect(async () => {
185
+ await decrypt(
186
+ {
187
+ nonce: encrypted.nonce,
188
+ data: encrypted.data,
189
+ },
190
+ passphrase,
191
+ );
192
+ }).rejects.toThrow();
193
+
194
+ const decrypted = await decrypt(
195
+ {
196
+ nonce: encrypted.nonce,
197
+ aad: toUint8Array(aad),
198
+ data: encrypted.data,
199
+ },
200
+ passphrase,
201
+ );
202
+ expect(decrypted.data).toBeInstanceOf(Uint8Array);
203
+ expect(decrypted.data).toEqual(toUint8Array(raw));
108
204
  },
109
205
  100_000,
110
206
  );
@@ -115,10 +211,11 @@ describe('Encryption impl', () => {
115
211
  node: nodeImpl,
116
212
  browser: browserImpl,
117
213
  web: webImpl,
118
- js: jsImpl,
214
+ wasm: wasmImpl,
119
215
  });
120
216
  describe.each(impls)('impl %s', (name, impl) => {
121
- checkImpl(impl);
217
+ const module = createModule(impl);
218
+ checkModule(module);
122
219
  });
123
220
  describe.each(impls.slice(1))(`cross impl %s/${impls[0][0]}`, (name, impl) => {
124
221
  describe(`${impls[0][0]} -> ${name}`, () => {
@@ -131,15 +228,14 @@ describe('Encryption impl', () => {
131
228
  });
132
229
 
133
230
  describe('Encryption impl browser', () => {
134
- describe('should work without crypto', () => {
135
- const { crypto } = globalThis;
231
+ describe('should work without crypto subtle', () => {
232
+ const { subtle } = crypto;
136
233
  beforeAll(() => {
137
- // @ts-expect-error remove crypto
138
- globalThis.crypto = undefined;
234
+ Object.defineProperty(crypto, 'subtle', { value: undefined, configurable: true });
139
235
  });
140
236
  afterAll(() => {
141
- globalThis.crypto = crypto;
237
+ Object.defineProperty(crypto, 'subtle', { value: subtle, configurable: true });
142
238
  });
143
- checkImpl(browserImpl);
239
+ checkModule(createModule(browserImpl));
144
240
  });
145
241
  });
package/tsconfig.json CHANGED
@@ -4,7 +4,8 @@
4
4
  "compilerOptions": {
5
5
  "outDir": "./dist",
6
6
  "paths": {
7
- "#encryption": ["./src/encryption/node.ts", "./src/encryption/browser.ts"]
7
+ "#encryption": ["./src/encryption/node.ts", "./src/encryption/browser.ts"],
8
+ "#lib-wasm": ["./lib/wasm.d.ts"]
8
9
  }
9
10
  }
10
11
  }
package/wasm-build.js ADDED
@@ -0,0 +1,30 @@
1
+ import path from 'node:path';
2
+ import fs from 'node:fs/promises';
3
+ import { spawn } from 'node:child_process';
4
+ import { fileURLToPath } from 'node:url';
5
+ import esbuild from 'esbuild';
6
+ import { wasmLoader } from 'esbuild-plugin-wasm';
7
+ import { once } from 'node:events';
8
+
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = path.dirname(__filename);
11
+
12
+ const wasmPack = spawn('wasm-pack', ['build', '--target', 'bundler', '--release'], {
13
+ stdio: 'inherit',
14
+ cwd: path.resolve(__dirname, './wasm'),
15
+ });
16
+
17
+ await once(wasmPack, 'exit');
18
+
19
+ await esbuild.build({
20
+ entryPoints: [path.resolve(__dirname, './wasm/pkg/wasm.js')],
21
+ outdir: path.resolve(__dirname, './lib'),
22
+ charset: 'utf8',
23
+ target: 'es2022',
24
+ format: 'esm',
25
+ bundle: true,
26
+ minify: false,
27
+ plugins: [wasmLoader({ mode: 'embedded' })],
28
+ });
29
+
30
+ await fs.copyFile(path.resolve(__dirname, './wasm/pkg/wasm.d.ts'), path.resolve(__dirname, './lib/wasm.d.ts'));
@@ -1,5 +0,0 @@
1
- import { type EncryptionResult } from './common.js';
2
- /** crypto-js encrypt */
3
- export declare function encrypt(data: Uint8Array, passphrase: string): Promise<EncryptionResult>;
4
- /** crypto-js decrypt */
5
- export declare function decrypt({ data, iv, salt }: EncryptionResult, passphrase: string): Promise<Uint8Array>;
@@ -1,54 +0,0 @@
1
- import { pbkdf2, createSHA256 } from 'hash-wasm';
2
- import AES from 'crypto-js/aes.js';
3
- import WordArray from 'crypto-js/lib-typedarrays.js';
4
- import { AES_IV_SIZE, AES_KEY_SIZE, PBKDF2_SALT_SIZE, PBKDF2_ITERATIONS } from './common.js';
5
- /** Convert word array to buffer data */
6
- function wordArrayToBuffer(wordArray) {
7
- const { sigBytes, words } = wordArray;
8
- if (sigBytes < 0 || words.length * 4 < sigBytes)
9
- throw new Error('Invalid word array');
10
- const result = new Uint8Array(sigBytes);
11
- for (let i = 0; i < sigBytes; i++) {
12
- result[i] = (words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff;
13
- }
14
- return result;
15
- }
16
- /** Convert buffer data to word array */
17
- function bufferToWordArray(buffer) {
18
- return WordArray.create(buffer);
19
- }
20
- /** Create aes params */
21
- async function aesKdfJs(passphrase, salt) {
22
- const result = await pbkdf2({
23
- password: passphrase,
24
- salt: salt,
25
- iterations: PBKDF2_ITERATIONS,
26
- hashLength: AES_KEY_SIZE,
27
- hashFunction: createSHA256(),
28
- outputType: 'binary',
29
- });
30
- return WordArray.create(result);
31
- }
32
- /** crypto-js encrypt */
33
- export async function encrypt(data, passphrase) {
34
- const salt = wordArrayToBuffer(WordArray.random(PBKDF2_SALT_SIZE));
35
- const key = await aesKdfJs(passphrase, salt);
36
- const iv = WordArray.random(AES_IV_SIZE);
37
- const encrypted = AES.encrypt(bufferToWordArray(data), key, { iv });
38
- return {
39
- salt: salt,
40
- iv: wordArrayToBuffer(iv),
41
- data: wordArrayToBuffer(encrypted.ciphertext),
42
- };
43
- }
44
- /** crypto-js decrypt */
45
- export async function decrypt({ data, iv, salt }, passphrase) {
46
- const key = await aesKdfJs(passphrase, salt);
47
- const decrypted = AES.decrypt({
48
- ciphertext: bufferToWordArray(data),
49
- }, key, {
50
- iv: bufferToWordArray(iv),
51
- });
52
- return wordArrayToBuffer(decrypted);
53
- }
54
- //# sourceMappingURL=pure-js.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"pure-js.js","sourceRoot":"","sources":["../../src/encryption/pure-js.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AACjD,OAAO,GAAG,MAAM,kBAAkB,CAAC;AACnC,OAAO,SAAS,MAAM,8BAA8B,CAAC;AAErD,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,gBAAgB,EAAyB,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAEpH,wCAAwC;AACxC,SAAS,iBAAiB,CAAC,SAAoB;IAC3C,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,GAAG,SAAS,CAAC;IACtC,IAAI,QAAQ,GAAG,CAAC,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,GAAG,QAAQ;QAAE,MAAM,IAAI,KAAK,CAAC,oBAAoB,CAAC,CAAC;IACvF,MAAM,MAAM,GAAG,IAAI,UAAU,CAAC,QAAQ,CAAC,CAAC;IACxC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,QAAQ,EAAE,CAAC,EAAE,EAAE,CAAC;QAChC,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,EAAE,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC;IAC/D,CAAC;IACD,OAAO,MAAM,CAAC;AAClB,CAAC;AAED,wCAAwC;AACxC,SAAS,iBAAiB,CAAC,MAAkB;IACzC,OAAO,SAAS,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;AACpC,CAAC;AAED,wBAAwB;AACxB,KAAK,UAAU,QAAQ,CAAC,UAAkB,EAAE,IAAgB;IACxD,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC;QACxB,QAAQ,EAAE,UAAU;QACpB,IAAI,EAAE,IAAI;QACV,UAAU,EAAE,iBAAiB;QAC7B,UAAU,EAAE,YAAY;QACxB,YAAY,EAAE,YAAY,EAAE;QAC5B,UAAU,EAAE,QAAQ;KACvB,CAAC,CAAC;IACH,OAAO,SAAS,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;AACpC,CAAC;AAED,wBAAwB;AACxB,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,IAAgB,EAAE,UAAkB;IAC9D,MAAM,IAAI,GAAG,iBAAiB,CAAC,SAAS,CAAC,MAAM,CAAC,gBAAgB,CAAC,CAAC,CAAC;IACnE,MAAM,GAAG,GAAG,MAAM,QAAQ,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC;IAC7C,MAAM,EAAE,GAAG,SAAS,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;IACzC,MAAM,SAAS,GAAG,GAAG,CAAC,OAAO,CAAC,iBAAiB,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC;IACpE,OAAO;QACH,IAAI,EAAE,IAAI;QACV,EAAE,EAAE,iBAAiB,CAAC,EAAE,CAAC;QACzB,IAAI,EAAE,iBAAiB,CAAC,SAAS,CAAC,UAAU,CAAC;KAChD,CAAC;AACN,CAAC;AAED,wBAAwB;AACxB,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,EAAoB,EAAE,UAAkB;IAClF,MAAM,GAAG,GAAG,MAAM,QAAQ,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC;IAC7C,MAAM,SAAS,GAAG,GAAG,CAAC,OAAO,CACzB;QACI,UAAU,EAAE,iBAAiB,CAAC,IAAI,CAAC;KACT,EAC9B,GAAG,EACH;QACI,EAAE,EAAE,iBAAiB,CAAC,EAAE,CAAC;KAC5B,CACJ,CAAC;IACF,OAAO,iBAAiB,CAAC,SAAS,CAAC,CAAC;AACxC,CAAC"}