goRailsYourself: github.com/mattetti/goRailsYourself/crypto Index | Examples | Files

package crypto

import "github.com/mattetti/goRailsYourself/crypto"

This crypto package ports some of Ruby on Rails' crypto (version 4+) logic so encrypted/signed messages can be shared between a Ruby app and a Go app. That said, this library is useful to anyone wanting to encrypt/sign data.

The initial focus of this package was to be able to easily share a Rails web session with a Go app. Rails uses three classes provided by ActiveSupport (a library used and maintained by the Rails team)

* MessageEncryptor
* MessageVerifier
* KeyGenerator

to encrypt and sign sessions. In order to read/write a cookie session, a Go app needs to be able to verify, decrypt/encrypt sign the session data based on a shared secret.

Key components of this package

The main components of this package are:

* MessageEncryptor
* MessageVerifier
* KeyGenerator

The difference between MessageVerifier and MessageEncryptor is that you want to use MessageEncryptor when you don't want the content of the data to be available to people accessing the data. In both cases, the data is signed but if the message is just signed, the content can be read.

Keygenerator is used to generate derived keys from a given secret. If you want to generate a random key that isn't derived, look at the GenerateRandomKey function.

Session serializer

As I'm writing this documentation, Rails doesn't let you change the default session serializer (Marhsal). To be able to share the session it needs to be serialized in a cross language format. I wrote a patch to change Rails' default serializer to JSON: https://gist.github.com/mattetti/7624413 Hopefully, an API will soon be available and JSON will become the default serializer. This package can use different serializers and you can also add your own. This is useful if for instance you only have Go apps and choose to use gob encoding or another encoding solution. Three serializers are available JSON, XML and Null, the last serializer is basically a no-op serializer used when the data doesn't need serialization and can be transported as strings.

Rails session flow

It's important to understand how Rails handles the crypto around the session. Here is a quick and high level of what Rails does (Ruby code):

# Secret set in the app.
 secret_key_base = "f7b5763636f4c1f3ff4bd444eacccca295d87b990cc104124017ad70550edcfd22b8e89465338254e0b608592a9aac29025440bfd9ce53579835ba06a86f85f9"

 key_generator = ActiveSupport::CachingKeyGenerator.new(ActiveSupport::KeyGenerator.new(secret_key_base, iterations: 1000))
 secret = key_generator.generate_key("encrypted cookie")
 sign_secret = key_generator.generate_key("signed encrypted cookie")

 encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, { serializer: JsonSessionSerializer } )
 # encrypt and sign the content of the session:
 encrypted_message = encryptor.encrypt_and_sign({msg: "hello world"})
 # The encrypted and signed message is stored in the session cookie
 # To decrypt and verify it:
 # encryptor.decrypt_and_verify(encrypted_message) # => {:msg => "hello world"}

The equivalent in Go is available in the documentation examples: http://godoc.org/github.com/mattetti/goRailsYourself/crypto#pkg-examples

Derived keys

A few important things need to be mentioned. Rails uses a unique secret that is used to derive different keys using a default salt. To read more about this process, see http://en.wikipedia.org/wiki/PBKDF2

Rails defaults to 1000 iterations when generating the derived keys, when generating the keys in Go, we need to match the iteration number to get the same keys. Note also that if the salt is changed in the Rails app, you need to update it in your Go code.

There are two derived keys: one for encryption and one for signing. These keys are derived from the same secret but are different to increase security. The keys are also of two different length. The message signature is done by default using HMAC/sha1 requiring a key of 64 bytes. However, the message is encrypted by default using AES-256 CBC requiring a key of 32 bytes. Ruby's openssl library and this package automatically truncate longer AES CBC keys so you can use two 64 byte keys. This is exactly what Rails does, it generates two keys of same length (64 bytes) and lets the OpenSSL wrapper truncate the key. I, however recommend you generate keys of different length to avoid any confusion. Here is an example:

railsSecret := "f7b5763636f4c1f3ff4bd444eacccca295d87b990cc104124017ad70550edcfd22b8e89465338254e0b608592a9aac29025440bfd9ce53579835ba06a86f85f9"
encryptedCookieSalt := []byte("encrypted cookie")
encryptedSignedCookieSalt := []byte("signed encrypted cookie")

kg := KeyGenerator{Secret: railsSecret}
secret := kg.CacheGenerate(encryptedCookieSalt, 32)
signSecret := kg.CacheGenerate(encryptedSignedCookieSalt, 64)
e := MessageEncryptor{Key: secret, SignKey: signSecret}

Without Ruby

The encryption used in Rails isn't specific to Ruby and this library can be used to communicate with apps that aren't in Ruby. As a matter of fact, you might want to use this library to encrypt/sign your web sessions/cookies even if you just have one Go app. The Rails implementation has been tested and vested by many people and is safe to use.

Index

Examples

Package Files

