@cloudpss/crypto 0.5.22
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/README.md +3 -0
- package/dist/encryption/browser.d.ts +5 -0
- package/dist/encryption/browser.js +26 -0
- package/dist/encryption/browser.js.map +1 -0
- package/dist/encryption/common.d.ts +21 -0
- package/dist/encryption/common.js +13 -0
- package/dist/encryption/common.js.map +1 -0
- package/dist/encryption/index.d.ts +26 -0
- package/dist/encryption/index.js +69 -0
- package/dist/encryption/index.js.map +1 -0
- package/dist/encryption/node.d.ts +5 -0
- package/dist/encryption/node.js +28 -0
- package/dist/encryption/node.js.map +1 -0
- package/dist/encryption/pure-js.d.ts +5 -0
- package/dist/encryption/pure-js.js +52 -0
- package/dist/encryption/pure-js.js.map +1 -0
- package/dist/encryption/web.d.ts +5 -0
- package/dist/encryption/web.js +41 -0
- package/dist/encryption/web.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/utils.d.ts +2 -0
- package/dist/utils.js +11 -0
- package/dist/utils.js.map +1 -0
- package/jest.config.js +3 -0
- package/package.json +31 -0
- package/src/encryption/browser.ts +30 -0
- package/src/encryption/common.ts +22 -0
- package/src/encryption/index.ts +80 -0
- package/src/encryption/node.ts +30 -0
- package/src/encryption/pure-js.ts +61 -0
- package/src/encryption/web.ts +52 -0
- package/src/index.ts +1 -0
- package/src/utils.ts +10 -0
- package/tests/encryption.js +134 -0
- package/tests/tsconfig.json +9 -0
- package/tsconfig.json +10 -0
package/README.md
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { EncryptionResult } from './common.js';
|
|
2
|
+
/** browser encrypt */
|
|
3
|
+
export declare function encrypt(data: Uint8Array, passphrase: string): Promise<EncryptionResult>;
|
|
4
|
+
/** browser decrypt */
|
|
5
|
+
export declare function decrypt(data: EncryptionResult, passphrase: string): Promise<Uint8Array>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
|
2
|
+
const module = () => {
|
|
3
|
+
if (typeof crypto == 'object' &&
|
|
4
|
+
typeof crypto.getRandomValues == 'function' &&
|
|
5
|
+
typeof crypto.subtle == 'object' &&
|
|
6
|
+
typeof crypto.subtle.importKey == 'function' &&
|
|
7
|
+
typeof crypto.subtle.deriveKey == 'function' &&
|
|
8
|
+
typeof crypto.subtle.encrypt == 'function' &&
|
|
9
|
+
typeof crypto.subtle.decrypt == 'function') {
|
|
10
|
+
return import('./web.js');
|
|
11
|
+
}
|
|
12
|
+
else {
|
|
13
|
+
return import('./pure-js.js');
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
/** browser encrypt */
|
|
17
|
+
export async function encrypt(data, passphrase) {
|
|
18
|
+
const { encrypt } = await module();
|
|
19
|
+
return await encrypt(data, passphrase);
|
|
20
|
+
}
|
|
21
|
+
/** browser decrypt */
|
|
22
|
+
export async function decrypt(data, passphrase) {
|
|
23
|
+
const { decrypt } = await module();
|
|
24
|
+
return await decrypt(data, passphrase);
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=browser.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser.js","sourceRoot":"","sources":["../../src/encryption/browser.ts"],"names":[],"mappings":"AAEA,4EAA4E;AAC5E,MAAM,MAAM,GAAG,GAAG,EAAE;IAChB,IACI,OAAO,MAAM,IAAI,QAAQ;QACzB,OAAO,MAAM,CAAC,eAAe,IAAI,UAAU;QAC3C,OAAO,MAAM,CAAC,MAAM,IAAI,QAAQ;QAChC,OAAO,MAAM,CAAC,MAAM,CAAC,SAAS,IAAI,UAAU;QAC5C,OAAO,MAAM,CAAC,MAAM,CAAC,SAAS,IAAI,UAAU;QAC5C,OAAO,MAAM,CAAC,MAAM,CAAC,OAAO,IAAI,UAAU;QAC1C,OAAO,MAAM,CAAC,MAAM,CAAC,OAAO,IAAI,UAAU,EAC5C,CAAC;QACC,OAAO,MAAM,CAAC,UAAU,CAAC,CAAC;IAC9B,CAAC;SAAM,CAAC;QACJ,OAAO,MAAM,CAAC,cAAc,CAAC,CAAC;IAClC,CAAC;AACL,CAAC,CAAC;AAEF,sBAAsB;AACtB,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,IAAgB,EAAE,UAAkB;IAC9D,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,MAAM,EAAE,CAAC;IACnC,OAAO,MAAM,OAAO,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC;AAC3C,CAAC;AAED,sBAAsB;AACtB,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,IAAsB,EAAE,UAAkB;IACpE,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,MAAM,EAAE,CAAC;IACnC,OAAO,MAAM,OAAO,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC;AAC3C,CAAC"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PBKDF2 迭代次数
|
|
3
|
+
*/
|
|
4
|
+
export declare const PBKDF2_ITERATIONS = 100000;
|
|
5
|
+
/**
|
|
6
|
+
* PBKDF2 盐值长度(byte)
|
|
7
|
+
*/
|
|
8
|
+
export declare const PBKDF2_SALT_SIZE: number;
|
|
9
|
+
/** IV 长度(byte) */
|
|
10
|
+
export declare const AES_IV_SIZE: number;
|
|
11
|
+
/** KEY 长度(byte) */
|
|
12
|
+
export declare const AES_KEY_SIZE: number;
|
|
13
|
+
/** 加密结果 */
|
|
14
|
+
export interface EncryptionResult {
|
|
15
|
+
/** 盐值 */
|
|
16
|
+
salt: Uint8Array;
|
|
17
|
+
/** 初始向量 */
|
|
18
|
+
iv: Uint8Array;
|
|
19
|
+
/** 加密后的数据 */
|
|
20
|
+
data: Uint8Array;
|
|
21
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PBKDF2 迭代次数
|
|
3
|
+
*/
|
|
4
|
+
export const PBKDF2_ITERATIONS = 100_000;
|
|
5
|
+
/**
|
|
6
|
+
* PBKDF2 盐值长度(byte)
|
|
7
|
+
*/
|
|
8
|
+
export const PBKDF2_SALT_SIZE = 128 / 8;
|
|
9
|
+
/** IV 长度(byte) */
|
|
10
|
+
export const AES_IV_SIZE = 128 / 8;
|
|
11
|
+
/** KEY 长度(byte) */
|
|
12
|
+
export const AES_KEY_SIZE = 256 / 8;
|
|
13
|
+
//# sourceMappingURL=common.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"common.js","sourceRoot":"","sources":["../../src/encryption/common.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,OAAO,CAAC;AACzC;;GAEG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,GAAG,GAAG,CAAC,CAAC;AACxC,kBAAkB;AAClB,MAAM,CAAC,MAAM,WAAW,GAAG,GAAG,GAAG,CAAC,CAAC;AACnC,mBAAmB;AACnB,MAAM,CAAC,MAAM,YAAY,GAAG,GAAG,GAAG,CAAC,CAAC"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CloudPSS 数据加密
|
|
3
|
+
* - 密钥生成算法:PBKDF2-HMAC-SHA256,盐长度 128,迭代 100,000 次
|
|
4
|
+
* - 加密算法:AES-256-CBC
|
|
5
|
+
*
|
|
6
|
+
* - 文件格式:
|
|
7
|
+
* - Magic Number: 0e 02 49 29 3f 07 7b 0a
|
|
8
|
+
* - Salt: 128 bits
|
|
9
|
+
* - IV: 128 bits
|
|
10
|
+
* - Encrypted Data
|
|
11
|
+
*/
|
|
12
|
+
/** CloudPSS 数据加密 */
|
|
13
|
+
export declare const MAGIC_NUMBER: Uint8Array;
|
|
14
|
+
/** 检查是否为 CloudPSS 加密数据 */
|
|
15
|
+
export declare function isEncrypted(data: BinaryData): boolean;
|
|
16
|
+
/**
|
|
17
|
+
* 加密数据
|
|
18
|
+
* @throws {TypeError} 如果密码无效
|
|
19
|
+
*/
|
|
20
|
+
export declare function encrypt(data: BinaryData, passphrase: string): Promise<Uint8Array>;
|
|
21
|
+
/**
|
|
22
|
+
* 解密数据
|
|
23
|
+
* @throws {TypeError} 如果数据不是有效的加密数据
|
|
24
|
+
* @throws {TypeError} 如果密码无效
|
|
25
|
+
*/
|
|
26
|
+
export declare function decrypt(data: BinaryData, passphrase: string): Promise<Uint8Array>;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import * as impl from '#encryption';
|
|
2
|
+
import { toUint8Array } from '../utils.js';
|
|
3
|
+
import { AES_IV_SIZE, PBKDF2_SALT_SIZE } from './common.js';
|
|
4
|
+
/**
|
|
5
|
+
* CloudPSS 数据加密
|
|
6
|
+
* - 密钥生成算法:PBKDF2-HMAC-SHA256,盐长度 128,迭代 100,000 次
|
|
7
|
+
* - 加密算法:AES-256-CBC
|
|
8
|
+
*
|
|
9
|
+
* - 文件格式:
|
|
10
|
+
* - Magic Number: 0e 02 49 29 3f 07 7b 0a
|
|
11
|
+
* - Salt: 128 bits
|
|
12
|
+
* - IV: 128 bits
|
|
13
|
+
* - Encrypted Data
|
|
14
|
+
*/
|
|
15
|
+
/** CloudPSS 数据加密 */
|
|
16
|
+
export const MAGIC_NUMBER = Uint8Array.from([0x0e, 0x02, 0x49, 0x29, 0x3f, 0x07, 0x7b, 0x0a]);
|
|
17
|
+
/** 检查是否为 CloudPSS 加密数据 */
|
|
18
|
+
function isEncryptedImpl(data) {
|
|
19
|
+
return (data.length > MAGIC_NUMBER.length + PBKDF2_SALT_SIZE + AES_IV_SIZE &&
|
|
20
|
+
MAGIC_NUMBER.every((v, i) => data[i] === v));
|
|
21
|
+
}
|
|
22
|
+
/** 检查是否为 CloudPSS 加密数据 */
|
|
23
|
+
export function isEncrypted(data) {
|
|
24
|
+
const buffer = toUint8Array(data);
|
|
25
|
+
return isEncryptedImpl(buffer);
|
|
26
|
+
}
|
|
27
|
+
/** 检查密码 */
|
|
28
|
+
function assertPassphrase(passphrase) {
|
|
29
|
+
if (typeof passphrase !== 'string') {
|
|
30
|
+
throw new TypeError('Invalid passphrase, must be a string');
|
|
31
|
+
}
|
|
32
|
+
if (passphrase.length === 0) {
|
|
33
|
+
throw new TypeError('Invalid passphrase, must not be empty');
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* 加密数据
|
|
38
|
+
* @throws {TypeError} 如果密码无效
|
|
39
|
+
*/
|
|
40
|
+
export async function encrypt(data, passphrase) {
|
|
41
|
+
assertPassphrase(passphrase);
|
|
42
|
+
const buffer = toUint8Array(data);
|
|
43
|
+
const encrypted = await impl.encrypt(buffer, passphrase);
|
|
44
|
+
const result = new Uint8Array(MAGIC_NUMBER.length + PBKDF2_SALT_SIZE + AES_IV_SIZE + encrypted.data.length);
|
|
45
|
+
result.set(MAGIC_NUMBER);
|
|
46
|
+
result.set(encrypted.salt, MAGIC_NUMBER.length);
|
|
47
|
+
result.set(encrypted.iv, MAGIC_NUMBER.length + PBKDF2_SALT_SIZE);
|
|
48
|
+
result.set(encrypted.data, MAGIC_NUMBER.length + PBKDF2_SALT_SIZE + AES_IV_SIZE);
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* 解密数据
|
|
53
|
+
* @throws {TypeError} 如果数据不是有效的加密数据
|
|
54
|
+
* @throws {TypeError} 如果密码无效
|
|
55
|
+
*/
|
|
56
|
+
export async function decrypt(data, passphrase) {
|
|
57
|
+
assertPassphrase(passphrase);
|
|
58
|
+
const buffer = toUint8Array(data);
|
|
59
|
+
if (!isEncryptedImpl(buffer)) {
|
|
60
|
+
throw new TypeError('Invalid encrypted data');
|
|
61
|
+
}
|
|
62
|
+
const encrypted = {
|
|
63
|
+
salt: buffer.subarray(MAGIC_NUMBER.length, MAGIC_NUMBER.length + PBKDF2_SALT_SIZE),
|
|
64
|
+
iv: buffer.subarray(MAGIC_NUMBER.length + PBKDF2_SALT_SIZE, MAGIC_NUMBER.length + PBKDF2_SALT_SIZE + AES_IV_SIZE),
|
|
65
|
+
data: buffer.subarray(MAGIC_NUMBER.length + PBKDF2_SALT_SIZE + AES_IV_SIZE),
|
|
66
|
+
};
|
|
67
|
+
return await impl.decrypt(encrypted, passphrase);
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/encryption/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type EncryptionResult } from './common.js';
|
|
2
|
+
/** nodejs encrypt */
|
|
3
|
+
export declare function encrypt(data: Uint8Array, passphrase: string): Promise<EncryptionResult>;
|
|
4
|
+
/** nodejs decrypt */
|
|
5
|
+
export declare function decrypt({ data, iv, salt }: EncryptionResult, passphrase: string): Promise<Uint8Array>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { pbkdf2 as _pbkdf2, createCipheriv, createDecipheriv, randomBytes } from 'node:crypto';
|
|
2
|
+
import { PBKDF2_ITERATIONS, PBKDF2_SALT_SIZE, AES_IV_SIZE, AES_KEY_SIZE } from './common.js';
|
|
3
|
+
import { promisify } from 'node:util';
|
|
4
|
+
import { toUint8Array } from '../utils.js';
|
|
5
|
+
const aesKdf = (passphrase, salt) => {
|
|
6
|
+
return promisify(_pbkdf2)(passphrase, salt, PBKDF2_ITERATIONS, AES_KEY_SIZE, 'sha256');
|
|
7
|
+
};
|
|
8
|
+
/** nodejs encrypt */
|
|
9
|
+
export async function encrypt(data, passphrase) {
|
|
10
|
+
const salt = randomBytes(PBKDF2_SALT_SIZE);
|
|
11
|
+
const key = await aesKdf(passphrase, salt);
|
|
12
|
+
const iv = randomBytes(AES_IV_SIZE);
|
|
13
|
+
const cipher = createCipheriv('aes-256-cbc', key, iv);
|
|
14
|
+
const encrypted = Buffer.concat([cipher.update(data), cipher.final()]);
|
|
15
|
+
return {
|
|
16
|
+
salt: toUint8Array(salt),
|
|
17
|
+
iv: toUint8Array(iv),
|
|
18
|
+
data: toUint8Array(encrypted),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/** nodejs decrypt */
|
|
22
|
+
export async function decrypt({ data, iv, salt }, passphrase) {
|
|
23
|
+
const key = await aesKdf(passphrase, salt);
|
|
24
|
+
const decipher = createDecipheriv('aes-256-cbc', key, iv);
|
|
25
|
+
const decrypted = Buffer.concat([decipher.update(data), decipher.final()]);
|
|
26
|
+
return toUint8Array(decrypted);
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=node.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"node.js","sourceRoot":"","sources":["../../src/encryption/node.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC/F,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAyB,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AACpH,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,MAAM,MAAM,GAAG,CAAC,UAAkB,EAAE,IAAgB,EAAmB,EAAE;IACrE,OAAO,SAAS,CAAC,OAAO,CAAC,CAAC,UAAU,EAAE,IAAI,EAAE,iBAAiB,EAAE,YAAY,EAAE,QAAQ,CAAC,CAAC;AAC3F,CAAC,CAAC;AAEF,qBAAqB;AACrB,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,IAAgB,EAAE,UAAkB;IAC9D,MAAM,IAAI,GAAG,WAAW,CAAC,gBAAgB,CAAC,CAAC;IAC3C,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC;IAC3C,MAAM,EAAE,GAAG,WAAW,CAAC,WAAW,CAAC,CAAC;IACpC,MAAM,MAAM,GAAG,cAAc,CAAC,aAAa,EAAE,GAAG,EAAE,EAAE,CAAC,CAAC;IACtD,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;IACvE,OAAO;QACH,IAAI,EAAE,YAAY,CAAC,IAAI,CAAC;QACxB,EAAE,EAAE,YAAY,CAAC,EAAE,CAAC;QACpB,IAAI,EAAE,YAAY,CAAC,SAAS,CAAC;KAChC,CAAC;AACN,CAAC;AAED,qBAAqB;AACrB,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,EAAoB,EAAE,UAAkB;IAClF,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC;IAC3C,MAAM,QAAQ,GAAG,gBAAgB,CAAC,aAAa,EAAE,GAAG,EAAE,EAAE,CAAC,CAAC;IAC1D,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;IAC3E,OAAO,YAAY,CAAC,SAAS,CAAC,CAAC;AACnC,CAAC"}
|
|
@@ -0,0 +1,5 @@
|
|
|
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>;
|
|
@@ -0,0 +1,52 @@
|
|
|
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
|
+
const result = new Uint8Array(sigBytes);
|
|
9
|
+
for (let i = 0; i < sigBytes; i++) {
|
|
10
|
+
result[i] = (words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff;
|
|
11
|
+
}
|
|
12
|
+
return result;
|
|
13
|
+
}
|
|
14
|
+
/** Convert buffer data to word array */
|
|
15
|
+
function bufferToWordArray(buffer) {
|
|
16
|
+
return WordArray.create(buffer);
|
|
17
|
+
}
|
|
18
|
+
/** Create aes params */
|
|
19
|
+
async function aesKdfJs(passphrase, salt) {
|
|
20
|
+
const result = await pbkdf2({
|
|
21
|
+
password: passphrase,
|
|
22
|
+
salt: salt,
|
|
23
|
+
iterations: PBKDF2_ITERATIONS,
|
|
24
|
+
hashLength: AES_KEY_SIZE,
|
|
25
|
+
hashFunction: createSHA256(),
|
|
26
|
+
outputType: 'binary',
|
|
27
|
+
});
|
|
28
|
+
return WordArray.create(result);
|
|
29
|
+
}
|
|
30
|
+
/** crypto-js encrypt */
|
|
31
|
+
export async function encrypt(data, passphrase) {
|
|
32
|
+
const salt = wordArrayToBuffer(WordArray.random(PBKDF2_SALT_SIZE));
|
|
33
|
+
const key = await aesKdfJs(passphrase, salt);
|
|
34
|
+
const iv = WordArray.random(AES_IV_SIZE);
|
|
35
|
+
const encrypted = AES.encrypt(bufferToWordArray(data), key, { iv });
|
|
36
|
+
return {
|
|
37
|
+
salt: salt,
|
|
38
|
+
iv: wordArrayToBuffer(iv),
|
|
39
|
+
data: wordArrayToBuffer(encrypted.ciphertext),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/** crypto-js decrypt */
|
|
43
|
+
export async function decrypt({ data, iv, salt }, passphrase) {
|
|
44
|
+
const key = await aesKdfJs(passphrase, salt);
|
|
45
|
+
const decrypted = AES.decrypt({
|
|
46
|
+
ciphertext: bufferToWordArray(data),
|
|
47
|
+
}, key, {
|
|
48
|
+
iv: bufferToWordArray(iv),
|
|
49
|
+
});
|
|
50
|
+
return wordArrayToBuffer(decrypted);
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=pure-js.js.map
|
|
@@ -0,0 +1 @@
|
|
|
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,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"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type EncryptionResult } from './common.js';
|
|
2
|
+
/** webcrypto encrypt */
|
|
3
|
+
export declare function encrypt(data: Uint8Array, passphrase: string): Promise<EncryptionResult>;
|
|
4
|
+
/** webcrypto decrypt */
|
|
5
|
+
export declare function decrypt({ data, salt, iv }: EncryptionResult, passphrase: string): Promise<Uint8Array>;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { AES_IV_SIZE, AES_KEY_SIZE, PBKDF2_SALT_SIZE, PBKDF2_ITERATIONS } from './common.js';
|
|
2
|
+
const encoder = new TextEncoder();
|
|
3
|
+
/** Create aes params */
|
|
4
|
+
async function aesKdfWeb(passphrase, salt) {
|
|
5
|
+
const pass = await crypto.subtle.importKey('raw', encoder.encode(passphrase), 'PBKDF2', false, ['deriveKey']);
|
|
6
|
+
const pbkdf2Params = {
|
|
7
|
+
name: 'PBKDF2',
|
|
8
|
+
salt,
|
|
9
|
+
iterations: PBKDF2_ITERATIONS,
|
|
10
|
+
hash: 'SHA-256',
|
|
11
|
+
};
|
|
12
|
+
return await crypto.subtle.deriveKey(pbkdf2Params, pass, { name: 'AES-CBC', length: AES_KEY_SIZE * 8 }, false, [
|
|
13
|
+
'encrypt',
|
|
14
|
+
'decrypt',
|
|
15
|
+
]);
|
|
16
|
+
}
|
|
17
|
+
/** webcrypto encrypt */
|
|
18
|
+
export async function encrypt(data, passphrase) {
|
|
19
|
+
const salt = crypto.getRandomValues(new Uint8Array(PBKDF2_SALT_SIZE));
|
|
20
|
+
const key = await aesKdfWeb(passphrase, salt);
|
|
21
|
+
const iv = crypto.getRandomValues(new Uint8Array(AES_IV_SIZE));
|
|
22
|
+
const encrypted = await crypto.subtle.encrypt({
|
|
23
|
+
name: 'AES-CBC',
|
|
24
|
+
iv,
|
|
25
|
+
}, key, data);
|
|
26
|
+
return {
|
|
27
|
+
salt: salt,
|
|
28
|
+
iv: iv,
|
|
29
|
+
data: new Uint8Array(encrypted),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/** webcrypto decrypt */
|
|
33
|
+
export async function decrypt({ data, salt, iv }, passphrase) {
|
|
34
|
+
const key = await aesKdfWeb(passphrase, salt);
|
|
35
|
+
const decrypted = await crypto.subtle.decrypt({
|
|
36
|
+
name: 'AES-CBC',
|
|
37
|
+
iv,
|
|
38
|
+
}, key, data);
|
|
39
|
+
return new Uint8Array(decrypted);
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=web.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"web.js","sourceRoot":"","sources":["../../src/encryption/web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,gBAAgB,EAAyB,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAEpH,MAAM,OAAO,GAAG,IAAI,WAAW,EAAE,CAAC;AAElC,wBAAwB;AACxB,KAAK,UAAU,SAAS,CAAC,UAAkB,EAAE,IAAgB;IACzD,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,EAAE,OAAO,CAAC,MAAM,CAAC,UAAU,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,WAAW,CAAC,CAAC,CAAC;IAC9G,MAAM,YAAY,GAAiB;QAC/B,IAAI,EAAE,QAAQ;QACd,IAAI;QACJ,UAAU,EAAE,iBAAiB;QAC7B,IAAI,EAAE,SAAS;KAClB,CAAC;IACF,OAAO,MAAM,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,YAAY,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,YAAY,GAAG,CAAC,EAAE,EAAE,KAAK,EAAE;QAC3G,SAAS;QACT,SAAS;KACZ,CAAC,CAAC;AACP,CAAC;AAED,wBAAwB;AACxB,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,IAAgB,EAAE,UAAkB;IAC9D,MAAM,IAAI,GAAG,MAAM,CAAC,eAAe,CAAC,IAAI,UAAU,CAAC,gBAAgB,CAAC,CAAC,CAAC;IACtE,MAAM,GAAG,GAAG,MAAM,SAAS,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC;IAC9C,MAAM,EAAE,GAAG,MAAM,CAAC,eAAe,CAAC,IAAI,UAAU,CAAC,WAAW,CAAC,CAAC,CAAC;IAC/D,MAAM,SAAS,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,OAAO,CACzC;QACI,IAAI,EAAE,SAAS;QACf,EAAE;KACL,EACD,GAAG,EACH,IAAI,CACP,CAAC;IACF,OAAO;QACH,IAAI,EAAE,IAAI;QACV,EAAE,EAAE,EAAE;QACN,IAAI,EAAE,IAAI,UAAU,CAAC,SAAS,CAAC;KAClC,CAAC;AACN,CAAC;AAED,wBAAwB;AACxB,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,EAAoB,EAAE,UAAkB;IAClF,MAAM,GAAG,GAAG,MAAM,SAAS,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC;IAC9C,MAAM,SAAS,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,OAAO,CACzC;QACI,IAAI,EAAE,SAAS;QACf,EAAE;KACL,EACD,GAAG,EACH,IAAI,CACP,CAAC;IACF,OAAO,IAAI,UAAU,CAAC,SAAS,CAAC,CAAC;AACrC,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { isEncrypted, encrypt, decrypt } from './encryption/index.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAC"}
|
package/dist/utils.d.ts
ADDED
package/dist/utils.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** 支持的数据转为 Uint8Array */
|
|
2
|
+
export function toUint8Array(data) {
|
|
3
|
+
if (data == null || typeof data != 'object' || typeof data.byteLength != 'number') {
|
|
4
|
+
throw new TypeError('Invalid data');
|
|
5
|
+
}
|
|
6
|
+
if (ArrayBuffer.isView(data)) {
|
|
7
|
+
return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
8
|
+
}
|
|
9
|
+
return new Uint8Array(data, 0, data.byteLength);
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=utils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,yBAAyB;AACzB,MAAM,UAAU,YAAY,CAAC,IAAgB;IACzC,IAAI,IAAI,IAAI,IAAI,IAAI,OAAO,IAAI,IAAI,QAAQ,IAAI,OAAO,IAAI,CAAC,UAAU,IAAI,QAAQ,EAAE,CAAC;QAChF,MAAM,IAAI,SAAS,CAAC,cAAc,CAAC,CAAC;IACxC,CAAC;IACD,IAAI,WAAW,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC;QAC3B,OAAO,IAAI,UAAU,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC;IACzE,CAAC;IACD,OAAO,IAAI,UAAU,CAAC,IAAI,EAAE,CAAC,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC;AACpD,CAAC"}
|
package/jest.config.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cloudpss/crypto",
|
|
3
|
+
"version": "0.5.22",
|
|
4
|
+
"author": "CloudPSS",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "dist/index.js",
|
|
8
|
+
"module": "dist/index.js",
|
|
9
|
+
"types": "dist/index.d.ts",
|
|
10
|
+
"exports": "./dist/index.js",
|
|
11
|
+
"imports": {
|
|
12
|
+
"#encryption": {
|
|
13
|
+
"browser": "./dist/encryption/browser.js",
|
|
14
|
+
"node": "./dist/encryption/node.js",
|
|
15
|
+
"default": "./dist/encryption/browser.js"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"crypto-js": "^4.2.0",
|
|
20
|
+
"hash-wasm": "^4.11.0"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/crypto-js": "^4.2.2"
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"start": "pnpm clean && tsc --watch",
|
|
27
|
+
"build": "pnpm clean && tsc",
|
|
28
|
+
"test": "NODE_OPTIONS=\"${NODE_OPTIONS:-} --experimental-vm-modules\" jest",
|
|
29
|
+
"clean": "rimraf dist"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { EncryptionResult } from './common.js';
|
|
2
|
+
|
|
3
|
+
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
|
4
|
+
const module = () => {
|
|
5
|
+
if (
|
|
6
|
+
typeof crypto == 'object' &&
|
|
7
|
+
typeof crypto.getRandomValues == 'function' &&
|
|
8
|
+
typeof crypto.subtle == 'object' &&
|
|
9
|
+
typeof crypto.subtle.importKey == 'function' &&
|
|
10
|
+
typeof crypto.subtle.deriveKey == 'function' &&
|
|
11
|
+
typeof crypto.subtle.encrypt == 'function' &&
|
|
12
|
+
typeof crypto.subtle.decrypt == 'function'
|
|
13
|
+
) {
|
|
14
|
+
return import('./web.js');
|
|
15
|
+
} else {
|
|
16
|
+
return import('./pure-js.js');
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/** browser encrypt */
|
|
21
|
+
export async function encrypt(data: Uint8Array, passphrase: string): Promise<EncryptionResult> {
|
|
22
|
+
const { encrypt } = await module();
|
|
23
|
+
return await encrypt(data, passphrase);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** browser decrypt */
|
|
27
|
+
export async function decrypt(data: EncryptionResult, passphrase: string): Promise<Uint8Array> {
|
|
28
|
+
const { decrypt } = await module();
|
|
29
|
+
return await decrypt(data, passphrase);
|
|
30
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PBKDF2 迭代次数
|
|
3
|
+
*/
|
|
4
|
+
export const PBKDF2_ITERATIONS = 100_000;
|
|
5
|
+
/**
|
|
6
|
+
* PBKDF2 盐值长度(byte)
|
|
7
|
+
*/
|
|
8
|
+
export const PBKDF2_SALT_SIZE = 128 / 8;
|
|
9
|
+
/** IV 长度(byte) */
|
|
10
|
+
export const AES_IV_SIZE = 128 / 8;
|
|
11
|
+
/** KEY 长度(byte) */
|
|
12
|
+
export const AES_KEY_SIZE = 256 / 8;
|
|
13
|
+
|
|
14
|
+
/** 加密结果 */
|
|
15
|
+
export interface EncryptionResult {
|
|
16
|
+
/** 盐值 */
|
|
17
|
+
salt: Uint8Array;
|
|
18
|
+
/** 初始向量 */
|
|
19
|
+
iv: Uint8Array;
|
|
20
|
+
/** 加密后的数据 */
|
|
21
|
+
data: Uint8Array;
|
|
22
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import * as impl from '#encryption';
|
|
2
|
+
import { toUint8Array } from '../utils.js';
|
|
3
|
+
import { AES_IV_SIZE, PBKDF2_SALT_SIZE } from './common.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* CloudPSS 数据加密
|
|
7
|
+
* - 密钥生成算法:PBKDF2-HMAC-SHA256,盐长度 128,迭代 100,000 次
|
|
8
|
+
* - 加密算法:AES-256-CBC
|
|
9
|
+
*
|
|
10
|
+
* - 文件格式:
|
|
11
|
+
* - Magic Number: 0e 02 49 29 3f 07 7b 0a
|
|
12
|
+
* - Salt: 128 bits
|
|
13
|
+
* - IV: 128 bits
|
|
14
|
+
* - Encrypted Data
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** CloudPSS 数据加密 */
|
|
18
|
+
export const MAGIC_NUMBER = Uint8Array.from([0x0e, 0x02, 0x49, 0x29, 0x3f, 0x07, 0x7b, 0x0a]);
|
|
19
|
+
|
|
20
|
+
/** 检查是否为 CloudPSS 加密数据 */
|
|
21
|
+
function isEncryptedImpl(data: Uint8Array): boolean {
|
|
22
|
+
return (
|
|
23
|
+
data.length > MAGIC_NUMBER.length + PBKDF2_SALT_SIZE + AES_IV_SIZE &&
|
|
24
|
+
MAGIC_NUMBER.every((v, i) => data[i] === v)
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** 检查是否为 CloudPSS 加密数据 */
|
|
29
|
+
export function isEncrypted(data: BinaryData): boolean {
|
|
30
|
+
const buffer = toUint8Array(data);
|
|
31
|
+
return isEncryptedImpl(buffer);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** 检查密码 */
|
|
35
|
+
function assertPassphrase(passphrase: string): void {
|
|
36
|
+
if (typeof passphrase !== 'string') {
|
|
37
|
+
throw new TypeError('Invalid passphrase, must be a string');
|
|
38
|
+
}
|
|
39
|
+
if (passphrase.length === 0) {
|
|
40
|
+
throw new TypeError('Invalid passphrase, must not be empty');
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 加密数据
|
|
46
|
+
* @throws {TypeError} 如果密码无效
|
|
47
|
+
*/
|
|
48
|
+
export async function encrypt(data: BinaryData, passphrase: string): Promise<Uint8Array> {
|
|
49
|
+
assertPassphrase(passphrase);
|
|
50
|
+
const buffer = toUint8Array(data);
|
|
51
|
+
const encrypted = await impl.encrypt(buffer, passphrase);
|
|
52
|
+
const result = new Uint8Array(MAGIC_NUMBER.length + PBKDF2_SALT_SIZE + AES_IV_SIZE + encrypted.data.length);
|
|
53
|
+
result.set(MAGIC_NUMBER);
|
|
54
|
+
result.set(encrypted.salt, MAGIC_NUMBER.length);
|
|
55
|
+
result.set(encrypted.iv, MAGIC_NUMBER.length + PBKDF2_SALT_SIZE);
|
|
56
|
+
result.set(encrypted.data, MAGIC_NUMBER.length + PBKDF2_SALT_SIZE + AES_IV_SIZE);
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 解密数据
|
|
62
|
+
* @throws {TypeError} 如果数据不是有效的加密数据
|
|
63
|
+
* @throws {TypeError} 如果密码无效
|
|
64
|
+
*/
|
|
65
|
+
export async function decrypt(data: BinaryData, passphrase: string): Promise<Uint8Array> {
|
|
66
|
+
assertPassphrase(passphrase);
|
|
67
|
+
const buffer = toUint8Array(data);
|
|
68
|
+
if (!isEncryptedImpl(buffer)) {
|
|
69
|
+
throw new TypeError('Invalid encrypted data');
|
|
70
|
+
}
|
|
71
|
+
const encrypted = {
|
|
72
|
+
salt: buffer.subarray(MAGIC_NUMBER.length, MAGIC_NUMBER.length + PBKDF2_SALT_SIZE),
|
|
73
|
+
iv: buffer.subarray(
|
|
74
|
+
MAGIC_NUMBER.length + PBKDF2_SALT_SIZE,
|
|
75
|
+
MAGIC_NUMBER.length + PBKDF2_SALT_SIZE + AES_IV_SIZE,
|
|
76
|
+
),
|
|
77
|
+
data: buffer.subarray(MAGIC_NUMBER.length + PBKDF2_SALT_SIZE + AES_IV_SIZE),
|
|
78
|
+
};
|
|
79
|
+
return await impl.decrypt(encrypted, passphrase);
|
|
80
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
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';
|
|
3
|
+
import { promisify } from 'node:util';
|
|
4
|
+
import { toUint8Array } from '../utils.js';
|
|
5
|
+
|
|
6
|
+
const aesKdf = (passphrase: string, salt: Uint8Array): Promise<Buffer> => {
|
|
7
|
+
return promisify(_pbkdf2)(passphrase, salt, PBKDF2_ITERATIONS, AES_KEY_SIZE, 'sha256');
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/** 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()]);
|
|
17
|
+
return {
|
|
18
|
+
salt: toUint8Array(salt),
|
|
19
|
+
iv: toUint8Array(iv),
|
|
20
|
+
data: toUint8Array(encrypted),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** 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);
|
|
30
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
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 type CryptoJS from 'crypto-js';
|
|
5
|
+
import { AES_IV_SIZE, AES_KEY_SIZE, PBKDF2_SALT_SIZE, type EncryptionResult, PBKDF2_ITERATIONS } from './common.js';
|
|
6
|
+
|
|
7
|
+
/** Convert word array to buffer data */
|
|
8
|
+
function wordArrayToBuffer(wordArray: WordArray): Uint8Array {
|
|
9
|
+
const { sigBytes, words } = wordArray;
|
|
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
|
+
|
|
17
|
+
/** Convert buffer data to word array */
|
|
18
|
+
function bufferToWordArray(buffer: Uint8Array): WordArray {
|
|
19
|
+
return WordArray.create(buffer);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Create aes params */
|
|
23
|
+
async function aesKdfJs(passphrase: string, salt: Uint8Array): Promise<WordArray> {
|
|
24
|
+
const result = await pbkdf2({
|
|
25
|
+
password: passphrase,
|
|
26
|
+
salt: salt,
|
|
27
|
+
iterations: PBKDF2_ITERATIONS,
|
|
28
|
+
hashLength: AES_KEY_SIZE,
|
|
29
|
+
hashFunction: createSHA256(),
|
|
30
|
+
outputType: 'binary',
|
|
31
|
+
});
|
|
32
|
+
return WordArray.create(result);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** crypto-js encrypt */
|
|
36
|
+
export async function encrypt(data: Uint8Array, passphrase: string): Promise<EncryptionResult> {
|
|
37
|
+
const salt = wordArrayToBuffer(WordArray.random(PBKDF2_SALT_SIZE));
|
|
38
|
+
const key = await aesKdfJs(passphrase, salt);
|
|
39
|
+
const iv = WordArray.random(AES_IV_SIZE);
|
|
40
|
+
const encrypted = AES.encrypt(bufferToWordArray(data), key, { iv });
|
|
41
|
+
return {
|
|
42
|
+
salt: salt,
|
|
43
|
+
iv: wordArrayToBuffer(iv),
|
|
44
|
+
data: wordArrayToBuffer(encrypted.ciphertext),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** crypto-js decrypt */
|
|
49
|
+
export async function decrypt({ data, iv, salt }: EncryptionResult, passphrase: string): Promise<Uint8Array> {
|
|
50
|
+
const key = await aesKdfJs(passphrase, salt);
|
|
51
|
+
const decrypted = AES.decrypt(
|
|
52
|
+
{
|
|
53
|
+
ciphertext: bufferToWordArray(data),
|
|
54
|
+
} as CryptoJS.lib.CipherParams,
|
|
55
|
+
key,
|
|
56
|
+
{
|
|
57
|
+
iv: bufferToWordArray(iv),
|
|
58
|
+
},
|
|
59
|
+
);
|
|
60
|
+
return wordArrayToBuffer(decrypted);
|
|
61
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { AES_IV_SIZE, AES_KEY_SIZE, PBKDF2_SALT_SIZE, type EncryptionResult, PBKDF2_ITERATIONS } from './common.js';
|
|
2
|
+
|
|
3
|
+
const encoder = new TextEncoder();
|
|
4
|
+
|
|
5
|
+
/** Create aes params */
|
|
6
|
+
async function aesKdfWeb(passphrase: string, salt: Uint8Array): Promise<CryptoKey> {
|
|
7
|
+
const pass = await crypto.subtle.importKey('raw', encoder.encode(passphrase), 'PBKDF2', false, ['deriveKey']);
|
|
8
|
+
const pbkdf2Params: Pbkdf2Params = {
|
|
9
|
+
name: 'PBKDF2',
|
|
10
|
+
salt,
|
|
11
|
+
iterations: PBKDF2_ITERATIONS,
|
|
12
|
+
hash: 'SHA-256',
|
|
13
|
+
};
|
|
14
|
+
return await crypto.subtle.deriveKey(pbkdf2Params, pass, { name: 'AES-CBC', length: AES_KEY_SIZE * 8 }, false, [
|
|
15
|
+
'encrypt',
|
|
16
|
+
'decrypt',
|
|
17
|
+
]);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** 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));
|
|
25
|
+
const encrypted = await crypto.subtle.encrypt(
|
|
26
|
+
{
|
|
27
|
+
name: 'AES-CBC',
|
|
28
|
+
iv,
|
|
29
|
+
},
|
|
30
|
+
key,
|
|
31
|
+
data,
|
|
32
|
+
);
|
|
33
|
+
return {
|
|
34
|
+
salt: salt,
|
|
35
|
+
iv: iv,
|
|
36
|
+
data: new Uint8Array(encrypted),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** webcrypto decrypt */
|
|
41
|
+
export async function decrypt({ data, salt, iv }: EncryptionResult, passphrase: string): Promise<Uint8Array> {
|
|
42
|
+
const key = await aesKdfWeb(passphrase, salt);
|
|
43
|
+
const decrypted = await crypto.subtle.decrypt(
|
|
44
|
+
{
|
|
45
|
+
name: 'AES-CBC',
|
|
46
|
+
iv,
|
|
47
|
+
},
|
|
48
|
+
key,
|
|
49
|
+
data,
|
|
50
|
+
);
|
|
51
|
+
return new Uint8Array(decrypted);
|
|
52
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { isEncrypted, encrypt, decrypt } from './encryption/index.js';
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** 支持的数据转为 Uint8Array */
|
|
2
|
+
export function toUint8Array(data: BinaryData): Uint8Array {
|
|
3
|
+
if (data == null || typeof data != 'object' || typeof data.byteLength != 'number') {
|
|
4
|
+
throw new TypeError('Invalid data');
|
|
5
|
+
}
|
|
6
|
+
if (ArrayBuffer.isView(data)) {
|
|
7
|
+
return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
8
|
+
}
|
|
9
|
+
return new Uint8Array(data, 0, data.byteLength);
|
|
10
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { MAGIC_NUMBER } from '../dist/encryption/index.js';
|
|
2
|
+
import { isEncrypted, encrypt, decrypt } from '../dist/index.js';
|
|
3
|
+
import { toUint8Array } from '../dist/utils.js';
|
|
4
|
+
import * as nodeImpl from '../dist/encryption/node.js';
|
|
5
|
+
import * as browserImpl from '../dist/encryption/browser.js';
|
|
6
|
+
import * as webImpl from '../dist/encryption/web.js';
|
|
7
|
+
import * as jsImpl from '../dist/encryption/pure-js.js';
|
|
8
|
+
|
|
9
|
+
describe('Encryption root export', () => {
|
|
10
|
+
it('has MAGIC_NUMBER', () => {
|
|
11
|
+
expect(MAGIC_NUMBER).toBeInstanceOf(Uint8Array);
|
|
12
|
+
expect(MAGIC_NUMBER.length).toBe(8);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('check is encrypted', () => {
|
|
16
|
+
// @ts-expect-error bad type
|
|
17
|
+
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
|
+
expect(isEncrypted(Buffer.alloc(40))).toBe(false);
|
|
22
|
+
expect(isEncrypted(Buffer.alloc(41))).toBe(false);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('encrypt check', async () => {
|
|
26
|
+
// @ts-expect-error bad type
|
|
27
|
+
await expect(() => encrypt({}, 'xx')).rejects.toThrow('Invalid data');
|
|
28
|
+
// @ts-expect-error bad type
|
|
29
|
+
// eslint-disable-next-line unicorn/new-for-builtins
|
|
30
|
+
await expect(() => encrypt(Buffer.alloc(0), new String(1))).rejects.toThrow('Invalid passphrase');
|
|
31
|
+
await expect(() => encrypt(Buffer.alloc(0), '')).rejects.toThrow('Invalid passphrase');
|
|
32
|
+
});
|
|
33
|
+
it('decrypt check', async () => {
|
|
34
|
+
// @ts-expect-error bad type
|
|
35
|
+
await expect(() => decrypt({}, 'xx')).rejects.toThrow('Invalid data');
|
|
36
|
+
// @ts-expect-error bad type
|
|
37
|
+
// eslint-disable-next-line unicorn/new-for-builtins
|
|
38
|
+
await expect(() => decrypt(Buffer.alloc(0), new String(1))).rejects.toThrow('Invalid passphrase');
|
|
39
|
+
await expect(() => decrypt(Buffer.alloc(0), '')).rejects.toThrow('Invalid passphrase');
|
|
40
|
+
await expect(() => decrypt(Buffer.alloc(100), 'xx')).rejects.toThrow('Invalid encrypted data');
|
|
41
|
+
});
|
|
42
|
+
|
|
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);
|
|
54
|
+
expect(encrypted).toBeInstanceOf(Uint8Array);
|
|
55
|
+
expect(encrypted.byteLength).toBeGreaterThan(raw.byteLength);
|
|
56
|
+
expect(isEncrypted(encrypted)).toBe(true);
|
|
57
|
+
const decrypted = await decrypt(encrypted, passphrase);
|
|
58
|
+
expect(decrypted).toBeInstanceOf(Uint8Array);
|
|
59
|
+
expect(decrypted).toEqual(toUint8Array(raw));
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 检查实现
|
|
66
|
+
* @param {any} impl impl module
|
|
67
|
+
*/
|
|
68
|
+
function checkImpl(impl) {
|
|
69
|
+
expect(impl).toMatchObject({
|
|
70
|
+
encrypt: expect.any(Function),
|
|
71
|
+
decrypt: expect.any(Function),
|
|
72
|
+
});
|
|
73
|
+
checkImplEncryption(impl.encrypt, impl.decrypt);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 检查实现
|
|
78
|
+
* @param {(arg0: Buffer, arg1: string) => any} encrypt encrypt
|
|
79
|
+
* @param {(arg0: any, arg1: string) => any} decrypt decrypt
|
|
80
|
+
*/
|
|
81
|
+
function checkImplEncryption(encrypt, decrypt) {
|
|
82
|
+
const data = [Buffer.alloc(0), Buffer.from('Hello, World!'), Buffer.from('Hello, World!'.repeat(100))];
|
|
83
|
+
const passphrase = 'test';
|
|
84
|
+
it.each(data.map((v) => ({ length: v.byteLength, buffer: v })))(
|
|
85
|
+
`len=$length`,
|
|
86
|
+
async ({ buffer: raw }) => {
|
|
87
|
+
const encrypted = await encrypt(raw, passphrase);
|
|
88
|
+
expect(encrypted.salt).toBeInstanceOf(Uint8Array);
|
|
89
|
+
expect(encrypted.salt.byteLength).toBe(16);
|
|
90
|
+
expect(encrypted.iv).toBeInstanceOf(Uint8Array);
|
|
91
|
+
expect(encrypted.iv.byteLength).toBe(16);
|
|
92
|
+
expect(encrypted.data).toBeInstanceOf(Uint8Array);
|
|
93
|
+
|
|
94
|
+
const decrypted = await decrypt(encrypted, passphrase);
|
|
95
|
+
expect(decrypted).toBeInstanceOf(Uint8Array);
|
|
96
|
+
expect(decrypted).toEqual(toUint8Array(raw));
|
|
97
|
+
},
|
|
98
|
+
100_000,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
describe('Encryption impl', () => {
|
|
103
|
+
const impls = Object.entries({
|
|
104
|
+
node: nodeImpl,
|
|
105
|
+
browser: browserImpl,
|
|
106
|
+
web: webImpl,
|
|
107
|
+
js: jsImpl,
|
|
108
|
+
});
|
|
109
|
+
describe.each(impls)('impl %s', (name, impl) => {
|
|
110
|
+
checkImpl(impl);
|
|
111
|
+
});
|
|
112
|
+
describe.each(impls.slice(1))(`cross impl %s/${impls[0][0]}`, (name, impl) => {
|
|
113
|
+
describe(`${impls[0][0]} -> ${name}`, () => {
|
|
114
|
+
checkImplEncryption(impls[0][1].encrypt, impl.decrypt);
|
|
115
|
+
});
|
|
116
|
+
describe(`${name} -> ${impls[0][0]}`, () => {
|
|
117
|
+
checkImplEncryption(impl.encrypt, impls[0][1].decrypt);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe('Encryption impl browser', () => {
|
|
123
|
+
describe('should work without crypto', () => {
|
|
124
|
+
const { crypto } = globalThis;
|
|
125
|
+
beforeAll(() => {
|
|
126
|
+
// @ts-expect-error remove crypto
|
|
127
|
+
globalThis.crypto = undefined;
|
|
128
|
+
});
|
|
129
|
+
afterAll(() => {
|
|
130
|
+
globalThis.crypto = crypto;
|
|
131
|
+
});
|
|
132
|
+
checkImpl(browserImpl);
|
|
133
|
+
});
|
|
134
|
+
});
|