Golang AES 加解密

本文介绍如何使用Go语言实现AES加密算法,包括CBC模式的加解密过程,并提供了一个示例代码,展示了如何使用自定义的填充和去除填充方法进行数据的加密和解密。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. 认识 AES

1.1 简介

Golang 实现 AES 加解密之前,先了解一下 AES 的基本知识。

在1997年,美国国家标准与技术研究院(NIST)发起了一个开放竞赛,旨在找到一个新的加密标准,以替代旧的数据加密标准(DES)。

这个新标准需要满足高安全性、效率、灵活性以及广泛的适用性等要求。期间,比利时密码学家 Joan Daemen 和 Vincent Rijmen 所设计,结合两位作者的名字,以 Rijndael 为名投稿新的加密标准竞赛。经过几年的评估和测试,NIST 最终选定了 Rijndael 算法 作为新的加密标准,并于 2001 年 11 月 26 日发布于 FIPS PUB 197,被正式命名为 AES(Advanced Encryption Standard,高级加密标准)。

自从 FIPS PUB 197 发布以来,AES 已经成为了全球最广泛使用的加密算法之一,它被用于无数应用中,从互联网安全(如SSL/TLS加密)到无线网络安全(如WPA2),以及许多其他需要保护敏感数据的场合。

1.2 区块与密钥长度

AES 是一种对称分组加密算法,区块长度固定为 16 字节(128bits)。

AES 秘钥的长度只能是16、24 或 32 字节,分别对应三种加密方式 AES-128、AES-192 和 AES-256。三者的区别是加密轮数不同。

AES分组长度(字节)密钥长度(字节)加密轮数
AES-128161610
AES-192162412
AES-256163214

1.3 加密模式

AES 支持 5 种加密模式。

AES 本身是一个块密码算法,它只能加密固定大小的数据块。然而,在实际应用中,我们通常需要加密更大的数据流或消息,并且需要解决一些特定的安全问题,如数据传输中的完整性、随机性和错误传播问题。这就是为什么需要加密模式的原因。

不同的加密模式采用不同策略来处理这些问题。以下是 AES 支持的 5 种加密模式。

  1. ECB(Electronic Codebook)模式: 最简单的模式,将明文分成固定大小的块,然后分别加密。这种模式的问题是相同明文块会加密成相同的密文块,因此缺乏随机性,降低安全性。

  2. CBC(Cipher Block Chaining)模式: 每个明文块与前一个密文块进行异或操作,然后加密。这增加了随机性,并且在错误传播方面具有良好性质,但需要一个初始化向量(IV),用于在加密第一个块之前与明文进行异或操作。

  3. CFB(Cipher Feedback)模式: 通过将前一个密文块作为输入与明文块进行异或操作,产生密文块。这种模式对于错误传播具有良好性质,但也需要一个初始化向量。

  4. OFB(Output Feedback)模式: 类似于 CFB,但是通过生成一个密钥流来与明文进行异或操作。对错误传播也具有良好性质。

  5. CTR(Counter)模式: 使用计数器来生成一个密钥流,然后与明文进行异或操作。在并行计算环境中效率较高。

2.数据填充

AES 是一种对称分组加密算法,区块长度固定为 128bits(16 字节)。

如果明文不是区块长度的整数倍,则需要填充。

如何填充呢?那么需要用到 PKCS。

2.1 什么是 PKCS?

PKCS(Public Key Cryptography Standards)是由 RSA 实验室发布的一系列公钥密码学标准,涵盖了加密、数字签名、证书管理等多个方面。

PKCS 系列包含多个标准,从 PKCS#1 到 PKCS#15,每个标准都定义了特定的密码学操作或格式。

PKCS#5 和 PKCS#7 是系列中的两个重要标准,它们主要涉及数据加密和填充方法。

2.2 PKCS#5

PKCS#5 表示 Password-Based Cryptography Standard,主要用于基于密码的加密。