aes_cbc.go crypto.go doc.go json_msg_serializer.go key_generator.go message_encryptor.go message_verifier.go null_msg_serializer.go pkcs7_padding.go xml_msg_serializer.go

func GenerateRandomKey Uses

func GenerateRandomKey(strength int) []byte

Generates a random key of the passed length. As a reminder, for AES keys of length 16, 24, or 32 bytes are expected for AES-128, AES-192, or AES-256.

func PKCS7Pad Uses

func PKCS7Pad(data []byte) []byte

PKCS7Pad() pads an byte array to be a multiple of 16 http://tools.ietf.org/html/rfc5652#section-6.3

func PKCS7Unpad Uses

func PKCS7Unpad(data []byte) []byte

PKCS7Unpad() removes any potential PKCS7 padding added.

type JsonMsgSerializer Uses

type JsonMsgSerializer struct {
}

func (JsonMsgSerializer) Serialize Uses

func (s JsonMsgSerializer) Serialize(v interface{}) (string, error)

func (JsonMsgSerializer) Unserialize Uses

func (s JsonMsgSerializer) Unserialize(data string, v interface{}) error

type KeyGenerator Uses

type KeyGenerator struct {
    Secret     string
    Iterations int
    // contains filtered or unexported fields
}

KeyGenerator is a simple wrapper around a PBKDF2 implementation. It can be used to derive a number of keys for various purposes from a given secret. This lets applications have a single secure secret, but avoid reusing that key in multiple incompatible contexts.

func (*KeyGenerator) CacheGenerate Uses

func (g *KeyGenerator) CacheGenerate(salt []byte, keySize int) []byte

CacheGenerate() write through cache used to save generated keys.

func (*KeyGenerator) Generate Uses

func (g *KeyGenerator) Generate(salt []byte, keySize int) []byte

Generates a derived key based on a salt. rails default key size is 64.

type MessageEncryptor Uses

type MessageEncryptor struct {
    Key []byte
    // optional property used to automatically set the
    // verifier if not already set.
    SignKey    []byte
    Cipher     string
    Verifier   *MessageVerifier
    Serializer MsgSerializer
}

MessageEncryptor is a simple way to encrypt values which get stored somewhere you don't trust.

The cipher text and initialization vector are base64 encoded and returned to you.

This can be used in situations similar to the MessageVerifier, but where you don't want users to be able to determine the value of the payload.

Different kind of ciphers will be supported, currently only Rails' default aes-cbc is supported. Note that as I'm writing this library, Rails default serializer is Ruby's Marshal which is not safe and cross language. You need to switch the serializer to JSON or another safer/cross language format to share encrypted messages between Ruby and Go.

func (*MessageEncryptor) Decrypt Uses

func (crypt *MessageEncryptor) Decrypt(value string, target interface{}) error

decrypt() decrypts a message using the set cipher and the secret. The passed value is expected to be a base 64 encoded string of the encrypted data + IV joined by "--"

func (*MessageEncryptor) DecryptAndVerify Uses

func (crypt *MessageEncryptor) DecryptAndVerify(msg string, target interface{}) error

Decrypt and verify a message. Messages need to be signed on top of being encrypted in order to avoid padding attacks. Reference: http://www.limited-entropy.com/padding-oracle-attacks. The serializer will populate the pointer you are passing as second argument.

Code:

type Person struct {
    Id        int    `json:"id"`
    FirstName string `json:"firstName"`
    LastName  string `json:"lastName"`
    Age       int    `json:"age"`
}
john := Person{Id: 12, FirstName: "John", LastName: "Doe", Age: 42}

railsSecret := "f7b5763636f4c1f3ff4bd444eacccca295d87b990cc104124017ad70550edcfd22b8e89465338254e0b608592a9aac29025440bfd9ce53579835ba06a86f85f9"
encryptedCookieSalt := []byte("encrypted cookie")
encryptedSignedCookieSalt := []byte("signed encrypted cookie")

kg := KeyGenerator{Secret: railsSecret}
// use 64 bit keys since the encryption uses 32 bytes
// but the signature uses 64. The crypto package handles that well.
secret := kg.CacheGenerate(encryptedCookieSalt, 32)
signSecret := kg.CacheGenerate(encryptedSignedCookieSalt, 64)
e := MessageEncryptor{Key: secret, SignKey: signSecret}
sessionString, err := e.EncryptAndSign(john)
if err != nil {
    panic(err)
}

// decrypting the person object contained in the session
var sessionContent Person
err = e.DecryptAndVerify(sessionString, &sessionContent)
if err != nil {
    panic(err)
}
fmt.Printf("%#v\n", sessionContent)

Output:

crypto.Person{Id:12, FirstName:"John", LastName:"Doe", Age:42}

func (*MessageEncryptor) Encrypt Uses

func (crypt *MessageEncryptor) Encrypt(value interface{}) (string, error)

Encrypt() encrypts a message using the set cipher and the secret. The returned value is a base 64 encoded string of the encrypted data + IV joined by "--". An encrypted message isn't safe unless it's signed!

