Files
MyGoNavi/internal/app/connection_package_appkey.go
tianqijiuyun-latiao 52d2ee7592 feat(connection-package): 支持连接恢复包双模式加密导入导出
- 新增 v2 连接恢复包 appKey 与文件密码双模式加密链路
- 扩展前后端导入导出流程并兼容 v1 与 legacy 格式
- 修复无文件密码恢复包导入误弹密码框导致的流程阻塞
2026-04-11 23:51:43 +08:00

197 lines
5.7 KiB
Go

package app
import (
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"errors"
"strings"
"sync"
"golang.org/x/crypto/argon2"
)
const (
connectionPackageAppKeyPurpose = "gonavi-export-key-v2"
connectionPackageAppKeyFallbackSeed = "gonavi-connection-package-v2-seed"
connectionPackageAppKeyFallbackSalt = "gonavi-connection-package-v2-salt"
)
var (
connectionPackageAppKeySeed string
connectionPackageAppKeySalt string
connectionPackageAppKeyMu sync.Mutex
connectionPackageAppKeyCached []byte
)
func deriveConnectionPackageAppKey() ([]byte, error) {
connectionPackageAppKeyMu.Lock()
defer connectionPackageAppKeyMu.Unlock()
if len(connectionPackageAppKeyCached) == connectionPackageAES256KeyBytes {
return append([]byte(nil), connectionPackageAppKeyCached...), nil
}
seed := strings.TrimSpace(connectionPackageAppKeySeed)
if seed == "" {
seed = connectionPackageAppKeyFallbackSeed
}
saltValue := strings.TrimSpace(connectionPackageAppKeySalt)
if saltValue == "" {
saltValue = connectionPackageAppKeyFallbackSalt
}
mac := hmac.New(sha256.New, []byte(seed))
if _, err := mac.Write([]byte(connectionPackageAppKeyPurpose)); err != nil {
return nil, err
}
intermediate := mac.Sum(nil)
saltHash := sha256.Sum256([]byte(saltValue))
key := argon2.IDKey(
intermediate,
saltHash[:connectionPackageSaltBytes],
connectionPackageKDFDefaultTimeCost,
connectionPackageKDFDefaultMemoryKiB,
connectionPackageKDFDefaultParallelism,
connectionPackageAES256KeyBytes,
)
connectionPackageAppKeyCached = append([]byte(nil), key...)
return append([]byte(nil), key...), nil
}
func resetConnectionPackageAppKeyCache() {
connectionPackageAppKeyMu.Lock()
defer connectionPackageAppKeyMu.Unlock()
connectionPackageAppKeyCached = nil
}
func encryptSecretField(appKey []byte, plaintext string, aad string) (string, error) {
if plaintext == "" {
return "", nil
}
aead, err := newConnectionPackageAEAD(appKey)
if err != nil {
return "", err
}
nonce := make([]byte, connectionPackageNonceBytes)
if _, err := rand.Read(nonce); err != nil {
return "", err
}
ciphertext := aead.Seal(nil, nonce, []byte(plaintext), []byte(aad))
encoded := make([]byte, 0, len(nonce)+len(ciphertext))
encoded = append(encoded, nonce...)
encoded = append(encoded, ciphertext...)
return base64.StdEncoding.EncodeToString(encoded), nil
}
func decryptSecretField(appKey []byte, encrypted string, aad string) (string, error) {
if encrypted == "" {
return "", nil
}
raw, err := base64.StdEncoding.DecodeString(encrypted)
if err != nil {
return "", err
}
if len(raw) <= connectionPackageNonceBytes {
return "", errors.New("invalid encrypted secret")
}
aead, err := newConnectionPackageAEAD(appKey)
if err != nil {
return "", err
}
plain, err := aead.Open(nil, raw[:connectionPackageNonceBytes], raw[connectionPackageNonceBytes:], []byte(aad))
if err != nil {
return "", err
}
return string(plain), nil
}
func encryptSecretBundle(appKey []byte, bundle connectionSecretBundle, connectionID string) (connectionSecretBundle, error) {
var encrypted connectionSecretBundle
var err error
encrypted.Password, err = encryptSecretField(appKey, bundle.Password, connectionID)
if err != nil {
return connectionSecretBundle{}, err
}
encrypted.SSHPassword, err = encryptSecretField(appKey, bundle.SSHPassword, connectionID)
if err != nil {
return connectionSecretBundle{}, err
}
encrypted.ProxyPassword, err = encryptSecretField(appKey, bundle.ProxyPassword, connectionID)
if err != nil {
return connectionSecretBundle{}, err
}
encrypted.HTTPTunnelPassword, err = encryptSecretField(appKey, bundle.HTTPTunnelPassword, connectionID)
if err != nil {
return connectionSecretBundle{}, err
}
encrypted.MySQLReplicaPassword, err = encryptSecretField(appKey, bundle.MySQLReplicaPassword, connectionID)
if err != nil {
return connectionSecretBundle{}, err
}
encrypted.MongoReplicaPassword, err = encryptSecretField(appKey, bundle.MongoReplicaPassword, connectionID)
if err != nil {
return connectionSecretBundle{}, err
}
encrypted.OpaqueURI, err = encryptSecretField(appKey, bundle.OpaqueURI, connectionID)
if err != nil {
return connectionSecretBundle{}, err
}
encrypted.OpaqueDSN, err = encryptSecretField(appKey, bundle.OpaqueDSN, connectionID)
if err != nil {
return connectionSecretBundle{}, err
}
return encrypted, nil
}
func decryptSecretBundle(appKey []byte, bundle connectionSecretBundle, connectionID string) (connectionSecretBundle, error) {
var decrypted connectionSecretBundle
var err error
decrypted.Password, err = decryptSecretField(appKey, bundle.Password, connectionID)
if err != nil {
return connectionSecretBundle{}, err
}
decrypted.SSHPassword, err = decryptSecretField(appKey, bundle.SSHPassword, connectionID)
if err != nil {
return connectionSecretBundle{}, err
}
decrypted.ProxyPassword, err = decryptSecretField(appKey, bundle.ProxyPassword, connectionID)
if err != nil {
return connectionSecretBundle{}, err
}
decrypted.HTTPTunnelPassword, err = decryptSecretField(appKey, bundle.HTTPTunnelPassword, connectionID)
if err != nil {
return connectionSecretBundle{}, err
}
decrypted.MySQLReplicaPassword, err = decryptSecretField(appKey, bundle.MySQLReplicaPassword, connectionID)
if err != nil {
return connectionSecretBundle{}, err
}
decrypted.MongoReplicaPassword, err = decryptSecretField(appKey, bundle.MongoReplicaPassword, connectionID)
if err != nil {
return connectionSecretBundle{}, err
}
decrypted.OpaqueURI, err = decryptSecretField(appKey, bundle.OpaqueURI, connectionID)
if err != nil {
return connectionSecretBundle{}, err
}
decrypted.OpaqueDSN, err = decryptSecretField(appKey, bundle.OpaqueDSN, connectionID)
if err != nil {
return connectionSecretBundle{}, err
}
return decrypted, nil
}