package main import ( "archive/tar" "bufio" "compress/gzip" "crypto/sha256" "encoding/hex" "flag" "fmt" "io" "os" "path/filepath" "strings" "time" ) // cmdBackup creates a backup of the password store func cmdBackup(args []string) { fs := flag.NewFlagSet("backup", flag.ExitOnError) outputFlag := fs.String("output", "", "output backup file path (default: passage-backup-YYYYMMDD-HHMMSS.tar.gz)") outputShort := fs.String("o", "", "output backup file path (short)") if err := fs.Parse(args); err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) os.Exit(1) } storeDir := getStoreDir() if !checkStoreInitialized(storeDir) { fmt.Fprintf(os.Stderr, "Error: password store not initialized. Run 'passage init' first\n") os.Exit(1) } // Determine output file outputFile := *outputFlag if outputFile == "" { outputFile = *outputShort } if outputFile == "" { // Generate default filename with timestamp timestamp := time.Now().Format("20060102-150405") outputFile = fmt.Sprintf("passage-backup-%s.tar.gz", timestamp) } // Validate output path if err := checkSneakyPaths(outputFile); err != nil { fmt.Fprintf(os.Stderr, "Error: invalid output path: %v\n", err) os.Exit(1) } // Create backup file backupFile, err := os.Create(outputFile) if err != nil { fmt.Fprintf(os.Stderr, "Error: failed to create backup file: %v\n", err) os.Exit(1) } defer backupFile.Close() // Create gzip writer gzipWriter := gzip.NewWriter(backupFile) defer gzipWriter.Close() // Create tar writer tarWriter := tar.NewWriter(gzipWriter) defer tarWriter.Close() // Calculate checksum while writing hash := sha256.New() multiWriter := io.MultiWriter(tarWriter, hash) // Walk the store directory and add files to tar fileCount := 0 err = filepath.Walk(storeDir, func(path string, info os.FileInfo, err error) error { if err != nil { return err } // Skip .git - it's usually huge and users can backup it separately if they want if info.IsDir() && strings.Contains(path, ".git") { return filepath.SkipDir } // Skip directories themselves if info.IsDir() { return nil } // Calculate relative path relPath, err := filepath.Rel(storeDir, path) if err != nil { return err } // Create tar header header, err := tar.FileInfoHeader(info, "") if err != nil { return err } header.Name = relPath header.Mode = int64(info.Mode()) // Write header if err := tarWriter.WriteHeader(header); err != nil { return err } // Open file and copy contents file, err := os.Open(path) if err != nil { return err } defer file.Close() // Copy file contents through multi-writer (to tar and hash) if _, err := io.Copy(multiWriter, file); err != nil { return err } fileCount++ return nil }) if err != nil { fmt.Fprintf(os.Stderr, "Error: failed to create backup: %v\n", err) os.Exit(1) } // Finalize tar and gzip tarWriter.Close() gzipWriter.Close() backupFile.Close() // Calculate final checksum checksum := hex.EncodeToString(hash.Sum(nil)) // Write checksum file for verification later checksumFile := outputFile + ".sha256" if err := os.WriteFile(checksumFile, []byte(checksum+" "+filepath.Base(outputFile)+"\n"), 0644); err != nil { // Checksum file failed? Not critical, but warn about it fmt.Fprintf(os.Stderr, "Warning: failed to write checksum file: %v\n", err) } fmt.Printf("Backup created: %s\n", outputFile) fmt.Printf("Files backed up: %d\n", fileCount) fmt.Printf("Checksum: %s\n", checksum) fmt.Printf("Checksum file: %s\n", checksumFile) } // cmdRestore restores a backup of the password store func cmdRestore(args []string) { fs := flag.NewFlagSet("restore", flag.ExitOnError) force := fs.Bool("force", false, "overwrite existing store without prompting") forceShort := fs.Bool("f", false, "overwrite existing store without prompting (short)") skipVerify := fs.Bool("skip-verify", false, "skip checksum verification") skipVerifyShort := fs.Bool("s", false, "skip checksum verification (short)") if err := fs.Parse(args); err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) os.Exit(1) } if fs.NArg() != 1 { fmt.Fprintf(os.Stderr, "Usage: passage restore [--force] [--skip-verify] backup-file.tar.gz\n") os.Exit(1) } backupFile := fs.Arg(0) // Validate backup file path if err := checkSneakyPaths(backupFile); err != nil { fmt.Fprintf(os.Stderr, "Error: invalid backup file path: %v\n", err) os.Exit(1) } // Check if backup file exists if _, err := os.Stat(backupFile); err != nil { fmt.Fprintf(os.Stderr, "Error: backup file not found: %s\n", backupFile) os.Exit(1) } // Verify checksum if not skipped (you really shouldn't skip this) if !*skipVerify && !*skipVerifyShort { checksumFile := backupFile + ".sha256" if _, err := os.Stat(checksumFile); err == nil { expectedChecksumData, err := os.ReadFile(checksumFile) if err == nil { lines := strings.Split(strings.TrimSpace(string(expectedChecksumData)), " ") if len(lines) >= 1 { expectedChecksum := strings.TrimSpace(lines[0]) if !verifyBackupChecksum(backupFile, expectedChecksum) { fmt.Fprintf(os.Stderr, "Error: backup file checksum verification failed!\n") fmt.Fprintf(os.Stderr, "Use --skip-verify to restore anyway (not recommended)\n") os.Exit(1) } fmt.Printf("Checksum verified: OK\n") } } } else { // No checksum file? That's suspicious - require explicit skip fmt.Fprintf(os.Stderr, "Warning: checksum file not found: %s\n", checksumFile) fmt.Fprintf(os.Stderr, "Use --skip-verify to restore without verification\n") os.Exit(1) } } storeDir := getStoreDir() // Check if store already exists if checkStoreInitialized(storeDir) { if !*force && !*forceShort { fmt.Printf("Warning: password store already exists at %s\n", storeDir) fmt.Printf("Restoring will overwrite existing files. Continue? [y/N] ") reader := bufio.NewReader(os.Stdin) response, _ := reader.ReadString('\n') response = strings.TrimSpace(strings.ToLower(response)) if response != "y" && response != "yes" { os.Exit(1) } } } // Open backup file file, err := os.Open(backupFile) if err != nil { fmt.Fprintf(os.Stderr, "Error: failed to open backup file: %v\n", err) os.Exit(1) } defer file.Close() // Create gzip reader gzipReader, err := gzip.NewReader(file) if err != nil { fmt.Fprintf(os.Stderr, "Error: failed to read gzip archive: %v\n", err) os.Exit(1) } defer gzipReader.Close() // Create tar reader tarReader := tar.NewReader(gzipReader) // Extract files one by one fileCount := 0 for { header, err := tarReader.Next() if err == io.EOF { break // All done } if err != nil { fmt.Fprintf(os.Stderr, "Error: failed to read tar archive: %v\n", err) os.Exit(1) } // Paranoid path check - even in backups, we don't trust paths if err := checkSneakyPaths(header.Name); err != nil { fmt.Fprintf(os.Stderr, "Error: invalid path in backup: %v\n", err) os.Exit(1) } // Construct full path targetPath := filepath.Join(storeDir, header.Name) // Create parent directories if err := os.MkdirAll(filepath.Dir(targetPath), 0700); err != nil { fmt.Fprintf(os.Stderr, "Error: failed to create directory: %v\n", err) os.Exit(1) } // Create file outFile, err := os.OpenFile(targetPath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, os.FileMode(header.Mode)) if err != nil { fmt.Fprintf(os.Stderr, "Error: failed to create file: %v\n", err) os.Exit(1) } // Copy file contents if _, err := io.Copy(outFile, tarReader); err != nil { outFile.Close() fmt.Fprintf(os.Stderr, "Error: failed to write file: %v\n", err) os.Exit(1) } outFile.Close() fileCount++ } fmt.Printf("Restore completed: %d files restored\n", fileCount) fmt.Printf("Store location: %s\n", storeDir) } // verifyBackupChecksum verifies the SHA256 checksum of a backup file func verifyBackupChecksum(backupFile, expectedChecksum string) bool { file, err := os.Open(backupFile) if err != nil { return false } defer file.Close() hash := sha256.New() if _, err := io.Copy(hash, file); err != nil { return false } actualChecksum := hex.EncodeToString(hash.Sum(nil)) return constantTimeStringCompare(actualChecksum, expectedChecksum) }