diff --git a/openssl.go b/openssl.go index 7931f56..b5a6b40 100644 --- a/openssl.go +++ b/openssl.go @@ -80,10 +80,24 @@ func (o OpenSSL) DecryptBytes(passphrase string, encryptedBase64Data []byte, kdf // Truncate to real message length data = data[0:n] - if len(data) < aes.BlockSize { + decrypted, err := o.DecryptBinaryBytes(passphrase, data, kdf) + if err != nil { + return nil, err + } + return decrypted, nil +} + +// DecryptBinaryBytes takes a slice of binary bytes, encrypted data to decrypt +// 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) DecryptBinaryBytes(passphrase string, encryptedData []byte, kdf DigestFunc) ([]byte, error) { + if len(encryptedData) < aes.BlockSize { return nil, fmt.Errorf("Data is too short") } - saltHeader := data[:aes.BlockSize] + saltHeader := encryptedData[:aes.BlockSize] if string(saltHeader[:8]) != o.openSSLSaltHeader { return nil, fmt.Errorf("Does not appear to have been encrypted with OpenSSL, salt header missing") } @@ -93,7 +107,7 @@ func (o OpenSSL) DecryptBytes(passphrase string, encryptedBase64Data []byte, kdf if err != nil { return nil, err } - return o.decrypt(creds.key, creds.iv, data) + return o.decrypt(creds.key, creds.iv, encryptedData) } func (o OpenSSL) decrypt(key, iv, data []byte) ([]byte, error) { @@ -113,7 +127,7 @@ func (o OpenSSL) decrypt(key, iv, data []byte) ([]byte, error) { return out, nil } -// EncryptBytes encrypts a slice of bytes in a manner compatible to OpenSSL encryption +// EncryptBytes encrypts a slice of bytes that are base64 encoded 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, kdf DigestFunc) ([]byte, error) { @@ -125,7 +139,19 @@ func (o OpenSSL) EncryptBytes(passphrase string, plainData []byte, kdf DigestFun return o.EncryptBytesWithSaltAndDigestFunc(passphrase, salt, plainData, kdf) } -// EncryptBytesWithSaltAndDigestFunc encrypts a slice of bytes in a manner compatible to OpenSSL +// EncryptBinaryBytes 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) EncryptBinaryBytes(passphrase string, plainData []byte, kdf DigestFunc) ([]byte, error) { + salt, err := o.GenerateSalt() + if err != nil { + return nil, err + } + + return o.EncryptBinaryBytesWithSaltAndDigestFunc(passphrase, salt, plainData, kdf) +} + +// EncryptBytesWithSaltAndDigestFunc encrypts a slice of bytes that are base64 encoded 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. @@ -138,21 +164,7 @@ func (o OpenSSL) EncryptBytes(passphrase string, plainData []byte, kdf DigestFun // 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 func (o OpenSSL) EncryptBytesWithSaltAndDigestFunc(passphrase string, salt, plainData []byte, hashFunc DigestFunc) ([]byte, error) { - if len(salt) != 8 { - return nil, ErrInvalidSalt - } - - data := make([]byte, len(plainData)+aes.BlockSize) - copy(data[0:], o.openSSLSaltHeader) - copy(data[8:], salt) - copy(data[aes.BlockSize:], plainData) - - creds, err := o.extractOpenSSLCreds([]byte(passphrase), salt, hashFunc) - if err != nil { - return nil, err - } - - enc, err := o.encrypt(creds.key, creds.iv, data) + enc, err := o.EncryptBinaryBytesWithSaltAndDigestFunc(passphrase, salt, plainData, hashFunc) if err != nil { return nil, err } @@ -176,6 +188,41 @@ func (o OpenSSL) encrypt(key, iv, data []byte) ([]byte, error) { return padded, nil } +// EncryptBinaryBytesWithSaltAndDigestFunc 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. +// +// The hash function corresponds to the `-md` parameter of OpenSSL. For OpenSSL pre-1.1.0c +// DigestMD5Sum was the default, since then it is DigestSHA256Sum. +// +// 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 +func (o OpenSSL) EncryptBinaryBytesWithSaltAndDigestFunc(passphrase string, salt, plainData []byte, hashFunc DigestFunc) ([]byte, error) { + if len(salt) != 8 { + return nil, ErrInvalidSalt + } + + data := make([]byte, len(plainData)+aes.BlockSize) + copy(data[0:], o.openSSLSaltHeader) + copy(data[8:], salt) + copy(data[aes.BlockSize:], plainData) + + creds, err := o.extractOpenSSLCreds([]byte(passphrase), salt, hashFunc) + if err != nil { + return nil, err + } + + enc, err := o.encrypt(creds.key, creds.iv, data) + if err != nil { + return nil, err + } + + return enc, nil +} + // openSSLEvpBytesToKey follows the OpenSSL (undocumented?) convention for extracting the key and IV from passphrase. // It uses the EVP_BytesToKey() method which is basically: // D_i = HASH^count(D_(i-1) || password || salt) where || denotes concatentaion, until there are sufficient bytes available diff --git a/openssl_test.go b/openssl_test.go index 3bb7516..8ea8071 100644 --- a/openssl_test.go +++ b/openssl_test.go @@ -67,6 +67,60 @@ func TestDecryptFromStringSHA256(t *testing.T) { } } +func TestDecryptBinaryFromString(t *testing.T) { + + plaintext := "hallowelt" + passphrase := "z4yH36a6zerhfE5427ZV" + + testtable := + []struct { + tname string + tMdParam string + tMdFunc DigestFunc + }{ + { + tname: "MD5", + tMdParam: "md5", + tMdFunc: DigestMD5Sum, + }, + { + tname: "SHA1", + tMdParam: "sha1", + tMdFunc: DigestSHA1Sum, + }, + { + tname: "SHA256", + tMdParam: "sha256", + tMdFunc: DigestSHA256Sum, + }, + } + + o := New() + + for _, tc := range testtable { + t.Run(tc.tname, func(t *testing.T) { + cmd := exec.Command("/bin/bash", "-c", fmt.Sprintf("echo -n \"%s\" | openssl aes-256-cbc -pass pass:%s -md %s", plaintext, passphrase, tc.tMdParam)) + var out bytes.Buffer + cmd.Stdout = &out + err := cmd.Run() + if err != nil { + t.Fatalf("Running openssl CLI failed: %v", err) + } + + data, err := o.DecryptBinaryBytes(passphrase, out.Bytes(), tc.tMdFunc) + + if err != nil { + t.Fatalf("Decryption failed: %v", err) + } + + if string(data) != plaintext { + t.Logf("Data: %s\nPlaintext: %s", string(data), plaintext) + t.Errorf("Decryption output did not equal expected output.") + } + }) + } +} + func TestEncryptToDecrypt(t *testing.T) { plaintext := "hallowelt" passphrase := "z4yH36a6zerhfE5427ZV" @@ -88,6 +142,27 @@ func TestEncryptToDecrypt(t *testing.T) { } } +func TestBinaryEncryptToDecrypt(t *testing.T) { + plaintext := "hallowelt" + passphrase := "z4yH36a6zerhfE5427ZV" + + o := New() + + enc, err := o.EncryptBinaryBytes(passphrase, []byte(plaintext), DigestSHA256Sum) + if err != nil { + t.Fatalf("Test errored at encrypt: %s", err) + } + + dec, err := o.DecryptBinaryBytes(passphrase, enc, DigestSHA256Sum) + if err != nil { + t.Fatalf("Test errored at decrypt: %s", err) + } + + if string(dec) != plaintext { + t.Errorf("Decrypted text did not match input.") + } +} + func TestEncryptToDecryptWithCustomSalt(t *testing.T) { plaintext := "hallowelt" passphrase := "z4yH36a6zerhfE5427ZV" @@ -110,6 +185,28 @@ func TestEncryptToDecryptWithCustomSalt(t *testing.T) { } } +func TestBinaryEncryptToDecryptWithCustomSalt(t *testing.T) { + plaintext := "hallowelt" + passphrase := "z4yH36a6zerhfE5427ZV" + salt := []byte("saltsalt") + + o := New() + + enc, err := o.EncryptBinaryBytesWithSaltAndDigestFunc(passphrase, salt, []byte(plaintext), DigestSHA256Sum) + if err != nil { + t.Fatalf("Test errored at encrypt: %s", err) + } + + dec, err := o.DecryptBinaryBytes(passphrase, enc, DigestSHA256Sum) + if err != nil { + t.Fatalf("Test errored at decrypt: %s", err) + } + + if string(dec) != plaintext { + t.Errorf("Decrypted text did not match input.") + } +} + func TestEncryptWithSaltShouldHaveSameOutput(t *testing.T) { plaintext := "outputshouldbesame" passphrase := "passphrasesupersecure" @@ -132,6 +229,28 @@ func TestEncryptWithSaltShouldHaveSameOutput(t *testing.T) { } } +func TestBinaryEncryptWithSaltShouldHaveSameOutput(t *testing.T) { + plaintext := "outputshouldbesame" + passphrase := "passphrasesupersecure" + salt := []byte("saltsalt") + + o := New() + + enc1, err := o.EncryptBinaryBytesWithSaltAndDigestFunc(passphrase, salt, []byte(plaintext), DigestSHA256Sum) + if err != nil { + t.Fatalf("Test errored at encrypt: %s", err) + } + + enc2, err := o.EncryptBinaryBytesWithSaltAndDigestFunc(passphrase, salt, []byte(plaintext), DigestSHA256Sum) + if err != nil { + t.Fatalf("Test errored at encrypt: %s", err) + } + + if string(enc1) != string(enc2) { + t.Errorf("Encrypted outputs are not same.") + } +} + func TestEncryptToOpenSSL(t *testing.T) { plaintext := "hallowelt" passphrase := "z4yH36a6zerhfE5427ZV" @@ -172,6 +291,67 @@ func TestEncryptToOpenSSL(t *testing.T) { } } +func TestBinaryEncryptToOpenSSL(t *testing.T) { + plaintext := "hallowelt" + passphrase := "z4yH36a6zerhfE5427ZV" + + testtable := + []struct { + tname string + tMdParam string + tMdFunc DigestFunc + }{ + { + tname: "MD5", + tMdParam: "md5", + tMdFunc: DigestMD5Sum, + }, + { + tname: "SHA1", + tMdParam: "sha1", + tMdFunc: DigestSHA1Sum, + }, + { + tname: "SHA256", + tMdParam: "sha256", + tMdFunc: DigestSHA256Sum, + }, + } + + o := New() + + for _, tc := range testtable { + t.Run(tc.tname, func(t *testing.T) { + salt, err := o.GenerateSalt() + if err != nil { + t.Fatalf("Failed to generate salt: %v", err) + } + + enc, err := o.EncryptBinaryBytesWithSaltAndDigestFunc(passphrase, salt, []byte(plaintext), tc.tMdFunc) + if err != nil { + t.Fatalf("Test errored at encrypt: %v", err) + } + + // Need to specify /dev/stdin as file so that we can pass in binary + // data to openssl without creating a file + cmd := exec.Command("/bin/bash", "-c", fmt.Sprintf("openssl aes-256-cbc -pass pass:%s -md %s -d -in /dev/stdin", passphrase, tc.tMdParam)) + + var out bytes.Buffer + cmd.Stdout = &out + cmd.Stdin = bytes.NewBuffer(enc) + + err = cmd.Run() + if err != nil { + t.Errorf("OpenSSL errored: %v", err) + } + + if out.String() != plaintext { + t.Errorf("OpenSSL output did not match input.\nOutput was: %s", out.String()) + } + }) + } +} + func TestGenerateSalt(t *testing.T) { knownSalts := [][]byte{}