6

用 JavaScript 執行 AES 加解密 - 香草口味

 11 months ago
source link: https://blog.darkthread.net/blog/vanilla-js-aes-decryption/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

用 JavaScript 執行 AES 加解密

最近突發奇想,想將系統查詢結果嵌入網頁匯出成 .html,概念上像 Excel 或 Word 一樣是個文件檔,方便 Email 轉寄、歸檔保存,而採用網頁的好處是免裝軟體,用瀏覽器就能開啟,透過 JavaScript 可實現極佳的互動操作體驗。

但我馬上想到一個問題,針對機敏資料,Excel/Word/PDF 可以加上密碼保護,HTML 檔不行! 我想到的解決方式是比照 Excel/Word 對資料內容加密,檢視網頁時需輸入密碼解密才能讀取內容。這樣的話,JavaScript 端必須有解密能力。

原以為要依賴第三方程式庫,驚喜發現當代瀏覽器很早前就已內建 Web Crypto API,負責低階加解密運算的 subtle API,個人電腦的主要瀏覽器(Chrome/Edge/Firefox/Safari/Opera)都有支援,想在工作環境應用不必擔心瀏覽器不給力。

Fig1_638207564587787932.png
資料來源

我的目標很簡單:到用 C# AES 加密的內容,在 JavaScript 能用相同金鑰解密,這樣就能實現與 Word/Excel/PDF 同等級的密碼保護。

借用之前 使用 Bouncy Castle DES/AES 加解密 文章的密碼字串 SHA256 轉 AES Key/IV byte[16] 邏輯,這次的 JavaScript 程式若能用 "#The3ncryp7Key" 將 "QfKvmv2wlAMhqXYM1c5gzLcrf24x+qnMXIwHpNqO4Os="

<!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,我花了點時了解:

實測 "QfKvmv2wlAMhqXYM1c5gzLcrf24x+qnMXIwHpNqO4Os=" 解密成功! (灑花)

Fig2_638207564593017841.gif

附上線上展示


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK