Documentation ¶
Overview ¶
Package crypto ports some of Ruby on Rails' crypto:
- version 4+: encrypted & signed messages (aes-cbc)
- version 5.2+: encrypted & authenticated messages (aes-256-gcm)
Messages can be shared between a Ruby app and a Go app. That said, this library is useful to anyone wanting to encrypt/sign/authenticate 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 ¶
Since Rails 5.2, the default session serializer can be set to use JSON by setting:
Rails.application.config.action_dispatch.cookies_serializer = :json.
In older Rails versions, it is necessary to make changes in order to move away from 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 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" # Rails 4+ / aes-cbc: 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"} # Rails 5.2+ / aes-256-gcm: key_generator = ActiveSupport::CachingKeyGenerator.new(ActiveSupport::KeyGenerator.new(secret_key_base, iterations: 1000)) secret = key_generator.generate_key("authenticated encrypted cookie", 32) encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: 'aes-256-gcm', serializer: JSON) # encrypt and authenticate the content of the session: encrypted_message = encryptor.encrypt_and_sign({msg: "hello world"}) # The encrypted and authenticated message is stored in the session cookie # To authenticate and decrypt 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.
With aes-cbc mode, there are two derived keys: one for encryption and one for signing. With aes-256-gcm mode, there is only one key that is used for both authentication and encryption. 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 for aes-cbc (Rails 4+):
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}
Here is an example for aes-256-gcm (Rails 5.2+):
railsSecret := "f7b5763636f4c1f3ff4bd444eacccca295d87b990cc104124017ad70550edcfd22b8e89465338254e0b608592a9aac29025440bfd9ce53579835ba06a86f85f9" authenticatedCookieSalt := []byte("authenticated encrypted cookie") kg := KeyGenerator{Secret: railsSecret} secret := kg.CacheGenerate(authenticated, 32) e := MessageEncryptor{Key: secret, Cipher: "aes-256-gcm"}
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.
It is recommended that new applications use the "aes-256-gcm" mode rather than the "aes-cbc" mode, as the prior is a less error prone scheme and does not rely on now out of favor cryptographic primitives.
Index ¶
- func GenerateRandomKey(strength int) []byte
- func PKCS7Pad(data []byte) []byte
- func PKCS7Unpad(data []byte) []byte
- type JsonMsgSerializer
- type KeyGenerator
- type MessageEncryptor
- func (crypt *MessageEncryptor) Decrypt(value string, target interface{}) error
- func (crypt *MessageEncryptor) DecryptAndVerify(msg string, target interface{}) error
- func (crypt *MessageEncryptor) Encrypt(value interface{}) (string, error)
- func (crypt *MessageEncryptor) EncryptAndSign(value interface{}) (string, error)
- type MessageVerifier
- type MsgSerializer
- type NullMsgSerializer
- type XMLMsgSerializer
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func GenerateRandomKey ¶
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 ¶
PKCS7Pad() pads an byte array to be a multiple of 16 http://tools.ietf.org/html/rfc5652#section-6.3
func PKCS7Unpad ¶
PKCS7Unpad() removes any potential PKCS7 padding added.
Types ¶
type JsonMsgSerializer ¶
type JsonMsgSerializer struct { }
func (JsonMsgSerializer) Serialize ¶
func (s JsonMsgSerializer) Serialize(v interface{}) (string, error)
func (JsonMsgSerializer) Unserialize ¶
func (s JsonMsgSerializer) Unserialize(data string, v interface{}) error
type KeyGenerator ¶
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 ¶
func (g *KeyGenerator) CacheGenerate(salt []byte, keySize int) []byte
CacheGenerate() write through cache used to save generated keys.
type MessageEncryptor ¶
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 are supported:
- aes-cbc - Rails' default until 5.2, requires a verifier
- aes-256-gcm - Rails 5.2+ default, ignores verifier.
Note: The old Rails default serializer, Marshal is neither safe or portable across langauges, use the JSON serializer.
func (*MessageEncryptor) Decrypt ¶
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 ¶
func (crypt *MessageEncryptor) DecryptAndVerify(msg string, target interface{}) error
DecryptAndVerify decrypts and either authenticates or verifies the signature of a message, depending on the selected cipher mode. Messages need to be either signed or authenticated (GCM) 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.
Example ¶
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 ¶
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 ¶
func (crypt *MessageEncryptor) EncryptAndSign(value interface{}) (string, error)
EncryptAndSign performs encryption with authentication, or encryption followed by signing, depending on the selected cipher mode. message can be any serializable type (string, struct, map, etc). Note that even if you can just Encrypt() in most cases you shouldn't use it directly and instead use this method. For aes-cbc mode, encryption alone is neither signed or authenticated, and is subject to padding oracle attacks. Reference: http://www.limited-entropy.com/padding-oracle-attacks. The output string can be converted back using DecryptAndVerify() and is encoded using base64.
Example ¶
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)
Output:
type MessageVerifier ¶
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 ¶
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 ¶
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.
Example ¶
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 ¶
func (crypt *MessageVerifier) IsValid() (bool, error)
Checks that the struct is properly set and ready for use.
func (*MessageVerifier) Verify ¶
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.
Example ¶
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 ¶
type NullMsgSerializer ¶
type NullMsgSerializer struct{}
func (NullMsgSerializer) Serialize ¶
func (s NullMsgSerializer) Serialize(vptr interface{}) (string, error)
func (NullMsgSerializer) Unserialize ¶
func (s NullMsgSerializer) Unserialize(data string, vptr interface{}) error
Can only deserialize to a string.
type XMLMsgSerializer ¶
type XMLMsgSerializer struct { }
func (XMLMsgSerializer) Serialize ¶
func (s XMLMsgSerializer) Serialize(v interface{}) (string, error)
func (XMLMsgSerializer) Unserialize ¶
func (s XMLMsgSerializer) Unserialize(data string, v interface{}) error