定义了一种从密码派生密钥的方法(PBKDF1 和 PBKDF2)。

PBKDF1 和 PBKDF2 都是密钥派生函数(Key Derivation Functions),用于从密码或口令生成加密密钥。它们的主要目的是增加从密码生成密钥的复杂度,从而提高安全性,抵御暴力破解和字典攻击。这两个函数都是 PKCS#5(Password-Based Cryptography Standard)标准的一部分。

此外还包含一种填充方法,通常称为 PKCS#5 填充。

PKCS#5 填充方法:

  • 用于块密码,如 DES。
  • 可以填充 1-8 字节。
  • 填充的字节值等于填充的字节数。

2.3 PKCS#7

PKCS#7 表示 Cryptographic Message Syntax Standard,定义了更广泛的加密消息语法。

此外还包含一种更通用的填充方法,称为 PKCS#7 填充。

PKCS#7 填充方法:

  • PKCS#5 可以填充 1-8 字节,而 PKCS#7 可以填充 1-255 字节。
  • 填充的字节值同样等于填充的字节数。

PKCS#7 和 PKCS#5 填充的主要区别:

适用范围:

  • PKCS#5 填充主要用于 8 字节块大小的加密算法(如 DES)。
  • PKCS#7 填充可用不大于 255 字节块大小的加密算法(如 16 字节的 AES)。

实际应用:

  • 在许多现代应用中,PKCS#5 和 PKCS#7 填充,这两个术语经常被互换使用,特别是在块大小为 16 字节(如 AES)的情况下。
  • 实际上,许多库在实现 PKCS#5 填充时,使用的是 PKCS#7 的填充方法。
  • PKCS5 和 PKCS7 填充有一个重要细节,如果明文长度已经是块大小的整数倍,仍然会进行填充,填充的长度等于块大小。原因是为了解密时统一处理填充

3.CBC 加解密

在 Go 中,我们可以用官方提供的 crypto/aes 标准库来给我们进行 AES 加密,不过这个库并没有给我们指定加密模式,需要我们通过 crypto/cipher 来选择加密模式。

对二进制密文,我们可以选择使用 encoding/base64 将二进制密文编码为 Base64,当然也可以选择其他编码方式,如 encoding/hexencoding/base32

CBC 加密模式如下图所示,初始向量 IV 和明文异或,每个块的密文作为后续块的“向量”。

只要初始向量 IV 不同,那么每次加密结果也会不同。

在这里插入图片描述

下面实现 AES CBC 模式加解密。

加密关键步骤:

  • 实现 PKCS7 填充函数。
  • 调用 aes.NewCipher() 创建 AES 块加密实例并指定 key。
  • 填充明文至块大小(16字节)的整数倍。
  • 使用 key 作为初始化向量。
  • 调用 cipher.NewCBCEncrypter() 创建 AES-CBC 加密器,使用 AES 块加密实例和初始化向量。
  • 执行加密。

解密关键步骤:

  • 实现 PKCS7 去填充函数。
  • 调用 aes.NewCipher() 创建 AES 块加密实例并指定 key。
  • 使用 key 作为初始化向量。
  • 调用 cipher.NewCBCDecrypter() 创建 AES-CBC 解密器,使用 AES 块加密实例和初始化向量。
  • 执行解密。
  • 去除明文的填充。

如果想获取可打印的密文,可对二进制密文进行 Base64 编码。

package main

import (
    "fmt"
    "crypto/cipher"
    "crypto/aes"
    "bytes"
    "encoding/base64"
)

// PKCS7Padding fills plaintext as an integral multiple of the block length
func PKCS7Padding(p []byte, blockSize int) []byte {
	pad := blockSize - len(p)%blockSize
	padtext := bytes.Repeat([]byte{byte(pad)}, pad)
	return append(p, padtext...)
}

