diff --git a/CHANGELOG.md b/CHANGELOG.md index c6961b2..be73379 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index 10cf141..7237f70 100644 --- a/README.md +++ b/README.md @@ -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)` diff --git a/TEST.md b/TEST.md index 687f312..cd57254 100644 --- a/TEST.md +++ b/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** | 混合加密模式性能表现。 | diff --git a/aes.go b/aes.go index a126cf5..370cfa4 100644 --- a/aes.go +++ b/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 { diff --git a/ecdsa.go b/ecdsa.go index 09c15db..0347914 100644 --- a/ecdsa.go +++ b/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} } diff --git a/ed25519.go b/ed25519.go index f5de5f9..c1919e0 100644 --- a/ed25519.go +++ b/ed25519.go @@ -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 { diff --git a/password.go b/password.go new file mode 100644 index 0000000..e1cb27b --- /dev/null +++ b/password.go @@ -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) +} diff --git a/password_test.go b/password_test.go new file mode 100644 index 0000000..61212a0 --- /dev/null +++ b/password_test.go @@ -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") + } +} diff --git a/rsa.go b/rsa.go index 546cab9..6075687 100644 --- a/rsa.go +++ b/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 diff --git a/x25519.go b/x25519.go index e918d29..fbe5ada 100644 --- a/x25519.go +++ b/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} }