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

@@ -9,6 +9,14 @@ class AesStream {
private $callback;
private ?string $outputFile;
private ?string $inputFile;
private int $offset;
private ?int $length;
//
private ?string $md5SumIn;
private ?string $sha1SumIn;
private ?string $md5SumOut;
private ?string $sha1SumOut;
public function __construct(string $key, string $iv) {
$this->key = $key;
@@ -16,6 +24,12 @@ class AesStream {
$this->inputFile = null;
$this->outputFile = null;
$this->callback = null;
$this->offset = 0;
$this->length = null;
$this->md5SumIn = null;
$this->sha1SumIn = null;
$this->md5SumOut = null;
$this->sha1SumOut = null;
if (!in_array(strlen($key), [16, 24, 32])) {
throw new \Exception("Invalid Key Size");
@@ -59,14 +73,23 @@ class AesStream {
}
set_time_limit(0);
$md5ContextIn = hash_init("md5");
$sha1ContextIn = hash_init("sha1");
$md5ContextOut = hash_init("md5");
$sha1ContextOut = hash_init("sha1");
$ivCounter = $this->iv;
$modulo = \gmp_init("0x1" . str_repeat("00", $blockSize), 16);
$written = 0;
while (!feof($inputHandle)) {
$chunk = fread($inputHandle, 65536);
$chunkSize = strlen($chunk);
if ($chunkSize > 0) {
hash_update($md5ContextIn, $chunk);
hash_update($sha1ContextIn, $chunk);
$blockCount = intval(ceil($chunkSize / $blockSize));
$encrypted = openssl_encrypt($chunk, $aesMode, $this->key, OPENSSL_RAW_DATA | OPENSSL_NO_PADDING, $ivCounter);
@@ -76,18 +99,50 @@ class AesStream {
$ivNumber = str_pad(\gmp_strval($ivNumber, 16), $blockSize * 2, "0", STR_PAD_LEFT);
$ivCounter = hex2bin($ivNumber);
if ($this->callback !== null) {
call_user_func($this->callback, $encrypted);
// partial content
$skip = false;
if ($this->offset > 0 && $written < $this->offset) {
if ($written + $chunkSize >= $this->offset) {
$encrypted = substr($encrypted, $this->offset - $written);
} else {
$skip = true;
}
}
if ($outputHandle !== null) {
fwrite($outputHandle, $encrypted);
if ($this->length !== null) {
$notSkipped = max($written - $this->offset, 0);
if ($notSkipped + $chunkSize >= $this->length) {
$encrypted = substr($encrypted, 0, $this->length - $notSkipped);
}
}
if (!$skip) {
if ($this->callback !== null) {
call_user_func($this->callback, $encrypted);
}
if ($outputHandle !== null) {
fwrite($outputHandle, $encrypted);
}
hash_update($md5ContextOut, $encrypted);
hash_update($sha1ContextOut, $encrypted);
}
$written += $chunkSize;
if ($this->length !== null && $written - $this->offset >= $this->length) {
break;
}
}
}
fclose($inputHandle);
if ($outputHandle) fclose($outputHandle);
if ($outputHandle) {
fclose($outputHandle);
}
$this->md5SumIn = hash_final($md5ContextIn, false);
$this->sha1SumIn = hash_final($sha1ContextIn, false);
return true;
}
@@ -103,4 +158,25 @@ class AesStream {
public function getIV(): string {
return $this->iv;
}
public function setRange(int $offset, int $length) {
$this->offset = $offset;
$this->length = $length;
}
public function getMD5SumIn(): ?string {
return $this->md5SumIn;
}
public function getSHA1SumIn(): ?string {
return $this->sha1SumIn;
}
public function getMD5SumOut(): ?string {
return $this->md5SumOut;
}
public function getSHA1SumOut(): ?string {
return $this->sha1SumOut;
}
}

View File

@@ -6,6 +6,6 @@ abstract class ApiObject implements \JsonSerializable {
public abstract function jsonSerialize(): array;
public function __toString() { return json_encode($this); }
public function __toString() { return json_encode($this->jsonSerialize()); }
}

View File

@@ -0,0 +1,136 @@
<?php
namespace Objects;
class GpgKey extends ApiObject {
const GPG2 = "/usr/bin/gpg2";
private int $id;
private bool $confirmed;
private string $fingerprint;
private string $algorithm;
private \DateTime $expires;
public function __construct(int $id, bool $confirmed, string $fingerprint, string $algorithm, string $expires) {
$this->id = $id;
$this->confirmed = $confirmed;
$this->fingerprint = $fingerprint;
$this->algorithm = $algorithm;
$this->expires = new \DateTime($expires);
}
public static function encrypt(string $body, string $gpgFingerprint): array {
$gpgFingerprint = escapeshellarg($gpgFingerprint);
$cmd = self::GPG2 . " --encrypt --output - --recipient $gpgFingerprint --trust-model always --batch --armor";
list($out, $err) = self::proc_exec($cmd, $body, true);
if ($out === null) {
return self::createError("Error while communicating with GPG agent");
} else if ($err) {
return self::createError($err);
} else {
return ["success" => true, "data" => $out];
}
}
public function jsonSerialize(): array {
return array(
"fingerprint" => $this->fingerprint,
"algorithm" => $this->algorithm,
"expires" => $this->expires->getTimestamp(),
"confirmed" => $this->confirmed
);
}
private static function proc_exec(string $cmd, ?string $stdin = null, bool $raw = false): ?array {
$descriptorSpec = array(0 => ["pipe", "r"], 1 => ["pipe", "w"], 2 => ["pipe", "w"]);
$process = proc_open($cmd, $descriptorSpec,$pipes);
if (!is_resource($process)) {
return null;
}
if ($stdin) {
fwrite($pipes[0], $stdin);
fclose($pipes[0]);
}
$out = stream_get_contents($pipes[1]);
$err = stream_get_contents($pipes[2]);
fclose($pipes[1]);
fclose($pipes[2]);
proc_close($process);
return [($raw ? $out : trim($out)), $err];
}
private static function createError(string $error) : array {
return ["success" => false, "error" => $error];
}
public static function getKeyInfo(string $key): array {
list($out, $err) = self::proc_exec(self::GPG2 . " --show-key", $key);
if ($out === null) {
return self::createError("Error while communicating with GPG agent");
}
if ($err) {
return self::createError($err);
}
$lines = explode("\n", $out);
if (count($lines) > 4) {
return self::createError("It seems like you have uploaded more than one GPG-Key");
} else if (count($lines) !== 4 || !preg_match("/(\S+)\s+(\w+)\s+.*\[expires: ([0-9-]+)]/", $lines[0], $matches)) {
return self::createError("Error parsing GPG output");
}
$keyType = $matches[1];
$keyAlg = $matches[2];
$expires = \DateTime::createFromFormat("Y-m-d", $matches[3]);
$fingerprint = trim($lines[1]);
$keyData = ["type" => $keyType, "algorithm" => $keyAlg, "expires" => $expires, "fingerprint" => $fingerprint];
return ["success" => true, "data" => $keyData];
}
public static function importKey(string $key): array {
list($out, $err) = self::proc_exec(self::GPG2 . " --import", $key);
if ($out === null) {
return self::createError("Error while communicating with GPG agent");
}
if (preg_match("/gpg:\s+Total number processed:\s+(\d+)/", $err, $matches) && intval($matches[1]) > 0) {
if ((preg_match("/.*\s+unchanged:\s+(\d+)/", $err, $matches) && intval($matches[1]) > 0) ||
(preg_match("/.*\s+imported:\s+(\d+)/", $err, $matches) && intval($matches[1]) > 0)) {
return ["success" => true];
}
}
return self::createError($err);
}
public static function export($gpgFingerprint, bool $armored): array {
$cmd = self::GPG2 . " --export ";
if ($armored) {
$cmd .= "--armor ";
}
$cmd .= escapeshellarg($gpgFingerprint);
list($out, $err) = self::proc_exec($cmd);
if ($err) {
return self::createError($err);
}
return ["success" => true, "data" => $out];
}
public function isConfirmed(): bool {
return $this->confirmed;
}
public function getId(): int {
return $this->id;
}
public function getFingerprint(): string {
return $this->fingerprint;
}
}

View File

@@ -0,0 +1,13 @@
<?php
namespace Objects;
class KeyBasedTwoFactorToken extends TwoFactorToken {
const TYPE = "fido2";
public function __construct(string $secret, ?int $id = null, bool $confirmed = false) {
parent::__construct(self::TYPE, $secret, $id, $confirmed);
}
}

View File

@@ -47,14 +47,16 @@ namespace Objects {
return $key;
}
public function sendCookie() {
setcookie('lang', $this->langCode, 0, "/", "");
public function sendCookie(?string $domain = null) {
$domain = empty($domain) ? "" : $domain;
setcookie('lang', $this->langCode, 0, "/", $domain, false, false);
}
public function jsonSerialize(): array {
return array(
'uid' => $this->languageId,
'code' => $this->langCode,
'shortCode' => explode("_", $this->langCode)[0],
'name' => $this->langName,
);
}

View File

@@ -11,7 +11,7 @@ use External\JWT;
class Session extends ApiObject {
# in minutes
const DURATION = 60*24;
const DURATION = 60*60*24*14;
private ?int $sessionId;
private User $user;
@@ -25,13 +25,14 @@ class Session extends ApiObject {
public function __construct(User $user, ?int $sessionId, ?string $csrfToken) {
$this->user = $user;
$this->sessionId = $sessionId;
$this->stayLoggedIn = true;
$this->stayLoggedIn = false;
$this->csrfToken = $csrfToken ?? generateRandomString(16);
}
public static function create($user, $stayLoggedIn): ?Session {
public static function create(User $user, bool $stayLoggedIn = false): ?Session {
$session = new Session($user, null, null);
if($session->insert($stayLoggedIn)) {
if ($session->insert($stayLoggedIn)) {
$session->stayLoggedIn = $stayLoggedIn;
return $session;
}
@@ -39,8 +40,8 @@ class Session extends ApiObject {
}
private function updateMetaData() {
$this->expires = time() + Session::DURATION * 60;
$this->ipAddress = $_SERVER['REMOTE_ADDR'];
$this->expires = time() + Session::DURATION;
$this->ipAddress = is_cli() ? "127.0.0.1" : $_SERVER['REMOTE_ADDR'];
try {
$userAgent = @get_browser($_SERVER['HTTP_USER_AGENT'], true);
$this->os = $userAgent['platform'] ?? "Unknown";
@@ -51,31 +52,36 @@ class Session extends ApiObject {
}
}
public function setData($data) {
public function setData(array $data) {
foreach($data as $key => $value) {
$_SESSION[$key] = $value;
}
}
public function stayLoggedIn($val) {
public function stayLoggedIn(bool $val) {
$this->stayLoggedIn = $val;
}
public function sendCookie() {
public function getCookie(): string {
$this->updateMetaData();
$settings = $this->user->getConfiguration()->getSettings();
$token = array('userId' => $this->user->getId(), 'sessionId' => $this->sessionId);
$sessionCookie = JWT::encode($token, $settings->getJwtSecret());
$token = ['userId' => $this->user->getId(), 'sessionId' => $this->sessionId];
return JWT::encode($token, $settings->getJwtSecret());
}
public function sendCookie(?string $domain = null) {
$domain = empty($domain) ? "" : $domain;
$sessionCookie = $this->getCookie();
$secure = strcmp(getProtocol(), "https") === 0;
setcookie('session', $sessionCookie, $this->getExpiresTime(), "/", "", $secure, true);
setcookie('session', $sessionCookie, $this->getExpiresTime(), "/", $domain, $secure, true);
}
public function getExpiresTime(): int {
return ($this->stayLoggedIn == 0 ? 0 : $this->expires);
return ($this->stayLoggedIn ? $this->expires : 0);
}
public function getExpiresSeconds(): int {
return ($this->stayLoggedIn == 0 ? -1 : $this->expires - time());
return ($this->stayLoggedIn ? $this->expires - time() : -1);
}
public function jsonSerialize(): array {
@@ -90,7 +96,7 @@ class Session extends ApiObject {
);
}
public function insert($stayLoggedIn): bool {
public function insert(bool $stayLoggedIn = false): bool {
$this->updateMetaData();
$sql = $this->user->getSQL();
@@ -105,13 +111,13 @@ class Session extends ApiObject {
$this->ipAddress,
$this->os,
$this->browser,
json_encode($_SESSION),
json_encode($_SESSION ?? []),
$stayLoggedIn,
$this->csrfToken)
->returning("uid")
->execute();
if($success) {
if ($success) {
$this->sessionId = $this->user->getSQL()->getLastInsertId();
return true;
}
@@ -120,6 +126,7 @@ class Session extends ApiObject {
}
public function destroy(): bool {
session_destroy();
return $this->user->getSQL()->update("Session")
->set("active", false)
->where(new Compare("Session.uid", $this->sessionId))
@@ -138,7 +145,7 @@ class Session extends ApiObject {
->where(new Compare("uid", $this->user->getId()))
->execute() &&
$sql->update("Session")
->set("Session.expires", (new DateTime())->modify("+$minutes minute"))
->set("Session.expires", (new DateTime())->modify("+$minutes second"))
->set("Session.ipAddress", $this->ipAddress)
->set("Session.os", $this->os)
->set("Session.browser", $this->browser)

View File

@@ -0,0 +1,52 @@
<?php
namespace Objects;
use Base32\Base32;
use chillerlan\QRCode\QRCode;
use chillerlan\QRCode\QROptions;
class TimeBasedTwoFactorToken extends TwoFactorToken {
const TYPE = "totp";
public function __construct(string $secret, ?int $id = null, bool $confirmed = false) {
parent::__construct(self::TYPE, $secret, $id, $confirmed);
}
public function getUrl(User $user): string {
$otpType = self::TYPE;
$name = rawurlencode($user->getUsername());
$settings = $user->getConfiguration()->getSettings();
$urlArgs = [
"secret" => $this->getSecret(),
"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->getSecret());
$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;
}
}

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

View File

@@ -0,0 +1,63 @@
<?php
namespace Objects;
abstract class TwoFactorToken extends ApiObject {
private ?int $id;
private string $type;
private string $secret;
private bool $confirmed;
private bool $authenticated;
public function __construct(string $type, string $secret, ?int $id = null, bool $confirmed = false) {
$this->id = $id;
$this->type = $type;
$this->secret = $secret;
$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 function authenticate() {
$this->authenticated = true;
$_SESSION["2faAuthenticated"] = true;
}
public function getType(): string {
return $this->type;
}
public function getSecret(): string {
return $this->secret;
}
public function isConfirmed(): bool {
return $this->confirmed;
}
public function getId(): int {
return $this->id;
}
public static function newInstance(string $type, string $secret, ?int $id = null, bool $confirmed = false) {
if ($type === TimeBasedTwoFactorToken::TYPE) {
return new TimeBasedTwoFactorToken($secret, $id, $confirmed);
} else {
// TODO: error message
return null;
}
}
public function isAuthenticated(): bool {
return $this->authenticated;
}
}

View File

@@ -8,7 +8,9 @@ use External\JWT;
use Driver\SQL\SQL;
use Driver\SQL\Condition\Compare;
use Driver\SQL\Condition\CondBool;
use Objects\TwoFactor\TwoFactorToken;
// TODO: User::authorize and User::readData have similar function body
class User extends ApiObject {
private ?SQL $sql;
@@ -22,6 +24,8 @@ class User extends ApiObject {
private ?string $profilePicture;
private Language $language;
private array $groups;
private ?GpgKey $gpgKey;
private ?TwoFactorToken $twoFactorToken;
public function __construct($configuration) {
$this->configuration = $configuration;
@@ -66,6 +70,8 @@ class User extends ApiObject {
public function getConfiguration(): Configuration { return $this->configuration; }
public function getGroups(): array { return $this->groups; }
public function hasGroup(int $group): bool { return isset($this->groups[$group]); }
public function getGPG(): ?GpgKey { return $this->gpgKey; }
public function getTwoFactorToken(): ?TwoFactorToken { return $this->twoFactorToken; }
public function getProfilePicture() : ?string { return $this->profilePicture; }
public function __debugInfo(): array {
@@ -93,6 +99,8 @@ class User extends ApiObject {
'groups' => $this->groups,
'language' => $this->language->jsonSerialize(),
'session' => $this->session->jsonSerialize(),
"gpg" => ($this->gpgKey ? $this->gpgKey->jsonSerialize() : null),
"2fa" => ($this->twoFactorToken ? $this->twoFactorToken->jsonSerialize() : null),
);
} else {
return array(
@@ -109,11 +117,13 @@ class User extends ApiObject {
$this->loggedIn = false;
$this->session = null;
$this->profilePicture = null;
$this->gpgKey = null;
$this->twoFactorToken = null;
}
public function logout(): bool {
$success = true;
if($this->loggedIn) {
if ($this->loggedIn) {
$success = $this->session->destroy();
$this->reset();
}
@@ -131,11 +141,15 @@ class User extends ApiObject {
}
public function sendCookies() {
if($this->loggedIn) {
$this->session->sendCookie();
$baseUrl = $this->getConfiguration()->getSettings()->getBaseUrl();
$domain = parse_url($baseUrl, PHP_URL_HOST);
if ($this->loggedIn) {
$this->session->sendCookie($domain);
}
$this->language->sendCookie();
$this->language->sendCookie($domain);
session_write_close();
}
@@ -147,7 +161,11 @@ class User extends ApiObject {
*/
public function readData($userId, $sessionId, bool $sessionUpdate = true): bool {
$res = $this->sql->select("User.name", "User.email", "User.fullName", "User.profilePicture",
$res = $this->sql->select("User.name", "User.email", "User.fullName",
"User.profilePicture",
"User.gpg_id", "GpgKey.confirmed as gpg_confirmed", "GpgKey.fingerprint as gpg_fingerprint",
"GpgKey.expires as gpg_expires", "GpgKey.algorithm as gpg_algorithm",
"User.2fa_id", "2FA.confirmed as 2fa_confirmed", "2FA.data as 2fa_data", "2FA.type as 2fa_type",
"Language.uid as langId", "Language.code as langCode", "Language.name as langName",
"Session.data", "Session.stay_logged_in", "Session.csrf_token", "Group.uid as groupId", "Group.name as groupName")
->from("User")
@@ -155,6 +173,8 @@ class User extends ApiObject {
->leftJoin("Language", "User.language_id", "Language.uid")
->leftJoin("UserGroup", "UserGroup.user_id", "User.uid")
->leftJoin("Group", "UserGroup.group_id", "Group.uid")
->leftJoin("GpgKey", "User.gpg_id", "GpgKey.uid")
->leftJoin("2FA", "User.2fa_id", "2FA.uid")
->where(new Compare("User.uid", $userId))
->where(new Compare("Session.uid", $sessionId))
->where(new Compare("Session.active", true))
@@ -175,11 +195,21 @@ class User extends ApiObject {
$this->profilePicture = $row["profilePicture"];
$this->session = new Session($this, $sessionId, $csrfToken);
$this->session->setData(json_decode($row["data"] ?? '{}'));
$this->session->stayLoggedIn($this->sql->parseBool(["stay_logged_in"]));
if($sessionUpdate) $this->session->update();
$this->session->setData(json_decode($row["data"] ?? '{}', true));
$this->session->stayLoggedIn($this->sql->parseBool($row["stay_logged_in"]));
if ($sessionUpdate) $this->session->update();
$this->loggedIn = true;
if (!empty($row["gpg_id"])) {
$this->gpgKey = new GpgKey($row["gpg_id"], $this->sql->parseBool($row["gpg_confirmed"]),
$row["gpg_fingerprint"], $row["gpg_algorithm"], $row["gpg_expires"]);
}
if (!empty($row["2fa_id"])) {
$this->twoFactorToken = TwoFactorToken::newInstance($row["2fa_type"], $row["2fa_data"],
$row["2fa_id"], $this->sql->parseBool($row["2fa_confirmed"]));
}
if(!is_null($row['langId'])) {
$this->setLanguage(Language::newInstance($row['langId'], $row['langCode'], $row['langName']));
}
@@ -194,7 +224,7 @@ class User extends ApiObject {
}
private function parseCookies() {
if(isset($_COOKIE['session']) && is_string($_COOKIE['session']) && !empty($_COOKIE['session'])) {
if (isset($_COOKIE['session']) && is_string($_COOKIE['session']) && !empty($_COOKIE['session'])) {
try {
$token = $_COOKIE['session'];
$settings = $this->configuration->getSettings();
@@ -218,7 +248,7 @@ class User extends ApiObject {
}
}
public function createSession($userId, $stayLoggedIn): bool {
public function createSession(int $userId, bool $stayLoggedIn = false): bool {
$this->uid = $userId;
$this->session = Session::create($this, $stayLoggedIn);
if ($this->session) {
@@ -237,6 +267,9 @@ class User extends ApiObject {
$res = $this->sql->select("ApiKey.user_id as uid", "User.name", "User.fullName", "User.email",
"User.confirmed", "User.profilePicture",
"User.gpg_id", "GpgKey.fingerprint as gpg_fingerprint", "GpgKey.expires as gpg_expires",
"GpgKey.confirmed as gpg_confirmed", "GpgKey.algorithm as gpg_algorithm",
"User.2fa_id", "2FA.confirmed as 2fa_confirmed", "2FA.data as 2fa_data", "2FA.type as 2fa_type",
"Language.uid as langId", "Language.code as langCode", "Language.name as langName",
"Group.uid as groupId", "Group.name as groupName")
->from("ApiKey")
@@ -244,6 +277,8 @@ class User extends ApiObject {
->leftJoin("UserGroup", "UserGroup.user_id", "User.uid")
->leftJoin("Group", "UserGroup.group_id", "Group.uid")
->leftJoin("Language", "User.language_id", "Language.uid")
->leftJoin("GpgKey", "User.gpg_id", "GpgKey.uid")
->leftJoin("2FA", "User.2fa_id", "2FA.uid")
->where(new Compare("ApiKey.api_key", $apiKey))
->where(new Compare("valid_until", $this->sql->currentTimestamp(), ">"))
->where(new Compare("ApiKey.active", 1))
@@ -265,6 +300,17 @@ class User extends ApiObject {
$this->email = $row['email'];
$this->profilePicture = $row["profilePicture"];
if (!empty($row["gpg_id"])) {
$this->gpgKey = new GpgKey($row["gpg_id"], $this->sql->parseBool($row["gpg_confirmed"]),
$row["gpg_fingerprint"], $row["gpg_algorithm"], $row["gpg_expires"]
);
}
if (!empty($row["2fa_id"])) {
$this->twoFactorToken = TwoFactorToken::newInstance($row["2fa_type"], $row["2fa_data"],
$row["2fa_id"], $this->sql->parseBool($row["2fa_confirmed"]));
}
if(!is_null($row['langId'])) {
$this->setLanguage(Language::newInstance($row['langId'], $row['langCode'], $row['langName']));
}