本文采用的是 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
除非你是真的想泄密,不然前端应该不会用这种对称加密。
我暂用不到,以后再补充。