diff --git a/README.md b/README.md index 86c3cbc..389a0da 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,28 @@ Authorization Server crypts the token using the Token Formatter and Authorizatio This library contains a default implementation of the formatter interface called _SHA256RC4TokenSecureFormatter_ based on the algorithms SHA256 and RC4. Programmers can develop their Token Formatter implementing the interface _TokenSecureFormatter_ and this is really recommended before publishing the API in a production environment. +There are additional Token Formatters using several algorithms +* _AES128GCMTokenSecureFormatter_ +* _AES256GCMTokenSecureFormatter_ +* _RSATokenSecureFormatter_ +* _JWTHS256TokenSecureFormatter_ + +Benchmark +``` +go test -bench=. -benchtime 5s +goos: linux +goarch: amd64 +pkg: github.com/go-chi/oauth +cpu: Intel(R) Xeon(R) CPU E5-2630 v4 @ 2.20GHz +BenchmarkRC4-4 534931 12012 ns/op +BenchmarkSHA256-4 383150 13230 ns/op +BenchmarkAES128GCM-4 724194 7403 ns/op +BenchmarkAES256GCM-4 870612 7387 ns/op +BenchmarkRSA-4 1882 3093425 ns/op +BenchmarkJWTHS256-4 564812 10076 ns/op +PASS +ok github.com/go-chi/oauth 38.633s +``` ## Credentials Verifier The interface _CredentialsVerifier_ defines the hooks called during the token generation process. The methods are called in this order: @@ -56,17 +78,25 @@ func main() { r.Use(middleware.Logger) r.Use(middleware.Recoverer) - s := oauth.NewOAuthBearerServer( - "mySecretKey-10101", - time.Second*120, - &TestUserVerifier{}, - nil) + s := oauth.NewBearerServer( + "mySecretKey-10101", + time.Second*120, + &TestUserVerifier{}, + nil) r.Post("/token", s.UserCredentials) r.Post("/auth", s.ClientCredentials) http.ListenAndServe(":8080", r) } ``` +If you want to use [AES256 GCM](https://en.wikipedia.org/wiki/Galois/Counter_Mode) cipher, initialize the server with the corresponding formatter +```Go + s := oauth.NewBearerServer( + "testkeytestkeytestkeytestkey1234", // needs to be exactly 32 byte for AES256 + time.Second*120, + &TestUserVerifier{}, + oauth.NewAES256GCMTokenSecurityProvider([]byte("testkeytestkeytestkeytestkey1234"))) +``` See [/test/authserver/main.go](https://github.com/go-chi/oauth/blob/master/test/authserver/main.go) for the full example. ## Authorization Middleware usage example @@ -80,6 +110,11 @@ This snippet shows how to use the middleware r.Get("/customers/{id}/orders", GetOrders) } ``` +If you want to use [AES256 GCM](https://en.wikipedia.org/wiki/Galois/Counter_Mode) cipher, initialize the server with the corresponding formatter +```Go + // Key must be exactly 32 byte for AES256 + r.Use(oauth.Authorize("testkeytestkeytestkeytestkey1234", oauth.NewAES256GCMTokenSecurityProvider([]byte("testkeytestkeytestkeytestkey1234")))) +``` See [/test/resourceserver/main.go](https://github.com/go-chi/oauth/blob/master/test/resourceserver/main.go) for the full example. Note that the authorization server and the authorization middleware are both using the same token formatter and the same secret key for encryption/decryption. diff --git a/security.go b/security.go index 81b4b14..fc16a26 100644 --- a/security.go +++ b/security.go @@ -1,11 +1,18 @@ package oauth import ( + "crypto/aes" + "crypto/cipher" + "crypto/hmac" + "crypto/rand" "crypto/rc4" + "crypto/rsa" "crypto/sha256" "encoding/base64" "encoding/json" "errors" + "io" + "strings" ) type TokenSecureFormatter interface { @@ -151,3 +158,192 @@ func (sc *SHA256RC4TokenSecureFormatter) DecryptToken(source []byte) ([]byte, er } return dest[32:], nil } + +type AES128GCMTokenSecureFormatter struct { + key []byte +} + +func NewAES128GCMTokenSecurityProvider(key []byte) *AES128GCMTokenSecureFormatter { + if len(key) != 16 { + panic("AES128 key must be exactly 16 bytes long") + } + var sc = &AES128GCMTokenSecureFormatter{key: key} + return sc +} + +func (sc *AES128GCMTokenSecureFormatter) CryptToken(source []byte) ([]byte, error) { + hasher := sha256.New() + hasher.Write(source) + hash := hasher.Sum(nil) + newSource := append(hash, source...) + + aes, err := aes.NewCipher(sc.key) + if err != nil { + return nil, err + } + gcm, err := cipher.NewGCM(aes) + if err != nil { + return nil, err + } + nonce := make([]byte, gcm.NonceSize()) + if _, err = io.ReadFull(rand.Reader, nonce); err != nil { + return nil, err + } + return gcm.Seal(nonce, nonce, newSource, nil), nil +} + +func (sc *AES128GCMTokenSecureFormatter) DecryptToken(source []byte) ([]byte, error) { + aes, err := aes.NewCipher(sc.key) + if err != nil { + panic(err) + } + gcm, err := cipher.NewGCM(aes) + if err != nil { + return nil, err + } + nonceSize := gcm.NonceSize() + nonce, ciphertext := source[:nonceSize], source[nonceSize:] + dest, err := gcm.Open(nil, nonce, ciphertext, nil) + if err != nil { + return nil, err + } + // hash check + hasher := sha256.New() + hasher.Write(dest[32:]) + hash := hasher.Sum(nil) + for i, b := range hash { + if b != dest[i] { + return nil, errors.New("Invalid token") + } + } + return dest[32:], nil +} + +type AES256GCMTokenSecureFormatter struct { + key []byte +} + +func NewAES256GCMTokenSecurityProvider(key []byte) *AES256GCMTokenSecureFormatter { + if len(key) != 32 { + panic("AES256 key must be exactly 32 bytes long") + } + var sc = &AES256GCMTokenSecureFormatter{key: key} + return sc +} + +func (sc *AES256GCMTokenSecureFormatter) CryptToken(source []byte) ([]byte, error) { + hasher := sha256.New() + hasher.Write(source) + hash := hasher.Sum(nil) + newSource := append(hash, source...) + + aes, err := aes.NewCipher(sc.key) + if err != nil { + return nil, err + } + gcm, err := cipher.NewGCM(aes) + if err != nil { + return nil, err + } + nonce := make([]byte, gcm.NonceSize()) + if _, err = io.ReadFull(rand.Reader, nonce); err != nil { + return nil, err + } + return gcm.Seal(nonce, nonce, newSource, nil), nil +} + +func (sc *AES256GCMTokenSecureFormatter) DecryptToken(source []byte) ([]byte, error) { + aes, err := aes.NewCipher(sc.key) + if err != nil { + panic(err) + } + gcm, err := cipher.NewGCM(aes) + if err != nil { + return nil, err + } + nonceSize := gcm.NonceSize() + nonce, ciphertext := source[:nonceSize], source[nonceSize:] + dest, err := gcm.Open(nil, nonce, ciphertext, nil) + if err != nil { + return nil, err + } + // hash check + hasher := sha256.New() + hasher.Write(dest[32:]) + hash := hasher.Sum(nil) + for i, b := range hash { + if b != dest[i] { + return nil, errors.New("Invalid token") + } + } + return dest[32:], nil +} + +type RSATokenSecureFormatter struct { + key *rsa.PrivateKey +} + +func NewRSATokenSecurityProvider(key *rsa.PrivateKey) *RSATokenSecureFormatter { + var sc = &RSATokenSecureFormatter{key: key} + return sc +} + +func (sc *RSATokenSecureFormatter) CryptToken(source []byte) ([]byte, error) { + ciphertext, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, &sc.key.PublicKey, source, nil) + if err != nil { + return nil, err + } + return ciphertext, nil +} + +func (sc *RSATokenSecureFormatter) DecryptToken(source []byte) ([]byte, error) { + plaintext, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, sc.key, source, nil) + if err != nil { + return nil, err + } + return plaintext, nil +} + +type JWTHS256TokenSecureFormatter struct { + key []byte +} + +func NewJWTHS256TokenSecurityProvider(key []byte) *JWTHS256TokenSecureFormatter { + var sc = &JWTHS256TokenSecureFormatter{key: key} + return sc +} + +func (sc *JWTHS256TokenSecureFormatter) CryptToken(source []byte) ([]byte, error) { + header := []byte(`{"alg": "HS256", "typ": "JWT"}`) + b64header := base64.URLEncoding.EncodeToString(header) + b64payload := base64.URLEncoding.EncodeToString(source) + jwt := []byte(b64header + "." + b64payload) + hasher := hmac.New(sha256.New, sc.key) + hasher.Write(jwt) + jwt = append(jwt, '.') + jwt = append(jwt, hasher.Sum(nil)...) + return jwt, nil +} + +func (sc *JWTHS256TokenSecureFormatter) DecryptToken(source []byte) ([]byte, error) { + jwt := string(source) + parts := strings.Split(jwt, ".") + if len(parts) != 3 { + return nil, errors.New("Invalid token") + } + b64header := parts[0] + b64payload := parts[1] + signature := []byte(parts[2]) + + hasher := hmac.New(sha256.New, sc.key) + hasher.Write([]byte(b64header + "." + b64payload)) + // verify signature + if !hmac.Equal(signature, hasher.Sum(nil)) { + return nil, errors.New("Invalid token") + } + payload, err := base64.URLEncoding.DecodeString(b64payload) + if err != nil { + return nil, err + } + return payload, nil +} diff --git a/security_test.go b/security_test.go index ee7aff7..cf4ea13 100644 --- a/security_test.go +++ b/security_test.go @@ -1,14 +1,19 @@ package oauth import ( + "crypto/rand" + "crypto/rsa" "testing" ) -var _sutRC4, _sutSHA256 *TokenProvider +var _sutRC4, _sutSHA256, _sutAES128GCM, _sutAES256GCM, _sutJWTHS256 *TokenProvider func init() { _sutRC4 = NewTokenProvider(NewRC4TokenSecurityProvider([]byte("testkey"))) _sutSHA256 = NewTokenProvider(NewSHA256RC4TokenSecurityProvider([]byte("testkey"))) + _sutAES128GCM = NewTokenProvider(NewAES128GCMTokenSecurityProvider([]byte("testkeytestkey12"))) + _sutAES256GCM = NewTokenProvider(NewAES256GCMTokenSecurityProvider([]byte("testkeytestkeytestkeytestkey1234"))) + _sutJWTHS256 = NewTokenProvider(NewJWTHS256TokenSecurityProvider([]byte("testkey"))) } func TestCrypt(t *testing.T) { @@ -95,3 +100,190 @@ func TestDecryptSHA256_LongKey(t *testing.T) { } } } + +func TestCryptAES128GCM(t *testing.T) { + var token string = `{"CreationDate":"2016-12-14","Expiration":"1000"}` + result, err := _sutAES128GCM.crypt([]byte(token)) + if err != nil { + t.Fatalf("Error %s", err.Error()) + } + t.Logf("Base64 Token : %v", result) +} + +func TestDecryptAES128GCM(t *testing.T) { + var token string = `{"CreationDate":"2016-12-14","Expiration":"1000"}` + var bToken []byte = []byte(token) + t.Logf("Base64 Token : %v", bToken) + result, err := _sutAES128GCM.crypt([]byte(token)) + if err != nil { + t.Fatalf("Error %s", err.Error()) + } + t.Logf("Base64 Token : %v", result) + decrypt, err := _sutAES128GCM.decrypt(result) + if err != nil { + t.Fatalf("Error %s", err.Error()) + } + t.Logf("Base64 Token Decrypted: %v", decrypt) + t.Logf("Base64 Token Decrypted: %s", decrypt) + for i := range bToken { + if bToken[i] != decrypt[i] { + t.Fatalf("Error in decryption") + } + } +} + +func TestCryptAES256GCM(t *testing.T) { + var token string = `{"CreationDate":"2016-12-14","Expiration":"1000"}` + result, err := _sutAES256GCM.crypt([]byte(token)) + if err != nil { + t.Fatalf("Error %s", err.Error()) + } + t.Logf("Base64 Token : %v", result) +} + +func TestDecryptAES256GCM(t *testing.T) { + var token string = `{"CreationDate":"2016-12-14","Expiration":"1000"}` + var bToken []byte = []byte(token) + t.Logf("Base64 Token : %v", bToken) + result, err := _sutAES256GCM.crypt([]byte(token)) + if err != nil { + t.Fatalf("Error %s", err.Error()) + } + t.Logf("Base64 Token : %v", result) + decrypt, err := _sutAES256GCM.decrypt(result) + if err != nil { + t.Fatalf("Error %s", err.Error()) + } + t.Logf("Base64 Token Decrypted: %v", decrypt) + t.Logf("Base64 Token Decrypted: %s", decrypt) + for i := range bToken { + if bToken[i] != decrypt[i] { + t.Fatalf("Error in decryption") + } + } +} + +func TestCryptRSA(t *testing.T) { + var token string = `{"CreationDate":"2016-12-14","Expiration":"1000"}` + key, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + t.Fatalf("Error %s", err.Error()) + } + _sutRSA := NewTokenProvider(NewRSATokenSecurityProvider(key)) + result, err := _sutRSA.crypt([]byte(token)) + if err != nil { + t.Fatalf("Error %s", err.Error()) + } + t.Logf("Base64 Token : %v", result) +} + +func TestDecryptRSA(t *testing.T) { + var token string = `{"CreationDate":"2016-12-14","Expiration":"1000"}` + var bToken []byte = []byte(token) + t.Logf("Base64 Token : %v", bToken) + key, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + t.Fatalf("Error %s", err.Error()) + } + _sutRSA := NewTokenProvider(NewRSATokenSecurityProvider(key)) + result, err := _sutRSA.crypt([]byte(token)) + if err != nil { + t.Fatalf("Error %s", err.Error()) + } + t.Logf("Base64 Token : %v", result) + decrypt, err := _sutRSA.decrypt(result) + if err != nil { + t.Fatalf("Error %s", err.Error()) + } + t.Logf("Base64 Token Decrypted: %v", decrypt) + t.Logf("Base64 Token Decrypted: %s", decrypt) + for i := range bToken { + if bToken[i] != decrypt[i] { + t.Fatalf("Error in decryption") + } + } +} + +func TestCryptJWTHS256(t *testing.T) { + var token string = `{"CreationDate":"2016-12-14","Expiration":"1000"}` + result, err := _sutJWTHS256.crypt([]byte(token)) + if err != nil { + t.Fatalf("Error %s", err.Error()) + } + t.Logf("Base64 Token : %v", result) +} + +func TestDecryptJWTHS256(t *testing.T) { + var token string = `{"CreationDate":"2016-12-14","Expiration":"1000"}` + var bToken []byte = []byte(token) + t.Logf("Base64 Token : %v", bToken) + result, err := _sutJWTHS256.crypt([]byte(token)) + if err != nil { + t.Fatalf("Error %s", err.Error()) + } + t.Logf("Base64 Token : %v", result) + decrypt, err := _sutJWTHS256.decrypt(result) + if err != nil { + t.Fatalf("Error %s", err.Error()) + } + t.Logf("Base64 Token Decrypted: %v", decrypt) + t.Logf("Base64 Token Decrypted: %s", decrypt) + for i := range bToken { + if bToken[i] != decrypt[i] { + t.Fatalf("Error in decryption") + } + } +} + +func BenchmarkRC4(b *testing.B) { + var token string = `{"CreationDate":"2016-12-14","Expiration":"1000"}` + for i := 0; i < b.N; i++ { + result, _ := _sutRC4.crypt([]byte(token)) + _sutRC4.decrypt(result) + } +} + +func BenchmarkSHA256(b *testing.B) { + var token string = `{"CreationDate":"2016-12-14","Expiration":"1000"}` + for i := 0; i < b.N; i++ { + result, _ := _sutSHA256.crypt([]byte(token)) + _sutSHA256.decrypt(result) + } +} + +func BenchmarkAES128GCM(b *testing.B) { + var token string = `{"CreationDate":"2016-12-14","Expiration":"1000"}` + for i := 0; i < b.N; i++ { + result, _ := _sutAES128GCM.crypt([]byte(token)) + _sutAES128GCM.decrypt(result) + } +} + +func BenchmarkAES256GCM(b *testing.B) { + var token string = `{"CreationDate":"2016-12-14","Expiration":"1000"}` + for i := 0; i < b.N; i++ { + result, _ := _sutAES256GCM.crypt([]byte(token)) + _sutAES256GCM.decrypt(result) + } +} + +func BenchmarkRSA(b *testing.B) { + var token string = `{"CreationDate":"2016-12-14","Expiration":"1000"}` + key, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + b.Fatalf("Error %s", err.Error()) + } + _sutRSA := NewTokenProvider(NewRSATokenSecurityProvider(key)) + for i := 0; i < b.N; i++ { + result, _ := _sutRSA.crypt([]byte(token)) + _sutRSA.decrypt(result) + } +} + +func BenchmarkJWTHS256(b *testing.B) { + var token string = `{"CreationDate":"2016-12-14","Expiration":"1000"}` + for i := 0; i < b.N; i++ { + result, _ := _sutJWTHS256.crypt([]byte(token)) + _sutJWTHS256.decrypt(result) + } +} \ No newline at end of file