用 JavaScript 執行 AES 加解密 - 香草口味
7 |
最近突發奇想,想將系統查詢結果嵌入網頁匯出成 .html,概念上像 Excel 或 Word 一樣是個文件檔,方便 Email 轉寄、歸檔保存,而採用網頁的好處是免裝軟體,用瀏覽器就能開啟,透過 JavaScript 可實現極佳的互動操作體驗。
但我馬上想到一個問題,針對機敏資料,Excel/Word/PDF 可以加上密碼保護,HTML 檔不行! 我想到的解決方式是比照 Excel/Word 對資料內容加密,檢視網頁時需輸入密碼解密才能讀取內容。這樣的話,JavaScript 端必須有解密能力。
原以為要依賴第三方程式庫,驚喜發現當代瀏覽器很早前就已內建 Web Crypto API,負責低階加解密運算的 subtle API,個人電腦的主要瀏覽器(Chrome/Edge/Firefox/Safari/Opera)都有支援,想在工作環境應用不必擔心瀏覽器不給力。
我的目標很簡單:到用 C# AES 加密的內容,在 JavaScript 能用相同金鑰解密,這樣就能實現與 Word/Excel/PDF 同等級的密碼保護。
借用之前 使用 Bouncy Castle DES/AES 加解密 文章的密碼字串 SHA256 轉 AES Key/IV byte[16] 邏輯,這次的 JavaScript 程式若能用 "#The3ncryp7Key" 將 "QfKvmv2wlAMhqXYM1c5gzLcrf24x+qnMXIwHpNqO4Os=" 還原回 "Hello, World! 加密測試" 就算成功。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>AES Encryption/Decryption Demo</title>
<style>
div { padding: 3px; }
input { width: 350px; }
#key { width: 200px; }
#encText,#decText {
color: darkblue;
height: 1em;
}
</style>
</head>
<body>
<div>
Key = <input type="text" id="key" value="#The3ncryp7Key">
</div>
<div>
<input type="text" id="plain" value="Hello World!">
<button onclick="encrypt()">Encrypt</button>
<div id="encText"></div>
</div>
<div>
<input type="text" id="encrypted" value="QfKvmv2wlAMhqXYM1c5gzLcrf24x+qnMXIwHpNqO4Os=">
<button onclick="decrypt()">Decrypt</button>
<div id="decText"></div>
</div>
<script>
if (!window.crypto?.subtle) {
alert("Your browser is unsupported!");
}
async function createCryptoKey(key, keyUsage ) {
const hashBuffer = await window.crypto.subtle.digest("SHA-256", new TextEncoder().encode(key));
// split the sha256 hash byte array into key and iv
let keyPart = new Uint8Array(hashBuffer.slice(0, 16));
let ivPart = new Uint8Array(hashBuffer.slice(16));
// create a CryptoKey object from the key byte array
const cryptoKey = await window.crypto.subtle.importKey(
"raw", // format
keyPart, // key data (as a Uint8Array)
{ name: "AES-CBC" }, // algorithm
false, // not extractable
[keyUsage]
);
// return CryptoKey and IV
return { cryptoKey, ivPart };
}
async function encryptData(enc, key) {
const { cryptoKey, ivPart } = await createCryptoKey(key, "encrypt");
const data = new TextEncoder().encode(enc);
const encryptedBytes = await window.crypto.subtle.encrypt(
{ name: "AES-CBC", iv: ivPart },
cryptoKey,
data
);
const encrypted = btoa(String.fromCharCode(...new Uint8Array(encryptedBytes)));
return encrypted;
}
async function decryptData(data, key) {
const { cryptoKey, ivPart } = await createCryptoKey(key, "decrypt");
// Convert the base64-encoded data to a Uint8Array
const dataBytes = new Uint8Array(atob(data).split("").map(c => c.charCodeAt(0)));
// Decrypt the data using the CryptoKey object
const decryptedBytes = await window.crypto.subtle.decrypt(
{ name: "AES-CBC", iv: ivPart },
cryptoKey,
dataBytes
);
return new TextDecoder().decode(decryptedBytes);
}
function encrypt() {
const plain = document.querySelector("#plain").value;
const key = document.querySelector("#key").value;
encryptData(plain, key).then((result) => {
document.querySelector("#encrypted").value = result;
document.querySelector("#encText").textContent = result;
}, (err) => {
console.log(err);
});
}
function decrypt() {
const encrypted = document.querySelector("#encrypted").value;
const key = document.querySelector("#key").value;
decryptData(encrypted, key).then((result) => {
document.querySelector("#decText").textContent = result;
}).catch((err) => {
alert(err.message);
});
}
</script>
</body>
</html>
靠著 Github Copilot 輔助,我先拼湊出半成品,接著依我的需求改成對密碼字串做 SHA256 雜湊產生 Key/IV,修修改改出可執行的版本,接著,爬文搞懂程式碼沒看過的 API 用法,把程式碼改得更簡潔,得到上述的版本。這是我心目中使用 AI 輔助開發的正確姿勢,而非期待描述完需求不動腦就拿到程式,若是如此,代表你的需求是大家早就玩爛的題材。舉凡進階一點的實務需求,都像在探索沒人去過的祕境,你很難期待 AI 直接報路讓你閉著眼睛走到目的地。你必須頭腦清楚,自己辦別方向掌握油門速度,但 Github Copilot 絕對有莫大幫助;AI 吃過的鹽比你吃過的米還多,實戰經驗豐富,隨時依據路況提供實用建議,但接受與否在你,成敗結果自負。優秀的開發者有 AI 協助,能更快完成各種挑戰,但如果什麼都不想學,巴望有了 AI 就能無腦寫完程式,應該會撞牆撞到吐。
總之,Copilot 噴出一堆我沒用過的 API,我花了點時了解:
- SubtleCrypto digest 計算 SHA1(不建議)、SHA-256、SHA-384、SHA-512 雜湊
- SubtleCrypto importKey 建立金鑰,支援 RSA、ECDSA、HYMAC、AES、PBKDF、HKDF 等加密演算法
- Uint8Array 要在 JavaScript 處理 byte[] 必學
- Base64 字串轉 Uint8Array: new Uint8Array(atob(data).split("").map(c => c.charCodeAt(0)))
- TextDecoder.decode()、TextEncoder.encode() 字串與 byte[] 轉換,只支援 UTF-8
實測 "QfKvmv2wlAMhqXYM1c5gzLcrf24x+qnMXIwHpNqO4Os=" 解密成功! (灑花)
附上線上展示。
Example of using browser crypto API to implement AES decryption with vanilla JavaScript.
Comments
# by Dxball
讓我想到 StatiCrypt https://github.com/robinmoisson/staticrypt
# by Jeffrey
to Dxball,酷! 原來有人也想過同樣的點子。感謝分享~ PS: 還發現其他類似專案 https://github.com/MaxLaumeister/PageCrypt https://github.com/Greenheart/pagecrypt
# by Ike
『這次的 JavaScript 程式若能用 "#The3ncryp7Key" 將 "QfKvmv2wlAMhqXYM1c5gzLcrf24x+qnMXIwHpNqO4Os="』 這句子不完整?
# by Jeffrey
to lke, 謝謝,已補正。
# by Huang
好像也適用在html原始碼保護?
# by Jeffrey
to Huang, 我的 Side Project 雛型展示 https://www.facebook.com/watch/?v=2387830581384194
# by aStudent
really nice code !! love the writer 博主的代碼很好用!我是個大學生,正在發愁怎麽做一個安全的網站,感謝博主的AES代碼