crypto

package
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jan 3, 2021 License: MIT, MIT Imports: 17 Imported by: 0

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

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func GenerateRandomKey

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

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

func PKCS7Unpad(data []byte) []byte

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

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

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

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

func (*KeyGenerator) Generate

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

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 MsgSerializer interface {
	Serialize(v interface{}) (string, error)
	Unserialize(data string, v interface{}) error
}

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

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL