Skip to content

Commit e7f1d0e

Browse files
authored
Merge pull request #8 from swisscom/master
bump
2 parents f44ab92 + 9d94f69 commit e7f1d0e

File tree

5 files changed

+295
-41
lines changed

5 files changed

+295
-41
lines changed

manifest.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ applications:
2323

2424
# ### push either as docker image
2525
docker:
26-
image: jamesclonk/backman:1.24.0 # choose version from https://hub.docker.com/r/jamesclonk/backman/tags, or 'latest'
26+
image: jamesclonk/backman:1.24.1 # choose version from https://hub.docker.com/r/jamesclonk/backman/tags, or 'latest'
2727
# ### or as buildpack/src
2828
# buildpacks:
2929
# - https://github.com/cloudfoundry/apt-buildpack

s3/encryption.go

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
package s3
2+
3+
import (
4+
"crypto/md5"
5+
"crypto/sha256"
6+
"encoding/hex"
7+
"fmt"
8+
"io"
9+
"path/filepath"
10+
11+
"github.com/minio/sio"
12+
"github.com/swisscom/backman/log"
13+
"golang.org/x/crypto/hkdf"
14+
"golang.org/x/crypto/scrypt"
15+
)
16+
17+
// header is the header identifying the encryption and kdf used
18+
// The header looks like this with each one representing 1 byte
19+
// | Magic | Version | Encryption | KDF |
20+
type header [4]byte
21+
22+
func (h header) Version() byte { return h[1] }
23+
func (h header) Encryption() byte { return h[2] }
24+
func (h header) KDF() byte { return h[3] }
25+
26+
// Validate validates the headers content
27+
func (h header) Validate() error {
28+
if h[0] != magicByte {
29+
return fmt.Errorf("wrong magic bytes, expected %v, got %v", magicByte, h[0])
30+
}
31+
switch h.Version() {
32+
case versionV10:
33+
break
34+
default:
35+
return fmt.Errorf("unexpected version: %v", h.Version())
36+
}
37+
switch h.Encryption() {
38+
case sio.AES_256_GCM, sio.CHACHA20_POLY1305:
39+
break
40+
default:
41+
return fmt.Errorf("unexpected encryption: %v", h.Encryption())
42+
}
43+
switch h.KDF() {
44+
case kdfScrypt:
45+
break
46+
default:
47+
return fmt.Errorf("unexpected KDF %v", h.KDF())
48+
}
49+
return nil
50+
}
51+
52+
// newHeader creates a new header for the given encryption and kdf
53+
func newHeader(encryption, kdf byte) header {
54+
return header{magicByte, versionV10, encryption, kdf}
55+
}
56+
57+
const (
58+
// needed to not collide with underlying sio header
59+
magicByte byte = 0xBA
60+
)
61+
62+
const (
63+
versionV10 = 0x10 // First KDF version with header
64+
)
65+
66+
const (
67+
kdfUnknown byte = iota
68+
kdfOldMD5 // needed for backwards compatibility
69+
kdfOldScryptHKDF // needed for backwards compatibility
70+
kdfScrypt = 0x10 // N=32768, r=8 and p=1.
71+
)
72+
73+
// getKey returns a key derived from the given masterKey, object and header
74+
// when the kdf is unknown or one of the old methods, it needs to peek in the reader and thus reset it before returning
75+
func getKey(masterKey string, object string, hdr header, reader io.ReadSeeker) ([]byte, error) {
76+
switch hdr.KDF() {
77+
case kdfScrypt:
78+
return generateKeyScrypt(masterKey, object)
79+
case kdfUnknown, kdfOldMD5, kdfOldScryptHKDF:
80+
// this is only for backwards compatibility
81+
key := generateKeyPre123(masterKey)
82+
if err := tryOldDecryption(key, reader); err != nil {
83+
key = generateKey124(masterKey, object)
84+
if err := tryOldDecryption(key, reader); err != nil {
85+
return nil, fmt.Errorf("could not get key for headerless encryption: %v", err)
86+
}
87+
return key, nil
88+
}
89+
return key, nil
90+
}
91+
return nil, fmt.Errorf("no valid kdf: %v", hdr.KDF())
92+
}
93+
94+
// generateKey derives a key from the given masterKey, object and header
95+
func generateKey(masterKey string, object string, hdr header) ([]byte, error) {
96+
switch hdr.KDF() {
97+
case kdfScrypt:
98+
return generateKeyScrypt(masterKey, object)
99+
case kdfOldMD5:
100+
return generateKeyPre123(masterKey), nil
101+
case kdfOldScryptHKDF:
102+
return generateKey124(masterKey, object), nil
103+
}
104+
return nil, fmt.Errorf("no valid kdf: %v", hdr.KDF())
105+
}
106+
107+
// generateKeyScrypt derives the key from the given masterKey and object with the scrypt KDF
108+
func generateKeyScrypt(masterKey, object string) ([]byte, error) {
109+
nonce := filepath.Base(object)
110+
hasher := sha256.New()
111+
if n, err := hasher.Write([]byte(fmt.Sprintf("%s%s", masterKey, nonce))); err != nil || n <= 0 {
112+
return nil, fmt.Errorf("could not get salt: %v", err)
113+
}
114+
key, err := scrypt.Key([]byte(masterKey), hasher.Sum(nil), 32768, 8, 1, 32)
115+
if err != nil {
116+
return nil, fmt.Errorf("could not derive encryption key: %v", err)
117+
}
118+
return key, nil
119+
}
120+
121+
// generateKeyPre123 derives the key via md5 hashing masterKey
122+
// This is not secure and mainly kept for being able to decrypt old backups
123+
func generateKeyPre123(masterKey string) []byte {
124+
hasher := md5.New()
125+
if n, err := hasher.Write([]byte(masterKey)); err != nil || n <= 0 {
126+
log.Fatalf("could not generate encryption key: %v", err)
127+
}
128+
return []byte(hex.EncodeToString(hasher.Sum(nil)))
129+
}
130+
131+
// generateKey124 derives the key from the given masterKey and object via scrypt and hkdf and using the hash(mk,o) as salt
132+
// This is overly complicated without providing a real improvement in security
133+
// It is mainly kept for being able to decrypt old backups
134+
func generateKey124(masterKey, object string) []byte {
135+
nonce := filepath.Base(object)
136+
137+
hasher := sha256.New()
138+
if n, err := hasher.Write([]byte(fmt.Sprintf("%s%s", masterKey, nonce))); err != nil || n <= 0 {
139+
log.Fatalf("could not get salt: %v", err)
140+
}
141+
salt := hex.EncodeToString(hasher.Sum(nil))
142+
143+
intKey, err := scrypt.Key([]byte(masterKey), []byte(salt), 32768, 8, 1, 32)
144+
if err != nil {
145+
log.Fatalf("could not get master key: %v", err)
146+
}
147+
148+
// derive encryption key, using filename as nonce (filenames contain timestamps and are unique per backman deployment)
149+
var key [32]byte
150+
kdf := hkdf.New(sha256.New, intKey, []byte(nonce)[:], nil)
151+
if _, err := io.ReadFull(kdf, key[:]); err != nil {
152+
log.Fatalf("failed to derive encryption key: %v", err)
153+
}
154+
return key[:]
155+
}
156+
157+
// tryOldDecryption peeks in the given reader and tries to decrypt with the given key
158+
// This is used to decrypt backups which don't have a header and therefore have no information about the used kdf/encryption
159+
func tryOldDecryption(key []byte, reader io.ReadSeeker) error {
160+
// reset reader to read from beginning
161+
if _, err := reader.Seek(0, 0); err != nil {
162+
return err
163+
}
164+
decrypter, err := sio.DecryptReader(reader, sio.Config{Key: key, CipherSuites: []byte{sio.AES_256_GCM}})
165+
if err != nil {
166+
return err
167+
}
168+
peek := make([]byte, 8)
169+
if _, err := decrypter.Read(peek); err != nil {
170+
return err
171+
}
172+
// reset again
173+
if _, err := reader.Seek(0, 0); err != nil {
174+
return err
175+
}
176+
return nil
177+
}
178+
179+
// readHeader reads and validates the header from the given reader
180+
func readHeader(reader io.Reader) (header, error) {
181+
hdr := header{}
182+
if _, err := reader.Read(hdr[:]); err != nil {
183+
return hdr, fmt.Errorf("could not read header: %v", err)
184+
}
185+
if err := hdr.Validate(); err != nil {
186+
// try old method
187+
hdr = newHeader(sio.AES_256_GCM, kdfUnknown)
188+
}
189+
return hdr, nil
190+
}