// PKCS7UnPadding removes padding data from the tail of plaintext
func PKCS7UnPadding(p []byte) []byte {
	length := len(p)
	paddLen := int(p[length-1])
	return p[:(length - paddLen)]
}


// AesCbcEncryptSame encrypts data with AES algorithm in CBC mode.
// Note that key length must be 16, 24 or 32 bytes to select AES-128, AES-192, or AES-256.
// This function using the iv from key has security risks so is not recommended to be used.
// If you want to generate the same ciphertext you can use this func.
func AesCbcEncryptSame(p, key []byte) ([]byte, error) {
	block, err := aes.NewCipher(key)
	if err != nil {
		return nil, err
	}

	p = PKCS7Padding(p, block.BlockSize())
	ciphertext := make([]byte, len(p))
	blockMode := cipher.NewCBCEncrypter(block, key[:block.BlockSize()])
	blockMode.CryptBlocks(ciphertext, p)
	return ciphertext, nil
}

// AesCbcDecryptSame decrypts cipher text with AES algorithm in CBC mode.
// Note that key length must be 16, 24 or 32 bytes to select AES-128, AES-192, or AES-256.
func AesCbcDecryptSame(c, key []byte) ([]byte, error) {
	block, err := aes.NewCipher(key)
	if err != nil {
		return nil, err
	}

	plaintext := make([]byte, len(c))
	blockMode := cipher.NewCBCDecrypter(block, key[:block.BlockSize()])
	blockMode.CryptBlocks(plaintext, c)

	return PKCS7UnPadding(plaintext)
}

// Base64AesCbcEncryptSame encrypts data with AES algorithm in CBC mode and then encode using base64.
func Base64AesCbcEncryptSame(p, key []byte) (string, error) {
	c, err := AesCbcEncryptSame(p, key)
	if err != nil {
		return "", err
	}
	return base64.StdEncoding.EncodeToString(c), nil
}

// Base64AesCbcDecryptSame decrypts cipher text encoded by base64 with AES algorithm in CBC mode.
func Base64AesCbcDecryptSame(c string, key []byte) ([]byte, error) {
	oriCipher, err := base64.StdEncoding.DecodeString(c)
	if err != nil {
		return nil, err
	}
	p, err := AesCbcDecryptSame(oriCipher, key)
	if err != nil {
		return nil, err
	}
	return p, nil
}

使用示例如下:

func main() {
   	p := []byte("plaintext")
	key := []byte("12345678abcdefgh")
	
	ciphertext, _ := Base64AesCbcEncryptSame(p, key)
	fmt.Println(ciphertext)

	plaintext, _ := Base64AesCbcDecryptSame(ciphertext, key)
	fmt.Println(string(plaintext))
}

运行输出:

A67NhD3RBiNaMgG6HTm8LQ==
plaintext

4.初始化向量

4.1 使用 key 作为初始化向量有什么问题?

不建议使用密钥的前 16 字节作为初始化向量(IV),这样做会严重降低加密系统的安全性。

使用固定IV(如密钥的一部分)会导致相同的明文总是生成相同的密文,这使得攻击者能够通过模式识别分析密文。

模式识别指观察密文中重复出现的特定结构或规律,例如:相同明文+相同IV+相同密钥 → 相同密文。

典型攻击场景示例:固定IV导致密文重复。

// 危险!固定IV的AES-CBC加密
iv = key[:16]  # 用密钥前16字节作为固定IV
cipher1 = encrypt("转账给A:100元", key, iv)  # 输出: 0x3a2b1c...
cipher2 = encrypt("转账给A:100元", key, iv)  # 输出: 0x3a2b1c... (与cipher1相同)

攻击者发现:

  • 相同交易总是产生相同密文。
  • 即使不知道具体金额,也能识别重复交易。

所以,对于CBC模式,应该遵守加密最佳实践:

  • NIST 等标准机构明确要求CBC模式 IV 必须随机且不可预测。

