continuous-integration/drone Build is failing
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.
107 lines
2.9 KiB
Go
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
|
|
}
|