From 8b7b49d5833ec675130fe7ddbd133f9af9982fe0 Mon Sep 17 00:00:00 2001 From: Knut Ahlers Date: Fri, 2 Nov 2018 12:37:30 +0100 Subject: [PATCH] Breaking: Fix race condition with guessing messagedigest - This removes messagedigest guessing as it might lead to race conditions during decryption when an md5 digest works as a valid key for the sha256 encrypted data - This removes deprecated functions (the previous point introduces a breaking change so this does not introduce another one) fixes #10 Signed-off-by: Knut Ahlers --- README.md | 20 ++++++- examples_test.go | 4 +- openssl.go | 143 +++++++++++++++++------------------------------ openssl_test.go | 35 ++++++------ 4 files changed, 87 insertions(+), 115 deletions(-) diff --git a/README.md b/README.md index 7a52276..a04dd26 100644 --- a/README.md +++ b/README.md @@ -8,14 +8,30 @@ `go-openssl` is a small library wrapping the `crypto/aes` functions in a way the output is compatible to OpenSSL / CryptoJS. For all encryption / decryption processes AES256 is used so this library will not be able to decrypt messages generated with other than `openssl aes-256-cbc`. If you're using CryptoJS to process the data you also need to use AES256 on that side. -## OpenSSL 1.1.0c compatibility +## Version support + +For this library only the latest major version is supported. All prior major versions should no longer be used. + +The versioning is following [SemVer](https://semver.org/) which means upgrading to a newer major version will break your code! + +## OpenSSL compatibility + +### 1.1.0c Starting with `v2.0.0` `go-openssl` generates the encryption keys using `sha256sum` algorithm. This is the default introduced in OpenSSL 1.1.0c. When encrypting data you can choose which digest method to use and therefore also continue to use `md5sum`. When decrypting OpenSSL encrypted data `md5sum`, `sha1sum` and `sha256sum` are supported. +### 1.1.1 + +The PBKDF2 key derivation is not yet supported. + ## Installation -``` +```bash +# Get the latest version go get github.com/Luzifer/go-openssl + +# OR get a specific version +go get gopkg.in/Luzifer/go-openssl.v3 ``` ## Usage example diff --git a/examples_test.go b/examples_test.go index 9cf008c..612ebab 100644 --- a/examples_test.go +++ b/examples_test.go @@ -8,7 +8,7 @@ func ExampleOpenSSL_EncryptString() { o := New() - enc, err := o.EncryptString(passphrase, plaintext) + enc, err := o.EncryptBytes(passphrase, []byte(plaintext), DigestSHA256Sum) if err != nil { fmt.Printf("An error occurred: %s\n", err) } @@ -22,7 +22,7 @@ func ExampleOpenSSL_DecryptString() { o := New() - dec, err := o.DecryptString(passphrase, opensslEncrypted) + dec, err := o.DecryptBytes(passphrase, []byte(opensslEncrypted), DigestMD5Sum) if err != nil { fmt.Printf("An error occurred: %s\n", err) } diff --git a/openssl.go b/openssl.go index e99e747..7931f56 100644 --- a/openssl.go +++ b/openssl.go @@ -14,6 +14,9 @@ import ( "io" ) +// CurrentOpenSSLDigestFunc is an alias to the key derivation function used in OpenSSL +var CurrentOpenSSLDigestFunc = DigestSHA256Sum + // ErrInvalidSalt is returned when a salt with a length of != 8 byte is passed var ErrInvalidSalt = errors.New("Salt needs to have exactly 8 byte") @@ -37,23 +40,37 @@ func New() *OpenSSL { } } -// DecryptString decrypts a string that was encrypted using OpenSSL and AES-256-CBC -func (o OpenSSL) DecryptString(passphrase, encryptedBase64String string) ([]byte, error) { - return o.DecryptBytes(passphrase, []byte(encryptedBase64String)) +// DigestFunc are functions to create a key from the passphrase +type DigestFunc func([]byte) []byte + +// DigestMD5Sum uses the (deprecated) pre-OpenSSL 1.1.0c MD5 digest to create the key +func DigestMD5Sum(data []byte) []byte { + h := md5.New() + h.Write(data) + return h.Sum(nil) } -var hashFuncList = []DigestFunc{DigestSHA256Sum, DigestMD5Sum, DigestSHA1Sum} +// DigestSHA1Sum uses SHA1 digest to create the key +func DigestSHA1Sum(data []byte) []byte { + h := sha1.New() + h.Write(data) + return h.Sum(nil) +} -func (o OpenSSL) decodeWithPassphrase(passphrase string, data []byte, salt []byte, hashFunc DigestFunc) ([]byte, error) { - creds, err := o.extractOpenSSLCreds([]byte(passphrase), salt, hashFunc) - if err != nil { - return nil, err - } - return o.decrypt(creds.key, creds.iv, data) +// DigestSHA256Sum uses SHA256 digest to create the key which is the default behaviour since OpenSSL 1.1.0c +func DigestSHA256Sum(data []byte) []byte { + h := sha256.New() + h.Write(data) + return h.Sum(nil) } // DecryptBytes takes a slice of bytes with base64 encoded, encrypted data to decrypt -func (o OpenSSL) DecryptBytes(passphrase string, encryptedBase64Data []byte) ([]byte, error) { +// and a key-derivation function. The key-derivation function must match the function +// used to encrypt the data. (In OpenSSL the value of the `-md` parameter.) +// +// You should not just try to loop the digest functions as this will cause a race +// condition and you will not be able to decrypt your data properly. +func (o OpenSSL) DecryptBytes(passphrase string, encryptedBase64Data []byte, kdf DigestFunc) ([]byte, error) { data := make([]byte, base64.StdEncoding.DecodedLen(len(encryptedBase64Data))) n, err := base64.StdEncoding.Decode(data, encryptedBase64Data) if err != nil { @@ -72,15 +89,11 @@ func (o OpenSSL) DecryptBytes(passphrase string, encryptedBase64Data []byte) ([] } salt := saltHeader[8:] - tmp := make([]byte, len(data)) - for _, f := range hashFuncList { - copy(tmp, data) - result, err := o.decodeWithPassphrase(passphrase, tmp, salt, f) - if err == nil { - return result, nil - } + creds, err := o.extractOpenSSLCreds([]byte(passphrase), salt, kdf) + if err != nil { + return nil, err } - return nil, err + return o.decrypt(creds.key, creds.iv, data) } func (o OpenSSL) decrypt(key, iv, data []byte) ([]byte, error) { @@ -103,55 +116,13 @@ func (o OpenSSL) decrypt(key, iv, data []byte) ([]byte, error) { // EncryptBytes encrypts a slice of bytes in a manner compatible to OpenSSL encryption // functions using AES-256-CBC as encryption algorithm. This function generates // a random salt on every execution. -func (o OpenSSL) EncryptBytes(passphrase string, plainData []byte) ([]byte, error) { +func (o OpenSSL) EncryptBytes(passphrase string, plainData []byte, kdf DigestFunc) ([]byte, error) { salt, err := o.GenerateSalt() if err != nil { return nil, err } - return o.EncryptBytesWithSaltAndDigestFunc(passphrase, salt, plainData, DigestSHA256Sum) -} - -// EncryptString encrypts a string in a manner compatible to OpenSSL encryption -// functions using AES-256-CBC as encryption algorithm. This function generates -// a random salt on every execution. -func (o OpenSSL) EncryptString(passphrase, plaintextString string) ([]byte, error) { - salt, err := o.GenerateSalt() - if err != nil { - return nil, err - } - - return o.EncryptBytesWithSaltAndDigestFunc(passphrase, salt, []byte(plaintextString), DigestSHA256Sum) -} - -// EncryptStringWithSalt encrypts a string in a manner compatible to OpenSSL -// encryption functions using AES-256-CBC as encryption algorithm. The salt -// needs to be passed in here which ensures the same result on every execution -// on cost of a much weaker encryption as with EncryptString. -// -// The salt passed into this function needs to have exactly 8 byte. -// -// If you don't have a good reason to use this, please don't! For more information -// see this: https://en.wikipedia.org/wiki/Salt_(cryptography)#Common_mistakes -// -// Deprecated: Use EncryptBytesWithSaltAndDigestFunc instead. -func (o OpenSSL) EncryptStringWithSalt(passphrase string, salt []byte, plaintextString string) ([]byte, error) { - return o.EncryptBytesWithSaltAndDigestFunc(passphrase, salt, []byte(plaintextString), DigestSHA256Sum) -} - -// EncryptBytesWithSalt encrypts a slice of bytes in a manner compatible to OpenSSL -// encryption functions using AES-256-CBC as encryption algorithm. The salt -// needs to be passed in here which ensures the same result on every execution -// on cost of a much weaker encryption as with EncryptString. -// -// The salt passed into this function needs to have exactly 8 byte. -// -// If you don't have a good reason to use this, please don't! For more information -// see this: https://en.wikipedia.org/wiki/Salt_(cryptography)#Common_mistakes -// -// Deprecated: Use EncryptBytesWithSaltAndDigestFunc instead. -func (o OpenSSL) EncryptBytesWithSalt(passphrase string, salt, plainData []byte) ([]byte, error) { - return o.EncryptBytesWithSaltAndDigestFunc(passphrase, salt, plainData, DigestSHA256Sum) + return o.EncryptBytesWithSaltAndDigestFunc(passphrase, salt, plainData, kdf) } // EncryptBytesWithSaltAndDigestFunc encrypts a slice of bytes in a manner compatible to OpenSSL @@ -189,17 +160,6 @@ func (o OpenSSL) EncryptBytesWithSaltAndDigestFunc(passphrase string, salt, plai return []byte(base64.StdEncoding.EncodeToString(enc)), nil } -// GenerateSalt generates a random 8 byte salt -func (o OpenSSL) GenerateSalt() ([]byte, error) { - salt := make([]byte, 8) // Generate an 8 byte salt - _, err := io.ReadFull(rand.Reader, salt) - if err != nil { - return nil, err - } - - return salt, nil -} - func (o OpenSSL) encrypt(key, iv, data []byte) ([]byte, error) { padded, err := o.pkcs7Pad(data, aes.BlockSize) if err != nil { @@ -238,28 +198,25 @@ func (o OpenSSL) hash(prev, password, salt []byte, hashFunc DigestFunc) []byte { return hashFunc(a) } -// DigestFunc are functions to create a key from the passphrase -type DigestFunc func([]byte) []byte +// GenerateSalt generates a random 8 byte salt +func (o OpenSSL) GenerateSalt() ([]byte, error) { + salt := make([]byte, 8) // Generate an 8 byte salt + _, err := io.ReadFull(rand.Reader, salt) + if err != nil { + return nil, err + } -// DigestMD5Sum uses the (deprecated) pre-OpenSSL 1.1.0c MD5 digest to create the key -func DigestMD5Sum(data []byte) []byte { - h := md5.New() - h.Write(data) - return h.Sum(nil) + return salt, nil } -// DigestSHA1Sum uses SHA1 digest to create the key -func DigestSHA1Sum(data []byte) []byte { - h := sha1.New() - h.Write(data) - return h.Sum(nil) -} - -// DigestSHA256Sum uses SHA256 digest to create the key which is the default behaviour since OpenSSL 1.1.0c -func DigestSHA256Sum(data []byte) []byte { - h := sha256.New() - h.Write(data) - return h.Sum(nil) +// MustGenerateSalt is a wrapper around GenerateSalt which will panic on an error. +// This allows you to use this function as a parameter to EncryptBytesWithSaltAndDigestFunc +func (o OpenSSL) MustGenerateSalt() []byte { + s, err := o.GenerateSalt() + if err != nil { + panic(err) + } + return s } // pkcs7Pad appends padding. diff --git a/openssl_test.go b/openssl_test.go index 298e7e8..3bb7516 100644 --- a/openssl_test.go +++ b/openssl_test.go @@ -16,7 +16,7 @@ func TestDecryptFromStringMD5(t *testing.T) { o := New() - data, err := o.DecryptString(passphrase, opensslEncrypted) + data, err := o.DecryptBytes(passphrase, []byte(opensslEncrypted), DigestMD5Sum) if err != nil { t.Fatalf("Test errored: %s", err) @@ -36,7 +36,7 @@ func TestDecryptFromStringSHA1(t *testing.T) { o := New() - data, err := o.DecryptString(passphrase, opensslEncrypted) + data, err := o.DecryptBytes(passphrase, []byte(opensslEncrypted), DigestSHA1Sum) if err != nil { t.Fatalf("Test errored: %s", err) @@ -56,7 +56,7 @@ func TestDecryptFromStringSHA256(t *testing.T) { o := New() - data, err := o.DecryptString(passphrase, opensslEncrypted) + data, err := o.DecryptBytes(passphrase, []byte(opensslEncrypted), DigestSHA256Sum) if err != nil { t.Fatalf("Test errored: %s", err) @@ -73,12 +73,12 @@ func TestEncryptToDecrypt(t *testing.T) { o := New() - enc, err := o.EncryptString(passphrase, plaintext) + enc, err := o.EncryptBytes(passphrase, []byte(plaintext), DigestSHA256Sum) if err != nil { t.Fatalf("Test errored at encrypt: %s", err) } - dec, err := o.DecryptString(passphrase, string(enc)) + dec, err := o.DecryptBytes(passphrase, enc, DigestSHA256Sum) if err != nil { t.Fatalf("Test errored at decrypt: %s", err) } @@ -95,12 +95,12 @@ func TestEncryptToDecryptWithCustomSalt(t *testing.T) { o := New() - enc, err := o.EncryptStringWithSalt(passphrase, salt, plaintext) + enc, err := o.EncryptBytesWithSaltAndDigestFunc(passphrase, salt, []byte(plaintext), DigestSHA256Sum) if err != nil { t.Fatalf("Test errored at encrypt: %s", err) } - dec, err := o.DecryptString(passphrase, string(enc)) + dec, err := o.DecryptBytes(passphrase, enc, DigestSHA256Sum) if err != nil { t.Fatalf("Test errored at decrypt: %s", err) } @@ -117,12 +117,12 @@ func TestEncryptWithSaltShouldHaveSameOutput(t *testing.T) { o := New() - enc1, err := o.EncryptStringWithSalt(passphrase, salt, plaintext) + enc1, err := o.EncryptBytesWithSaltAndDigestFunc(passphrase, salt, []byte(plaintext), DigestSHA256Sum) if err != nil { t.Fatalf("Test errored at encrypt: %s", err) } - enc2, err := o.EncryptStringWithSalt(passphrase, salt, plaintext) + enc2, err := o.EncryptBytesWithSaltAndDigestFunc(passphrase, salt, []byte(plaintext), DigestSHA256Sum) if err != nil { t.Fatalf("Test errored at encrypt: %s", err) } @@ -160,7 +160,6 @@ func TestEncryptToOpenSSL(t *testing.T) { var out bytes.Buffer cmd.Stdout = &out - cmd.Stderr = &out err = cmd.Run() if err != nil { @@ -199,38 +198,38 @@ func TestSaltValidation(t *testing.T) { o := New() - if _, err := o.EncryptStringWithSalt(passphrase, []byte("12345"), plaintext); err != ErrInvalidSalt { + if _, err := o.EncryptBytesWithSaltAndDigestFunc(passphrase, []byte("12345"), []byte(plaintext), DigestSHA256Sum); err != ErrInvalidSalt { t.Errorf("5-character salt was accepted, needs to have 8 character") } - if _, err := o.EncryptStringWithSalt(passphrase, []byte("1234567890"), plaintext); err != ErrInvalidSalt { + if _, err := o.EncryptBytesWithSaltAndDigestFunc(passphrase, []byte("1234567890"), []byte(plaintext), DigestSHA256Sum); err != ErrInvalidSalt { t.Errorf("10-character salt was accepted, needs to have 8 character") } - if _, err := o.EncryptStringWithSalt(passphrase, []byte{0xcb, 0xd5, 0x1a, 0x3, 0x84, 0xba, 0xa8, 0xc8}, plaintext); err == ErrInvalidSalt { + if _, err := o.EncryptBytesWithSaltAndDigestFunc(passphrase, []byte{0xcb, 0xd5, 0x1a, 0x3, 0x84, 0xba, 0xa8, 0xc8}, []byte(plaintext), DigestSHA256Sum); err == ErrInvalidSalt { t.Errorf("Salt with 8 byte unprintable characters was not accepted") } } -func benchmarkDecrypt(ciphertext string, b *testing.B) { +func benchmarkDecrypt(ciphertext []byte, kdf DigestFunc, b *testing.B) { passphrase := "z4yH36a6zerhfE5427ZV" o := New() for n := 0; n < b.N; n++ { - o.DecryptString(passphrase, ciphertext) + o.DecryptBytes(passphrase, ciphertext, kdf) } } func BenchmarkDecryptMD5(b *testing.B) { - benchmarkDecrypt("U2FsdGVkX19ZM5qQJGe/d5A/4pccgH+arBGTp+QnWPU=", b) + benchmarkDecrypt([]byte("U2FsdGVkX19ZM5qQJGe/d5A/4pccgH+arBGTp+QnWPU="), DigestMD5Sum, b) } func BenchmarkDecryptSHA1(b *testing.B) { - benchmarkDecrypt("U2FsdGVkX1/Yy9kegseq2Ewd4UvjFYCpIEA1cltTA1Q=", b) + benchmarkDecrypt([]byte("U2FsdGVkX1/Yy9kegseq2Ewd4UvjFYCpIEA1cltTA1Q="), DigestSHA1Sum, b) } func BenchmarkDecryptSHA256(b *testing.B) { - benchmarkDecrypt("U2FsdGVkX1+O68d7BO9ibP8nB5+xtb/27IHlyjJWpl8=", b) + benchmarkDecrypt([]byte("U2FsdGVkX1+O68d7BO9ibP8nB5+xtb/27IHlyjJWpl8="), DigestSHA256Sum, b) } func benchmarkEncrypt(plaintext string, hashFunc DigestFunc, b *testing.B) {