= 2) { $q = $value[0]; if (($q === '"' && $value[$len - 1] === '"') || ($q === "'" && $value[$len - 1] === "'")) { $value = substr($value, 1, -1); } } $existing = getenv($name); if ($existing !== false && $existing !== '') { continue; } putenv("$name=$value"); $_ENV[$name] = $value; } } qdb_load_dotenv(__DIR__ . '/.env'); // --- MASTER-KEY (Server-seitig zum Speichern der DB) --- // Per ENV 'QDB_MASTER_KEY' (Base64- oder ASCII-String) oder Fallback auf bisherigen Wert. function get_master_key_bytes(): string { $env = getenv('QDB_MASTER_KEY'); if (!$env || $env === '') { throw new RuntimeException('QDB_MASTER_KEY environment variable is not set. Cannot proceed without a master key.'); } $b = base64_decode($env, true); if ($b !== false && strlen($b) > 0) { return str_pad(substr($b, 0, 32), 32, "\0"); } return str_pad(substr($env, 0, 32), 32, "\0"); } // --- HKDF (SHA-256) über Token -> Session-Key (32 Byte) --- // Kompatibel zu Android-Implementierung: salt = leer, info = "qdb-aes" function hkdf_session_key_from_token(string $tokenHex, string $info = 'qdb-aes', int $len = 32): string { $ikm = @hex2bin(trim($tokenHex)); if ($ikm === false) $ikm = $tokenHex; // Falls bereits binär if (function_exists('hash_hkdf')) { return hash_hkdf('sha256', $ikm, $len, $info, ''); } // Fallback: eigene HKDF-Implementierung $hashLen = 32; $salt = str_repeat("\0", $hashLen); $prk = hash_hmac('sha256', $ikm, $salt, true); $okm = ''; $t = ''; $n = (int)ceil($len / $hashLen); for ($i = 1; $i <= $n; $i++) { $t = hash_hmac('sha256', $t . $info . chr($i), $prk, true); $okm .= $t; } return substr($okm, 0, $len); } // --- Token-Storage (DB-backed via session table) --- function token_add(string $token, int $ttlSeconds = 86400, array $extra = []): void { require_once __DIR__ . '/db_init.php'; [$pdo, $tmpDb, $lockFp] = qdb_open(true); $now = time(); $pdo->prepare( "INSERT INTO session (token, userID, role, entityID, createdAt, expiresAt, temp) VALUES (:t, :uid, :role, :eid, :ca, :ea, :tmp)" )->execute([ ':t' => $token, ':uid' => $extra['userID'] ?? '', ':role' => $extra['role'] ?? '', ':eid' => $extra['entityID'] ?? '', ':ca' => $now, ':ea' => $now + $ttlSeconds, ':tmp' => (int)(!empty($extra['temp'])), ]); // Opportunistic cleanup: remove expired sessions $pdo->prepare("DELETE FROM session WHERE expiresAt < :now")->execute([':now' => $now]); qdb_save($tmpDb, $lockFp); } function token_get_record(string $token): ?array { require_once __DIR__ . '/db_init.php'; [$pdo, $tmpDb, $lockFp] = qdb_open(false); $stmt = $pdo->prepare("SELECT * FROM session WHERE token = :t AND expiresAt >= :now"); $stmt->execute([':t' => $token, ':now' => time()]); $row = $stmt->fetch(PDO::FETCH_ASSOC); qdb_discard($tmpDb, $lockFp); if (!$row) return null; // Map DB columns to the keys the rest of the codebase expects return [ 'token' => $row['token'], 'role' => $row['role'], 'entityID' => $row['entityID'], 'userID' => $row['userID'], 'created' => (int)$row['createdAt'], 'exp' => (int)$row['expiresAt'], 'temp' => (int)$row['temp'], ]; } function token_revoke(string $token): void { require_once __DIR__ . '/db_init.php'; [$pdo, $tmpDb, $lockFp] = qdb_open(true); $pdo->prepare("DELETE FROM session WHERE token = :t")->execute([':t' => $token]); qdb_save($tmpDb, $lockFp); } // --- Auth helpers for endpoints --- function get_bearer_token(): ?string { $auth = $_SERVER['HTTP_AUTHORIZATION'] ?? ($_SERVER['Authorization'] ?? ''); if (stripos($auth, 'Bearer ') === 0) { return substr($auth, 7); } return null; } function qdb_auth_abort(array $body, int $status): never { if (defined('QDB_TESTING') && QDB_TESTING) { require_once __DIR__ . '/lib/QdbHttpResponse.php'; throw new QdbHttpResponse($body, $status); } http_response_code($status); header('Content-Type: application/json; charset=UTF-8'); echo json_encode($body); exit; } function require_valid_token(): array { $token = get_bearer_token(); if (!$token) { qdb_auth_abort(["error" => "Missing Bearer token"], 401); } $rec = token_get_record($token); if (!$rec) { qdb_auth_abort(["error" => "Invalid or expired token"], 403); } if (!empty($rec['temp'])) { qdb_auth_abort(["error" => "Password change required before access"], 403); } $rec['_token'] = $token; return $rec; } function require_role(array $allowed, array $tokenRecord): void { $role = $tokenRecord['role'] ?? ''; if (!in_array($role, $allowed, true)) { qdb_auth_abort(["error" => "Insufficient permissions"], 403); } } /** * Build a SQL WHERE clause fragment that restricts client visibility by role. * Returns [string $clause, array $params]. * $clientAlias is the table alias or name that has a coachID column (e.g. 'client'). */ function rbac_client_filter(array $tokenRecord, string $clientAlias = 'client'): array { $role = $tokenRecord['role'] ?? ''; $entityID = $tokenRecord['entityID'] ?? ''; switch ($role) { case 'admin': return ['1=1', []]; case 'supervisor': return [ "$clientAlias.coachID IN (SELECT coachID FROM coach WHERE supervisorID = :rbac_eid)", [':rbac_eid' => $entityID] ]; case 'coach': return [ "$clientAlias.coachID = :rbac_eid", [':rbac_eid' => $entityID] ]; default: return ['0=1', []]; } } // --- AES-256-CBC: IV(16) + CIPHERTEXT --- function aes256_cbc_encrypt_bytes(string $plain, string $key): string { $key = str_pad(substr($key, 0, 32), 32, "\0"); $iv = random_bytes(16); $cipher = openssl_encrypt($plain, 'AES-256-CBC', $key, OPENSSL_RAW_DATA, $iv); if ($cipher === false) throw new Exception('openssl_encrypt failed'); return $iv . $cipher; } function aes256_cbc_decrypt_bytes(string $data, string $key): string { if (strlen($data) < 16) throw new Exception('cipher too short'); $key = str_pad(substr($key, 0, 32), 32, "\0"); $iv = substr($data, 0, 16); $ct = substr($data, 16); $plain = openssl_decrypt($ct, 'AES-256-CBC', $key, OPENSSL_RAW_DATA, $iv); if ($plain === false) throw new Exception('openssl_decrypt failed'); return $plain; }