Compare commits

...

6 Commits
v1.0.6 ... main

Author SHA1 Message Date
AI Engineer
0d17076c8a chore: infrastructure alignment and doc sync (by AICoder) 2026-05-16 01:22:44 +08:00
AI Engineer
d977f8a727 feat: implement unified DefaultAES interface and align infrastructure 2026-05-12 23:10:29 +08:00
AI Engineer
eb90ce303c 对齐 Tag v1.3.0 (By AI) 2026-05-10 15:48:12 +08:00
AI Engineer
13f975e041 chore: final infrastructure alignment 2026-05-10 13:11:06 +08:00
AI Engineer
5cf6db17e2 chore: infrastructure alignment 2026-05-10 13:04:16 +08:00
AI Engineer
7c8f3464c0 docs: support ByPassword series and release v1.1.0 (by AI) 2026-05-07 20:59:23 +08:00
16 changed files with 452 additions and 22 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
.ai/
.geminiignore
.gemini
/CODE-FULL.md

5
CHANGELOG-LATEST.md Normal file
View File

@ -0,0 +1,5 @@
## [v1.1.2] - 2026-05-12
### Added
- **统一配置解密接口 (加固版)**:新增 `OnSetDefaultAES``SetDefaultAES` 注入接口,支持自动擦除密钥与自动锁定通道。
- **基础设施对齐**:同步更新 `redis`, `api`, `db`, `mail` 等项目接入 `crypto` 安全解密体系。

View File

@ -1,5 +1,20 @@
# Changelog: @go/crypto # Changelog: @go/crypto
## [v1.1.2] - 2026-05-12
### Added
- **统一配置解密接口 (加固版)**:新增 `OnSetDefaultAES` 回调与 `SetDefaultAES` 注入接口。
- **极致内存安全**`SetDefaultAES` 注入生产密钥后将**自动擦除** (ZeroMemory) 传入的密钥源字节,并**自动锁定**注册通道。
- **基础设施对齐**:同步更新 `redis`, `api`, `db`, `mail` 等项目,移除本地 `confAes`,全面接入安全加固后的 `crypto.confAES` 体系。
## [v1.1.1] - 2026-05-12
### 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 ## [v1.0.6] - 2026-05-06
### Changed ### Changed

View File

@ -18,12 +18,18 @@
### 对称加密 (Symmetric) ### 对称加密 (Symmetric)
- `func NewAESCBC(safeKeyBuf, safeIvBuf *safe.SafeBuf) (*Symmetric, error)` - `func NewAESCBC(safeKeyBuf, safeIvBuf *safe.SafeBuf) (*Symmetric, error)`
- `func NewAESCBCAndEraseKey(key, iv []byte) (*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) EncryptBytes(data []byte) ([]byte, error)`
- `func (s *Symmetric) DecryptBytes(data []byte) ([]byte, error)` - `func (s *Symmetric) DecryptBytes(data []byte) ([]byte, error)`
- `func (s *Symmetric) TryDecrypt(data []byte) []byte` (解密失败返回原始数据) - `func (s *Symmetric) TryDecrypt(data []byte) []byte` (解密失败返回原始数据)
### 非对称加密与签名 (Asymmetric) ### 非对称加密与签名 (Asymmetric)
- `func NewRSA(safePrivateKeyBuf, safePublicKeyBuf *safe.SafeBuf) (*Asymmetric, error)` - `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) Sign(data []byte, hash ...crypto.Hash) ([]byte, error)`
- `func (a *Asymmetric) Verify(data, signature []byte, hash ...crypto.Hash) (bool, error)` - `func (a *Asymmetric) Verify(data, signature []byte, hash ...crypto.Hash) (bool, error)`
- `func (a *Asymmetric) EncryptBytes(data []byte) ([]byte, error)` - `func (a *Asymmetric) EncryptBytes(data []byte) ([]byte, error)`
@ -35,6 +41,12 @@
- `func GenerateEd25519KeyPair() (priv, pub []byte, err error)` - `func GenerateEd25519KeyPair() (priv, pub []byte, err error)`
- `func GenerateX25519KeyPair() (priv, pub []byte, err error)` - `func GenerateX25519KeyPair() (priv, pub []byte, err error)`
### 全局配置解密 (Default AES)
为实现全项目配置解密的统一管理与生产环境密钥注入,提供以下全局接口:
- `var DefaultAES *Symmetric`:全局默认 AES 实例(初始使用内置硬编码密钥)。
- `func OnSetDefaultAES(h func(*Symmetric))`:注册 `DefaultAES` 变更回调。
- `func SetDefaultAES(key, iv []byte)`:注入生产环境密钥并触发所有回调更新。
### 哈希算法 (Hash) ### 哈希算法 (Hash)
- `func MD5(data []byte) []byte` - `func MD5(data []byte) []byte`
- `func Sha256(data []byte) []byte` - `func Sha256(data []byte) []byte`

