Skip to content

Commit d773f93

Browse files
committed
objstore: add experimental encryption wrapper
Signed-off-by: Michael Hoffmann <[email protected]>
1 parent 0796692 commit d773f93

File tree

7 files changed

+243
-14
lines changed

7 files changed

+243
-14
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ NOTE: As semantic versioning states all 0.y.z releases can contain breaking chan
99
We use *breaking :warning:* to mark changes that are not backward compatible (relates only to v0.y.z releases.)
1010

1111
## Unreleased
12+
- [#46](https://github.com/thanos-io/objstore/pull/46) Objstore: Add experimental encryption wrapper
1213

1314
### Fixed
1415
- [#33](https://github.com/thanos-io/objstore/pull/33) Tracing: Add `ContextWithTracer()` to inject the tracer into the context.

README.md

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ See [MAINTAINERS.md](https://github.com/thanos-io/thanos/blob/main/MAINTAINERS.m
4949
The core this module is the [`Bucket` interface](objstore.go):
5050

5151
```go mdox-exec="sed -n '37,50p' objstore.go"
52+
OpDelete = "delete"
53+
OpAttributes = "attributes"
54+
)
55+
5256
// Bucket provides read and write access to an object storage bucket.
5357
// NOTE: We assume strong consistency for write-read flow.
5458
type Bucket interface {
@@ -59,15 +63,15 @@ type Bucket interface {
5963
// Upload should be idempotent.
6064
Upload(ctx context.Context, name string, r io.Reader) error
6165

62-
// Delete removes the object with the given name.
63-
// If object does not exists in the moment of deletion, Delete should throw error.
64-
Delete(ctx context.Context, name string) error
65-
6666
```
6767
6868
All [provider implementations](providers) have to implement `Bucket` interface that allows common read and write operations that all supported by all object providers. If you want to limit the code that will do bucket operation to only read access (smart idea, allowing to limit access permissions), you can use the [`BucketReader` interface](objstore.go):
6969
7070
```go mdox-exec="sed -n '68,88p' objstore.go"
71+
// thanos_objstore_bucket_operation_failures_total metric.
72+
// TODO(bwplotka): Remove this when moved to Go 1.14 and replace with InstrumentedBucketReader.
73+
ReaderWithExpectedErrs(IsOpFailureExpectedFunc) BucketReader
74+
}
7175

7276
// BucketReader provides read access to an object storage bucket.
7377
type BucketReader interface {
@@ -85,10 +89,6 @@ type BucketReader interface {
8589
// Exists checks if the given object exists in the bucket.
8690
Exists(ctx context.Context, name string) (bool, error)
8791

88-
// IsObjNotFoundErr returns true if error means that object is not found. Relevant to Get operations.
89-
IsObjNotFoundErr(err error) bool
90-
91-
// Attributes returns information about the specified object.
9292
```
9393
9494
Those interfaces represent the object storage operations your code can use from `objstore` clients.
@@ -152,6 +152,7 @@ config:
152152
insecure: false
153153
signature_version2: false
154154
secret_key: ""
155+
session_token: ""
155156
put_user_metadata: {}
156157
http_config:
157158
idle_conn_timeout: 1m30s
@@ -181,6 +182,9 @@ config:
181182
encryption_key: ""
182183
sts_endpoint: ""
183184
prefix: ""
185+
client_side_encryption:
186+
enabled: false
187+
key_base64: ""
184188
```
185189
186190
At a minimum, you will need to provide a value for the `bucket`, `endpoint`, `access_key`, and `secret_key` keys. The rest of the keys are optional.
@@ -345,6 +349,9 @@ config:
345349
bucket: ""
346350
service_account: ""
347351
prefix: ""
352+
client_side_encryption:
353+
enabled: false
354+
key_base64: ""
348355
```
349356
350357
###### Using GOOGLE_APPLICATION_CREDENTIALS
@@ -445,6 +452,9 @@ config:
445452
disable_compression: false
446453
msi_resource: ""
447454
prefix: ""
455+
client_side_encryption:
456+
enabled: false
457+
key_base64: ""
448458
```
449459
450460
If `msi_resource` is used, authentication is done via system-assigned managed identity. The value for Azure should be `https://<storage-account-name>.blob.core.windows.net`.
@@ -489,6 +499,9 @@ config:
489499
timeout: 5m
490500
use_dynamic_large_objects: false
491501
prefix: ""
502+
client_side_encryption:
503+
enabled: false
504+
key_base64: ""
492505
```
493506

494507
##### Tencent COS
@@ -523,6 +536,9 @@ config:
523536
insecure_skip_verify: false
524537
disable_compression: false
525538
prefix: ""
539+
client_side_encryption:
540+
enabled: false
541+
key_base64: ""
526542
```
527543

528544
The `secret_key` and `secret_id` field is required. The `http_config` field is optional for optimize HTTP transport settings. There are two ways to configure the required bucket information:
@@ -543,6 +559,9 @@ config:
543559
access_key_id: ""
544560
access_key_secret: ""
545561
prefix: ""
562+
client_side_encryption:
563+
enabled: false
564+
key_base64: ""
546565
```
547566

548567
##### Baidu BOS
@@ -557,6 +576,9 @@ config:
557576
access_key: ""
558577
secret_key: ""
559578
prefix: ""
579+
client_side_encryption:
580+
enabled: false
581+
key_base64: ""
560582
```
561583

562584
##### Filesystem
@@ -572,6 +594,9 @@ type: FILESYSTEM
572594
config:
573595
directory: ""
574596
prefix: ""
597+
client_side_encryption:
598+
enabled: false
599+
key_base64: ""
575600
```
576601

577602
### Oracle Cloud Infrastructure Object Storage

client/factory.go

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package client
55

66
import (
77
"context"
8+
"encoding/base64"
89
"fmt"
910
"strings"
1011

@@ -41,9 +42,15 @@ const (
4142
)
4243

4344
type BucketConfig struct {
44-
Type ObjProvider `yaml:"type"`
45-
Config interface{} `yaml:"config"`
46-
Prefix string `yaml:"prefix" default:""`
45+
Type ObjProvider `yaml:"type"`
46+
Config interface{} `yaml:"config"`
47+
Prefix string `yaml:"prefix" default:""`
48+
ClientSideEncryption ClientSideEncryptionConfig `yaml:"client_side_encryption"`
49+
}
50+
51+
type ClientSideEncryptionConfig struct {
52+
Enabled bool `yaml:"enabled"`
53+
KeyBase64 string `yaml:"key_base64"`
4754
}
4855

4956
// NewBucket initializes and returns new object storage clients.
@@ -87,5 +94,16 @@ func NewBucket(logger log.Logger, confContentYaml []byte, reg prometheus.Registe
8794
return nil, errors.Wrap(err, fmt.Sprintf("create %s client", bucketConf.Type))
8895
}
8996

97+
if bucketConf.ClientSideEncryption.Enabled {
98+
key, err := base64.RawStdEncoding.DecodeString(bucketConf.ClientSideEncryption.KeyBase64)
99+
if err != nil {
100+
return nil, errors.Wrap(err, "unable to read base64 key")
101+
}
102+
if len(key) != 32 {
103+
return nil, errors.New("decoded key must have size 32")
104+
}
105+
bucket = objstore.BucketWithEncryption(bucket, key)
106+
}
107+
90108
return objstore.NewTracingBucket(objstore.BucketWithMetrics(bucket.Name(), objstore.NewPrefixedBucket(bucket, bucketConf.Prefix), reg)), nil
91109
}

go.mod

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ require (
1313
github.com/fatih/structtag v1.2.0
1414
github.com/go-kit/log v0.2.1
1515
github.com/minio/minio-go/v7 v7.0.45
16+
github.com/minio/sio v0.3.0
1617
github.com/ncw/swift v1.0.53
1718
github.com/opentracing/opentracing-go v1.2.0
1819
github.com/oracle/oci-go-sdk/v65 v65.13.0
@@ -100,5 +101,5 @@ require (
100101
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v0.5.1
101102
github.com/kr/text v0.2.0 // indirect
102103
github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b // indirect
103-
golang.org/x/crypto v0.3.0 // indirect
104+
golang.org/x/crypto v0.6.0 // indirect
104105
)

go.sum

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,8 @@ github.com/minio/minio-go/v7 v7.0.45 h1:g4IeM9M9pW/Lo8AGGNOjBZYlvmtlE1N5TQEYWXRW
307307
github.com/minio/minio-go/v7 v7.0.45/go.mod h1:nCrRzjoSUQh8hgKKtu3Y708OLvRLtuASMg2/nvmbarw=
308308
github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g=
309309
github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM=
310+
github.com/minio/sio v0.3.0 h1:syEFBewzOMOYVzSTFpp1MqpSZk8rUNbz8VIIc+PNzus=
311+
github.com/minio/sio v0.3.0/go.mod h1:8b0yPp2avGThviy/+OCJBI6OMpvxoUuiLvE6F1lebhw=
310312
github.com/mitchellh/mapstructure v1.4.3 h1:OVowDSCllw/YjdLkam3/sm7wEtOy59d8ndGgCcyj8cs=
311313
github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
312314
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -407,11 +409,12 @@ go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
407409
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
408410
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
409411
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
412+
golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
410413
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
411414
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
412415
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
413-
golang.org/x/crypto v0.3.0 h1:a06MkbcxBrEFc0w0QIZWXrH/9cCX6KJyWbBOIwAn+7A=
414-
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
416+
golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
417+
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
415418
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
416419
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
417420
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=

objstore.go

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,11 @@ package objstore
66
import (
77
"bytes"
88
"context"
9+
"crypto/rand"
10+
"crypto/sha256"
911
"io"
1012
"io/fs"
13+
"math"
1114
"os"
1215
"path"
1316
"path/filepath"
@@ -18,6 +21,7 @@ import (
1821
"github.com/efficientgo/core/logerrcapture"
1922
"github.com/go-kit/log"
2023
"github.com/go-kit/log/level"
24+
"github.com/minio/sio"
2125
"github.com/pkg/errors"
2226
"github.com/prometheus/client_golang/prometheus"
2327
"github.com/prometheus/client_golang/prometheus/promauto"
@@ -395,6 +399,105 @@ func DownloadDir(ctx context.Context, logger log.Logger, bkt BucketReader, origi
395399
// IsOpFailureExpectedFunc allows to mark certain errors as expected, so they will not increment thanos_objstore_bucket_operation_failures_total metric.
396400
type IsOpFailureExpectedFunc func(error) bool
397401

402+
// BucketWithEncryption takes a bucket and transparently encrypts and decrypts its payloads.
403+
func BucketWithEncryption(b Bucket, key []byte) *encryptedBucket {
404+
return &encryptedBucket{Bucket: b, masterKey: key}
405+
}
406+
407+
type encryptedBucket struct {
408+
Bucket
409+
410+
masterKey []byte
411+
}
412+
413+
const saltSizeBytes = 32
414+
415+
// As per https://github.com/minio/sio/blob/master/DARE.md#appendices we need a unique key data stream.
416+
// We derive a unique key from the configuration provided master key by fetching 32 random bits salt and
417+
// using KDF(master key ++ salt) as our derived encryption key. The salt is prepended to the encrypted
418+
// object. This is okay since the salt does not need to be kept a secret.
419+
func (eb *encryptedBucket) deriveKey(salt []byte) []byte {
420+
dk := sha256.Sum256(append(eb.masterKey, salt...))
421+
return dk[:]
422+
}
423+
424+
func (eb *encryptedBucket) encryptionConfig(salt []byte) sio.Config {
425+
return sio.Config{Key: eb.deriveKey(salt), MinVersion: sio.Version20, CipherSuites: []byte{sio.AES_256_GCM}}
426+
}
427+
428+
func (eb *encryptedBucket) Attributes(ctx context.Context, name string) (ObjectAttributes, error) {
429+
attrs, err := eb.Bucket.Attributes(ctx, name)
430+
if err != nil {
431+
return attrs, err
432+
}
433+
434+
decSize, err := sio.DecryptedSize(uint64(attrs.Size) - saltSizeBytes)
435+
if err != nil {
436+
return ObjectAttributes{}, errors.Wrap(err, "unable to determine unecrypted size")
437+
}
438+
439+
if decSize > math.MaxInt64 {
440+
return ObjectAttributes{}, errors.New("size of decrypted blob too large")
441+
}
442+
443+
return ObjectAttributes{Size: int64(decSize), LastModified: attrs.LastModified}, nil
444+
}
445+
446+
func (eb *encryptedBucket) Upload(ctx context.Context, name string, r io.Reader) error {
447+
salt := make([]byte, saltSizeBytes)
448+
if _, err := rand.Read(salt); err != nil {
449+
return errors.Wrap(err, "unable to derive encryption key for stream")
450+
}
451+
452+
er, err := sio.EncryptReader(r, eb.encryptionConfig(salt))
453+
if err != nil {
454+
return errors.Wrap(err, "unable to create encryption stream")
455+
}
456+
457+
tr := io.MultiReader(bytes.NewReader(salt), er)
458+
return eb.Bucket.Upload(ctx, name, tr)
459+
}
460+
461+
func (eb *encryptedBucket) Get(ctx context.Context, name string) (io.ReadCloser, error) {
462+
return eb.GetRange(ctx, name, 0, -1)
463+
}
464+
465+
func (eb *encryptedBucket) GetRange(ctx context.Context, name string, off, length int64) (io.ReadCloser, error) {
466+
saltReader, err := eb.Bucket.GetRange(ctx, name, 0, saltSizeBytes)
467+
if err != nil {
468+
return nil, errors.Wrap(err, "unable to fetch salt")
469+
}
470+
defer saltReader.Close()
471+
472+
salt, err := io.ReadAll(saltReader)
473+
if err != nil {
474+
return nil, errors.Wrap(err, "unable to read salt")
475+
}
476+
477+
br := &bucketReaderAt{ctx: ctx, name: name, b: eb.Bucket}
478+
dr, err := sio.DecryptReaderAt(br, eb.encryptionConfig(salt))
479+
if err != nil {
480+
return nil, errors.Wrap(err, "unable to create decryption stream")
481+
}
482+
return io.NopCloser(io.NewSectionReader(dr, off, length)), nil
483+
}
484+
485+
type bucketReaderAt struct {
486+
ctx context.Context
487+
name string
488+
b BucketReader
489+
}
490+
491+
func (br *bucketReaderAt) ReadAt(p []byte, off int64) (n int, err error) {
492+
rc, err := br.b.GetRange(br.ctx, br.name, off+saltSizeBytes, int64(len(p)))
493+
if err != nil {
494+
return 0, err
495+
}
496+
defer rc.Close()
497+
498+
return rc.Read(p)
499+
}
500+
398501
var _ InstrumentedBucket = &metricBucket{}
399502

400503
// BucketWithMetrics takes a bucket and registers metrics with the given registry for

0 commit comments

Comments
 (0)