Core Update 1.4.0

This commit is contained in:
2022-02-20 16:53:26 +01:00
parent 918244125c
commit bd1f302433
86 changed files with 3301 additions and 41128 deletions

View 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;
}
}

View 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;
}
}

View 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));
}
}

View 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;
}
}
}

View 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);
}
}

View 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;
}
}

View 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;
}
}