keys/lib/password.go
2026-05-10 15:53:01 +08:00

185 lines
4.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}