diff --git a/README.md b/README.md index 47c8a34..f35b18b 100644 --- a/README.md +++ b/README.md @@ -9,13 +9,8 @@ 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 @@ -23,48 +18,58 @@ Import v2 of this package with `go get github.com/alexrudd/cognito-srp/v2`and up 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... + } } -``` +``` \ No newline at end of file diff --git a/cognitosrp.go b/cognitosrp.go index 79c6853..25f7758 100644 --- a/cognitosrp.go +++ b/cognitosrp.go @@ -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 } @@ -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()) @@ -142,14 +153,15 @@ 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 @@ -157,6 +169,7 @@ func (csrp *CognitoSRP) PasswordVerifierChallenge(challengeParms map[string]stri func (csrp *CognitoSRP) generateRandomSmallA() *big.Int { randomLongInt := getRandom(128) + return big.NewInt(0).Mod(randomLongInt, csrp.bigN) } @@ -165,19 +178,22 @@ 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))) } @@ -185,11 +201,13 @@ func (csrp *CognitoSRP) getPasswordAuthenticationKey(username, password string, 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) } @@ -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 } @@ -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)) } @@ -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 } @@ -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 +} diff --git a/cognitosrp_test.go b/cognitosrp_test.go index 1c262c7..ac39adf 100644 --- a/cognitosrp_test.go +++ b/cognitosrp_test.go @@ -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) } } @@ -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'") diff --git a/go.mod b/go.mod index 4b8ca2d..f28c194 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ -module github.com/alexrudd/cognito-srp/v2 +module github.com/alexrudd/cognito-srp/v3 -go 1.12 +go 1.15 diff --git a/go.sum b/go.sum deleted file mode 100644 index e69de29..0000000