之前通過重新學(xué)習(xí)各種常見 web 密碼的加密方案丑婿,對各種方案的加密原理和應(yīng)用場景有了更深入的理解妆够,接下來就是再學(xué)習(xí)下數(shù)據(jù)加密方案商玫,數(shù)據(jù)加密方案常見的其實(shí)就是對稱加密(AES)和非對稱加密(RSA)狂打。
AES
AES(Advanced Encryption Standard)高級加密標(biāo)準(zhǔn),基于 Rijndael 算法诬辈,該算法為比利時(shí)密碼學(xué)家 Joan Daemen 和 Vincent Rijmen 所設(shè)計(jì)酵使,結(jié)合兩位作者的名字,以 Rijndael 為名投稿高級加密標(biāo)準(zhǔn)的甄選流程焙糟,經(jīng)過五年的甄選流程口渔,在 2001 年由美國國家標(biāo)準(zhǔn)與技術(shù)研究院(NIST)發(fā)布,并在 2002 年成為有效的標(biāo)準(zhǔn)穿撮,目前已經(jīng)成為最流行的對稱加密算法之一缺脉。
對稱加密算法是在加密和解密時(shí)使用相同的密鑰。舉個(gè)極簡的例子悦穿,假如 A 端和 B 端使用對稱加密的方案進(jìn)行數(shù)據(jù)通訊攻礼,他們共同的密鑰為數(shù)字 3,A 向 B 傳輸?shù)臄?shù)字在發(fā)送前都會乘以密鑰數(shù)字 3栗柒,當(dāng) B 端接收到 A 端的數(shù)字后礁扮,除以密鑰數(shù)字 3 就能得到真實(shí)的數(shù)字。
AES 的實(shí)現(xiàn)原理
AES 是一種區(qū)塊加密標(biāo)準(zhǔn)瞬沦,概括來說是將明文數(shù)據(jù)按每 128 位的大小切塊太伊,再用密鑰將每個(gè)塊的數(shù)據(jù)進(jìn)行加密,密鑰長度可以選擇 128 位逛钻,192位僚焦,256位。因?yàn)?AES 算法基于 Rijndael 算法曙痘,并不完全等同叠赐,所以這兩者在使用上是有區(qū)別的, Rijndael 算法允許明文數(shù)據(jù)可以按照 128位屡江、192位、256位 來切塊赛不,支持的切分范圍更廣惩嘉,但是當(dāng) Rijndael 算法 被選為 AES 時(shí),NIST 限制了 AES 的參數(shù)范圍踢故,只允許明文按照 128 位切塊文黎。
AES 的加密過程是在一個(gè) 4×4 的字節(jié)矩陣上運(yùn)作的,其初值就是一個(gè)明文區(qū)塊殿较,128 位對應(yīng)的就是 16 字節(jié)耸峭,剛好構(gòu)成一個(gè) 4×4 的明文區(qū)塊。
AES 根據(jù)密鑰長度來確定加密輪數(shù)淋纲,加密輪數(shù)是指劳闹,將當(dāng)前計(jì)算出來的密文按照相同的計(jì)算方式帶入下一輪進(jìn)行計(jì)算,輪數(shù)就是控制循環(huán)的次數(shù),密鑰長度和加密輪數(shù)的關(guān)系為:
密鑰長度 | 加密輪數(shù) |
---|---|
128 | 10 |
192 | 12 |
256 | 14 |
AES 加密步驟
1本涕、輪密鑰加(AddRoundKey)
將回合密鑰和數(shù)據(jù)矩陣中值做異或運(yùn)算(XOR)得到新的數(shù)據(jù)矩陣贿条,回合密鑰是每輪循環(huán)都會由密碼生成方案通過主密鑰生成的一個(gè)子密鑰窖铡,子密鑰也是一個(gè) 4×4 的字節(jié)矩陣。
2、字節(jié)代換(SubBytes)
將數(shù)據(jù)矩陣中的每個(gè)字節(jié)與 S 盒(S-BOX)中的對應(yīng)元素進(jìn)行置換人断,S 盒是密碼學(xué)中用于對輸入數(shù)據(jù)進(jìn)行非線性替代的基本組件,其主要目的是引入混淆(confusion)痢站,從而使得輸出與輸入之間的關(guān)系更加復(fù)雜吻贿,增強(qiáng)密碼系統(tǒng)的抗分析能力。AES 的 S 盒作為標(biāo)準(zhǔn)的一部分是固定不變的呻右,所有人用到的 S 盒是一樣的跪妥。
3、行移位(ShiftRows)
數(shù)據(jù)矩陣的第一行保持列位置不變窿冯,其他每一行都向左循環(huán)移動特定的偏移量骗奖,第二行移動1個(gè)偏移量,第三行移動2個(gè)偏移量醒串,第四行移動3個(gè)偏移量执桌。假如源數(shù)據(jù)為以下所示:
4、列混合(MixColumns)
使用固定矩陣對每一列進(jìn)行轉(zhuǎn)換芜赌,替換得到新的列仰挣。
在每次加密輪次中重復(fù)上述 1-4 的步驟,只有在最后一次加密輪中省略第四步列混合缠沈,將每個(gè)塊加密后的密文進(jìn)行拼接膘壶,最后就得到了最終的密文數(shù)據(jù)。
分組密碼工作模式
分組密碼工作模式是指在使用分組密碼(如 AES洲愤、DES)進(jìn)行加密時(shí)颓芭,處理明文數(shù)據(jù)的方法。分組密碼通常將數(shù)據(jù)分成固定長度的塊來加密柬赐,而分組密碼工作模式?jīng)Q定了如何處理這些塊亡问,以及如何將它們組合起來生成密文,工作模式有 ECB肛宋、CBC州藕、OFB、CFB酝陈、GCM床玻、CTR,其中最常用的是 CBC 模式沉帮。
- CBC 密碼分組鏈接模式(Cipher Block Chaining Mode)
每個(gè)明文塊在加密前會與前一個(gè)密文塊進(jìn)行異或(XOR)運(yùn)算锈死,首個(gè)塊與初始化向量(IV)異或贫堰,初始化向量是隨機(jī)化的。因?yàn)槊總€(gè)塊的加密需要依賴上一個(gè)塊馅精,所有并行處理能力不強(qiáng)严嗜。
塊數(shù)據(jù)補(bǔ)全
因?yàn)榉纸M密碼自身只能加密長度等于密碼分組長度的單塊數(shù)據(jù),所以當(dāng)明文塊數(shù)據(jù)長度不夠時(shí)洲敢,需要使用填充方式將明文塊大小填充到指定長度漫玄,填充模式現(xiàn)在普遍使用的是 pkcs7 標(biāo)準(zhǔn),pkcs7 填充方式為压彭,先計(jì)算最后塊需要補(bǔ)齊的字節(jié)數(shù)睦优,然后每個(gè)字節(jié)填充的數(shù)據(jù)就是這個(gè)需要補(bǔ)齊的字節(jié)數(shù)。這里需要注意的是壮不,pkcs7 要求即使塊的字節(jié)數(shù)和塊大小取余不為0汗盘,也要填充一個(gè)16字節(jié)的塊,方便之后的塊數(shù)據(jù)去除 pkcs7 填充字節(jié)询一。
代碼實(shí)踐
- php
php 想要實(shí)現(xiàn) AES 加解密需要安裝 OpenSSL 擴(kuò)展
<?php
// 加密數(shù)據(jù)
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);
}
// 解密數(shù)據(jù)
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
// 加密數(shù)據(jù)
$ciphertext = aesEncrypt($plaintext, $key, $iv);
echo "Ciphertext: " . $ciphertext . PHP_EOL;
// 解密信息
$decryptedText = aesDecrypt($ciphertext, $key, $iv);
echo "Decrypted text: " . $decryptedText . PHP_EOL;
?>
- go 代碼實(shí)現(xiàn)
go 沒有 php 那樣方便的函數(shù)隐孽,需要自己實(shí)現(xiàn)塊填充和塊去除。
package encrypt
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/base64"
"encoding/hex"
"errors"
"io"
)
// AesCbcEncrypt AES CBC模式加密
func AesCbcEncrypt(content, secret string) string {
//記載密鑰健蕊,并且轉(zhuǎn)16進(jìn)制菱阵,方便對比密鑰字節(jié)長度
key, _ := hex.DecodeString(secret)
//明文字符串轉(zhuǎn)字節(jié)切片
plaintext := []byte(content)
//采用 pkcs7 標(biāo)準(zhǔn)進(jìn)行塊數(shù)據(jù)填充
plaintext = pkcs7Pad(plaintext, aes.BlockSize)
block, err := aes.NewCipher(key)
if err != nil {
panic(err)
}
//CBC 模式的需要加向量值 IV,IV 的字節(jié)數(shù)和塊的字節(jié)長度相等缩功,將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)
//轉(zhuǎn)base64方便傳輸
result := base64.StdEncoding.EncodeToString(ciphertext)
return result
}
// pkcs7 填充標(biāo)準(zhǔn)
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個(gè)字節(jié)作為 IV 值
iv := ciphertext[:aes.BlockSize]
cipherContent := ciphertext[aes.BlockSize:]
//驗(yàn)證塊的完整性
if len(cipherContent)%aes.BlockSize != 0 {
panic("密文格式錯(cuò)誤")
}
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
}