func (*MessageEncryptor) EncryptAndSign Uses

func (crypt *MessageEncryptor) EncryptAndSign(value interface{}) (string, error)

Encrypt and sign a message (string, struct, anything that can be safely serialized/serialized). Note that even if you can just Encrypt() in most cases you shouldn't use it directly and instead use this method. The reason is that we need to sign the message in order to avoid padding attacks. Reference: http://www.limited-entropy.com/padding-oracle-attacks. The output string can be converted back using DecryptAndVerify() and is encoded using base64.

Code:

type Person struct {
    Id        int    `json:"id"`
    FirstName string `json:"firstName"`
    LastName  string `json:"lastName"`
    Age       int    `json:"age"`
}
john := Person{Id: 12, FirstName: "John", LastName: "Doe", Age: 42}

k := GenerateRandomKey(32)
signKey := []byte("this is a secret!")
e := MessageEncryptor{Key: k, SignKey: signKey}

// string encoding example
msg, err := e.EncryptAndSign("my secret data")
if err != nil {
    panic(err)
}
fmt.Println(msg)

// struct encoding example
msg, err = e.EncryptAndSign(john)
if err != nil {
    panic(err)
}
fmt.Println(msg)

type MessageVerifier Uses

type MessageVerifier struct {
    // Secret of 32-bytes if using the default hashing.
    Secret []byte
    // Hasher defaults to sha1 if not set.
    Hasher func() hash.Hash
    // Serializer defines the way the data is serializer/deserialized.
    Serializer MsgSerializer
}

MessageVerifier makes it easy to generate and verify messages which are signed to prevent tampering.

This is useful for cases like remember-me tokens and auto-unsubscribe links where the session store isn't suitable or available.

func (*MessageVerifier) DigestFor Uses

func (crypt *MessageVerifier) DigestFor(data string) string

DigestFor returns the digest form of a string after hashing it via the verifier's digest and secret.

func (*MessageVerifier) Generate Uses

func (crypt *MessageVerifier) Generate(value interface{}) (string, error)

Generate() Converts an interface into a string containing the serialized data and a digest. The string can be passed around and tampering can be checked using the digest. See Verify() to extract the data out of the signed string.

Code:

v := MessageVerifier{
    Secret:     []byte("Hey, I'm a secret!"),
    Serializer: JsonMsgSerializer{},
}
foo := map[string]interface{}{"foo": "this is foo", "bar": 42, "baz": []string{"bar", "baz"}}
generated, _ := v.Generate(foo)
fmt.Println(generated)

Output:

eyJiYXIiOjQyLCJiYXoiOlsiYmFyIiwiYmF6Il0sImZvbyI6InRoaXMgaXMgZm9vIn0=--895bf35965ebef12451372225ff3f73428f48e90

func (*MessageVerifier) IsValid Uses

func (crypt *MessageVerifier) IsValid() (bool, error)

Checks that the struct is properly set and ready for use.

func (*MessageVerifier) Verify Uses

func (crypt *MessageVerifier) Verify(msg string, target interface{}) error

Verify() takes a base64 encoded message string joined to a digest by a double dash "--" and returns an error if anything wrong happen. If the verification worked, the target interface object passed is populated.

Code:

v := MessageVerifier{
    Secret:     []byte("Hey, I'm a secret!"),
    Serializer: JsonMsgSerializer{},
}

data := testStruct{Foo: "foo", Bar: 42}
generated, _ := v.Generate(data)
fmt.Println(generated)
var verified testStruct
_ = v.Verify(generated, &verified)
fmt.Printf("%#v", verified)

Output:

eyJGb28iOiJmb28iLCJCYXIiOjQyfQ==--b1bdb9d2b372f19dcca800e5989ee7502f1b72a5
crypto.testStruct{Foo:"foo", Bar:42, Baz:[]string(nil)}

type MsgSerializer Uses

type MsgSerializer interface {
    Serialize(v interface{}) (string, error)
    Unserialize(data string, v interface{}) error
}

type NullMsgSerializer Uses

type NullMsgSerializer struct{}

func (NullMsgSerializer) Serialize Uses

func (s NullMsgSerializer) Serialize(vptr interface{}) (string, error)

func (NullMsgSerializer) Unserialize Uses

func (s NullMsgSerializer) Unserialize(data string, vptr interface{}) error

Can only deserialize to a string.

type XMLMsgSerializer Uses

type XMLMsgSerializer struct {
}

func (XMLMsgSerializer) Serialize Uses

func (s XMLMsgSerializer) Serialize(v interface{}) (string, error)

func (XMLMsgSerializer) Unserialize Uses

func (s XMLMsgSerializer) Unserialize(data string, v interface{}) error

Package crypto imports 17 packages (graph) and is imported by 1 packages. Updated 2018-06-22. Refresh now. Tools for package owners.