最近突發奇想,想將系統查詢結果嵌入網頁匯出成 .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,我花了點時了解:

實測 "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代碼

Post a comment