Skip to content

Commit

Permalink
Update API to reflect changes in aws-sdk-go-v2
Browse files Browse the repository at this point in the history
  • Loading branch information
alexrudd committed Nov 29, 2020
1 parent 7c45a0d commit 6091e50
Show file tree
Hide file tree
Showing 5 changed files with 135 additions and 96 deletions.
99 changes: 52 additions & 47 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,62 +9,67 @@ This is almost a direct port of [capless/warrant](https://github.com/capless/war

All crypto functions are tested against equivalent values produced by warrant

## v2

The version of this package in the `master` branch makes the assumption that it will be used directly with `aws-sdk-go-v2`. Because of this it intentionally leaks types from the aws package as part of its public API. This has the advantage of reducing code (slightly), but carries the larger disadvantages of complicating dependencies.

It is recommended you use version 2 of this package located in the `v2` branch, though for now both versions will be maintained.

Import v2 of this package with `go get github.com/alexrudd/cognito-srp/v2`and update your imports and code accordingly.
* v2 - Removed dependency on `aws-sdk-go-v2`
* v3 - Package and usage have been updated to improve compatibility with latest `aws-sdk-go-v2` API

## Usage

```go
package main

import (
"fmt"
"time"
"context"
"fmt"
"time"

cognitosrp "github.com/alexrudd/cognito-srp/v3"

"github.com/alexrudd/cognito-srp/v2"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/aws/endpoints"
"github.com/aws/aws-sdk-go-v2/aws/external"
cip "github.com/aws/aws-sdk-go-v2/service/cognitoidentityprovider"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
cip "github.com/aws/aws-sdk-go-v2/service/cognitoidentityprovider"
"github.com/aws/aws-sdk-go-v2/service/cognitoidentityprovider/types"
)

func main() {
// configure cognito srp
csrp, _ := cognitosrp.NewCognitoSRP("user", "pa55w0rd", "eu-west-1_myPoolId", "client", nil)

// configure cognito identity provider
cfg, _ := external.LoadDefaultAWSConfig()
cfg.Region = endpoints.EuWest1RegionID
cfg.Credentials = aws.AnonymousCredentials
svc := cip.New(cfg)

// initiate auth
req := svc.InitiateAuthRequest(&cip.InitiateAuthInput{
AuthFlow: cip.AuthFlowTypeUserSrpAuth,
ClientId: aws.String(csrp.GetClientId()),
AuthParameters: csrp.GetAuthParams(),
})
resp, _ := req.Send()

// respond to password verifier challenge
if resp.ChallengeName == cip.ChallengeNameTypePasswordVerifier {
challengeResponses, _ := csrp.PasswordVerifierChallenge(resp.ChallengeParameters, time.Now())
chal := svc.RespondToAuthChallengeRequest(&cip.RespondToAuthChallengeInput{
ChallengeName: cip.ChallengeNameTypePasswordVerifier,
ChallengeResponses: challengeResponses,
ClientId: aws.String(csrp.GetClientId()),
})
resp, _ := chal.Send()

// print the tokens
fmt.Println(resp.AuthenticationResult)
} else {
// other challenges await...
}
// configure cognito srp
csrp, _ := cognitosrp.NewCognitoSRP("user", "pa55w0rd", "eu-west-1_myPoolId", "client", nil)

// configure cognito identity provider
cfg, _ := config.LoadDefaultConfig(
config.WithRegion("eu-west-1"),
config.WithCredentialsProvider(aws.AnonymousCredentials{}),
)
svc := cip.NewFromConfig(cfg)

// initiate auth
resp, err := svc.InitiateAuth(context.Background(), &cip.InitiateAuthInput{
AuthFlow: types.AuthFlowTypeUserSrpAuth,
ClientId: aws.String(csrp.GetClientId()),
AuthParameters: csrp.GetAuthParams(),
})
if err != nil {
panic(err)
}

// respond to password verifier challenge
if resp.ChallengeName == types.ChallengeNameTypePasswordVerifier {
challengeResponses, _ := csrp.PasswordVerifierChallenge(resp.ChallengeParameters, time.Now())

resp, err := svc.RespondToAuthChallenge(context.Background(), &cip.RespondToAuthChallengeInput{
ChallengeName: types.ChallengeNameTypePasswordVerifier,
ChallengeResponses: challengeResponses,
ClientId: aws.String(csrp.GetClientId()),
})
if err != nil {
panic(err)
}

// print the tokens
fmt.Printf("Access Token: %s\n", *resp.AuthenticationResult.AccessToken)
fmt.Printf("ID Token: %s\n", *resp.AuthenticationResult.IdToken)
fmt.Printf("Refresh Token: %s\n", *resp.AuthenticationResult.RefreshToken)
} else {
// other challenges await...
}
}
```
```
100 changes: 67 additions & 33 deletions cognitosrp.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,14 +96,16 @@ func (csrp *CognitoSRP) GetUserPoolName() string {

// GetAuthParams returns the AuthParms map of values required for make
// InitiateAuth requests
func (csrp *CognitoSRP) GetAuthParams() map[string]string {
params := map[string]string{
"USERNAME": csrp.username,
"SRP_A": bigToHex(csrp.bigA),
func (csrp *CognitoSRP) GetAuthParams() map[string]*string {
params := map[string]*string{
"USERNAME": stringPtr(csrp.username),
"SRP_A": stringPtr(bigToHex(csrp.bigA)),
}
if csrp.clientSecret != nil {
params["SECRET_HASH"], _ = csrp.GetSecretHash(csrp.username)

if secret, err := csrp.GetSecretHash(csrp.username); err == nil {
params["SECRET_HASH"] = stringPtr(secret)
}

return params
}

Expand All @@ -113,26 +115,35 @@ func (csrp *CognitoSRP) GetSecretHash(username string) (string, error) {
if csrp.clientSecret == nil {
return "", fmt.Errorf("unable to create secret hash as client secret has not been configured")
}
msg := username + csrp.clientId
key := []byte(*csrp.clientSecret)
h := hmac.New(sha256.New, key)

var (
msg = username + csrp.clientId
key = []byte(*csrp.clientSecret)
h = hmac.New(sha256.New, key)
)

h.Write([]byte(msg))

sh := base64.StdEncoding.EncodeToString(h.Sum(nil))

return sh, nil
}

// PasswordVerifierChallenge returns the ChallengeResponses map to be used
// inside the cognitoidentityprovider.RespondToAuthChallengeInput object which
// fulfils the PASSWORD_VERIFIER Cognito challenge
func (csrp *CognitoSRP) PasswordVerifierChallenge(challengeParms map[string]string, ts time.Time) (map[string]string, error) {
internalUsername := challengeParms["USERNAME"]
userId := challengeParms["USER_ID_FOR_SRP"]
saltHex := challengeParms["SALT"]
srpBHex := challengeParms["SRP_B"]
secretBlockB64 := challengeParms["SECRET_BLOCK"]
timestamp := ts.In(time.UTC).Format("Mon Jan 2 03:04:05 MST 2006")

hkdf := csrp.getPasswordAuthenticationKey(userId, csrp.password, hexToBig(srpBHex), hexToBig(saltHex))
func (csrp *CognitoSRP) PasswordVerifierChallenge(challengeParms map[string]*string, ts time.Time) (map[string]*string, error) {
var (
internalUsername = stringVal(challengeParms["USERNAME"])
userId = stringVal(challengeParms["USER_ID_FOR_SRP"])
saltHex = stringVal(challengeParms["SALT"])
srpBHex = stringVal(challengeParms["SRP_B"])
secretBlockB64 = stringVal(challengeParms["SECRET_BLOCK"])

timestamp = ts.In(time.UTC).Format("Mon Jan 2 03:04:05 MST 2006")
hkdf = csrp.getPasswordAuthenticationKey(userId, csrp.password, hexToBig(srpBHex), hexToBig(saltHex))
)

secretBlockBytes, err := base64.StdEncoding.DecodeString(secretBlockB64)
if err != nil {
return nil, fmt.Errorf("unable to decode challenge parameter 'SECRET_BLOCK', %s", err.Error())
Expand All @@ -142,21 +153,23 @@ func (csrp *CognitoSRP) PasswordVerifierChallenge(challengeParms map[string]stri
hmacObj := hmac.New(sha256.New, hkdf)
hmacObj.Write([]byte(msg))
signature := base64.StdEncoding.EncodeToString(hmacObj.Sum(nil))
response := map[string]string{
"TIMESTAMP": timestamp,
"USERNAME": internalUsername,
"PASSWORD_CLAIM_SECRET_BLOCK": secretBlockB64,
"PASSWORD_CLAIM_SIGNATURE": signature,

response := map[string]*string{
"TIMESTAMP": stringPtr(timestamp),
"USERNAME": stringPtr(internalUsername),
"PASSWORD_CLAIM_SECRET_BLOCK": stringPtr(secretBlockB64),
"PASSWORD_CLAIM_SIGNATURE": stringPtr(signature),
}
if csrp.clientSecret != nil {
response["SECRET_HASH"], _ = csrp.GetSecretHash(internalUsername)
if secret, err := csrp.GetSecretHash(csrp.username); err == nil {
response["SECRET_HASH"] = stringPtr(secret)
}

return response, nil
}

func (csrp *CognitoSRP) generateRandomSmallA() *big.Int {
randomLongInt := getRandom(128)

return big.NewInt(0).Mod(randomLongInt, csrp.bigN)
}

Expand All @@ -165,31 +178,36 @@ func (csrp *CognitoSRP) calculateA() *big.Int {
if big.NewInt(0).Mod(bigA, csrp.bigN).Cmp(big.NewInt(0)) == 0 {
panic("Safety check for A failed. A must not be divisable by N")
}

return bigA
}

func (csrp *CognitoSRP) getPasswordAuthenticationKey(username, password string, bigB, salt *big.Int) []byte {
userPass := fmt.Sprintf("%s%s:%s", csrp.poolName, username, password)
userPassHash := hashSha256([]byte(userPass))
var (
userPass = fmt.Sprintf("%s%s:%s", csrp.poolName, username, password)
userPassHash = hashSha256([]byte(userPass))

uVal := calculateU(csrp.bigA, bigB)
xVal := hexToBig(hexHash(padHex(salt.Text(16)) + userPassHash))
gModPowXN := big.NewInt(0).Exp(csrp.g, xVal, csrp.bigN)
intVal1 := big.NewInt(0).Sub(bigB, big.NewInt(0).Mul(csrp.k, gModPowXN))
intVal2 := big.NewInt(0).Add(csrp.a, big.NewInt(0).Mul(uVal, xVal))
sVal := big.NewInt(0).Exp(intVal1, intVal2, csrp.bigN)
uVal = calculateU(csrp.bigA, bigB)
xVal = hexToBig(hexHash(padHex(salt.Text(16)) + userPassHash))
gModPowXN = big.NewInt(0).Exp(csrp.g, xVal, csrp.bigN)
intVal1 = big.NewInt(0).Sub(bigB, big.NewInt(0).Mul(csrp.k, gModPowXN))
intVal2 = big.NewInt(0).Add(csrp.a, big.NewInt(0).Mul(uVal, xVal))
sVal = big.NewInt(0).Exp(intVal1, intVal2, csrp.bigN)
)

return computeHKDF(padHex(sVal.Text(16)), padHex(bigToHex(uVal)))
}

func hashSha256(buf []byte) string {
a := sha256.New()
a.Write(buf)

return hex.EncodeToString(a.Sum(nil))
}

func hexHash(hexStr string) string {
buf, _ := hex.DecodeString(hexStr)

return hashSha256(buf)
}

Expand All @@ -198,6 +216,7 @@ func hexToBig(hexStr string) *big.Int {
if !ok {
panic(fmt.Sprintf("unable to covert \"%s\" to big Int", hexStr))
}

return i
}

Expand All @@ -208,6 +227,7 @@ func bigToHex(val *big.Int) string {
func getRandom(n int) *big.Int {
b := make([]byte, n)
rand.Read(b)

return hexToBig(hex.EncodeToString(b))
}

Expand All @@ -217,6 +237,7 @@ func padHex(hexStr string) string {
} else if strings.Contains("89ABCDEFabcdef", string(hexStr[0])) {
hexStr = fmt.Sprintf("00%s", hexStr)
}

return hexStr
}

Expand All @@ -231,9 +252,22 @@ func computeHKDF(ikm, salt string) []byte {
extractor = hmac.New(sha256.New, prk)
extractor.Write(infoBitsUpdate)
hmacHash := extractor.Sum(nil)

return hmacHash[:16]
}

func calculateU(bigA, bigB *big.Int) *big.Int {
return hexToBig(hexHash(padHex(bigA.Text(16)) + padHex(bigB.Text(16))))
}

func stringPtr(s string) *string {
return &s
}

func stringVal(s *string) string {
if s == nil {
return ""
}

return *s
}
28 changes: 14 additions & 14 deletions cognitosrp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,15 +71,15 @@ func Test_GetAuthParams(t *testing.T) {

params := csrp.GetAuthParams()

if params["USERNAME"] != csrp.username {
t.Errorf("actual USERNAME: %s, did not match expected USERNAME: %s", params["USERNAME"], csrp.username)
if *params["USERNAME"] != csrp.username {
t.Errorf("actual USERNAME: %s, did not match expected USERNAME: %s", *params["USERNAME"], csrp.username)
}
if params["SRP_A"] != csrp.bigA.Text(16) {
t.Errorf("actual SRP_A: %s, did not match expected SRP_A: %s", params["SRP_A"], csrp.bigA.Text(16))
if *params["SRP_A"] != csrp.bigA.Text(16) {
t.Errorf("actual SRP_A: %s, did not match expected SRP_A: %s", *params["SRP_A"], csrp.bigA.Text(16))
}
expectedHash := "LoIX/oPJWZzFYv8liJYRo+CHv16FNDY10JlZEDjL3Vg="
if params["SECRET_HASH"] != expectedHash {
t.Errorf("actual SECRET_HASH: %s, did not match expected SECRET_HASH: %s", params["SECRET_HASH"], expectedHash)
if *params["SECRET_HASH"] != expectedHash {
t.Errorf("actual SECRET_HASH: %s, did not match expected SECRET_HASH: %s", *params["SECRET_HASH"], expectedHash)
}
}

Expand Down Expand Up @@ -109,22 +109,22 @@ func Test_PasswordVerifierChallenge(t *testing.T) {
csrp, _ := NewCognitoSRP("test", "test", "eu-west-1_myPool", "123abd", &cs)
csrp.a = big.NewInt(1234567890)
csrp.bigA = csrp.calculateA()
challengeParmas := map[string]string{
"USER_ID_FOR_SRP": "test",
"SALT": big.NewInt(1234567890).Text(16),
"SRP_B": big.NewInt(1234567890).Text(16),
"SECRET_BLOCK": base64.StdEncoding.EncodeToString([]byte("secretssecrestssecrets")),
challengeParmas := map[string]*string{
"USER_ID_FOR_SRP": stringPtr("test"),
"SALT": stringPtr(big.NewInt(1234567890).Text(16)),
"SRP_B": stringPtr(big.NewInt(1234567890).Text(16)),
"SECRET_BLOCK": stringPtr(base64.StdEncoding.EncodeToString([]byte("secretssecrestssecrets"))),
}

challResp, _ := csrp.PasswordVerifierChallenge(challengeParmas, time.Date(2018, 7, 10, 11, 1, 0, 0, time.UTC))

expected := "tdvQu/Li/qWl8Nni0aFPs+MwY4rvKZm0kSMrGIMSUHk="
if challResp["PASSWORD_CLAIM_SIGNATURE"] != expected {
t.Errorf("actual PASSWORD_CLAIM_SIGNATURE: %s, did not match expected PASSWORD_CLAIM_SIGNATURE: %s", challResp["PASSWORD_CLAIM_SIGNATURE"], expected)
if *challResp["PASSWORD_CLAIM_SIGNATURE"] != expected {
t.Errorf("actual PASSWORD_CLAIM_SIGNATURE: %s, did not match expected PASSWORD_CLAIM_SIGNATURE: %s", *challResp["PASSWORD_CLAIM_SIGNATURE"], expected)
}

// Bad challenge params
challengeParmas["SECRET_BLOCK"] = "not base64 encoded"
challengeParmas["SECRET_BLOCK"] = stringPtr("not base64 encoded")
_, err := csrp.PasswordVerifierChallenge(challengeParmas, time.Date(2018, 7, 10, 11, 46, 0, 0, time.UTC))
if err == nil {
t.Fatal("PasswordVerifierChallenge should error on bad 'SECRET_BLOCK'")
Expand Down
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module github.com/alexrudd/cognito-srp/v2
module github.com/alexrudd/cognito-srp/v3

go 1.12
go 1.15
Empty file removed go.sum
Empty file.

0 comments on commit 6091e50

Please sign in to comment.