Initial commit: DSH Web Manager
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user