Core Update 1.4.0
This commit is contained in:
37
core/Objects/TwoFactor/AttestationObject.class.php
Normal file
37
core/Objects/TwoFactor/AttestationObject.class.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace Objects\TwoFactor;
|
||||
|
||||
use Objects\ApiObject;
|
||||
|
||||
class AttestationObject extends ApiObject {
|
||||
|
||||
use \Objects\TwoFactor\CBORDecoder;
|
||||
|
||||
private string $format;
|
||||
private array $statement;
|
||||
private AuthenticationData $authData;
|
||||
|
||||
public function __construct(string $buffer) {
|
||||
$data = $this->decode($buffer)->getNormalizedData();
|
||||
$this->format = $data["fmt"];
|
||||
$this->statement = $data["attStmt"];
|
||||
$this->authData = new AuthenticationData($data["authData"]);
|
||||
}
|
||||
|
||||
public function jsonSerialize(): array {
|
||||
return [
|
||||
"format" => $this->format,
|
||||
"statement" => [
|
||||
"sig" => base64_encode($this->statement["sig"] ?? ""),
|
||||
"x5c" => base64_encode(($this->statement["x5c"] ?? [""])[0]),
|
||||
],
|
||||
"authData" => $this->authData->jsonSerialize()
|
||||
];
|
||||
}
|
||||
|
||||
public function getAuthData(): AuthenticationData {
|
||||
return $this->authData;
|
||||
}
|
||||
|
||||
}
|
||||
79
core/Objects/TwoFactor/AuthenticationData.class.php
Normal file
79
core/Objects/TwoFactor/AuthenticationData.class.php
Normal file
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace Objects\TwoFactor;
|
||||
|
||||
use Objects\ApiObject;
|
||||
|
||||
class AuthenticationData extends ApiObject {
|
||||
|
||||
private string $rpIDHash;
|
||||
private int $flags;
|
||||
private int $counter;
|
||||
private string $aaguid;
|
||||
private string $credentialID;
|
||||
private PublicKey $publicKey;
|
||||
|
||||
public function __construct(string $buffer) {
|
||||
|
||||
if (strlen($buffer) < 32 + 1 + 4) {
|
||||
throw new \Exception("Invalid authentication data buffer size");
|
||||
}
|
||||
|
||||
$offset = 0;
|
||||
$this->rpIDHash = substr($buffer, $offset, 32); $offset += 32;
|
||||
$this->flags = ord($buffer[$offset]); $offset += 1;
|
||||
$this->counter = unpack("N", $buffer, $offset)[1]; $offset += 4;
|
||||
|
||||
if (strlen($buffer) >= $offset + 4 + 2) {
|
||||
$this->aaguid = substr($buffer, $offset, 16); $offset += 16;
|
||||
$credentialIdLength = unpack("n", $buffer, $offset)[1]; $offset += 2;
|
||||
$this->credentialID = substr($buffer, $offset, $credentialIdLength); $offset += $credentialIdLength;
|
||||
|
||||
$credentialData = substr($buffer, $offset);
|
||||
$this->publicKey = new PublicKey($credentialData);
|
||||
}
|
||||
}
|
||||
|
||||
public function jsonSerialize(): array {
|
||||
return [
|
||||
"rpIDHash" => base64_encode($this->rpIDHash),
|
||||
"flags" => $this->flags,
|
||||
"counter" => $this->counter,
|
||||
"aaguid" => base64_encode($this->aaguid),
|
||||
"credentialID" => base64_encode($this->credentialID),
|
||||
"publicKey" => $this->publicKey->jsonSerialize()
|
||||
];
|
||||
}
|
||||
|
||||
public function getHash(): string {
|
||||
return $this->rpIDHash;
|
||||
}
|
||||
|
||||
public function verifyIntegrity(string $rp): bool {
|
||||
return $this->rpIDHash === hash("sha256", $rp, true);
|
||||
}
|
||||
|
||||
public function isUserPresent(): bool {
|
||||
return boolval($this->flags & (1 << 0));
|
||||
}
|
||||
|
||||
public function isUserVerified(): bool {
|
||||
return boolval($this->flags & (1 << 2));
|
||||
}
|
||||
|
||||
public function attestedCredentialData(): bool {
|
||||
return boolval($this->flags & (1 << 6));
|
||||
}
|
||||
|
||||
public function hasExtensionData(): bool {
|
||||
return boolval($this->flags & (1 << 7));
|
||||
}
|
||||
|
||||
public function getPublicKey(): PublicKey {
|
||||
return $this->publicKey;
|
||||
}
|
||||
|
||||
public function getCredentialID() {
|
||||
return $this->credentialID;
|
||||
}
|
||||
}
|
||||
16
core/Objects/TwoFactor/CBORDecoder.trait.php
Normal file
16
core/Objects/TwoFactor/CBORDecoder.trait.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace Objects\TwoFactor;
|
||||
|
||||
use CBOR\StringStream;
|
||||
|
||||
trait CBORDecoder {
|
||||
|
||||
protected function decode(string $buffer): \CBOR\CBORObject {
|
||||
$objectManager = new \CBOR\OtherObject\OtherObjectManager();
|
||||
$tagManager = new \CBOR\Tag\TagObjectManager();
|
||||
$decoder = new \CBOR\Decoder($tagManager, $objectManager);
|
||||
return $decoder->decode(new StringStream($buffer));
|
||||
}
|
||||
|
||||
}
|
||||
74
core/Objects/TwoFactor/KeyBasedTwoFactorToken.class.php
Normal file
74
core/Objects/TwoFactor/KeyBasedTwoFactorToken.class.php
Normal file
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace Objects\TwoFactor;
|
||||
|
||||
use Cose\Algorithm\Signature\ECDSA\ECSignature;
|
||||
|
||||
class KeyBasedTwoFactorToken extends TwoFactorToken {
|
||||
|
||||
const TYPE = "fido";
|
||||
|
||||
private ?string $challenge;
|
||||
private ?string $credentialId;
|
||||
private ?PublicKey $publicKey;
|
||||
|
||||
public function __construct(string $data, ?int $id = null, bool $confirmed = false) {
|
||||
parent::__construct(self::TYPE, $id, $confirmed);
|
||||
if (!$confirmed) {
|
||||
$this->challenge = base64_decode($data);
|
||||
$this->credentialId = null;
|
||||
$this->publicKey = null;
|
||||
} else {
|
||||
$jsonData = json_decode($data, true);
|
||||
$this->challenge = base64_decode($_SESSION["challenge"] ?? "");
|
||||
$this->credentialId = base64_decode($jsonData["credentialID"]);
|
||||
$this->publicKey = PublicKey::fromJson($jsonData["publicKey"]);
|
||||
}
|
||||
}
|
||||
|
||||
public function getData(): string {
|
||||
return $this->challenge;
|
||||
}
|
||||
|
||||
public function getPublicKey(): ?PublicKey {
|
||||
return $this->publicKey;
|
||||
}
|
||||
|
||||
public function getCredentialId() {
|
||||
return $this->credentialId;
|
||||
}
|
||||
|
||||
public function jsonSerialize(): array {
|
||||
$json = parent::jsonSerialize();
|
||||
|
||||
if (!empty($this->challenge) && !$this->isAuthenticated()) {
|
||||
$json["challenge"] = base64_encode($this->challenge);
|
||||
}
|
||||
|
||||
if (!empty($this->credentialId)) {
|
||||
$json["credentialID"] = base64_encode($this->credentialId);
|
||||
}
|
||||
|
||||
return $json;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
67
core/Objects/TwoFactor/PublicKey.class.php
Normal file
67
core/Objects/TwoFactor/PublicKey.class.php
Normal file
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace Objects\TwoFactor;
|
||||
|
||||
use Objects\ApiObject;
|
||||
|
||||
class PublicKey extends ApiObject {
|
||||
|
||||
use \Objects\TwoFactor\CBORDecoder;
|
||||
|
||||
private int $keyType;
|
||||
private int $usedAlgorithm;
|
||||
private int $curveType;
|
||||
private string $xCoordinate;
|
||||
private string $yCoordinate;
|
||||
|
||||
public function __construct(?string $cborData = null) {
|
||||
if ($cborData) {
|
||||
$data = $this->decode($cborData)->getNormalizedData();
|
||||
$this->keyType = $data["1"];
|
||||
$this->usedAlgorithm = $data["3"];
|
||||
$this->curveType = $data["-1"];
|
||||
$this->xCoordinate = $data["-2"];
|
||||
$this->yCoordinate = $data["-3"];
|
||||
}
|
||||
}
|
||||
|
||||
public static function fromJson($jsonData): PublicKey {
|
||||
$publicKey = new PublicKey(null);
|
||||
$publicKey->keyType = $jsonData["keyType"];
|
||||
$publicKey->usedAlgorithm = $jsonData["usedAlgorithm"];
|
||||
$publicKey->curveType = $jsonData["curveType"];
|
||||
$publicKey->xCoordinate = base64_decode($jsonData["coordinates"]["x"]);
|
||||
$publicKey->yCoordinate = base64_decode($jsonData["coordinates"]["y"]);
|
||||
return $publicKey;
|
||||
}
|
||||
|
||||
public function getUsedAlgorithm(): int {
|
||||
return $this->usedAlgorithm;
|
||||
}
|
||||
|
||||
public function jsonSerialize(): array {
|
||||
return [
|
||||
"keyType" => $this->keyType,
|
||||
"usedAlgorithm" => $this->usedAlgorithm,
|
||||
"curveType" => $this->curveType,
|
||||
"coordinates" => [
|
||||
"x" => base64_encode($this->xCoordinate),
|
||||
"y" => base64_encode($this->yCoordinate)
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function getNormalizedData(): array {
|
||||
return [
|
||||
"1" => $this->keyType,
|
||||
"3" => $this->usedAlgorithm,
|
||||
"-1" => $this->curveType,
|
||||
"-2" => $this->xCoordinate,
|
||||
"-3" => $this->yCoordinate,
|
||||
];
|
||||
}
|
||||
|
||||
public function getU2F(): string {
|
||||
return bin2hex("\x04" . $this->xCoordinate . $this->yCoordinate);
|
||||
}
|
||||
}
|
||||
59
core/Objects/TwoFactor/TimeBasedTwoFactorToken.class.php
Normal file
59
core/Objects/TwoFactor/TimeBasedTwoFactorToken.class.php
Normal file
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace Objects\TwoFactor;
|
||||
|
||||
use Base32\Base32;
|
||||
use chillerlan\QRCode\QRCode;
|
||||
use chillerlan\QRCode\QROptions;
|
||||
use Objects\User;
|
||||
|
||||
class TimeBasedTwoFactorToken extends TwoFactorToken {
|
||||
|
||||
const TYPE = "totp";
|
||||
private string $secret;
|
||||
|
||||
public function __construct(string $secret, ?int $id = null, bool $confirmed = false) {
|
||||
parent::__construct(self::TYPE, $id, $confirmed);
|
||||
$this->secret = $secret;
|
||||
}
|
||||
|
||||
public function getUrl(User $user): string {
|
||||
$otpType = self::TYPE;
|
||||
$name = rawurlencode($user->getUsername());
|
||||
$settings = $user->getConfiguration()->getSettings();
|
||||
$urlArgs = [
|
||||
"secret" => $this->secret,
|
||||
"issuer" => $settings->getSiteName(),
|
||||
];
|
||||
|
||||
$urlArgs = http_build_query($urlArgs);
|
||||
return "otpauth://$otpType/$name?$urlArgs";
|
||||
}
|
||||
|
||||
public function generateQRCode(User $user) {
|
||||
$options = new QROptions(['outputType' => QRCode::OUTPUT_IMAGE_PNG, "imageBase64" => false]);
|
||||
$qrcode = new QRCode($options);
|
||||
return $qrcode->render($this->getUrl($user));
|
||||
}
|
||||
|
||||
public function generate(?int $at = null, int $length = 6, int $period = 30): string {
|
||||
if ($at === null) {
|
||||
$at = time();
|
||||
}
|
||||
|
||||
$seed = intval($at / $period);
|
||||
$secret = Base32::decode($this->secret);
|
||||
$hmac = hash_hmac('sha1', pack("J", $seed), $secret, true);
|
||||
$offset = ord($hmac[-1]) & 0xF;
|
||||
$code = (unpack("N", substr($hmac, $offset, 4))[1] & 0x7fffffff) % intval(pow(10, $length));
|
||||
return substr(str_pad(strval($code), $length, "0", STR_PAD_LEFT), -1 * $length);
|
||||
}
|
||||
|
||||
public function verify(string $code): bool {
|
||||
return $this->generate() === $code;
|
||||
}
|
||||
|
||||
public function getData(): string {
|
||||
return $this->secret;
|
||||
}
|
||||
}
|
||||
62
core/Objects/TwoFactor/TwoFactorToken.class.php
Normal file
62
core/Objects/TwoFactor/TwoFactorToken.class.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace Objects\TwoFactor;
|
||||
use Objects\ApiObject;
|
||||
|
||||
abstract class TwoFactorToken extends ApiObject {
|
||||
|
||||
private ?int $id;
|
||||
private string $type;
|
||||
private bool $confirmed;
|
||||
private bool $authenticated;
|
||||
|
||||
public function __construct(string $type, ?int $id = null, bool $confirmed = false) {
|
||||
$this->id = $id;
|
||||
$this->type = $type;
|
||||
$this->confirmed = $confirmed;
|
||||
$this->authenticated = $_SESSION["2faAuthenticated"] ?? false;
|
||||
}
|
||||
|
||||
public function jsonSerialize(): array {
|
||||
return [
|
||||
"id" => $this->id,
|
||||
"type" => $this->type,
|
||||
"confirmed" => $this->confirmed,
|
||||
"authenticated" => $this->authenticated,
|
||||
];
|
||||
}
|
||||
|
||||
public abstract function getData(): string;
|
||||
|
||||
public function authenticate() {
|
||||
$this->authenticated = true;
|
||||
$_SESSION["2faAuthenticated"] = true;
|
||||
}
|
||||
|
||||
public function getType(): string {
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
public function isConfirmed(): bool {
|
||||
return $this->confirmed;
|
||||
}
|
||||
|
||||
public function getId(): int {
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public static function newInstance(string $type, string $data, ?int $id = null, bool $confirmed = false) {
|
||||
if ($type === TimeBasedTwoFactorToken::TYPE) {
|
||||
return new TimeBasedTwoFactorToken($data, $id, $confirmed);
|
||||
} else if ($type === KeyBasedTwoFactorToken::TYPE) {
|
||||
return new KeyBasedTwoFactorToken($data, $id, $confirmed);
|
||||
} else {
|
||||
// TODO: error message
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public function isAuthenticated(): bool {
|
||||
return $this->authenticated;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user