diff --git a/CHANGELOG.md b/CHANGELOG.md index 20e4420..4904610 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog: @go/crypto-sm +## [v1.1.0] - 2026-05-07 + +### Added +- **国密算法强化**:同步引入基于 Argon2id 的密码派生密钥能力。 +- **便捷构造器**:新增 `NewSM4CBCByPassword`, `NewSM4GCMByPassword` 及 `NewSM2ByPassword` API,其中 SM2 支持基于密码的确定性密钥对生成。 +- **内存安全**:所有密码构造器均强制执行 `EraseKey` 策略,派生后物理擦除密码与盐。 +- **依赖更新**:升级 `apigo.cc/go/crypto` 至 v1.1.0。 + ## [v1.0.6] - 2026-05-06 ### Changed diff --git a/README.md b/README.md index 0776077..9134b25 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ ### SM2 (国密非对称) - `func NewSM2AndEraseKey(priv, pub []byte) (*crypto.Asymmetric, error)` +- `func NewSM2ByPassword(password, salt []byte) (*crypto.Asymmetric, error)` (确定性生成) - `func GenerateSM2KeyPair() (priv, pub []byte, err error)` ### SM3 (国密哈希) @@ -24,7 +25,9 @@ ### SM4 (国密对称) - `func NewSM4CBCAndEraseKey(key, iv []byte) (*crypto.Symmetric, error)` +- `func NewSM4CBCByPassword(password, salt []byte) (*crypto.Symmetric, error)` - `func NewSM4GCMAndEraseKey(key, iv []byte) (*crypto.Symmetric, error)` +- `func NewSM4GCMByPassword(password, salt []byte) (*crypto.Symmetric, error)` ## 📦 安装 diff --git a/TEST.md b/TEST.md index 35bca70..70daaec 100644 --- a/TEST.md +++ b/TEST.md @@ -1,7 +1,7 @@ # Test Report: @go/crypto-sm ## 📋 测试概览 -- **测试时间**: 2026-05-06 +- **测试时间**: 2026-05-07 - **测试环境**: darwin/amd64 - **Go 版本**: 1.25.0 @@ -11,6 +11,8 @@ | `TestSM2` | PASS | SM2 签名、验签与加解密测试。 | | `TestSM3` | PASS | SM3 哈希确定性验证。 | | `TestSM4` | PASS | SM4 CBC/GCM 模式加解密测试。 | +| `TestSMPassword` | PASS | 基于密码的 SM4 与 SM2 构造器测试。 | +| `TestSM2Deterministic` | PASS | SM2 密钥基于密码的确定性生成验证。 | ## 🛡️ 鲁棒性防御 (Robustness) - **摩擦消除**:移除 `Must` 冗余 API,完全对齐 `go/crypto` 规范。 diff --git a/go.mod b/go.mod index 97325c3..68db4a0 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module apigo.cc/go/crypto-sm go 1.25.0 require ( - apigo.cc/go/crypto v1.0.6 + apigo.cc/go/crypto v1.1.0 apigo.cc/go/encoding v1.0.5 apigo.cc/go/safe v1.0.5 github.com/emmansun/gmsm v0.28.0 diff --git a/sm2.go b/sm2.go index fab7501..0b32a01 100644 --- a/sm2.go +++ b/sm2.go @@ -28,6 +28,36 @@ func NewSM2WithoutEraseKey(privateKey, publicKey []byte) (*crypto.Asymmetric, er return crypto.NewAsymmetricWithoutEraseKey(SM2, privateKey, publicKey, false) } +func NewSM2ByPassword(password, salt []byte) (*crypto.Asymmetric, error) { + seed := crypto.DeriveKey(password, salt, 32) + defer safe.ZeroMemory(seed) + + curve := sm2.P256() + params := curve.Params() + + // Derive D: seed % (n-1) + 1 + // SM2 private key d should be in [1, n-1] + 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(sm2.PrivateKey) + priv.Curve = curve + priv.D = d + priv.PublicKey.X, priv.PublicKey.Y = curve.ScalarBaseMult(d.Bytes()) + + privateKey, err := smx509.MarshalPKCS8PrivateKey(priv) + if err != nil { + return nil, err + } + publicKey, err := smx509.MarshalPKIXPublicKey(&priv.PublicKey) + if err != nil { + return nil, err + } + return NewSM2AndEraseKey(privateKey, publicKey) +} + func GenerateSM2KeyPair() ([]byte, []byte, error) { privKey, err := sm2.GenerateKey(rand.Reader) if err != nil { diff --git a/sm4.go b/sm4.go index a7544d0..8b76248 100644 --- a/sm4.go +++ b/sm4.go @@ -36,7 +36,20 @@ func NewSM4GCMWithoutEraseKey(key, iv []byte) (*crypto.Symmetric, error) { return crypto.NewSymmetricWithoutEraseKey(SM4GCM, key, iv) } +func NewSM4CBCByPassword(password, salt []byte) (*crypto.Symmetric, error) { + derived := crypto.DeriveKey(password, salt, 16+16) + defer safe.ZeroMemory(derived) + return NewSM4CBCAndEraseKey(derived[:16], derived[16:]) +} + +func NewSM4GCMByPassword(password, salt []byte) (*crypto.Symmetric, error) { + derived := crypto.DeriveKey(password, salt, 16+12) + defer safe.ZeroMemory(derived) + return NewSM4GCMAndEraseKey(derived[:16], derived[16:]) +} + func (s *SM4Cipher) Encrypt(data []byte, key []byte, iv []byte) ([]byte, error) { + block, err := sm4.NewCipher(key) if err != nil { return nil, err diff --git a/sm_password_test.go b/sm_password_test.go new file mode 100644 index 0000000..8e88d0f --- /dev/null +++ b/sm_password_test.go @@ -0,0 +1,59 @@ +package sm_test + +import ( + "bytes" + "testing" + + "apigo.cc/go/cast" + "apigo.cc/go/crypto-sm" +) + +func TestSMPassword(t *testing.T) { + password := []byte("sm-secret-password") + salt := []byte("sm-salt") + data := []byte("hello sm password") + + // 1. SM4-GCM + p1 := append([]byte(nil), password...) + s1 := append([]byte(nil), salt...) + sm4, err := sm.NewSM4GCMByPassword(p1, s1) + if err != nil { + t.Fatal(err) + } + enc := cast.As(sm4.EncryptBytes(data)) + dec := cast.As(sm4.DecryptBytes(enc)) + if !bytes.Equal(data, dec) { + t.Error("SM4-GCM password roundtrip failed") + } + + // 2. SM2 + p2 := append([]byte(nil), password...) + s2 := append([]byte(nil), salt...) + sm2a, err := sm.NewSM2ByPassword(p2, s2) + if err != nil { + t.Fatal(err) + } + sig := cast.As(sm2a.Sign(data)) + if ok, _ := sm2a.Verify(data, sig); !ok { + t.Error("SM2 password sign/verify failed") + } +} + +func TestSM2Deterministic(t *testing.T) { + password := []byte("sm2-determ-pass") + salt := []byte("sm2-determ-salt") + + p1 := append([]byte(nil), password...) + s1 := append([]byte(nil), salt...) + a1, _ := sm.NewSM2ByPassword(p1, s1) + + p2 := append([]byte(nil), password...) + s2 := append([]byte(nil), salt...) + a2, _ := sm.NewSM2ByPassword(p2, s2) + + data := []byte("test") + sig, _ := a1.Sign(data) + if ok, _ := a2.Verify(data, sig); !ok { + t.Error("SM2 deterministic generation failed") + } +}