加解密的艺术

一 对称加密

对称加密和非对称加密是两种不同的加密方法,它们在数据安全和信息传输中扮演着重要的角色。下面我将分别介绍这两种加密技术:

1.1 对称加密(Symmetric Encryption)

对称加密是指加密和解密都使用相同的密钥。这意味着发送方和接收方都必须拥有这个密钥才能进行加密和解密操作。

常见的对称加密算法:

  • AES(高级加密标准)

  • DES(数据加密标准)

  • 3DES(三重数据加密算法)

1.2 对称加密特点

一般来说,对称加密具有如下特点:

  1. 速度较快:由于加密和解密使用相同的密钥,对称加密通常比非对称加密要快。

  2. 密钥管理:对称加密的主要挑战在于密钥的分发和管理。如果密钥泄露,加密的安全性就会受到威胁。

  3. 适用于大量数据:由于速度快,对称加密适合加密大量数据。

从这里可以看到,对称加密主要有两大优势:第一就是运算速度快;第二就是适用于大量数据。

但是,对称加密有一个致命的问题,就是密钥管理。如何从服务端将密钥安全的传输到客户端是个问题!另外就是当一对多通信的时候,如何管理好密钥不被泄露也是一个考验。这是对称加密的不足之处。

1.3 代码案例

接下来松哥给大家演示下 Java 代码如何做对称加解密。

在 Java 中实现对称加密,通常使用 Java 加密架构(Java Cryptography Architecture, JCA)提供的类和接口。

下面是一个使用 AES(高级加密标准)算法进行对称加密和解密的简单示例:

