Files

103 lines
2.8 KiB
Go

package backup
import (
"bytes"
"crypto/rand"
"errors"
"os"
"path/filepath"
"syscall"
"golang.org/x/crypto/argon2"
)
const passwordMagic = "DSHPASS\x01"
const MaxEncryptedBytes int64 = int64(maxArchiveBytes + len(passwordMagic) + 16)
// The format fixes KDF costs; untrusted archives cannot request arbitrary memory.
func passwordKey(password string, salt []byte) ([]byte, error) {
if len(password) == 0 || len(password) > 1024 {
return nil, errors.New("backup password required (maximum 1024 bytes)")
}
return argon2.IDKey([]byte(password), salt, 3, 64*1024, 2, 32), nil
}
func CreateWithPassword(home, destination, password string, metadata Metadata) error {
salt := make([]byte, 16)
if _, err := rand.Read(salt); err != nil {
return err
}
key, err := passwordKey(password, salt)
if err != nil {
return err
}
defer clear(key)
dir, err := os.MkdirTemp(filepath.Dir(destination), ".password-backup-")
if err != nil {
return err
}
defer os.RemoveAll(dir)
inner := filepath.Join(dir, "archive")
if err = CreateWithMetadata(home, inner, key, metadata); err != nil {
return err
}
data, err := os.ReadFile(inner)
if err != nil {
return err
}
data = append(append([]byte(passwordMagic), salt...), data...)
root, err := os.OpenRoot(filepath.Dir(destination))
if err != nil {
return err
}
defer root.Close()
return replace(root, filepath.Base(destination), data)
}
// Prepare authenticates and validates into a private staging home before the caller
// stops a container or snapshots its live home. Only ciphertext is retained.
func Prepare(archive, password string, legacyKey []byte) (restore func(string) error, cleanup func(), err error) {
f, err := os.OpenFile(archive, os.O_RDONLY|syscall.O_NOFOLLOW|syscall.O_NONBLOCK, 0)
if err != nil {
return nil, nil, err
}
data, err := readBounded(f, MaxEncryptedBytes)
f.Close()
if err != nil {
return nil, nil, err
}
key := append([]byte(nil), legacyKey...)
if bytes.HasPrefix(data, []byte(passwordMagic)) {
if len(data) < len(passwordMagic)+16 {
return nil, nil, errors.New("truncated backup")
}
key, err = passwordKey(password, data[len(passwordMagic):len(passwordMagic)+16])
if err != nil {
return nil, nil, err
}
data = data[len(passwordMagic)+16:]
}
dir, err := os.MkdirTemp("", "dsh-backup-validate-")
if err != nil {
clear(key)
return nil, nil, err
}
cleanup = func() { clear(key); os.RemoveAll(dir) }
inner := filepath.Join(dir, "archive")
if err = os.WriteFile(inner, data, 0600); err == nil {
err = os.Mkdir(filepath.Join(dir, "home"), 0700)
}
if err == nil {
err = Restore(inner, filepath.Join(dir, "home"), key)
}
if err != nil {
cleanup()
return nil, nil, err
}
if err = os.RemoveAll(filepath.Join(dir, "home")); err != nil {
cleanup()
return nil, nil, err
}
return func(home string) error { return Restore(inner, home, key) }, cleanup, nil
}