-
-
Save jenky/a4465f73adf90206b3e98c3d36a3be4f to your computer and use it in GitHub Desktop.
| <?php | |
| use Aws\AwsClient; | |
| use Aws\Result; | |
| use Carbon\Carbon; | |
| use phpseclib3\Math\BigInteger; | |
| class AwsCognitoIdentitySRP | |
| { | |
| const N_HEX = 'FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD1'. | |
| '29024E088A67CC74020BBEA63B139B22514A08798E3404DD'. | |
| 'EF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245'. | |
| 'E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7ED'. | |
| 'EE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3D'. | |
| 'C2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F'. | |
| '83655D23DCA3AD961C62F356208552BB9ED529077096966D'. | |
| '670C354E4ABC9804F1746C08CA18217C32905E462E36CE3B'. | |
| 'E39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9'. | |
| 'DE2BCBF6955817183995497CEA956AE515D2261898FA0510'. | |
| '15728E5A8AAAC42DAD33170D04507A33A85521ABDF1CBA64'. | |
| 'ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7'. | |
| 'ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6B'. | |
| 'F12FFA06D98A0864D87602733EC86A64521F2B18177B200C'. | |
| 'BBE117577A615D6C770988C0BAD946E208E24FA074E5AB31'. | |
| '43DB5BFCE0FD108E4B82D120A93AD2CAFFFFFFFFFFFFFFFF'; | |
| const G_HEX = '2'; | |
| const INFO_BITS = 'Caldera Derived Key'; | |
| /** | |
| * @var \phpseclib3\Math\BigInteger | |
| */ | |
| protected $N; | |
| /** | |
| * @var \phpseclib3\Math\BigInteger | |
| */ | |
| protected $g; | |
| /** | |
| * @var \phpseclib3\Math\BigInteger | |
| */ | |
| protected $k; | |
| /** | |
| * @var \phpseclib3\Math\BigInteger | |
| */ | |
| protected $a; | |
| /** | |
| * @var \phpseclib3\Math\BigInteger | |
| */ | |
| protected $A; | |
| /** | |
| * @var string | |
| */ | |
| protected $clientId; | |
| /** | |
| * @var string | |
| */ | |
| protected $poolName; | |
| /** | |
| * @var \Aws\AwsClient | |
| */ | |
| protected $client; | |
| /** | |
| * Create new AWS CognitoIDP instance. | |
| * | |
| * @param \Aws\AwsClient $client | |
| * @param string $clientId | |
| * @param string $poolName | |
| * @return void | |
| */ | |
| public function __construct(AwsClient $client, string $clientId, string $poolName) | |
| { | |
| $this->N = new BigInteger(static::N_HEX, 16); | |
| $this->g = new BigInteger(static::G_HEX, 16); | |
| $this->k = new BigInteger($this->hexHash('00'.static::N_HEX.'0'.static::G_HEX), 16); | |
| $this->smallA(); | |
| $this->largeA(); | |
| $this->client = $client; | |
| $this->clientId = $clientId; | |
| $this->poolName = $poolName; | |
| } | |
| /** | |
| * Get random a value. | |
| * | |
| * @return \phpseclib3\Math\BigInteger | |
| */ | |
| public function smallA(): BigInteger | |
| { | |
| if (is_null($this->a)) { | |
| $this->a = $this->generateRandomSmallA(); | |
| } | |
| return $this->a; | |
| } | |
| /** | |
| * Get the client's public value A with the generated random number a. | |
| * | |
| * @return \phpseclib3\Math\BigInteger | |
| */ | |
| public function largeA(): BigInteger | |
| { | |
| if (is_null($this->A)) { | |
| $this->A = $this->calculateA($this->smallA()); | |
| } | |
| return $this->A; | |
| } | |
| /** | |
| * Generate random bytes as hexadecimal string. | |
| * | |
| * @param int $bytes | |
| * @return \phpseclib3\Math\BigInteger | |
| */ | |
| public function bytes(int $bytes = 32): BigInteger | |
| { | |
| $bytes = bin2hex(random_bytes($bytes)); | |
| return new BigInteger($bytes, 16); | |
| } | |
| /** | |
| * Converts a BigInteger (or hex string) to hex format padded with zeroes for hashing. | |
| * | |
| * @param \phpseclib3\Math\BigInteger|string $longInt | |
| * @return string | |
| */ | |
| public function padHex($longInt): string | |
| { | |
| $hashStr = $longInt instanceof BigInteger ? $longInt->toHex() : $longInt; | |
| if (strlen($hashStr) % 2 === 1) { | |
| $hashStr = '0'.$hashStr; | |
| } elseif (strpos('89ABCDEFabcdef', $hashStr[0] ?? '') !== false) { | |
| $hashStr = '00'.$hashStr; | |
| } | |
| return $hashStr; | |
| } | |
| /** | |
| * Calculate a hash from a hex string. | |
| * | |
| * @param string $value | |
| * @return string | |
| */ | |
| public function hexHash(string $value): string | |
| { | |
| return $this->hash(hex2bin($value)); | |
| } | |
| /** | |
| * Calculate a hash from string. | |
| * | |
| * @param string $value | |
| * @return string | |
| */ | |
| public function hash($value): string | |
| { | |
| $hash = hash('sha256', $value); | |
| return str_repeat('0', 64 - strlen($hash)).$hash; | |
| } | |
| /** | |
| * Performs modulo between big integers. | |
| * | |
| * @param \phpseclib3\Math\BigInteger $a | |
| * @param \phpseclib3\Math\BigInteger $b | |
| * @return \phpseclib3\Math\BigInteger | |
| */ | |
| protected function mod(BigInteger $a, BigInteger $b): BigInteger | |
| { | |
| return $a->powMod(new BigInteger(1), $b); | |
| } | |
| /** | |
| * Generate a random big integer. | |
| * | |
| * @return \phpseclib3\Math\BigInteger | |
| */ | |
| public function generateRandomSmallA(): BigInteger | |
| { | |
| return $this->mod($this->bytes(128), $this->N); | |
| } | |
| /** | |
| * Calculate the client's public value A = g^a%N. | |
| * | |
| * @param \phpseclib3\Math\BigInteger $a | |
| * @return \phpseclib3\Math\BigInteger | |
| * | |
| * @throws \InvalidArgumentException | |
| */ | |
| public function calculateA(BigInteger $a): BigInteger | |
| { | |
| $A = $this->g->powMod($a, $this->N); | |
| if ($this->mod($a, $this->N)->equals(new BigInteger(0))) { | |
| throw new \InvalidArgumentException('Public key failed A mod N == 0 check.'); | |
| } | |
| return $A; | |
| } | |
| /** | |
| * Calculate the client's value U which is the hash of A and B. | |
| * | |
| * @param \phpseclib3\Math\BigInteger $A | |
| * @param \phpseclib3\Math\BigInteger $B | |
| * @return \phpseclib3\Math\BigInteger | |
| */ | |
| public function calculateU(BigInteger $A, BigInteger $B): BigInteger | |
| { | |
| $A = $this->padHex($A); | |
| $B = $this->padHex($B); | |
| return new BigInteger($this->hexHash($A.$B), 16); | |
| } | |
| /** | |
| * Extract the pool ID from pool name. | |
| * | |
| * @return null|string | |
| */ | |
| protected function poolId(): ?string | |
| { | |
| return explode('_', $this->poolName)[1] ?? null; | |
| } | |
| /** | |
| * Authenticate user with given username and password. | |
| * | |
| * @param string $username | |
| * @param string $password | |
| * @return \Aws\Result | |
| * | |
| * @throws \RuntimeException | |
| */ | |
| public function authenticateUser(string $username, string $password): Result | |
| { | |
| $result = $this->client->initiateAuth([ | |
| 'AuthFlow' => 'USER_SRP_AUTH', | |
| 'ClientId' => $this->clientId, | |
| 'UserPoolId' => $this->poolName, | |
| 'AuthParameters' => [ | |
| 'USERNAME' => $username, | |
| 'SRP_A' => $this->largeA()->toHex(), | |
| ], | |
| ]); | |
| if ($result->get('ChallengeName') != 'PASSWORD_VERIFIER') { | |
| throw new \RuntimeException("ChallengeName `{$result->get('ChallengeName')}` is not supported."); | |
| } | |
| return $this->client->respondToAuthChallenge([ | |
| 'ChallengeName' => 'PASSWORD_VERIFIER', | |
| 'ClientId' => $this->clientId, | |
| 'ChallengeResponses' => $this->processChallenge($result, $password) | |
| ]); | |
| } | |
| /** | |
| * Generate authentication challenge response params. | |
| * | |
| * @param \Aws\Result $result | |
| * @param string $password | |
| * @return array | |
| */ | |
| protected function processChallenge(Result $result, string $password): array | |
| { | |
| $challengeParameters = $result->get('ChallengeParameters'); | |
| $time = Carbon::now('UTC')->format('D M j H:i:s e Y'); | |
| $secretBlock = base64_decode($challengeParameters['SECRET_BLOCK']); | |
| $userId = $challengeParameters['USER_ID_FOR_SRP']; | |
| $hkdf = $this->getPasswordAuthenticationKey( | |
| $userId, | |
| $password, | |
| $challengeParameters['SRP_B'], | |
| $challengeParameters['SALT'] | |
| ); | |
| $msg = $this->poolId().$userId.$secretBlock.$time; | |
| $signature = hash_hmac('sha256', $msg, $hkdf, true); | |
| return [ | |
| 'TIMESTAMP' => $time, | |
| 'USERNAME' => $userId, | |
| 'PASSWORD_CLAIM_SECRET_BLOCK' => $challengeParameters['SECRET_BLOCK'], | |
| 'PASSWORD_CLAIM_SIGNATURE' => base64_encode($signature), | |
| ]; | |
| } | |
| /** | |
| * Calculates the final hkdf based on computed S value, and computed U value and the key. | |
| * | |
| * @param string $username | |
| * @param string $password | |
| * @param string $server | |
| * @param string $salt | |
| * @return string | |
| * | |
| * @throws \RuntimeException | |
| */ | |
| protected function getPasswordAuthenticationKey(string $username, string $password, string $server, string $salt): string | |
| { | |
| $u = $this->calculateU($this->largeA(), $serverB = new BigInteger($server, 16)); | |
| if ($u->equals(new BigInteger(0))) { | |
| throw new \RuntimeException('U cannot be zero.'); | |
| } | |
| $usernamePassword = sprintf('%s%s:%s', $this->poolId(), $username, $password); | |
| $usernamePasswordHash = $this->hash($usernamePassword); | |
| $x = new BigInteger($this->hexHash($this->padHex($salt).$usernamePasswordHash), 16); | |
| $gModPowXN = $this->g->modPow($x, $this->N); | |
| $intValue2 = $serverB->subtract($this->k->multiply($gModPowXN)); | |
| $s = $intValue2->modPow($this->smallA()->add($u->multiply($x)), $this->N); | |
| return $this->computeHkdf( | |
| hex2bin($this->padHex($s)), | |
| hex2bin($this->padHex($u)) | |
| ); | |
| } | |
| /** | |
| * Standard hkdf algorithm. | |
| * | |
| * @param string $ikm | |
| * @param string $salt | |
| * @return string | |
| */ | |
| protected function computeHkdf(string $ikm, string $salt): string | |
| { | |
| return hash_hkdf('sha256', $ikm, 16, static::INFO_BITS, $salt); | |
| } | |
| } |
Hello @jenky,
we're encountering an issue with the DEVICE_PASSWORD_VERIFIER challenge - it's returning an 'Incorrect username and password' error. Could you possibly assist us by providing code that utilizes both DEVICE_SRP_AUTH and DEVICE_PASSWORD_VERIFIER? We believe this could resolve our problem.
public function authenticateUser(string $username, string $password, string $deviceKey)
{
try {
$result = $this->client->initiateAuth([
'AuthFlow' => 'USER_PASSWORD_AUTH',
'ClientId' => config('services.aws_cognito.client_id'),
'UserPoolId' => config('services.aws_cognito.user_pool_id'),
'AuthParameters' => [
'USERNAME' => $username,
'PASSWORD' => $password,
'DEVICE_KEY' => $deviceKey,
],
]);
if ($result->get('ChallengeName') == 'DEVICE_SRP_AUTH') {
$result = $this->client->respondToAuthChallenge([
'ChallengeName' => 'DEVICE_SRP_AUTH',
'ClientId' => $this->clientId,
'ChallengeResponses' => [
'USERNAME' => $username,
'DEVICE_KEY' => $deviceKey,
'SRP_A' => $this->largeA()->toHex(),
],
'SESSION' => $result->get('Session'),
]);
}
if ($result->get('ChallengeName') != 'DEVICE_PASSWORD_VERIFIER') {
throw new RuntimeException("ChallengeName `{$result->get('ChallengeName')}` is not supported.");
}
if ($result->get('ChallengeName') == 'DEVICE_PASSWORD_VERIFIER') {
$challengeParameters = $this->processChallenge($result, $password);
$result = $this->client->respondToAuthChallenge([
'ChallengeName' => 'DEVICE_PASSWORD_VERIFIER',
'ClientId' => $this->clientId,
'ChallengeResponses' => $challengeParameters,
]);
}
} catch (\Throwable $th) {
print_r($th->getMessage());
die;
}
}
/**
* Generate authentication challenge response params.
*/
/**
* Generate authentication challenge response params.
*/
protected function processChallenge(Result $result, $password): array
{
$challengeParameters = $result->get('ChallengeParameters');
$time = Carbon::now()->tz('UTC')->format('D M j H:i:s e Y');
$secretBlock = base64_decode($challengeParameters['SECRET_BLOCK']);
$userName = $challengeParameters['USERNAME'];
$deviceKey = $challengeParameters['DEVICE_KEY'];
$deviceGroupKey = '*********';
//$fullPassword = $this->getDeviceSecretVerifierConfig($deviceGroupKey, $userName);
$hkdf = $this->getPasswordAuthenticationKey(
$userName,
$password,
$challengeParameters['SRP_B'],
$challengeParameters['SALT']
);
$msg = $this->poolId().$userName.$secretBlock.$time;
$signature = hash_hmac('sha256', $msg, $hkdf, true);
return [
'TIMESTAMP' => $time,
'USERNAME' => $userName,
'DEVICE_KEY' => $deviceKey,
'PASSWORD_CLAIM_SECRET_BLOCK' => $challengeParameters['SECRET_BLOCK'],
'PASSWORD_CLAIM_SIGNATURE' => base64_encode($signature),
];
}
Thank you!
@steveWinter Hi, can you plz share you js code in which you have mplenented password_verifirer challenge ?
@jenky
Thanks for the great gist.
I would like to publish a composer package based on this implementation, is that a problem?
The package will include a reference to this gist.
I have made a composer package of the code based on this great gist. (Of course, the link to this gist is included.)
Please let me know if there are any problems and I will remove it.
I have question what will be in the case of DEVICE_SRP_AUTH, DEVICE_PASSWORD_VERIFIER