package backup import ( "context" "crypto/rand" "encoding/hex" "fmt" "time" "git.tswf.io/infra/go-synapse-backupper/pkg/adapters/config" "git.tswf.io/infra/go-synapse-backupper/pkg/adapters/crypto/composite" "git.tswf.io/infra/go-synapse-backupper/pkg/adapters/crypto/keymanager" "git.tswf.io/infra/go-synapse-backupper/pkg/adapters/crypto/mlkem768" "git.tswf.io/infra/go-synapse-backupper/pkg/adapters/crypto/x25519" adapterpgdump "git.tswf.io/infra/go-synapse-backupper/pkg/adapters/pgdump" "git.tswf.io/infra/go-synapse-backupper/pkg/adapters/pipeline" "git.tswf.io/infra/go-synapse-backupper/pkg/adapters/retention" "git.tswf.io/infra/go-synapse-backupper/pkg/adapters/storage/local" domaincrypto "git.tswf.io/infra/go-synapse-backupper/pkg/domain/crypto" domainpgdump "git.tswf.io/infra/go-synapse-backupper/pkg/domain/pgdump" ) func RunOnce(ctx context.Context, cfg *config.Config) error { registry := domaincrypto.NewRegistry() if err := registry.Register(0x0006, func() domaincrypto.KEM { return mlkem768.New() }); err != nil { return fmt.Errorf("register mlkem768: %w", err) } if err := registry.Register(0x0007, func() domaincrypto.KEM { return x25519.New() }); err != nil { return fmt.Errorf("register x25519: %w", err) } keyMgr := keymanager.NewKeyManager(registry) pqPub, err := keyMgr.LoadPub(cfg.PQPublicKeyPath, cfg.PQScheme) if err != nil { return fmt.Errorf("load pq public key: %w", err) } classicalPub, err := keyMgr.LoadPub(cfg.ClassicalPublicKeyPath, cfg.ClassicalScheme) if err != nil { return fmt.Errorf("load classical public key: %w", err) } recipients := []domaincrypto.RecipientPub{pqPub, classicalPub} encryptor := composite.NewEncryptor(registry) dumper := adapterpgdump.New() sink := local.NewLocalSink(cfg.Backup.Dir) runner := pipeline.NewRunner( pipeline.WithDumper(dumper), pipeline.WithEncryptor(encryptor), ) now := time.Now() pgDumpOpts := domainpgdump.Options{ Host: cfg.PG.Host, Port: cfg.PG.Port, Database: cfg.PG.Database, User: cfg.PG.User, Password: cfg.PG.Password, ExcludeTables: cfg.PG.ExcludeTables, Key: ArtifactKey(now), } if err := runner.Run(ctx, pgDumpOpts, recipients, sink, rand.Reader); err != nil { return fmt.Errorf("backup pipeline failed: %w", err) } if _, err := retention.PruneByAge(ctx, cfg.Backup.Dir, cfg.Backup.RetentionDays, now); err != nil { return fmt.Errorf("retention pruning failed: %w", err) } return nil } // ArtifactKey returns a backup file name that includes a UTC timestamp plus a // short random suffix so that two backups started in the same second do not // collide. func ArtifactKey(timestamp time.Time) string { var randomBytes [3]byte if _, err := rand.Read(randomBytes[:]); err != nil { return fmt.Sprintf( "synapse-%s-%09d.dump.pqenc", timestamp.UTC().Format("20060102-150405"), timestamp.Nanosecond(), ) } return fmt.Sprintf( "synapse-%s-%s.dump.pqenc", timestamp.UTC().Format("20060102-150405"), hex.EncodeToString(randomBytes[:]), ) }