-
-
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); | |
} | |
} |
Life saver...
This is hopefully so painfully obvious that no one else has this problem, but I debugged 'Incorrect username or password' for hours.
It turned out that I was using my IdentityPoolId instead of my UserPoolId in the poolId field. Changing it got the auth working immediately
Thanks so much for this, was a life saver. BUT!!! the service I am accessing has now switched on MFA permanently and I need to have my system capable of accessing it unattended.
The answer is to implement Device Authentication. This seems to almost work for me but fails as the final step (DEVICE_PASSWORD_VERIFIER) just thowing the usual 'Incorrect username or password'.
As far as I can see there are two potential problem areas:
1 - confirmDevice - Where you have to confirm the device you're using and send over DeviceSecretVerifierConfig details including a PasswordVerifier and salt. Given that this is the primary area of difference from your code above I think this is where it is going wrong and hence even if I am getting things correct on the finl DEVICE_PASSWORD_VERIFIER section it will of course fail due to this earlier issue. This is my hack:
$passSalt = $this->getDeviceSecretVerifierConfig($deviceGroupKey, $deviceK);
$result = $this->client->confirmDevice([
'AccessToken' => $accessT,
'DeviceKey' => $deviceK,
'DeviceName' => $location." Auto",
'DeviceSecretVerifierConfig' => [
'PasswordVerifier' => $passSalt['PasswordVerifier'],
'Salt' => $passSalt['Salt'],
],
]);
public function getDeviceSecretVerifierConfig(string $deviceGroupKey, string $deviceKey): array
{
$randomPassword = $this->bytes(40);
$fullPassword = $this->hash(sprintf('%s%s:%s', $deviceGroupKey, $deviceKey, $randomPassword));
$salt = $this->bytes(16);
$SaltToHashDevices = $this->padHex(new BigInteger($salt->toHex(),16));
$passwordVerifier = $this->g->modPow(
new BigInteger($this->hexHash($SaltToHashDevices . $fullPassword), 16),
$this->N
);
return [
'Salt' => base64_encode($this->padHex($SaltToHashDevices)),
'PasswordVerifier' => base64_encode($this->padHex($passwordVerifier)),
'rndPass' => $randomPassword //->toString()
];
}
2 - DEVICE_PASSWORD_VERIFIER has a problem... as said though I believe the problem is most likely with the above.
So to the question, has anyone expanded this for device authentication, and if you have it working PLEASE cna you let me have the solution as it is making me mad.
commented
So After many hours....... I have managed to fix it. The issue was in my getDeviceSecretVerifierConfig function which now reads as follows:
public function getDeviceSecretVerifierConfig(string $deviceGroupKey, string $deviceKey): array
{
$randomPassword = $this->bytes(40);
$fullPassword = $this->hash(sprintf('%s%s:%s', $deviceGroupKey, $deviceKey, $randomPassword));
$salt = $this->bytes(16);
$SaltToHashDevices = $this->padHex(new BigInteger($salt->toHex(),16));
$x = new BigInteger($this->hexHash($SaltToHashDevices.$fullPassword), 16);
$gModPowXN = $this->g->modPow($x, $this->N);
$passwordVerifier = $this->padHex($gModPowXN->toHex());
return [
'Salt' => base64_encode(hex2bin($SaltToHashDevices)),
'PasswordVerifier' => base64_encode(hex2bin($passwordVerifier)),
'rndPass' => $randomPassword
];
}
Hopefully this will save someone else a whole lot of time if they need to add device verification. Thankfully I can now wave goodbye to sms MFA requests.... or at least until the service is 'played' with again.
Enjoy
Hi, we are trying to implement device authentication, using this code. The problem is that after responding to the SMS_MFA challenge, I try to call confirmDevice like this:
$response = $this->client->respondToAuthChallenge([
"ChallengeName" => "SMS_MFA",
"ChallengeResponses" => $challengeResponses,
'Session' => $session,
'ClientId' => $this->appClientId,
'UserPoolId' => $this->userPoolId,
]);
$metadata = $response->get('AuthenticationResult');
$accessToken = $metadata['AccessToken'] ?? null;
$deviceGroupKey = $metadata['NewDeviceMetadata']['DeviceGroupKey'] ?? null;
$deviceKey = $metadata['NewDeviceMetadata']['DeviceKey'] ?? null;
$passSalt = $srp->getDeviceSecretVerifierConfig($deviceGroupKey, $deviceKey);
$result = $this->client->confirmDevice([
'AccessToken' => $accessToken,
'DeviceKey' => $deviceKey,
'DeviceName' => "My personal location" . " Auto",
'DeviceSecretVerifierConfig' => [
'PasswordVerifier' => $passSalt['PasswordVerifier'],
'Salt' => $passSalt['Salt'],
],
]);
but the client returns the error "Invalid device key given.". does anyone have ideas on how to fix this?
thanks!
you are a life saver. thank you. your buy me a beer link 404s but if you've got another send it over.
You can try this one. I'm glad that my code was able to help.
I have question what will be in the case of DEVICE_SRP_AUTH, DEVICE_PASSWORD_VERIFIER
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'm also getting the "incorrect username or password" error: were you able to solve it? Thanks in advance. @marco3211 @jenky