mirror of
https://github.com/Luzifer/go-openssl.git
synced 2024-12-20 19:01:18 +00:00
Initial version of go-openssl
This commit is contained in:
commit
5c6e757983
4 changed files with 347 additions and 0 deletions
63
README.md
Normal file
63
README.md
Normal file
|
@ -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`.
|
34
examples_test.go
Normal file
34
examples_test.go
Normal file
|
@ -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
|
||||
}
|
173
openssl.go
Normal file
173
openssl.go
Normal file
|
@ -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
|
||||
}
|
77
openssl_test.go
Normal file
77
openssl_test.go
Normal file
|
@ -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())
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue