web-base/Core/Objects/TwoFactor/KeyBasedTwoFactorToken.class.php

120 lines
3.4 KiB
PHP
Raw Permalink Normal View History

2022-02-20 16:53:26 +01:00
<?php
2022-11-18 18:06:46 +01:00
namespace Core\Objects\TwoFactor;
2022-02-20 16:53:26 +01:00
2022-11-27 15:58:44 +01:00
use Core\Driver\SQL\SQL;
2023-01-16 21:47:23 +01:00
use Core\Objects\DatabaseEntity\Attribute\Transient;
2022-02-20 16:53:26 +01:00
use Cose\Algorithm\Signature\ECDSA\ECSignature;
2022-11-18 18:06:46 +01:00
use Core\Objects\DatabaseEntity\TwoFactorToken;
2023-01-16 21:47:23 +01:00
use Cose\Key\Key;
2022-02-20 16:53:26 +01:00
class KeyBasedTwoFactorToken extends TwoFactorToken {
const TYPE = "fido";
2023-01-16 21:47:23 +01:00
#[Transient]
2022-11-27 12:33:27 +01:00
private ?string $challenge;
2023-01-16 21:47:23 +01:00
#[Transient]
private ?string $credentialID;
#[Transient]
2022-11-27 12:33:27 +01:00
private ?PublicKey $publicKey;
2022-02-20 16:53:26 +01:00
2023-01-16 21:47:23 +01:00
private function __construct() {
2022-11-27 15:58:44 +01:00
parent::__construct(self::TYPE);
2023-01-16 21:47:23 +01:00
}
public function generateChallenge(int $length = 32) {
$this->challenge = base64_encode(generateRandomString($length, "raw"));
$_SESSION["challenge"] = $this->challenge;
}
public static function create(int $challengeLength = 32): KeyBasedTwoFactorToken {
$token = new KeyBasedTwoFactorToken();
$token->generateChallenge($challengeLength);
return $token;
}
public function hasChallenge(): bool {
2024-04-07 18:29:33 +02:00
return isset($this->challenge) && !empty($this->challenge);
}
2023-01-16 21:47:23 +01:00
public function getChallenge(): string {
return $this->challenge;
2022-11-27 15:58:44 +01:00
}
2022-06-20 19:52:31 +02:00
protected function readData(string $data) {
2022-11-27 15:58:44 +01:00
if (!$this->isConfirmed()) {
2023-01-16 21:47:23 +01:00
$this->challenge = $data;
$this->credentialID = null;
2022-02-20 16:53:26 +01:00
$this->publicKey = null;
} else {
$jsonData = json_decode($data, true);
2023-01-16 21:47:23 +01:00
$this->challenge = $_SESSION["challenge"] ?? "";
$this->credentialID = base64_decode($jsonData["credentialID"]);
2022-02-20 16:53:26 +01:00
$this->publicKey = PublicKey::fromJson($jsonData["publicKey"]);
}
}
public function getData(): string {
2023-01-16 21:47:23 +01:00
if (!$this->isConfirmed()) {
return $this->challenge;
2022-11-27 15:58:44 +01:00
} else {
return json_encode([
2023-01-16 21:47:23 +01:00
"credentialID" => $this->credentialID,
2022-11-27 15:58:44 +01:00
"publicKey" => $this->publicKey->jsonSerialize()
]);
}
}
2023-01-16 21:47:23 +01:00
public function confirmKeyBased(SQL $sql, string $credentialID, PublicKey $publicKey): bool {
$this->credentialID = $credentialID;
2022-11-27 15:58:44 +01:00
$this->publicKey = $publicKey;
return parent::confirm($sql);
2022-02-20 16:53:26 +01:00
}
public function getPublicKey(): ?PublicKey {
return $this->publicKey;
}
2022-06-20 19:52:31 +02:00
public function getCredentialId(): ?string {
2023-01-16 21:47:23 +01:00
return $this->credentialID;
2022-02-20 16:53:26 +01:00
}
2023-01-07 15:34:05 +01:00
public function jsonSerialize(?array $propertyNames = null): array {
$jsonData = parent::jsonSerialize();
2022-02-20 16:53:26 +01:00
2023-01-16 21:47:23 +01:00
if (!$this->isAuthenticated()) {
if (!empty($this->challenge) && ($propertyNames === null || in_array("challenge", $propertyNames))) {
$jsonData["challenge"] = $this->challenge;
}
2022-02-20 16:53:26 +01:00
2023-01-16 21:47:23 +01:00
if (!empty($this->credentialID) && ($propertyNames === null || in_array("credentialID", $propertyNames))) {
$jsonData["credentialID"] = base64_encode($this->credentialID);
}
2022-02-20 16:53:26 +01:00
}
2023-01-07 15:34:05 +01:00
return $jsonData;
2022-02-20 16:53:26 +01:00
}
// TODO: algorithms, hardcoded values, ...
public function verify(string $signature, string $data): bool {
switch ($this->publicKey->getUsedAlgorithm()) {
case -7: // EC2
if (strlen($signature) !== 64) {
$signature = \Cose\Algorithm\Signature\ECDSA\ECSignature::fromAsn1($signature, 64);
}
$coseKey = new \Cose\Key\Key($this->publicKey->getNormalizedData());
$ec2key = new \Cose\Key\Ec2Key($coseKey->getData());
$publicKey = $ec2key->toPublic();
$signature = ECSignature::toAsn1($signature, 64);
return openssl_verify($data, $signature, $publicKey->asPEM(), "sha256") === 1;
default:
// Not implemented :(
return false;
}
}
}