|
1 |
| -import { crypto as cr } from "@noble/hashes/crypto"; |
2 |
| -import { concatBytes, equalsBytes } from "./utils.js"; |
| 1 | +import { ctr, cbc } from "@noble/ciphers/aes"; |
| 2 | +import type { CipherWithOutput } from "@noble/ciphers/utils"; |
3 | 3 |
|
4 |
| -const crypto: any = { web: cr }; |
5 |
| - |
6 |
| -function validateOpt(key: Uint8Array, iv: Uint8Array, mode: string) { |
| 4 | +function getCipher( |
| 5 | + key: Uint8Array, |
| 6 | + iv: Uint8Array, |
| 7 | + mode: string, |
| 8 | + pkcs7PaddingEnabled = true |
| 9 | +): CipherWithOutput { |
7 | 10 | if (!mode.startsWith("aes-")) {
|
8 |
| - throw new Error(`AES submodule doesn't support mode ${mode}`); |
9 |
| - } |
10 |
| - if (iv.length !== 16) { |
11 |
| - throw new Error("AES: wrong IV length"); |
| 11 | + throw new Error("AES: unsupported mode"); |
12 | 12 | }
|
13 |
| - if ( |
14 |
| - (mode.startsWith("aes-128") && key.length !== 16) || |
15 |
| - (mode.startsWith("aes-256") && key.length !== 32) |
16 |
| - ) { |
| 13 | + const len = key.length; |
| 14 | + if ((mode.startsWith("aes-128") && len !== 16) || (mode.startsWith("aes-256") && len !== 32)) { |
17 | 15 | throw new Error("AES: wrong key length");
|
18 | 16 | }
|
19 |
| -} |
20 |
| - |
21 |
| -async function getBrowserKey( |
22 |
| - mode: string, |
23 |
| - key: Uint8Array, |
24 |
| - iv: Uint8Array |
25 |
| -): Promise<[CryptoKey, AesCbcParams | AesCtrParams]> { |
26 |
| - if (!crypto.web) { |
27 |
| - throw new Error("Browser crypto not available."); |
| 17 | + if (iv.length !== 16) { |
| 18 | + throw new Error("AES: wrong IV length"); |
28 | 19 | }
|
29 |
| - let keyMode: string | undefined; |
30 | 20 | if (["aes-128-cbc", "aes-256-cbc"].includes(mode)) {
|
31 |
| - keyMode = "cbc"; |
| 21 | + return cbc(key, iv, { disablePadding: !pkcs7PaddingEnabled }); |
32 | 22 | }
|
33 | 23 | if (["aes-128-ctr", "aes-256-ctr"].includes(mode)) {
|
34 |
| - keyMode = "ctr"; |
| 24 | + return ctr(key, iv); |
35 | 25 | }
|
36 |
| - if (!keyMode) { |
37 |
| - throw new Error("AES: unsupported mode"); |
38 |
| - } |
39 |
| - const wKey = await crypto.web.subtle.importKey( |
40 |
| - "raw", |
41 |
| - key, |
42 |
| - { name: `AES-${keyMode.toUpperCase()}`, length: key.length * 8 }, |
43 |
| - true, |
44 |
| - ["encrypt", "decrypt"] |
45 |
| - ); |
46 |
| - // node.js uses whole 128 bit as a counter, without nonce, instead of 64 bit |
47 |
| - // recommended by NIST SP800-38A |
48 |
| - return [wKey, { name: `aes-${keyMode}`, iv, counter: iv, length: 128 }]; |
| 26 | + throw new Error("AES: unsupported mode"); |
49 | 27 | }
|
50 | 28 |
|
51 |
| -export async function encrypt( |
| 29 | +export function encrypt( |
52 | 30 | msg: Uint8Array,
|
53 | 31 | key: Uint8Array,
|
54 | 32 | iv: Uint8Array,
|
55 | 33 | mode = "aes-128-ctr",
|
56 | 34 | pkcs7PaddingEnabled = true
|
57 |
| -): Promise<Uint8Array> { |
58 |
| - validateOpt(key, iv, mode); |
59 |
| - if (crypto.web) { |
60 |
| - const [wKey, wOpt] = await getBrowserKey(mode, key, iv); |
61 |
| - const cipher = await crypto.web.subtle.encrypt(wOpt, wKey, msg); |
62 |
| - // Remove PKCS7 padding on cbc mode by stripping end of message |
63 |
| - let res = new Uint8Array(cipher); |
64 |
| - if (!pkcs7PaddingEnabled && wOpt.name === "aes-cbc" && !(msg.length % 16)) { |
65 |
| - res = res.slice(0, -16); |
66 |
| - } |
67 |
| - return res; |
68 |
| - } else if (crypto.node) { |
69 |
| - const cipher = crypto.node.createCipheriv(mode, key, iv); |
70 |
| - cipher.setAutoPadding(pkcs7PaddingEnabled); |
71 |
| - return concatBytes(cipher.update(msg), cipher.final()); |
72 |
| - } else { |
73 |
| - throw new Error("The environment doesn't have AES module"); |
74 |
| - } |
| 35 | +): Uint8Array { |
| 36 | + return getCipher(key, iv, mode, pkcs7PaddingEnabled).encrypt(msg); |
75 | 37 | }
|
76 | 38 |
|
77 |
| -async function getPadding( |
78 |
| - cypherText: Uint8Array, |
79 |
| - key: Uint8Array, |
80 |
| - iv: Uint8Array, |
81 |
| - mode: string |
82 |
| -) { |
83 |
| - const lastBlock = cypherText.slice(-16); |
84 |
| - for (let i = 0; i < 16; i++) { |
85 |
| - // Undo xor of iv and fill with lastBlock ^ padding (16) |
86 |
| - lastBlock[i] ^= iv[i] ^ 16; |
87 |
| - } |
88 |
| - const res = await encrypt(lastBlock, key, iv, mode); |
89 |
| - return res.slice(0, 16); |
90 |
| -} |
91 |
| - |
92 |
| -export async function decrypt( |
93 |
| - cypherText: Uint8Array, |
| 39 | +export function decrypt( |
| 40 | + ciphertext: Uint8Array, |
94 | 41 | key: Uint8Array,
|
95 | 42 | iv: Uint8Array,
|
96 | 43 | mode = "aes-128-ctr",
|
97 | 44 | pkcs7PaddingEnabled = true
|
98 |
| -): Promise<Uint8Array> { |
99 |
| - validateOpt(key, iv, mode); |
100 |
| - if (crypto.web) { |
101 |
| - const [wKey, wOpt] = await getBrowserKey(mode, key, iv); |
102 |
| - // Add empty padding so Chrome will correctly decrypt message |
103 |
| - if (!pkcs7PaddingEnabled && wOpt.name === "aes-cbc") { |
104 |
| - const padding = await getPadding(cypherText, key, iv, mode); |
105 |
| - cypherText = concatBytes(cypherText, padding); |
106 |
| - } |
107 |
| - const msg = await crypto.web.subtle.decrypt(wOpt, wKey, cypherText); |
108 |
| - const msgBytes = new Uint8Array(msg); |
109 |
| - // Safari always ignores padding (if no padding -> broken message) |
110 |
| - if (wOpt.name === "aes-cbc") { |
111 |
| - const encrypted = await encrypt(msgBytes, key, iv, mode); |
112 |
| - if (!equalsBytes(encrypted, cypherText)) { |
113 |
| - throw new Error("AES: wrong padding"); |
114 |
| - } |
115 |
| - } |
116 |
| - return msgBytes; |
117 |
| - } else if (crypto.node) { |
118 |
| - const decipher = crypto.node.createDecipheriv(mode, key, iv); |
119 |
| - decipher.setAutoPadding(pkcs7PaddingEnabled); |
120 |
| - return concatBytes(decipher.update(cypherText), decipher.final()); |
121 |
| - } else { |
122 |
| - throw new Error("The environment doesn't have AES module"); |
123 |
| - } |
| 45 | +): Uint8Array { |
| 46 | + return getCipher(key, iv, mode, pkcs7PaddingEnabled).decrypt(ciphertext); |
124 | 47 | }
|
0 commit comments