|
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