之前通过重新学习各种常见 web 密码的加密方案,对各种方案的加密原理和应用场景有了更深入的理解,接下来就是再学习下数据加密方案,数据加密方案常见的其实就是对称加密(AES)和非对称加密(RSA)。
AES
AES(Advanced Encryption Standard)高级加密标准,基于 Rijndael 算法,该算法为比利时密码学家 Joan Daemen 和 Vincent Rijmen 所设计,结合两位作者的名字,以 Rijndael 为名投稿高级加密标准的甄选流程,经过五年的甄选流程,在 2001 年由美国国家标准与技术研究院(NIST)发布,并在 2002 年成为有效的标准,目前已经成为最流行的对称加密算法之一。
对称加密算法是在加密和解密时使用相同的密钥。举个极简的例子,假如 A 端和 B 端使用对称加密的方案进行数据通讯,他们共同的密钥为数字 3,A 向 B 传输的数字在发送前都会乘以密钥数字 3,当 B 端接收到 A 端的数字后,除以密钥数字 3 就能得到真实的数字。
AES 的实现原理
AES 是一种区块加密标准,概括来说是将明文数据按每 128 位的大小切块,再用密钥将每个块的数据进行加密,密钥长度可以选择 128 位,192位,256位。因为 AES 算法基于 Rijndael 算法,并不完全等同,所以这两者在使用上是有区别的, Rijndael 算法允许明文数据可以按照 128位、192位、256位 来切块,支持的切分范围更广,但是当 Rijndael 算法 被选为 AES 时,NIST 限制了 AES 的参数范围,只允许明文按照 128 位切块。
AES 的加密过程是在一个 4×4 的字节矩阵上运作的,其初值就是一个明文区块,128 位对应的就是 16 字节,刚好构成一个 4×4 的明文区块。
AES 根据密钥长度来确定加密轮数,加密轮数是指,将当前计算出来的密文按照相同的计算方式带入下一轮进行计算,轮数就是控制循环的次数,密钥长度和加密轮数的关系为:
密钥长度 | 加密轮数 |
---|---|
128 | 10 |
192 | 12 |
256 | 14 |
AES 加密步骤
1、轮密钥加(AddRoundKey)
将回合密钥和数据矩阵中值做异或运算(XOR)得到新的数据矩阵,回合密钥是每轮循环都会由密码生成方案通过主密钥生成的一个子密钥,子密钥也是一个 4×4 的字节矩阵。
2、字节代换(SubBytes)
将数据矩阵中的每个字节与 S 盒(S-BOX)中的对应元素进行置换,S 盒是密码学中用于对输入数据进行非线性替代的基本组件,其主要目的是引入混淆(confusion),从而使得输出与输入之间的关系更加复杂,增强密码系统的抗分析能力。AES 的 S 盒作为标准的一部分是固定不变的,所有人用到的 S 盒是一样的。
3、行移位(ShiftRows)
数据矩阵的第一行保持列位置不变,其他每一行都向左循环移动特定的偏移量,第二行移动1个偏移量,第三行移动2个偏移量,第四行移动3个偏移量。假如源数据为以下所示:
4、列混合(MixColumns)
使用固定矩阵对每一列进行转换,替换得到新的列。
在每次加密轮次中重复上述 1-4 的步骤,只有在最后一次加密轮中省略第四步列混合,将每个块加密后的密文进行拼接,最后就得到了最终的密文数据。
分组密码工作模式
分组密码工作模式是指在使用分组密码(如 AES、DES)进行加密时,处理明文数据的方法。分组密码通常将数据分成固定长度的块来加密,而分组密码工作模式决定了如何处理这些块,以及如何将它们组合起来生成密文,工作模式有 ECB、CBC、OFB、CFB、GCM、CTR,其中最常用的是 CBC 模式。
- CBC 密码分组链接模式(Cipher Block Chaining Mode)
每个明文块在加密前会与前一个密文块进行异或(XOR)运算,首个块与初始化向量(IV)异或,初始化向量是随机化的。因为每个块的加密需要依赖上一个块,所有并行处理能力不强。
块数据补全
因为分组密码自身只能加密长度等于密码分组长度的单块数据,所以当明文块数据长度不够时,需要使用填充方式将明文块大小填充到指定长度,填充模式现在普遍使用的是 pkcs7 标准,pkcs7 填充方式为,先计算最后块需要补齐的字节数,然后每个字节填充的数据就是这个需要补齐的字节数。这里需要注意的是,pkcs7 要求即使块的字节数和块大小取余不为0,也要填充一个16字节的块,方便之后的块数据去除 pkcs7 填充字节。
代码实践
- php
php 想要实现 AES 加解密需要安装 OpenSSL 扩展
<?php
// 加密数据
function aesEncrypt($plaintext, $key, $iv) {
// Ensure the key is 32 bytes (256 bits) for AES-256
$key = substr(hash('sha256', $key, true), 0, 32);
// Encrypt the plaintext
$ciphertext = openssl_encrypt($plaintext, 'aes-256-cbc', $key, OPENSSL_RAW_DATA, $iv);
// Return the base64 encoded ciphertext
return base64_encode($ciphertext);
}
// 解密数据
function aesDecrypt($ciphertext, $key, $iv) {
// Ensure the key is 32 bytes (256 bits) for AES-256
$key = substr(hash('sha256', $key, true), 0, 32);
// Decode the base64 encoded ciphertext
$ciphertext = base64_decode($ciphertext);
// Decrypt the ciphertext
$plaintext = openssl_decrypt($ciphertext, 'aes-256-cbc', $key, OPENSSL_RAW_DATA, $iv);
return $plaintext;
}
// 明文
$plaintext = "This is a secret message.";
$key = "your-secret-key";
$iv = openssl_random_pseudo_bytes(16); // IV should be 16 bytes for AES-256-CBC
// 加密数据
$ciphertext = aesEncrypt($plaintext, $key, $iv);
echo "Ciphertext: " . $ciphertext . PHP_EOL;
// 解密信息
$decryptedText = aesDecrypt($ciphertext, $key, $iv);
echo "Decrypted text: " . $decryptedText . PHP_EOL;
?>
- go 代码实现
go 没有 php 那样方便的函数,需要自己实现块填充和块去除。
package encrypt
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/base64"
"encoding/hex"
"errors"
"io"
)
// AesCbcEncrypt AES CBC模式加密
func AesCbcEncrypt(content, secret string) string {
//记载密钥,并且转16进制,方便对比密钥字节长度
key, _ := hex.DecodeString(secret)
//明文字符串转字节切片
plaintext := []byte(content)
//采用 pkcs7 标准进行块数据填充
plaintext = pkcs7Pad(plaintext, aes.BlockSize)
block, err := aes.NewCipher(key)
if err != nil {
panic(err)
}
//CBC 模式的需要加向量值 IV,IV 的字节数和块的字节长度相等,将IV拼接在明文的前边
ciphertext := make([]byte, aes.BlockSize+len(plaintext))
iv := ciphertext[:aes.BlockSize]
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
panic(err)
}
mode := cipher.NewCBCEncrypter(block, iv)
mode.CryptBlocks(ciphertext[aes.BlockSize:], plaintext)
//转base64方便传输
result := base64.StdEncoding.EncodeToString(ciphertext)
return result
}
// pkcs7 填充标准
func pkcs7Pad(data []byte, blockSize int) []byte {
padding := blockSize - len(data)%blockSize
padText := make([]byte, len(data)+padding)
copy(padText, data)
for i := len(data); i < len(padText); i++ {
padText[i] = byte(padding)
}
return padText
}
// AesDecrypt AES CBC模式解密
func AesCbcDecrypt(content, secret string) []byte {
ciphertext, err := base64.StdEncoding.DecodeString(content)
if err != nil {
panic(err)
}
key, _ := hex.DecodeString(secret)
block, err := aes.NewCipher(key)
if err != nil {
panic(err)
}
if len(ciphertext) < aes.BlockSize {
panic("密文长度过短")
}
//取前16个字节作为 IV 值
iv := ciphertext[:aes.BlockSize]
cipherContent := ciphertext[aes.BlockSize:]
//验证块的完整性
if len(cipherContent)%aes.BlockSize != 0 {
panic("密文格式错误")
}
mode := cipher.NewCBCDecrypter(block, iv)
paddingText := make([]byte, len(cipherContent))
mode.CryptBlocks(paddingText, cipherContent)
result, err := pkcs7UnPad(paddingText, aes.BlockSize)
if err != nil {
panic(err)
}
return result
}
// pkcs7 块填充去除
func pkcs7UnPad(plainText []byte, blockSize int) ([]byte, error) {
length := len(plainText)
number := int(plainText[length-1])
if number >= length || number > blockSize {
return nil, errors.New("byte length is error")
}
return plainText[:length-number], nil
}