Files
曾志威 9da95beb42
continuous-integration/drone Build is failing
feat: refine management UI, connection diagnostics and local startup
Migrate the workspace to React and Ant Design, improve account forms and responsive layouts, support outbound diagnostics and SSH fingerprint confirmation, and persist the management startup port. Preserve database migrations, remove S3 settings storage, and improve gateway audit handling.
2026-10-13 19:43:23 +08:00

107 lines
2.9 KiB
Go

package store
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha256"
"encoding/json"
"errors"
"golang.org/x/crypto/pbkdf2"
)
// The codec owns the portable encryption envelope. Repository callers supply
// the snapshot only after passphrase validation and key derivation succeed.
type EncryptedBackup struct {
Version int `json:"version"`
Salt []byte `json:"salt"`
Nonce []byte `json:"nonce"`
Ciphertext []byte `json:"ciphertext"`
}
const backupPurpose = "linkport-backup-v1"
func backupAEAD(pass string, salt []byte) (cipher.AEAD, error) {
if len(pass) < 12 || len(pass) > 1024 {
return nil, errors.New("backup passphrase must contain 12-1024 bytes")
}
key := pbkdf2.Key([]byte(pass), salt, 600000, 32, sha256.New)
block, e := aes.NewCipher(key)
if e != nil {
return nil, e
}
return cipher.NewGCM(block)
}
func sealBackup(pass string, load func() (Bundle, error)) (EncryptedBackup, error) {
b := EncryptedBackup{Version: 1, Salt: make([]byte, 16), Nonce: make([]byte, 12)}
if len(pass) < 12 || len(pass) > 1024 {
return b, errors.New("backup passphrase must contain 12-1024 bytes")
}
if _, e := rand.Read(b.Salt); e != nil {
return b, e
}
if _, e := rand.Read(b.Nonce); e != nil {
return b, e
}
a, e := backupAEAD(pass, b.Salt)
if e != nil {
return b, e
}
bundle, e := load()
if e != nil {
return b, e
}
raw, e := json.Marshal(bundle)
if e != nil {
return b, e
}
if len(raw) > (16<<20)-a.Overhead() {
return b, errors.New("configuration exceeds the 16 MiB backup limit")
}
b.Ciphertext = a.Seal(nil, b.Nonce, raw, []byte(backupPurpose))
return b, nil
}
func OpenBackup(b EncryptedBackup, pass string) (Bundle, error) {
if b.Version != 1 || len(b.Salt) != 16 || len(b.Nonce) != 12 || len(b.Ciphertext) < 16 || len(b.Ciphertext) > 16<<20 {
return Bundle{}, errors.New("invalid backup envelope")
}
a, e := backupAEAD(pass, b.Salt)
if e != nil {
return Bundle{}, e
}
raw, e := a.Open(nil, b.Nonce, b.Ciphertext, []byte(backupPurpose))
if e != nil {
return Bundle{}, errors.New("incorrect passphrase or damaged backup")
}
var bundle Bundle
e = json.Unmarshal(raw, &bundle)
if e == nil && bundle.Version != 2 {
e = errors.New("unsupported configuration version")
}
return bundle, e
}
func SealSyncBundle(b Bundle, pass string) (EncryptedBackup, error) {
envelope := EncryptedBackup{Version: 1, Salt: make([]byte, 16), Nonce: make([]byte, 12)}
if _, err := rand.Read(envelope.Salt); err != nil {
return envelope, err
}
if _, err := rand.Read(envelope.Nonce); err != nil {
return envelope, err
}
a, err := backupAEAD(pass, envelope.Salt)
if err != nil {
return envelope, err
}
raw, err := json.Marshal(b)
if err != nil {
return envelope, err
}
if len(raw) > (16<<20)-a.Overhead() {
return envelope, errors.New("同步配置超过 16 MiB")
}
envelope.Ciphertext = a.Seal(nil, envelope.Nonce, raw, []byte(backupPurpose))
return envelope, nil
}