diff --git a/core/Migrations/Version29000Date20240324113502.php b/core/Migrations/Version29000Date20240324113502.php new file mode 100644 index 0000000000000..ff9c1610b6693 --- /dev/null +++ b/core/Migrations/Version29000Date20240324113502.php @@ -0,0 +1,44 @@ + + * + * @author 2024 S1m + * @author 2024 Richard Steinmetz + * + * @license AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OC\Core\Migrations; + +use Closure; +use OCP\DB\ISchemaWrapper; +use OCP\DB\Types; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; + +class Version29000Date20240324113502 extends SimpleMigrationStep { + public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper { + /** @var ISchemaWrapper $schema */ + $schema = $schemaClosure(); + + $table = $schema->getTable('webauthn'); + $table->addColumn('user_verification', Types::BOOLEAN, ['notnull' => false, 'default' => false]); + return $schema; + } +} diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index 7baad0e51696d..39ebb8b540dad 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -1276,6 +1276,7 @@ 'OC\\Core\\Migrations\\Version29000Date20240124132201' => $baseDir . '/core/Migrations/Version29000Date20240124132201.php', 'OC\\Core\\Migrations\\Version29000Date20240124132202' => $baseDir . '/core/Migrations/Version29000Date20240124132202.php', 'OC\\Core\\Migrations\\Version29000Date20240131122720' => $baseDir . '/core/Migrations/Version29000Date20240131122720.php', + 'OC\\Core\\Migrations\\Version29000Date20240324113502' => $baseDir . '/core/Migrations/Version29000Date20240324113502.php', 'OC\\Core\\Notification\\CoreNotifier' => $baseDir . '/core/Notification/CoreNotifier.php', 'OC\\Core\\Service\\LoginFlowV2Service' => $baseDir . '/core/Service/LoginFlowV2Service.php', 'OC\\DB\\Adapter' => $baseDir . '/lib/private/DB/Adapter.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index d96a1b2d1b52f..527ae39549865 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -1309,6 +1309,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OC\\Core\\Migrations\\Version29000Date20240124132201' => __DIR__ . '/../../..' . '/core/Migrations/Version29000Date20240124132201.php', 'OC\\Core\\Migrations\\Version29000Date20240124132202' => __DIR__ . '/../../..' . '/core/Migrations/Version29000Date20240124132202.php', 'OC\\Core\\Migrations\\Version29000Date20240131122720' => __DIR__ . '/../../..' . '/core/Migrations/Version29000Date20240131122720.php', + 'OC\\Core\\Migrations\\Version29000Date20240324113502' => __DIR__ . '/../../..' . '/core/Migrations/Version29000Date20240324113502.php', 'OC\\Core\\Notification\\CoreNotifier' => __DIR__ . '/../../..' . '/core/Notification/CoreNotifier.php', 'OC\\Core\\Service\\LoginFlowV2Service' => __DIR__ . '/../../..' . '/core/Service/LoginFlowV2Service.php', 'OC\\DB\\Adapter' => __DIR__ . '/../../..' . '/lib/private/DB/Adapter.php', diff --git a/lib/private/Authentication/WebAuthn/CredentialRepository.php b/lib/private/Authentication/WebAuthn/CredentialRepository.php index 81dad20fc6099..6760bfb3a88a4 100644 --- a/lib/private/Authentication/WebAuthn/CredentialRepository.php +++ b/lib/private/Authentication/WebAuthn/CredentialRepository.php @@ -61,7 +61,7 @@ public function findAllForUserEntity(PublicKeyCredentialUserEntity $publicKeyCre }, $entities); } - public function saveAndReturnCredentialSource(PublicKeyCredentialSource $publicKeyCredentialSource, ?string $name = null): PublicKeyCredentialEntity { + public function saveAndReturnCredentialSource(PublicKeyCredentialSource $publicKeyCredentialSource, ?string $name = null, bool $userVerification = false): PublicKeyCredentialEntity { $oldEntity = null; try { @@ -75,13 +75,18 @@ public function saveAndReturnCredentialSource(PublicKeyCredentialSource $publicK $name = 'default'; } - $entity = PublicKeyCredentialEntity::fromPublicKeyCrendentialSource($name, $publicKeyCredentialSource); + $entity = PublicKeyCredentialEntity::fromPublicKeyCrendentialSource($name, $publicKeyCredentialSource, $userVerification); if ($oldEntity) { $entity->setId($oldEntity->getId()); if ($defaultName) { $entity->setName($oldEntity->getName()); } + + // Don't downgrade UV just because it was skipped during a login due to another key + if ($oldEntity->getUserVerification()) { + $entity->setUserVerification(true); + } } return $this->credentialMapper->insertOrUpdate($entity); diff --git a/lib/private/Authentication/WebAuthn/Db/PublicKeyCredentialEntity.php b/lib/private/Authentication/WebAuthn/Db/PublicKeyCredentialEntity.php index 6f97ded483d71..bb223431a8a37 100644 --- a/lib/private/Authentication/WebAuthn/Db/PublicKeyCredentialEntity.php +++ b/lib/private/Authentication/WebAuthn/Db/PublicKeyCredentialEntity.php @@ -41,6 +41,9 @@ * @method void setPublicKeyCredentialId(string $id); * @method string getData(); * @method void setData(string $data); + * @since 30.0.0 + * @method bool|null getUserVerification(); + * @method void setUserVerification(bool $userVerification); */ class PublicKeyCredentialEntity extends Entity implements JsonSerializable { /** @var string */ @@ -55,20 +58,25 @@ class PublicKeyCredentialEntity extends Entity implements JsonSerializable { /** @var string */ protected $data; + /** @var bool|null */ + protected $userVerification; + public function __construct() { $this->addType('name', 'string'); $this->addType('uid', 'string'); $this->addType('publicKeyCredentialId', 'string'); $this->addType('data', 'string'); + $this->addType('userVerification', 'boolean'); } - public static function fromPublicKeyCrendentialSource(string $name, PublicKeyCredentialSource $publicKeyCredentialSource): PublicKeyCredentialEntity { + public static function fromPublicKeyCrendentialSource(string $name, PublicKeyCredentialSource $publicKeyCredentialSource, bool $userVerification): PublicKeyCredentialEntity { $publicKeyCredentialEntity = new self(); $publicKeyCredentialEntity->setName($name); $publicKeyCredentialEntity->setUid($publicKeyCredentialSource->getUserHandle()); $publicKeyCredentialEntity->setPublicKeyCredentialId(base64_encode($publicKeyCredentialSource->getPublicKeyCredentialId())); $publicKeyCredentialEntity->setData(json_encode($publicKeyCredentialSource)); + $publicKeyCredentialEntity->setUserVerification($userVerification); return $publicKeyCredentialEntity; } diff --git a/lib/private/Authentication/WebAuthn/Manager.php b/lib/private/Authentication/WebAuthn/Manager.php index b05e1757267ed..f90f2365dc2f2 100644 --- a/lib/private/Authentication/WebAuthn/Manager.php +++ b/lib/private/Authentication/WebAuthn/Manager.php @@ -107,8 +107,8 @@ public function startRegistration(IUser $user, string $serverHost): PublicKeyCre ]; $authenticatorSelectionCriteria = new AuthenticatorSelectionCriteria( - null, - AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_DISCOURAGED, + AuthenticatorSelectionCriteria::AUTHENTICATOR_ATTACHMENT_NO_PREFERENCE, + AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_PREFERRED, null, false, ); @@ -170,7 +170,8 @@ public function finishRegister(PublicKeyCredentialCreationOptions $publicKeyCred } // Persist the data - return $this->repository->saveAndReturnCredentialSource($publicKeyCredentialSource, $name); + $userVerification = $response->attestationObject->authData->isUserVerified(); + return $this->repository->saveAndReturnCredentialSource($publicKeyCredentialSource, $name, $userVerification); } private function stripPort(string $serverHost): string { @@ -179,7 +180,11 @@ private function stripPort(string $serverHost): string { public function startAuthentication(string $uid, string $serverHost): PublicKeyCredentialRequestOptions { // List of registered PublicKeyCredentialDescriptor classes associated to the user - $registeredPublicKeyCredentialDescriptors = array_map(function (PublicKeyCredentialEntity $entity) { + $userVerificationRequirement = AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_REQUIRED; + $registeredPublicKeyCredentialDescriptors = array_map(function (PublicKeyCredentialEntity $entity) use (&$userVerificationRequirement) { + if ($entity->getUserVerification() !== true) { + $userVerificationRequirement = AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_DISCOURAGED; + } $credential = $entity->toPublicKeyCredentialSource(); return new PublicKeyCredentialDescriptor( $credential->type, @@ -192,7 +197,7 @@ public function startAuthentication(string $uid, string $serverHost): PublicKeyC random_bytes(32), // Challenge $this->stripPort($serverHost), // Relying Party ID $registeredPublicKeyCredentialDescriptors, // Registered PublicKeyCredentialDescriptor classes - AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_DISCOURAGED, + $userVerificationRequirement, 60000, // Timeout ); } diff --git a/version.php b/version.php index a0b6fe052cb8c..1e4c89deb9d7b 100644 --- a/version.php +++ b/version.php @@ -30,7 +30,7 @@ // between betas, final and RCs. This is _not_ the public version number. Reset minor/patch level // when updating major/minor version number. -$OC_Version = [30, 0, 0, 0]; +$OC_Version = [30, 0, 0, 1]; // The human-readable string $OC_VersionString = '30.0.0 dev';