s3/encryption_test.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package s3
2+
3+
import (
4+
"bytes"
5+
"testing"
6+
7+
"github.com/minio/sio"
8+
)
9+
10+
func TestEncryptionDecryption(t *testing.T) {
11+
tests := []struct {
12+
name string
13+
masterkey string
14+
object string
15+
hdr header
16+
writeHeader bool
17+
}{
18+
{
19+
name: "old md5 kdf",
20+
masterkey: "test",
21+
object: "some-bucket/my-file.ext",
22+
hdr: newHeader(sio.AES_256_GCM, kdfOldMD5),
23+
},
24+
{
25+
name: "old scrypt kdf",
26+
masterkey: "test",
27+
object: "some-bucket/my-file.ext",
28+
hdr: newHeader(sio.AES_256_GCM, kdfOldScryptHKDF),
29+
},
30+
{
31+
name: "new scrypt kdf",
32+
masterkey: "test",
33+
object: "some-bucket/my-file.ext",
34+
hdr: newHeader(sio.AES_256_GCM, kdfScrypt),
35+
writeHeader: true,
36+
},
37+
}
38+
39+
for _, tt := range tests {
40+
t.Run(tt.name, func(t *testing.T) {
41+
var testdata = []byte("testdata")
42+
var encBuf = &bytes.Buffer{}
43+
enckey, err := generateKey(tt.masterkey, tt.object, tt.hdr)
44+
if err != nil {
45+
t.Fatal(err)
46+
}
47+
_, err = sio.Encrypt(encBuf, bytes.NewBuffer(testdata), sio.Config{Key: enckey, CipherSuites: []byte{tt.hdr.Encryption()}})
48+
if err != nil {
49+
t.Fatal(err)
50+
}
51+
52+
encData := encBuf.Bytes()
53+
if tt.writeHeader {
54+
encData = append(tt.hdr[:], encData...)
55+
}
56+
reader := bytes.NewReader(encData)
57+
hdr, err := readHeader(reader)
58+
if err != nil {
59+
t.Fatal(err)
60+
}
61+
decKey, err := getKey(tt.masterkey, tt.object, hdr, reader)
62+
if err != nil {
63+
t.Fatal(err)
64+
}
65+
if !bytes.Equal(decKey, enckey) {
66+
t.Fatalf("expected %s to be %s", decKey, enckey)
67+
}
68+
var outBuf = &bytes.Buffer{}
69+
_, err = sio.Decrypt(outBuf, reader, sio.Config{Key: decKey, CipherSuites: []byte{hdr.Encryption()}})
70+
if err != nil {
71+
t.Fatal(err)
72+
}
73+
if !bytes.Equal(outBuf.Bytes(), testdata) {
74+
t.Fatalf("expected %s to be %s", outBuf.Bytes(), testdata)
75+
}
76+
})
77+
}
78+
}

