美文网首页
AES CBC模式加密解密实操(Rust、Python、Go、J

AES CBC模式加密解密实操(Rust、Python、Go、J

作者: thepoy | 来源:发表于2022-10-27 19:17 被阅读0次

    本文采用的是 CBC 模式,此模式的最大的特点之一即为流模式,最重要的就是每个加解密过程都使用不重复的、唯一的IV(初始化向量)。

    网上充斥着许多固定 IV 的错误文章,不可采信。

    0 密文的存储

    在文章最开头,我们讨论一下密文如何保存的问题。

    我说过了,IV必须随机生成,这也就意味着同一段明文生成的密文也将是随机的,每次都不相同。

    很多人将 KEY 和 IV 保存成一个或 IV 固定,我就很好奇,那你为什么用 CBC 模式?直接删除 IV,换成 ECB 模式不好吗?

    从下面代码的打印结果中你可以看到,密文和IV的长度分别是32、16,既然长度固定,那为什么不能将二者合并呢?

    于我是保存的思路便是:

    IV extend KEY
    

    生成一个新的数组或bytes,前16位是IV,后32位是密文,合并后保存到数据库的一个字段里,查询时按长度分割即可。

    1 Rust

    开发的程序使用的就是 Rust,故而最初的加解密也使用 Rust 完成,使用的库:

    依赖:

    rand_core = { version = "0.6", features = ["std"] }
    cbc = "0.1.2"
    aes = "0.8"
    

    代码:

    use aes::cipher::{block_padding::Pkcs7, BlockDecryptMut, BlockEncryptMut, KeyIvInit};
    use rand_core::{OsRng, RngCore};
    
    type Aes128CbcEnc = cbc::Encryptor<aes::Aes128>;
    type Aes128CbcDec = cbc::Decryptor<aes::Aes128>;
    
    const KEY: &[u8; 16] = b"abcdedghijklmnop"; // 模拟密钥,请勿在实际程序中使用
    
    /// 生成随机 iv
    fn generate_iv() -> [u8; 16] {
        let mut rng = OsRng;
        let mut bytes = [0u8; 16];
        rng.fill_bytes(&mut bytes);
    
        bytes
    }
    
    /// 加密
    pub fn encrypt(plain: &[u8]) -> (Vec<u8>, [u8; 16]) {
        // 随机值
        let iv = generate_iv();
    
        let mut buf = [0u8; 48];
        let pt_len = plain.len();
        buf[..pt_len].copy_from_slice(plain);
        let ct = Aes128CbcEnc::new(KEY.into(), &iv.into())
            .encrypt_padded_b2b_mut::<Pkcs7>(plain, &mut buf)
            .unwrap();
    
        (ct.to_vec(), iv)
    }
    
    /// 解密
    pub fn decrypt(cipher: &[u8], iv: [u8; 16]) -> Vec<u8> {
        let cipher_len = cipher.len();
        let mut buf = [0u8; 48];
        buf[..cipher_len].copy_from_slice(cipher);
    
        let pt = Aes128CbcDec::new(KEY.into(), &iv.into())
            .decrypt_padded_b2b_mut::<Pkcs7>(cipher, &mut buf)
            .unwrap();
    
        pt.to_vec()
    }
    
    fn main() {
        // 账号密码应为单向加密,参考:https://github.com/RustCrypto/password-hashes
        // 这里的示例代码应用来加密如手机号、身份证号、银行卡号等涉及用户隐私的数据
        let separator = "*".repeat(40);
    
        let plain = b"This is not a password";
        println!("明文:{:?}", plain);
        let (ct, iv) = encrypt(plain);
        println!(
            "{}\n密文:{:?}\n初始化向量:{:?}\n{}",
            separator, ct, iv, separator
        );
        let pt = decrypt(&ct, iv);
        println!("解密结果:{:?}", pt);
    
        assert_eq!(plain.to_vec(), pt);
    }
    

    打印结果:

    明文:[84, 104, 105, 115, 32, 105, 115, 32, 110, 111, 116, 32, 97, 32, 112, 97, 115, 115, 119, 111, 114, 100]
    ****************************************
    密文:[132, 69, 28, 205, 44, 250, 144, 23, 230, 230, 194, 204, 35, 167, 56, 224, 183, 193, 137, 108, 245, 203, 93, 164, 17, 188, 134, 59, 53, 192, 153, 130]
    初始化向量:[240, 219, 228, 209, 163, 125, 234, 213, 88, 177, 17, 98, 255, 222, 149, 78]
    ****************************************
    解密结果:[84, 104, 105, 115, 32, 105, 115, 32, 110, 111, 116, 32, 97, 32, 112, 97, 115, 115, 119, 111, 114, 100]
    

    tips: 为什么使用 bytes?

    密文终究是要保存到数据库中的,而且密文的形式或格式在数据库查询方面毫无区别, 毕竟都加密了,没有规律可言。

    我选择的数据库是 PostgreSQL,bytea 的空间占用也比 varchar 和 char 小,查询速度也比二者快,所以无需转换成 base64 或其他形式的字符串。

    2 Python

    使用 Python 再完成一遍完全是因为我用 Python 写了一个迁移脚本,原始数据是明文,迁移到数据库里需要加密处理一下。

    使用的库为:

    代码:

    import os
    
    from typing import List, Optional, Union
    from Crypto.Cipher import AES
    from Crypto.Util.Padding import pad, unpad
    
    KEY = b"abcdedghijklmnop"  # 模拟密钥,请勿在实际程序中使用
    """不涉及到文件io的时候,bytes 的默认编码就是 utf-8"""
    
    BytesData = Union[List[int], str, bytes]
    """自定义数据类型,iv、plain、cipher 都可以使用此类型"""
    
    
    class Cipher:
        @staticmethod
        def to_bytes(src: BytesData) -> bytes:
            """将一些其他类型的数据转换成 bytes"""
            if isinstance(src, list):
                return bytes(src)
    
            if isinstance(src, str):
                return src.encode()
    
            return src
    
        @staticmethod
        def generate_iv() -> bytes:
            """生成一个随机 iv(伪随机)"""
            return os.urandom(16)
    
        def encrypt(self, plain: str):
            """加密
    
            Parameters
            ----------
            plain : str
                待加密的明文
    
            Returns
            -------
            tuple(bytes, bytes)
                返回密文 bytes 和生成的 iv。
            """
            iv = self.generate_iv()
    
            cipher = AES.new(KEY, AES.MODE_CBC, iv)
    
            cipher_data = cipher.encrypt(pad(plain.encode(), AES.block_size))
    
            return cipher_data, iv
    
        def decrypt(self, cipher_data: BytesData, iv_data: BytesData):
            """解密
    
            Parameters
            ----------
            cipher_data : BytesData
                多个类型的密文。
            iv_data : BytesData
                iv 数据,如果你没有改代码,类型应该就是 bytes。
    
                当然你也可以传入 str、list[int]类型的数据。
    
            Returns
            -------
            bytes
                明文 bytes。
            """
            cipher = AES.new(KEY, AES.MODE_CBC, self.to_bytes(iv_data))
    
            plain_data = cipher.decrypt(self.to_bytes(cipher_data))
    
            return unpad(plain_data, AES.block_size)
    
        @staticmethod
        def to_byte_array(src: bytes):
            """将 bytes 转换成 list[int],与 rust 对比用的函数,你可以删掉"""
            return [i for i in src]
    
    
    if __name__ == "__main__":
        separator = "*" * 40
    
        c = Cipher()
    
        plain = "This is not a password"
        print("明文:", c.to_byte_array(plain.encode()))
    
        ct, iv = c.encrypt(plain)
    
        print(separator)
        print("密文:%s", c.to_byte_array(ct))
        print("初始化向量:%s", c.to_byte_array(iv))
        print(separator)
    
        pt = c.decrypt(ct, iv)
        print("解密结果:", c.to_byte_array(pt))
    

    打印结果:

    明文: [84, 104, 105, 115, 32, 105, 115, 32, 110, 111, 116, 32, 97, 32, 112, 97, 115, 115, 119, 111, 114, 100]
    ****************************************
    密文:[42, 240, 23, 146, 27, 103, 222, 252, 118, 70, 103, 147, 132, 168, 174, 145, 130, 65, 121, 220, 40, 173, 232, 34, 120, 212, 188, 179, 250, 38, 61, 37]
    初始化向量:[20, 37, 230, 37, 209, 158, 136, 197, 18, 4, 45, 22, 121, 227, 165, 210]
    ****************************************
    解密结果: [84, 104, 105, 115, 32, 105, 115, 32, 110, 111, 116, 32, 97, 32, 112, 97, 115, 115, 119, 111, 114, 100]
    

    3 Go

    我暂用不到,以后再补充。

    4 JS/TS

    除非你是真的想泄密,不然前端应该不会用这种对称加密。

    我暂用不到,以后再补充。

    相关文章

      网友评论

          本文标题:AES CBC模式加密解密实操(Rust、Python、Go、J

          本文链接:https://www.haomeiwen.com/subject/iduftdtx.html