@@ -26,9 +26,10 @@ import (
2626
2727// GCS - presents methods for manipulate data on GCS
2828type GCS struct {
29- client * storage.Client
30- Config * config.GCSConfig
31- clientPool * pool.ObjectPool
29+ client * storage.Client
30+ Config * config.GCSConfig
31+ clientPool * pool.ObjectPool
32+ encryptionKey []byte // Customer-Supplied Encryption Key (CSEK)
3233}
3334
3435type debugGCSTransport struct {
@@ -188,14 +189,39 @@ func (gcs *GCS) Connect(ctx context.Context) error {
188189 gcs .clientPool = pool .NewObjectPoolWithDefaultConfig (ctx , factory )
189190 gcs .clientPool .Config .MaxTotal = gcs .Config .ClientPoolSize * 3
190191 gcs .client , err = storage .NewClient (ctx , storageClientOptions ... )
191- return err
192+ if err != nil {
193+ return err
194+ }
195+
196+ // Validate and decode the encryption key if provided
197+ if gcs .Config .EncryptionKey != "" {
198+ key , err := base64 .StdEncoding .DecodeString (gcs .Config .EncryptionKey )
199+ if err != nil {
200+ return errors .Wrap (err , "gcs: malformed encryption_key, must be base64-encoded 256-bit key" )
201+ }
202+ if len (key ) != 32 {
203+ return fmt .Errorf ("gcs: malformed encryption_key, must be base64-encoded 256-bit key (got %d bytes)" , len (key ))
204+ }
205+ gcs .encryptionKey = key
206+ log .Info ().Msg ("GCS: Customer-Supplied Encryption Key (CSEK) configured" )
207+ }
208+
209+ return nil
192210}
193211
194212func (gcs * GCS ) Close (ctx context.Context ) error {
195213 gcs .clientPool .Close (ctx )
196214 return gcs .client .Close ()
197215}
198216
217+ // applyEncryption returns an ObjectHandle with encryption key applied if configured
218+ func (gcs * GCS ) applyEncryption (obj * storage.ObjectHandle ) * storage.ObjectHandle {
219+ if gcs .encryptionKey != nil {
220+ return obj .Key (gcs .encryptionKey )
221+ }
222+ return obj
223+ }
224+
199225func (gcs * GCS ) Walk (ctx context.Context , gcsPath string , recursive bool , process func (ctx context.Context , r RemoteFile ) error ) error {
200226 rootPath := path .Join (gcs .Config .Path , gcsPath )
201227 return gcs .WalkAbsolute (ctx , rootPath , recursive , process )
@@ -252,7 +278,7 @@ func (gcs *GCS) GetFileReaderAbsolute(ctx context.Context, key string) (io.ReadC
252278 return nil , err
253279 }
254280 pClient := pClientObj .(* clientObject ).Client
255- obj := pClient .Bucket (gcs .Config .Bucket ).Object (key )
281+ obj := gcs . applyEncryption ( pClient .Bucket (gcs .Config .Bucket ).Object (key ) )
256282 reader , err := obj .NewReader (ctx )
257283 if err != nil {
258284 if pErr := gcs .clientPool .InvalidateObject (ctx , pClientObj ); pErr != nil {
@@ -281,7 +307,7 @@ func (gcs *GCS) PutFileAbsolute(ctx context.Context, key string, r io.ReadCloser
281307 return err
282308 }
283309 pClient := pClientObj .(* clientObject ).Client
284- obj := pClient .Bucket (gcs .Config .Bucket ).Object (key )
310+ obj := gcs . applyEncryption ( pClient .Bucket (gcs .Config .Bucket ).Object (key ) )
285311 // always retry transient errors to mitigate retry logic bugs.
286312 obj = obj .Retryer (storage .WithPolicy (storage .RetryAlways ))
287313 writer := obj .NewWriter (ctx )
@@ -314,7 +340,8 @@ func (gcs *GCS) StatFile(ctx context.Context, key string) (RemoteFile, error) {
314340}
315341
316342func (gcs * GCS ) StatFileAbsolute (ctx context.Context , key string ) (RemoteFile , error ) {
317- objAttr , err := gcs .client .Bucket (gcs .Config .Bucket ).Object (key ).Attrs (ctx )
343+ obj := gcs .applyEncryption (gcs .client .Bucket (gcs .Config .Bucket ).Object (key ))
344+ objAttr , err := obj .Attrs (ctx )
318345 if err != nil {
319346 if errors .Is (err , storage .ErrObjectNotExist ) {
320347 return nil , ErrNotFound
@@ -369,7 +396,7 @@ func (gcs *GCS) CopyObject(ctx context.Context, srcSize int64, srcBucket, srcKey
369396 }
370397 pClient := pClientObj .(* clientObject ).Client
371398 src := pClient .Bucket (srcBucket ).Object (srcKey )
372- dst := pClient .Bucket (gcs .Config .Bucket ).Object (dstKey )
399+ dst := gcs . applyEncryption ( pClient .Bucket (gcs .Config .Bucket ).Object (dstKey ) )
373400 // always retry transient errors to mitigate retry logic bugs.
374401 dst = dst .Retryer (storage .WithPolicy (storage .RetryAlways ))
375402 attrs , err := src .Attrs (ctx )
@@ -379,7 +406,10 @@ func (gcs *GCS) CopyObject(ctx context.Context, srcSize int64, srcBucket, srcKey
379406 }
380407 return 0 , err
381408 }
382- if _ , err = dst .CopierFrom (src ).Run (ctx ); err != nil {
409+ copier := dst .CopierFrom (src )
410+ // If encryption is enabled, the destination will be encrypted
411+ // Note: source objects from object disks are not encrypted by clickhouse-backup
412+ if _ , err = copier .Run (ctx ); err != nil {
383413 if pErr := gcs .clientPool .InvalidateObject (ctx , pClientObj ); pErr != nil {
384414 log .Warn ().Msgf ("gcs.CopyObject: gcs.clientPool.InvalidateObject error: %+v" , pErr )
385415 }
0 commit comments