s3/objects.go

Lines changed: 24 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,17 @@
11
package s3
22

33
import (
4+
"bytes"
45
"context"
5-
"crypto/sha256"
6-
"encoding/hex"
76
"fmt"
87
"io"
98
"io/ioutil"
10-
"path/filepath"
119
"sort"
1210

1311
"github.com/minio/minio-go/v6"
1412
"github.com/minio/sio"
1513
"github.com/swisscom/backman/config"
1614
"github.com/swisscom/backman/log"
17-
"golang.org/x/crypto/hkdf"
18-
"golang.org/x/crypto/scrypt"
1915
)
2016

2117
func (s *Client) List(folderPath string) ([]minio.ObjectInfo, error) {
@@ -53,14 +49,20 @@ func (s *Client) UploadWithContext(ctx context.Context, object string, reader io
5349
var err error
5450
uploadReader := reader
5551
if len(config.Get().S3.EncryptionKey) != 0 {
56-
key := getKey(config.Get().S3.EncryptionKey, object)
57-
uploadReader, err = sio.EncryptReader(reader, sio.Config{Key: key, CipherSuites: []byte{sio.AES_256_GCM}})
52+
hdr := newHeader(sio.AES_256_GCM, kdfScrypt)
53+
if err := hdr.Validate(); err != nil {
54+
return fmt.Errorf("header is invalid: %v", err)
55+
}
56+
key, err := generateKey(config.Get().S3.EncryptionKey, object, hdr)
57+
if err != nil {
58+
return fmt.Errorf("could not get encryption key: %v", err)
59+
}
60+
uploadReader, err = sio.EncryptReader(reader, sio.Config{Key: key, CipherSuites: []byte{hdr.Encryption()}})
5861
if err != nil {
59-
log.Debugf("failed to encrypt reader: %v", err)
60-
return err
62+
return fmt.Errorf("failed to encrypt reader: %v", err)
6163
}
64+
uploadReader = io.MultiReader(bytes.NewBuffer(hdr[:]), uploadReader)
6265
}
63-
6466
n, err := s.Client.PutObjectWithContext(ctx, s.BucketName, object, uploadReader, size, minio.PutObjectOptions{ContentType: "application/gzip"})
6567
if err != nil {
6668
return err
@@ -88,14 +90,21 @@ func (s *Client) DownloadWithContext(ctx context.Context, object string) (io.Rea
8890
if err != nil {
8991
return nil, err
9092
}
91-
92-
if len(config.Get().S3.EncryptionKey) > 0 {
93-
key := getKey(config.Get().S3.EncryptionKey, object)
94-
decrypted, err := sio.DecryptReader(reader, sio.Config{Key: key, CipherSuites: []byte{sio.AES_256_GCM}})
93+
masterKey := config.Get().S3.EncryptionKey
94+
if len(masterKey) > 0 {
95+
hdr, err := readHeader(reader)
9596
if err != nil {
96-
log.Debugf("failed to decrypt reader: %v", err)
9797
return nil, err
9898
}
99+
key, err := getKey(masterKey, object, hdr, reader)
100+
if err != nil {
101+
return nil, fmt.Errorf("could not derive key: %v", err)
102+
}
103+
104+
decrypted, err := sio.DecryptReader(reader, sio.Config{Key: key, CipherSuites: []byte{hdr.Encryption()}})
105+
if err != nil {
106+
return nil, fmt.Errorf("failed to decrypt reader: %v", err)
107+
}
99108
return ioutil.NopCloser(decrypted), nil
100109
}
101110
return reader, nil
@@ -108,26 +117,3 @@ func (s *Client) Delete(object string) error {
108117
}
109118
return nil
110119
}
111-
112-
func getKey(password, object string) []byte {
113-
nonce := filepath.Base(object)
114-
115-
hasher := sha256.New()
116-
if n, err := hasher.Write([]byte(fmt.Sprintf("%s%s", password, nonce))); err != nil || n <= 0 {
117-
log.Fatalf("could not get salt: %v", err)
118-
}
119-
salt := hex.EncodeToString(hasher.Sum(nil))
120-
121-
masterKey, err := scrypt.Key([]byte(password), []byte(salt), 32768, 8, 1, 32)
122-
if err != nil {
123-
log.Fatalf("could not get master key: %v", err)
124-
}
125-
126-
// derive encryption key, using filename as nonce (filenames contain timestamps and are unique per backman deployment)
127-
var key [32]byte
128-
kdf := hkdf.New(sha256.New, []byte(masterKey), []byte(nonce)[:], nil)
129-
if _, err := io.ReadFull(kdf, key[:]); err != nil {
130-
log.Fatalf("failed to derive encryption key: %v", err)
131-
}
132-
return key[:]
133-
}

0 commit comments

Comments
 (0)