public class SymmetricEncryptionExample {    // 生成密钥
    public static SecretKey generateKey() throws Exception {     
       KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
        keyGenerator.init(128); // 可以是128, 192或256位
        return keyGenerator.generateKey();
    }    
    // 加密方法
    public static String encrypt(String data, SecretKey key) throws Exception {      
      Cipher cipher = Cipher.getInstance("AES");
        cipher.init(Cipher.ENCRYPT_MODE, key);       
         byte[] encryptedBytes = cipher.doFinal(data.getBytes());        
         return Base64.getEncoder().encodeToString(encryptedBytes);
    }    
    // 解密方法
    public static String decrypt(String encryptedData, SecretKey key) throws Exception {        
    Cipher cipher = Cipher.getInstance("AES");
        cipher.init(Cipher.DECRYPT_MODE, key);       
         byte[] decryptedBytes = cipher.doFinal(Base64.getDecoder().decode(encryptedData));       
          return new String(decryptedBytes);
    }    
    public static void main(String[] args) {        
    try {           
             // 生成密钥
            SecretKey key = generateKey();           
             // 原始数据
            String originalData = "Hello, JavaBoy!";            
            // 加密
            String encryptedData = encrypt(originalData, key);
            System.out.println("加密数据: " + encryptedData);            
            // 解密
            String decryptedData = decrypt(encryptedData, key);
            System.out.println("解密数据: " + decryptedData);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
  1. 生成密钥:使用 KeyGenerator 生成一个AES密钥。

  2. 加密:使用 Cipher 类进行加密,将数据转换成字节后加密,并使用 Base64 编码转换为字符串,以便于存储或传输。

  3. 解密:将加密的字符串解码回字节,然后使用相同的密钥进行解密。

以上代码大家需要注意的是:

  • 密钥长度(如 128 位)应根据安全需求选择。

  • 确保密钥安全存储,不要在代码中硬编码密钥。

  • 对于生产环境,应考虑使用更安全的密钥管理策略。

出于安全考虑,我们一般使用上面的方案生成密钥。这种方案生成的密钥有一个特点就是系统每次重启就会变。如果你希望能够自己控制密钥的生成,那么可以通过如下方式生成密钥:

public static SecretKey generateKeyFromPassword(String password, int keySize) throws NoSuchAlgorithmException, InvalidKeySpecException {   
  SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");   
   KeySpec spec = new PBEKeySpec(password.toCharArray(), "salt".getBytes(), 65536, keySize);   
   return new SecretKeySpec(factory.generateSecret(spec).getEncoded(), "AES");
}

这样就可以通过自己传入的参数去控制密钥。

1.4 前后端搭配

在跨语言(如 JavaScript 和 Java)使用 AES 算法进行加密和解密时,关键是确保两端使用相同的密钥、算法模式(如 CBC, ECB 等)、填充模式(如 PKCS5Padding, PKCS7Padding 等)和初始化向量(IV,如果使用了需要 IV的 模式如 CBC)。

之前有小伙伴说自己前端加密之后后端总是无法解密,松哥这里也给一个前后端搭配的例子。

前端加密后端解密

前端代码:

<!DOCTYPE html><html lang="en"><head>
    <meta charset="UTF-8">
    <title>AES加密示例</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.0.0/crypto-js.min.js"></script>
    </head>
    <body>
    <script>
    function encryptAES(text, secretKey) {        
    const key = CryptoJS.enc.Utf8.parse(secretKey);        
    const iv = CryptoJS.lib.WordArray.random(128 / 8); 
    // 对于CBC模式,需要IV
    
        const encrypted = CryptoJS.AES.encrypt(text, key, {           
         iv: iv,           
          mode: CryptoJS.mode.CBC,            
          padding: CryptoJS.pad.Pkcs7
        });        
        // 返回加密后的文本和IV(Base64格式),实际使用中可能需要安全地传输这些值
        return {          
          ciphertext: encrypted.toString(),          
            iv: iv.toString(CryptoJS.enc.Base64)
        };
    }    
    const secretKey = 'helloworldhelloworldhelloworld11'; /
    / 确保密钥是32个字符长(256位)
    const text = 'Hello, javaboy!';    
    const result = encryptAES(text, secretKey);    
    console.log(result);
    </script>
    </body>
    </html>


后端代码:

import javax.crypto.Cipher;import javax.crypto.spec.IvParameterSpec;import javax.crypto.spec.SecretKeySpec;import java.util.Base64;public class AESDecrypt {    public static String decryptAES(String encryptedData, String secretKey, String iv) throws Exception {        IvParameterSpec ivParameterSpec = new IvParameterSpec(Base64.getDecoder().decode(iv));        SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey.getBytes("UTF-8"), "AES");        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec);        byte[] decodedValue = Base64.getDecoder().decode(encryptedData);        byte[] decryptedValue = cipher.doFinal(decodedValue);        return new String(decryptedValue, "UTF-8");
    }    public static void main(String[] args) {        try {            //前端加密后的文本
            String encryptedText = "PYANpAjMsRnBIEhovtEXQw==";            String secretKey = "helloworldhelloworldhelloworld11";            //前端 IV,要和加密后的文本一起传到后端
            String iv = "y/jUHcgSOpOiyNlsfjNUBg==";            String decryptedText = decryptAES(encryptedText, secretKey, iv);
            System.out.println("解密文本: " + decryptedText);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

注意这里前端加密时会产生一个 iv 参数,要随着前端加密结果一起传递给后端。

后端加密前端解密

在 Java 进行 AES 加密并在 JavaScript 中解密时,同样需要确保两端使用相同的密钥、算法模式(如 CBC、ECB 等)、填充模式(如 PKCS5Padding、PKCS7Padding 等)以及(如果适用)相同的初始化向量(IV)。

后端代码:

public class AESEncrypt {    public static String encryptAES(String plainText, String secretKey, String iv) throws Exception {        KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
        keyGenerator.init(256); // 对于AES-256
        SecretKey secret = new SecretKeySpec(secretKey.getBytes("UTF-8"), "AES");        IvParameterSpec ivParameterSpec = new IvParameterSpec(Base64.getDecoder().decode(iv));        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        cipher.init(Cipher.ENCRYPT_MODE, secret, ivParameterSpec);        byte[] encrypted = cipher.doFinal(plainText.getBytes("UTF-8"));        return Base64.getEncoder().encodeToString(encrypted);
    }    public static void main(String[] args) {        try {            String plainText = "Hello, 江南一点雨!";            String secretKey = "helloworldhelloworldhelloworld11"; // 确保密钥是32个字符长(256位)
            String iv = Base64.getEncoder().encodeToString(new byte[16]); // 示例IV,实际应用中应更安全地生成

            String encryptedText = encryptAES(plainText, secretKey, iv);
            System.out.println("Encrypted text: " + encryptedText);
            System.out.println("IV (Base64): " + iv); // 确保将IV发送给解密方

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

前端代码:

<!DOCTYPE html><html lang="en"><head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.0.0/crypto-js.min.js"></script>
    <script>
        function decryptAES(ciphertext, secretKey, iv) {            const key = CryptoJS.enc.Utf8.parse(secretKey);            const ivParsed = CryptoJS.enc.Base64.parse(iv);            const decrypted = CryptoJS.AES.decrypt(
                {                    ciphertext: CryptoJS.enc.Base64.parse(ciphertext)
                },
                key,
                {                    iv: ivParsed,                    mode: CryptoJS.mode.CBC,                    padding: CryptoJS.pad.Pkcs7
                }
            );            return decrypted.toString(CryptoJS.enc.Utf8);
        }        const secretKey = 'helloworldhelloworldhelloworld11';        const iv = 'AAAAAAAAAAAAAAAAAAAAAA=='; // 从Java代码获取
        const ciphertext = '1lPNVF1injas78KUWeKp5FusEr6f0pGgcrLAg9ELFr8='; // 从Java代码获取

        const decryptedText = decryptAES(ciphertext, secretKey, iv);        console.log(decryptedText);    </script></head><body></body></html>

以上前后端交互加解密代码松哥亲测都是没问题的,大家有这方面的需求记得及时收藏本文,可以作为参考。

二 非对称加密

2.1 什么是非对称加密

非对称加密,也称为公钥加密,是一种使用两个不同密钥(公钥和私钥)的加密方式。

一般来说,非对称加密有如下几种不同的特点:

  1. 公钥与私钥

  • 公钥与私钥的生成:非对称加密使用一对密钥,公钥是公开的,任何人都可以访问,而私钥是私有的,只有密钥的持有者可以访问。这两个密钥是由数学算法生成的,且相互关联但不可从一方推导出另一方。

  1. 安全性高

  • 难以破解:由于公钥和私钥的复杂数学关系,非对称加密的安全性较高。攻击者很难从公钥中推断出私钥,从而保证了加密数据的安全性。

  • 抗量子计算攻击:一些区块链项目开始采用抗量子计算攻击的加密算法,如椭圆曲线数字签名算法(ECDSA)的量子安全变体,以应对未来量子计算的威胁。

  1. 公开密钥分发方便

  • 公钥的公开性:公钥可以公开给任何人,因此分发起来非常方便。任何人都可以使用公钥来加密数据,但只有私钥持有者才能解密。

  1. 身份验证与数字签名

  • 身份验证:公钥可以用作用户的身份标识,其他人可以验证用户的身份而无需了解其私钥。这有助于在区块链等去中心化网络中建立可信身份。

  • 数字签名:私钥持有者可以使用私钥对消息进行签名,其他人则可以使用公钥来验证签名的真实性。这确保了消息的完整性和来源的可靠性。

  1. 安全通信

  • 加密通信:公钥可以用于加密消息,只有持有相应私钥的人才能解密。这确保了通信过程中的数据安全,防止了信息被未经授权的人员访问。

  1. 数字资产控制

  • 区块链地址与私钥:区块链地址通常由公钥派生而来,用户通过私钥来控制与该地址相关联的数字资产。私钥的安全性对数字资产的安全至关重要。

  1. 加密解密速度

  • 相对较慢:非对称加密的加密和解密速度相对于对称加密要慢得多,因为它需要进行更加复杂的数学计算。然而,这并不影响其在安全通信、身份验证等领域的应用。

2.2 非对称加密的用途

非对称加密有两个经典使用场景。

  1. 加密:这是我们最为熟知的用法,就是公钥加密,私钥解密。

  2. 签名:考虑到网络不可信,数据在传输过程中可能被篡改,这个时候公私钥可以反过来用,用私钥对数据进行签名,公钥进行验签,确保数据安全完整。

针对第二点用途,有的小伙伴会将之表述为用私钥进行加密,公钥进行解密,反正大伙知道说的是同一回事。

非对称加密算法,尽管在理论上能够用于数据加密和数字签名,但在实践中,其高计算复杂度和低效率成为了主要障碍。

相比对称加密算法,非对称加密的运算速度要慢上几个数量级,这极大地影响了其处理大数据量的能力。此外,由于非对称加密算法的加密和解密过程与密钥长度紧密相关,且不支持分组加密模式,导致它只能处理不超过密钥长度的少量数据,无法进行大量数据的加密。

为了克服非对称加密在性能上的不足,现代加密系统通常采用混合加密策略,即结合对称加密和非对称加密的优点。在这种策略中,非对称加密主要用于安全地传输一个对称加密的密钥(即“密钥协商”)给另一方。一旦双方安全地共享了这个对称密钥,就可以使用高效的对称加密算法来加密和解密大量数据。这种结合使用的方法不仅提高了加密效率,还增强了通信的安全性,被广泛应用于各种安全通信协议中,如SSL/TLS。

在数字签名领域,为了提升非对称加密的效率也做了一些适配。具体做法是,首先对原始数据进行摘要处理(可以利用 MD5、SHA 等),得到一个固定长度的摘要值。然后,使用非对称加密算法对这个摘要值进行加密,生成数字签名。由于摘要算法能够高效地将任意长度的数据压缩为固定长度的摘要,因此不管原始数据多大,摘要数据长度都一样,签名过程也能保持高效。当验证签名时,只需重新计算原始数据的摘要,并与解密后的签名进行比较,即可快速判断数据是否被篡改。

2.3 加密案例

Java 代码使用 RSA 加解密案例:

public class RsaDemo {    public static void main(String[] args) {        try {            KeyPair keyPair = generateRSAKeyPair();            PublicKey publicKey = keyPair.getPublic();            PrivateKey privateKey = keyPair.getPrivate();            String originalData = "Hello, 江南一点雨!";            String encryptedData = encrypt(publicKey, originalData);            String decryptedData = decrypt(privateKey, encryptedData);

            System.out.println("加密后的数据: " + encryptedData);
            System.out.println("解密后的数据: " + decryptedData);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }    public static String decrypt(PrivateKey privateKey, String encryptedData) throws Exception {        Cipher cipher = Cipher.getInstance("RSA");
        cipher.init(Cipher.DECRYPT_MODE, privateKey);        byte[] decryptedBytes = cipher.doFinal(Base64.getDecoder().decode(encryptedData));        return new String(decryptedBytes);
    }    public static String encrypt(PublicKey publicKey, String data) throws Exception {        Cipher cipher = Cipher.getInstance("RSA");
        cipher.init(Cipher.ENCRYPT_MODE, publicKey);        byte[] bytes = cipher.doFinal(data.getBytes());        return Base64.getEncoder().encodeToString(bytes);
    }    public static KeyPair generateRSAKeyPair() throws NoSuchAlgorithmException {        KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
        keyGen.initialize(2048); // 可以指定密钥长度,如2048位
        return keyGen.generateKeyPair();
    }
}

2.4 前后端搭配

前端加密后端解密

后端代码和上面案例中一致,不同的是,我们在拿到公钥之后,可以将公钥打印出来,这个公钥将来要传递给前端:

public static void main(String[] args) {    try {        KeyPair keyPair = generateRSAKeyPair();        PublicKey publicKey = keyPair.getPublic();        PrivateKey privateKey = keyPair.getPrivate();
        System.out.println("公钥: " + Base64.getEncoder().encodeToString(publicKey.getEncoded()));        String originalData = "Hello, 江南一点雨!";        String encryptedData = encrypt(publicKey, originalData);        String decryptedData = decrypt(privateKey, encryptedData);
        System.out.println("加密后的数据: " + encryptedData);
        System.out.println("解密后的数据: " + decryptedData);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

前端代码如下:

<!DOCTYPE html><html lang="en"><head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>RSA Encryption Example</title>
    <!-- 引入jsencrypt库 -->
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/bin/jsencrypt.min.js"></script></head><body><script>
    // 重写前端加密方法
    function encryptData(publicKey, data) {        // 创建一个新的JSEncrypt对象
        var encryptor = new JSEncrypt();        // 设置公钥
        encryptor.setPublicKey(publicKey);        // 加密数据
        var encrypted = encryptor.encrypt(data);        return encrypted;
    }    // 示例公钥(实际使用时应该替换为服务器提供的公钥)
    var publicKey = `MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnP5FOXIghidLDE2FgFwzi4Y+qTdYjnc0eMoiL5i4gdOeTE+7Uu9YvDB51GcBWKD9lvmZr5rX8z0OBpwe526qsTekNpXoIVk9DB34US0HOrAXEEwpUTVuSR656cJGAwmWBkVaalQynEz4Dlzrm53zExVYueruYUuzFyuZDaQcFnl3rWUH/XDkCIe23z0R1TfT3Q2OYNKft0u56r0S/ko99utXuYJK9yowe7QGT6q4cSJwsITQTomCARAq9q+bSNuGEYa4FlYCKIKWIhKbMhz0FYIMB2fJN10GyZbbvKASqeMkuCoD2Efgd8/6uMwOaMcx9LgEkcFaQ3qgDutsPXNUswIDAQAB`;    // 要加密的数据
    var data = 'Hello, javaboy!';    // 调用加密方法
    var encryptedData = encryptData(publicKey, data);    // 输出加密后的数据
    console.log('Encrypted Data:', encryptedData);</script></body></html>

后端加密前端解密

在 Java 中使用 RSA 算法加密数据,并在 JavaScript 中解密这些数据,意味着服务端用前端的公钥加密,前端用自己的私钥解密,这种场景前端私钥很容易被盗取,因此不推荐这种用法。我也就不举例了。

好啦,又和小伙伴们聊了一遍对称加密和非对称加密,上面的案例代码松哥都是测试通过的,小伙伴们可以作为参考。


评论区