@@ -19,6 +19,7 @@ import (
1919 "encoding/base64"
2020
2121 "github.com/chainlaunch/chainlaunch/pkg/config"
22+ "github.com/chainlaunch/chainlaunch/pkg/crypto"
2223 "github.com/chainlaunch/chainlaunch/pkg/db"
2324 "github.com/chainlaunch/chainlaunch/pkg/logger"
2425 "github.com/chainlaunch/chainlaunch/pkg/notifications"
@@ -40,15 +41,20 @@ type BackupService struct {
4041 stopCh chan struct {}
4142 databasePath string
4243 configService * config.ConfigService
44+ encryptor * crypto.Encryptor
4345}
4446
45- // NewBackupService creates a new backup service
47+ // NewBackupService creates a new backup service.
48+ // The encryptor parameter is used to encrypt/decrypt sensitive backup target
49+ // credentials (secret keys, restic passwords). Pass nil to disable encryption
50+ // (not recommended for production).
4651func NewBackupService (
4752 queries * db.Queries ,
4853 logger * logger.Logger ,
4954 notificationSvc * notificationService.NotificationService ,
5055 databasePath string ,
5156 configService * config.ConfigService ,
57+ encryptor * crypto.Encryptor ,
5258) * BackupService {
5359 c := cron .New (cron .WithSeconds ())
5460 c .Start ()
@@ -62,6 +68,7 @@ func NewBackupService(
6268 stopCh : make (chan struct {}),
6369 databasePath : databasePath ,
6470 configService : configService ,
71+ encryptor : encryptor ,
6572 }
6673
6774 // Load and schedule existing backup schedules
@@ -109,22 +116,41 @@ func (s *BackupService) CreateBackupTarget(ctx context.Context, params CreateBac
109116 return nil , fmt .Errorf ("failed to generate restic password: %w" , err )
110117 }
111118
119+ // Encrypt sensitive credentials before storing
120+ encAccessKeyID , err := s .encryptSecret (params .AccessKeyID )
121+ if err != nil {
122+ return nil , fmt .Errorf ("failed to encrypt access key ID: %w" , err )
123+ }
124+ encSecretKey , err := s .encryptSecret (params .SecretKey )
125+ if err != nil {
126+ return nil , fmt .Errorf ("failed to encrypt secret key: %w" , err )
127+ }
128+ encResticPassword , err := s .encryptSecret (resticPassword )
129+ if err != nil {
130+ return nil , fmt .Errorf ("failed to encrypt restic password: %w" , err )
131+ }
132+
112133 target , err := s .queries .CreateBackupTarget (ctx , & db.CreateBackupTargetParams {
113134 Name : params .Name ,
114135 Type : string (params .Type ),
115136 BucketName : sql.NullString {String : params .BucketName , Valid : params .BucketName != "" },
116137 Region : sql.NullString {String : params .Region , Valid : params .Region != "" },
117138 BucketPath : sql.NullString {String : params .BucketPath , Valid : params .BucketPath != "" },
118- AccessKeyID : sql.NullString {String : params . AccessKeyID , Valid : params . AccessKeyID != "" },
119- SecretKey : sql.NullString {String : params . SecretKey , Valid : params . SecretKey != "" },
139+ AccessKeyID : sql.NullString {String : encAccessKeyID , Valid : encAccessKeyID != "" },
140+ SecretKey : sql.NullString {String : encSecretKey , Valid : encSecretKey != "" },
120141 S3PathStyle : sql.NullBool {Bool : params .ForcePathStyle , Valid : true },
121142 Endpoint : sql.NullString {String : params .Endpoint , Valid : params .Endpoint != "" },
122- ResticPassword : sql.NullString {String : resticPassword , Valid : true },
143+ ResticPassword : sql.NullString {String : encResticPassword , Valid : true },
123144 })
124145 if err != nil {
125146 return nil , fmt .Errorf ("failed to create backup target: %w" , err )
126147 }
127148
149+ decAccessKeyID , err := s .decryptSecret (target .AccessKeyID .String )
150+ if err != nil {
151+ return nil , fmt .Errorf ("failed to decrypt access key ID: %w" , err )
152+ }
153+
128154 return & BackupTargetDTO {
129155 ID : target .ID ,
130156 Name : target .Name ,
@@ -133,7 +159,7 @@ func (s *BackupService) CreateBackupTarget(ctx context.Context, params CreateBac
133159 Region : target .Region .String ,
134160 Endpoint : target .Endpoint .String ,
135161 BucketPath : target .BucketPath .String ,
136- AccessKeyID : target . AccessKeyID . String ,
162+ AccessKeyID : decAccessKeyID ,
137163 ForcePathStyle : target .S3PathStyle .Bool ,
138164 CreatedAt : target .CreatedAt ,
139165 UpdatedAt : & target .UpdatedAt .Time ,
@@ -329,11 +355,25 @@ func (s *BackupService) performS3Backup(ctx context.Context, backup *db.Backup,
329355 return fmt .Errorf ("backup configuration error: invalid endpoint URL: %w" , err )
330356 }
331357
358+ // Decrypt credentials for use
359+ accessKeyID , err := s .decryptSecret (target .AccessKeyID .String )
360+ if err != nil {
361+ return fmt .Errorf ("failed to decrypt access key ID: %w" , err )
362+ }
363+ secretKey , err := s .decryptSecret (target .SecretKey .String )
364+ if err != nil {
365+ return fmt .Errorf ("failed to decrypt secret key: %w" , err )
366+ }
367+ resticPassword , err := s .decryptSecret (target .ResticPassword .String )
368+ if err != nil {
369+ return fmt .Errorf ("failed to decrypt restic password: %w" , err )
370+ }
371+
332372 // Set up restic environment variables for S3
333373 env := []string {
334- fmt .Sprintf ("AWS_ACCESS_KEY_ID=%s" , target . AccessKeyID . String ),
335- fmt .Sprintf ("AWS_SECRET_ACCESS_KEY=%s" , target . SecretKey . String ),
336- fmt .Sprintf ("RESTIC_PASSWORD=%s" , target . ResticPassword . String ),
374+ fmt .Sprintf ("AWS_ACCESS_KEY_ID=%s" , accessKeyID ),
375+ fmt .Sprintf ("AWS_SECRET_ACCESS_KEY=%s" , secretKey ),
376+ fmt .Sprintf ("RESTIC_PASSWORD=%s" , resticPassword ),
337377 fmt .Sprintf ("AWS_ENDPOINT=%s" , customURL .Host ),
338378 }
339379
@@ -658,6 +698,10 @@ func (s *BackupService) ListBackupTargets(ctx context.Context) ([]*BackupTargetD
658698
659699 dtos := make ([]* BackupTargetDTO , len (targets ))
660700 for i , target := range targets {
701+ decAccessKeyID , err := s .decryptSecret (target .AccessKeyID .String )
702+ if err != nil {
703+ return nil , fmt .Errorf ("failed to decrypt access key ID: %w" , err )
704+ }
661705 dtos [i ] = & BackupTargetDTO {
662706 ID : target .ID ,
663707 Name : target .Name ,
@@ -666,7 +710,7 @@ func (s *BackupService) ListBackupTargets(ctx context.Context) ([]*BackupTargetD
666710 Region : target .Region .String ,
667711 Endpoint : target .Endpoint .String ,
668712 BucketPath : target .BucketPath .String ,
669- AccessKeyID : target . AccessKeyID . String ,
713+ AccessKeyID : decAccessKeyID ,
670714 ForcePathStyle : target .S3PathStyle .Bool ,
671715 CreatedAt : target .CreatedAt ,
672716 UpdatedAt : & target .UpdatedAt .Time ,
@@ -683,6 +727,11 @@ func (s *BackupService) GetBackupTarget(ctx context.Context, id int64) (*BackupT
683727 return nil , fmt .Errorf ("failed to get backup target: %w" , err )
684728 }
685729
730+ decAccessKeyID , err := s .decryptSecret (target .AccessKeyID .String )
731+ if err != nil {
732+ return nil , fmt .Errorf ("failed to decrypt access key ID: %w" , err )
733+ }
734+
686735 return & BackupTargetDTO {
687736 ID : target .ID ,
688737 Name : target .Name ,
@@ -691,7 +740,7 @@ func (s *BackupService) GetBackupTarget(ctx context.Context, id int64) (*BackupT
691740 Region : target .Region .String ,
692741 Endpoint : target .Endpoint .String ,
693742 BucketPath : target .BucketPath .String ,
694- AccessKeyID : target . AccessKeyID . String ,
743+ AccessKeyID : decAccessKeyID ,
695744 ForcePathStyle : target .S3PathStyle .Bool ,
696745 CreatedAt : target .CreatedAt ,
697746 UpdatedAt : & target .UpdatedAt .Time ,
@@ -905,11 +954,25 @@ func (s *BackupService) deleteBackupFile(ctx context.Context, backup *db.Backup,
905954
906955// deleteS3BackupFile deletes a backup file from S3 using restic
907956func (s * BackupService ) deleteS3BackupFile (ctx context.Context , backup * db.Backup , target * db.BackupTarget ) error {
957+ // Decrypt credentials for use
958+ accessKeyID , err := s .decryptSecret (target .AccessKeyID .String )
959+ if err != nil {
960+ return fmt .Errorf ("failed to decrypt access key ID: %w" , err )
961+ }
962+ secretKey , err := s .decryptSecret (target .SecretKey .String )
963+ if err != nil {
964+ return fmt .Errorf ("failed to decrypt secret key: %w" , err )
965+ }
966+ resticPassword , err := s .decryptSecret (target .ResticPassword .String )
967+ if err != nil {
968+ return fmt .Errorf ("failed to decrypt restic password: %w" , err )
969+ }
970+
908971 // Set up restic environment variables
909972 env := []string {
910- fmt .Sprintf ("AWS_ACCESS_KEY_ID=%s" , target . AccessKeyID . String ),
911- fmt .Sprintf ("AWS_SECRET_ACCESS_KEY=%s" , target . SecretKey . String ),
912- fmt .Sprintf ("RESTIC_PASSWORD=%s" , target . ResticPassword . String ),
973+ fmt .Sprintf ("AWS_ACCESS_KEY_ID=%s" , accessKeyID ),
974+ fmt .Sprintf ("AWS_SECRET_ACCESS_KEY=%s" , secretKey ),
975+ fmt .Sprintf ("RESTIC_PASSWORD=%s" , resticPassword ),
913976 fmt .Sprintf ("AWS_ENDPOINT=%s" , target .Endpoint .String ),
914977 }
915978
@@ -981,6 +1044,27 @@ func (s *BackupService) findLatestSnapshot(env []string) (string, error) {
9811044 return snapshots [0 ].ID , nil
9821045}
9831046
1047+ // encryptSecret encrypts a secret string if an encryptor is configured.
1048+ // Returns the original string if no encryptor is set (backwards compatible).
1049+ func (s * BackupService ) encryptSecret (secret string ) (string , error ) {
1050+ if s .encryptor == nil || secret == "" {
1051+ return secret , nil
1052+ }
1053+ return s .encryptor .Encrypt (secret )
1054+ }
1055+
1056+ // decryptSecret decrypts a secret string if an encryptor is configured.
1057+ // Handles both encrypted and plaintext values for backwards compatibility.
1058+ func (s * BackupService ) decryptSecret (secret string ) (string , error ) {
1059+ if s .encryptor == nil || secret == "" {
1060+ return secret , nil
1061+ }
1062+ if ! crypto .IsEncrypted (secret ) {
1063+ return secret , nil
1064+ }
1065+ return s .encryptor .Decrypt (secret )
1066+ }
1067+
9841068// Add helper function to generate secure password
9851069func generateSecurePassword () (string , error ) {
9861070 bytes := make ([]byte , 32 ) // 256 bits
@@ -1008,6 +1092,16 @@ func (s *BackupService) UpdateBackupTarget(ctx context.Context, params UpdateBac
10081092 }
10091093 }
10101094
1095+ // Encrypt sensitive credentials before storing
1096+ encAccessKeyID , err := s .encryptSecret (params .AccessKeyID )
1097+ if err != nil {
1098+ return nil , fmt .Errorf ("failed to encrypt access key ID: %w" , err )
1099+ }
1100+ encSecretKey , err := s .encryptSecret (params .SecretKey )
1101+ if err != nil {
1102+ return nil , fmt .Errorf ("failed to encrypt secret key: %w" , err )
1103+ }
1104+
10111105 // Update the target
10121106 target , err := s .queries .UpdateBackupTarget (ctx , & db.UpdateBackupTargetParams {
10131107 ID : params .ID ,
@@ -1016,8 +1110,8 @@ func (s *BackupService) UpdateBackupTarget(ctx context.Context, params UpdateBac
10161110 BucketName : sql.NullString {String : params .BucketName , Valid : params .BucketName != "" },
10171111 Region : sql.NullString {String : params .Region , Valid : params .Region != "" },
10181112 BucketPath : sql.NullString {String : params .BucketPath , Valid : params .BucketPath != "" },
1019- AccessKeyID : sql.NullString {String : params . AccessKeyID , Valid : params . AccessKeyID != "" },
1020- SecretKey : sql.NullString {String : params . SecretKey , Valid : params . SecretKey != "" },
1113+ AccessKeyID : sql.NullString {String : encAccessKeyID , Valid : encAccessKeyID != "" },
1114+ SecretKey : sql.NullString {String : encSecretKey , Valid : encSecretKey != "" },
10211115 S3PathStyle : sql.NullBool {Bool : params .ForcePathStyle , Valid : true },
10221116 Endpoint : sql.NullString {String : params .Endpoint , Valid : params .Endpoint != "" },
10231117 })
@@ -1028,6 +1122,11 @@ func (s *BackupService) UpdateBackupTarget(ctx context.Context, params UpdateBac
10281122 return nil , fmt .Errorf ("failed to update backup target: %w" , err )
10291123 }
10301124
1125+ decAccessKeyID , err := s .decryptSecret (target .AccessKeyID .String )
1126+ if err != nil {
1127+ return nil , fmt .Errorf ("failed to decrypt access key ID: %w" , err )
1128+ }
1129+
10311130 return & BackupTargetDTO {
10321131 ID : target .ID ,
10331132 Name : target .Name ,
@@ -1036,7 +1135,7 @@ func (s *BackupService) UpdateBackupTarget(ctx context.Context, params UpdateBac
10361135 Region : target .Region .String ,
10371136 Endpoint : target .Endpoint .String ,
10381137 BucketPath : target .BucketPath .String ,
1039- AccessKeyID : target . AccessKeyID . String ,
1138+ AccessKeyID : decAccessKeyID ,
10401139 ForcePathStyle : target .S3PathStyle .Bool ,
10411140 CreatedAt : target .CreatedAt ,
10421141 UpdatedAt : & target .UpdatedAt .Time ,
0 commit comments