Skip to content

Instantly share code, notes, and snippets.

@dbeattie71
Last active August 17, 2017 19:48
Show Gist options
  • Save dbeattie71/44ea3a13145f185d303e620c299ab1c5 to your computer and use it in GitHub Desktop.
Save dbeattie71/44ea3a13145f185d303e620c299ab1c5 to your computer and use it in GitHub Desktop.
Attempt to port Cognito authentication to C#
using System;
using System.Security.Cryptography;
using System.Text;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Digests;
using Org.BouncyCastle.Math;
namespace ConsoleApp3
{
//https://gist.github.com/guywald/7a753e032ca2f979453b7f8aa4fb6569
//https://gist.github.com/KonajuGames/0253adf035d83e3b58a872fb00e4f398
public class AuthenticationHelper
{
private const string InitN = "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD1"
+ "29024E088A67CC74020BBEA63B139B22514A08798E3404DD"
+ "EF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245"
+ "E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7ED"
+ "EE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3D"
+ "C2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F"
+ "83655D23DCA3AD961C62F356208552BB9ED529077096966D"
+ "670C354E4ABC9804F1746C08CA18217C32905E462E36CE3B"
+ "E39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9"
+ "DE2BCBF6955817183995497CEA956AE515D2261898FA0510"
+ "15728E5A8AAAC42DAD33170D04507A33A85521ABDF1CBA64"
+ "ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7"
+ "ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6B"
+ "F12FFA06D98A0864D87602733EC86A64521F2B18177B200C"
+ "BBE117577A615D6C770988C0BAD946E208E24FA074E5AB31"
+ "43DB5BFCE0FD108E4B82D120A93AD2CAFFFFFFFFFFFFFFFF";
public const string NewPasswordRequiredChallengeUserAttributePrefix = "userAttributes.";
private const string DerivedKeyInfo = "Caldera Derived Key";
private const int DerivedKeySize = 16;
private readonly BigInteger _a;
public BigInteger A;
private readonly BigInteger _g;
private readonly BigInteger _k;
public BigInteger N;
private readonly string _userPoolId;
public AuthenticationHelper(string userPool)
{
N = new BigInteger(InitN, 16);
_g = new BigInteger("2");
_k = new BigInteger(1, Hash(N.ToByteArray(), _g.ToByteArray()));
var random = new Random();
do
{
_a = new BigInteger(1024, random).Mod(N);
A = _g.ModPow(_a, N);
} while (Equals(A.Mod(N), BigInteger.Zero));
_userPoolId = userPool.Substring(userPool.IndexOf('_') + 1);
}
public byte[] GetPasswordAuthenticationKey(string userId, string userPassword, BigInteger b, BigInteger salt)
{
var u = new BigInteger(1, Hash(A.ToByteArray(), b.ToByteArray()));
var usernamePassword = $"{_userPoolId}{userId}:{userPassword}";
var userIdhash10 = HmacSha256Hash(usernamePassword);
var x = new BigInteger(1, Hash(salt.ToByteArray(), userIdhash10));
var s = Mod(b.Subtract(_k.Multiply(_g.ModPow(x, N))).ModPow(_a.Add(u.Multiply(x)), N), N);
var hmac = new HMACSHA256();
var hkdf = new HKDF(hmac, s.ToByteArray(), u.ToByteArray());
return hkdf.Expand(Encoding.UTF8.GetBytes(DerivedKeyInfo), DerivedKeySize);
}
public static byte[] Hash(params byte[][] blocks)
{
IDigest hash = new Sha256Digest();
var result = new byte[hash.GetDigestSize()];
foreach (var block in blocks)
hash.BlockUpdate(block, 0, block.Length);
hash.DoFinal(result, 0);
return result;
}
public static byte[] HmacSha256Hash(params string[] values)
{
IDigest hash = new Sha256Digest();
var result = new byte[hash.GetDigestSize()];
foreach (var value in values)
{
var bytes = Encoding.ASCII.GetBytes(value);
hash.BlockUpdate(bytes, 0, bytes.Length);
}
hash.DoFinal(result, 0);
return result;
}
private static BigInteger Mod(BigInteger dividend, BigInteger divisor)
{
// Apparently needed for negative dividends, according to AWS SDK for iOS
return divisor.Add(dividend.Remainder(divisor)).Remainder(divisor);
}
}
}
/* MIT LICENSE
Copyright (c) 2017 Markus Lachinger <[email protected]>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
*/
using Amazon.Runtime;
using System;
using System.Collections.Generic;
using System.Text;
using Amazon.CognitoIdentityProvider.Model;
using System.Security.Cryptography;
using Org.BouncyCastle.Security;
using Org.BouncyCastle.Math;
using System.Globalization;
namespace Com.Mmlac.Blog.IO
{
class Authentication
{
private static String AWS_CLIENT_ID = <insert your client ID here as String>;
public Authentication() { }
public static void LoginUser(String username, String password)
{
var AWS_REGION = //TODO: change to the region your pool is in! i.e.: Amazon.RegionEndpoint.USWest2
String POOL_NAME = <insert pool name here as String>; // (without the region identifier. It is usually <region>_<name>, just the name part here!)
AnonymousAWSCredentials cred = new AnonymousAWSCredentials();
// Identify your Cognito UserPool Provider
using (var provider = new Amazon.CognitoIdentityProvider.AmazonCognitoIdentityProviderClient(cred, AWS_REGION))
{
//Get the SRP variables A and a
var TupleAa = AuthenticationHelper.CreateAaTuple();
//Initiate auth with the generated SRP A
var authResponse = provider.InitiateAuth(new InitiateAuthRequest
{
ClientId = AWS_CLIENT_ID,
AuthFlow = Amazon.CognitoIdentityProvider.AuthFlowType.USER_SRP_AUTH,
AuthParameters = new Dictionary<string, string>() {
{ "USERNAME", username },
{ "SRP_A", TupleAa.Item1.ToString(16) }
}
});
//Now with the authResponse containing the password challenge for us, we need to
//set up a reply
//ChallengeParameters SALT, SECRET_BLOCK, SRP_B, USERNAME, USER_ID_FOR_SRP
DateTime timestamp = TimeZoneInfo.ConvertTimeToUtc(DateTime.Now);
//The timestamp format returned to AWS _needs_ to be in US Culture
CultureInfo usCulture = new CultureInfo("en-US");
String timeStr = timestamp.ToString("ddd MMM d HH:mm:ss \"UTC\" yyyy", usCulture);
//Do the hard work to generate the claim we return to AWS
byte[] claim = AuthenticationHelper.authenticateUser(authResponse.ChallengeParameters["USERNAME"],
"password",
POOL_NAME,
TupleAa,
authResponse.ChallengeParameters["SALT"],
authResponse.ChallengeParameters["SRP_B"],
authResponse.ChallengeParameters["SECRET_BLOCK"],
timeStr
);
String claimBase64 = System.Convert.ToBase64String(claim);
//Our response to AWS. If successful it will return an object with Tokens,
//if unsuccessful, it will throw an Exception that you should catch and handle.
var resp = provider.RespondToAuthChallenge(new RespondToAuthChallengeRequest
{
ChallengeName = authResponse.ChallengeName,
ClientId = "7dqval98fp7b0cmin849q97h7i",
ChallengeResponses = new Dictionary<string, string>() {
{ "PASSWORD_CLAIM_SECRET_BLOCK", authResponse.ChallengeParameters["SECRET_BLOCK"] },
{ "PASSWORD_CLAIM_SIGNATURE", claimBase64 },
{ "USERNAME", "mmlac" },
{ "TIMESTAMP", timeStr }
}
});
Console.WriteLine(""); //Statement for the debugger to hook
// From here on you either have your keys and the auth was successful, or you need to prompt your
// user for the correct credentials again. Or something else went wrong, the Exception contains the
// specific error text returned from AWS
}
}
}
/**
* Class for SRP client side math.
*/
class AuthenticationHelper
{
// static variables
private static String HEX_N =
"FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD1"
+ "29024E088A67CC74020BBEA63B139B22514A08798E3404DD"
+ "EF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245"
+ "E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7ED"
+ "EE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3D"
+ "C2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F"
+ "83655D23DCA3AD961C62F356208552BB9ED529077096966D"
+ "670C354E4ABC9804F1746C08CA18217C32905E462E36CE3B"
+ "E39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9"
+ "DE2BCBF6955817183995497CEA956AE515D2261898FA0510"
+ "15728E5A8AAAC42DAD33170D04507A33A85521ABDF1CBA64"
+ "ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7"
+ "ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6B"
+ "F12FFA06D98A0864D87602733EC86A64521F2B18177B200C"
+ "BBE117577A615D6C770988C0BAD946E208E24FA074E5AB31"
+ "43DB5BFCE0FD108E4B82D120A93AD2CAFFFFFFFFFFFFFFFF";
public static BigInteger N = new BigInteger(HEX_N, 16);
private static BigInteger g = BigInteger.ValueOf(2);
public static BigInteger k;
public static int EPHEMERAL_KEY_LENGTH = 1024;
public static int DERIVED_KEY_SIZE = 16;
public static String DERIVED_KEY_INFO = "Caldera Derived Key";
public static SecureRandom SECURE_RANDOM;
//static initializer
static AuthenticationHelper()
{
SECURE_RANDOM = SecureRandom.GetInstance("SHA1PRNG");
HashAlgorithm messageDigest = THREAD_MESSAGE_DIGEST;
byte[] nArr = N.ToByteArray();
byte[] gArr = g.ToByteArray();
byte[] content = new byte[nArr.Length + gArr.Length];
Buffer.BlockCopy(nArr, 0, content, 0, nArr.Length);
Buffer.BlockCopy(gArr, 0, content, nArr.Length, gArr.Length);
byte[] digest = messageDigest.ComputeHash(content);
k = new BigInteger(1, digest);
Console.WriteLine();
}
[ThreadStatic] private static HashAlgorithm THREAD_MESSAGE_DIGEST = HashAlgorithm.Create("SHA-256");
// methods below - all stateless
// return the Tuple of ( A, a )
public static Tuple<BigInteger, BigInteger> CreateAaTuple()
{
BigInteger a, A;
do
{
a = new BigInteger(EPHEMERAL_KEY_LENGTH, SECURE_RANDOM).Mod(N);
A = g.ModPow(a, N);
} while (A.Mod(N).Equals(BigInteger.Zero));
return Tuple.Create<BigInteger, BigInteger>(A, a);
}
//raturns the claim
public static byte[] authenticateUser(String username,
String password,
String poolName,
Tuple<BigInteger, BigInteger> TupleAa,
String saltString,
String srp_b,
String secretBlock,
String formattedTimestamp)
{
byte[] authSecretBlock = System.Convert.FromBase64String(secretBlock);
BigInteger B = new BigInteger(srp_b, 16);
if (B.Mod(AuthenticationHelper.N).Equals(BigInteger.Zero))
{
throw new Exception("B cannot be zero");
}
BigInteger salt = new BigInteger(saltString, 16);
// We need to generate the key to hash the response based on our A and what AWS sent back
byte[] key = getPasswordAuthenticationKey(username, password, poolName, TupleAa, B, salt);
// HMAC our data with key (HKDF(S)) (the shared secret)
byte[] hmac;
try
{
HMAC mac = HMAC.Create("HMACSHA256");
mac.Key = key;
//bytes bytes bytes....
byte[] poolNameByte = Encoding.UTF8.GetBytes(poolName);
byte[] name = Encoding.UTF8.GetBytes(username);
//secretBlock here
byte[] timeByte = Encoding.UTF8.GetBytes(formattedTimestamp);
byte[] content = new byte[poolNameByte.Length + name.Length + authSecretBlock.Length + timeByte.Length];
Buffer.BlockCopy(poolNameByte, 0, content, 0, poolNameByte.Length);
Buffer.BlockCopy(name, 0, content, poolNameByte.Length, name.Length);
Buffer.BlockCopy(authSecretBlock, 0, content, poolNameByte.Length + name.Length, authSecretBlock.Length);
Buffer.BlockCopy(timeByte, 0, content, poolNameByte.Length + name.Length + authSecretBlock.Length, timeByte.Length);
hmac = mac.ComputeHash(content);
}
catch (Exception e)
{
throw new Exception("Exception in authentication", e);
}
return hmac;
}
public static byte[] getPasswordAuthenticationKey(String userId,
String userPassword,
String poolName,
Tuple<BigInteger, BigInteger> Aa,
BigInteger B,
BigInteger salt)
{
// Authenticate the password
// u = H(A, B)
HashAlgorithm messageDigest = THREAD_MESSAGE_DIGEST;
byte[] aArr = Aa.Item1.ToByteArray();
byte[] bArr = B.ToByteArray();
byte[] content = new byte[aArr.Length + bArr.Length];
Buffer.BlockCopy(aArr, 0, content, 0, aArr.Length);
Buffer.BlockCopy(bArr, 0, content, aArr.Length, bArr.Length);
byte[] digest = messageDigest.ComputeHash(content);
BigInteger u = new BigInteger(1, digest);
if (u.Equals(BigInteger.Zero))
{
throw new Exception("Hash of A and B cannot be zero");
}
// x = H(salt | H(poolName | userId | ":" | password))
byte[] poolArr = Encoding.UTF8.GetBytes(poolName);
byte[] idArr = Encoding.UTF8.GetBytes(userId);
byte[] colonArr = Encoding.UTF8.GetBytes(":");
byte[] passArr = Encoding.UTF8.GetBytes(userPassword);
byte[] userIdContent = new byte[poolArr.Length + idArr.Length + colonArr.Length + passArr.Length];
Buffer.BlockCopy(poolArr, 0, userIdContent, 0, poolArr.Length);
Buffer.BlockCopy(idArr, 0, userIdContent, poolArr.Length, idArr.Length);
Buffer.BlockCopy(colonArr, 0, userIdContent, poolArr.Length + idArr.Length, colonArr.Length);
Buffer.BlockCopy(passArr, 0, userIdContent, poolArr.Length + idArr.Length + colonArr.Length, passArr.Length);
byte[] userIdHash = messageDigest.ComputeHash(userIdContent);
byte[] saltArr = salt.ToByteArray();
byte[] xArr = new byte[saltArr.Length + userIdHash.Length];
Buffer.BlockCopy(saltArr, 0, xArr, 0, saltArr.Length);
Buffer.BlockCopy(userIdHash, 0, xArr, saltArr.Length, userIdHash.Length);
byte[] xDigest = messageDigest.ComputeHash(xArr);
BigInteger x = new BigInteger(1, xDigest);
BigInteger S = (B.Subtract(k.Multiply(g.ModPow(x, N))).ModPow(Aa.Item2.Add(u.Multiply(x)), N)).Mod(N);
Hkdf hkdf = new Hkdf();
byte[] key = hkdf.DeriveKey(u.ToByteArray(), S.ToByteArray(), Encoding.UTF8.GetBytes(DERIVED_KEY_INFO), DERIVED_KEY_SIZE);
return key;
}
}
}
//https://gist.github.com/charlesportwoodii/7c5cf32e92ee88fec5e8f3270d0b44fc
using System;
using System.IO;
using System.Security.Cryptography;
namespace ConsoleApp3
{
/// <summary>
/// HMAC-based Extract-and-Expand Key Derivation Function (HKDF)
/// https://tools.ietf.org/html/rfc5869
/// </summary>
public class HKDF
{
private readonly int hashLength;
private readonly HMAC hmac;
private readonly byte[] prk;
/// <summary>
/// Initializes a new instance of the <see cref="HKDF" /> class.
/// </summary>
/// <param name="hmac">The HMAC hash function to use.</param>
/// <param name="ikm">input keying material.</param>
/// <param name="salt">
/// optional salt value (a non-secret random value); if not provided, it is set to a string of
/// HMAC.HashSize/8 zeros.
/// </param>
public HKDF(HMAC hmac, byte[] ikm, byte[] salt = null)
{
this.hmac = hmac;
hashLength = hmac.HashSize / 8;
// now we compute the PRK
hmac.Key = salt ?? new byte[hashLength];
prk = hmac.ComputeHash(ikm);
}
/// <summary>
/// Expands the specified info.
/// </summary>
/// <param name="info">optional context and application specific information (can be a zero-length string)</param>
/// <param name="l">length of output keying material in octets (&lt;= 255*HashLen)</param>
/// <returns>OKM (output keying material) of L octets</returns>
public byte[] Expand(byte[] info, int l)
{
if (info == null) info = new byte[0];
hmac.Key = prk;
var n = (int) Math.Ceiling(l * 1f / hashLength);
var t = new byte[n * hashLength];
using (var ms = new MemoryStream())
{
var prev = new byte[0];
for (var i = 1; i <= n; i++)
{
ms.Write(prev, 0, prev.Length);
if (info.Length > 0) ms.Write(info, 0, info.Length);
ms.WriteByte((byte) (0x01 * i));
prev = hmac.ComputeHash(ms.ToArray());
Array.Copy(prev, 0, t, (i - 1) * hashLength, hashLength);
ms.SetLength(0); //reset
}
}
var okm = new byte[l];
Array.Copy(t, okm, okm.Length);
return okm;
}
}
}
using System;
using System.Globalization;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using Amazon;
using Amazon.CognitoIdentityProvider;
using Amazon.CognitoIdentityProvider.Model;
using Amazon.Runtime;
using Amazon.Runtime.Internal;
using Amazon.Runtime.Internal.Util;
using Amazon.Util;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Math;
using Org.BouncyCastle.Security;
namespace ConsoleApp3
{
//https://gist.github.com/KonajuGames/0253adf035d83e3b58a872fb00e4f398
//http://stackoverflow.com/questions/12185122/calculating-hmacsha256-using-c-sharp-to-match-payment-provider-example
//http://pastebin.com/xAAuZrJX
public class Program
{
private const string ClientId = "";
private const string ClientSecret = "";
private const string UserPoolId = "";
private const string Email = "";
public static void Main(string[] args)
{
var p = new Program();
p.Test1();
Console.WriteLine("done...");
Console.ReadLine();
}
public string SecretHash()
{
return CryptoUtilFactory.CryptoInstance.HMACSign($"{Email}{ClientId}", ClientSecret,
SigningAlgorithm.HmacSHA256);
}
public async Task Test1()
{
try
{
//AWSConfigs.ProxyConfig.Host = "127.0.0.1";
//AWSConfigs.ProxyConfig.Port = 8888;
WebRequest.DefaultWebProxy = new WebProxy("127.0.0.1", 8888);
var authenticationHelper = new AuthenticationHelper(UserPoolId);
var authParams = new AutoConstructedDictionary<string, string>
{
["SECRET_HASH"] = SecretHash(),
["USERNAME"] = Email,
["SRP_A"] = authenticationHelper.A.ToString(16),
["DEVICE_KEY"] = null
};
var request = new InitiateAuthRequest
{
AuthFlow = AuthFlowType.USER_SRP_AUTH,
AuthParameters = authParams,
ClientId = ClientId
};
var amazonCognitoIdentityProviderClient = new AmazonCognitoIdentityProviderClient(RegionEndpoint.USEast1);
var response = await amazonCognitoIdentityProviderClient.InitiateAuthAsync(request);
var userIdForSrp = response.ChallengeParameters["USER_ID_FOR_SRP"];
var userIdForSrpBytes = Encoding.UTF8.GetBytes(userIdForSrp);
var b = new BigInteger(response.ChallengeParameters["SRP_B"], 16);
if (Equals(b.Mod(authenticationHelper.N), BigInteger.Zero))
throw new InvalidOperationException("SRP error, B cannot be zero");
var salt = new BigInteger(response.ChallengeParameters["SALT"], 16);
var key = authenticationHelper.GetPasswordAuthenticationKey(userIdForSrp, "MyPassword", b, salt);
var userPoolId = UserPoolId.Substring(UserPoolId.IndexOf('_') + 1);
var userPoolIdBytes = Encoding.UTF8.GetBytes(userPoolId);
var formatTimestamp = AWSSDKUtils.CorrectedUtcNow.ToString("ddd MMM d HH:mm:ss UTC yyyy",
CultureInfo.InvariantCulture);
var keySpec = new KeyParameter(key);
var mac = MacUtilities.GetMac("HMAC-SHA_256");
mac.Init(keySpec);
mac.BlockUpdate(userPoolIdBytes, 0, userPoolId.Length);
mac.BlockUpdate(userIdForSrpBytes, 0, userIdForSrpBytes.Length);
var secretBlock = Convert.FromBase64String(response.ChallengeParameters["SECRET_BLOCK"]);
mac.BlockUpdate(secretBlock, 0, secretBlock.Length);
var dateString = AWSSDKUtils.CorrectedUtcNow.ToString("ddd MMM d HH:mm:ss UTC yyyy",
CultureInfo.InvariantCulture);
var dateBytes = Encoding.UTF8.GetBytes(dateString);
mac.BlockUpdate(dateBytes, 0, dateBytes.Length);
var hmac = new byte[mac.GetMacSize()];
mac.DoFinal(hmac, 0);
var claimSignature = Convert.ToBase64String(hmac);
var srpAuthResponses = new AutoConstructedDictionary<string, string>
{
["PASSWORD_CLAIM_SECRET_BLOCK"] = response.ChallengeParameters["SECRET_BLOCK"],
["PASSWORD_CLAIM_SIGNATURE"] = claimSignature,
["TIMESTAMP"] = formatTimestamp,
["USERNAME"] = response.ChallengeParameters["USERNAME"],
["DEVICE_KEY"] = null,
["SECRET_HASH"] = SecretHash()
};
var authChallengeRequest = new RespondToAuthChallengeRequest
{
ChallengeName = response.ChallengeName,
ChallengeResponses = srpAuthResponses,
ClientId = ClientId,
Session = response.Session
};
var authChallengeResponse =
await amazonCognitoIdentityProviderClient.RespondToAuthChallengeAsync(authChallengeRequest);
}
catch (Exception e)
{
Console.WriteLine(e);
}
}
}
}
@jsheehan61
Copy link

jsheehan61 commented Aug 17, 2017

I'm getting username or password is incorrect. Where would you even start debugging this

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment