mirror of
https://github.com/Luzifer/go-openssl.git
synced 2024-12-20 19:01:18 +00:00
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 <knut@ahlers.me>
This commit is contained in:
parent
5d2bdfffee
commit
8b7b49d583
4 changed files with 87 additions and 115 deletions
20
README.md
20
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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
143
openssl.go
143
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 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.
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Reference in a new issue