commit 5c6e7579832374b7acd6bbd2d78e14d758b1f792 Author: Knut Ahlers Date: Fri Jul 17 23:49:39 2015 +0200 Initial version of go-openssl diff --git a/README.md b/README.md new file mode 100644 index 0000000..88026e6 --- /dev/null +++ b/README.md @@ -0,0 +1,63 @@ +# Luzifer / go-openssl + +`go-openssl` is a small library wrapping the `crypto/aes` functions in a way the output is compatible to OpenSSL. 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`. + +## Installation + +``` +go get github.com/Luzifer/go-openssl +``` + +## Usage example + +The usage is quite simple as you don't need any special knowledge about OpenSSL and/or AES256: + +### Encrypt + +```go +import ( + "fmt" + "github.com/Luzifer/go-openssl" +) + +func main() { + plaintext := "Hello World!" + passphrase := "z4yH36a6zerhfE5427ZV" + + o := openssl.New() + + enc, err := o.EncryptString(passphrase, plaintext) + if err != nil { + fmt.Printf("An error occurred: %s\n", err) + } + + fmt.Printf("Encrypted text: %s\n", string(enc)) +} +``` + +### Decrypt + +```go +import ( + "fmt" + "github.com/Luzifer/go-openssl" +) + +func main() { + opensslEncrypted := "U2FsdGVkX19ZM5qQJGe/d5A/4pccgH+arBGTp+QnWPU=" + passphrase := "z4yH36a6zerhfE5427ZV" + + o := openssl.New() + + dec, err := o.DecryptString(passphrase, opensslEncrypted) + if err != nil { + fmt.Printf("An error occurred: %s\n", err) + } + + fmt.Printf("Decrypted text: %s\n", string(dec)) +} +``` + +## Testing + +To execute the tests for this library you need to be on a system having `/bin/bash` and `openssl` available as the compatibility of the output is tested directly against the `openssl` binary. The library itself should be usable on all operating systems supported by Go and `crypto/aes`. diff --git a/examples_test.go b/examples_test.go new file mode 100644 index 0000000..ddb1e68 --- /dev/null +++ b/examples_test.go @@ -0,0 +1,34 @@ +package openssl + +import "fmt" + +func ExampleEncryptString() { + plaintext := "Hello World!" + passphrase := "z4yH36a6zerhfE5427ZV" + + o := New() + + enc, err := o.EncryptString(passphrase, plaintext) + if err != nil { + fmt.Printf("An error occurred: %s\n", err) + } + + fmt.Printf("Encrypted text: %s\n", string(enc)) +} + +func ExampleDecryptString() { + opensslEncrypted := "U2FsdGVkX19ZM5qQJGe/d5A/4pccgH+arBGTp+QnWPU=" + passphrase := "z4yH36a6zerhfE5427ZV" + + o := New() + + dec, err := o.DecryptString(passphrase, opensslEncrypted) + if err != nil { + fmt.Printf("An error occurred: %s\n", err) + } + + fmt.Printf("Decrypted text: %s\n", string(dec)) + + // Output: + // Decrypted text: hallowelt +} diff --git a/openssl.go b/openssl.go new file mode 100644 index 0000000..bdacae6 --- /dev/null +++ b/openssl.go @@ -0,0 +1,173 @@ +package openssl // import "github.com/Luzifer/go-openssl" + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/md5" + "crypto/rand" + "encoding/base64" + "fmt" + "io" +) + +// OpenSSL is a helper to generate OpenSSL compatible encryption +// with autmatic IV derivation and storage. As long as the key is known all +// data can also get decrypted using OpenSSL CLI. +// Code from http://dequeue.blogspot.de/2014/11/decrypting-something-encrypted-with.html +type OpenSSL struct { + openSSLSaltHeader string +} + +type openSSLCreds struct { + key []byte + iv []byte +} + +// New instanciates and initializes a new OpenSSL encrypter +func New() *OpenSSL { + return &OpenSSL{ + openSSLSaltHeader: "Salted__", // OpenSSL salt is always this string + 8 bytes of actual salt + } +} + +// DecryptString decrypts a string that was encrypted using OpenSSL and AES-256-CBC +func (o *OpenSSL) DecryptString(passphrase, encryptedBase64String string) ([]byte, error) { + data, err := base64.StdEncoding.DecodeString(encryptedBase64String) + if err != nil { + return nil, err + } + saltHeader := data[:aes.BlockSize] + if string(saltHeader[:8]) != o.openSSLSaltHeader { + return nil, fmt.Errorf("Does not appear to have been encrypted with OpenSSL, salt header missing.") + } + salt := saltHeader[8:] + creds, err := o.extractOpenSSLCreds([]byte(passphrase), salt) + if err != nil { + return nil, err + } + return o.decrypt(creds.key, creds.iv, data) +} + +func (o *OpenSSL) decrypt(key, iv, data []byte) ([]byte, error) { + if len(data) == 0 || len(data)%aes.BlockSize != 0 { + return nil, fmt.Errorf("bad blocksize(%v), aes.BlockSize = %v\n", len(data), aes.BlockSize) + } + c, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + cbc := cipher.NewCBCDecrypter(c, iv) + cbc.CryptBlocks(data[aes.BlockSize:], data[aes.BlockSize:]) + out, err := o.pkcs7Unpad(data[aes.BlockSize:], aes.BlockSize) + if out == nil { + return nil, err + } + return out, nil +} + +// EncryptString encrypts a string in a manner compatible to OpenSSL encryption +// functions using AES-256-CBC as encryption algorithm +func (o *OpenSSL) EncryptString(passphrase, plaintextString string) ([]byte, error) { + salt := make([]byte, 8) // Generate an 8 byte salt + _, err := io.ReadFull(rand.Reader, salt) + if err != nil { + return nil, err + } + + data := make([]byte, len(plaintextString)+aes.BlockSize) + copy(data[0:], o.openSSLSaltHeader) + copy(data[8:], salt) + copy(data[aes.BlockSize:], plaintextString) + + creds, err := o.extractOpenSSLCreds([]byte(passphrase), salt) + if err != nil { + return nil, err + } + + enc, err := o.encrypt(creds.key, creds.iv, data) + if err != nil { + return nil, err + } + + return []byte(base64.StdEncoding.EncodeToString(enc)), nil +} + +func (o *OpenSSL) encrypt(key, iv, data []byte) ([]byte, error) { + padded, err := o.pkcs7Pad(data, aes.BlockSize) + if err != nil { + return nil, err + } + + c, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + cbc := cipher.NewCBCEncrypter(c, iv) + cbc.CryptBlocks(padded[aes.BlockSize:], padded[aes.BlockSize:]) + + return padded, 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 +// 48 bytes since we're expecting to handle AES-256, 32bytes for a key and 16bytes for the IV +func (o *OpenSSL) extractOpenSSLCreds(password, salt []byte) (openSSLCreds, error) { + m := make([]byte, 48) + prev := []byte{} + for i := 0; i < 3; i++ { + prev = o.hash(prev, password, salt) + copy(m[i*16:], prev) + } + return openSSLCreds{key: m[:32], iv: m[32:]}, nil +} + +func (o *OpenSSL) hash(prev, password, salt []byte) []byte { + a := make([]byte, len(prev)+len(password)+len(salt)) + copy(a, prev) + copy(a[len(prev):], password) + copy(a[len(prev)+len(password):], salt) + return o.md5sum(a) +} + +func (o *OpenSSL) md5sum(data []byte) []byte { + h := md5.New() + h.Write(data) + return h.Sum(nil) +} + +// pkcs7Pad appends padding. +func (o *OpenSSL) pkcs7Pad(data []byte, blocklen int) ([]byte, error) { + if blocklen <= 0 { + return nil, fmt.Errorf("invalid blocklen %d", blocklen) + } + padlen := 1 + for ((len(data) + padlen) % blocklen) != 0 { + padlen = padlen + 1 + } + + pad := bytes.Repeat([]byte{byte(padlen)}, padlen) + return append(data, pad...), nil +} + +// pkcs7Unpad returns slice of the original data without padding. +func (o *OpenSSL) pkcs7Unpad(data []byte, blocklen int) ([]byte, error) { + if blocklen <= 0 { + return nil, fmt.Errorf("invalid blocklen %d", blocklen) + } + if len(data)%blocklen != 0 || len(data) == 0 { + return nil, fmt.Errorf("invalid data len %d", len(data)) + } + padlen := int(data[len(data)-1]) + if padlen > blocklen || padlen == 0 { + return nil, fmt.Errorf("invalid padding") + } + pad := data[len(data)-padlen:] + for i := 0; i < padlen; i++ { + if pad[i] != byte(padlen) { + return nil, fmt.Errorf("invalid padding") + } + } + return data[:len(data)-padlen], nil +} diff --git a/openssl_test.go b/openssl_test.go new file mode 100644 index 0000000..eca1acc --- /dev/null +++ b/openssl_test.go @@ -0,0 +1,77 @@ +package openssl + +import ( + "bytes" + "fmt" + "os/exec" + "testing" +) + +func TestDecryptFromString(t *testing.T) { + // > echo -n "hallowelt" | openssl aes-256-cbc -pass pass:z4yH36a6zerhfE5427ZV -a -salt + // U2FsdGVkX19ZM5qQJGe/d5A/4pccgH+arBGTp+QnWPU= + + opensslEncrypted := "U2FsdGVkX19ZM5qQJGe/d5A/4pccgH+arBGTp+QnWPU=" + passphrase := "z4yH36a6zerhfE5427ZV" + + o := New() + + data, err := o.DecryptString(passphrase, opensslEncrypted) + + if err != nil { + t.Fatalf("Test errored: %s", err) + } + + if string(data) != "hallowelt" { + t.Errorf("Decryption output did not equal expected output.") + } +} + +func TestEncryptToDecrypt(t *testing.T) { + plaintext := "hallowelt" + passphrase := "z4yH36a6zerhfE5427ZV" + + o := New() + + enc, err := o.EncryptString(passphrase, plaintext) + if err != nil { + t.Fatalf("Test errored at encrypt: %s", err) + } + + dec, err := o.DecryptString(passphrase, string(enc)) + if err != nil { + t.Fatalf("Test errored at decrypt: %s", err) + } + + if string(dec) != plaintext { + t.Errorf("Decrypted text did not match input.") + } +} + +func TestEncryptToOpenSSL(t *testing.T) { + plaintext := "hallowelt" + passphrase := "z4yH36a6zerhfE5427ZV" + + o := New() + + enc, err := o.EncryptString(passphrase, plaintext) + if err != nil { + t.Fatalf("Test errored at encrypt: %s", err) + } + + // WTF? Without "echo" openssl tells us "error reading input file" + cmd := exec.Command("/bin/bash", "-c", fmt.Sprintf("echo \"%s\" | openssl aes-256-cbc -k %s -d -a", string(enc), passphrase)) + + var out bytes.Buffer + cmd.Stdout = &out + cmd.Stderr = &out + + err = cmd.Run() + if err != nil { + t.Errorf("OpenSSL errored: %s", err) + } + + if out.String() != plaintext { + t.Errorf("OpenSSL output did not match input.\nOutput was: %s", out.String()) + } +}