Skip to content

Commit

Permalink
enigma: implemented new token generator based on hmac-sha256
Browse files Browse the repository at this point in the history
Closes #11
  • Loading branch information
Aeneas Rekkas committed Jan 8, 2016
1 parent 71a9105 commit 01f9ede
Show file tree
Hide file tree
Showing 7 changed files with 230 additions and 117 deletions.
42 changes: 42 additions & 0 deletions enigma/challenge.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package enigma

import "strings"

// Challenge represents an validatable token.
type Challenge struct {
// Key is the messages's key
Key string

// Signature is the messages's signature
Signature string
}

// FromString extracts key and signature from "<key>.<signature>".
func (a *Challenge) FromString(data string) {
a.Key = ""
a.Signature = ""

if data == "" {
return
}

parts := strings.Split(data, ".")
if len(parts) != 2 {
return
}

key := strings.TrimSpace(parts[0])
sig := strings.TrimSpace(parts[1])
if key == "" || sig == "" {
return
}

a.Key = key
a.Signature = sig
return
}

// String will return the Challenge as "<key>.<signature>".
func (a *Challenge) String() string {
return a.Key + "." + a.Signature
}
11 changes: 6 additions & 5 deletions generator/generator_test.go → enigma/challenge_test.go
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
package generator
package enigma

import (
"github.com/stretchr/testify/assert"
"testing"
)

func TestTokenToString(t *testing.T) {
ac := &Token{
func TestChallengeToString(t *testing.T) {
ac := &Challenge{
Key: "foo",
Signature: "bar",
}
assert.Equal(t, "foo.bar", ac.String())
}
func TestTokenFromString(t *testing.T) {
ac := new(Token)

func TestChallengeFromString(t *testing.T) {
ac := new(Challenge)
for _, c := range [][]string{
{"foo.bar", "foo", "bar"},
{"foo.", "", ""},
Expand Down
103 changes: 103 additions & 0 deletions enigma/crypto.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package enigma

import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"github.com/go-errors/errors"
"github.com/ory-am/fosite/rand"
)

// CryptoEnigma is the default implementation for generating and validating challenges.
type CryptoEnigma struct {
AuthCodeEntropy int
GlobalSecret []byte
}

// key should be at least 256 bit long, making it
const minimumEntropy = 32

// the secrets (client and global) should each have at least 16 characters making it harder to guess them
const minimumSecretLength = 32

// GenerateAuthorizeCode generates a new authorize code or returns an error. set secret
// This method implements rfc6819 Section 5.1.4.2.2: Use High Entropy for Secrets.
func (c *CryptoEnigma) GenerateChallenge(secret []byte) (*Challenge, error) {
if len(secret) < minimumSecretLength/2 || len(c.GlobalSecret) < minimumSecretLength/2 {
return nil, errors.New("Secret or GlobalSecret are not strong enough")
}

if c.AuthCodeEntropy < minimumEntropy {
c.AuthCodeEntropy = minimumEntropy
}

// When creating secrets not intended for usage by human users (e.g.,
// client secrets or token handles), the authorization server should
// include a reasonable level of entropy in order to mitigate the risk
// of guessing attacks. The token value should be >=128 bits long and
// constructed from a cryptographically strong random or pseudo-random
// number sequence (see [RFC4086] for best current practice) generated
// by the authorization server.
randomBytes, err := rand.RandomBytes(c.AuthCodeEntropy, 20)
if err != nil {
return nil, errors.New(err)
}

if len(randomBytes) < c.AuthCodeEntropy {
return nil, errors.New("Could not read enough random data for key generation")
}

useSecret := append([]byte{}, c.GlobalSecret...)
mac := hmac.New(sha256.New, append(useSecret, secret...))
_, err = mac.Write(randomBytes)
if err != nil {
return nil, errors.New(err)
}
signature := mac.Sum([]byte{})

encodedKey := make([]byte, base64.StdEncoding.EncodedLen(c.AuthCodeEntropy))
base64.StdEncoding.Encode(encodedKey, randomBytes)
encodedKey = bytes.Trim(encodedKey, "\x00")

encodedSignature := make([]byte, base64.StdEncoding.EncodedLen(len(signature)))
base64.StdEncoding.Encode(encodedSignature, signature)
encodedSignature = bytes.Trim(encodedSignature, "\x00")

return &Challenge{
Key: string(encodedKey),
Signature: string(encodedSignature),
}, nil
}

// ValidateAuthorizeCodeSignature returns an AuthorizeCode, if the code argument is a valid authorize code
// and the signature matches the key.
func (c *CryptoEnigma) ValidateChallenge(secret []byte, t *Challenge) (err error) {
if t.Key == "" || t.Signature == "" {
return errors.New("Key and signature must both be not empty")
}

key := make([]byte, base64.StdEncoding.DecodedLen(len(t.Key)))
if _, err := base64.StdEncoding.Decode(key, []byte(t.Signature)); err != nil {
return err
}

signature := make([]byte, base64.StdEncoding.DecodedLen(len(t.Signature)))
if _, err := base64.StdEncoding.Decode(signature, []byte(t.Signature)); err != nil {
return err
}

useSecret := append([]byte{}, c.GlobalSecret...)
mac := hmac.New(sha256.New, append(useSecret, secret...))
_, err = mac.Write(key)
if err != nil {
return errors.New(err)
}

if !hmac.Equal(signature, mac.Sum([]byte{})) {
// Hash is invalid
return errors.New("Key and signature do not match")
}

return nil
}
67 changes: 67 additions & 0 deletions enigma/crypto_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package enigma

import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"testing"
)

func TestGenerateFailsWithShortCredentials(t *testing.T) {
cg := CryptoEnigma{
GlobalSecret: []byte("foo"),
}

challenge, err := cg.GenerateChallenge([]byte("bar"))
require.NotNil(t, err, "%s", err)
require.Nil(t, challenge)

cg.GlobalSecret = []byte("12345678901234567890")
challenge, err = cg.GenerateChallenge([]byte("bar"))
require.NotNil(t, err, "%s", err)
require.Nil(t, challenge)

cg.GlobalSecret = []byte("bar")
challenge, err = cg.GenerateChallenge([]byte("12345678901234567890"))
require.NotNil(t, err, "%s", err)
require.Nil(t, challenge)
}

func TestGenerate(t *testing.T) {
cg := CryptoEnigma{
GlobalSecret: []byte("12345678901234567890"),
}

challenge, err := cg.GenerateChallenge([]byte("09876543210987654321"))
require.Nil(t, err, "%s", err)
require.NotNil(t, challenge)
t.Logf("%s.%s", challenge.Key, challenge.Signature)

err = cg.ValidateChallenge([]byte("bar"), challenge)
require.NotNil(t, err, "%s", err)

err = cg.ValidateChallenge([]byte("baz"), challenge)
require.NotNil(t, err, "%s", err)

cg.GlobalSecret = []byte("baz")
err = cg.ValidateChallenge([]byte("bar"), challenge)
require.NotNil(t, err, "%s", err)
}

func TestValidateSignatureRejects(t *testing.T) {
var err error
cg := CryptoEnigma{
GlobalSecret: []byte("12345678901234567890"),
}
token := new(Challenge)
for _, c := range []string{
"",
" ",
"foo.bar",
"foo.",
".foo",
} {
token.FromString(c)
err = cg.ValidateChallenge([]byte("09876543210987654321"), token)
assert.NotNil(t, err, "%s", err)
}
}
12 changes: 12 additions & 0 deletions enigma/enigma.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package enigma

// Enigma provides a set of methods to create access, refresh and authorize tokens.
type Enigma interface {

// GenerateChallenge generates a challenge (comparable to an opaque token).
GenerateChallenge(secret []byte) (*Challenge, error)

// ValidateSignature verifies that the challenge key matches the challenge signature, making the challenge
// valid or returning an error if it is invalid.
ValidateChallenge(secret []byte, challenge *Challenge) error
}
75 changes: 0 additions & 75 deletions generator/crypto.go

This file was deleted.

37 changes: 0 additions & 37 deletions generator/crypto_test.go

This file was deleted.

0 comments on commit 01f9ede

Please sign in to comment.