185 lines
4.6 KiB
Go
185 lines
4.6 KiB
Go
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
|
||
}
|