mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-05-12 00:59:41 +08:00
- 新增 v2 连接恢复包 appKey 与文件密码双模式加密链路 - 扩展前后端导入导出流程并兼容 v1 与 legacy 格式 - 修复无文件密码恢复包导入误弹密码框导致的流程阻塞
197 lines
5.7 KiB
Go
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
|
|
}
|