docs: support ByPassword series and release v1.1.0 (by AI)
This commit is contained in:
parent
5f4e5591cf
commit
7c8f3464c0
@ -1,5 +1,13 @@
|
||||
# Changelog: @go/crypto
|
||||
|
||||
## [v1.1.0] - 2026-05-07
|
||||
|
||||
### Added
|
||||
- **密码学强化**:新增基于 Argon2id 的密码派生密钥 (KDF) 支持。
|
||||
- **便捷构造器**:新增 `New...ByPassword` 系列 API,支持通过密码与盐直接创建 AES (GCM/CBC)、RSA、ECDSA、Ed25519 及 X25519 实例。
|
||||
- **确定性生成**:非对称算法(ECDSA, Ed25519, X25519)支持基于密码的确定性密钥对生成,确保多端/多次调用的一致性(注:RSA 因算法特性,其确定性受 Go 版本内部实现影响)。
|
||||
- **强制内存安全**:所有 `ByPassword` 接口均强制执行 `EraseKey` 策略,派生完成后立即擦除原始密码与盐。
|
||||
|
||||
## [v1.0.6] - 2026-05-06
|
||||
|
||||
### Changed
|
||||
|
||||
@ -18,12 +18,18 @@
|
||||
### 对称加密 (Symmetric)
|
||||
- `func NewAESCBC(safeKeyBuf, safeIvBuf *safe.SafeBuf) (*Symmetric, error)`
|
||||
- `func NewAESCBCAndEraseKey(key, iv []byte) (*Symmetric, error)`
|
||||
- `func NewAESCBCByPassword(password, salt []byte) (*Symmetric, error)` (Argon2id 驱动)
|
||||
- `func NewAESGCMByPassword(password, salt []byte) (*Symmetric, error)` (Argon2id 驱动)
|
||||
- `func (s *Symmetric) EncryptBytes(data []byte) ([]byte, error)`
|
||||
- `func (s *Symmetric) DecryptBytes(data []byte) ([]byte, error)`
|
||||
- `func (s *Symmetric) TryDecrypt(data []byte) []byte` (解密失败返回原始数据)
|
||||
|
||||
### 非对称加密与签名 (Asymmetric)
|
||||
- `func NewRSA(safePrivateKeyBuf, safePublicKeyBuf *safe.SafeBuf) (*Asymmetric, error)`
|
||||
- `func NewRSAByPassword(password, salt []byte, bitSize ...int) (*Asymmetric, error)` (Argon2id 确定性生成)
|
||||
- `func NewECDSAByPassword(password, salt []byte, bitSize ...int) (*Asymmetric, error)` (确定性生成)
|
||||
- `func NewED25519ByPassword(password, salt []byte) (*Asymmetric, error)` (确定性生成)
|
||||
- `func NewX25519ByPassword(password, salt []byte) (*Asymmetric, error)` (确定性生成)
|
||||
- `func (a *Asymmetric) Sign(data []byte, hash ...crypto.Hash) ([]byte, error)`
|
||||
- `func (a *Asymmetric) Verify(data, signature []byte, hash ...crypto.Hash) (bool, error)`
|
||||
- `func (a *Asymmetric) EncryptBytes(data []byte) ([]byte, error)`
|
||||
|
||||
11
TEST.md
11
TEST.md
@ -1,7 +1,7 @@
|
||||
# Test Report: @go/crypto
|
||||
|
||||
## 📋 测试概览
|
||||
- **测试时间**: 2026-05-06
|
||||
- **测试时间**: 2026-05-07
|
||||
- **测试环境**: darwin/amd64
|
||||
- **Go 版本**: 1.25.0
|
||||
|
||||
@ -12,6 +12,8 @@
|
||||
| `TestAsymmetric` | PASS | RSA, ECDSA, Ed25519, X25519 签名与加解密测试。 |
|
||||
| `TestMustAndTryMethods` | PASS | 配合 `cast.As` 消除摩擦及 `TryDecrypt` 逻辑测试。 |
|
||||
| `TestSecurityErase` | PASS | 密钥内存安全擦除验证。 |
|
||||
| `TestPasswordBased` | PASS | 基于密码 (Argon2id) 的对称与非对称密钥派生功能测试。 |
|
||||
| `TestDeterministic` | PASS | 非对称密钥基于密码的确定性生成验证。 |
|
||||
|
||||
## 🛡️ 鲁棒性防御 (Robustness)
|
||||
- **Panic 防御**:在 CBC 模式解密中强制校验块对齐,拦截底层库可能抛出的 Panic。
|
||||
@ -20,6 +22,7 @@
|
||||
## ⚡ 性能基准 (Benchmarks)
|
||||
| 函数 | 平均耗时 | 性能分析 |
|
||||
| :--- | :--- | :--- |
|
||||
| `AES_GCM` | **4805 ns/op** | 性能优异。 |
|
||||
| `RSA_Sign` | **1349477 ns/op** | 复合 RSA 标准耗时。 |
|
||||
| `Ed25519_Sign` | **26689 ns/op** | 高性能签名。 |
|
||||
| `AES_GCM` | **4854 ns/op** | 性能优异。 |
|
||||
| `RSA_Sign` | **1407588 ns/op** | 符合 RSA 标准耗时。 |
|
||||
| `Ed25519_Sign` | **28299 ns/op** | 高性能签名。 |
|
||||
| `X25519_Encrypt` | **204639 ns/op** | 混合加密模式性能表现。 |
|
||||
|
||||
12
aes.go
12
aes.go
@ -39,6 +39,18 @@ func NewAESGCMWithoutEraseKey(key, iv []byte) (*Symmetric, error) {
|
||||
return NewSymmetricWithoutEraseKey(AESGCM, key, iv)
|
||||
}
|
||||
|
||||
func NewAESCBCByPassword(password, salt []byte) (*Symmetric, error) {
|
||||
derived := DeriveKey(password, salt, 32+16)
|
||||
defer safe.ZeroMemory(derived)
|
||||
return NewSymmetricAndEraseKey(AESCBC, derived[:32], derived[32:])
|
||||
}
|
||||
|
||||
func NewAESGCMByPassword(password, salt []byte) (*Symmetric, error) {
|
||||
derived := DeriveKey(password, salt, 32+12)
|
||||
defer safe.ZeroMemory(derived)
|
||||
return NewSymmetricAndEraseKey(AESGCM, derived[:32], derived[32:])
|
||||
}
|
||||
|
||||
func (c *AESCipher) Encrypt(data []byte, key []byte, iv []byte) ([]byte, error) {
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
|
||||
45
ecdsa.go
45
ecdsa.go
@ -8,6 +8,7 @@ import (
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"io"
|
||||
"math/big"
|
||||
|
||||
"apigo.cc/go/safe"
|
||||
"golang.org/x/crypto/hkdf"
|
||||
@ -35,6 +36,50 @@ func NewECDSAWithoutEraseKey(safePrivateKeyBuf, safePublicKeyBuf []byte) (*Asymm
|
||||
return NewAsymmetricWithoutEraseKey(ECDSAGCM, safePrivateKeyBuf, safePublicKeyBuf, false)
|
||||
}
|
||||
|
||||
func NewECDSAByPassword(password, salt []byte, bitSize ...int) (*Asymmetric, error) {
|
||||
size := 256
|
||||
if len(bitSize) > 0 {
|
||||
size = bitSize[0]
|
||||
}
|
||||
var curve elliptic.Curve
|
||||
keyLen := 32
|
||||
switch size {
|
||||
case 256:
|
||||
curve = elliptic.P256()
|
||||
keyLen = 32
|
||||
case 384:
|
||||
curve = elliptic.P384()
|
||||
keyLen = 48
|
||||
default:
|
||||
curve = elliptic.P521()
|
||||
keyLen = 66 // (521+7)/8
|
||||
}
|
||||
|
||||
seed := DeriveKey(password, salt, uint32(keyLen))
|
||||
defer safe.ZeroMemory(seed)
|
||||
|
||||
params := curve.Params()
|
||||
d := new(big.Int).SetBytes(seed)
|
||||
nMinusOne := new(big.Int).Sub(params.N, big.NewInt(1))
|
||||
d.Mod(d, nMinusOne)
|
||||
d.Add(d, big.NewInt(1))
|
||||
|
||||
priv := new(ecdsa.PrivateKey)
|
||||
priv.Curve = curve
|
||||
priv.D = d
|
||||
priv.PublicKey.X, priv.PublicKey.Y = curve.ScalarBaseMult(d.Bytes())
|
||||
|
||||
privateKey, err := x509.MarshalECPrivateKey(priv)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
publicKey, err := x509.MarshalPKIXPublicKey(&priv.PublicKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewECDSAAndEraseKey(privateKey, publicKey)
|
||||
}
|
||||
|
||||
func NewECDSAAlgorithm(useGCM bool, hash crypto.Hash, kdfInfo, kdfSalt []byte) *ECDSAAlgorithm {
|
||||
return &ECDSAAlgorithm{UseGCM: useGCM, Hash: hash, KdfInfo: kdfInfo, KdfSalt: kdfSalt}
|
||||
}
|
||||
|
||||
@ -23,6 +23,14 @@ func NewED25519WithoutEraseKey(safePrivateKeyBuf, safePublicKeyBuf []byte) (*Asy
|
||||
return NewAsymmetricWithoutEraseKey(ED25519, safePrivateKeyBuf, safePublicKeyBuf, false)
|
||||
}
|
||||
|
||||
func NewED25519ByPassword(password, salt []byte) (*Asymmetric, error) {
|
||||
seed := DeriveKey(password, salt, 32)
|
||||
defer safe.ZeroMemory(seed)
|
||||
privKey := ed25519.NewKeyFromSeed(seed)
|
||||
pubKey := privKey.Public().(ed25519.PublicKey)
|
||||
return NewED25519AndEraseKey(privKey, pubKey)
|
||||
}
|
||||
|
||||
func GenerateEd25519KeyPair() ([]byte, []byte, error) {
|
||||
pubKey, privKey, err := ed25519.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
|
||||
30
password.go
Normal file
30
password.go
Normal file
@ -0,0 +1,30 @@
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"io"
|
||||
|
||||
"apigo.cc/go/safe"
|
||||
"golang.org/x/crypto/argon2"
|
||||
"golang.org/x/crypto/hkdf"
|
||||
)
|
||||
|
||||
// Default Argon2id parameters
|
||||
const (
|
||||
Argon2Time = 3
|
||||
Argon2Memory = 64 * 1024 // 64MB
|
||||
Argon2Threads = 4
|
||||
)
|
||||
|
||||
// DeriveKey using Argon2id and automatically erases password and salt
|
||||
func DeriveKey(password, salt []byte, keyLen uint32) []byte {
|
||||
defer safe.ZeroMemory(password)
|
||||
defer safe.ZeroMemory(salt)
|
||||
return argon2.IDKey(password, salt, Argon2Time, Argon2Memory, Argon2Threads, keyLen)
|
||||
}
|
||||
|
||||
// NewDeterministicReader creates an io.Reader that produces a deterministic stream of bytes from a seed.
|
||||
// This is used to make RSA/ECDSA key generation deterministic based on a password.
|
||||
func NewDeterministicReader(seed []byte, info []byte) io.Reader {
|
||||
return hkdf.New(sha256.New, seed, nil, info)
|
||||
}
|
||||
139
password_test.go
Normal file
139
password_test.go
Normal file
@ -0,0 +1,139 @@
|
||||
package crypto_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"apigo.cc/go/crypto"
|
||||
)
|
||||
|
||||
func TestPasswordBasedAES(t *testing.T) {
|
||||
password := []byte("secret-password")
|
||||
salt := []byte("fixed-salt")
|
||||
data := []byte("hello world password")
|
||||
|
||||
// Test GCM
|
||||
p1 := append([]byte(nil), password...)
|
||||
s1 := append([]byte(nil), salt...)
|
||||
aesGCM, err := crypto.NewAESGCMByPassword(p1, s1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
enc, _ := aesGCM.EncryptBytes(data)
|
||||
dec, _ := aesGCM.DecryptBytes(enc)
|
||||
if !bytes.Equal(data, dec) {
|
||||
t.Error("AES-GCM password-based roundtrip failed")
|
||||
}
|
||||
|
||||
// Test CBC
|
||||
p2 := append([]byte(nil), password...)
|
||||
s2 := append([]byte(nil), salt...)
|
||||
aesCBC, err := crypto.NewAESCBCByPassword(p2, s2)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
enc2, _ := aesCBC.EncryptBytes(data)
|
||||
dec2, _ := aesCBC.DecryptBytes(enc2)
|
||||
if !bytes.Equal(data, dec2) {
|
||||
t.Error("AES-CBC password-based roundtrip failed")
|
||||
}
|
||||
|
||||
// Verify EraseKey (best effort check - though they are zeroed inside factory)
|
||||
if bytes.Equal(p1, []byte("secret-password")) {
|
||||
t.Error("Password 1 was not erased")
|
||||
}
|
||||
if bytes.Equal(s1, []byte("fixed-salt")) {
|
||||
t.Error("Salt 1 was not erased")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPasswordBasedAsymmetric(t *testing.T) {
|
||||
password := []byte("asymm-password")
|
||||
salt := []byte("asymm-salt")
|
||||
data := []byte("hello asymm")
|
||||
|
||||
// 1. RSA
|
||||
p1 := append([]byte(nil), password...)
|
||||
s1 := append([]byte(nil), salt...)
|
||||
rsa, err := crypto.NewRSAByPassword(p1, s1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
enc, _ := rsa.EncryptBytes(data)
|
||||
dec, _ := rsa.DecryptBytes(enc)
|
||||
if !bytes.Equal(data, dec) {
|
||||
t.Error("RSA password-based roundtrip failed")
|
||||
}
|
||||
|
||||
// 2. ECDSA
|
||||
p2 := append([]byte(nil), password...)
|
||||
s2 := append([]byte(nil), salt...)
|
||||
ecdsa, err := crypto.NewECDSAByPassword(p2, s2)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sig, _ := ecdsa.Sign(data)
|
||||
if ok, _ := ecdsa.Verify(data, sig); !ok {
|
||||
t.Error("ECDSA password-based sign/verify failed")
|
||||
}
|
||||
|
||||
// 3. Ed25519
|
||||
p3 := append([]byte(nil), password...)
|
||||
s3 := append([]byte(nil), salt...)
|
||||
ed, err := crypto.NewED25519ByPassword(p3, s3)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sig2, _ := ed.Sign(data)
|
||||
if ok, _ := ed.Verify(data, sig2); !ok {
|
||||
t.Error("Ed25519 password-based failed")
|
||||
}
|
||||
|
||||
// 4. X25519
|
||||
p4 := append([]byte(nil), password...)
|
||||
s4 := append([]byte(nil), salt...)
|
||||
x, err := crypto.NewX25519ByPassword(p4, s4)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
enc2, _ := x.EncryptBytes(data)
|
||||
dec2, _ := x.DecryptBytes(enc2)
|
||||
if !bytes.Equal(data, dec2) {
|
||||
t.Error("X25519 password-based roundtrip failed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeterministicGeneration(t *testing.T) {
|
||||
password := []byte("determ-pass")
|
||||
salt := []byte("determ-salt")
|
||||
|
||||
// Two instances with same password/salt should have same public key
|
||||
p1 := append([]byte(nil), password...)
|
||||
s1 := append([]byte(nil), salt...)
|
||||
ed1, _ := crypto.NewED25519ByPassword(p1, s1)
|
||||
|
||||
p2 := append([]byte(nil), password...)
|
||||
s2 := append([]byte(nil), salt...)
|
||||
ed2, _ := crypto.NewED25519ByPassword(p2, s2)
|
||||
|
||||
// Since we don't have GetPublicKeyBytes, we can test by signing with ed1 and verifying with ed2 (if public keys match)
|
||||
data := []byte("test")
|
||||
sig, _ := ed1.Sign(data)
|
||||
if ok, _ := ed2.Verify(data, sig); !ok {
|
||||
t.Error("Deterministic generation failed (Ed25519): public keys do not match")
|
||||
}
|
||||
|
||||
// Test ECDSA determinism
|
||||
p3 := append([]byte(nil), password...)
|
||||
s3 := append([]byte(nil), salt...)
|
||||
ec1, _ := crypto.NewECDSAByPassword(p3, s3)
|
||||
|
||||
p4 := append([]byte(nil), password...)
|
||||
s4 := append([]byte(nil), salt...)
|
||||
ec2, _ := crypto.NewECDSAByPassword(p4, s4)
|
||||
|
||||
sig2, _ := ec1.Sign(data)
|
||||
if ok, _ := ec2.Verify(data, sig2); !ok {
|
||||
t.Error("Deterministic generation failed (ECDSA): public keys do not match")
|
||||
}
|
||||
}
|
||||
24
rsa.go
24
rsa.go
@ -33,6 +33,30 @@ func NewRSAWithoutEraseKey(safePrivateKeyBuf, safePublicKeyBuf []byte) (*Asymmet
|
||||
return NewAsymmetricWithoutEraseKey(RSA, safePrivateKeyBuf, safePublicKeyBuf, false)
|
||||
}
|
||||
|
||||
func NewRSAByPassword(password, salt []byte, bitSize ...int) (*Asymmetric, error) {
|
||||
size := 2048
|
||||
if len(bitSize) > 0 {
|
||||
size = bitSize[0]
|
||||
}
|
||||
seed := DeriveKey(password, salt, 32)
|
||||
defer safe.ZeroMemory(seed)
|
||||
reader := NewDeterministicReader(seed, []byte("RSA Key Generation"))
|
||||
|
||||
priKey, err := rsa.GenerateKey(reader, size)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
privateKey, err := x509.MarshalPKCS8PrivateKey(priKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
publicKey, err := x509.MarshalPKIXPublicKey(&priKey.PublicKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewRSAAndEraseKey(privateKey, publicKey)
|
||||
}
|
||||
|
||||
func GenerateRSAKeyPair(bitSize int) ([]byte, []byte, error) {
|
||||
if bitSize < 2048 {
|
||||
bitSize = 2048
|
||||
|
||||
10
x25519.go
10
x25519.go
@ -33,6 +33,16 @@ func NewX25519WithoutEraseKey(safePrivateKeyBuf, safePublicKeyBuf []byte) (*Asym
|
||||
return NewAsymmetricWithoutEraseKey(X25519GCM, safePrivateKeyBuf, safePublicKeyBuf, false)
|
||||
}
|
||||
|
||||
func NewX25519ByPassword(password, salt []byte) (*Asymmetric, error) {
|
||||
seed := DeriveKey(password, salt, 32)
|
||||
defer safe.ZeroMemory(seed)
|
||||
pubKey, err := curve25519.X25519(seed, curve25519.Basepoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewX25519AndEraseKey(seed, pubKey)
|
||||
}
|
||||
|
||||
func NewX25519Algorithm(useGCM bool, hash crypto.Hash, kdfInfo, kdfSalt []byte) *X25519Algorithm {
|
||||
return &X25519Algorithm{UseGCM: useGCM, Hash: hash, KdfInfo: kdfInfo, KdfSalt: kdfSalt}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user