Current File : /home/pacjaorg/wpt.pacja.org/km/plugins/multifactorauth/webauthn/src/CredentialRepository.php |
<?php
/**
* @package Joomla.Plugin
* @subpackage Multifactorauth.webauthn
*
* @copyright (C) 2022 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Plugin\Multifactorauth\Webauthn;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
use Joomla\CMS\User\UserFactoryInterface;
use Joomla\Component\Users\Administrator\Helper\Mfa as MfaHelper;
use Joomla\Component\Users\Administrator\Table\MfaTable;
use Webauthn\AttestationStatement\AttestationStatement;
use Webauthn\AttestedCredentialData;
use Webauthn\PublicKeyCredentialDescriptor;
use Webauthn\PublicKeyCredentialSource;
use Webauthn\PublicKeyCredentialSourceRepository;
use Webauthn\PublicKeyCredentialUserEntity;
use Webauthn\TrustPath\EmptyTrustPath;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Implementation of the credentials repository for the WebAuthn library.
*
* Important assumption: interaction with Webauthn through the library is only performed for the currently logged in
* user. Therefore all Methods which take a credential ID work by checking the Joomla MFA records of the current
* user only. This is a necessity. The records are stored encrypted, therefore we cannot do a partial search in the
* table. We have to load the records, decrypt them and inspect them. We cannot do that for thousands of records but
* we CAN do that for the few records each user has under their account.
*
* This behavior can be changed by passing a user ID in the constructor of the class.
*
* @since 4.2.0
*/
class CredentialRepository implements PublicKeyCredentialSourceRepository
{
/**
* The user ID we will operate with
*
* @var integer
* @since 4.2.0
*/
private $userId = 0;
/**
* CredentialRepository constructor.
*
* @param int $userId The user ID this repository will be working with.
*
* @throws \Exception
* @since 4.2.0
*/
public function __construct(int $userId = 0)
{
if (empty($userId)) {
$user = Factory::getApplication()->getIdentity()
?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0);
$userId = $user->id;
}
$this->userId = $userId;
}
/**
* Finds a WebAuthn record given a credential ID
*
* @param string $publicKeyCredentialId The public credential ID to look for
*
* @return PublicKeyCredentialSource|null
* @since 4.2.0
*/
public function findOneByCredentialId(string $publicKeyCredentialId): ?PublicKeyCredentialSource
{
$publicKeyCredentialUserEntity = new PublicKeyCredentialUserEntity('', $this->userId, '', '');
$credentials = $this->findAllForUserEntity($publicKeyCredentialUserEntity);
foreach ($credentials as $record) {
if ($record->getAttestedCredentialData()->getCredentialId() != $publicKeyCredentialId) {
continue;
}
return $record;
}
return null;
}
/**
* Find all WebAuthn entries given a user entity
*
* @param PublicKeyCredentialUserEntity $publicKeyCredentialUserEntity The user entity to search by
*
* @return array|PublicKeyCredentialSource[]
* @throws \Exception
* @since 4.2.0
*/
public function findAllForUserEntity(PublicKeyCredentialUserEntity $publicKeyCredentialUserEntity): array
{
if (empty($publicKeyCredentialUserEntity)) {
$userId = $this->userId;
} else {
$userId = $publicKeyCredentialUserEntity->getId();
}
$return = [];
$results = MfaHelper::getUserMfaRecords($userId);
if (count($results) < 1) {
return $return;
}
/** @var MfaTable $result */
foreach ($results as $result) {
$options = $result->options;
if (!is_array($options) || empty($options)) {
continue;
}
if (!isset($options['attested']) && !isset($options['pubkeysource'])) {
continue;
}
if (isset($options['attested']) && is_string($options['attested'])) {
$options['attested'] = json_decode($options['attested'], true);
$return[$result->id] = $this->attestedCredentialToPublicKeyCredentialSource(
AttestedCredentialData::createFromArray($options['attested']),
$userId
);
} elseif (isset($options['pubkeysource']) && is_string($options['pubkeysource'])) {
$options['pubkeysource'] = json_decode($options['pubkeysource'], true);
$return[$result->id] = PublicKeyCredentialSource::createFromArray($options['pubkeysource']);
} elseif (isset($options['pubkeysource']) && is_array($options['pubkeysource'])) {
$return[$result->id] = PublicKeyCredentialSource::createFromArray($options['pubkeysource']);
}
}
return $return;
}
/**
* Converts a legacy AttestedCredentialData object stored in the database into a PublicKeyCredentialSource object.
*
* This makes several assumptions which can be problematic and the reason why the WebAuthn library version 2 moved
* away from attested credentials to public key credential sources:
*
* - The credential is always of the public key type (that's safe as the only option supported)
* - You can access it with any kind of authenticator transport: USB, NFC, Internal or Bluetooth LE (possibly
* dangerous)
* - There is no attestations (generally safe since browsers don't seem to support attestation yet)
* - There is no trust path (generally safe since browsers don't seem to provide one)
* - No counter was stored (dangerous since it can lead to replay attacks).
*
* @param AttestedCredentialData $record Legacy attested credential data object
* @param int $userId User ID we are getting the credential source for
*
* @return PublicKeyCredentialSource
* @since 4.2.0
*/
private function attestedCredentialToPublicKeyCredentialSource(AttestedCredentialData $record, int $userId): PublicKeyCredentialSource
{
return new PublicKeyCredentialSource(
$record->getCredentialId(),
PublicKeyCredentialDescriptor::CREDENTIAL_TYPE_PUBLIC_KEY,
[
PublicKeyCredentialDescriptor::AUTHENTICATOR_TRANSPORT_USB,
PublicKeyCredentialDescriptor::AUTHENTICATOR_TRANSPORT_NFC,
PublicKeyCredentialDescriptor::AUTHENTICATOR_TRANSPORT_INTERNAL,
PublicKeyCredentialDescriptor::AUTHENTICATOR_TRANSPORT_BLE,
],
AttestationStatement::TYPE_NONE,
new EmptyTrustPath(),
$record->getAaguid(),
$record->getCredentialPublicKey(),
$userId,
0
);
}
/**
* Save a WebAuthn record
*
* @param PublicKeyCredentialSource $publicKeyCredentialSource The record to save
*
* @return void
* @throws \Exception
* @since 4.2.0
*/
public function saveCredentialSource(PublicKeyCredentialSource $publicKeyCredentialSource): void
{
// I can only create or update credentials for the user this class was created for
if ($publicKeyCredentialSource->getUserHandle() != $this->userId) {
throw new \RuntimeException('Cannot create or update WebAuthn credentials for a different user.', 403);
}
// Do I have an existing record for this credential?
$recordId = null;
$publicKeyCredentialUserEntity = new PublicKeyCredentialUserEntity('', $this->userId, '', '');
$credentials = $this->findAllForUserEntity($publicKeyCredentialUserEntity);
foreach ($credentials as $id => $record) {
if ($record->getAttestedCredentialData()->getCredentialId() != $publicKeyCredentialSource->getAttestedCredentialData()->getCredentialId()) {
continue;
}
$recordId = $id;
break;
}
// Create or update a record
/** @var MVCFactoryInterface $factory */
$factory = Factory::getApplication()->bootComponent('com_users')->getMVCFactory();
/** @var MfaTable $mfaTable */
$mfaTable = $factory->createTable('Mfa', 'Administrator');
if ($recordId) {
$mfaTable->load($recordId);
$options = $mfaTable->options;
if (isset($options['attested'])) {
unset($options['attested']);
}
$options['pubkeysource'] = $publicKeyCredentialSource;
$mfaTable->save(
[
'options' => $options,
]
);
} else {
$mfaTable->reset();
$mfaTable->save(
[
'user_id' => $this->userId,
'title' => 'WebAuthn auto-save',
'method' => 'webauthn',
'default' => 0,
'options' => ['pubkeysource' => $publicKeyCredentialSource],
]
);
}
}
}