完善不同加密方法的使用

This commit is contained in:
yun
2026-09-30 15:49:12 +08:00
parent 034315ddbd
commit 7db722f8b9
69 changed files with 21362 additions and 2563 deletions
+124 -27
View File
@@ -1,52 +1,149 @@
// Package base64x 提供 Base64 编解码的统一 []byte API。
//
// 本包是 encoding/base64 的薄封装,统一了输入输出类型(一律使用 []byte)
// 与错误行为(解码失败一律返回 nil 切片与包装后的 error,绝不返回部分结果),
// 并提供友好、可读的错误信息。
//
// 四个变体的适用场景:
//
// - Encode / Decode:RFC 4648 标准字母表(+/)且带 = 填充。
// 适合 MIME、邮件、PEM 等传统场景;字符 + 与 / 不能直接放进 URL 查询串
// 或文件名(会被转义或截断)。
// - EncodeURL / DecodeURL:URL 与文件名安全字母表(-_)且带 = 填充。
// 适合 Cookie、HTTP 头、文件名,但 = 在某些上下文(如 JWT、部分 URL 解析器)
// 仍需转义。
// - EncodeRaw / DecodeRaw:标准字母表、**无填充**。
// 适合放进 JSON、YAML、定长字段(长度必须是 4 的倍数约束被去掉)。
// - EncodeRawURL / DecodeRawURL:URL 安全字母表、**无填充**。
// 适合 URL 路径/查询串、文件名与 JSON(最通用的可打印编码);
// 本包推荐在不确定场景时优先使用它。
//
// 安全注意:Base64 是**编码**而非加密,不提供任何机密性,
// 任何人都能还原原文;敏感数据必须先加密再编码。
// 解码不可信输入时应注意结果长度可能约为输入的 3/4,
// 需要限制输入大小以防内存放大类拒绝服务。
package base64x
import "encoding/base64"
import (
"encoding/base64"
"fmt"
)
// 普通的
func StdEncode(s string) string {
return base64.StdEncoding.EncodeToString([]byte(s))
// Encode 使用标准 Base64 字母表(+/)编码数据,输出带 = 填充。
//
// 参数:
// - data: 待编码的字节串,可以为 nil(返回空字符串)。
//
// 返回值:标准 Base64 字符串,长度总是 4 的倍数。
//
// 安全注意:Base64 不提供机密性;且标准字母表含 + 与 /,
// 不要直接用于 URL 或文件名,请使用 EncodeURL 或 EncodeRawURL。
func Encode(data []byte) string {
return base64.StdEncoding.EncodeToString(data)
}
func StdDecode(sEnc string) (string, error) {
sDec, err := base64.StdEncoding.DecodeString(sEnc)
// Decode 解码标准 Base64 字符串(要求带正确的 = 填充)。
//
// 参数:
// - s: 待解码的标准 Base64 字符串。
//
// 返回值:解码后的字节串;失败时返回 nil 与包装后的 error。
//
// 安全注意:失败时不会返回任何部分解码结果,避免调用方误用不完整数据;
// 解码结果可能很长,处理不可信输入前应限制输入长度。
func Decode(s string) ([]byte, error) {
decoded, err := base64.StdEncoding.DecodeString(s)
if err != nil {
return "", err
return nil, fmt.Errorf("标准 Base64 解码失败: %w", err)
}
return string(sDec), nil
return decoded, nil
}
// URL和文件名安全的
func UrlEncode(s string) string {
return base64.URLEncoding.EncodeToString([]byte(s))
// EncodeURL 使用 URL/文件名安全字母表(-_)编码数据,输出带 = 填充。
//
// 参数:
// - data: 待编码的字节串,可以为 nil(返回空字符串)。
//
// 返回值:URL 安全 Base64 字符串,长度总是 4 的倍数。
//
// 安全注意:字母表不含 + 与 /,可安全用于 URL 与文件名;
// 但 = 填充在某些上下文仍需转义,若需彻底避免请使用 EncodeRawURL。
func EncodeURL(data []byte) string {
return base64.URLEncoding.EncodeToString(data)
}
func UrlDecode(sEnc string) (string, error) {
sDec, err := base64.URLEncoding.DecodeString(sEnc)
// DecodeURL 解码 URL 安全 Base64 字符串(要求带正确的 = 填充)。
//
// 参数:
// - s: 待解码的 URL 安全 Base64 字符串。
//
// 返回值:解码后的字节串;失败时返回 nil 与包装后的 error。
//
// 安全注意:失败时不会返回部分结果;本函数不接受标准字母表中的 + 与 /。
func DecodeURL(s string) ([]byte, error) {
decoded, err := base64.URLEncoding.DecodeString(s)
if err != nil {
return "", err
return nil, fmt.Errorf("URL 安全 Base64 解码失败: %w", err)
}
return string(sDec), nil
return decoded, nil
}
// 无填充
func RawEncode(s string) string {
return base64.RawStdEncoding.EncodeToString([]byte(s))
// EncodeRaw 使用标准 Base64 字母表(+/)编码数据,输出**不带** = 填充。
//
// 参数:
// - data: 待编码的字节串,可以为 nil(返回空字符串)。
//
// 返回值:无填充的标准 Base64 字符串,长度不再保证是 4 的倍数。
//
// 安全注意:无填充版本更适合放进 JSON、定长字段;但字母表仍含 + 与 /,
// 不要直接用于 URL 或文件名。
func EncodeRaw(data []byte) string {
return base64.RawStdEncoding.EncodeToString(data)
}
func RawDecode(sEnc string) (string, error) {
sDec, err := base64.RawStdEncoding.DecodeString(sEnc)
// DecodeRaw 解码无填充的标准 Base64 字符串。
//
// 参数:
// - s: 待解码的无填充标准 Base64 字符串。
//
// 返回值:解码后的字节串;失败时返回 nil 与包装后的 error。
//
// 安全注意:失败时不会返回部分结果;带 = 填充的输入会被拒绝。
func DecodeRaw(s string) ([]byte, error) {
decoded, err := base64.RawStdEncoding.DecodeString(s)
if err != nil {
return "", err
return nil, fmt.Errorf("无填充标准 Base64 解码失败: %w", err)
}
return string(sDec), nil
return decoded, nil
}
// URL和文件名安全的,无填充
func RawUrlEncode(s string) string {
return base64.RawURLEncoding.EncodeToString([]byte(s))
// EncodeRawURL 使用 URL/文件名安全字母表(-_)编码数据,输出**不带** = 填充。
//
// 参数:
// - data: 待编码的字节串,可以为 nil(返回空字符串)。
//
// 返回值:URL 安全且无填充的 Base64 字符串,可直接放进 URL 路径、
// 查询串、文件名与 JSON 字符串。
//
// 安全注意:这是本包适用面最广的编码,但仍然只是编码:
// 若数据是秘密,必须先加密(例如 AES-GCM)后再编码。
func EncodeRawURL(data []byte) string {
return base64.RawURLEncoding.EncodeToString(data)
}
func RawUrlDecode(s string) (string, error) {
// DecodeRawURL 解码 URL 安全且无填充的 Base64 字符串。
//
// 参数:
// - s: 待解码的 URL 安全无填充 Base64 字符串。
//
// 返回值:解码后的字节串;失败时返回 nil 与包装后的 error。
//
// 安全注意:失败时不会返回部分结果;输入中的 = 填充、
// +、/ 以及空白字符都会导致解码失败。
func DecodeRawURL(s string) ([]byte, error) {
decoded, err := base64.RawURLEncoding.DecodeString(s)
return string(decoded), err
if err != nil {
return nil, fmt.Errorf("URL 安全无填充 Base64 解码失败: %w", err)
}
return decoded, nil
}
+345 -25
View File
@@ -1,43 +1,363 @@
package base64x_test
import (
"bytes"
"encoding/base64"
"strings"
"testing"
"code.yun.ink/pkg/encryptx/base64x"
)
func TestBase64xStd(t *testing.T) {
original := "Hello, World!"
encoded := base64x.StdEncode(original)
decoded, err := base64x.StdDecode(encoded)
if err != nil {
t.Error(err)
}
if original != decoded {
t.Errorf("expected %q, got %q", original, decoded)
// variants 汇总四个编解码变体,便于表驱动测试。
var variants = []struct {
name string
encode func([]byte) string
decode func(string) ([]byte, error)
// alphabetURL 表示该变体使用 URL 安全字母表(不含 + 与 /)。
alphabetURL bool
// padded 表示该变体输出带 = 填充。
padded bool
}{
{"标准", base64x.Encode, base64x.Decode, false, true},
{"URL安全", base64x.EncodeURL, base64x.DecodeURL, true, true},
{"标准无填充", base64x.EncodeRaw, base64x.DecodeRaw, false, false},
{"URL安全无填充", base64x.EncodeRawURL, base64x.DecodeRawURL, true, false},
}
// payloads 覆盖空、短、UTF-8 与二进制等各类输入。
var payloads = [][]byte{
nil,
{},
[]byte("a"),
[]byte("ab"),
[]byte("abc"),
[]byte("Hello, World!"),
[]byte("中文与 emoji 🚀 混合内容"),
bytes.Repeat([]byte{0x00}, 64),
bytes.Repeat([]byte{0xff, 0x00, 0x7f, 0x80}, 128),
func() []byte {
b := make([]byte, 256)
for i := range b {
b[i] = byte(i)
}
return b
}(),
}
// TestRoundTrip 校验四个变体各自的编解码往返一致。
func TestRoundTrip(t *testing.T) {
for _, v := range variants {
for i, payload := range payloads {
encoded := v.encode(payload)
decoded, err := v.decode(encoded)
if err != nil {
t.Fatalf("%s 用例 %d: 解码失败: %v", v.name, i, err)
}
if !bytes.Equal(decoded, payload) {
t.Errorf("%s 用例 %d: 往返不一致,得到 %x,期望 %x", v.name, i, decoded, payload)
}
}
}
}
func TestBase64xUrl(t *testing.T) {
original := "Hello, World!"
encoded := base64x.UrlEncode(original)
decoded, err := base64x.UrlDecode(encoded)
if err != nil {
t.Error(err)
// TestEncodingFormat 校验各变体的字符集与填充约定。
func TestEncodingFormat(t *testing.T) {
// 选用会使标准 Base64 产生 "+" 与 "/" 的字节,以验证 URL 安全变体确实替换了它们。
payload := []byte{0xfb, 0xff, 0xbf, 0x00, 0xfe, 0x3e, 0x3f}
if got := base64x.Encode(payload); !strings.ContainsAny(got, "+/") {
t.Fatalf("测试数据未能触发标准字母表的 +/ 字符,实际编码为 %q", got)
}
if original != decoded {
t.Errorf("expected %q, got %q", original, decoded)
for _, v := range variants {
encoded := v.encode(payload)
if v.alphabetURL {
if strings.ContainsAny(encoded, "+/") {
t.Errorf("%s 编码结果含非 URL 安全字符: %q", v.name, encoded)
}
}
hasPadding := strings.HasSuffix(encoded, "=")
if hasPadding != v.padded {
t.Errorf("%s 填充状态错误: 编码为 %q, 期望带填充=%v", v.name, encoded, v.padded)
}
if v.padded && len(encoded)%4 != 0 {
t.Errorf("%s 带填充编码长度应为 4 的倍数: %q", v.name, encoded)
}
if !isASCII(encoded) {
t.Errorf("%s 编码结果含非 ASCII 字符: %q", v.name, encoded)
}
}
}
func TestBase64xRaw(t *testing.T) {
original := "Hello, World!"
encoded := base64x.RawEncode(original)
decoded, err := base64x.RawDecode(encoded)
if err != nil {
t.Error(err)
// TestEncodeEdgeLengths 校验填充边界(1/2/3 字节分组)的编码结果符合 RFC 4648。
func TestEncodeEdgeLengths(t *testing.T) {
cases := []struct {
in []byte
std string
raw string
url string
rawU string
}{
{[]byte("f"), "Zg==", "Zg", "Zg==", "Zg"},
{[]byte("fo"), "Zm8=", "Zm8", "Zm8=", "Zm8"},
{[]byte("foo"), "Zm9v", "Zm9v", "Zm9v", "Zm9v"},
{[]byte("foob"), "Zm9vYg==", "Zm9vYg", "Zm9vYg==", "Zm9vYg"},
{[]byte("fooba"), "Zm9vYmE=", "Zm9vYmE", "Zm9vYmE=", "Zm9vYmE"},
{[]byte("foobar"), "Zm9vYmFy", "Zm9vYmFy", "Zm9vYmFy", "Zm9vYmFy"},
{[]byte{0xfb, 0xff, 0xbf}, "+/+/", "+/+/", "-_-_", "-_-_"},
}
if original != decoded {
t.Errorf("expected %q, got %q", original, decoded)
for _, c := range cases {
if got := base64x.Encode(c.in); got != c.std {
t.Errorf("Encode(%q) = %q, 期望 %q", c.in, got, c.std)
}
if got := base64x.EncodeRaw(c.in); got != c.raw {
t.Errorf("EncodeRaw(%q) = %q, 期望 %q", c.in, got, c.raw)
}
if got := base64x.EncodeURL(c.in); got != c.url {
t.Errorf("EncodeURL(%q) = %q, 期望 %q", c.in, got, c.url)
}
if got := base64x.EncodeRawURL(c.in); got != c.rawU {
t.Errorf("EncodeRawURL(%q) = %q, 期望 %q", c.in, got, c.rawU)
}
}
}
// TestDecodeRejectsGarbage 校验 Decode* 对垃圾输入返回 nil 切片与 error。
//
// 所有用例都已用实验确认对四个变体都真正非法(见 TestStdlibBehavior)。
func TestDecodeRejectsGarbage(t *testing.T) {
garbage := []struct {
name string
in string
}{
{"非字母表字符", "!!!not base64!!!"},
{"含空格", "SGVs bG8="},
{"含制表符", "SGVs\tbG8="},
{"只有填充", "===="},
{"单个填充", "="},
{"首字符即填充", "=AAAA"},
{"残留 1 个字符", "Z"},
{"错误填充位置", "SG=VsbG8="},
{"两段拼接的填充", "Zg==Zg=="},
{"填充数量过多", "A==="},
{"emoji 输入", "🚀🚀🚀🚀"},
{"中文输入", "你好世界"},
{"二进制控制字符", "\x00\x01\x02\x03"},
}
for _, v := range variants {
for _, g := range garbage {
decoded, err := v.decode(g.in)
if err == nil {
t.Errorf("%s 解码 %q(%s)应当失败,却成功得到 %x", v.name, g.in, g.name, decoded)
}
if decoded != nil {
t.Errorf("%s 解码 %q(%s)失败时应返回 nil,实际 %x", v.name, g.in, g.name, decoded)
}
if err != nil && err.Error() == "" {
t.Errorf("%s 解码 %q 返回了空错误信息", v.name, g.in)
}
}
}
}
// TestStdlibBehavior 固定住 Go encoding/base64 解码器的实际行为
// (以下断言全部由实验测得,不是推测)。这些行为会随封装一起暴露给调用方:
//
// 1. 解码器会**忽略** \r 与 \n(MIME 历史行为),但空格与制表符仍然非法;
// 2. 标准与 URL 两套字母表在解码时**不通用**:Std 只接受 '+'/'/',
// URL 只接受 '-'/'_'(编码方向同样区分);
// 3. 无填充解码器**拒绝**任何 '=' 填充,即使位置正确;
// 4. 带填充解码器接受"多余填充"与非规范尾部比特,
// 例如 "SGV=" 会被解码为 2 字节而不报错。
//
// 因此调用方不能依赖解码器拒绝所有畸形数据,需要自行约束输入格式。
func TestStdlibBehavior(t *testing.T) {
// 1. CR/LF 被忽略,空格与制表符非法。
if got, err := base64x.Decode("SGVsbG8=\n"); err != nil || !bytes.Equal(got, []byte("Hello")) {
t.Errorf("预期换行被忽略并解码出 Hello,得到 %x, err=%v", got, err)
}
if got, err := base64x.Decode("SGVsbG8=\r\n"); err != nil || !bytes.Equal(got, []byte("Hello")) {
t.Errorf("预期 CRLF 被忽略并解码出 Hello,得到 %x, err=%v", got, err)
}
if _, err := base64x.Decode("SGVs bG8="); err == nil {
t.Error("空格应导致解码失败")
}
if _, err := base64x.Decode("SGVs\tbG8="); err == nil {
t.Error("制表符应导致解码失败")
}
// 无填充解码器遇到换行同样失败(因为输入本身不完整/带填充)。
if _, err := base64x.DecodeRaw("SGVsbG8=\n"); err == nil {
t.Error("无填充解码器应拒绝含填充的输入")
}
// 2. 字母表不通用。
if got, err := base64x.Decode("a+b/"); err != nil || !bytes.Equal(got, []byte{0x6b, 0xe6, 0xff}) {
t.Errorf("标准解码器应接受 a+b/,得到 %x, err=%v", got, err)
}
if _, err := base64x.Decode("a-b_"); err == nil {
t.Error("标准解码器应拒绝 URL 字母表 a-b_")
}
if got, err := base64x.DecodeURL("a-b_"); err != nil || !bytes.Equal(got, []byte{0x6b, 0xe6, 0xff}) {
t.Errorf("URL 解码器应接受 a-b_,得到 %x, err=%v", got, err)
}
if _, err := base64x.DecodeURL("a+b/"); err == nil {
t.Error("URL 解码器应拒绝标准字母表 a+b/")
}
// 编码方向严格区分字母表。
if got := base64x.Encode([]byte{0xfb, 0xff, 0xbf}); got != "+/+/" {
t.Errorf("标准编码应为 +/+/,得到 %q", got)
}
if got := base64x.EncodeURL([]byte{0xfb, 0xff, 0xbf}); got != "-_-_" {
t.Errorf("URL 编码应为 -_-_,得到 %q", got)
}
// 3. 无填充解码器拒绝一切填充。
for _, in := range []string{"Zg==", "Zm8=", "SGVsbG8=", "AA==", "AAA="} {
if _, err := base64x.DecodeRaw(in); err == nil {
t.Errorf("无填充解码器应拒绝带填充输入 %q", in)
}
if _, err := base64x.DecodeRawURL(in); err == nil {
t.Errorf("无填充 URL 解码器应拒绝带填充输入 %q", in)
}
}
// 4. 带填充解码器接受多余填充与非规范尾部比特。
if got, err := base64x.Decode("SGV="); err != nil || !bytes.Equal(got, []byte("He")) {
t.Errorf("预期 SGV= 被宽松接受为 He(非规范尾部比特),得到 %x, err=%v", got, err)
}
// 而无填充解码器接受非规范尾部比特,但长度必须合法。
if got, err := base64x.DecodeRaw("SGV"); err != nil || !bytes.Equal(got, []byte("He")) {
t.Errorf("无填充解码器预期接受 SGV 为 He,得到 %x, err=%v", got, err)
}
if got, err := base64x.DecodeRaw("AA"); err != nil || !bytes.Equal(got, []byte{0x00}) {
t.Errorf("无填充解码器预期接受 AA 为 00,得到 %x, err=%v", got, err)
}
}
// TestPaddingSensitiveLengths 记录"长度与填充不匹配"时的实际行为:
// 带填充解码器要求输入长度为 4 的倍数,无填充解码器要求长度不为 4k+1。
func TestPaddingSensitiveLengths(t *testing.T) {
// 应当成功的用例(合法长度)。
ok := []struct {
name string
in string
dec func(string) ([]byte, error)
want []byte
}{
{"标准 4 字节分组", "AAAA", base64x.Decode, []byte{0x00, 0x00, 0x00}},
{"标准带填充 1 字节", "Zg==", base64x.Decode, []byte("f")},
{"标准带填充 2 字节", "Zm8=", base64x.Decode, []byte("fo")},
{"无填充 2 字符", "Zg", base64x.DecodeRaw, []byte("f")},
{"无填充 3 字符", "Zm8", base64x.DecodeRaw, []byte("fo")},
{"无填充 4 字符", "AAAA", base64x.DecodeRaw, []byte{0x00, 0x00, 0x00}},
{"无填充 7 字符", "SGVsbG8", base64x.DecodeRaw, []byte("Hello")},
{"无填充 URL 7 字符", "SGVsbG8", base64x.DecodeRawURL, []byte("Hello")},
{"带填充 URL 8 字符", "SGVsbG8=", base64x.DecodeURL, []byte("Hello")},
}
for _, c := range ok {
got, err := c.dec(c.in)
if err != nil {
t.Errorf("%s: 解码 %q 意外失败: %v", c.name, c.in, err)
continue
}
if !bytes.Equal(got, c.want) {
t.Errorf("%s: 解码 %q = %x, 期望 %x", c.name, c.in, got, c.want)
}
}
// 应当失败的用例。
failures := []struct {
name string
in string
dec func(string) ([]byte, error)
}{
{"带填充解码器遇到缺失填充", "Zg", base64x.Decode},
{"带填充解码器遇到缺失填充(2)", "Zm8", base64x.Decode},
{"带填充解码器遇到长度非 4 倍数", "SGVsbG8", base64x.Decode},
{"带填充 URL 解码器遇到缺失填充", "Zg", base64x.DecodeURL},
{"带填充 URL 解码器遇到长度非 4 倍数", "SGVsbG8", base64x.DecodeURL},
{"无填充解码器遇到 1 个残留字符", "Z", base64x.DecodeRaw},
{"无填充 URL 解码器遇到 1 个残留字符", "Z", base64x.DecodeRawURL},
{"带填充解码器遇到 1 个残留字符", "Z", base64x.Decode},
{"无填充解码器遇到 5 个字符", "AAAAA", base64x.DecodeRaw},
{"无填充解码器遇到带填充 4 字符", "Zg==", base64x.DecodeRaw},
{"无填充 URL 解码器遇到带填充 4 字符", "Zg==", base64x.DecodeRawURL},
}
for _, c := range failures {
got, err := c.dec(c.in)
if err == nil {
t.Errorf("%s: 解码 %q 应当失败,却成功得到 %x", c.name, c.in, got)
}
if got != nil {
t.Errorf("%s: 解码 %q 失败时应返回 nil,实际 %x", c.name, c.in, got)
}
}
}
// TestDecodeEmptyInput 明确各变体对空串的影响(空串是合法的"零长度数据")。
func TestDecodeEmptyInput(t *testing.T) {
for _, v := range variants {
decoded, err := v.decode("")
if err != nil {
t.Errorf("%s 解码空串应成功: %v", v.name, err)
continue
}
if len(decoded) != 0 {
t.Errorf("%s 解码空串得到 %d 字节,期望 0", v.name, len(decoded))
}
}
}
// TestCrossCheckWithStdlib 与标准库交叉验证,确保封装没有引入行为差异。
func TestCrossCheckWithStdlib(t *testing.T) {
payload := []byte("cross check with encoding/base64")
if got, want := base64x.Encode(payload), base64.StdEncoding.EncodeToString(payload); got != want {
t.Errorf("Encode = %q, 标准库 = %q", got, want)
}
if got, want := base64x.EncodeURL(payload), base64.URLEncoding.EncodeToString(payload); got != want {
t.Errorf("EncodeURL = %q, 标准库 = %q", got, want)
}
if got, want := base64x.EncodeRaw(payload), base64.RawStdEncoding.EncodeToString(payload); got != want {
t.Errorf("EncodeRaw = %q, 标准库 = %q", got, want)
}
if got, want := base64x.EncodeRawURL(payload), base64.RawURLEncoding.EncodeToString(payload); got != want {
t.Errorf("EncodeRawURL = %q, 标准库 = %q", got, want)
}
decoded, err := base64x.Decode(base64.StdEncoding.EncodeToString(payload))
if err != nil || !bytes.Equal(decoded, payload) {
t.Errorf("Decode 与标准库编码互操作失败: %v", err)
}
}
// TestNoPanicOnAnyInput 用一批畸形输入整体验证解码与编码都不 panic。
func TestNoPanicOnAnyInput(t *testing.T) {
defer func() {
if r := recover(); r != nil {
t.Fatalf("畸形输入导致 panic: %v", r)
}
}()
inputs := []string{"", "=", "==", "====", "A", "AA", "AAA", "AAAA", "\x00\x01", strings.Repeat("=", 100), strings.Repeat("A", 1000)}
for _, in := range inputs {
_, _ = base64x.Decode(in)
_, _ = base64x.DecodeURL(in)
_, _ = base64x.DecodeRaw(in)
_, _ = base64x.DecodeRawURL(in)
_ = base64x.Encode([]byte(in))
_ = base64x.EncodeRaw([]byte(in))
}
}
// isASCII 判断字符串是否只含 ASCII 可打印字符。
func isASCII(s string) bool {
for i := 0; i < len(s); i++ {
if s[i] < 0x20 || s[i] > 0x7e {
return false
}
}
return true
}