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

本文采用的是 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

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

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容