代码: 全选
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>加密與解密</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js"></script>
<script src="https://bitwiseshiftleft.github.io/sjcl/sjcl.js"></script>
<style>
body {
font-family: Arial, sans-serif;
padding: 20px;
max-width: 600px;
margin: 0 auto;
background-color: #f4f4f4;
}
h2, h4 {
color: #333;
}
textarea, input, button {
display: block;
width: 100%;
margin-bottom: 10px;
padding: 10px;
font-size: 16px;
}
textarea {
height: 100px;
resize: vertical;
}
input[type="password"] {
border: 1px solid #ccc;
border-radius: 4px;
}
button {
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background-color: #0056b3;
}
pre {
background-color: #fff;
border: 1px solid #ddd;
padding: 10px;
white-space: pre-wrap;
word-wrap: break-word;
cursor: pointer;
margin-bottom: 10px;
border-radius: 4px;
}
pre:hover {
background-color: #f0f0f0;
}
@media (max-width: 600px) {
body {
padding: 10px;
}
textarea, input, button {
font-size: 14px;
}
}
</style>
</head>
<body>
<h2>加密</h2>
<h4>輸入明文與密碼</h4>
<textarea id="ejc_text" placeholder="輸入明文"></textarea>
<input id="ejc_encryptpw" type="password" placeholder="輸入加密密碼">
<button type="button" id="ejc_encrypt">加密</button>
<h4>CryptoJS AES 加密結果</h4>
<pre id="ejc_aes_ecb">ECB 模式結果</pre>
<pre id="ejc_aes_cbc">CBC 模式結果</pre>
<pre id="ejc_aes_cfb">CFB 模式結果</pre>
<pre id="ejc_aes_ctr">CTR 模式結果</pre>
<pre id="ejc_aes_ofb">OFB 模式結果</pre>
<h4>SJCL AES 加密結果 (CCM, GCM, OCB2)</h4>
<pre id="sjcl_mode_ccm">CCM 模式結果</pre>
<pre id="sjcl_mode_gcm">GCM 模式結果</pre>
<pre id="sjcl_mode_ocb2">OCB2 模式結果</pre>
<h2>解密</h2>
<textarea id="ejc_encrypttext" placeholder="輸入加密結果(JSON格式)"></textarea>
<input id="ejc_decryptpw" type="password" placeholder="輸入解密密碼">
<button type="button" id="ejc_decrypt">解密</button>
<textarea id="ejc_decryptresult" placeholder="解密結果" readonly></textarea>
<script>
// CryptoJS 模式映射
const cryptoModes = {
ecb: CryptoJS.mode.ECB,
cbc: CryptoJS.mode.CBC,
cfb: CryptoJS.mode.CFB,
ctr: CryptoJS.mode.CTR,
ofb: CryptoJS.mode.OFB
};
// 加密按鈕事件
document.getElementById('ejc_encrypt').addEventListener('click', () => {
const text = document.getElementById('ejc_text').value;
const password = document.getElementById('ejc_encryptpw').value;
if (!text || !password) {
alert('請輸入明文和密碼');
return;
}
// CryptoJS 加密 (保持不變)
for (const [modeName, mode] of Object.entries(cryptoModes)) {
const salt = CryptoJS.lib.WordArray.random(16);
const iv = modeName !== 'ecb' ? CryptoJS.lib.WordArray.random(16) : '';
const key = CryptoJS.PBKDF2(password, salt, { keySize: 256/32, iterations: 1000 });
const encrypted = CryptoJS.AES.encrypt(text, key, {
iv: iv,
mode: mode,
padding: CryptoJS.pad.Pkcs7
});
// 注意:這裡的 HMAC 計算是針對 ciphertext 的,並非 AEAD 模式的內建 Tag
const hmac = CryptoJS.HmacSHA256(encrypted.ciphertext, key).toString(CryptoJS.enc.Base64);
const result = {
ct: encrypted.ciphertext.toString(CryptoJS.enc.Base64),
salt: salt.toString(CryptoJS.enc.Base64),
iv: iv ? iv.toString(CryptoJS.enc.Base64) : '',
'hmac-sha256': hmac, // 這裡添加 HMAC
config: {
mode: modeName.toUpperCase(),
padding: 'Pkcs7',
iterations: 1000,
keySize: 256
}
};
document.getElementById(`ejc_aes_${modeName}`).textContent = JSON.stringify(result, null, 2);
}
// ---
// SJCL 加密 (更新為 CCM, GCM, OCB2)
const sjclModes = ['ccm', 'gcm', 'ocb2'];
for (const mode of sjclModes) {
try {
// 生成隨機鹽
const salt = sjcl.random.randomWords(4); // 16 bytes
// 使用 PBKDF2 生成密鑰
const key = sjcl.misc.pbkdf2(password, salt, 1000, 256);
// 生成隨機 IV (Nonce)
// SJCL AEAD 模式的 IV 長度通常為 128 位 (16 bytes, 即 4 words)
const iv = sjcl.random.randomWords(4);
// 設置加密參數
const tagSize = 128; // 對於 AEAD 模式,tagSize 通常是 128 位 (16 bytes),也可以是 64 位 (8 bytes) 或 96 位 (12 bytes)
// SJCL 預設為 64 位,但 128 位更安全
const params = {
mode: mode,
iter: 1000,
ks: 256,
ts: tagSize // 設置標籤長度為認證模式
};
// 加密
// sjcl.mode[mode].encrypt 方法的參數順序:
// (cipher, plaintext, iv, adata, tlen)
// cipher: sjcl.cipher.aes(key)
// plaintext: sjcl.codec.utf8String.toBits(text)
// iv: iv (Nonce)
// adata: 額外認證數據 (通常可選,這裡留空)
// tlen: 標籤長度 (tagSize)
const ct = sjcl.mode[mode].encrypt(
new sjcl.cipher.aes(key),
sjcl.codec.utf8String.toBits(text),
iv,
undefined, // adata (Associated Data)
tagSize // tagSize
);
// 構建結果對象
// 對於 AEAD 模式,密文 `ct` 已經包含了加密的數據和認證標籤 (tag)
// 所以不需要額外計算 HMAC
const result = {
ct: sjcl.codec.base64.fromBits(ct),
salt: sjcl.codec.base64.fromBits(salt),
iv: sjcl.codec.base64.fromBits(iv), // IV 是必須的
config: {
mode: mode.toUpperCase(),
iterations: 1000,
keySize: 256,
tagSize: tagSize // 記錄使用的標籤長度
}
};
document.getElementById(`sjcl_mode_${mode}`).textContent = JSON.stringify(result, null, 2);
} catch (e) {
console.error(`SJCL ${mode.toUpperCase()} 加密失敗:`, e);
document.getElementById(`sjcl_mode_${mode}`).textContent = `SJCL ${mode.toUpperCase()} 加密失敗: ${e.message}`;
}
}
});
// 點擊 pre 填充加密結果
document.querySelectorAll('pre').forEach(pre => {
pre.addEventListener('click', () => {
document.getElementById('ejc_encrypttext').value = pre.textContent;
});
});
// 解密按鈕事件
document.getElementById('ejc_decrypt').addEventListener('click', () => {
const encryptedText = document.getElementById('ejc_encrypttext').value;
const password = document.getElementById('ejc_decryptpw').value;
const resultArea = document.getElementById('ejc_decryptresult');
if (!encryptedText || !password) {
resultArea.value = '請輸入加密結果和密碼';
return;
}
try {
const data = JSON.parse(encryptedText);
// 檢查是否為 SJCL 舊格式 (sjcl.decrypt 統一處理)
if ('v' in data && 'cipher' in data && data.cipher === 'aes') {
try {
const decrypted = sjcl.decrypt(password, JSON.stringify(data));
resultArea.value = decrypted;
} catch (e) {
resultArea.value = 'SJCL 舊格式解密失敗:' + e.message;
}
return;
}
// 檢查是否為新 SJCL 格式 (CCM, GCM, OCB2)
const sjclModes = ['ccm', 'gcm', 'ocb2'];
if (data.config && sjclModes.includes(data.config.mode.toLowerCase())) {
const mode = data.config.mode.toLowerCase();
const key = sjcl.misc.pbkdf2(
password,
sjcl.codec.base64.toBits(data.salt),
data.config.iterations,
data.config.keySize
);
const ct = sjcl.codec.base64.toBits(data.ct);
const iv = data.iv ? sjcl.codec.base64.toBits(data.iv) : undefined;
const tagSize = data.config.tagSize; // 從配置中獲取標籤長度
// 對於 AEAD 模式 (CCM, GCM, OCB2),解密操作會自動驗證 Tag
// 如果 Tag 不匹配,會拋出錯誤,無需手動驗證 HMAC
try {
const decrypted = sjcl.mode[mode].decrypt(
new sjcl.cipher.aes(key),
ct, // 密文 (包含 Tag)
iv, // IV
undefined, // adata (Associated Data)
tagSize // tagSize
);
resultArea.value = sjcl.codec.utf8String.fromBits(decrypted);
} catch (e) {
// 如果解密失敗,通常是因為密碼錯誤、數據損壞或 Tag 驗證失敗
resultArea.value = `SJCL ${mode.toUpperCase()} 解密失敗:` + e.message;
}
return;
}
// 假設為 CryptoJS 格式 (保持不變)
const { ct, salt, iv, config } = data;
const key = CryptoJS.PBKDF2(password, CryptoJS.enc.Base64.parse(salt), {
keySize: config.keySize/32,
iterations: config.iterations
});
// 如果有 HMAC,先驗證 HMAC
if (data['hmac-sha256']) {
const computedHmac = CryptoJS.HmacSHA256(CryptoJS.enc.Base64.parse(ct), key).toString(CryptoJS.enc.Base64);
if (computedHmac !== data['hmac-sha256']) {
resultArea.value = 'CryptoJS 解密失敗:HMAC 驗證失敗';
return;
}
}
const decrypted = CryptoJS.AES.decrypt(
{ ciphertext: CryptoJS.enc.Base64.parse(ct) },
key,
{
iv: iv ? CryptoJS.enc.Base64.parse(iv) : undefined,
mode: cryptoModes[config.mode.toLowerCase()],
padding: CryptoJS.pad.Pkcs7
}
);
const decryptedText = decrypted.toString(CryptoJS.enc.Utf8);
if (!decryptedText) {
resultArea.value = 'CryptoJS 解密失敗:密碼錯誤或數據損壞';
} else {
resultArea.value = decryptedText;
}
} catch (e) {
resultArea.value = '解密失敗:無效的 JSON 格式或數據' + e.message;
}
});
</script>
</body>
</html>