keys/lib/password.go

185 lines
4.6 KiB
Go
Raw Normal View History

2026-05-10 15:53:01 +08:00
package lib
import (
"errors"
"os"
"path/filepath"
"strings"
"apigo.cc/go/crypto"
"apigo.cc/go/crypto-sm"
"apigo.cc/go/file"
"apigo.cc/go/safe"
)
// PwdNameToPath returns the name as is, allowing subdirectories via slashes.
func PwdNameToPath(pwdName string) string {
return pwdName
}
// SavePassword encrypts and saves a password value associated with a specific key.
func SavePassword(keyName, pwdName string, data []byte, activeKey, activeIV []byte, algo string) error {
if keyName == "" || pwdName == "" {
return errors.New("key name and password name cannot be empty")
}
basePath := GetPasswordPath(keyName)
relPath := PwdNameToPath(pwdName)
filePath := filepath.Join(basePath, relPath)
dir := filepath.Dir(filePath)
if err := EnsureDir(dir); err != nil {
return err
}
fileIv, err := CryptoRandBytes(12)
if err != nil {
return err
}
dummySalt, _ := CryptoRandBytes(16)
sym, err := createSymmetric(activeKey, activeIV, fileIv, algo)
if err != nil {
return err
}
encData, err := sym.EncryptBytes(data)
if err != nil {
return err
}
header, cipherPadding, err := PackFileHeader(dummySalt, fileIv)
if err != nil {
return err
}
paddingBuf, _ := CryptoRandBytes(cipherPadding)
finalBuf := append([]byte(nil), header...)
finalBuf = append(finalBuf, paddingBuf...)
finalBuf = append(finalBuf, encData...)
return file.WriteBytes(filePath, finalBuf)
}
// LoadPassword loads and decrypts a password value.
func LoadPassword(keyName, pwdName string, activeKey, activeIV []byte, algo string) ([]byte, error) {
basePath := GetPasswordPath(keyName)
relPath := PwdNameToPath(pwdName)
filePath := filepath.Join(basePath, relPath)
if !file.Exists(filePath) {
return nil, errors.New("password not found")
}
data, err := file.ReadBytes(filePath)
if err != nil {
return nil, err
}
if len(data) < FileHeaderSize {
return nil, errors.New("invalid password file format")
}
header := data[:FileHeaderSize]
_, fileIv, cipherPadding, err := UnpackFileHeader(header)
if err != nil {
return nil, errors.New("invalid password file header")
}
cipherStart := FileHeaderSize + cipherPadding
if len(data) <= cipherStart {
return nil, errors.New("invalid password file format (no ciphertext)")
}
cipherText := data[cipherStart:]
sym, err := createSymmetric(activeKey, activeIV, fileIv, algo)
if err != nil {
return nil, err
}
dec, err := sym.DecryptBytes(cipherText)
if err != nil {
// --- 🛡️ 诱饵模式 (Decoy Mode) 🛡️ ---
// 解密失败不报错,而是返回一段随机的 12-24 位字符
// 让攻击者无法确定是算法选错了、Key错了还是密码本身就是这样。
return CryptoRandDecoy(12, 24), nil
}
return dec, nil
}
func createSymmetric(key, keyIv, fileIv []byte, algo string) (*crypto.Symmetric, error) {
k := append([]byte(nil), key...)
v := append([]byte(nil), fileIv...)
defer safe.ZeroMemory(k)
defer safe.ZeroMemory(v)
switch strings.ToLower(algo) {
case "aes-gcm", "gcm", "":
return crypto.NewAESGCMWithoutEraseKey(k, v)
case "aes-cbc", "cbc":
return crypto.NewAESCBCWithoutEraseKey(k, append(v, 0, 0, 0, 0))
case "sm4-gcm", "sm4":
return sm.NewSM4GCMWithoutEraseKey(k, v)
case "sm4-cbc":
return sm.NewSM4CBCWithoutEraseKey(k, append(v, 0, 0, 0, 0))
default:
return nil, errors.New("unsupported algorithm: " + algo)
}
}
// CryptoRandDecoy generates a random string for decoy purposes.
func CryptoRandDecoy(min, max int) []byte {
chars := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*"
n := min
if max > min {
rb, _ := CryptoRandBytes(1)
n = min + (int(rb[0]) % (max - min + 1))
}
res := make([]byte, n)
rb, _ := CryptoRandBytes(n)
for i := 0; i < n; i++ {
res[i] = chars[int(rb[i])%len(chars)]
}
return res
}
// RemovePassword deletes a password file.
func RemovePassword(keyName, pwdName string) error {
basePath := GetPasswordPath(keyName)
relPath := PwdNameToPath(pwdName)
filePath := filepath.Join(basePath, relPath)
if !file.Exists(filePath) {
return errors.New("password not found")
}
return os.Remove(filePath)
}
// ListPasswords lists all passwords under a key, returning them as path names.
func ListPasswords(keyName string) ([]string, error) {
basePath := GetPasswordPath(keyName)
if !file.Exists(basePath) {
return nil, nil // No passwords yet
}
var passwords []string
err := filepath.Walk(basePath, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() {
rel, err := filepath.Rel(basePath, path)
if err == nil {
passwords = append(passwords, rel)
}
}
return nil
})
return passwords, err
}