11
TEST.md
View File

@ -1,7 +1,7 @@
# Test Report: @go/crypto # Test Report: @go/crypto
## 📋 测试概览 ## 📋 测试概览
- **测试时间**: 2026-05-06 - **测试时间**: 2026-05-07
- **测试环境**: darwin/amd64 - **测试环境**: darwin/amd64
- **Go 版本**: 1.25.0 - **Go 版本**: 1.25.0
@ -12,6 +12,8 @@
| `TestAsymmetric` | PASS | RSA, ECDSA, Ed25519, X25519 签名与加解密测试。 | | `TestAsymmetric` | PASS | RSA, ECDSA, Ed25519, X25519 签名与加解密测试。 |
| `TestMustAndTryMethods` | PASS | 配合 `cast.As` 消除摩擦及 `TryDecrypt` 逻辑测试。 | | `TestMustAndTryMethods` | PASS | 配合 `cast.As` 消除摩擦及 `TryDecrypt` 逻辑测试。 |
| `TestSecurityErase` | PASS | 密钥内存安全擦除验证。 | | `TestSecurityErase` | PASS | 密钥内存安全擦除验证。 |
| `TestPasswordBased` | PASS | 基于密码 (Argon2id) 的对称与非对称密钥派生功能测试。 |
| `TestDeterministic` | PASS | 非对称密钥基于密码的确定性生成验证。 |
## 🛡️ 鲁棒性防御 (Robustness) ## 🛡️ 鲁棒性防御 (Robustness)
- **Panic 防御**:在 CBC 模式解密中强制校验块对齐,拦截底层库可能抛出的 Panic。 - **Panic 防御**:在 CBC 模式解密中强制校验块对齐,拦截底层库可能抛出的 Panic。
@ -20,6 +22,7 @@
## ⚡ 性能基准 (Benchmarks) ## ⚡ 性能基准 (Benchmarks)
| 函数 | 平均耗时 | 性能分析 | | 函数 | 平均耗时 | 性能分析 |
| :--- | :--- | :--- | | :--- | :--- | :--- |
| `AES_GCM` | **4805 ns/op** | 性能优异。 | | `AES_GCM` | **4854 ns/op** | 性能优异。 |
| `RSA_Sign` | **1349477 ns/op** | 复合 RSA 标准耗时。 | | `RSA_Sign` | **1407588 ns/op** | 符合 RSA 标准耗时。 |
| `Ed25519_Sign` | **26689 ns/op** | 高性能签名。 | | `Ed25519_Sign` | **28299 ns/op** | 高性能签名。 |
| `X25519_Encrypt` | **204639 ns/op** | 混合加密模式性能表现。 |

12
aes.go
View File

