Update website
This commit is contained in:
parent
41ce1aa076
commit
ea0eb1c6e0
4222 changed files with 721797 additions and 14 deletions
289
admin/phpMyAdmin/libraries/classes/WebAuthn/CBORDecoder.php
Normal file
289
admin/phpMyAdmin/libraries/classes/WebAuthn/CBORDecoder.php
Normal file
|
@ -0,0 +1,289 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace PhpMyAdmin\WebAuthn;
|
||||
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
use function ord;
|
||||
use function unpack;
|
||||
|
||||
use const INF;
|
||||
use const NAN;
|
||||
|
||||
/**
|
||||
* Concise Binary Object Representation (CBOR) decoder.
|
||||
*
|
||||
* This is not a general purpose CBOR decoder and only implements the CTAP2 canonical CBOR encoding form.
|
||||
*
|
||||
* @see https://www.rfc-editor.org/rfc/rfc7049
|
||||
* @see https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#message-encoding
|
||||
*/
|
||||
final class CBORDecoder
|
||||
{
|
||||
/**
|
||||
* @return mixed
|
||||
*
|
||||
* @throws WebAuthnException
|
||||
*/
|
||||
public function decode(DataStream $stream)
|
||||
{
|
||||
return $this->wellFormed($stream);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://www.rfc-editor.org/rfc/rfc7049#appendix-C
|
||||
*
|
||||
* @return mixed
|
||||
*
|
||||
* @throws WebAuthnException
|
||||
*/
|
||||
private function wellFormed(DataStream $stream)
|
||||
{
|
||||
// process initial bytes
|
||||
$initialByte = ord($stream->take(1));
|
||||
$majorType = $initialByte >> 5;
|
||||
$value = $additionalInformation = $initialByte & 0x1f;
|
||||
switch ($additionalInformation) {
|
||||
case 24:
|
||||
if ($majorType !== 7) {
|
||||
$value = ord($stream->take(1));
|
||||
}
|
||||
|
||||
break;
|
||||
case 25:
|
||||
if ($majorType !== 7) {
|
||||
$unpackedValue = unpack('n', $stream->take(2));
|
||||
Assert::isArray($unpackedValue);
|
||||
Assert::keyExists($unpackedValue, 1);
|
||||
Assert::integer($unpackedValue[1]);
|
||||
$value = $unpackedValue[1];
|
||||
}
|
||||
|
||||
break;
|
||||
case 26:
|
||||
if ($majorType !== 7) {
|
||||
$unpackedValue = unpack('N', $stream->take(4));
|
||||
Assert::isArray($unpackedValue);
|
||||
Assert::keyExists($unpackedValue, 1);
|
||||
Assert::integer($unpackedValue[1]);
|
||||
$value = $unpackedValue[1];
|
||||
}
|
||||
|
||||
break;
|
||||
case 27:
|
||||
if ($majorType !== 7) {
|
||||
$unpackedValue = unpack('J', $stream->take(8));
|
||||
Assert::isArray($unpackedValue);
|
||||
Assert::keyExists($unpackedValue, 1);
|
||||
Assert::integer($unpackedValue[1]);
|
||||
$value = $unpackedValue[1];
|
||||
}
|
||||
|
||||
break;
|
||||
case 28:
|
||||
case 29:
|
||||
case 30:
|
||||
case 31:
|
||||
throw new WebAuthnException();
|
||||
}
|
||||
|
||||
// process content
|
||||
switch ($majorType) {
|
||||
case 0:
|
||||
return $this->getUnsignedInteger($value);
|
||||
|
||||
case 1:
|
||||
return $this->getNegativeInteger($value);
|
||||
|
||||
case 2:
|
||||
return $this->getByteString($stream, $value);
|
||||
|
||||
case 3:
|
||||
return $this->getTextString($stream, $value);
|
||||
|
||||
case 4:
|
||||
return $this->getList($stream, $value);
|
||||
|
||||
case 5:
|
||||
return $this->getMap($stream, $value);
|
||||
|
||||
case 6:
|
||||
return $this->getTag($stream);
|
||||
|
||||
case 7:
|
||||
return $this->getFloatNumberOrSimpleValue($stream, $value, $additionalInformation);
|
||||
|
||||
default:
|
||||
throw new WebAuthnException();
|
||||
}
|
||||
}
|
||||
|
||||
private function getUnsignedInteger(int $value): int
|
||||
{
|
||||
return $value;
|
||||
}
|
||||
|
||||
private function getNegativeInteger(int $value): int
|
||||
{
|
||||
return -1 - $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws WebAuthnException
|
||||
*/
|
||||
private function getByteString(DataStream $stream, int $value): string
|
||||
{
|
||||
return $stream->take($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws WebAuthnException
|
||||
*/
|
||||
private function getTextString(DataStream $stream, int $value): string
|
||||
{
|
||||
return $stream->take($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-return list<mixed>
|
||||
*
|
||||
* @throws WebAuthnException
|
||||
*/
|
||||
private function getList(DataStream $stream, int $value): array
|
||||
{
|
||||
$list = [];
|
||||
for ($i = 0; $i < $value; $i++) {
|
||||
/** @psalm-suppress MixedAssignment */
|
||||
$list[] = $this->wellFormed($stream);
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-return array<array-key, mixed>
|
||||
*
|
||||
* @throws WebAuthnException
|
||||
*/
|
||||
private function getMap(DataStream $stream, int $value): array
|
||||
{
|
||||
$map = [];
|
||||
for ($i = 0; $i < $value; $i++) {
|
||||
/** @psalm-suppress MixedAssignment, MixedArrayOffset */
|
||||
$map[$this->wellFormed($stream)] = $this->wellFormed($stream);
|
||||
}
|
||||
|
||||
return $map;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*
|
||||
* @throws WebAuthnException
|
||||
*/
|
||||
private function getTag(DataStream $stream)
|
||||
{
|
||||
// 1 embedded data item
|
||||
return $this->wellFormed($stream);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*
|
||||
* @throws WebAuthnException
|
||||
*/
|
||||
private function getFloatNumberOrSimpleValue(DataStream $stream, int $value, int $additionalInformation)
|
||||
{
|
||||
switch ($additionalInformation) {
|
||||
case 20:
|
||||
return true;
|
||||
|
||||
case 21:
|
||||
return false;
|
||||
|
||||
case 22:
|
||||
return null;
|
||||
|
||||
case 24:
|
||||
// simple value
|
||||
return ord($stream->take(1));
|
||||
|
||||
case 25:
|
||||
return $this->getHalfFloat($stream);
|
||||
|
||||
case 26:
|
||||
return $this->getSingleFloat($stream);
|
||||
|
||||
case 27:
|
||||
return $this->getDoubleFloat($stream);
|
||||
|
||||
case 31:
|
||||
// "break" stop code for indefinite-length items
|
||||
throw new WebAuthnException();
|
||||
|
||||
default:
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* IEEE 754 Half-Precision Float (16 bits follow)
|
||||
*
|
||||
* @see https://www.rfc-editor.org/rfc/rfc7049#appendix-D
|
||||
*
|
||||
* @throws WebAuthnException
|
||||
*/
|
||||
private function getHalfFloat(DataStream $stream): float
|
||||
{
|
||||
$value = unpack('n', $stream->take(2));
|
||||
Assert::isArray($value);
|
||||
Assert::keyExists($value, 1);
|
||||
Assert::integer($value[1]);
|
||||
|
||||
$half = $value[1];
|
||||
$exp = ($half >> 10) & 0x1f;
|
||||
$mant = $half & 0x3ff;
|
||||
|
||||
if ($exp === 0) {
|
||||
$val = $mant * (2 ** -24);
|
||||
} elseif ($exp !== 31) {
|
||||
$val = ($mant + 1024) * (2 ** ($exp - 25));
|
||||
} else {
|
||||
$val = $mant === 0 ? INF : NAN;
|
||||
}
|
||||
|
||||
return $half & 0x8000 ? -$val : $val;
|
||||
}
|
||||
|
||||
/**
|
||||
* IEEE 754 Single-Precision Float (32 bits follow)
|
||||
*
|
||||
* @throws WebAuthnException
|
||||
*/
|
||||
private function getSingleFloat(DataStream $stream): float
|
||||
{
|
||||
$value = unpack('G', $stream->take(4));
|
||||
Assert::isArray($value);
|
||||
Assert::keyExists($value, 1);
|
||||
Assert::float($value[1]);
|
||||
|
||||
return $value[1];
|
||||
}
|
||||
|
||||
/**
|
||||
* IEEE 754 Double-Precision Float (64 bits follow)
|
||||
*
|
||||
* @throws WebAuthnException
|
||||
*/
|
||||
private function getDoubleFloat(DataStream $stream): float
|
||||
{
|
||||
$value = unpack('E', $stream->take(8));
|
||||
Assert::isArray($value);
|
||||
Assert::keyExists($value, 1);
|
||||
Assert::float($value[1]);
|
||||
|
||||
return $value[1];
|
||||
}
|
||||
}
|
497
admin/phpMyAdmin/libraries/classes/WebAuthn/CustomServer.php
Normal file
497
admin/phpMyAdmin/libraries/classes/WebAuthn/CustomServer.php
Normal file
|
@ -0,0 +1,497 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace PhpMyAdmin\WebAuthn;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use SodiumException;
|
||||
use Throwable;
|
||||
use Webmozart\Assert\Assert;
|
||||
use Webmozart\Assert\InvalidArgumentException;
|
||||
|
||||
use function hash;
|
||||
use function hash_equals;
|
||||
use function json_decode;
|
||||
use function mb_strlen;
|
||||
use function mb_substr;
|
||||
use function ord;
|
||||
use function parse_url;
|
||||
use function random_bytes;
|
||||
use function sodium_base642bin;
|
||||
use function sodium_bin2base64;
|
||||
use function unpack;
|
||||
|
||||
use const PHP_URL_HOST;
|
||||
use const SODIUM_BASE64_VARIANT_ORIGINAL;
|
||||
use const SODIUM_BASE64_VARIANT_URLSAFE_NO_PADDING;
|
||||
|
||||
/**
|
||||
* Web Authentication API server.
|
||||
*
|
||||
* @see https://www.w3.org/TR/webauthn-3/
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API
|
||||
* @see https://webauthn.guide/
|
||||
*/
|
||||
final class CustomServer implements Server
|
||||
{
|
||||
public function getCredentialCreationOptions(string $userName, string $userId, string $relyingPartyId): array
|
||||
{
|
||||
return [
|
||||
'challenge' => $this->generateChallenge(),
|
||||
'rp' => ['name' => 'phpMyAdmin (' . $relyingPartyId . ')', 'id' => $relyingPartyId],
|
||||
'user' => ['id' => $userId, 'name' => $userName, 'displayName' => $userName],
|
||||
'pubKeyCredParams' => $this->getCredentialParameters(),
|
||||
'authenticatorSelection' => [
|
||||
'authenticatorAttachment' => 'cross-platform',
|
||||
'userVerification' => 'discouraged',
|
||||
],
|
||||
'timeout' => 60000,
|
||||
'attestation' => 'none',
|
||||
];
|
||||
}
|
||||
|
||||
public function getCredentialRequestOptions(
|
||||
string $userName,
|
||||
string $userId,
|
||||
string $relyingPartyId,
|
||||
array $allowedCredentials
|
||||
): array {
|
||||
foreach ($allowedCredentials as $key => $credential) {
|
||||
$allowedCredentials[$key]['id'] = sodium_bin2base64(
|
||||
sodium_base642bin($credential['id'], SODIUM_BASE64_VARIANT_URLSAFE_NO_PADDING),
|
||||
SODIUM_BASE64_VARIANT_ORIGINAL
|
||||
);
|
||||
}
|
||||
|
||||
return [
|
||||
'challenge' => $this->generateChallenge(),
|
||||
'allowCredentials' => $allowedCredentials,
|
||||
'timeout' => 60000,
|
||||
'attestation' => 'none',
|
||||
'userVerification' => 'discouraged',
|
||||
];
|
||||
}
|
||||
|
||||
public function parseAndValidateAssertionResponse(
|
||||
string $assertionResponseJson,
|
||||
array $allowedCredentials,
|
||||
string $challenge,
|
||||
ServerRequestInterface $request
|
||||
): void {
|
||||
$assertionCredential = $this->getAssertionCredential($assertionResponseJson);
|
||||
|
||||
if ($allowedCredentials !== []) {
|
||||
Assert::true($this->isCredentialIdAllowed($assertionCredential['rawId'], $allowedCredentials));
|
||||
}
|
||||
|
||||
$authenticatorData = $this->getAuthenticatorData($assertionCredential['response']['authenticatorData']);
|
||||
|
||||
$clientData = $this->getCollectedClientData($assertionCredential['response']['clientDataJSON']);
|
||||
Assert::same($clientData['type'], 'webauthn.get');
|
||||
|
||||
try {
|
||||
$knownChallenge = sodium_base642bin($challenge, SODIUM_BASE64_VARIANT_ORIGINAL);
|
||||
$cDataChallenge = sodium_base642bin($clientData['challenge'], SODIUM_BASE64_VARIANT_URLSAFE_NO_PADDING);
|
||||
} catch (SodiumException $exception) {
|
||||
throw new WebAuthnException((string) $exception);
|
||||
}
|
||||
|
||||
Assert::true(hash_equals($knownChallenge, $cDataChallenge));
|
||||
|
||||
$host = $request->getUri()->getHost();
|
||||
Assert::same($host, parse_url($clientData['origin'], PHP_URL_HOST));
|
||||
|
||||
$rpIdHash = hash('sha256', $host, true);
|
||||
Assert::true(hash_equals($rpIdHash, $authenticatorData['rpIdHash']));
|
||||
|
||||
$isUserPresent = (ord($authenticatorData['flags']) & 1) !== 0;
|
||||
Assert::true($isUserPresent);
|
||||
}
|
||||
|
||||
public function parseAndValidateAttestationResponse(
|
||||
string $attestationResponse,
|
||||
string $credentialCreationOptions,
|
||||
ServerRequestInterface $request
|
||||
): array {
|
||||
try {
|
||||
$attestationCredential = $this->getAttestationCredential($attestationResponse);
|
||||
} catch (Throwable $exception) {
|
||||
throw new WebAuthnException('Invalid authenticator response.');
|
||||
}
|
||||
|
||||
$creationOptions = json_decode($credentialCreationOptions, true);
|
||||
Assert::isArray($creationOptions);
|
||||
Assert::keyExists($creationOptions, 'challenge');
|
||||
Assert::string($creationOptions['challenge']);
|
||||
Assert::keyExists($creationOptions, 'user');
|
||||
Assert::isArray($creationOptions['user']);
|
||||
Assert::keyExists($creationOptions['user'], 'id');
|
||||
Assert::string($creationOptions['user']['id']);
|
||||
|
||||
$clientData = $this->getCollectedClientData($attestationCredential['response']['clientDataJSON']);
|
||||
|
||||
// Verify that the value of C.type is webauthn.create.
|
||||
Assert::same($clientData['type'], 'webauthn.create');
|
||||
|
||||
// Verify that the value of C.challenge equals the base64url encoding of options.challenge.
|
||||
$optionsChallenge = sodium_base642bin($creationOptions['challenge'], SODIUM_BASE64_VARIANT_ORIGINAL);
|
||||
$clientDataChallenge = sodium_base642bin($clientData['challenge'], SODIUM_BASE64_VARIANT_URLSAFE_NO_PADDING);
|
||||
Assert::true(hash_equals($optionsChallenge, $clientDataChallenge));
|
||||
|
||||
// Verify that the value of C.origin matches the Relying Party's origin.
|
||||
$host = $request->getUri()->getHost();
|
||||
Assert::same($host, parse_url($clientData['origin'], PHP_URL_HOST), 'Invalid origin.');
|
||||
|
||||
// Perform CBOR decoding on the attestationObject field.
|
||||
$attestationObject = $this->getAttestationObject($attestationCredential['response']['attestationObject']);
|
||||
|
||||
$authenticatorData = $this->getAuthenticatorData($attestationObject['authData']);
|
||||
Assert::notNull($authenticatorData['attestedCredentialData']);
|
||||
|
||||
// Verify that the rpIdHash in authData is the SHA-256 hash of the RP ID expected by the Relying Party.
|
||||
$rpIdHash = hash('sha256', $host, true);
|
||||
Assert::true(hash_equals($rpIdHash, $authenticatorData['rpIdHash']), 'Invalid rpIdHash.');
|
||||
|
||||
// Verify that the User Present bit of the flags in authData is set.
|
||||
$isUserPresent = (ord($authenticatorData['flags']) & 1) !== 0;
|
||||
Assert::true($isUserPresent);
|
||||
|
||||
Assert::same($attestationObject['fmt'], 'none');
|
||||
Assert::same($attestationObject['attStmt'], []);
|
||||
|
||||
$encodedCredentialId = sodium_bin2base64(
|
||||
$authenticatorData['attestedCredentialData']['credentialId'],
|
||||
SODIUM_BASE64_VARIANT_URLSAFE_NO_PADDING
|
||||
);
|
||||
$encodedCredentialPublicKey = sodium_bin2base64(
|
||||
$authenticatorData['attestedCredentialData']['credentialPublicKey'],
|
||||
SODIUM_BASE64_VARIANT_URLSAFE_NO_PADDING
|
||||
);
|
||||
$userHandle = sodium_bin2base64(
|
||||
sodium_base642bin($creationOptions['user']['id'], SODIUM_BASE64_VARIANT_ORIGINAL),
|
||||
SODIUM_BASE64_VARIANT_URLSAFE_NO_PADDING
|
||||
);
|
||||
|
||||
return [
|
||||
'publicKeyCredentialId' => $encodedCredentialId,
|
||||
'type' => 'public-key',
|
||||
'transports' => [],
|
||||
'attestationType' => $attestationObject['fmt'],
|
||||
'aaguid' => $authenticatorData['attestedCredentialData']['aaguid'],
|
||||
'credentialPublicKey' => $encodedCredentialPublicKey,
|
||||
'userHandle' => $userHandle,
|
||||
'counter' => $authenticatorData['signCount'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* In order to prevent replay attacks, the challenges MUST contain enough entropy to make guessing them infeasible.
|
||||
* Challenges SHOULD therefore be at least 16 bytes long.
|
||||
*
|
||||
* @see https://www.w3.org/TR/webauthn-3/#sctn-cryptographic-challenges
|
||||
*
|
||||
* @psalm-return non-empty-string
|
||||
*
|
||||
* @throws WebAuthnException
|
||||
*/
|
||||
private function generateChallenge(): string
|
||||
{
|
||||
try {
|
||||
return sodium_bin2base64(random_bytes(32), SODIUM_BASE64_VARIANT_ORIGINAL);
|
||||
} catch (Throwable $throwable) { // @codeCoverageIgnore
|
||||
throw new WebAuthnException('Error when generating challenge.'); // @codeCoverageIgnore
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://www.w3.org/TR/webauthn-3/#sctn-authenticator-data
|
||||
*
|
||||
* @psalm-return array{
|
||||
* rpIdHash: string,
|
||||
* flags: string,
|
||||
* signCount: int,
|
||||
* attestedCredentialData: array{
|
||||
* aaguid: string,
|
||||
* credentialId: string,
|
||||
* credentialPublicKey: string,
|
||||
* credentialPublicKeyDecoded: mixed[]
|
||||
* }|null,
|
||||
* extensions: string|null
|
||||
* }
|
||||
*
|
||||
* @throws WebAuthnException
|
||||
*/
|
||||
private function getAuthenticatorData(string $authData): array
|
||||
{
|
||||
$authDataLength = mb_strlen($authData, '8bit');
|
||||
Assert::true($authDataLength >= 37);
|
||||
$authDataStream = new DataStream($authData);
|
||||
|
||||
$rpIdHash = $authDataStream->take(32);
|
||||
$flags = $authDataStream->take(1);
|
||||
|
||||
// 32-bit unsigned big-endian integer
|
||||
$unpackedSignCount = unpack('N', $authDataStream->take(4));
|
||||
Assert::isArray($unpackedSignCount);
|
||||
Assert::keyExists($unpackedSignCount, 1);
|
||||
Assert::integer($unpackedSignCount[1]);
|
||||
$signCount = $unpackedSignCount[1];
|
||||
|
||||
$attestedCredentialData = null;
|
||||
// Bit 6: Attested credential data included (AT).
|
||||
if ((ord($flags) & 64) !== 0) {
|
||||
/** Authenticator Attestation GUID */
|
||||
$aaguid = $authDataStream->take(16);
|
||||
|
||||
// 16-bit unsigned big-endian integer
|
||||
$unpackedCredentialIdLength = unpack('n', $authDataStream->take(2));
|
||||
Assert::isArray($unpackedCredentialIdLength);
|
||||
Assert::keyExists($unpackedCredentialIdLength, 1);
|
||||
Assert::integer($unpackedCredentialIdLength[1]);
|
||||
$credentialIdLength = $unpackedCredentialIdLength[1];
|
||||
|
||||
$credentialId = $authDataStream->take($credentialIdLength);
|
||||
|
||||
$credentialPublicKeyDecoded = (new CBORDecoder())->decode($authDataStream);
|
||||
Assert::isArray($credentialPublicKeyDecoded);
|
||||
$credentialPublicKey = mb_substr(
|
||||
$authData,
|
||||
37 + 18 + $credentialIdLength,
|
||||
$authDataStream->getPosition(),
|
||||
'8bit'
|
||||
);
|
||||
|
||||
$attestedCredentialData = [
|
||||
'aaguid' => $aaguid,
|
||||
'credentialId' => $credentialId,
|
||||
'credentialPublicKey' => $credentialPublicKey,
|
||||
'credentialPublicKeyDecoded' => $credentialPublicKeyDecoded,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'rpIdHash' => $rpIdHash,
|
||||
'flags' => $flags,
|
||||
'signCount' => $signCount,
|
||||
'attestedCredentialData' => $attestedCredentialData,
|
||||
'extensions' => null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-param non-empty-string $id
|
||||
* @psalm-param list<array{id: non-empty-string, type: non-empty-string}> $allowedCredentials
|
||||
*
|
||||
* @throws WebAuthnException
|
||||
*/
|
||||
private function isCredentialIdAllowed(string $id, array $allowedCredentials): bool
|
||||
{
|
||||
foreach ($allowedCredentials as $credential) {
|
||||
try {
|
||||
$credentialId = sodium_base642bin($credential['id'], SODIUM_BASE64_VARIANT_URLSAFE_NO_PADDING);
|
||||
} catch (SodiumException $exception) {
|
||||
throw new WebAuthnException();
|
||||
}
|
||||
|
||||
if (hash_equals($credentialId, $id)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://www.iana.org/assignments/cose/cose.xhtml#algorithms
|
||||
*
|
||||
* @psalm-return list<array{alg: int, type: 'public-key'}>
|
||||
*/
|
||||
private function getCredentialParameters(): array
|
||||
{
|
||||
return [
|
||||
['alg' => -257, 'type' => 'public-key'], // RS256
|
||||
['alg' => -259, 'type' => 'public-key'], // RS512
|
||||
['alg' => -37, 'type' => 'public-key'], // PS256
|
||||
['alg' => -39, 'type' => 'public-key'], // PS512
|
||||
['alg' => -7, 'type' => 'public-key'], // ES256
|
||||
['alg' => -36, 'type' => 'public-key'], // ES512
|
||||
['alg' => -8, 'type' => 'public-key'], // EdDSA
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-param non-empty-string $assertionResponseJson
|
||||
*
|
||||
* @psalm-return array{
|
||||
* id: non-empty-string,
|
||||
* type: 'public-key',
|
||||
* rawId: non-empty-string,
|
||||
* response: array{
|
||||
* clientDataJSON: non-empty-string,
|
||||
* authenticatorData: non-empty-string,
|
||||
* signature: non-empty-string,
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* @throws SodiumException
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
private function getAssertionCredential(string $assertionResponseJson): array
|
||||
{
|
||||
$credential = json_decode($assertionResponseJson, true);
|
||||
Assert::isArray($credential);
|
||||
Assert::keyExists($credential, 'id');
|
||||
Assert::stringNotEmpty($credential['id']);
|
||||
Assert::keyExists($credential, 'type');
|
||||
Assert::same($credential['type'], 'public-key');
|
||||
Assert::keyExists($credential, 'rawId');
|
||||
Assert::stringNotEmpty($credential['rawId']);
|
||||
Assert::keyExists($credential, 'response');
|
||||
Assert::isArray($credential['response']);
|
||||
Assert::keyExists($credential['response'], 'clientDataJSON');
|
||||
Assert::stringNotEmpty($credential['response']['clientDataJSON']);
|
||||
Assert::keyExists($credential['response'], 'authenticatorData');
|
||||
Assert::stringNotEmpty($credential['response']['authenticatorData']);
|
||||
Assert::keyExists($credential['response'], 'signature');
|
||||
Assert::stringNotEmpty($credential['response']['signature']);
|
||||
|
||||
$id = sodium_base642bin($credential['id'], SODIUM_BASE64_VARIANT_URLSAFE_NO_PADDING);
|
||||
$rawId = sodium_base642bin($credential['rawId'], SODIUM_BASE64_VARIANT_ORIGINAL);
|
||||
Assert::stringNotEmpty($id);
|
||||
Assert::stringNotEmpty($rawId);
|
||||
Assert::true(hash_equals($rawId, $id));
|
||||
|
||||
$clientDataJSON = sodium_base642bin($credential['response']['clientDataJSON'], SODIUM_BASE64_VARIANT_ORIGINAL);
|
||||
Assert::stringNotEmpty($clientDataJSON);
|
||||
$authenticatorData = sodium_base642bin(
|
||||
$credential['response']['authenticatorData'],
|
||||
SODIUM_BASE64_VARIANT_ORIGINAL
|
||||
);
|
||||
Assert::stringNotEmpty($authenticatorData);
|
||||
$signature = sodium_base642bin($credential['response']['signature'], SODIUM_BASE64_VARIANT_ORIGINAL);
|
||||
Assert::stringNotEmpty($signature);
|
||||
|
||||
return [
|
||||
'id' => $credential['id'],
|
||||
'type' => 'public-key',
|
||||
'rawId' => $rawId,
|
||||
'response' => [
|
||||
'clientDataJSON' => $clientDataJSON,
|
||||
'authenticatorData' => $authenticatorData,
|
||||
'signature' => $signature,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://www.w3.org/TR/webauthn-3/#iface-authenticatorattestationresponse
|
||||
*
|
||||
* @psalm-param non-empty-string $attestationResponse
|
||||
*
|
||||
* @psalm-return array{
|
||||
* id: non-empty-string,
|
||||
* rawId: non-empty-string,
|
||||
* type: 'public-key',
|
||||
* response: array{clientDataJSON: non-empty-string, attestationObject: non-empty-string}
|
||||
* }
|
||||
*
|
||||
* @throws SodiumException
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
private function getAttestationCredential(string $attestationResponse): array
|
||||
{
|
||||
$credential = json_decode($attestationResponse, true);
|
||||
Assert::isArray($credential);
|
||||
Assert::keyExists($credential, 'id');
|
||||
Assert::stringNotEmpty($credential['id']);
|
||||
Assert::keyExists($credential, 'rawId');
|
||||
Assert::stringNotEmpty($credential['rawId']);
|
||||
Assert::keyExists($credential, 'type');
|
||||
Assert::string($credential['type']);
|
||||
Assert::same($credential['type'], 'public-key');
|
||||
Assert::keyExists($credential, 'response');
|
||||
Assert::isArray($credential['response']);
|
||||
Assert::keyExists($credential['response'], 'clientDataJSON');
|
||||
Assert::stringNotEmpty($credential['response']['clientDataJSON']);
|
||||
Assert::keyExists($credential['response'], 'attestationObject');
|
||||
Assert::stringNotEmpty($credential['response']['attestationObject']);
|
||||
|
||||
$id = sodium_base642bin($credential['id'], SODIUM_BASE64_VARIANT_URLSAFE_NO_PADDING);
|
||||
$rawId = sodium_base642bin($credential['rawId'], SODIUM_BASE64_VARIANT_ORIGINAL);
|
||||
Assert::stringNotEmpty($id);
|
||||
Assert::stringNotEmpty($rawId);
|
||||
Assert::true(hash_equals($rawId, $id));
|
||||
|
||||
$clientDataJSON = sodium_base642bin($credential['response']['clientDataJSON'], SODIUM_BASE64_VARIANT_ORIGINAL);
|
||||
Assert::stringNotEmpty($clientDataJSON);
|
||||
$attestationObject = sodium_base642bin(
|
||||
$credential['response']['attestationObject'],
|
||||
SODIUM_BASE64_VARIANT_ORIGINAL
|
||||
);
|
||||
Assert::stringNotEmpty($attestationObject);
|
||||
|
||||
return [
|
||||
'id' => $credential['id'],
|
||||
'rawId' => $rawId,
|
||||
'type' => 'public-key',
|
||||
'response' => [
|
||||
'clientDataJSON' => $clientDataJSON,
|
||||
'attestationObject' => $attestationObject,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://www.w3.org/TR/webauthn-3/#dictionary-client-data
|
||||
*
|
||||
* @psalm-param non-empty-string $clientDataJSON
|
||||
*
|
||||
* @return array{
|
||||
* type: 'webauthn.create'|'webauthn.get',
|
||||
* challenge: non-empty-string,
|
||||
* origin: non-empty-string
|
||||
* }
|
||||
*/
|
||||
private function getCollectedClientData(string $clientDataJSON): array
|
||||
{
|
||||
$clientData = json_decode($clientDataJSON, true);
|
||||
|
||||
Assert::isArray($clientData);
|
||||
Assert::keyExists($clientData, 'type');
|
||||
Assert::stringNotEmpty($clientData['type']);
|
||||
Assert::inArray($clientData['type'], ['webauthn.create', 'webauthn.get']);
|
||||
Assert::keyExists($clientData, 'challenge');
|
||||
Assert::stringNotEmpty($clientData['challenge']);
|
||||
Assert::keyExists($clientData, 'origin');
|
||||
Assert::stringNotEmpty($clientData['origin']);
|
||||
|
||||
return [
|
||||
'type' => $clientData['type'],
|
||||
'challenge' => $clientData['challenge'],
|
||||
'origin' => $clientData['origin'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-param non-empty-string $attestationObjectEncoded
|
||||
*
|
||||
* @psalm-return array{fmt: string, attStmt: mixed[], authData: string}
|
||||
*
|
||||
* @throws WebAuthnException
|
||||
*/
|
||||
private function getAttestationObject(string $attestationObjectEncoded): array
|
||||
{
|
||||
$decoded = (new CBORDecoder())->decode(new DataStream($attestationObjectEncoded));
|
||||
|
||||
Assert::isArray($decoded);
|
||||
Assert::keyExists($decoded, 'fmt');
|
||||
Assert::string($decoded['fmt']);
|
||||
Assert::keyExists($decoded, 'attStmt');
|
||||
Assert::isArray($decoded['attStmt']);
|
||||
Assert::keyExists($decoded, 'authData');
|
||||
Assert::string($decoded['authData']);
|
||||
|
||||
return $decoded;
|
||||
}
|
||||
}
|
68
admin/phpMyAdmin/libraries/classes/WebAuthn/DataStream.php
Normal file
68
admin/phpMyAdmin/libraries/classes/WebAuthn/DataStream.php
Normal file
|
@ -0,0 +1,68 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace PhpMyAdmin\WebAuthn;
|
||||
|
||||
use function fopen;
|
||||
use function fread;
|
||||
use function ftell;
|
||||
use function fwrite;
|
||||
use function rewind;
|
||||
|
||||
final class DataStream
|
||||
{
|
||||
/** @var resource */
|
||||
private $stream;
|
||||
|
||||
/**
|
||||
* @throws WebAuthnException
|
||||
*/
|
||||
public function __construct(string $binaryString)
|
||||
{
|
||||
$resource = fopen('php://memory', 'rb+');
|
||||
if ($resource === false || fwrite($resource, $binaryString) === false) {
|
||||
throw new WebAuthnException();
|
||||
}
|
||||
|
||||
if (! rewind($resource)) {
|
||||
throw new WebAuthnException();
|
||||
}
|
||||
|
||||
$this->stream = $resource;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws WebAuthnException
|
||||
*/
|
||||
public function take(int $length): string
|
||||
{
|
||||
if ($length < 0) {
|
||||
throw new WebAuthnException();
|
||||
}
|
||||
|
||||
if ($length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$string = fread($this->stream, $length);
|
||||
if ($string === false) {
|
||||
throw new WebAuthnException();
|
||||
}
|
||||
|
||||
return $string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws WebAuthnException
|
||||
*/
|
||||
public function getPosition(): int
|
||||
{
|
||||
$position = ftell($this->stream);
|
||||
if ($position === false) {
|
||||
throw new WebAuthnException();
|
||||
}
|
||||
|
||||
return $position;
|
||||
}
|
||||
}
|
78
admin/phpMyAdmin/libraries/classes/WebAuthn/Server.php
Normal file
78
admin/phpMyAdmin/libraries/classes/WebAuthn/Server.php
Normal file
|
@ -0,0 +1,78 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace PhpMyAdmin\WebAuthn;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
/**
|
||||
* Web Authentication API server.
|
||||
*
|
||||
* @see https://www.w3.org/TR/webauthn-3/
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API
|
||||
* @see https://webauthn.guide/
|
||||
*/
|
||||
interface Server
|
||||
{
|
||||
/**
|
||||
* @psalm-return array{
|
||||
* challenge: non-empty-string,
|
||||
* rp: array{name: string, id: string},
|
||||
* user: array{id: string, name: string, displayName: string},
|
||||
* pubKeyCredParams: list<array{alg: int, type: 'public-key'}>,
|
||||
* authenticatorSelection: array<string, string>,
|
||||
* timeout: positive-int,
|
||||
* attestation: non-empty-string
|
||||
* }
|
||||
*
|
||||
* @throws WebAuthnException
|
||||
*/
|
||||
public function getCredentialCreationOptions(string $userName, string $userId, string $relyingPartyId): array;
|
||||
|
||||
/**
|
||||
* @psalm-param list<array{id: non-empty-string, type: non-empty-string}> $allowedCredentials
|
||||
*
|
||||
* @return array<string, array<int, array<string, string>>|int|string>
|
||||
*
|
||||
* @throws WebAuthnException
|
||||
*/
|
||||
public function getCredentialRequestOptions(
|
||||
string $userName,
|
||||
string $userId,
|
||||
string $relyingPartyId,
|
||||
array $allowedCredentials
|
||||
): array;
|
||||
|
||||
/**
|
||||
* @see https://www.w3.org/TR/webauthn-3/#sctn-verifying-assertion
|
||||
*
|
||||
* @psalm-param non-empty-string $assertionResponseJson
|
||||
* @psalm-param list<array{id: non-empty-string, type: non-empty-string}> $allowedCredentials
|
||||
* @psalm-param non-empty-string $challenge
|
||||
*
|
||||
* @throws WebAuthnException
|
||||
*/
|
||||
public function parseAndValidateAssertionResponse(
|
||||
string $assertionResponseJson,
|
||||
array $allowedCredentials,
|
||||
string $challenge,
|
||||
ServerRequestInterface $request
|
||||
): void;
|
||||
|
||||
/**
|
||||
* @see https://www.w3.org/TR/webauthn-3/#sctn-registering-a-new-credential
|
||||
*
|
||||
* @psalm-param non-empty-string $attestationResponse
|
||||
* @psalm-param non-empty-string $credentialCreationOptions
|
||||
*
|
||||
* @return mixed[]
|
||||
*
|
||||
* @throws WebAuthnException
|
||||
*/
|
||||
public function parseAndValidateAttestationResponse(
|
||||
string $attestationResponse,
|
||||
string $credentialCreationOptions,
|
||||
ServerRequestInterface $request
|
||||
): array;
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace PhpMyAdmin\WebAuthn;
|
||||
|
||||
use Exception;
|
||||
|
||||
class WebAuthnException extends Exception
|
||||
{
|
||||
}
|
|
@ -0,0 +1,277 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace PhpMyAdmin\WebAuthn;
|
||||
|
||||
use PhpMyAdmin\TwoFactor;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Webauthn\AuthenticatorSelectionCriteria;
|
||||
use Webauthn\PublicKeyCredentialCreationOptions;
|
||||
use Webauthn\PublicKeyCredentialDescriptor;
|
||||
use Webauthn\PublicKeyCredentialRequestOptions;
|
||||
use Webauthn\PublicKeyCredentialRpEntity;
|
||||
use Webauthn\PublicKeyCredentialSource;
|
||||
use Webauthn\PublicKeyCredentialSourceRepository;
|
||||
use Webauthn\PublicKeyCredentialUserEntity;
|
||||
use Webauthn\Server as WebauthnServer;
|
||||
use Webauthn\TrustPath\EmptyTrustPath;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
use function array_map;
|
||||
use function base64_encode;
|
||||
use function json_decode;
|
||||
use function sodium_base642bin;
|
||||
use function sodium_bin2base64;
|
||||
|
||||
use const SODIUM_BASE64_VARIANT_ORIGINAL;
|
||||
use const SODIUM_BASE64_VARIANT_URLSAFE_NO_PADDING;
|
||||
|
||||
final class WebauthnLibServer implements Server
|
||||
{
|
||||
/** @var TwoFactor */
|
||||
private $twofactor;
|
||||
|
||||
public function __construct(TwoFactor $twofactor)
|
||||
{
|
||||
$this->twofactor = $twofactor;
|
||||
}
|
||||
|
||||
public function getCredentialCreationOptions(string $userName, string $userId, string $relyingPartyId): array
|
||||
{
|
||||
$userEntity = new PublicKeyCredentialUserEntity($userName, $userId, $userName);
|
||||
$relyingPartyEntity = new PublicKeyCredentialRpEntity('phpMyAdmin (' . $relyingPartyId . ')', $relyingPartyId);
|
||||
$publicKeyCredentialSourceRepository = $this->createPublicKeyCredentialSourceRepository();
|
||||
$server = new WebauthnServer($relyingPartyEntity, $publicKeyCredentialSourceRepository);
|
||||
$publicKeyCredentialCreationOptions = $server->generatePublicKeyCredentialCreationOptions(
|
||||
$userEntity,
|
||||
PublicKeyCredentialCreationOptions::ATTESTATION_CONVEYANCE_PREFERENCE_NONE,
|
||||
[],
|
||||
AuthenticatorSelectionCriteria::createFromArray([
|
||||
'authenticatorAttachment' => 'cross-platform',
|
||||
'userVerification' => 'discouraged',
|
||||
])
|
||||
);
|
||||
/** @psalm-var array{
|
||||
* challenge: non-empty-string,
|
||||
* rp: array{name: non-empty-string, id: non-empty-string},
|
||||
* user: array{id: non-empty-string, name: non-empty-string, displayName: non-empty-string},
|
||||
* pubKeyCredParams: list<array{alg: int, type: 'public-key'}>,
|
||||
* authenticatorSelection: array<string, string>,
|
||||
* timeout: positive-int,
|
||||
* attestation: non-empty-string
|
||||
* } $creationOptions */
|
||||
$creationOptions = $publicKeyCredentialCreationOptions->jsonSerialize();
|
||||
$creationOptions['challenge'] = sodium_bin2base64(
|
||||
sodium_base642bin($creationOptions['challenge'], SODIUM_BASE64_VARIANT_URLSAFE_NO_PADDING),
|
||||
SODIUM_BASE64_VARIANT_ORIGINAL
|
||||
);
|
||||
Assert::stringNotEmpty($creationOptions['challenge']);
|
||||
|
||||
return $creationOptions;
|
||||
}
|
||||
|
||||
public function getCredentialRequestOptions(
|
||||
string $userName,
|
||||
string $userId,
|
||||
string $relyingPartyId,
|
||||
array $allowedCredentials
|
||||
): array {
|
||||
$userEntity = new PublicKeyCredentialUserEntity($userName, $userId, $userName);
|
||||
$relyingPartyEntity = new PublicKeyCredentialRpEntity('phpMyAdmin (' . $relyingPartyId . ')', $relyingPartyId);
|
||||
$publicKeyCredentialSourceRepository = $this->createPublicKeyCredentialSourceRepository();
|
||||
$server = new WebauthnServer($relyingPartyEntity, $publicKeyCredentialSourceRepository);
|
||||
$credentialSources = $publicKeyCredentialSourceRepository->findAllForUserEntity($userEntity);
|
||||
$allowedCredentials = array_map(
|
||||
static function (PublicKeyCredentialSource $credential): PublicKeyCredentialDescriptor {
|
||||
return $credential->getPublicKeyCredentialDescriptor();
|
||||
},
|
||||
$credentialSources
|
||||
);
|
||||
$publicKeyCredentialRequestOptions = $server->generatePublicKeyCredentialRequestOptions(
|
||||
'discouraged',
|
||||
$allowedCredentials
|
||||
);
|
||||
/**
|
||||
* @psalm-var array{
|
||||
* challenge: string,
|
||||
* allowCredentials?: list<array{id: non-empty-string, type: non-empty-string}>
|
||||
* } $requestOptions
|
||||
*/
|
||||
$requestOptions = $publicKeyCredentialRequestOptions->jsonSerialize();
|
||||
$requestOptions['challenge'] = sodium_bin2base64(
|
||||
sodium_base642bin($requestOptions['challenge'], SODIUM_BASE64_VARIANT_URLSAFE_NO_PADDING),
|
||||
SODIUM_BASE64_VARIANT_ORIGINAL
|
||||
);
|
||||
if (isset($requestOptions['allowCredentials'])) {
|
||||
foreach ($requestOptions['allowCredentials'] as $key => $credential) {
|
||||
$requestOptions['allowCredentials'][$key]['id'] = sodium_bin2base64(
|
||||
sodium_base642bin($credential['id'], SODIUM_BASE64_VARIANT_URLSAFE_NO_PADDING),
|
||||
SODIUM_BASE64_VARIANT_ORIGINAL
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $requestOptions;
|
||||
}
|
||||
|
||||
public function parseAndValidateAssertionResponse(
|
||||
string $assertionResponseJson,
|
||||
array $allowedCredentials,
|
||||
string $challenge,
|
||||
ServerRequestInterface $request
|
||||
): void {
|
||||
Assert::string($this->twofactor->config['settings']['userHandle']);
|
||||
$userHandle = sodium_base642bin(
|
||||
$this->twofactor->config['settings']['userHandle'],
|
||||
SODIUM_BASE64_VARIANT_URLSAFE_NO_PADDING
|
||||
);
|
||||
$userEntity = new PublicKeyCredentialUserEntity(
|
||||
$this->twofactor->user,
|
||||
$userHandle,
|
||||
$this->twofactor->user
|
||||
);
|
||||
$host = $request->getUri()->getHost();
|
||||
$relyingPartyEntity = new PublicKeyCredentialRpEntity('phpMyAdmin (' . $host . ')', $host);
|
||||
$publicKeyCredentialSourceRepository = $this->createPublicKeyCredentialSourceRepository();
|
||||
$server = new WebauthnServer($relyingPartyEntity, $publicKeyCredentialSourceRepository);
|
||||
$requestOptions = PublicKeyCredentialRequestOptions::createFromArray([
|
||||
'challenge' => $challenge,
|
||||
'allowCredentials' => $allowedCredentials,
|
||||
'rpId' => $host,
|
||||
'timeout' => 60000,
|
||||
]);
|
||||
Assert::isInstanceOf($requestOptions, PublicKeyCredentialRequestOptions::class);
|
||||
$server->loadAndCheckAssertionResponse(
|
||||
$assertionResponseJson,
|
||||
$requestOptions,
|
||||
$userEntity,
|
||||
$request
|
||||
);
|
||||
}
|
||||
|
||||
public function parseAndValidateAttestationResponse(
|
||||
string $attestationResponse,
|
||||
string $credentialCreationOptions,
|
||||
ServerRequestInterface $request
|
||||
): array {
|
||||
$creationOptions = json_decode($credentialCreationOptions, true);
|
||||
Assert::isArray($creationOptions);
|
||||
Assert::keyExists($creationOptions, 'challenge');
|
||||
Assert::keyExists($creationOptions, 'user');
|
||||
Assert::isArray($creationOptions['user']);
|
||||
Assert::keyExists($creationOptions['user'], 'id');
|
||||
$host = $request->getUri()->getHost();
|
||||
$relyingPartyEntity = new PublicKeyCredentialRpEntity('phpMyAdmin (' . $host . ')', $host);
|
||||
$publicKeyCredentialSourceRepository = $this->createPublicKeyCredentialSourceRepository();
|
||||
$server = new WebauthnServer($relyingPartyEntity, $publicKeyCredentialSourceRepository);
|
||||
$creationOptionsArray = [
|
||||
'rp' => ['name' => 'phpMyAdmin (' . $host . ')', 'id' => $host],
|
||||
'pubKeyCredParams' => [
|
||||
['alg' => -257, 'type' => 'public-key'], // RS256
|
||||
['alg' => -259, 'type' => 'public-key'], // RS512
|
||||
['alg' => -37, 'type' => 'public-key'], // PS256
|
||||
['alg' => -39, 'type' => 'public-key'], // PS512
|
||||
['alg' => -7, 'type' => 'public-key'], // ES256
|
||||
['alg' => -36, 'type' => 'public-key'], // ES512
|
||||
['alg' => -8, 'type' => 'public-key'], // EdDSA
|
||||
],
|
||||
'challenge' => $creationOptions['challenge'],
|
||||
'attestation' => 'none',
|
||||
'user' => [
|
||||
'name' => $this->twofactor->user,
|
||||
'id' => $creationOptions['user']['id'],
|
||||
'displayName' => $this->twofactor->user,
|
||||
],
|
||||
'authenticatorSelection' => [
|
||||
'authenticatorAttachment' => 'cross-platform',
|
||||
'userVerification' => 'discouraged',
|
||||
],
|
||||
'timeout' => 60000,
|
||||
];
|
||||
$credentialCreationOptions = PublicKeyCredentialCreationOptions::createFromArray($creationOptionsArray);
|
||||
Assert::isInstanceOf($credentialCreationOptions, PublicKeyCredentialCreationOptions::class);
|
||||
$publicKeyCredentialSource = $server->loadAndCheckAttestationResponse(
|
||||
$attestationResponse,
|
||||
$credentialCreationOptions,
|
||||
$request
|
||||
);
|
||||
|
||||
return $publicKeyCredentialSource->jsonSerialize();
|
||||
}
|
||||
|
||||
private function createPublicKeyCredentialSourceRepository(): PublicKeyCredentialSourceRepository
|
||||
{
|
||||
return new class ($this->twofactor) implements PublicKeyCredentialSourceRepository {
|
||||
/** @var TwoFactor */
|
||||
private $twoFactor;
|
||||
|
||||
public function __construct(TwoFactor $twoFactor)
|
||||
{
|
||||
$this->twoFactor = $twoFactor;
|
||||
}
|
||||
|
||||
public function findOneByCredentialId(string $publicKeyCredentialId): ?PublicKeyCredentialSource
|
||||
{
|
||||
$data = $this->read();
|
||||
if (isset($data[base64_encode($publicKeyCredentialId)])) {
|
||||
return PublicKeyCredentialSource::createFromArray($data[base64_encode($publicKeyCredentialId)]);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return PublicKeyCredentialSource[]
|
||||
*/
|
||||
public function findAllForUserEntity(PublicKeyCredentialUserEntity $publicKeyCredentialUserEntity): array
|
||||
{
|
||||
$sources = [];
|
||||
foreach ($this->read() as $data) {
|
||||
$source = PublicKeyCredentialSource::createFromArray($data);
|
||||
if ($source->getUserHandle() !== $publicKeyCredentialUserEntity->getId()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$sources[] = $source;
|
||||
}
|
||||
|
||||
return $sources;
|
||||
}
|
||||
|
||||
public function saveCredentialSource(PublicKeyCredentialSource $publicKeyCredentialSource): void
|
||||
{
|
||||
$data = $this->read();
|
||||
$id = $publicKeyCredentialSource->getPublicKeyCredentialId();
|
||||
$data[base64_encode($id)] = $publicKeyCredentialSource->jsonSerialize();
|
||||
$this->write($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[][]
|
||||
*/
|
||||
private function read(): array
|
||||
{
|
||||
/** @psalm-var list<mixed[]> $credentials */
|
||||
$credentials = $this->twoFactor->config['settings']['credentials'];
|
||||
foreach ($credentials as &$credential) {
|
||||
if (isset($credential['trustPath'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$credential['trustPath'] = ['type' => EmptyTrustPath::class];
|
||||
}
|
||||
|
||||
return $credentials;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[] $data
|
||||
*/
|
||||
private function write(array $data): void
|
||||
{
|
||||
$this->twoFactor->config['settings']['credentials'] = $data;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue