diff --git a/cli/command_benchmark_crypto.go b/cli/command_benchmark_crypto.go index b5dbebd8a..acde14a02 100644 --- a/cli/command_benchmark_crypto.go +++ b/cli/command_benchmark_crypto.go @@ -13,10 +13,11 @@ ) var ( - benchmarkCryptoCommand = benchmarkCommands.Command("crypto", "Run hash and encryption benchmarks") - benchmarkCryptoBlockSize = benchmarkCryptoCommand.Flag("block-size", "Size of a block to encrypt").Default("1MB").Bytes() - benchmarkCryptoEncryption = benchmarkCryptoCommand.Flag("encryption", "Test encrypted formats").Default("true").Bool() - benchmarkCryptoRepeat = benchmarkCryptoCommand.Flag("repeat", "Number of repetitions").Default("100").Int() + benchmarkCryptoCommand = benchmarkCommands.Command("crypto", "Run hash and encryption benchmarks") + benchmarkCryptoBlockSize = benchmarkCryptoCommand.Flag("block-size", "Size of a block to encrypt").Default("1MB").Bytes() + benchmarkCryptoEncryption = benchmarkCryptoCommand.Flag("encryption", "Test encrypted formats").Default("true").Bool() + benchmarkCryptoRepeat = benchmarkCryptoCommand.Flag("repeat", "Number of repetitions").Default("100").Int() + benchmarkCryptoDeprecatedAlgorithms = benchmarkCryptoCommand.Flag("deprecated", "Include deprecated algorithms").Bool() ) func runBenchmarkCryptoAction(ctx *kingpin.ParseContext) error { @@ -31,7 +32,7 @@ type benchResult struct { data := make([]byte, *benchmarkCryptoBlockSize) for _, ha := range hashing.SupportedAlgorithms() { - for _, ea := range encryption.SupportedAlgorithms() { + for _, ea := range encryption.SupportedAlgorithms(*benchmarkCryptoDeprecatedAlgorithms) { isEncrypted := ea != encryption.NoneAlgorithm if *benchmarkCryptoEncryption != isEncrypted { continue diff --git a/cli/command_repository_create.go b/cli/command_repository_create.go index e98b531a9..341784315 100644 --- a/cli/command_repository_create.go +++ b/cli/command_repository_create.go @@ -18,8 +18,8 @@ var ( createCommand = repositoryCommands.Command("create", "Create new repository in a specified location.") - createBlockHashFormat = createCommand.Flag("block-hash", "Block hash algorithm.").PlaceHolder("ALGO").Default(hashing.DefaultAlgorithm).Enum(hashing.SupportedAlgorithms()...) - createBlockEncryptionFormat = createCommand.Flag("encryption", "Block encryption algorithm.").PlaceHolder("ALGO").Default(encryption.DefaultAlgorithm).Enum(encryption.SupportedAlgorithms()...) + createBlockHashFormat = createCommand.Flag("block-hash", "Content hash algorithm.").PlaceHolder("ALGO").Default(hashing.DefaultAlgorithm).Enum(hashing.SupportedAlgorithms()...) + createBlockEncryptionFormat = createCommand.Flag("encryption", "Content encryption algorithm.").PlaceHolder("ALGO").Default(encryption.DefaultAlgorithm).Enum(encryption.SupportedAlgorithms(false)...) createSplitter = createCommand.Flag("object-splitter", "The splitter to use for new objects in the repository").Default(splitter.DefaultAlgorithm).Enum(splitter.SupportedAlgorithms()...) createOnly = createCommand.Flag("create-only", "Create repository, but don't connect to it.").Short('c').Bool() diff --git a/go.mod b/go.mod index 9a127f0c2..c1bc16d15 100644 --- a/go.mod +++ b/go.mod @@ -32,7 +32,7 @@ require ( github.com/studio-b12/gowebdav v0.0.0-20190103184047-38f79aeaf1ac github.com/zalando/go-keyring v0.0.0-20190715212148-76787ff3b3bd gocloud.dev v0.18.0 - golang.org/x/crypto v0.0.0-20191117063200-497ca9f6d64f + golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d golang.org/x/exp v0.0.0-20190829153037-c13cbed26979 golang.org/x/net v0.0.0-20190923162816-aa69164e4478 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 diff --git a/go.sum b/go.sum index fcfa12c7e..7d529e009 100644 --- a/go.sum +++ b/go.sum @@ -396,6 +396,8 @@ golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191117063200-497ca9f6d64f h1:kz4KIr+xcPUsI3VMoqWfPMvtnJ6MGfiVwsWSVzphMO4= golang.org/x/crypto v0.0.0-20191117063200-497ca9f6d64f/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d h1:1ZiEyfaQIg3Qh0EoqpwAakHVhecoE5wlSg5GjnafJGw= +golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= diff --git a/internal/server/api_repo.go b/internal/server/api_repo.go index 524ace852..a75930098 100644 --- a/internal/server/api_repo.go +++ b/internal/server/api_repo.go @@ -121,7 +121,7 @@ func (s *Server) handleRepoSupportedAlgorithms(ctx context.Context, r *http.Requ HashAlgorithms: hashing.SupportedAlgorithms(), DefaultEncryptionAlgorithm: encryption.DefaultAlgorithm, - EncryptionAlgorithms: encryption.SupportedAlgorithms(), + EncryptionAlgorithms: encryption.SupportedAlgorithms(false), DefaultSplitterAlgorithm: splitter.DefaultAlgorithm, SplitterAlgorithms: splitter.SupportedAlgorithms(), diff --git a/repo/content/content_formatter_test.go b/repo/content/content_formatter_test.go index 6ae494231..f4811b113 100644 --- a/repo/content/content_formatter_test.go +++ b/repo/content/content_formatter_test.go @@ -39,7 +39,7 @@ func TestFormatters(t *testing.T) { for _, hashAlgo := range hashing.SupportedAlgorithms() { hashAlgo := hashAlgo t.Run(hashAlgo, func(t *testing.T) { - for _, encryptionAlgo := range encryption.SupportedAlgorithms() { + for _, encryptionAlgo := range encryption.SupportedAlgorithms(true) { encryptionAlgo := encryptionAlgo t.Run(encryptionAlgo, func(t *testing.T) { ctx := testlogging.Context(t) diff --git a/repo/encryption/aes256_gcm_hmac_sha256_encryptor.go b/repo/encryption/aes256_gcm_hmac_sha256_encryptor.go new file mode 100644 index 000000000..69657d0ed --- /dev/null +++ b/repo/encryption/aes256_gcm_hmac_sha256_encryptor.go @@ -0,0 +1,66 @@ +package encryption + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/hmac" + "crypto/sha256" + + "github.com/pkg/errors" +) + +var zeroAES256GCMNonce = make([]byte, 12) + +type aes256GCMHmacSha256 struct { + keyDerivationSecret []byte +} + +// aeadForContent returns cipher.AEAD using key derived from a given contentID. +func (e aes256GCMHmacSha256) aeadForContent(contentID []byte) (cipher.AEAD, error) { + h := hmac.New(sha256.New, e.keyDerivationSecret) + if _, err := h.Write(contentID); err != nil { + return nil, errors.Wrap(err, "unable to derive encryption key") + } + + key := h.Sum(nil) + + c, err := aes.NewCipher(key) + if err != nil { + return nil, errors.Wrap(err, "unable to create AES-256 cipher") + } + + return cipher.NewGCM(c) +} + +func (e aes256GCMHmacSha256) Decrypt(input, contentID []byte) ([]byte, error) { + a, err := e.aeadForContent(contentID) + if err != nil { + return nil, err + } + + return a.Open(nil, zeroAES256GCMNonce, input, contentID) +} + +func (e aes256GCMHmacSha256) Encrypt(input, contentID []byte) ([]byte, error) { + a, err := e.aeadForContent(contentID) + if err != nil { + return nil, err + } + + return a.Seal(nil, zeroAES256GCMNonce, input, contentID), nil +} + +func (e aes256GCMHmacSha256) IsAuthenticated() bool { + return true +} + +func init() { + Register("AES256-GCM-HMAC-SHA256", "AES-256-GCM using per-content key generated using HMAC-SHA256", false, func(p Parameters) (Encryptor, error) { + keyDerivationSecret, err := deriveKey(p, []byte(purposeEncryptionKey), 32) + if err != nil { + return nil, err + } + + return aes256GCMHmacSha256{keyDerivationSecret}, nil + }) +} diff --git a/repo/encryption/chacha20_poly1305_hmac_sha256_encryptor.go b/repo/encryption/chacha20_poly1305_hmac_sha256_encryptor.go new file mode 100644 index 000000000..fe36c1403 --- /dev/null +++ b/repo/encryption/chacha20_poly1305_hmac_sha256_encryptor.go @@ -0,0 +1,61 @@ +package encryption + +import ( + "crypto/cipher" + "crypto/hmac" + "crypto/sha256" + + "github.com/pkg/errors" + "golang.org/x/crypto/chacha20poly1305" +) + +var chacha20poly1305ZeroNonce = make([]byte, chacha20poly1305.NonceSize) + +type chacha20poly1305hmacSha256Encryptor struct { + keyDerivationSecret []byte +} + +// aeadForContent returns cipher.AEAD using key derived from a given contentID. +func (e chacha20poly1305hmacSha256Encryptor) aeadForContent(contentID []byte) (cipher.AEAD, error) { + h := hmac.New(sha256.New, e.keyDerivationSecret) + if _, err := h.Write(contentID); err != nil { + return nil, errors.Wrap(err, "unable to derive encryption key") + } + + key := h.Sum(nil) + + return chacha20poly1305.New(key) +} + +func (e chacha20poly1305hmacSha256Encryptor) Decrypt(input, contentID []byte) ([]byte, error) { + a, err := e.aeadForContent(contentID) + if err != nil { + return nil, err + } + + return a.Open(nil, chacha20poly1305ZeroNonce, input, contentID) +} + +func (e chacha20poly1305hmacSha256Encryptor) Encrypt(input, contentID []byte) ([]byte, error) { + a, err := e.aeadForContent(contentID) + if err != nil { + return nil, err + } + + return a.Seal(nil, chacha20poly1305ZeroNonce, input, contentID), nil +} + +func (e chacha20poly1305hmacSha256Encryptor) IsAuthenticated() bool { + return true +} + +func init() { + Register("CHACHA20-POLY1305-HMAC-SHA256", "CHACHA20-POLY1305 using per-content key generated using HMAC-SHA256", false, func(p Parameters) (Encryptor, error) { + keyDerivationSecret, err := deriveKey(p, []byte(purposeEncryptionKey), 32) + if err != nil { + return nil, err + } + + return chacha20poly1305hmacSha256Encryptor{keyDerivationSecret}, nil + }) +} diff --git a/repo/encryption/ctr_encryptor.go b/repo/encryption/deprecated_ctr_encryptor.go similarity index 84% rename from repo/encryption/ctr_encryptor.go rename to repo/encryption/deprecated_ctr_encryptor.go index e6fc85375..5cf7844cb 100644 --- a/repo/encryption/ctr_encryptor.go +++ b/repo/encryption/deprecated_ctr_encryptor.go @@ -70,7 +70,7 @@ func newCTREncryptorFactory(keySize int, createCipherWithKey func(key []byte) (c } func init() { - Register("AES-128-CTR", "AES-128 in CTR mode", false, newCTREncryptorFactory(16, aes.NewCipher)) //nolint:gomnd - Register("AES-192-CTR", "AES-192 in CTR mode", false, newCTREncryptorFactory(24, aes.NewCipher)) //nolint:gomnd - Register("AES-256-CTR", "AES-256 in CTR mode", false, newCTREncryptorFactory(32, aes.NewCipher)) //nolint:gomnd + Register("AES-128-CTR", "DEPRECATED: AES-128 in CTR mode", true, newCTREncryptorFactory(16, aes.NewCipher)) //nolint:gomnd + Register("AES-192-CTR", "DEPRECATED: AES-192 in CTR mode", true, newCTREncryptorFactory(24, aes.NewCipher)) //nolint:gomnd + Register("AES-256-CTR", "DEPRECATED: AES-256 in CTR mode", true, newCTREncryptorFactory(32, aes.NewCipher)) //nolint:gomnd } diff --git a/repo/encryption/salsa_encryptor.go b/repo/encryption/deprecated_salsa_encryptor.go similarity index 76% rename from repo/encryption/salsa_encryptor.go rename to repo/encryption/deprecated_salsa_encryptor.go index 3c3726d70..a9936185c 100644 --- a/repo/encryption/salsa_encryptor.go +++ b/repo/encryption/deprecated_salsa_encryptor.go @@ -63,15 +63,21 @@ func (s salsaEncryptor) encryptDecrypt(input, contentID []byte) ([]byte, error) } func init() { - Register("SALSA20", "SALSA20 using shared key and 64-bit nonce", true, func(p Parameters) (Encryptor, error) { + Register("SALSA20", "DEPRECATED: SALSA20 using shared key and 64-bit nonce", true, func(p Parameters) (Encryptor, error) { var k [salsaKeyLength]byte copy(k[:], p.GetMasterKey()[0:salsaKeyLength]) return salsaEncryptor{8, &k, nil}, nil }) - Register("SALSA20-HMAC", "SALSA20 with HMAC-SHA256 using shared key and 64-bit nonce", true, func(p Parameters) (Encryptor, error) { - encryptionKey := deriveKey(p, []byte(purposeEncryptionKey), salsaKeyLength) - hmacSecret := deriveKey(p, []byte(purposeHMACSecret), hmacLength) + Register("SALSA20-HMAC", "DEPRECATED: SALSA20 with HMAC-SHA256 using shared key and 64-bit nonce", true, func(p Parameters) (Encryptor, error) { + encryptionKey, err := deriveKey(p, []byte(purposeEncryptionKey), salsaKeyLength) + if err != nil { + return nil, err + } + hmacSecret, err := deriveKey(p, []byte(purposeHMACSecret), hmacLength) + if err != nil { + return nil, err + } var k [salsaKeyLength]byte copy(k[:], encryptionKey) diff --git a/repo/encryption/encryption.go b/repo/encryption/encryption.go index ee680f208..0b6910b5f 100644 --- a/repo/encryption/encryption.go +++ b/repo/encryption/encryption.go @@ -10,6 +10,8 @@ "golang.org/x/crypto/hkdf" ) +const minDerivedKeyLength = 32 + // Encryptor performs encryption and decryption of contents of data. type Encryptor interface { // Encrypt returns encrypted bytes corresponding to the given plaintext. @@ -46,16 +48,21 @@ func CreateEncryptor(p Parameters) (Encryptor, error) { type EncryptorFactory func(p Parameters) (Encryptor, error) // DefaultAlgorithm is the name of the default encryption algorithm. -const DefaultAlgorithm = "SALSA20-HMAC" +const DefaultAlgorithm = "AES256-GCM-HMAC-SHA256" // NoneAlgorithm is the name of the algorithm that does not encrypt. const NoneAlgorithm = "NONE" // SupportedAlgorithms returns the names of the supported encryption // methods -func SupportedAlgorithms() []string { +func SupportedAlgorithms(includeDeprecated bool) []string { var result []string - for k := range encryptors { + + for k, e := range encryptors { + if e.deprecated && !includeDeprecated { + continue + } + result = append(result, k) } @@ -87,10 +94,14 @@ func cloneBytes(b []byte) []byte { // deriveKey uses HKDF to derive a key of a given length and a given purpose from parameters. // nolint:unparam -func deriveKey(p Parameters, purpose []byte, length int) []byte { +func deriveKey(p Parameters, purpose []byte, length int) ([]byte, error) { + if length < minDerivedKeyLength { + return nil, errors.Errorf("derived key must be at least 32 bytes, was %v", length) + } + key := make([]byte, length) k := hkdf.New(sha256.New, p.GetMasterKey(), purpose, nil) io.ReadFull(k, key) //nolint:errcheck - return key + return key, nil } diff --git a/repo/encryption/encryption_test.go b/repo/encryption/encryption_test.go index 2f04050b7..442ddcafb 100644 --- a/repo/encryption/encryption_test.go +++ b/repo/encryption/encryption_test.go @@ -3,6 +3,8 @@ import ( "bytes" "crypto/rand" + "encoding/hex" + mathrand "math/rand" "testing" "github.com/kopia/kopia/repo/encryption" @@ -30,7 +32,7 @@ func TestRoundTrip(t *testing.T) { contentID2 := make([]byte, 16) rand.Read(contentID2) //nolint:errcheck - for _, encryptionAlgo := range encryption.SupportedAlgorithms() { + for _, encryptionAlgo := range encryption.SupportedAlgorithms(true) { encryptionAlgo := encryptionAlgo t.Run(encryptionAlgo, func(t *testing.T) { e, err := encryption.CreateEncryptor(parameters{encryptionAlgo, masterKey}) @@ -73,14 +75,115 @@ func TestRoundTrip(t *testing.T) { // decrypt using wrong content ID badPlainText2, err := e.Decrypt(cipherText2, contentID1) - if err != nil || plainText2 == nil { - t.Errorf("invalid response from Decrypt: %v %v", plainText2, err) + if e.IsAuthenticated() { + if err == nil && encryptionAlgo != "SALSA20-HMAC" { + // "SALSA20-HMAC" is deprecated & wrong, and only validates that checksum is + // valid for some content, but does not validate that we decrypted the + // intended content. + t.Errorf("expected decrypt to fail for authenticated encryption") + } + } else { + if bytes.Equal(badPlainText2, plainText2) { + t.Errorf("decrypted plaintext matches, but it should not: %x", plainText2) + } } - if bytes.Equal(badPlainText2, plainText2) { - t.Errorf("decrypted plaintext matches, but it should not: %x", plainText2) + // flip some bits in the cipherText + if e.IsAuthenticated() { + cipherText2[mathrand.Intn(len(cipherText2))] ^= byte(1 + mathrand.Intn(254)) + if _, err := e.Decrypt(cipherText2, contentID1); err == nil { + t.Errorf("expected decrypt failure on invalid ciphertext, got success") + } } } }) } } + +func TestCiphertextSamples(t *testing.T) { + cases := []struct { + masterKey []byte + contentID []byte + payload []byte + samples map[string]string + }{ + { + masterKey: []byte("01234567890123456789012345678901"), // 32 bytes + contentID: []byte("aabbccddeeffgghhiijjkkllmmnnoopp"), // 32 bytes + payload: []byte("foo"), + + // samples of base16-encoded ciphertexts of payload encrypted with masterKey & contentID + samples: map[string]string{ + "NONE": hex.EncodeToString([]byte("foo")), + + "AES256-GCM-HMAC-SHA256": "785c71de7c8ae8a5c0b5e2ad03f0be21620329", + "CHACHA20-POLY1305-HMAC-SHA256": "c93d644c5de803f017cad8ca331b7331e4cf55", + + // deprecated + "AES-128-CTR": "54cd8d", + "AES-192-CTR": "2d084b", + "AES-256-CTR": "8a580a", + "SALSA20": "bf5ec3", + "SALSA20-HMAC": "8bf37fd9ec69843c3c2ac2a2cfdd59f36077206a15289efde640d0e677d03e6ac8f8ec", + }, + }, + { + masterKey: []byte("01234567890123456789012345678901"), // 32 bytes + contentID: []byte("00000000000000000000000000000000"), // 32 bytes + payload: []byte("quick brown fox jumps over the lazy dog"), + + // samples of base16-encoded ciphertexts of payload encrypted with masterKey & contentID + samples: map[string]string{ + "NONE": hex.EncodeToString([]byte("quick brown fox jumps over the lazy dog")), + + "AES256-GCM-HMAC-SHA256": "e485b1f970e5d31f74b81c5b6336c3c5ef0de8f507943ce402b8ad3f282b8fd2e0b2554b13d0274ae088e119e2823f435bff9723b8201d", + "CHACHA20-POLY1305-HMAC-SHA256": "3e539c14afbcb990a546404bd0f0cb4d92c7d56593e04338dbb035aa38a75df37fcc42ebbe348ef13a1a40afcb55b1e2e3834b529388c4", + + // deprecated + "AES-128-CTR": "974c5c1782076e3de7255deabe8706a509b5772a8b7a8e7f83d01de7098c945934417071ec5351", + "AES-192-CTR": "1200e755ec14125e87136b5281957895eeb429be673b2241da261f949283aea59fd2fa64387764", + "AES-256-CTR": "39f13367828efb5fb22b97865ca0dbaad352d0c1a3083ff056bc771b812239445ed8af022f3760", + "SALSA20": "65ce12b14739aecbf9e6a9b9b9c4a72ffa8886fe0b071c0abdfb3d3e5c336b90f9af411ba69faf", + "SALSA20-HMAC": "a1dc47f250def4d97a422d505fb5e9a9a13699762cb32cfe7705982fa68ce71f54544ab932a1045fb0601087159954d563f0de0aaa15690d93ea63748bf91889e577daeeed5cf8", + }, + }} + + for _, tc := range cases { + verifyCiphertextSamples(t, tc.masterKey, tc.contentID, tc.payload, tc.samples) + } +} + +func verifyCiphertextSamples(t *testing.T, masterKey, contentID, payload []byte, samples map[string]string) { + for _, encryptionAlgo := range encryption.SupportedAlgorithms(true) { + enc, err := encryption.CreateEncryptor(parameters{encryptionAlgo, masterKey}) + if err != nil { + t.Fatal(err) + } + + ct := samples[encryptionAlgo] + if ct == "" { + v, err := enc.Encrypt(payload, contentID) + if err != nil { + t.Fatal(err) + } + + t.Errorf("missing ciphertext sample for %q: %q,", encryptionAlgo, hex.EncodeToString(v)) + } else { + b, err := hex.DecodeString(ct) + if err != nil { + t.Errorf("invalid ciphertext for %v: %v", encryptionAlgo, err) + continue + } + + plainText, err := enc.Decrypt(b, contentID) + if err != nil { + t.Errorf("unable to decrypt %v: %v", encryptionAlgo, err) + continue + } + + if !bytes.Equal(plainText, payload) { + t.Errorf("invalid plaintext after decryption %x, want %x", plainText, payload) + } + } + } +}