前言
要实现Go与PHP 8之间的AES加密协同工作,我们需要确保两端使用相同的加密模式、密钥长度、以及密钥和初始化向量(IV)。下面,我将提供一个详细的教程,说明如何在Go中使用crypto/aes
和crypto/cipher
库来实现AES加密,并在PHP 8中使用OpenSSL来解密这些数据(反之亦然)。
Go基于基础Crypto库实现AES封装加密
在Go语言中,使用标准库crypto/aes
和crypto/cipher
可以实现AES加密的封装。以下是一个使用AES-CBC模式进行加密和解密的简单示例。AES-CBC(Cipher Block Chaining)模式是一种常见的加密模式,它使用前一个密文块来加密下一个明文块,从而增强了加密的安全性。
首先,你需要安装Go环境。然后,你可以按照以下步骤创建一个AES加密和解密的封装:
-
生成AES密钥和IV(初始化向量):AES密钥的长度通常是16(AES-128)、24(AES-192)或32(AES-256)字节。IV也应该是与块大小相同(对于AES,块大小是16字节)。
-
加密过程:使用
cipher.NewCBCEncrypter
创建一个加密器,并使用它来加密数据。 -
解密过程:使用
cipher.NewCBCDecrypter
创建一个解密器,并使用它来解密数据。
以下是完整的示例代码:
package main
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/hex"
"fmt"
"io"
)
// Encrypt 使用AES CBC模式加密数据
func Encrypt(plaintext, key []byte) (ciphertext, iv []byte, err error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, nil, err
}
// 生成随机的IV
iv = make([]byte, aes.BlockSize)
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
return nil, nil, err
}
// 加密
ciphertext = make([]byte, aes.BlockSize+len(plaintext))
mode := cipher.NewCBCEncrypter(block, iv)
mode.CryptBlocks(ciphertext[aes.BlockSize:], plaintext)
// 返回IV和密文
return ciphertext, iv, nil
}
// Decrypt 使用AES CBC模式解密数据
func Decrypt(ciphertext, key, iv []byte) (plaintext []byte, err error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
if len(ciphertext) < aes.BlockSize {
return nil, fmt.Errorf("ciphertext too short")
}
if len(ciphertext)%aes.BlockSize != 0 {
return nil, fmt.Errorf("ciphertext is not a multiple of the block size")
}
// 解密
plaintext = make([]byte, len(ciphertext)-aes.BlockSize)
mode := cipher.NewCBCDecrypter(block, iv)
mode.CryptBlocks(plaintext, ciphertext[aes.BlockSize:])
// 去除填充(这里假设使用了PKCS#7填充)
plaintext = PKCS7Unpad(plaintext)
return plaintext, nil
}
// PKCS7Unpad removes the PKCS#7 padding from a slice of bytes.
func PKCS7Unpad(data []byte) []byte {
length := len(data)
unpadding := int(data[length-1])
return data[:(length - unpadding)]
}
func main() {
key := []byte("this is a key123") // 应使用更安全的密钥生成方法
plaintext := []byte("hello world")
ciphertext, iv, err := Encrypt(plaintext, key)
if err != nil {
panic(err)
}
fmt.Printf("IV: %s\nCiphertext: %s\n", hex.EncodeToString(iv), hex.EncodeToString(ciphertext))
decryptedText, err := Decrypt(ciphertext, key, iv)
if err != nil {
panic(err)
}
fmt.Println("Decrypted text:", string(decryptedText))
}
注意:
- 密钥
key
应该是一个安全的随机字节序列,上面的示例中仅用于演示。 - IV(初始化向量)每次加密时都应该是随机的,以确保加密的安全性。
- 在实际应用中,你应当确保密文、密钥和IV都安全地存储和传输。
- 解密函数中的
PKCS7Unpad
用于去除加密时可能添加的填充,以还原原始数据。这个例子假设使用了PKCS#7填充,这是AES加密中常用的填充方式。
其他事项
当然,我们可以继续讨论关于AES加密在Go语言中的实现,特别是关于如何更健壮地处理错误、如何管理密钥和IV的安全存储,以及如何在网络应用中安全地传输这些数据。
密钥和IV的管理
- 密钥生成:在实际应用中,你应该使用安全的随机数生成器来生成密钥。Go的
crypto/rand
包提供了这样的功能。 - 密钥存储:密钥应该安全地存储在硬件安全模块(HSM)或经过加密的数据库中,而不是硬编码在源代码中。
- IV的传输:IV不需要保密,但它必须与密文一起安全地传输到解密方,以便解密过程能够正确进行。
传输&模式和性能
-
加密数据的传输:当加密数据需要在网络上传输时,你应该使用TLS(传输层安全性协议)或类似的协议来保护数据的机密性和完整性。TLS会加密整个传输层的数据包,从而保护你的加密数据不被中间人攻击。
-
加密模式的选择:除了CBC模式外,AES还支持其他几种模式,如ECB(电子密码本模式)、CFB(密码反馈模式)、OFB(输出反馈模式)和CTR(计数器模式)等。每种模式都有其特定的用途和安全性考虑。CBC模式是最常用的模式之一,因为它结合了加密和完整性校验的功能(尽管不是强校验)。然而,对于某些应用场景,你可能需要选择其他模式。
-
填充和去填充:在上面的示例中,我们使用了PKCS#7填充来确保数据块的大小符合AES的要求。在解密时,我们需要去除这个填充。但是,如果密文在传输过程中被篡改,去填充过程可能会失败。因此,你应该在解密过程中检查并处理这种潜在的错误。
-
性能考虑:AES加密是一个计算密集型的操作,特别是在处理大量数据时。因此,在性能敏感的应用中,你可能需要考虑使用硬件加速(如AES-NI指令集)或优化你的加密和解密逻辑。
示例扩展
下面是一个扩展的示例,它包括了更详细的错误处理和日志记录(假设你有一个日志库):
package main
import (
// ... 导入必要的包
"log"
)
func main() {
// ... 生成密钥和IV
plaintext := []byte("sensitive data")
ciphertext, iv, err := Encrypt(plaintext, key)
if err != nil {
log.Fatalf("Failed to encrypt data: %v", err)
}
// ... 将ciphertext和iv安全地存储或传输
// 假设我们稍后需要解密
decryptedText, err := Decrypt(ciphertext, key, iv)
if err != nil {
log.Fatalf("Failed to decrypt data: %v", err)
}
log.Printf("Decrypted text: %s", decryptedText)
}
// ... Encrypt 和 Decrypt 函数的实现(与上面相同)
// LogError 是一个简单的错误日志记录函数(假设)
func LogError(err error) {
// 使用你的日志库来记录错误
log.Printf("Error: %v", err)
}
PHP端 OpenSSL 解密
在PHP端,我们使用OpenSSL函数来解密由Go生成的AES加密数据。
<?php
function decryptAES($ciphertext, $key) {
// Base64解码
$ciphertextDec = base64_decode($ciphertext);
// 解密
$decrypted = openssl_decrypt($ciphertextDec, 'AES-256-CBC', $key, OPENSSL_RAW_DATA, substr($ciphertextDec, 0, 16));
// 去除PKCS#7填充
$length = strlen($decrypted);
$padding = ord($decrypted[$length - 1]);
$decrypted = substr($decrypted, 0, -$padding);
return $decrypted;
}
$key = "your-32-bytes-long-secret-key-here";
$ciphertext = "..."; // 这里是从Go端得到的Base64加密字符串
$decrypted = decryptAES($ciphertext, $key);
echo "Decrypted text: " . $decrypted . PHP_EOL;
注意事项
-
密钥和IV:确保Go和PHP端使用的密钥和IV(特别是IV)完全相同。在上面的Go示例中,IV是随机生成的,但在实际应用中,如果需要在两端解密相同的数据,IV必须一致。如果只是想简单地演示加密和解密,可以将IV作为加密函数的一部分返回给PHP端。
-
错误处理:在生产环境中,应该增加更多的错误处理逻辑来确保加密和解密操作的健壮性。
-
填充:这里使用了PKCS#7填充(Go中的
PKCS7Padding
和PHP中的手动去除填充)。确保两端使用相同的填充机制。 -
Base64编码:加密后的数据通常是二进制的,不适合直接作为字符串传输或存储。Go示例中使用了Base64编码,PHP示例中相应地进行了Base64解码。
通过以上步骤,你应该能够在确保Go和PHP 8之间的AES加密协同工作无缝进行。我们将重点关注如何在需要时传递IV(初始化向量),因为IV在加密和解密过程中必须是相同的。
Go端:修改以返回IV
在Go的加密函数中,我们可以修改它以返回IV,这样PHP端就可以使用这个IV来解密数据。
package main
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/base64"
"fmt"
"io"
)
// ... (PKCS7Padding 和 PKCS7Unpadding 函数保持不变)
// EncryptAESWithIV 加密数据并返回IV
func EncryptAESWithIV(plaintext, key []byte) (string, []byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return "", nil, err
}
// 生成IV
iv := make([]byte, aes.BlockSize)
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
return "", nil, err
}
// 加密
mode := cipher.NewCBCEncrypter(block, iv)
ciphertext := make([]byte, aes.BlockSize+len(plaintext))
mode.CryptBlocks(ciphertext[aes.BlockSize:], PKCS7Padding(plaintext, aes.BlockSize))
copy(ciphertext[:aes.BlockSize], iv)
// 转换为Base64
ciphertextBase64 := base64.StdEncoding.EncodeToString(ciphertext)
return ciphertextBase64, iv, nil
}
func main() {
key := []byte("your-32-bytes-long-secret-key-here")
plaintext := []byte("Hello, world!")
encrypted, iv, err := EncryptAESWithIV(plaintext, key)
if err != nil {
panic(err)
}
fmt.Println("Encrypted (Base64):", encrypted)
fmt.Println("IV (Base64):", base64.StdEncoding.EncodeToString(iv)) // 可选:将IV也转换为Base64方便查看或传输
// 在实际应用中,你需要将encrypted和iv(可能是Base64编码的)发送给PHP端
}
PHP端:使用提供的IV进行解密
在PHP端,你现在将接收到加密后的数据和IV(可能是Base64编码的),你需要先解码IV,然后使用它来解密数据。
<?php
function decryptAESWithIV($ciphertext, $key, $ivBase64) {
// Base64解码IV
$iv = base64_decode($ivBase64);
// Base64解码加密数据
$ciphertextDec = base64_decode($ciphertext);
// 解密
$decrypted = openssl_decrypt($ciphertextDec, 'AES-256-CBC', $key, OPENSSL_RAW_DATA, $iv);
// 去除PKCS#7填充
$length = strlen($decrypted);
$padding = ord($decrypted[$length - 1]);
$decrypted = substr($decrypted, 0, -$padding);
return $decrypted;
}
$key = "your-32-bytes-long-secret-key-here";
$ciphertext = "..."; // 从Go端接收的Base64加密字符串
$ivBase64 = "..."; // 从Go端接收的Base64编码的IV
$decrypted = decryptAESWithIV($ciphertext, $key, $ivBase64);
echo "Decrypted text: " . $decrypted . PHP_EOL;
注意事项
- IV的传输:IV不需要保密,但它必须与加密时使用的IV完全相同。因此,你可以安全地将IV与加密数据一起发送给解密方。
- 安全性:确保你的密钥(key)是安全的,并且只在你信任的系统之间共享。
- 错误处理:在PHP端,你可能还想添加一些错误处理逻辑来检查
openssl_decrypt
是否成功执行。 - 测试:在生产环境中部署之前,请确保在多种情况下测试你的加密和解密逻辑,以确保其可靠性和安全性。
总结
我们在日常开发中,会经常遇到和其他平台数据做对接,接口与接口对接过程中,除了必要签名授权外,我们还需要对敏感数据进行加密,使用的比较多就是AES对称加密,但是每个系统之间的开发语言并非都是一致的,不同的业务系统使用的编程语言都是各异,所以在加解密的过程中,我们要协调双方的加密方式,密钥和IV以及填充方式等,以上就用Go和PHP做对比,使用AES CBC模式进行加密处理,确保双方的系统交互没有信息泄露的问题,当然Java、Python、C等其他语言,都是可以,这里就不一一举例了(重点我对这些语言也不是很熟,哈哈),但是在实际应用中,你可能还需要考虑其他安全因素,如使用HTTPS来保护数据传输过程中的安全等。好了,本篇内容到此结束。