“For the CBC and CFB modes, the IV must be unpredictable; in particular, for any given plaintext, it must not be possible to predict the IV that will be associated to the plaintext in advance of the generation of the IV.”

参见 NIST SP 800-38A - Appendix C (Initialization Vectors)

  • 不应从密钥派生 IV。

4.2 核心安全要求

为了保证加密的安全性,IV 需要具备如下要求。

  • 不可预测性 (Unpredictability)

IV不能通过任何确定性方法(如密钥派生、计数器、固定值等)生成,必须使用密码学安全的随机数生成器(CSPRNG)。

  • 唯一性 (Uniqueness)

同一密钥下,每个加密操作的IV必须唯一,否则会导致明文信息泄露(如CBC模式的"IV重用攻击")。

  • 无需保密 (Non-secret)

IV 可以明文传输或存储,但必须确保其完整性和真实性(防止篡改)。

4.3 最佳实践

每次加密,随机生成不同的 IV。

// 加密时生成随机IV
iv := make([]byte, aes.BlockSize)
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
    return nil, err
}

// 将IV与密文一起存储/传输
ciphertext := make([]byte, len(iv)+len(encrypted))
copy(ciphertext[:len(iv)], iv)
copy(ciphertext[len(iv):], encrypted)

所以上面的加密稍微变更一下 IV 的生成方式,提高安全性。

// AesCbcEncrypt encrypts data with AES algorithm in CBC mode.
// Note that key length must be 16, 24 or 32 bytes to select AES-128, AES-192, or AES-256.
func AesCbcEncrypt(p, key []byte) ([]byte, error) {
	block, err := aes.NewCipher(key)
	if err != nil {
		return nil, err
	}

	p = PKCS7Padding(p, block.BlockSize())
	ciphertext := make([]byte, aes.BlockSize+len(p))
	iv := ciphertext[:aes.BlockSize]
	if _, err = rand.Read(iv); err != nil {
		return nil, err
	}

	blockMode := cipher.NewCBCEncrypter(block, iv)
	blockMode.CryptBlocks(ciphertext[aes.BlockSize:], p)
	return ciphertext, nil
}

// AesCbcDecrypt decrypts cipher text with AES algorithm in CBC mode.
// Note that key length must be 16, 24 or 32 bytes to select AES-128, AES-192, or AES-256.
func AesCbcDecrypt(ciphertext, key []byte) ([]byte, error) {
	block, err := aes.NewCipher(key)
	if err != nil {
		return nil, err
	}

	if len(ciphertext) < aes.BlockSize {
		return nil, errors.New("ciphertext too short")
	}

	iv := ciphertext[:aes.BlockSize]
	ciphertext = ciphertext[aes.BlockSize:]

	plaintext := make([]byte, len(ciphertext))
	blockMode := cipher.NewCBCDecrypter(block, iv)
	blockMode.CryptBlocks(plaintext, ciphertext)

	return PKCS7UnPadding(plaintext)
}

5.dablelv/cyan

以上代码已放置功能函数库 dablelv/cyan,可直 import 直接使用,欢迎大家 star 和 pr。

import (
	"github.com/dablelv/cyan/crypto"
)

p := []byte("plaintext")
key := []byte("12345678abcdefgh")

ciphertext, _ := crypto.Base64AesCbcEncrypt(p, key)				// 每次都会发生变化
plaintext, _ := crypto.Base64AesCbcDecrypt(ciphertext, key)		// plaintext
ciphertext, _ := crypto.Base64AesCbcEncryptSame(p, key)			// A67NhD3RBiNaMgG6HTm8LQ==
plaintext, _ := crypto.Base64AesCbcDecryptSame(ciphertext, key)	// plaintext

如果想了解 AES 实现原理,可参考 AES加密算法的详细介绍与实现


参考文献

PKCS - wikipedia
crypto/aes - Go Packages
AES加密算法的详细介绍与实现
Online AES Encryption / Decryption
NIST SP 800-38A - Appendix C (Initialization Vectors)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值