数据加密,是一门历史悠久的技术,指通过加密算法和加密密钥将明文转变为密文,而解密则是通过解密算法和解密密钥将密文恢复为明文。它的核心是密码学。数据加密仍是计算机系统对信息进行保护的一种最可靠的办法。它利用密码技术对信息进行加密,实现信息隐蔽,从而起到保护信息的安全的作用。
本文阿宝哥将介绍如何对数据进行混合加密,即使用对称加密算法与非对称加密算法对数据进行加密,从而进一步保证数据的安全性。阅读完本文,你将了解以下内容:
在最后的 阿宝哥有话说 环节,阿宝哥还将简单介绍一下什么是消息摘要算法和什么是 MD5 算法及其用途与缺陷。好的,现在让我们步入正题。为了让刚接触混合加密的小伙伴更好地了解并掌握混合加密,阿宝哥将乘坐 “时光机” 带大家来到某个发版的夜晚...
那一晚我们团队的小伙伴正在等服务端数据升级,为了让大家 “忘记” 这个漫漫的升级过程,阿宝哥就立马组织了一场关于混合加密的技术分享会。在阿宝哥 “威逼利诱” 之下,团队的小伙伴们很快就到齐了,之后阿宝哥以以下对话拉开了分享会的序幕:
几分钟过后,小哥讲完了,基本关键点都有回答上来,但还遗漏了一些内容。为了让小伙伴们更好地理解对称加密,阿宝哥对小哥表述的内容进行了重新梳理,下面让我们来一起认识一下对称加密。
对称密钥算法(英语:Symmetric-key algorithm)又称为对称加密、私钥加密、共享密钥加密,是密码学中的一类加密算法。这类算法在加密和解密时使用相同的密钥,或是使用两个可以简单地相互推算的密钥。
算法公开、计算量小、加密速度快、加密效率高,适合对大量数据进行加密的场景。 比如 HLS(HTTP Live Streaming)普通加密场景中,一般会使用 AES-128 对称加密算法对 TS 切片进行加密,以保证多媒体资源安全。
发送方使用密钥将明文数据加密成密文,然后发送出去,接收方收到密文后,使用同一个密钥将密文解密成明文读取。
常见的对称加密算法有 AES、ChaCha20、3DES、Salsa20、DES、Blowfish、IDEA、RC5、RC6、Camellia。这里我们以常见的 AES 算法为例,来介绍一下 AES(Advanced Encryption Standard)对称加密与解密的过程。
下面阿宝哥将使用 crypto-js 这个库来介绍 AES 算法的加密与解密,该库提供了 CryptoJS.AES.encrypt()
方法用于实现 AES 加密,而 AES 解密对应的方法是 CryptoJS.AES.decrypt()
。
基于上述两个方法阿宝哥进一步封装了 aesEncrypt()
和 aesDecrypt()
这两个方法,它们分别用于 AES 加密与解密,其具体实现如下所示:
// AES加密 function aesEncrypt(content) { let text = CryptoJS.enc.Utf8.parse(JSON.stringify(content)); let encrypted = CryptoJS.AES.encrypt(text, key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7, }); return encrypted.toString(); }
// AES解密 function aesDecrypt(content) { let decrypt = CryptoJS.AES.decrypt(content, key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7, }); return decrypt.toString(CryptoJS.enc.Utf8); }
在以上示例中,我们在页面上创建了 3 个 textarea,分别用于存放明文、加密后的密文和解密后的明文。当用户点击 加密 按钮时,会对用户输入的明文进行 AES 加密,完成加密后,会把密文显示在密文对应的 textarea 中,当用户点击 解密 按钮时,会对密文进行 AES 解密,完成解密后,会把解密后的明文显示在对应的 textarea 中。
以上示例对应的完整代码如下所示:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>AES 对称加密与解密示例</title> <style> .block { flex: 1; } </style> </head> <body> <h3>阿宝哥:AES 对称加密与解密示例(CBC 模式)</h3> <div style="display: flex;"> <div class="block"> <p>①明文加密 => <button onclick="encrypt()">加密</button></p> <textarea id="plaintext" rows="5" cols="15"></textarea> </div> <div class="block"> <p>②密文解密 => <button onclick="decrypt()">解密</button></p> <textarea id="ciphertext" rows="5" cols="15"></textarea> </div> <div class="block"> <p>③解密后的明文</p> <textarea id="decryptedCiphertext" rows="5" cols="15"></textarea> </div> </div> <!-- 引入 CDN Crypto.js AES加密 --> <script src="https://cdn.bootcdn.net/ajax/libs/crypto-js/4.0.0/core.min.js"></script> <script src="https://cdn.bootcdn.net/ajax/libs/crypto-js/4.0.0/enc-base64.min.js"></script> <script src="https://cdn.bootcdn.net/ajax/libs/crypto-js/4.0.0/cipher-core.min.js"></script> <script src="https://cdn.bootcdn.net/ajax/libs/crypto-js/4.0.0/aes.min.js"></script> <script src="https://cdn.bootcdn.net/ajax/libs/crypto-js/4.0.0/pad-pkcs7.min.js"></script> <script src="https://cdn.bootcdn.net/ajax/libs/crypto-js/4.0.0/enc-utf8.min.js"></script> <!-- 引入 CDN Crypto.js 结束 --> <script> const key = CryptoJS.enc.Utf8.parse("0123456789abcdef"); // 密钥 const iv = CryptoJS.enc.Utf8.parse("abcdef0123456789"); // 初始向量 const plaintextEle = document.querySelector("#plaintext"); const ciphertextEle = document.querySelector("#ciphertext"); const decryptedCiphertextEle = document.querySelector( "#decryptedCiphertext" ); function encrypt() { let plaintext = plaintextEle.value; ciphertextEle.value = aesEncrypt(plaintext); } function decrypt() { let ciphertext = ciphertextEle.value; decryptedCiphertextEle.value = aesDecrypt(ciphertext).replace(/\"/g,''); } // AES加密 function aesEncrypt(content) { let text = CryptoJS.enc.Utf8.parse(JSON.stringify(content)); let encrypted = CryptoJS.AES.encrypt(text, key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7, }); return encrypted.toString(); } // AES解密 function aesDecrypt(content) { let decrypt = CryptoJS.AES.decrypt(content, key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7, }); return decrypt.toString(CryptoJS.enc.Utf8); } </script> </body> </html>
在上面的示例中,我们通过 AES 对称加密算法,对 “我是阿宝哥” 明文进行加密,从而实现信息隐蔽。
那么使用对称加密算法就可以解决我们前面的问题么?答案是否定,这是因为对称加密存在一些的缺点。
通过使用对称加密算法,我们已经把明文加密成密文。虽然这解决了数据的安全性,但同时也带来了另一个新的问题。因为对称加密算法,加密和解密时使用的是同一个密钥,所以对称加密的安全性就不仅仅取决于加密算法本身的强度,更取决于密钥是否被安全的传输或保管。
另外对于实际应用场景,为了避免单一的密钥被攻破,从而导致所有的加密数据被破解,对于不同的数据,我们一般会使用不同的密钥进行加密,这样虽然提高了安全性,但也增加了密钥管理的难度。
由于对称加密存在以上的问题,因此它并不是一种好的解决方案。为了找到更好的方案,阿宝哥开始了另一轮新的对话。
非对称加密算法需要两个密钥:公开密钥(publickey:简称公钥)和私有密钥(privatekey:简称私钥)。公钥与私钥是一对,如果用公钥对数据进行加密,只有用对应的私钥才能解密。 因为加密和解密使用的是两个不同的密钥,所以这种算法叫作非对称加密算法。
安全性更高,公钥是公开的,私钥是自己保存的,不需要将私钥提供给别人。
常见的非对称加密算法有 RSA、Elgamal、背包算法、Rabin、D-H、ECC(椭圆曲线加密算法)。这里我们以常见的 RSA 算法为例,来介绍一下 RSA 非对称加密与解密的过程。
RSA 是 1977 年由罗纳德·李维斯特(Ron Rivest)、阿迪·萨莫尔(Adi Shamir)和伦纳德·阿德曼(Leonard Adleman)一起提出的。当时他们三人都在麻省理工学院工作。RSA 就是他们三人姓氏开头字母拼在一起组成的。
下面阿宝哥将使用 jsencrypt 这个库来介绍 RSA 算法的加密与解密,该库提供了 encrypt()
方法用于实现 RSA 加密,而 RSA 解密对应的方法是 decrypt()
。
使用 jsencrypt 这个库之前,我们需要先生成公钥和私钥。接下来阿宝哥以 macOS 系统为例,来介绍一下如何生成公私钥。
首先我们先来生成私钥,在命令行输入以下命令:
$ openssl genrsa -out rsa_1024_priv.pem 1024
在该命令成功运行之后,在当前目录下会生成一个 rsa_1024_priv.pem 文件,该文件的内容如下:
-----BEGIN RSA PRIVATE KEY----- MIICWwIBAAKBgQDocWYwnJ4DYur0BjxFjJkLv4QRJpTJnwjiwxkuJZe1HTIIuLbu /yHyHLhc2MAHKL0Ob+8tcKXKsL1oxs467+q0jA+glOUtBXFcUnutWBbnf9qIDkKP ... bKkRJNJ2PpfWA45Vdq6u+izrn9e2TabKjWIfTfT/ZQ== -----END RSA PRIVATE KEY-----
然后我们来生成公钥,同样在命令行输入以下命令:
$ openssl rsa -pubout -in rsa_1024_priv.pem -out rsa_1024_pub.pem
在该命令成功运行之后,在当前目录下会生成一个 rsa_1024_pub.pem 文件,该文件的内容如下:
-----BEGIN PUBLIC KEY----- MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDocWYwnJ4DYur0BjxFjJkLv4QR JpTJnwjiwxkuJZe1HTIIuLbu/yHyHLhc2MAHKL0Ob+8tcKXKsL1oxs467+q0jA+g lOUtBXFcUnutWBbnf9qIDkKP2uoDdZ//LUeW7jibVrVJbXU2hxB8bQpBkltZf/xs cyhRIeiXxs13vlSHVwIDAQAB -----END PUBLIC KEY-----
创建完公私钥之后,我们就可以进一步创建 RSA 加密器和解密器,具体代码如下:
const PUBLIC_KEY = `-----BEGIN PUBLIC KEY----- MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDocWYwnJ4DYur0BjxFjJkLv4QR ... cyhRIeiXxs13vlSHVwIDAQAB -----END PUBLIC KEY-----`; const PRIVATE_KEY = `-----BEGIN RSA PRIVATE KEY----- MIICWwIBAAKBgQDocWYwnJ4DYur0BjxFjJkLv4QRJpTJnwjiwxkuJZe1HTIIuLbu ... bKkRJNJ2PpfWA45Vdq6u+izrn9e2TabKjWIfTfT/ZQ== -----END RSA PRIVATE KEY-----`; const encryptor = new JSEncrypt(); // RSA加密器 encryptor.setPublicKey(PUBLIC_KEY); const decryptor = new JSEncrypt(); // RSA解密器 decryptor.setPrivateKey(PRIVATE_KEY);
在以上示例中,我们在页面上创建了 3 个 textarea,分别用于存放明文、加密后的密文和解密后的明文。当用户点击 加密 按钮时,会对用户输入的明文进行 RSA 加密,完成加密后,会把密文显示在密文对应的 textarea 中,当用户点击 解密 按钮时,会对密文进行 RSA 解密,完成解密后,会把解密后的明文显示在对应的 textarea 中。
以上示例对应的完整代码如下所示:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>RSA 对称加密与解密示例</title> <style> .block { flex: 1; } </style> </head> <body> <h3>阿宝哥:RSA 对称加密与解密示例</h3> <div style="display: flex;"> <div class="block"> <p>①明文加密 => <button onclick="encrypt()">加密</button></p> <textarea id="plaintext" rows="5" cols="15"></textarea> </div> <div class="block"> <p>②密文解密 => <button onclick="decrypt()">解密</button></p> <textarea id="ciphertext" rows="5" cols="15"></textarea> </div> <div class="block"> <p>③解密后的明文</p> <textarea id="decryptedCiphertext" rows="5" cols="15"></textarea> </div> </div> <script src="https://cdn.bootcdn.net/ajax/libs/jsencrypt/2.3.1/jsencrypt.min.js"></script> <script> const PUBLIC_KEY = `-----BEGIN PUBLIC KEY----- MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDocWYwnJ4DYur0BjxFjJkLv4QR JpTJnwjiwxkuJZe1HTIIuLbu/yHyHLhc2MAHKL0Ob+8tcKXKsL1oxs467+q0jA+g lOUtBXFcUnutWBbnf9qIDkKP2uoDdZ//LUeW7jibVrVJbXU2hxB8bQpBkltZf/xs cyhRIeiXxs13vlSHVwIDAQAB -----END PUBLIC KEY-----`; const PRIVATE_KEY = `-----BEGIN RSA PRIVATE KEY----- MIICWwIBAAKBgQDocWYwnJ4DYur0BjxFjJkLv4QRJpTJnwjiwxkuJZe1HTIIuLbu /yHyHLhc2MAHKL0Ob+8tcKXKsL1oxs467+q0jA+glOUtBXFcUnutWBbnf9qIDkKP 2uoDdZ//LUeW7jibVrVJbXU2hxB8bQpBkltZf/xscyhRIeiXxs13vlSHVwIDAQAB AoGAKOarYKpuc5IYXdArEuHmnFaa2pm7XK8LVTuXVrNuuoPkpfw61Fs4ke3T0yKg x6G3gq7Xm1tTEROAgMtaxqwo1D5n1H0nkyDFggLB0K9Ws0frp7HENtSQwdNSry1A iD8TLxkhoWo7BS0VViLT1gKOfnw4YeMJP+CcOQ+DQjCsUMECQQD0Nc0vKBTlK6GT 28gcIMVoQy2KicjiH222A9/TLCNAQ9DEeZDYEptuTfrlbggfWdgQ3nc6CyvGf6c5 6uBPi/+5AkEA86oqqZPi7ekkUVHx0VSkp0mTlD1tAPhDE8cLX8vyImGExS+tTznz ROyzm3T1M1PisdQIU8Wd5rqvHP6dB0enjwJAauhKpMQ1MYYCPApQ9g9anCQcgbOD 34nGq5HSoE2IOQ/3Cqv1PsIWjRlSJrIemCrqrafWJfDR/xnPCUnLXMd68QJAPNwG 1d4zMvslcA5ImOFMUuBEtST2geSAVINFqwK0krPKxrmWzxAJW/DHF5AJ4m0UVRhB kDLusn90V4iczgGurwJAZUz6w01OeoLhsOuWNvkbTq+IV0NQ5GAEGA721Ck5zp86 bKkRJNJ2PpfWA45Vdq6u+izrn9e2TabKjWIfTfT/ZQ== -----END RSA PRIVATE KEY-----`; const encryptor = new JSEncrypt(); // RSA加密器 encryptor.setPublicKey(PUBLIC_KEY); const decryptor = new JSEncrypt(); // RSA解密器 decryptor.setPrivateKey(PRIVATE_KEY); const plaintextEle = document.querySelector("#plaintext"); const ciphertextEle = document.querySelector("#ciphertext"); const decryptedCiphertextEle = document.querySelector( "#decryptedCiphertext" ); function encrypt() { let plaintext = plaintextEle.value; ciphertextEle.value = encryptor.encrypt(plaintext); } function decrypt() { let ciphertext = ciphertextEle.value; decryptedCiphertextEle.value = decryptor.decrypt(ciphertext); } </script> </body> </html>
在上面的示例中,我们通过 RSA 非对称加密算法,对 “我是阿宝哥” 明文进行加密,从而实现信息隐蔽。
那么使用非对称加密算法就可以解决我们前面的问题么?答案是否定,这是因为非对称加密也存在一些的缺点。
非对称加密算法加密和解密花费时间长、速度慢,只适合对少量数据进行加密。因为我们要提供的是通用的解决方案,即要同时考虑到少量数据和大量数据的情况,所以非对称加密也不是一个好的解决方案。为了解决问题,阿宝哥又重新开启了一轮新的对话。
混合加密是结合 对称加密 和 非对称加密 各自优点的一种加密方式。其具体的实现思路是先使用 对称加密算法 对数据进行加密,然后使用非对称加密算法对 对称加密的密钥 进行非对称加密,之后再把加密后的密钥和加密后的数据发送给接收方。
为了让小伙伴们更加直观理解上述的过程,阿宝哥花了点心思画了一张图,用来进一步说明混合加密的过程,下面我们就一起来看图吧。
了解完 “混合加密数据传输流程”,阿宝哥跟小伙伴一起来实现一下上述的混合加密流程。这里我们会基于前面介绍过的对称加密和非对称加密的示例进行开发,即以下示例会直接利用前面非对称加密示例中用到的公私钥。
function getRandomAESKey() { return ( Math.random().toString(36).substring(2, 10) + Math.random().toString(36).substring(2, 10) ); }
// AES加密 function aesEncrypt(key, iv, content) { let text = CryptoJS.enc.Utf8.parse(JSON.stringify(content)); let encrypted = CryptoJS.AES.encrypt(text, key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7, }); return encrypted.toString(); } // AES解密 function aesDecrypt(key, iv, content) { let decrypt = CryptoJS.AES.decrypt(content, key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7, }); return decrypt.toString(CryptoJS.enc.Utf8); }
const PUBLIC_KEY = `-----BEGIN PUBLIC KEY----- MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDocWYwnJ4DYur0BjxFjJkLv4QR ... cyhRIeiXxs13vlSHVwIDAQAB -----END PUBLIC KEY-----`; const PRIVATE_KEY = `-----BEGIN RSA PRIVATE KEY----- MIICWwIBAAKBgQDocWYwnJ4DYur0BjxFjJkLv4QRJpTJnwjiwxkuJZe1HTIIuLbu ... bKkRJNJ2PpfWA45Vdq6u+izrn9e2TabKjWIfTfT/ZQ== -----END RSA PRIVATE KEY-----`; const rsaEncryptor = new JSEncrypt(); // RSA加密器 rsaEncryptor.setPublicKey(PUBLIC_KEY); const rsaDecryptor = new JSEncrypt(); // RSA解密器 rsaDecryptor.setPrivateKey(PRIVATE_KEY);
function hybirdEncrypt(data) { const iv = getRandomAESKey(); const key = getRandomAESKey(); const encryptedData = aesEncrypt(key, iv, data); const encryptedIv = rsaEncryptor.encrypt(iv); const encryptedKey = rsaEncryptor.encrypt(key); return { iv: encryptedIv, key: encryptedKey, data: encryptedData, }; } function hybirdDecrypt(encryptedResult) { const iv = rsaDecryptor.decrypt(encryptedResult.iv); const key = rsaDecryptor.decrypt(encryptedResult.key); const data = encryptedResult.data; return aesDecrypt(key, iv, data); }
以上步骤完成之后,我们基本已经完成了混合加密的功能,在看完整代码之前,我们先来看一下实际的运行效果:
备注:密文解密下方对应的 textarea 文本框中,除了加密的数据之外,还会包含使用 RSA 加密过的 AES CBC 模式中的 iv 和 key。
在以上示例中,我们在页面上创建了 3 个 textarea,分别用于存放明文、加密后的数据和解密后的明文。当用户点击 加密 按钮时,会对用户输入的明文进行混合加密,完成加密后,会把加密的数据显示在密文对应的 textarea 中,当用户点击 解密 按钮时,会对密文进行 混合解密,即先使用 RSA 私钥解密 AES 的 key 和 iv,然后再使用它们对 AES 加密过的密文进行 AES 解密,完成解密后,会把解密后的明文显示在对应的 textarea 中。
以上示例对应的完整代码如下所示:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>混合加密与解密示例</title> <style> .block { flex: 1; } </style> </head> <body> <h3>阿宝哥:混合加密与解密示例</h3> <div style="display: flex;"> <div class="block"> <p>①明文加密 => <button onclick="encrypt()">加密</button></p> <textarea id="plaintext" rows="5" cols="15"></textarea> </div> <div class="block"> <p>②密文解密 => <button onclick="decrypt()">解密</button></p> <textarea id="ciphertext" rows="5" cols="15"></textarea> </div> <div class="block"> <p>③解密后的明文</p> <textarea id="decryptedCiphertext" rows="5" cols="15"></textarea> </div> </div> <!-- 引入 CDN Crypto.js AES加密 --> <script src="https://cdn.bootcdn.net/ajax/libs/crypto-js/4.0.0/core.min.js"></script> <script src="https://cdn.bootcdn.net/ajax/libs/crypto-js/4.0.0/enc-base64.min.js"></script> <script src="https://cdn.bootcdn.net/ajax/libs/crypto-js/4.0.0/md5.min.js"></script> <script src="https://cdn.bootcdn.net/ajax/libs/crypto-js/4.0.0/evpkdf.min.js"></script> <script src="https://cdn.bootcdn.net/ajax/libs/crypto-js/4.0.0/cipher-core.min.js"></script> <script src="https://cdn.bootcdn.net/ajax/libs/crypto-js/4.0.0/aes.min.js"></script> <script src="https://cdn.bootcdn.net/ajax/libs/crypto-js/4.0.0/pad-pkcs7.min.js"></script> <script src="https://cdn.bootcdn.net/ajax/libs/crypto-js/4.0.0/enc-utf8.min.js"></script> <!-- 引入 CDN Crypto.js 结束 --> <script src="https://cdn.bootcdn.net/ajax/libs/jsencrypt/2.3.1/jsencrypt.min.js"></script> <script> const PUBLIC_KEY = `-----BEGIN PUBLIC KEY----- MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDocWYwnJ4DYur0BjxFjJkLv4QR JpTJnwjiwxkuJZe1HTIIuLbu/yHyHLhc2MAHKL0Ob+8tcKXKsL1oxs467+q0jA+g lOUtBXFcUnutWBbnf9qIDkKP2uoDdZ//LUeW7jibVrVJbXU2hxB8bQpBkltZf/xs cyhRIeiXxs13vlSHVwIDAQAB -----END PUBLIC KEY-----`; const PRIVATE_KEY = `-----BEGIN RSA PRIVATE KEY----- MIICWwIBAAKBgQDocWYwnJ4DYur0BjxFjJkLv4QRJpTJnwjiwxkuJZe1HTIIuLbu /yHyHLhc2MAHKL0Ob+8tcKXKsL1oxs467+q0jA+glOUtBXFcUnutWBbnf9qIDkKP 2uoDdZ//LUeW7jibVrVJbXU2hxB8bQpBkltZf/xscyhRIeiXxs13vlSHVwIDAQAB AoGAKOarYKpuc5IYXdArEuHmnFaa2pm7XK8LVTuXVrNuuoPkpfw61Fs4ke3T0yKg x6G3gq7Xm1tTEROAgMtaxqwo1D5n1H0nkyDFggLB0K9Ws0frp7HENtSQwdNSry1A iD8TLxkhoWo7BS0VViLT1gKOfnw4YeMJP+CcOQ+DQjCsUMECQQD0Nc0vKBTlK6GT 28gcIMVoQy2KicjiH222A9/TLCNAQ9DEeZDYEptuTfrlbggfWdgQ3nc6CyvGf6c5 6uBPi/+5AkEA86oqqZPi7ekkUVHx0VSkp0mTlD1tAPhDE8cLX8vyImGExS+tTznz ROyzm3T1M1PisdQIU8Wd5rqvHP6dB0enjwJAauhKpMQ1MYYCPApQ9g9anCQcgbOD 34nGq5HSoE2IOQ/3Cqv1PsIWjRlSJrIemCrqrafWJfDR/xnPCUnLXMd68QJAPNwG 1d4zMvslcA5ImOFMUuBEtST2geSAVINFqwK0krPKxrmWzxAJW/DHF5AJ4m0UVRhB kDLusn90V4iczgGurwJAZUz6w01OeoLhsOuWNvkbTq+IV0NQ5GAEGA721Ck5zp86 bKkRJNJ2PpfWA45Vdq6u+izrn9e2TabKjWIfTfT/ZQ== -----END RSA PRIVATE KEY-----`; const rsaEncryptor = new JSEncrypt(); // RSA加密器 rsaEncryptor.setPublicKey(PUBLIC_KEY); const rsaDecryptor = new JSEncrypt(); // RSA解密器 rsaDecryptor.setPrivateKey(PRIVATE_KEY); const plaintextEle = document.querySelector("#plaintext"); const ciphertextEle = document.querySelector("#ciphertext"); const decryptedCiphertextEle = document.querySelector( "#decryptedCiphertext" ); function getRandomAESKey() { return ( Math.random().toString(36).substring(2, 10) + Math.random().toString(36).substring(2, 10) ); } // AES加密 function aesEncrypt(key, iv, content) { let text = CryptoJS.enc.Utf8.parse(JSON.stringify(content)); let encrypted = CryptoJS.AES.encrypt(text, key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7, }); return encrypted.toString(); } // AES解密 function aesDecrypt(key, iv, content) { let decrypt = CryptoJS.AES.decrypt(content, key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7, }); return decrypt.toString(CryptoJS.enc.Utf8); } function hybirdEncrypt(data) { const iv = getRandomAESKey(); const key = getRandomAESKey(); const encryptedData = aesEncrypt(key, iv, data); const encryptedIv = rsaEncryptor.encrypt(iv); const encryptedKey = rsaEncryptor.encrypt(key); return { iv: encryptedIv, key: encryptedKey, data: encryptedData, }; } function hybirdDecrypt(encryptedResult) { const iv = rsaDecryptor.decrypt(encryptedResult.iv); const key = rsaDecryptor.decrypt(encryptedResult.key); const data = encryptedResult.data; return aesDecrypt(key, iv, data); } function encrypt() { let plaintext = plaintextEle.value; const encryptedResult = hybirdEncrypt(plaintext); ciphertextEle.value = JSON.stringify(encryptedResult); } function decrypt() { let ciphertext = ciphertextEle.value; const encryptedResult = JSON.parse(ciphertext); decryptedCiphertextEle.value = hybirdDecrypt(encryptedResult).replace(/\"/g,''); } </script> </body> </html>
通过这个示例,相信大家对混合加密已经有了一定的了解。但在实际 Web 项目中,我们一般不会在客户端进行数据解密,而是会把数据提交到服务端,然后由服务端进行数据解密和数据处理。
HTTP 协议对大多数 Web 开发者来说,都不会陌生。HTTP 协议是基于请求和响应,具体如下图所示:
在对数据安全要求较高的场景或传输敏感数据时,我们就可以考虑利用前面的混合加密方案对提交到服务端的数据进行混合加密,当服务端接收到对应的加密数据时,再使用对应的解密算法对加密的数据进行解密,从而进一步进行数据处理。
但是如果服务端也要返回敏感数据时,应该怎么办呢?这里阿宝哥给大家介绍一种方案,该方案只需使用一对公私钥。当然该方案仅供大家参考,如果你有好的方案,欢迎给阿宝哥留言或跟阿宝哥交流哟。
下面我们来看一下该方案的具体操作流程:
① 生成一个唯一的 reqId(请求 ID),用于标识当前请求;
② 分别生成一个随机的 AES Key 和 AES IV(采用 AES CBC 模式);
③ 采用 RSA 非对称加密算法,分别对 AES Key 和 AES IV 进行 RSA 非对称加密;
④ 采用随机生成的 AES Key 和 AES IV 对敏感数据进行 AES 对称加密;
⑤ 把 reqId 作为 key,AES Key 和 AES IV 组成的对象作为 value 保存到 Map 或 {} 对象中;
⑥ 把 reqId、加密后的 AES Key、AES IV 和加密后的数据保存到对象中提交到服务端;
⑦ 当服务端接收到数据后,对接收的数据进行解密,然后使用客户端传过来的解密后的 AES Key 和 AES IV 对响应数据进行 AES 对称加密;
⑧ 服务端在完成数据加密后,把 reqId 和加密后的数据包装成响应对象,返回给客户端;
⑨ 当客户端成功接收服务端的响应后,先获取 reqId,进而从保存 AES Key 和 IV 的 Map 获取该 reqId 对应的 AES 加密信息;
⑩ 客户端使用当前 reqId 对应的加密信息,对服务端返回的数据进行解密,当完成解密之后,从 Map 或 {} 对象中删除已有记录。
现在我们来对上述流程做个简单分析,首先 AES 加密信息都是随机生成的且根据每个请求独立地保存到内存中,把 AES 加密信息中的 Key 和 IV 提交到服务端的时候都会使用 RSA 非对称加密算法进行加密。
在服务端返回数据的时候,会使用当前请求对应的 AES 加密信息对返回的结果进行加密,同时返回当前请求对应的 reqId(请求 ID)。即服务端不需要再生成新的 AES 加密信息,来对响应数据进行加密,这样就不需要在响应对象中传递 AES 加密信息。
该方案看似挺完美的,由于我们加密的信息还是存在内存中,如果使用开发者工具对 Web 应用进行调试时,那么还是可以看到每个请求对应的加密信息。那么这个问题该如何解决呢?能不能防止使用开发者工具对我们的 Web 应用进行调试,答案是有的。
不过这里阿宝哥就不继续展开了,后面可能会单独写一篇文章来介绍如何防止使用开发者工具调试 Web 应用,感兴趣的小伙伴可以给我留言哟。
其实在日常工作中,除了对称加密和非对称加密算法之外。还有一种用得比较广的消息摘要算法。消息摘要算法是密码学算法中非常重要的一个分支,它通过对所有数据提取指纹信息以实现数据签名、数据完整性校验等功能,由于其不可逆性,有时候会被用做敏感信息的加密。消息摘要算法也被称为哈希(Hash)算法或散列算法。
任何消息经过散列函数处理后,都会获得唯一的散列值,这一过程称为 “消息摘要”,其散列值称为 “数字指纹”,其算法自然就是 “消息摘要算法”了。 换句话说,如果其数字指纹一致,就说明其消息是一致的。
(图片来源 —— https://zh.wikipedia.org/wiki...)
消息摘要算法的主要特征是加密过程不需要密钥,并且经过加密的数据无法被解密,目前可以解密逆向的只有 CRC32 算法,只有输入相同的明文数据经过相同的消息摘要算法才能得到相同的密文。 消息摘要算法不存在密钥的管理与分发问题,适合于分布式网络上使用。消息摘要算法主要应用在 “数字签名” 领域,作为对明文的摘要算法。著名的摘要算法有 RSA 公司的 MD5 算法和 SHA-1 算法及其大量的变体。
消息摘要算法拥有以下特点:
MD5(Message Digest Algorithm 5,消息摘要算法版本5),它由 MD2、MD3、MD4 发展而来,由 Ron Rivest(RSA 公司)在 1992 年提出,目前被广泛应用于数据完整性校验、数据(消息)摘要、数据签名等。MD2、MD4、MD5 都产生 16 字节(128 位)的校验值,一般用 32 位十六进制数表示。MD2 的算法较慢但相对安全,MD4 速度很快,但安全性下降,MD5 比 MD4 更安全、速度更快。
随着计算机技术的发展和计算水平的不断提高,MD5 算法暴露出来的漏洞也越来越多。1996 年后被证实存在弱点,可以被加以破解,对于需要高度安全性的数据,专家一般建议改用其他算法,如 SHA-2。2004 年,证实 MD5 算法无法防止碰撞(collision),因此不适用于安全性认证,如 SSL 公开密钥认证或是数字签名等用途。
128 位的 MD5 散列在大多数情况下会被表示为 32 位十六进制数字。以下是一个 43 位长的仅 ASCII 字母列的MD5 散列:
MD5("The quick brown fox jumps over the lazy dog") = 9e107d9d372bb6826bd81d3542a419d6
即使在原文中作一个小变化(比如把 dog 改为 cog,只改变一个字符)其散列也会发生巨大的变化:
MD5("The quick brown fox jumps over the lazy cog") = 1055d3e698d289f2af8663725127bd4b
接着我们再来举几个 MD5 散列的例子:
MD5("") -> d41d8cd98f00b204e9800998ecf8427e MD5("semlinker") -> 688881f1c8aa6ffd3fcec471e0391e4d MD5("kakuqo") -> e18c3c4dd05aef020946e6afbf9e04ef
文件分发防篡改
在互联网上分发软件安装包时,出于安全性考虑,为了防止软件被篡改,比如在软件安装程序中添加木马程序。软件开发者通常会使用消息摘要算法,比如 MD5 算法产生一个与文件匹配的数字指纹,这样接收者在接收到文件后,就可以利用一些现成的工具来检查文件完整性。
消息传输防篡改
假设在网络上你需要发送电子文档给你的朋友,在文件发送前,先对文档的内容进行 MD5 运算,得出该电子文档的 “数字指纹”,并把该 “数字指纹” 随电子文档一同发送给对方。当对方接收到电子文档之后,也使用 MD5 算法对文档的内容进行哈希运算,在运算完成后也会得到一个对应 “数字指纹”,当该指纹与你所发送文档的 “数字指纹” 一致时,表示文档在传输过程中未被篡改。
哈希碰撞是指不同的输入却产生了相同的输出,好的哈希算法,应该没有人能从中找到 “碰撞” 或者说极度难找到,虽然 “碰撞” 是肯定存在的。
2005 年山东大学的王小云教授发布算法可以轻易构造 MD5 碰撞实例,此后 2007 年,有国外学者在王小云教授算法的基础上,提出了更进一步的 MD5 前缀碰撞构造算法 “chosen prefix collision”,此后还有专家陆续提供了MD5 碰撞构造的开源的库。
2009 年,中国科学院的谢涛和冯登国仅用了 220.96 的碰撞算法复杂度,破解了 MD5 的碰撞抵抗,该攻击在普通计算机上运行只需要数秒钟。
MD5 碰撞很容易构造,基于 MD5 来验证数据完整性已不可靠,考虑到近期谷歌已成功构造了 SHA-1(英语:Secure Hash Algorithm 1,中文名:安全散列算法1)的碰撞实例,对于数据完整性,应使用 SHA256 或更强的算法代替。
其实 MD5 的相关知识还有挺多,比如 MD5 密文反向查询、密码加盐和实现内容资源防盗链等。这里阿宝哥就不继续展开了,感兴趣的小伙伴可以阅读阿宝哥之前写的 ”一文读懂 MD5 算法“ 这篇文章。