如何实现PHP服务端加密文件并在JavaScript客户端解密?
PHP服务端加密文件,JavaScript客户端解密方案
问题背景
你已实现PHP端文件加密接口,返回包含IV与密文的Base64编码数据,需在浏览器端通过JavaScript完成解密并还原原始文件。
PHP端已实现的加密代码
加密函数
function encryptFile($path, $key) { $iv = openssl_random_pseudo_bytes(16); // 注意:AES-256-CBC要求32字节密钥,若直接使用短字符串密钥,PHP会自动用0填充至32字节 // 推荐优化:将密钥哈希为标准32字节,避免兼容性问题:$key = hash('sha256', $key, true); $encrypted = openssl_encrypt(file_get_contents($path), 'aes-256-cbc', $key, OPENSSL_RAW_DATA, $iv); return base64_encode($iv . $encrypted); }
加密文件下载接口
public function downloadEncryptedFile() { $key = 'fee-fi-fum'; $path = storage_path('app/public/nicesong.mp3'); $encrypted = encryptFile($path, $key); return response($encrypted, 200, [ 'Content-Type' => 'application/octet-stream', 'Content-Disposition' => 'attachment; filename="your-file.encrypted"' ]); }
JavaScript客户端解密实现
核心逻辑
- 读取下载的加密文件(Base64格式字符串)
- 解码Base64为二进制数据,分离出前16字节的IV与剩余密文
- 将密钥处理为符合AES-256要求的32字节格式(与PHP端逻辑对齐)
- 使用浏览器原生Web Crypto API完成解密
- 还原为原始文件并触发下载
完整代码示例
// 解密主函数 async function decryptEncryptedFile(file, password) { // 1. 读取加密文件内容(Base64字符串) const fileContent = await readFileAsText(file); // 2. Base64解码为二进制数组 const binaryData = base64ToUint8Array(fileContent); // 3. 拆分IV(前16字节)和密文 const iv = binaryData.slice(0, 16); const ciphertext = binaryData.slice(16); // 4. 处理密钥:和PHP端逻辑对齐 // 场景1:PHP端直接使用短密钥字符串(自动补0到32字节) const keyBytes = new TextEncoder().encode(password); const paddedKey = new Uint8Array(32); paddedKey.set(keyBytes, 0); // 用0填充至32字节,匹配PHP行为 const key = await window.crypto.subtle.importKey( 'raw', paddedKey, { name: 'AES-CBC', length: 256 }, false, ['decrypt'] ); // 场景2:若PHP端已将密钥转为SHA256哈希(推荐),替换为以下代码: // const keyMaterial = await window.crypto.subtle.importKey( // 'raw', // new TextEncoder().encode(password), // { name: 'PBKDF2' }, // false, // ['deriveKey'] // ); // const key = await window.crypto.subtle.deriveKey( // { name: 'PBKDF2', salt: new Uint8Array(0), iterations: 1, hash: 'SHA-256' }, // keyMaterial, // { name: 'AES-CBC', length: 256 }, // false, // ['decrypt'] // ); // 5. 执行解密 const decryptedBuffer = await window.crypto.subtle.decrypt( { name: 'AES-CBC', iv: iv }, key, ciphertext ); // 6. 还原为原始文件并下载 const decryptedBlob = new Blob([decryptedBuffer], { type: 'audio/mpeg' }); // 替换为原始文件的MIME类型 downloadBlob(decryptedBlob, 'nicesong.mp3'); } // 辅助函数:读取文件为文本 function readFileAsText(file) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(reader.result); reader.onerror = reject; reader.readAsText(file); }); } // 辅助函数:Base64转Uint8Array function base64ToUint8Array(base64) { const binaryString = window.atob(base64); const bytes = new Uint8Array(binaryString.length); for (let i = 0; i < binaryString.length; i++) { bytes[i] = binaryString.charCodeAt(i); } return bytes; } // 辅助函数:触发文件下载 function downloadBlob(blob, filename) { const url = window.URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = filename; document.body.appendChild(link); link.click(); window.URL.revokeObjectURL(url); document.body.removeChild(link); } // 使用示例:绑定文件选择事件 document.getElementById('encrypted-file-input').addEventListener('change', async (e) => { const file = e.target.files[0]; if (!file) return; const password = 'fee-fi-fum'; // 与PHP端一致的密钥 await decryptEncryptedFile(file, password); });
关键注意事项
- 密钥一致性:PHP与JS端的密钥处理逻辑必须完全对齐,否则解密会失败。优先推荐将密钥转为SHA256哈希的标准32字节格式。
- MIME类型:创建Blob时需指定原始文件的正确MIME类型(如MP3对应
audio/mpeg)。 - 环境限制:Web Crypto API仅在HTTPS环境或localhost下可用,避免明文传输密钥。
内容的提问来源于stack exchange,提问作者3m1n3nc3
相关产品推荐
相关产品推荐