@ -39,6 +39,18 @@ func NewAESGCMWithoutEraseKey(key, iv []byte) (*Symmetric, error) {
return NewSymmetricWithoutEraseKey(AESGCM, key, iv) 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) { func (c *AESCipher) Encrypt(data []byte, key []byte, iv []byte) ([]byte, error) {
block, err := aes.NewCipher(key) block, err := aes.NewCipher(key)
if err != nil { if err != nil {

69
default.go Normal file
View File

@ -0,0 +1,69 @@
package crypto
import (
"sync"
)
var (
// defaultAES 用于配置解密的默认 AES 实例 (私有)
defaultAES *Symmetric
defaultAESHandlers []func(*Symmetric)
defaultAESLock sync.RWMutex
defaultAESOnce sync.Once
isDefaultAESSet bool
isLocked bool
)
func init() {
// 默认硬编码密钥,仅用于基础防护。
defaultAES, _ = NewAESGCMWithoutEraseKey(
[]byte("?GQ$0K0GgLdO=f+~L68PLm$uhKr4'=tV"),
[]byte("VFs7@sK61cj^f?HZ"),
)
}
// OnSetDefaultAES 注册配置解密 AES 实例变更的回调。
// 即使在 SetDefaultAES 调用之后,此方法仍然有效,直到调用 LockDefaultAES。
// 注册时会立即以当前实例(硬编码或已注入的生产密钥)调用一次回调。
func OnSetDefaultAES(h func(*Symmetric)) {
defaultAESLock.Lock()
defer defaultAESLock.Unlock()
if isLocked {
// 如果已经锁定,不再允许注册新的回调
return
}
defaultAESHandlers = append(defaultAESHandlers, h)
if defaultAES != nil {
h(defaultAES)
}
}
// SetDefaultAES 设置全局默认 AES 密钥与 IV。
// 仅允许调用一次。调用后会立即锁定注册通道,后续 OnSetDefaultAES 将不再起作用。
// 注意:此方法会自动擦除 (ZeroMemory) 传入的 key 和 iv 字节切片。
func SetDefaultAES(key, iv []byte) {
defaultAESOnce.Do(func() {
// 创建新实例并擦除原始密钥
newAES, err := NewAESGCMAndEraseKey(key, iv)
if err != nil {
return
}
defaultAESLock.Lock()
defer defaultAESLock.Unlock()
defaultAES = newAES
isLocked = true
// 通知所有在初始化阶段注册的观察者
for _, h := range defaultAESHandlers {
h(defaultAES)
}
// 彻底清理并释放引用
defaultAESHandlers = nil
})
}

54
default_test.go Normal file
View File

@ -0,0 +1,54 @@
package crypto
import (
"bytes"
"testing"
)
func TestDefaultAES(t *testing.T) {
// 1. 测试初始默认值
var confAES *Symmetric
OnSetDefaultAES(func(aes *Symmetric) {
confAES = aes
})
if confAES == nil {
t.Fatal("confAES should be initialized by OnSetDefaultAES")
}
// 2. 测试 SetDefaultAES 触发更新与锁定
rawKey := []byte("12345678901234567890123456789012")
newKey := bytes.Clone(rawKey)
newIv := []byte("123456789012")
SetDefaultAES(newKey, newIv)
// 验证密钥已被擦除 (ZeroMemory 会用随机 junk 覆盖,所以检查是否不再等于原始值)
if bytes.Equal(newKey, rawKey) {
t.Error("newKey should be overwritten after SetDefaultAES")
}
// 此时 confAES 应该已经被回调更新了
data := []byte("hello world")
encrypted, err := confAES.EncryptAndErase(bytes.Clone(data))
if err != nil {
t.Fatalf("Encrypt failed: %v", err)
}
// 3. 测试安全性SetDefaultAES 之后不再允许 OnSetDefaultAES
var blockedAES *Symmetric
OnSetDefaultAES(func(aes *Symmetric) {
blockedAES = aes
})
if blockedAES != nil {
t.Error("OnSetDefaultAES should be blocked after SetDefaultAES (auto-lock)")
}
// 4. 测试 SetDefaultAES 仅允许一次
anotherKey := []byte("another key 32 bytes long.......")
SetDefaultAES(anotherKey, newIv)
// 验证密钥没有改变(通过解密验证)
_, err = confAES.DecryptBytes(encrypted)
if err != nil {
t.Errorf("Decryption should still work with the first injected key: %v", err)
}
}

View File

@ -8,6 +8,7 @@ import (
"crypto/x509" "crypto/x509"
"errors" "errors"
"io" "io"
"math/big"
"apigo.cc/go/safe" "apigo.cc/go/safe"
"golang.org/x/crypto/hkdf" "golang.org/x/crypto/hkdf"
@ -35,6 +36,50 @@ func NewECDSAWithoutEraseKey(safePrivateKeyBuf, safePublicKeyBuf []byte) (*Asymm
return NewAsymmetricWithoutEraseKey(ECDSAGCM, safePrivateKeyBuf, safePublicKeyBuf, false) 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 { func NewECDSAAlgorithm(useGCM bool, hash crypto.Hash, kdfInfo, kdfSalt []byte) *ECDSAAlgorithm {
return &ECDSAAlgorithm{UseGCM: useGCM, Hash: hash, KdfInfo: kdfInfo, KdfSalt: kdfSalt} return &ECDSAAlgorithm{UseGCM: useGCM, Hash: hash, KdfInfo: kdfInfo, KdfSalt: kdfSalt}
} }

View File

@ -23,6 +23,14 @@ func NewED25519WithoutEraseKey(safePrivateKeyBuf, safePublicKeyBuf []byte) (*Asy
return NewAsymmetricWithoutEraseKey(ED25519, safePrivateKeyBuf, safePublicKeyBuf, false) 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) { func GenerateEd25519KeyPair() ([]byte, []byte, error) {
pubKey, privKey, err := ed25519.GenerateKey(rand.Reader) pubKey, privKey, err := ed25519.GenerateKey(rand.Reader)
if err != nil { if err != nil {

18
go.mod
View File

@ -3,19 +3,13 @@ module apigo.cc/go/crypto
go 1.25.0 go 1.25.0
require ( require (
apigo.cc/go/encoding v1.0.5 apigo.cc/go/cast v1.3.3
apigo.cc/go/safe v1.0.5 apigo.cc/go/encoding v1.3.1
golang.org/x/crypto v0.50.0 apigo.cc/go/safe v1.3.1
golang.org/x/crypto v0.51.0
) )
require ( require (
apigo.cc/go/cast v1.2.8 // indirect apigo.cc/go/rand v1.3.1 // indirect
apigo.cc/go/rand v1.0.5 // indirect golang.org/x/sys v0.44.0 // indirect
golang.org/x/sys v0.43.0 // indirect
) )
replace apigo.cc/go/encoding => ../encoding
replace apigo.cc/go/safe => ../safe
replace apigo.cc/go/rand => ../rand

18
go.sum
View File

@ -1,6 +1,12 @@
apigo.cc/go/cast v1.2.8 h1:plb676DH2TjYljzf8OEMGT6lIhmZ/xaxEFfs0kDOiSI= apigo.cc/go/cast v1.3.3 h1:aln5eDR5DZVWVzZ/y5SJh1gQNgWv2sT82I25NaO9g34=
apigo.cc/go/cast v1.2.8/go.mod h1:lGlwImiOvHxG7buyMWhFzcdvQzmSaoKbmr7bcDfUpHk= apigo.cc/go/cast v1.3.3/go.mod h1:lGlwImiOvHxG7buyMWhFzcdvQzmSaoKbmr7bcDfUpHk=
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= apigo.cc/go/encoding v1.3.1 h1:y8O58KYAyulkThg1O2ji2BqjnFoSvk42sit9I3z+K7Y=
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= apigo.cc/go/encoding v1.3.1/go.mod h1:xAJk5b83VZ31mXMTnyp0dfMoBKfT/AHDn0u+cQfojgY=
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= apigo.cc/go/rand v1.3.1 h1:7FvsI6PtQ5XrWER0dTiLVo0p7GIxRidT/TBKhVy93j8=
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= apigo.cc/go/rand v1.3.1/go.mod h1:mZ/4Soa3bk+XvDaqPWJuUe1bfEi4eThBj1XmEAuYxsk=
apigo.cc/go/safe v1.3.1 h1:irTCqPAC97gGsX/Lw5AzLelDt1xXLEZIAaVhLELWe9Q=
apigo.cc/go/safe v1.3.1/go.mod h1:XdOpBhN2vkImalaykYXXmEpczqWa1y3ah6/Q72cdRqE=
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=

30
password.go Normal file
View 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
View 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
View File

@ -33,6 +33,30 @@ func NewRSAWithoutEraseKey(safePrivateKeyBuf, safePublicKeyBuf []byte) (*Asymmet
return NewAsymmetricWithoutEraseKey(RSA, safePrivateKeyBuf, safePublicKeyBuf, false) 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) { func GenerateRSAKeyPair(bitSize int) ([]byte, []byte, error) {
if bitSize < 2048 { if bitSize < 2048 {
bitSize = 2048 bitSize = 2048

View File

@ -33,6 +33,16 @@ func NewX25519WithoutEraseKey(safePrivateKeyBuf, safePublicKeyBuf []byte) (*Asym
return NewAsymmetricWithoutEraseKey(X25519GCM, safePrivateKeyBuf, safePublicKeyBuf, false) 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 { func NewX25519Algorithm(useGCM bool, hash crypto.Hash, kdfInfo, kdfSalt []byte) *X25519Algorithm {
return &X25519Algorithm{UseGCM: useGCM, Hash: hash, KdfInfo: kdfInfo, KdfSalt: kdfSalt} return &X25519Algorithm{UseGCM: useGCM, Hash: hash, KdfInfo: kdfInfo, KdfSalt: kdfSalt}
} }