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
|
|||
|
|
}
|