-
Notifications
You must be signed in to change notification settings - Fork 7
/
crypto.ts
162 lines (138 loc) · 4.03 KB
/
crypto.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
import { crypto } from "https://deno.land/std@0.149.0/crypto/mod.ts";
import {
decode as bd,
encode as be,
} from "https://deno.land/std@0.149.0/encoding/base64.ts";
// Helper functions to encrypt / decrypt using AES with default config in CryptoJS
// NOTE: Encryption will be using SHA-384 instead of MD5 in Key Derivation, so it can't be decrypted using CryptoJS
// Decryption will try MD5 after SHA-384 generated key fails, so it can decrypt cipher text encrypted by CryptoJS
// For CryptoJS cipher text:
// It has a constant header for the first 8 byte.
// It has the salt in the second 8 byte.
// The rest is the cipher text.
// +----------+----------+-----------------+
// | Salted__ | <salt> | <cipherText> |
// +----------+----------+-----------------+
// | 64 bit | 64 bit | variable length |
// +----------+----------+-----------------+
// size in byte, same as CryptoJS's default.
const HEADER_SIZE = 8;
const SALT_SIZE = 8;
const KEY_SIZE = 32;
const IV_SIZE = 16;
export const encryptCryptoJSAES = async (
plainText: string,
passphrase: string,
iterations = 1,
): Promise<string> => {
const salt = new Uint8Array(SALT_SIZE);
crypto.getRandomValues(salt);
const { key, iv } = await EVPKDF(
new TextEncoder().encode(passphrase),
salt,
"SHA-384",
iterations,
);
const cipherText = await crypto.subtle.encrypt(
{ name: "AES-CBC", iv },
key,
new TextEncoder().encode(plainText),
);
// get the CryptoJS style cipher text.
// "Salted__" + key + encryptedText
return be(
concatUint8Array(
new TextEncoder().encode("Salted__"),
salt,
new Uint8Array(cipherText),
),
);
};
// decrypt cipher text generated by default CryptoJS AES.
// cipher text should be in base64 (the default of CryptoJS's toString())
export const decryptCryptoJSAES = async (
cipherTextBase64: string,
passphrase: string,
iterations = 1,
): Promise<string> => {
const cipherText = bd(cipherTextBase64);
const { salt, body } = parseCryptoJSCipherText(cipherText);
const { key, iv } = await EVPKDF(
new TextEncoder().encode(passphrase),
salt,
"SHA-384",
iterations,
);
let plainText: ArrayBuffer;
try {
plainText = await crypto.subtle.decrypt(
{ name: "AES-CBC", iv },
key,
body,
);
} catch (_) {
const { key, iv } = await EVPKDF(
new TextEncoder().encode(passphrase),
salt,
"MD5",
iterations,
);
plainText = await crypto.subtle.decrypt(
{ name: "AES-CBC", iv },
key,
body,
);
}
return new TextDecoder().decode(plainText);
};
const parseCryptoJSCipherText = (cipherText: Uint8Array): {
salt: Uint8Array;
body: Uint8Array;
} => ({
salt: cipherText.subarray(HEADER_SIZE, HEADER_SIZE + SALT_SIZE),
body: cipherText.subarray(HEADER_SIZE + SALT_SIZE, cipherText.length),
});
// a customed version of EVPKDF using sha-384 instead of md5.
const EVPKDF = async (
passphrase: Uint8Array,
salt: Uint8Array,
algorithm: "SHA-384" | "MD5",
iterations: number,
): Promise<{ key: CryptoKey; iv: Uint8Array }> => {
let rawKey = new Uint8Array();
let block = new Uint8Array();
while (rawKey.byteLength < KEY_SIZE + IV_SIZE) {
let buffer = await crypto.subtle.digest(
algorithm,
concatUint8Array(block, passphrase, salt),
);
for (let i = 1; i < iterations; i++) {
buffer = await crypto.subtle.digest(
algorithm,
buffer,
);
}
block = new Uint8Array(buffer);
rawKey = concatUint8Array(rawKey, block);
}
return {
key: await crypto.subtle.importKey(
"raw",
rawKey.subarray(0, KEY_SIZE),
"AES-CBC",
false,
["encrypt", "decrypt"],
),
iv: rawKey.subarray(KEY_SIZE, rawKey.length),
};
};
const concatUint8Array = (...arrays: Uint8Array[]): Uint8Array => {
const size = arrays.reduce((len, array) => len + array.length, 0);
const merged = new Uint8Array(size);
let mergedLen = 0;
for (const array of arrays) {
merged.set(array, mergedLen);
mergedLen += array.length;
}
return merged;
};