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 }