aes.js 3.7 KB

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