@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.
- package/benchmark.js +44 -0
- package/dist/encryption/browser.d.ts +3 -3
- package/dist/encryption/browser.js +1 -2
- package/dist/encryption/browser.js.map +1 -1
- package/dist/encryption/common.d.ts +45 -16
- package/dist/encryption/common.js +59 -9
- package/dist/encryption/common.js.map +1 -1
- package/dist/encryption/index.d.ts +4 -21
- package/dist/encryption/index.js +11 -63
- package/dist/encryption/index.js.map +1 -1
- package/dist/encryption/module.d.ts +22 -0
- package/dist/encryption/module.js +62 -0
- package/dist/encryption/module.js.map +1 -0
- package/dist/encryption/node.d.ts +3 -3
- package/dist/encryption/node.js +19 -15
- package/dist/encryption/node.js.map +1 -1
- package/dist/encryption/wasm.d.ts +5 -0
- package/dist/encryption/wasm.js +21 -0
- package/dist/encryption/wasm.js.map +1 -0
- package/dist/encryption/web.d.ts +3 -3
- package/dist/encryption/web.js +17 -15
- package/dist/encryption/web.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/lib/wasm.d.ts +26 -0
- package/lib/wasm.js +149 -0
- package/package.json +11 -10
- package/src/encryption/browser.ts +4 -5
- package/src/encryption/common.ts +83 -16
- package/src/encryption/index.ts +12 -71
- package/src/encryption/module.ts +94 -0
- package/src/encryption/node.ts +24 -15
- package/src/encryption/wasm.ts +46 -0
- package/src/encryption/web.ts +24 -15
- package/src/index.ts +1 -1
- package/tests/encryption.js +151 -55
- package/tsconfig.json +2 -1
- package/wasm-build.js +30 -0
- package/dist/encryption/pure-js.d.ts +0 -5
- package/dist/encryption/pure-js.js +0 -54
- package/dist/encryption/pure-js.js.map +0 -1
- 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
|
+
}
|
package/src/encryption/node.ts
CHANGED
|
@@ -1,30 +1,39 @@
|
|
|
1
1
|
import { pbkdf2 as _pbkdf2, createCipheriv, createDecipheriv, randomBytes } from 'node:crypto';
|
|
2
|
-
import {
|
|
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
|
|
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:
|
|
12
|
-
const
|
|
13
|
-
const key = await aesKdf(passphrase,
|
|
14
|
-
const
|
|
15
|
-
|
|
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
|
-
|
|
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({
|
|
26
|
-
const key = await aesKdf(passphrase,
|
|
27
|
-
const decipher = createDecipheriv('aes-256-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
+
}
|
package/src/encryption/web.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import {
|
|
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-
|
|
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:
|
|
22
|
-
const
|
|
23
|
-
const key = await aesKdfWeb(passphrase,
|
|
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-
|
|
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
|
-
|
|
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,
|
|
42
|
-
const key = await aesKdfWeb(passphrase,
|
|
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-
|
|
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';
|
package/tests/encryption.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
Buffer.
|
|
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
|
-
|
|
56
|
-
expect(
|
|
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
|
|
60
|
-
|
|
61
|
-
|
|
84
|
+
await encryptAad(Buffer.alloc(0), aad2, passphrase);
|
|
85
|
+
}).rejects.toThrow('Invalid AAD size');
|
|
86
|
+
});
|
|
62
87
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
expect(
|
|
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}
|
|
98
|
+
* 检查实现模块
|
|
99
|
+
* @param {any} module wrapped module
|
|
73
100
|
*/
|
|
74
|
-
function
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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 {
|
|
85
|
-
* @param {
|
|
151
|
+
* @param {Function} encrypt encrypt
|
|
152
|
+
* @param {Function} decrypt decrypt
|
|
86
153
|
*/
|
|
87
154
|
function checkImplEncryption(encrypt, decrypt) {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
expect(encrypted.
|
|
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
|
-
|
|
214
|
+
wasm: wasmImpl,
|
|
119
215
|
});
|
|
120
216
|
describe.each(impls)('impl %s', (name, impl) => {
|
|
121
|
-
|
|
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 {
|
|
231
|
+
describe('should work without crypto subtle', () => {
|
|
232
|
+
const { subtle } = crypto;
|
|
136
233
|
beforeAll(() => {
|
|
137
|
-
|
|
138
|
-
globalThis.crypto = undefined;
|
|
234
|
+
Object.defineProperty(crypto, 'subtle', { value: undefined, configurable: true });
|
|
139
235
|
});
|
|
140
236
|
afterAll(() => {
|
|
141
|
-
|
|
237
|
+
Object.defineProperty(crypto, 'subtle', { value: subtle, configurable: true });
|
|
142
238
|
});
|
|
143
|
-
|
|
239
|
+
checkModule(createModule(browserImpl));
|
|
144
240
|
});
|
|
145
241
|
});
|
package/tsconfig.json
CHANGED
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"}
|