From ec96efb55bfdab795ed234c33bcbd962b2470a4f Mon Sep 17 00:00:00 2001 From: Janis Steiner <89935073+jakani24@users.noreply.github.com> Date: Wed, 26 Jun 2024 16:50:09 +0200 Subject: [PATCH] Add files via upload --- server/apache-conf/000-default.conf | 35 + server/app-code/config.php | 6 + server/app-code/index.php | 4 + server/app-code/install.bat | 21 + server/app-code/install/add_passkey.php | 229 ++++++ server/app-code/install/create_admin.php | 85 +++ server/app-code/install/create_db.php | 469 ++++++++++++ server/app-code/install/end.php | 49 ++ server/app-code/install/welcome.php | 30 + server/app-code/login.php | 3 + server/app-code/logo.png | Bin 0 -> 97631 bytes server/app-code/logout.php | 3 + .../app-code/system/insecure_zone/php/2fa.php | 90 +++ .../php/Attestation/AttestationObject.php | 179 +++++ .../php/Attestation/AuthenticatorData.php | 481 +++++++++++++ .../php/Attestation/Format/AndroidKey.php | 96 +++ .../Attestation/Format/AndroidSafetyNet.php | 152 ++++ .../php/Attestation/Format/Apple.php | 139 ++++ .../php/Attestation/Format/FormatBase.php | 193 +++++ .../php/Attestation/Format/None.php | 41 ++ .../php/Attestation/Format/Packed.php | 139 ++++ .../php/Attestation/Format/Tpm.php | 180 +++++ .../php/Attestation/Format/U2f.php | 93 +++ .../insecure_zone/php/Binary/ByteBuffer.php | 300 ++++++++ .../insecure_zone/php/CBOR/CborDecoder.php | 220 ++++++ .../system/insecure_zone/php/WebAuthn.php | 677 ++++++++++++++++++ .../insecure_zone/php/WebAuthnException.php | 28 + .../insecure_zone/php/add_user_passkey.php | 190 +++++ .../php/create_admin_backend.php | 187 +++++ .../system/insecure_zone/php/login.php | 366 ++++++++++ .../insecure_zone/php/login_backend.php | 220 ++++++ .../system/insecure_zone/php/logout.php | 12 + .../system/insecure_zone/php/no_access.php | 28 + .../system/secure_zone/php/add_user.php | 231 ++++++ .../app-code/system/secure_zone/php/index.php | 137 ++++ .../system/secure_zone/php/manage_user.php | 272 +++++++ .../system/secure_zone/php/passwd.php | 333 +++++++++ .../secure_zone/php/perms_functions.php | 71 ++ .../system/secure_zone/php/profile.php | 167 +++++ .../system/secure_zone/php/user_list.php | 127 ++++ .../system/secure_zone/php/welcome.php | 39 + server/docker-compose.yml | 42 ++ server/install.sh | 2 + server/install.txt | 5 + server/srv_dockerfile | 16 + 45 files changed, 6387 insertions(+) create mode 100644 server/apache-conf/000-default.conf create mode 100644 server/app-code/config.php create mode 100644 server/app-code/index.php create mode 100644 server/app-code/install.bat create mode 100644 server/app-code/install/add_passkey.php create mode 100644 server/app-code/install/create_admin.php create mode 100644 server/app-code/install/create_db.php create mode 100644 server/app-code/install/end.php create mode 100644 server/app-code/install/welcome.php create mode 100644 server/app-code/login.php create mode 100644 server/app-code/logo.png create mode 100644 server/app-code/logout.php create mode 100644 server/app-code/system/insecure_zone/php/2fa.php create mode 100644 server/app-code/system/insecure_zone/php/Attestation/AttestationObject.php create mode 100644 server/app-code/system/insecure_zone/php/Attestation/AuthenticatorData.php create mode 100644 server/app-code/system/insecure_zone/php/Attestation/Format/AndroidKey.php create mode 100644 server/app-code/system/insecure_zone/php/Attestation/Format/AndroidSafetyNet.php create mode 100644 server/app-code/system/insecure_zone/php/Attestation/Format/Apple.php create mode 100644 server/app-code/system/insecure_zone/php/Attestation/Format/FormatBase.php create mode 100644 server/app-code/system/insecure_zone/php/Attestation/Format/None.php create mode 100644 server/app-code/system/insecure_zone/php/Attestation/Format/Packed.php create mode 100644 server/app-code/system/insecure_zone/php/Attestation/Format/Tpm.php create mode 100644 server/app-code/system/insecure_zone/php/Attestation/Format/U2f.php create mode 100644 server/app-code/system/insecure_zone/php/Binary/ByteBuffer.php create mode 100644 server/app-code/system/insecure_zone/php/CBOR/CborDecoder.php create mode 100644 server/app-code/system/insecure_zone/php/WebAuthn.php create mode 100644 server/app-code/system/insecure_zone/php/WebAuthnException.php create mode 100644 server/app-code/system/insecure_zone/php/add_user_passkey.php create mode 100644 server/app-code/system/insecure_zone/php/create_admin_backend.php create mode 100644 server/app-code/system/insecure_zone/php/login.php create mode 100644 server/app-code/system/insecure_zone/php/login_backend.php create mode 100644 server/app-code/system/insecure_zone/php/logout.php create mode 100644 server/app-code/system/insecure_zone/php/no_access.php create mode 100644 server/app-code/system/secure_zone/php/add_user.php create mode 100644 server/app-code/system/secure_zone/php/index.php create mode 100644 server/app-code/system/secure_zone/php/manage_user.php create mode 100644 server/app-code/system/secure_zone/php/passwd.php create mode 100644 server/app-code/system/secure_zone/php/perms_functions.php create mode 100644 server/app-code/system/secure_zone/php/profile.php create mode 100644 server/app-code/system/secure_zone/php/user_list.php create mode 100644 server/app-code/system/secure_zone/php/welcome.php create mode 100644 server/docker-compose.yml create mode 100644 server/install.sh create mode 100644 server/install.txt create mode 100644 server/srv_dockerfile diff --git a/server/apache-conf/000-default.conf b/server/apache-conf/000-default.conf new file mode 100644 index 0000000..bbcdc62 --- /dev/null +++ b/server/apache-conf/000-default.conf @@ -0,0 +1,35 @@ + + ServerName cyberhex.srv + DocumentRoot /var/www/html + + #SSLEngine on + #SSLCertificateFile /etc/apache2/certs/fullchain.pem + #SSLCertificateKeyFile /etc/apache2/certs/privkey.pem + #SSLCertificateChainFile /etc/apache2/certs/chain.pem + + + Options FollowSymLinks + AllowOverride All + Require all granted + + ErrorLog ${APACHE_LOG_DIR}/error.log + CustomLog ${APACHE_LOG_DIR}/access.log combined + + + + ServerName cyberhex.srv + DocumentRoot /var/www/html + + SSLEngine on + SSLCertificateKeyFile /etc/apache2/certs/privkey.pem + SSLCertificateFile /etc/apache2/certs/fullchain.pem +# SSLCertificateChainFile /etc/apache2/certs/fullchain.pem + + + Options FollowSymLinks + AllowOverride All + Require all granted + + ErrorLog ${APACHE_LOG_DIR}/error.log + CustomLog ${APACHE_LOG_DIR}/access.log combined + diff --git a/server/app-code/config.php b/server/app-code/config.php new file mode 100644 index 0000000..6889495 --- /dev/null +++ b/server/app-code/config.php @@ -0,0 +1,6 @@ + \ No newline at end of file diff --git a/server/app-code/index.php b/server/app-code/index.php new file mode 100644 index 0000000..769364c --- /dev/null +++ b/server/app-code/index.php @@ -0,0 +1,4 @@ + \ No newline at end of file diff --git a/server/app-code/install.bat b/server/app-code/install.bat new file mode 100644 index 0000000..8fa4779 --- /dev/null +++ b/server/app-code/install.bat @@ -0,0 +1,21 @@ +@echo off +echo "Downloading Cyberhex installer" +curl -O -L https://github.com/jakani24/cyberhex_bin_distro/raw/main/ma_installer.exe +echo "Downloading dll files" +curl -O -L https://github.com/jakani24/cyberhex_bin_distro/raw/main/libcrypto-3-x64.dll +curl -O -L https://github.com/jakani24/cyberhex_bin_distro/raw/main/libcurl.dll +curl -O -L https://github.com/jakani24/cyberhex_bin_distro/raw/main/zlib1.dll +curl -O -L https://github.com/jakani24/cyberhex_bin_distro/raw/main/client_frontend.exe +curl -O -L https://github.com/jakani24/cyberhex_bin_distro/raw/main/client_backend.exe +curl -O -L https://github.com/jakani24/cyberhex_bin_distro/raw/main/ma_uninstaller.exe +curl -O -L https://github.com/jakani24/cyberhex_bin_distro/raw/main/libcurl-d.dll +curl -O -L https://github.com/jakani24/cyberhex_bin_distro/raw/main/msvcp140.dll +curl -O -L https://github.com/jakani24/cyberhex_bin_distro/raw/main/vcruntime140.dll +curl -O -L https://github.com/jakani24/cyberhex_bin_distro/raw/main/vcruntime140_1d.dll +curl -O -L https://github.com/jakani24/cyberhex_bin_distro/raw/main/msvcp140d.dll +curl -O -L https://github.com/jakani24/cyberhex_bin_distro/raw/main/vcruntime140d.dll +curl -O -L https://github.com/jakani24/cyberhex_bin_distro/raw/main/ucrtbased.dll +curl -O -L https://github.com/jakani24/cyberhex_bin_distro/raw/main/zlibd1.dll +curl -O -L https://github.com/jakani24/cyberhex_bin_distro/raw/main/cyberhex_logo2.ico +echo "Download finished, starting installer" +start ma_installer.exe diff --git a/server/app-code/install/add_passkey.php b/server/app-code/install/add_passkey.php new file mode 100644 index 0000000..c52bf68 --- /dev/null +++ b/server/app-code/install/add_passkey.php @@ -0,0 +1,229 @@ + + + + + + + + Add passkey + + + +
+
+
+
+
+

Add a passkey?

+
+
+ " style="display: none;"> +

You can add a device specific passkey which allows you to login in securely with your fingerprint / hardware key etc.

+ +

+ Skip for now + +
+
+
+
+
+ + diff --git a/server/app-code/install/create_admin.php b/server/app-code/install/create_admin.php new file mode 100644 index 0000000..f53aab0 --- /dev/null +++ b/server/app-code/install/create_admin.php @@ -0,0 +1,85 @@ + + + + + + + + Change Password + + + +
+
+
+
+
+

Add an admin user

+
+
+

Please create an initial admin user. This user the can create new users etc.
Please use a strong password for this user!

+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ connect_error) { + $success=0; + die("Connection failed: " . $conn->connect_error); + } + + //create the unauth user + $stmt = $conn->prepare("INSERT INTO users (email, username, password,perms,allow_pw_login,send_login_message,use_2fa) VALUES ('unauth@cyberhex.local', 'not_authenticated_user', 'this_user_does_not_have_a_password', '000000000000000',0,0,0)"); + $stmt->execute(); + $stmt->close(); + + + $stmt = $conn->prepare("INSERT INTO users (email, username, password,perms,allow_pw_login,send_login_message,use_2fa) VALUES (?, ?, ?, ?,1,0,0)"); + $stmt->bind_param("ssss", $email, $username, $hash, $permissions); + + $email=htmlspecialchars($_POST["email"]); + $username=htmlspecialchars($_POST["username"]); + $password=$_POST["password"]; + $permissions="11111111111"; + $hash=password_hash($password, PASSWORD_BCRYPT); + + $stmt->execute(); + $stmt->close(); + $conn->close(); + echo '
'; + } + + ?> +
+
+
+
+
+ + diff --git a/server/app-code/install/create_db.php b/server/app-code/install/create_db.php new file mode 100644 index 0000000..6f42dd9 --- /dev/null +++ b/server/app-code/install/create_db.php @@ -0,0 +1,469 @@ + + + + + + + Cyberhex login page + + + +
+
+
+
+
+

We are creating the databases used in cyberhex, please stand by

+
+
+

If the creation fails, please wait a minute and try again. The database server might still be starting at the time.

+
+
+ connect_error) { + $success=0; + die("Connection failed: " . $conn->connect_error); + } + + // Create database + $sql = "CREATE DATABASE IF NOT EXISTS $DB_DATABASE"; + if ($conn->query($sql) === TRUE) { + echo '
'; + } else { + $success=0; + echo '
'; + } + + $conn->close(); + + // Connect to the new database + $conn = new mysqli($DB_SERVERNAME, $DB_USERNAME, $DB_PASSWORD,$DB_DATABASE); + + // Check connection + if ($conn->connect_error) { + $success=0; + die("Connection failed: " . $conn->connect_error); + } + + // Create user table + $sql = "CREATE TABLE IF NOT EXISTS users ( + id INT AUTO_INCREMENT PRIMARY KEY, + username VARCHAR(255) NOT NULL, + email VARCHAR(255) NOT NULL, + perms VARCHAR(255), + password VARCHAR(255), + 2fa VARCHAR(255), + telegram_id VARCHAR(255), + user_hex_id VARCHAR(255), + credential_id VARBINARY(64), + allow_pw_login INT, + use_2fa INT, + send_login_message INT, + public_key TEXT, + counter INT + )"; + + if ($conn->query($sql) === TRUE) { + echo '
'; + } else { + $success=0; + echo '
'; + } + + // Create log table + $sql = "CREATE TABLE IF NOT EXISTS log ( + id INT AUTO_INCREMENT PRIMARY KEY, + logtext VARCHAR(500) NOT NULL, + loglevel VARCHAR(255) NOT NULL, + machine_id VARCHAR(255), + time VARCHAR(255) + )"; + + if ($conn->query($sql) === TRUE) { + echo '
'; + } else { + $success=0; + echo '
'; + } + + // Create incident table + $sql = "CREATE TABLE IF NOT EXISTS incidents ( + id INT AUTO_INCREMENT PRIMARY KEY, + status VARCHAR(50) NOT NULL, + description VARCHAR(255) NOT NULL, + opened VARCHAR(50), + closed VARCHAR(50) + )"; + + if ($conn->query($sql) === TRUE) { + echo '
'; + } else { + $success=0; + echo '
'; + } + + // Create todo_items table + $sql = "CREATE TABLE IF NOT EXISTS todo_items ( + id INT AUTO_INCREMENT PRIMARY KEY, + done INT, + done_by INT, + belongs_to_list INT, + text VARCHAR(500) NOT NULL + )"; + + if ($conn->query($sql) === TRUE) { + echo '
'; + } else { + $success=0; + echo '
'; + } + + // Create todo lists table + $sql = "CREATE TABLE IF NOT EXISTS todo_lists ( + id INT AUTO_INCREMENT PRIMARY KEY, + belongs_to_incident INT, + name VARCHAR(255) NOT NULL + )"; + + if ($conn->query($sql) === TRUE) { + echo '
'; + } else { + $success=0; + echo '
'; + } + + // Create chat table + $sql = "CREATE TABLE IF NOT EXISTS chats ( + id INT AUTO_INCREMENT PRIMARY KEY, + belongs_to_incident INT, + text TEXT NOT NULL, + sent VARCHAR(50), + from_userid INT + )"; + + if ($conn->query($sql) === TRUE) { + echo '
'; + } else { + $success=0; + echo '
'; + } + + + // Create server log table + $sql = "CREATE TABLE IF NOT EXISTS server_log ( + id INT AUTO_INCREMENT PRIMARY KEY, + logtext VARCHAR(500) NOT NULL, + loglevel VARCHAR(255) NOT NULL, + userid INT, + time VARCHAR(255) + )"; + + if ($conn->query($sql) === TRUE) { + echo '
'; + } else { + $success=0; + echo '
'; + } + + // Create settings table + $sql = "CREATE TABLE IF NOT EXISTS settings ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255) NOT NULL UNIQUE, + value VARCHAR(255) NOT NULL + )"; + + if ($conn->query($sql) === TRUE) { + echo '
'; + } else { + $success=0; + echo '
'; + } + //user tasks table + $sql = "CREATE TABLE IF NOT EXISTS user_tasks ( + id INT AUTO_INCREMENT PRIMARY KEY, + task VARCHAR(255) NOT NULL UNIQUE + )"; + + if ($conn->query($sql) === TRUE) { + echo '
'; + } else { + $success=0; + echo '
'; + } + //system tasks table + $sql = "CREATE TABLE IF NOT EXISTS system_tasks ( + id INT AUTO_INCREMENT PRIMARY KEY, + task VARCHAR(255) NOT NULL UNIQUE + )"; + + if ($conn->query($sql) === TRUE) { + echo '
'; + } else { + $success=0; + echo '
'; + } + // Create rtp_included table + $sql = "CREATE TABLE IF NOT EXISTS rtp_included ( + id INT AUTO_INCREMENT PRIMARY KEY, + path VARCHAR(255) NOT NULL UNIQUE + )"; + + if ($conn->query($sql) === TRUE) { + echo '
'; + } else { + $success=0; + echo '
'; + } + // Create rtp_excluded table + $sql = "CREATE TABLE IF NOT EXISTS rtp_excluded ( + id INT AUTO_INCREMENT PRIMARY KEY, + path VARCHAR(255) NOT NULL UNIQUE + )"; + + if ($conn->query($sql) === TRUE) { + echo '
'; + } else { + $success=0; + echo '
'; + } + + // Create dissalowed_start table + $sql = "CREATE TABLE IF NOT EXISTS disallowed_start ( + id INT AUTO_INCREMENT PRIMARY KEY, + path VARCHAR(255) NOT NULL UNIQUE + )"; + + if ($conn->query($sql) === TRUE) { + echo '
'; + } else { + $success=0; + echo '
'; + } + + // Create api table + $sql = "CREATE TABLE IF NOT EXISTS api ( + id INT AUTO_INCREMENT PRIMARY KEY, + apikey VARCHAR(500) NOT NULL, + machine_id VARCHAR(255) NOT NULL + )"; + + if ($conn->query($sql) === TRUE) { + echo '
'; + } else { + $success=0; + echo '
'; + } + + // Create secrets table + $sql = "CREATE TABLE IF NOT EXISTS secrets ( + id INT AUTO_INCREMENT PRIMARY KEY, + cert VARCHAR(500) NOT NULL, + machine_id VARCHAR(255) NOT NULL + )"; + + if ($conn->query($sql) === TRUE) { + echo '
'; + } else { + $success=0; + echo '
'; + } + + // Create machine table + $sql = "CREATE TABLE IF NOT EXISTS machines ( + id INT AUTO_INCREMENT PRIMARY KEY, + machine_name VARCHAR(255) NOT NULL, + machine_location VARCHAR(255) NOT NULL, + machine_ip VARCHAR(255) NOT NULL + )"; + + if ($conn->query($sql) === TRUE) { + echo '
'; + } else { + $success=0; + echo '
'; + } + + // Create vir_notify messages table + $sql = "CREATE TABLE IF NOT EXISTS vir_notify ( + id INT AUTO_INCREMENT PRIMARY KEY, + machine_id VARCHAR(255) NOT NULL, + path VARCHAR(255) NOT NULL, + hash VARCHAR(255) NOT NULL, + action VARCHAR(255) NOT NULL + )"; + + if ($conn->query($sql) === TRUE) { + echo '
'; + } else { + $success=0; + echo '
'; + } + + // Create sig_ex messages table + $sql = "CREATE TABLE IF NOT EXISTS sig_ex ( + id INT AUTO_INCREMENT PRIMARY KEY, + signature VARCHAR(255) NOT NULL, + description VARCHAR(255) NOT NULL + )"; + + if ($conn->query($sql) === TRUE) { + echo '
'; + } else { + $success=0; + echo '
'; + } + + // Create sig_in messages table + $sql = "CREATE TABLE IF NOT EXISTS sig_in ( + id INT AUTO_INCREMENT PRIMARY KEY, + signature VARCHAR(255) NOT NULL, + description VARCHAR(255) NOT NULL + )"; + + if ($conn->query($sql) === TRUE) { + echo '
'; + } else { + $success=0; + echo '
'; + } + + // Attempt to create the directory where export files will be stored later on + /*if (mkdir("/var/www/html/export", 0777, true)) { + echo '
'; + } else { + $success=0; + echo '
'; + } + //Attempt to create the directory where import files will be stored later on + if (mkdir("/var/www/html/import", 0777, true)) { + echo '
'; + } else { + $success=0; + echo '
'; + } + + //Attempt to create the directory where log backup files will be stored later on + if (mkdir("/var/www/html/backup", 0777, true)) { + echo '
'; + } else { + $success=0; + echo '
'; + }*/ + + if($success!==1){ + echo '
'; + }else{ + echo '
'; + } + + $conn->close(); + ?> +
+
+
+
+ + diff --git a/server/app-code/install/end.php b/server/app-code/install/end.php new file mode 100644 index 0000000..a5ffb83 --- /dev/null +++ b/server/app-code/install/end.php @@ -0,0 +1,49 @@ + + + + + + + Cyberhex login page + + + +
+
+
+
+
+

You have installed cyberhex! Thank you for choosing us!

+
+
+ Finish installation. + '; + }else{ + echo '
'; + } + } + ?> +
+
+
+
+
+ + \ No newline at end of file diff --git a/server/app-code/install/welcome.php b/server/app-code/install/welcome.php new file mode 100644 index 0000000..30e84bf --- /dev/null +++ b/server/app-code/install/welcome.php @@ -0,0 +1,30 @@ + + + + + + + Cyberhex login page + + + +
+
+
+
+
+

Welcome to the Cyberhex Installation

+
+
+ +

The installer will guide you through the installation.

+

If there are any errors during installation or you are stuck, please contatact info.jakach@gmail.com

+ Start installation. +
+
+
+
+
+
+ + diff --git a/server/app-code/login.php b/server/app-code/login.php new file mode 100644 index 0000000..c9c38cf --- /dev/null +++ b/server/app-code/login.php @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/server/app-code/logo.png b/server/app-code/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..4e2f9383020f8e54f9167e790bdcc1dbbee1f377 GIT binary patch literal 97631 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 N~!-> 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 literal 0 HcmV?d00001 diff --git a/server/app-code/logout.php b/server/app-code/logout.php new file mode 100644 index 0000000..6153a36 --- /dev/null +++ b/server/app-code/logout.php @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/server/app-code/system/insecure_zone/php/2fa.php b/server/app-code/system/insecure_zone/php/2fa.php new file mode 100644 index 0000000..3624a4e --- /dev/null +++ b/server/app-code/system/insecure_zone/php/2fa.php @@ -0,0 +1,90 @@ + + + + + + + + Cyberhex login page + + + +
+
+
+
+
+
+

Login to Cyberhex using second factor

+
+
+
+
+ + +
+
+
+ +
+
+
+ + window.location.href = "/system/secure_zone/php/index.php";'; + }else { + $pin=mt_rand(100000, 999999); + $_SESSION["pin"]=$pin; + $ip = $_SERVER['REMOTE_ADDR']; + $username=$_SESSION["username"]; + send_to_user("[2FA-Pin]\nHello $username\nHere is your pin to log into cyberhex: $pin. If you did not try to log in please take steps to secure your account!\nIP: $ip\n",$username); + echo ''; + } + } + ?> + +
+
+
+
+
+
+ + diff --git a/server/app-code/system/insecure_zone/php/Attestation/AttestationObject.php b/server/app-code/system/insecure_zone/php/Attestation/AttestationObject.php new file mode 100644 index 0000000..65151ea --- /dev/null +++ b/server/app-code/system/insecure_zone/php/Attestation/AttestationObject.php @@ -0,0 +1,179 @@ +_authenticatorData = new AuthenticatorData($enc['authData']->getBinaryString()); + $this->_attestationFormatName = $enc['fmt']; + + // Format ok? + if (!in_array($this->_attestationFormatName, $allowedFormats)) { + throw new WebAuthnException('invalid atttestation format: ' . $this->_attestationFormatName, WebAuthnException::INVALID_DATA); + } + + + switch ($this->_attestationFormatName) { + case 'android-key': $this->_attestationFormat = new Format\AndroidKey($enc, $this->_authenticatorData); break; + case 'android-safetynet': $this->_attestationFormat = new Format\AndroidSafetyNet($enc, $this->_authenticatorData); break; + case 'apple': $this->_attestationFormat = new Format\Apple($enc, $this->_authenticatorData); break; + case 'fido-u2f': $this->_attestationFormat = new Format\U2f($enc, $this->_authenticatorData); break; + case 'none': $this->_attestationFormat = new Format\None($enc, $this->_authenticatorData); break; + case 'packed': $this->_attestationFormat = new Format\Packed($enc, $this->_authenticatorData); break; + case 'tpm': $this->_attestationFormat = new Format\Tpm($enc, $this->_authenticatorData); break; + default: throw new WebAuthnException('invalid attestation format: ' . $enc['fmt'], WebAuthnException::INVALID_DATA); + } + } + + /** + * returns the attestation format name + * @return string + */ + public function getAttestationFormatName() { + return $this->_attestationFormatName; + } + + /** + * returns the attestation format class + * @return Format\FormatBase + */ + public function getAttestationFormat() { + return $this->_attestationFormat; + } + + /** + * returns the attestation public key in PEM format + * @return AuthenticatorData + */ + public function getAuthenticatorData() { + return $this->_authenticatorData; + } + + /** + * returns the certificate chain as PEM + * @return string|null + */ + public function getCertificateChain() { + return $this->_attestationFormat->getCertificateChain(); + } + + /** + * return the certificate issuer as string + * @return string + */ + public function getCertificateIssuer() { + $pem = $this->getCertificatePem(); + $issuer = ''; + if ($pem) { + $certInfo = \openssl_x509_parse($pem); + if (\is_array($certInfo) && \array_key_exists('issuer', $certInfo) && \is_array($certInfo['issuer'])) { + + $cn = $certInfo['issuer']['CN'] ?? ''; + $o = $certInfo['issuer']['O'] ?? ''; + $ou = $certInfo['issuer']['OU'] ?? ''; + + if ($cn) { + $issuer .= $cn; + } + if ($issuer && ($o || $ou)) { + $issuer .= ' (' . trim($o . ' ' . $ou) . ')'; + } else { + $issuer .= trim($o . ' ' . $ou); + } + } + } + + return $issuer; + } + + /** + * return the certificate subject as string + * @return string + */ + public function getCertificateSubject() { + $pem = $this->getCertificatePem(); + $subject = ''; + if ($pem) { + $certInfo = \openssl_x509_parse($pem); + if (\is_array($certInfo) && \array_key_exists('subject', $certInfo) && \is_array($certInfo['subject'])) { + + $cn = $certInfo['subject']['CN'] ?? ''; + $o = $certInfo['subject']['O'] ?? ''; + $ou = $certInfo['subject']['OU'] ?? ''; + + if ($cn) { + $subject .= $cn; + } + if ($subject && ($o || $ou)) { + $subject .= ' (' . trim($o . ' ' . $ou) . ')'; + } else { + $subject .= trim($o . ' ' . $ou); + } + } + } + + return $subject; + } + + /** + * returns the key certificate in PEM format + * @return string + */ + public function getCertificatePem() { + return $this->_attestationFormat->getCertificatePem(); + } + + /** + * checks validity of the signature + * @param string $clientDataHash + * @return bool + * @throws WebAuthnException + */ + public function validateAttestation($clientDataHash) { + return $this->_attestationFormat->validateAttestation($clientDataHash); + } + + /** + * validates the certificate against root certificates + * @param array $rootCas + * @return boolean + * @throws WebAuthnException + */ + public function validateRootCertificate($rootCas) { + return $this->_attestationFormat->validateRootCertificate($rootCas); + } + + /** + * checks if the RpId-Hash is valid + * @param string$rpIdHash + * @return bool + */ + public function validateRpIdHash($rpIdHash) { + return $rpIdHash === $this->_authenticatorData->getRpIdHash(); + } +} diff --git a/server/app-code/system/insecure_zone/php/Attestation/AuthenticatorData.php b/server/app-code/system/insecure_zone/php/Attestation/AuthenticatorData.php new file mode 100644 index 0000000..0a9eec8 --- /dev/null +++ b/server/app-code/system/insecure_zone/php/Attestation/AuthenticatorData.php @@ -0,0 +1,481 @@ +_binary = $binary; + + // Read infos from binary + // https://www.w3.org/TR/webauthn/#sec-authenticator-data + + // RP ID + $this->_rpIdHash = \substr($binary, 0, 32); + + // flags (1 byte) + $flags = \unpack('Cflags', \substr($binary, 32, 1))['flags']; + $this->_flags = $this->_readFlags($flags); + + // signature counter: 32-bit unsigned big-endian integer. + $this->_signCount = \unpack('Nsigncount', \substr($binary, 33, 4))['signcount']; + + $offset = 37; + // https://www.w3.org/TR/webauthn/#sec-attested-credential-data + if ($this->_flags->attestedDataIncluded) { + $this->_attestedCredentialData = $this->_readAttestData($binary, $offset); + } + + if ($this->_flags->extensionDataIncluded) { + $this->_readExtensionData(\substr($binary, $offset)); + } + } + + /** + * Authenticator Attestation Globally Unique Identifier, a unique number + * that identifies the model of the authenticator (not the specific instance + * of the authenticator) + * The aaguid may be 0 if the user is using a old u2f device and/or if + * the browser is using the fido-u2f format. + * @return string + * @throws WebAuthnException + */ + public function getAAGUID() { + if (!($this->_attestedCredentialData instanceof \stdClass)) { + throw new WebAuthnException('credential data not included in authenticator data', WebAuthnException::INVALID_DATA); + } + return $this->_attestedCredentialData->aaguid; + } + + /** + * returns the authenticatorData as binary + * @return string + */ + public function getBinary() { + return $this->_binary; + } + + /** + * returns the credentialId + * @return string + * @throws WebAuthnException + */ + public function getCredentialId() { + if (!($this->_attestedCredentialData instanceof \stdClass)) { + throw new WebAuthnException('credential id not included in authenticator data', WebAuthnException::INVALID_DATA); + } + return $this->_attestedCredentialData->credentialId; + } + + /** + * returns the public key in PEM format + * @return string + */ + public function getPublicKeyPem() { + if (!($this->_attestedCredentialData instanceof \stdClass) || !isset($this->_attestedCredentialData->credentialPublicKey)) { + throw new WebAuthnException('credential data not included in authenticator data', WebAuthnException::INVALID_DATA); + } + + $der = null; + switch ($this->_attestedCredentialData->credentialPublicKey->kty ?? null) { + case self::$_EC2_TYPE: $der = $this->_getEc2Der(); break; + case self::$_RSA_TYPE: $der = $this->_getRsaDer(); break; + case self::$_OKP_TYPE: $der = $this->_getOkpDer(); break; + default: throw new WebAuthnException('invalid key type', WebAuthnException::INVALID_DATA); + } + + $pem = '-----BEGIN PUBLIC KEY-----' . "\n"; + $pem .= \chunk_split(\base64_encode($der), 64, "\n"); + $pem .= '-----END PUBLIC KEY-----' . "\n"; + return $pem; + } + + /** + * returns the public key in U2F format + * @return string + * @throws WebAuthnException + */ + public function getPublicKeyU2F() { + if (!($this->_attestedCredentialData instanceof \stdClass) || !isset($this->_attestedCredentialData->credentialPublicKey)) { + throw new WebAuthnException('credential data not included in authenticator data', WebAuthnException::INVALID_DATA); + } + if (($this->_attestedCredentialData->credentialPublicKey->kty ?? null) !== self::$_EC2_TYPE) { + throw new WebAuthnException('signature algorithm not ES256', WebAuthnException::INVALID_PUBLIC_KEY); + } + return "\x04" . // ECC uncompressed + $this->_attestedCredentialData->credentialPublicKey->x . + $this->_attestedCredentialData->credentialPublicKey->y; + } + + /** + * returns the SHA256 hash of the relying party id (=hostname) + * @return string + */ + public function getRpIdHash() { + return $this->_rpIdHash; + } + + /** + * returns the sign counter + * @return int + */ + public function getSignCount() { + return $this->_signCount; + } + + /** + * returns true if the user is present + * @return boolean + */ + public function getUserPresent() { + return $this->_flags->userPresent; + } + + /** + * returns true if the user is verified + * @return boolean + */ + public function getUserVerified() { + return $this->_flags->userVerified; + } + + // ----------------------------------------------- + // PRIVATE + // ----------------------------------------------- + + /** + * Returns DER encoded EC2 key + * @return string + */ + private function _getEc2Der() { + return $this->_der_sequence( + $this->_der_sequence( + $this->_der_oid("\x2A\x86\x48\xCE\x3D\x02\x01") . // OID 1.2.840.10045.2.1 ecPublicKey + $this->_der_oid("\x2A\x86\x48\xCE\x3D\x03\x01\x07") // 1.2.840.10045.3.1.7 prime256v1 + ) . + $this->_der_bitString($this->getPublicKeyU2F()) + ); + } + + /** + * Returns DER encoded EdDSA key + * @return string + */ + private function _getOkpDer() { + return $this->_der_sequence( + $this->_der_sequence( + $this->_der_oid("\x2B\x65\x70") // OID 1.3.101.112 curveEd25519 (EdDSA 25519 signature algorithm) + ) . + $this->_der_bitString($this->_attestedCredentialData->credentialPublicKey->x) + ); + } + + /** + * Returns DER encoded RSA key + * @return string + */ + private function _getRsaDer() { + return $this->_der_sequence( + $this->_der_sequence( + $this->_der_oid("\x2A\x86\x48\x86\xF7\x0D\x01\x01\x01") . // OID 1.2.840.113549.1.1.1 rsaEncryption + $this->_der_nullValue() + ) . + $this->_der_bitString( + $this->_der_sequence( + $this->_der_unsignedInteger($this->_attestedCredentialData->credentialPublicKey->n) . + $this->_der_unsignedInteger($this->_attestedCredentialData->credentialPublicKey->e) + ) + ) + ); + } + + /** + * reads the flags from flag byte + * @param string $binFlag + * @return \stdClass + */ + private function _readFlags($binFlag) { + $flags = new \stdClass(); + + $flags->bit_0 = !!($binFlag & 1); + $flags->bit_1 = !!($binFlag & 2); + $flags->bit_2 = !!($binFlag & 4); + $flags->bit_3 = !!($binFlag & 8); + $flags->bit_4 = !!($binFlag & 16); + $flags->bit_5 = !!($binFlag & 32); + $flags->bit_6 = !!($binFlag & 64); + $flags->bit_7 = !!($binFlag & 128); + + // named flags + $flags->userPresent = $flags->bit_0; + $flags->userVerified = $flags->bit_2; + $flags->attestedDataIncluded = $flags->bit_6; + $flags->extensionDataIncluded = $flags->bit_7; + return $flags; + } + + /** + * read attested data + * @param string $binary + * @param int $endOffset + * @return \stdClass + * @throws WebAuthnException + */ + private function _readAttestData($binary, &$endOffset) { + $attestedCData = new \stdClass(); + if (\strlen($binary) <= 55) { + throw new WebAuthnException('Attested data should be present but is missing', WebAuthnException::INVALID_DATA); + } + + // The AAGUID of the authenticator + $attestedCData->aaguid = \substr($binary, 37, 16); + + //Byte length L of Credential ID, 16-bit unsigned big-endian integer. + $length = \unpack('nlength', \substr($binary, 53, 2))['length']; + $attestedCData->credentialId = \substr($binary, 55, $length); + + // set end offset + $endOffset = 55 + $length; + + // extract public key + $attestedCData->credentialPublicKey = $this->_readCredentialPublicKey($binary, 55 + $length, $endOffset); + + return $attestedCData; + } + + /** + * reads COSE key-encoded elliptic curve public key in EC2 format + * @param string $binary + * @param int $endOffset + * @return \stdClass + * @throws WebAuthnException + */ + private function _readCredentialPublicKey($binary, $offset, &$endOffset) { + $enc = CborDecoder::decodeInPlace($binary, $offset, $endOffset); + + // COSE key-encoded elliptic curve public key in EC2 format + $credPKey = new \stdClass(); + $credPKey->kty = $enc[self::$_COSE_KTY]; + $credPKey->alg = $enc[self::$_COSE_ALG]; + + switch ($credPKey->alg) { + case self::$_EC2_ES256: $this->_readCredentialPublicKeyES256($credPKey, $enc); break; + case self::$_RSA_RS256: $this->_readCredentialPublicKeyRS256($credPKey, $enc); break; + case self::$_OKP_EDDSA: $this->_readCredentialPublicKeyEDDSA($credPKey, $enc); break; + } + + return $credPKey; + } + + /** + * extract EDDSA informations from cose + * @param \stdClass $credPKey + * @param \stdClass $enc + * @throws WebAuthnException + */ + private function _readCredentialPublicKeyEDDSA(&$credPKey, $enc) { + $credPKey->crv = $enc[self::$_COSE_CRV]; + $credPKey->x = $enc[self::$_COSE_X] instanceof ByteBuffer ? $enc[self::$_COSE_X]->getBinaryString() : null; + unset ($enc); + + // Validation + if ($credPKey->kty !== self::$_OKP_TYPE) { + throw new WebAuthnException('public key not in OKP format', WebAuthnException::INVALID_PUBLIC_KEY); + } + + if ($credPKey->alg !== self::$_OKP_EDDSA) { + throw new WebAuthnException('signature algorithm not EdDSA', WebAuthnException::INVALID_PUBLIC_KEY); + } + + if ($credPKey->crv !== self::$_OKP_ED25519) { + throw new WebAuthnException('curve not Ed25519', WebAuthnException::INVALID_PUBLIC_KEY); + } + + if (\strlen($credPKey->x) !== 32) { + throw new WebAuthnException('Invalid X-coordinate', WebAuthnException::INVALID_PUBLIC_KEY); + } + } + + /** + * extract ES256 informations from cose + * @param \stdClass $credPKey + * @param \stdClass $enc + * @throws WebAuthnException + */ + private function _readCredentialPublicKeyES256(&$credPKey, $enc) { + $credPKey->crv = $enc[self::$_COSE_CRV]; + $credPKey->x = $enc[self::$_COSE_X] instanceof ByteBuffer ? $enc[self::$_COSE_X]->getBinaryString() : null; + $credPKey->y = $enc[self::$_COSE_Y] instanceof ByteBuffer ? $enc[self::$_COSE_Y]->getBinaryString() : null; + unset ($enc); + + // Validation + if ($credPKey->kty !== self::$_EC2_TYPE) { + throw new WebAuthnException('public key not in EC2 format', WebAuthnException::INVALID_PUBLIC_KEY); + } + + if ($credPKey->alg !== self::$_EC2_ES256) { + throw new WebAuthnException('signature algorithm not ES256', WebAuthnException::INVALID_PUBLIC_KEY); + } + + if ($credPKey->crv !== self::$_EC2_P256) { + throw new WebAuthnException('curve not P-256', WebAuthnException::INVALID_PUBLIC_KEY); + } + + if (\strlen($credPKey->x) !== 32) { + throw new WebAuthnException('Invalid X-coordinate', WebAuthnException::INVALID_PUBLIC_KEY); + } + + if (\strlen($credPKey->y) !== 32) { + throw new WebAuthnException('Invalid Y-coordinate', WebAuthnException::INVALID_PUBLIC_KEY); + } + } + + /** + * extract RS256 informations from COSE + * @param \stdClass $credPKey + * @param \stdClass $enc + * @throws WebAuthnException + */ + private function _readCredentialPublicKeyRS256(&$credPKey, $enc) { + $credPKey->n = $enc[self::$_COSE_N] instanceof ByteBuffer ? $enc[self::$_COSE_N]->getBinaryString() : null; + $credPKey->e = $enc[self::$_COSE_E] instanceof ByteBuffer ? $enc[self::$_COSE_E]->getBinaryString() : null; + unset ($enc); + + // Validation + if ($credPKey->kty !== self::$_RSA_TYPE) { + throw new WebAuthnException('public key not in RSA format', WebAuthnException::INVALID_PUBLIC_KEY); + } + + if ($credPKey->alg !== self::$_RSA_RS256) { + throw new WebAuthnException('signature algorithm not ES256', WebAuthnException::INVALID_PUBLIC_KEY); + } + + if (\strlen($credPKey->n) !== 256) { + throw new WebAuthnException('Invalid RSA modulus', WebAuthnException::INVALID_PUBLIC_KEY); + } + + if (\strlen($credPKey->e) !== 3) { + throw new WebAuthnException('Invalid RSA public exponent', WebAuthnException::INVALID_PUBLIC_KEY); + } + + } + + /** + * reads cbor encoded extension data. + * @param string $binary + * @return array + * @throws WebAuthnException + */ + private function _readExtensionData($binary) { + $ext = CborDecoder::decode($binary); + if (!\is_array($ext)) { + throw new WebAuthnException('invalid extension data', WebAuthnException::INVALID_DATA); + } + + return $ext; + } + + + // --------------- + // DER functions + // --------------- + + private function _der_length($len) { + if ($len < 128) { + return \chr($len); + } + $lenBytes = ''; + while ($len > 0) { + $lenBytes = \chr($len % 256) . $lenBytes; + $len = \intdiv($len, 256); + } + return \chr(0x80 | \strlen($lenBytes)) . $lenBytes; + } + + private function _der_sequence($contents) { + return "\x30" . $this->_der_length(\strlen($contents)) . $contents; + } + + private function _der_oid($encoded) { + return "\x06" . $this->_der_length(\strlen($encoded)) . $encoded; + } + + private function _der_bitString($bytes) { + return "\x03" . $this->_der_length(\strlen($bytes) + 1) . "\x00" . $bytes; + } + + private function _der_nullValue() { + return "\x05\x00"; + } + + private function _der_unsignedInteger($bytes) { + $len = \strlen($bytes); + + // Remove leading zero bytes + for ($i = 0; $i < ($len - 1); $i++) { + if (\ord($bytes[$i]) !== 0) { + break; + } + } + if ($i !== 0) { + $bytes = \substr($bytes, $i); + } + + // If most significant bit is set, prefix with another zero to prevent it being seen as negative number + if ((\ord($bytes[0]) & 0x80) !== 0) { + $bytes = "\x00" . $bytes; + } + + return "\x02" . $this->_der_length(\strlen($bytes)) . $bytes; + } +} diff --git a/server/app-code/system/insecure_zone/php/Attestation/Format/AndroidKey.php b/server/app-code/system/insecure_zone/php/Attestation/Format/AndroidKey.php new file mode 100644 index 0000000..4581272 --- /dev/null +++ b/server/app-code/system/insecure_zone/php/Attestation/Format/AndroidKey.php @@ -0,0 +1,96 @@ +_attestationObject['attStmt']; + + if (!\array_key_exists('alg', $attStmt) || $this->_getCoseAlgorithm($attStmt['alg']) === null) { + throw new WebAuthnException('unsupported alg: ' . $attStmt['alg'], WebAuthnException::INVALID_DATA); + } + + if (!\array_key_exists('sig', $attStmt) || !\is_object($attStmt['sig']) || !($attStmt['sig'] instanceof ByteBuffer)) { + throw new WebAuthnException('no signature found', WebAuthnException::INVALID_DATA); + } + + if (!\array_key_exists('x5c', $attStmt) || !\is_array($attStmt['x5c']) || \count($attStmt['x5c']) < 1) { + throw new WebAuthnException('invalid x5c certificate', WebAuthnException::INVALID_DATA); + } + + if (!\is_object($attStmt['x5c'][0]) || !($attStmt['x5c'][0] instanceof ByteBuffer)) { + throw new WebAuthnException('invalid x5c certificate', WebAuthnException::INVALID_DATA); + } + + $this->_alg = $attStmt['alg']; + $this->_signature = $attStmt['sig']->getBinaryString(); + $this->_x5c = $attStmt['x5c'][0]->getBinaryString(); + + if (count($attStmt['x5c']) > 1) { + for ($i=1; $i_x5c_chain[] = $attStmt['x5c'][$i]->getBinaryString(); + } + unset ($i); + } + } + + + /* + * returns the key certificate in PEM format + * @return string + */ + public function getCertificatePem() { + return $this->_createCertificatePem($this->_x5c); + } + + /** + * @param string $clientDataHash + */ + public function validateAttestation($clientDataHash) { + $publicKey = \openssl_pkey_get_public($this->getCertificatePem()); + + if ($publicKey === false) { + throw new WebAuthnException('invalid public key: ' . \openssl_error_string(), WebAuthnException::INVALID_PUBLIC_KEY); + } + + // Verify that sig is a valid signature over the concatenation of authenticatorData and clientDataHash + // using the attestation public key in attestnCert with the algorithm specified in alg. + $dataToVerify = $this->_authenticatorData->getBinary(); + $dataToVerify .= $clientDataHash; + + $coseAlgorithm = $this->_getCoseAlgorithm($this->_alg); + + // check certificate + return \openssl_verify($dataToVerify, $this->_signature, $publicKey, $coseAlgorithm->openssl) === 1; + } + + /** + * validates the certificate against root certificates + * @param array $rootCas + * @return boolean + * @throws WebAuthnException + */ + public function validateRootCertificate($rootCas) { + $chainC = $this->_createX5cChainFile(); + if ($chainC) { + $rootCas[] = $chainC; + } + + $v = \openssl_x509_checkpurpose($this->getCertificatePem(), -1, $rootCas); + if ($v === -1) { + throw new WebAuthnException('error on validating root certificate: ' . \openssl_error_string(), WebAuthnException::CERTIFICATE_NOT_TRUSTED); + } + return $v; + } +} + diff --git a/server/app-code/system/insecure_zone/php/Attestation/Format/AndroidSafetyNet.php b/server/app-code/system/insecure_zone/php/Attestation/Format/AndroidSafetyNet.php new file mode 100644 index 0000000..ba0db52 --- /dev/null +++ b/server/app-code/system/insecure_zone/php/Attestation/Format/AndroidSafetyNet.php @@ -0,0 +1,152 @@ +_attestationObject['attStmt']; + + if (!\array_key_exists('ver', $attStmt) || !$attStmt['ver']) { + throw new WebAuthnException('invalid Android Safety Net Format', WebAuthnException::INVALID_DATA); + } + + if (!\array_key_exists('response', $attStmt) || !($attStmt['response'] instanceof ByteBuffer)) { + throw new WebAuthnException('invalid Android Safety Net Format', WebAuthnException::INVALID_DATA); + } + + $response = $attStmt['response']->getBinaryString(); + + // Response is a JWS [RFC7515] object in Compact Serialization. + // JWSs have three segments separated by two period ('.') characters + $parts = \explode('.', $response); + unset ($response); + if (\count($parts) !== 3) { + throw new WebAuthnException('invalid JWS data', WebAuthnException::INVALID_DATA); + } + + $header = $this->_base64url_decode($parts[0]); + $payload = $this->_base64url_decode($parts[1]); + $this->_signature = $this->_base64url_decode($parts[2]); + $this->_signedValue = $parts[0] . '.' . $parts[1]; + unset ($parts); + + $header = \json_decode($header); + $payload = \json_decode($payload); + + if (!($header instanceof \stdClass)) { + throw new WebAuthnException('invalid JWS header', WebAuthnException::INVALID_DATA); + } + if (!($payload instanceof \stdClass)) { + throw new WebAuthnException('invalid JWS payload', WebAuthnException::INVALID_DATA); + } + + if (!isset($header->x5c) || !is_array($header->x5c) || count($header->x5c) === 0) { + throw new WebAuthnException('No X.509 signature in JWS Header', WebAuthnException::INVALID_DATA); + } + + // algorithm + if (!\in_array($header->alg, array('RS256', 'ES256'))) { + throw new WebAuthnException('invalid JWS algorithm ' . $header->alg, WebAuthnException::INVALID_DATA); + } + + $this->_x5c = \base64_decode($header->x5c[0]); + $this->_payload = $payload; + + if (count($header->x5c) > 1) { + for ($i=1; $ix5c); $i++) { + $this->_x5c_chain[] = \base64_decode($header->x5c[$i]); + } + unset ($i); + } + } + + /** + * ctsProfileMatch: A stricter verdict of device integrity. + * If the value of ctsProfileMatch is true, then the profile of the device running your app matches + * the profile of a device that has passed Android compatibility testing and + * has been approved as a Google-certified Android device. + * @return bool + */ + public function ctsProfileMatch() { + return isset($this->_payload->ctsProfileMatch) ? !!$this->_payload->ctsProfileMatch : false; + } + + + /* + * returns the key certificate in PEM format + * @return string + */ + public function getCertificatePem() { + return $this->_createCertificatePem($this->_x5c); + } + + /** + * @param string $clientDataHash + */ + public function validateAttestation($clientDataHash) { + $publicKey = \openssl_pkey_get_public($this->getCertificatePem()); + + // Verify that the nonce in the response is identical to the Base64 encoding + // of the SHA-256 hash of the concatenation of authenticatorData and clientDataHash. + if (empty($this->_payload->nonce) || $this->_payload->nonce !== \base64_encode(\hash('SHA256', $this->_authenticatorData->getBinary() . $clientDataHash, true))) { + throw new WebAuthnException('invalid nonce in JWS payload', WebAuthnException::INVALID_DATA); + } + + // Verify that attestationCert is issued to the hostname "attest.android.com" + $certInfo = \openssl_x509_parse($this->getCertificatePem()); + if (!\is_array($certInfo) || ($certInfo['subject']['CN'] ?? '') !== 'attest.android.com') { + throw new WebAuthnException('invalid certificate CN in JWS (' . ($certInfo['subject']['CN'] ?? '-'). ')', WebAuthnException::INVALID_DATA); + } + + // Verify that the basicIntegrity attribute in the payload of response is true. + if (empty($this->_payload->basicIntegrity)) { + throw new WebAuthnException('invalid basicIntegrity in payload', WebAuthnException::INVALID_DATA); + } + + // check certificate + return \openssl_verify($this->_signedValue, $this->_signature, $publicKey, OPENSSL_ALGO_SHA256) === 1; + } + + + /** + * validates the certificate against root certificates + * @param array $rootCas + * @return boolean + * @throws WebAuthnException + */ + public function validateRootCertificate($rootCas) { + $chainC = $this->_createX5cChainFile(); + if ($chainC) { + $rootCas[] = $chainC; + } + + $v = \openssl_x509_checkpurpose($this->getCertificatePem(), -1, $rootCas); + if ($v === -1) { + throw new WebAuthnException('error on validating root certificate: ' . \openssl_error_string(), WebAuthnException::CERTIFICATE_NOT_TRUSTED); + } + return $v; + } + + + /** + * decode base64 url + * @param string $data + * @return string + */ + private function _base64url_decode($data) { + return \base64_decode(\strtr($data, '-_', '+/') . \str_repeat('=', 3 - (3 + \strlen($data)) % 4)); + } +} + diff --git a/server/app-code/system/insecure_zone/php/Attestation/Format/Apple.php b/server/app-code/system/insecure_zone/php/Attestation/Format/Apple.php new file mode 100644 index 0000000..e4f38e0 --- /dev/null +++ b/server/app-code/system/insecure_zone/php/Attestation/Format/Apple.php @@ -0,0 +1,139 @@ +_attestationObject['attStmt']; + + + // certificate for validation + if (\array_key_exists('x5c', $attStmt) && \is_array($attStmt['x5c']) && \count($attStmt['x5c']) > 0) { + + // The attestation certificate attestnCert MUST be the first element in the array + $attestnCert = array_shift($attStmt['x5c']); + + if (!($attestnCert instanceof ByteBuffer)) { + throw new WebAuthnException('invalid x5c certificate', WebAuthnException::INVALID_DATA); + } + + $this->_x5c = $attestnCert->getBinaryString(); + + // certificate chain + foreach ($attStmt['x5c'] as $chain) { + if ($chain instanceof ByteBuffer) { + $this->_x5c_chain[] = $chain->getBinaryString(); + } + } + } else { + throw new WebAuthnException('invalid Apple attestation statement: missing x5c', WebAuthnException::INVALID_DATA); + } + } + + + /* + * returns the key certificate in PEM format + * @return string|null + */ + public function getCertificatePem() { + return $this->_createCertificatePem($this->_x5c); + } + + /** + * @param string $clientDataHash + */ + public function validateAttestation($clientDataHash) { + return $this->_validateOverX5c($clientDataHash); + } + + /** + * validates the certificate against root certificates + * @param array $rootCas + * @return boolean + * @throws WebAuthnException + */ + public function validateRootCertificate($rootCas) { + $chainC = $this->_createX5cChainFile(); + if ($chainC) { + $rootCas[] = $chainC; + } + + $v = \openssl_x509_checkpurpose($this->getCertificatePem(), -1, $rootCas); + if ($v === -1) { + throw new WebAuthnException('error on validating root certificate: ' . \openssl_error_string(), WebAuthnException::CERTIFICATE_NOT_TRUSTED); + } + return $v; + } + + /** + * validate if x5c is present + * @param string $clientDataHash + * @return bool + * @throws WebAuthnException + */ + protected function _validateOverX5c($clientDataHash) { + $publicKey = \openssl_pkey_get_public($this->getCertificatePem()); + + if ($publicKey === false) { + throw new WebAuthnException('invalid public key: ' . \openssl_error_string(), WebAuthnException::INVALID_PUBLIC_KEY); + } + + // Concatenate authenticatorData and clientDataHash to form nonceToHash. + $nonceToHash = $this->_authenticatorData->getBinary(); + $nonceToHash .= $clientDataHash; + + // Perform SHA-256 hash of nonceToHash to produce nonce + $nonce = hash('SHA256', $nonceToHash, true); + + $credCert = openssl_x509_read($this->getCertificatePem()); + if ($credCert === false) { + throw new WebAuthnException('invalid x5c certificate: ' . \openssl_error_string(), WebAuthnException::INVALID_DATA); + } + + $keyData = openssl_pkey_get_details(openssl_pkey_get_public($credCert)); + $key = is_array($keyData) && array_key_exists('key', $keyData) ? $keyData['key'] : null; + + + // Verify that nonce equals the value of the extension with OID ( 1.2.840.113635.100.8.2 ) in credCert. + $parsedCredCert = openssl_x509_parse($credCert); + $nonceExtension = $parsedCredCert['extensions']['1.2.840.113635.100.8.2'] ?? ''; + + // nonce padded by ASN.1 string: 30 24 A1 22 04 20 + // 30 — type tag indicating sequence + // 24 — 36 byte following + // A1 — Enumerated [1] + // 22 — 34 byte following + // 04 — type tag indicating octet string + // 20 — 32 byte following + + $asn1Padding = "\x30\x24\xA1\x22\x04\x20"; + if (substr($nonceExtension, 0, strlen($asn1Padding)) === $asn1Padding) { + $nonceExtension = substr($nonceExtension, strlen($asn1Padding)); + } + + if ($nonceExtension !== $nonce) { + throw new WebAuthnException('nonce doesn\'t equal the value of the extension with OID 1.2.840.113635.100.8.2', WebAuthnException::INVALID_DATA); + } + + // Verify that the credential public key equals the Subject Public Key of credCert. + $authKeyData = openssl_pkey_get_details(openssl_pkey_get_public($this->_authenticatorData->getPublicKeyPem())); + $authKey = is_array($authKeyData) && array_key_exists('key', $authKeyData) ? $authKeyData['key'] : null; + + if ($key === null || $key !== $authKey) { + throw new WebAuthnException('credential public key doesn\'t equal the Subject Public Key of credCert', WebAuthnException::INVALID_DATA); + } + + return true; + } + +} + diff --git a/server/app-code/system/insecure_zone/php/Attestation/Format/FormatBase.php b/server/app-code/system/insecure_zone/php/Attestation/Format/FormatBase.php new file mode 100644 index 0000000..765af85 --- /dev/null +++ b/server/app-code/system/insecure_zone/php/Attestation/Format/FormatBase.php @@ -0,0 +1,193 @@ +_attestationObject = $AttestionObject; + $this->_authenticatorData = $authenticatorData; + } + + /** + * + */ + public function __destruct() { + // delete X.509 chain certificate file after use + if ($this->_x5c_tempFile && \is_file($this->_x5c_tempFile)) { + \unlink($this->_x5c_tempFile); + } + } + + /** + * returns the certificate chain in PEM format + * @return string|null + */ + public function getCertificateChain() { + if ($this->_x5c_tempFile && \is_file($this->_x5c_tempFile)) { + return \file_get_contents($this->_x5c_tempFile); + } + return null; + } + + /** + * returns the key X.509 certificate in PEM format + * @return string + */ + public function getCertificatePem() { + // need to be overwritten + return null; + } + + /** + * checks validity of the signature + * @param string $clientDataHash + * @return bool + * @throws WebAuthnException + */ + public function validateAttestation($clientDataHash) { + // need to be overwritten + return false; + } + + /** + * validates the certificate against root certificates + * @param array $rootCas + * @return boolean + * @throws WebAuthnException + */ + public function validateRootCertificate($rootCas) { + // need to be overwritten + return false; + } + + + /** + * create a PEM encoded certificate with X.509 binary data + * @param string $x5c + * @return string + */ + protected function _createCertificatePem($x5c) { + $pem = '-----BEGIN CERTIFICATE-----' . "\n"; + $pem .= \chunk_split(\base64_encode($x5c), 64, "\n"); + $pem .= '-----END CERTIFICATE-----' . "\n"; + return $pem; + } + + /** + * creates a PEM encoded chain file + * @return type + */ + protected function _createX5cChainFile() { + $content = ''; + if (\is_array($this->_x5c_chain) && \count($this->_x5c_chain) > 0) { + foreach ($this->_x5c_chain as $x5c) { + $certInfo = \openssl_x509_parse($this->_createCertificatePem($x5c)); + + // check if certificate is self signed + if (\is_array($certInfo) && \is_array($certInfo['issuer']) && \is_array($certInfo['subject'])) { + $selfSigned = false; + + $subjectKeyIdentifier = $certInfo['extensions']['subjectKeyIdentifier'] ?? null; + $authorityKeyIdentifier = $certInfo['extensions']['authorityKeyIdentifier'] ?? null; + + if ($authorityKeyIdentifier && substr($authorityKeyIdentifier, 0, 6) === 'keyid:') { + $authorityKeyIdentifier = substr($authorityKeyIdentifier, 6); + } + if ($subjectKeyIdentifier && substr($subjectKeyIdentifier, 0, 6) === 'keyid:') { + $subjectKeyIdentifier = substr($subjectKeyIdentifier, 6); + } + + if (($subjectKeyIdentifier && !$authorityKeyIdentifier) || ($authorityKeyIdentifier && $authorityKeyIdentifier === $subjectKeyIdentifier)) { + $selfSigned = true; + } + + if (!$selfSigned) { + $content .= "\n" . $this->_createCertificatePem($x5c) . "\n"; + } + } + } + } + + if ($content) { + $this->_x5c_tempFile = \tempnam(\sys_get_temp_dir(), 'x5c_'); + if (\file_put_contents($this->_x5c_tempFile, $content) !== false) { + return $this->_x5c_tempFile; + } + } + + return null; + } + + + /** + * returns the name and openssl key for provided cose number. + * @param int $coseNumber + * @return \stdClass|null + */ + protected function _getCoseAlgorithm($coseNumber) { + // https://www.iana.org/assignments/cose/cose.xhtml#algorithms + $coseAlgorithms = array( + array( + 'hash' => 'SHA1', + 'openssl' => OPENSSL_ALGO_SHA1, + 'cose' => array( + -65535 // RS1 + )), + + array( + 'hash' => 'SHA256', + 'openssl' => OPENSSL_ALGO_SHA256, + 'cose' => array( + -257, // RS256 + -37, // PS256 + -7, // ES256 + 5 // HMAC256 + )), + + array( + 'hash' => 'SHA384', + 'openssl' => OPENSSL_ALGO_SHA384, + 'cose' => array( + -258, // RS384 + -38, // PS384 + -35, // ES384 + 6 // HMAC384 + )), + + array( + 'hash' => 'SHA512', + 'openssl' => OPENSSL_ALGO_SHA512, + 'cose' => array( + -259, // RS512 + -39, // PS512 + -36, // ES512 + 7 // HMAC512 + )) + ); + + foreach ($coseAlgorithms as $coseAlgorithm) { + if (\in_array($coseNumber, $coseAlgorithm['cose'], true)) { + $return = new \stdClass(); + $return->hash = $coseAlgorithm['hash']; + $return->openssl = $coseAlgorithm['openssl']; + return $return; + } + } + + return null; + } +} diff --git a/server/app-code/system/insecure_zone/php/Attestation/Format/None.php b/server/app-code/system/insecure_zone/php/Attestation/Format/None.php new file mode 100644 index 0000000..ba95e40 --- /dev/null +++ b/server/app-code/system/insecure_zone/php/Attestation/Format/None.php @@ -0,0 +1,41 @@ +_attestationObject['attStmt']; + + if (!\array_key_exists('alg', $attStmt) || $this->_getCoseAlgorithm($attStmt['alg']) === null) { + throw new WebAuthnException('unsupported alg: ' . $attStmt['alg'], WebAuthnException::INVALID_DATA); + } + + if (!\array_key_exists('sig', $attStmt) || !\is_object($attStmt['sig']) || !($attStmt['sig'] instanceof ByteBuffer)) { + throw new WebAuthnException('no signature found', WebAuthnException::INVALID_DATA); + } + + $this->_alg = $attStmt['alg']; + $this->_signature = $attStmt['sig']->getBinaryString(); + + // certificate for validation + if (\array_key_exists('x5c', $attStmt) && \is_array($attStmt['x5c']) && \count($attStmt['x5c']) > 0) { + + // The attestation certificate attestnCert MUST be the first element in the array + $attestnCert = array_shift($attStmt['x5c']); + + if (!($attestnCert instanceof ByteBuffer)) { + throw new WebAuthnException('invalid x5c certificate', WebAuthnException::INVALID_DATA); + } + + $this->_x5c = $attestnCert->getBinaryString(); + + // certificate chain + foreach ($attStmt['x5c'] as $chain) { + if ($chain instanceof ByteBuffer) { + $this->_x5c_chain[] = $chain->getBinaryString(); + } + } + } + } + + + /* + * returns the key certificate in PEM format + * @return string|null + */ + public function getCertificatePem() { + if (!$this->_x5c) { + return null; + } + return $this->_createCertificatePem($this->_x5c); + } + + /** + * @param string $clientDataHash + */ + public function validateAttestation($clientDataHash) { + if ($this->_x5c) { + return $this->_validateOverX5c($clientDataHash); + } else { + return $this->_validateSelfAttestation($clientDataHash); + } + } + + /** + * validates the certificate against root certificates + * @param array $rootCas + * @return boolean + * @throws WebAuthnException + */ + public function validateRootCertificate($rootCas) { + if (!$this->_x5c) { + return false; + } + + $chainC = $this->_createX5cChainFile(); + if ($chainC) { + $rootCas[] = $chainC; + } + + $v = \openssl_x509_checkpurpose($this->getCertificatePem(), -1, $rootCas); + if ($v === -1) { + throw new WebAuthnException('error on validating root certificate: ' . \openssl_error_string(), WebAuthnException::CERTIFICATE_NOT_TRUSTED); + } + return $v; + } + + /** + * validate if x5c is present + * @param string $clientDataHash + * @return bool + * @throws WebAuthnException + */ + protected function _validateOverX5c($clientDataHash) { + $publicKey = \openssl_pkey_get_public($this->getCertificatePem()); + + if ($publicKey === false) { + throw new WebAuthnException('invalid public key: ' . \openssl_error_string(), WebAuthnException::INVALID_PUBLIC_KEY); + } + + // Verify that sig is a valid signature over the concatenation of authenticatorData and clientDataHash + // using the attestation public key in attestnCert with the algorithm specified in alg. + $dataToVerify = $this->_authenticatorData->getBinary(); + $dataToVerify .= $clientDataHash; + + $coseAlgorithm = $this->_getCoseAlgorithm($this->_alg); + + // check certificate + return \openssl_verify($dataToVerify, $this->_signature, $publicKey, $coseAlgorithm->openssl) === 1; + } + + /** + * validate if self attestation is in use + * @param string $clientDataHash + * @return bool + */ + protected function _validateSelfAttestation($clientDataHash) { + // Verify that sig is a valid signature over the concatenation of authenticatorData and clientDataHash + // using the credential public key with alg. + $dataToVerify = $this->_authenticatorData->getBinary(); + $dataToVerify .= $clientDataHash; + + $publicKey = $this->_authenticatorData->getPublicKeyPem(); + + // check certificate + return \openssl_verify($dataToVerify, $this->_signature, $publicKey, OPENSSL_ALGO_SHA256) === 1; + } +} + diff --git a/server/app-code/system/insecure_zone/php/Attestation/Format/Tpm.php b/server/app-code/system/insecure_zone/php/Attestation/Format/Tpm.php new file mode 100644 index 0000000..338cd45 --- /dev/null +++ b/server/app-code/system/insecure_zone/php/Attestation/Format/Tpm.php @@ -0,0 +1,180 @@ +_attestationObject['attStmt']; + + if (!\array_key_exists('ver', $attStmt) || $attStmt['ver'] !== '2.0') { + throw new WebAuthnException('invalid tpm version: ' . $attStmt['ver'], WebAuthnException::INVALID_DATA); + } + + if (!\array_key_exists('alg', $attStmt) || $this->_getCoseAlgorithm($attStmt['alg']) === null) { + throw new WebAuthnException('unsupported alg: ' . $attStmt['alg'], WebAuthnException::INVALID_DATA); + } + + if (!\array_key_exists('sig', $attStmt) || !\is_object($attStmt['sig']) || !($attStmt['sig'] instanceof ByteBuffer)) { + throw new WebAuthnException('signature not found', WebAuthnException::INVALID_DATA); + } + + if (!\array_key_exists('certInfo', $attStmt) || !\is_object($attStmt['certInfo']) || !($attStmt['certInfo'] instanceof ByteBuffer)) { + throw new WebAuthnException('certInfo not found', WebAuthnException::INVALID_DATA); + } + + if (!\array_key_exists('pubArea', $attStmt) || !\is_object($attStmt['pubArea']) || !($attStmt['pubArea'] instanceof ByteBuffer)) { + throw new WebAuthnException('pubArea not found', WebAuthnException::INVALID_DATA); + } + + $this->_alg = $attStmt['alg']; + $this->_signature = $attStmt['sig']->getBinaryString(); + $this->_certInfo = $attStmt['certInfo']; + $this->_pubArea = $attStmt['pubArea']; + + // certificate for validation + if (\array_key_exists('x5c', $attStmt) && \is_array($attStmt['x5c']) && \count($attStmt['x5c']) > 0) { + + // The attestation certificate attestnCert MUST be the first element in the array + $attestnCert = array_shift($attStmt['x5c']); + + if (!($attestnCert instanceof ByteBuffer)) { + throw new WebAuthnException('invalid x5c certificate', WebAuthnException::INVALID_DATA); + } + + $this->_x5c = $attestnCert->getBinaryString(); + + // certificate chain + foreach ($attStmt['x5c'] as $chain) { + if ($chain instanceof ByteBuffer) { + $this->_x5c_chain[] = $chain->getBinaryString(); + } + } + + } else { + throw new WebAuthnException('no x5c certificate found', WebAuthnException::INVALID_DATA); + } + } + + + /* + * returns the key certificate in PEM format + * @return string|null + */ + public function getCertificatePem() { + if (!$this->_x5c) { + return null; + } + return $this->_createCertificatePem($this->_x5c); + } + + /** + * @param string $clientDataHash + */ + public function validateAttestation($clientDataHash) { + return $this->_validateOverX5c($clientDataHash); + } + + /** + * validates the certificate against root certificates + * @param array $rootCas + * @return boolean + * @throws WebAuthnException + */ + public function validateRootCertificate($rootCas) { + if (!$this->_x5c) { + return false; + } + + $chainC = $this->_createX5cChainFile(); + if ($chainC) { + $rootCas[] = $chainC; + } + + $v = \openssl_x509_checkpurpose($this->getCertificatePem(), -1, $rootCas); + if ($v === -1) { + throw new WebAuthnException('error on validating root certificate: ' . \openssl_error_string(), WebAuthnException::CERTIFICATE_NOT_TRUSTED); + } + return $v; + } + + /** + * validate if x5c is present + * @param string $clientDataHash + * @return bool + * @throws WebAuthnException + */ + protected function _validateOverX5c($clientDataHash) { + $publicKey = \openssl_pkey_get_public($this->getCertificatePem()); + + if ($publicKey === false) { + throw new WebAuthnException('invalid public key: ' . \openssl_error_string(), WebAuthnException::INVALID_PUBLIC_KEY); + } + + // Concatenate authenticatorData and clientDataHash to form attToBeSigned. + $attToBeSigned = $this->_authenticatorData->getBinary(); + $attToBeSigned .= $clientDataHash; + + // Validate that certInfo is valid: + + // Verify that magic is set to TPM_GENERATED_VALUE. + if ($this->_certInfo->getBytes(0, 4) !== $this->_TPM_GENERATED_VALUE) { + throw new WebAuthnException('tpm magic not TPM_GENERATED_VALUE', WebAuthnException::INVALID_DATA); + } + + // Verify that type is set to TPM_ST_ATTEST_CERTIFY. + if ($this->_certInfo->getBytes(4, 2) !== $this->_TPM_ST_ATTEST_CERTIFY) { + throw new WebAuthnException('tpm type not TPM_ST_ATTEST_CERTIFY', WebAuthnException::INVALID_DATA); + } + + $offset = 6; + $qualifiedSigner = $this->_tpmReadLengthPrefixed($this->_certInfo, $offset); + $extraData = $this->_tpmReadLengthPrefixed($this->_certInfo, $offset); + $coseAlg = $this->_getCoseAlgorithm($this->_alg); + + // Verify that extraData is set to the hash of attToBeSigned using the hash algorithm employed in "alg". + if ($extraData->getBinaryString() !== \hash($coseAlg->hash, $attToBeSigned, true)) { + throw new WebAuthnException('certInfo:extraData not hash of attToBeSigned', WebAuthnException::INVALID_DATA); + } + + // Verify the sig is a valid signature over certInfo using the attestation + // public key in aikCert with the algorithm specified in alg. + return \openssl_verify($this->_certInfo->getBinaryString(), $this->_signature, $publicKey, $coseAlg->openssl) === 1; + } + + + /** + * returns next part of ByteBuffer + * @param ByteBuffer $buffer + * @param int $offset + * @return ByteBuffer + */ + protected function _tpmReadLengthPrefixed(ByteBuffer $buffer, &$offset) { + $len = $buffer->getUint16Val($offset); + $data = $buffer->getBytes($offset + 2, $len); + $offset += (2 + $len); + + return new ByteBuffer($data); + } + +} + diff --git a/server/app-code/system/insecure_zone/php/Attestation/Format/U2f.php b/server/app-code/system/insecure_zone/php/Attestation/Format/U2f.php new file mode 100644 index 0000000..2b51ba8 --- /dev/null +++ b/server/app-code/system/insecure_zone/php/Attestation/Format/U2f.php @@ -0,0 +1,93 @@ +_attestationObject['attStmt']; + + if (\array_key_exists('alg', $attStmt) && $attStmt['alg'] !== $this->_alg) { + throw new WebAuthnException('u2f only accepts algorithm -7 ("ES256"), but got ' . $attStmt['alg'], WebAuthnException::INVALID_DATA); + } + + if (!\array_key_exists('sig', $attStmt) || !\is_object($attStmt['sig']) || !($attStmt['sig'] instanceof ByteBuffer)) { + throw new WebAuthnException('no signature found', WebAuthnException::INVALID_DATA); + } + + if (!\array_key_exists('x5c', $attStmt) || !\is_array($attStmt['x5c']) || \count($attStmt['x5c']) !== 1) { + throw new WebAuthnException('invalid x5c certificate', WebAuthnException::INVALID_DATA); + } + + if (!\is_object($attStmt['x5c'][0]) || !($attStmt['x5c'][0] instanceof ByteBuffer)) { + throw new WebAuthnException('invalid x5c certificate', WebAuthnException::INVALID_DATA); + } + + $this->_signature = $attStmt['sig']->getBinaryString(); + $this->_x5c = $attStmt['x5c'][0]->getBinaryString(); + } + + + /* + * returns the key certificate in PEM format + * @return string + */ + public function getCertificatePem() { + $pem = '-----BEGIN CERTIFICATE-----' . "\n"; + $pem .= \chunk_split(\base64_encode($this->_x5c), 64, "\n"); + $pem .= '-----END CERTIFICATE-----' . "\n"; + return $pem; + } + + /** + * @param string $clientDataHash + */ + public function validateAttestation($clientDataHash) { + $publicKey = \openssl_pkey_get_public($this->getCertificatePem()); + + if ($publicKey === false) { + throw new WebAuthnException('invalid public key: ' . \openssl_error_string(), WebAuthnException::INVALID_PUBLIC_KEY); + } + + // Let verificationData be the concatenation of (0x00 || rpIdHash || clientDataHash || credentialId || publicKeyU2F) + $dataToVerify = "\x00"; + $dataToVerify .= $this->_authenticatorData->getRpIdHash(); + $dataToVerify .= $clientDataHash; + $dataToVerify .= $this->_authenticatorData->getCredentialId(); + $dataToVerify .= $this->_authenticatorData->getPublicKeyU2F(); + + $coseAlgorithm = $this->_getCoseAlgorithm($this->_alg); + + // check certificate + return \openssl_verify($dataToVerify, $this->_signature, $publicKey, $coseAlgorithm->openssl) === 1; + } + + /** + * validates the certificate against root certificates + * @param array $rootCas + * @return boolean + * @throws WebAuthnException + */ + public function validateRootCertificate($rootCas) { + $chainC = $this->_createX5cChainFile(); + if ($chainC) { + $rootCas[] = $chainC; + } + + $v = \openssl_x509_checkpurpose($this->getCertificatePem(), -1, $rootCas); + if ($v === -1) { + throw new WebAuthnException('error on validating root certificate: ' . \openssl_error_string(), WebAuthnException::CERTIFICATE_NOT_TRUSTED); + } + return $v; + } +} diff --git a/server/app-code/system/insecure_zone/php/Binary/ByteBuffer.php b/server/app-code/system/insecure_zone/php/Binary/ByteBuffer.php new file mode 100644 index 0000000..861ed60 --- /dev/null +++ b/server/app-code/system/insecure_zone/php/Binary/ByteBuffer.php @@ -0,0 +1,300 @@ +_data = (string)$binaryData; + $this->_length = \strlen($binaryData); + } + + + // ----------------------- + // PUBLIC STATIC + // ----------------------- + + /** + * create a ByteBuffer from a base64 url encoded string + * @param string $base64url + * @return ByteBuffer + */ + public static function fromBase64Url($base64url): ByteBuffer { + $bin = self::_base64url_decode($base64url); + if ($bin === false) { + throw new WebAuthnException('ByteBuffer: Invalid base64 url string', WebAuthnException::BYTEBUFFER); + } + return new ByteBuffer($bin); + } + + /** + * create a ByteBuffer from a base64 url encoded string + * @param string $hex + * @return ByteBuffer + */ + public static function fromHex($hex): ByteBuffer { + $bin = \hex2bin($hex); + if ($bin === false) { + throw new WebAuthnException('ByteBuffer: Invalid hex string', WebAuthnException::BYTEBUFFER); + } + return new ByteBuffer($bin); + } + + /** + * create a random ByteBuffer + * @param string $length + * @return ByteBuffer + */ + public static function randomBuffer($length): ByteBuffer { + if (\function_exists('random_bytes')) { // >PHP 7.0 + return new ByteBuffer(\random_bytes($length)); + + } else if (\function_exists('openssl_random_pseudo_bytes')) { + return new ByteBuffer(\openssl_random_pseudo_bytes($length)); + + } else { + throw new WebAuthnException('ByteBuffer: cannot generate random bytes', WebAuthnException::BYTEBUFFER); + } + } + + // ----------------------- + // PUBLIC + // ----------------------- + + public function getBytes($offset, $length): string { + if ($offset < 0 || $length < 0 || ($offset + $length > $this->_length)) { + throw new WebAuthnException('ByteBuffer: Invalid offset or length', WebAuthnException::BYTEBUFFER); + } + return \substr($this->_data, $offset, $length); + } + + public function getByteVal($offset): int { + if ($offset < 0 || $offset >= $this->_length) { + throw new WebAuthnException('ByteBuffer: Invalid offset', WebAuthnException::BYTEBUFFER); + } + return \ord(\substr($this->_data, $offset, 1)); + } + + public function getJson($jsonFlags=0) { + $data = \json_decode($this->getBinaryString(), null, 512, $jsonFlags); + if (\json_last_error() !== JSON_ERROR_NONE) { + throw new WebAuthnException(\json_last_error_msg(), WebAuthnException::BYTEBUFFER); + } + return $data; + } + + public function getLength(): int { + return $this->_length; + } + + public function getUint16Val($offset) { + if ($offset < 0 || ($offset + 2) > $this->_length) { + throw new WebAuthnException('ByteBuffer: Invalid offset', WebAuthnException::BYTEBUFFER); + } + return unpack('n', $this->_data, $offset)[1]; + } + + public function getUint32Val($offset) { + if ($offset < 0 || ($offset + 4) > $this->_length) { + throw new WebAuthnException('ByteBuffer: Invalid offset', WebAuthnException::BYTEBUFFER); + } + $val = unpack('N', $this->_data, $offset)[1]; + + // Signed integer overflow causes signed negative numbers + if ($val < 0) { + throw new WebAuthnException('ByteBuffer: Value out of integer range.', WebAuthnException::BYTEBUFFER); + } + return $val; + } + + public function getUint64Val($offset) { + if (PHP_INT_SIZE < 8) { + throw new WebAuthnException('ByteBuffer: 64-bit values not supported by this system', WebAuthnException::BYTEBUFFER); + } + if ($offset < 0 || ($offset + 8) > $this->_length) { + throw new WebAuthnException('ByteBuffer: Invalid offset', WebAuthnException::BYTEBUFFER); + } + $val = unpack('J', $this->_data, $offset)[1]; + + // Signed integer overflow causes signed negative numbers + if ($val < 0) { + throw new WebAuthnException('ByteBuffer: Value out of integer range.', WebAuthnException::BYTEBUFFER); + } + + return $val; + } + + public function getHalfFloatVal($offset) { + //FROM spec pseudo decode_half(unsigned char *halfp) + $half = $this->getUint16Val($offset); + + $exp = ($half >> 10) & 0x1f; + $mant = $half & 0x3ff; + + if ($exp === 0) { + $val = $mant * (2 ** -24); + } elseif ($exp !== 31) { + $val = ($mant + 1024) * (2 ** ($exp - 25)); + } else { + $val = ($mant === 0) ? INF : NAN; + } + + return ($half & 0x8000) ? -$val : $val; + } + + public function getFloatVal($offset) { + if ($offset < 0 || ($offset + 4) > $this->_length) { + throw new WebAuthnException('ByteBuffer: Invalid offset', WebAuthnException::BYTEBUFFER); + } + return unpack('G', $this->_data, $offset)[1]; + } + + public function getDoubleVal($offset) { + if ($offset < 0 || ($offset + 8) > $this->_length) { + throw new WebAuthnException('ByteBuffer: Invalid offset', WebAuthnException::BYTEBUFFER); + } + return unpack('E', $this->_data, $offset)[1]; + } + + /** + * @return string + */ + public function getBinaryString(): string { + return $this->_data; + } + + /** + * @param string|ByteBuffer $buffer + * @return bool + */ + public function equals($buffer): bool { + if (is_object($buffer) && $buffer instanceof ByteBuffer) { + return $buffer->getBinaryString() === $this->getBinaryString(); + + } else if (is_string($buffer)) { + return $buffer === $this->getBinaryString(); + } + + return false; + } + + /** + * @return string + */ + public function getHex(): string { + return \bin2hex($this->_data); + } + + /** + * @return bool + */ + public function isEmpty(): bool { + return $this->_length === 0; + } + + + /** + * jsonSerialize interface + * return binary data in RFC 1342-Like serialized string + * @return string + */ + public function jsonSerialize(): string { + if (ByteBuffer::$useBase64UrlEncoding) { + return self::_base64url_encode($this->_data); + + } else { + return '=?BINARY?B?' . \base64_encode($this->_data) . '?='; + } + } + + /** + * Serializable-Interface + * @return string + */ + public function serialize(): string { + return \serialize($this->_data); + } + + /** + * Serializable-Interface + * @param string $serialized + */ + public function unserialize($serialized) { + $this->_data = \unserialize($serialized); + $this->_length = \strlen($this->_data); + } + + /** + * (PHP 8 deprecates Serializable-Interface) + * @return array + */ + public function __serialize(): array { + return [ + 'data' => \serialize($this->_data) + ]; + } + + /** + * object to string + * @return string + */ + public function __toString(): string { + return $this->getHex(); + } + + /** + * (PHP 8 deprecates Serializable-Interface) + * @param array $data + * @return void + */ + public function __unserialize($data) { + if ($data && isset($data['data'])) { + $this->_data = \unserialize($data['data']); + $this->_length = \strlen($this->_data); + } + } + + // ----------------------- + // PROTECTED STATIC + // ----------------------- + + /** + * base64 url decoding + * @param string $data + * @return string + */ + protected static function _base64url_decode($data): string { + return \base64_decode(\strtr($data, '-_', '+/') . \str_repeat('=', 3 - (3 + \strlen($data)) % 4)); + } + + /** + * base64 url encoding + * @param string $data + * @return string + */ + protected static function _base64url_encode($data): string { + return \rtrim(\strtr(\base64_encode($data), '+/', '-_'), '='); + } +} diff --git a/server/app-code/system/insecure_zone/php/CBOR/CborDecoder.php b/server/app-code/system/insecure_zone/php/CBOR/CborDecoder.php new file mode 100644 index 0000000..e6b5427 --- /dev/null +++ b/server/app-code/system/insecure_zone/php/CBOR/CborDecoder.php @@ -0,0 +1,220 @@ +getLength()) { + throw new WebAuthnException('Unused bytes after data item.', WebAuthnException::CBOR); + } + return $result; + } + + /** + * @param ByteBuffer|string $bufOrBin + * @param int $startOffset + * @param int|null $endOffset + * @return mixed + */ + public static function decodeInPlace($bufOrBin, $startOffset, &$endOffset = null) { + $buf = $bufOrBin instanceof ByteBuffer ? $bufOrBin : new ByteBuffer($bufOrBin); + + $offset = $startOffset; + $data = self::_parseItem($buf, $offset); + $endOffset = $offset; + return $data; + } + + // --------------------- + // protected + // --------------------- + + /** + * @param ByteBuffer $buf + * @param int $offset + * @return mixed + */ + protected static function _parseItem(ByteBuffer $buf, &$offset) { + $first = $buf->getByteVal($offset++); + $type = $first >> 5; + $val = $first & 0b11111; + + if ($type === self::CBOR_MAJOR_FLOAT_SIMPLE) { + return self::_parseFloatSimple($val, $buf, $offset); + } + + $val = self::_parseExtraLength($val, $buf, $offset); + + return self::_parseItemData($type, $val, $buf, $offset); + } + + protected static function _parseFloatSimple($val, ByteBuffer $buf, &$offset) { + switch ($val) { + case 24: + $val = $buf->getByteVal($offset); + $offset++; + return self::_parseSimple($val); + + case 25: + $floatValue = $buf->getHalfFloatVal($offset); + $offset += 2; + return $floatValue; + + case 26: + $floatValue = $buf->getFloatVal($offset); + $offset += 4; + return $floatValue; + + case 27: + $floatValue = $buf->getDoubleVal($offset); + $offset += 8; + return $floatValue; + + case 28: + case 29: + case 30: + throw new WebAuthnException('Reserved value used.', WebAuthnException::CBOR); + + case 31: + throw new WebAuthnException('Indefinite length is not supported.', WebAuthnException::CBOR); + } + + return self::_parseSimple($val); + } + + /** + * @param int $val + * @return mixed + * @throws WebAuthnException + */ + protected static function _parseSimple($val) { + if ($val === 20) { + return false; + } + if ($val === 21) { + return true; + } + if ($val === 22) { + return null; + } + throw new WebAuthnException(sprintf('Unsupported simple value %d.', $val), WebAuthnException::CBOR); + } + + protected static function _parseExtraLength($val, ByteBuffer $buf, &$offset) { + switch ($val) { + case 24: + $val = $buf->getByteVal($offset); + $offset++; + break; + + case 25: + $val = $buf->getUint16Val($offset); + $offset += 2; + break; + + case 26: + $val = $buf->getUint32Val($offset); + $offset += 4; + break; + + case 27: + $val = $buf->getUint64Val($offset); + $offset += 8; + break; + + case 28: + case 29: + case 30: + throw new WebAuthnException('Reserved value used.', WebAuthnException::CBOR); + + case 31: + throw new WebAuthnException('Indefinite length is not supported.', WebAuthnException::CBOR); + } + + return $val; + } + + protected static function _parseItemData($type, $val, ByteBuffer $buf, &$offset) { + switch ($type) { + case self::CBOR_MAJOR_UNSIGNED_INT: // uint + return $val; + + case self::CBOR_MAJOR_NEGATIVE_INT: + return -1 - $val; + + case self::CBOR_MAJOR_BYTE_STRING: + $data = $buf->getBytes($offset, $val); + $offset += $val; + return new ByteBuffer($data); // bytes + + case self::CBOR_MAJOR_TEXT_STRING: + $data = $buf->getBytes($offset, $val); + $offset += $val; + return $data; // UTF-8 + + case self::CBOR_MAJOR_ARRAY: + return self::_parseArray($buf, $offset, $val); + + case self::CBOR_MAJOR_MAP: + return self::_parseMap($buf, $offset, $val); + + case self::CBOR_MAJOR_TAG: + return self::_parseItem($buf, $offset); // 1 embedded data item + } + + // This should never be reached + throw new WebAuthnException(sprintf('Unknown major type %d.', $type), WebAuthnException::CBOR); + } + + protected static function _parseMap(ByteBuffer $buf, &$offset, $count) { + $map = array(); + + for ($i = 0; $i < $count; $i++) { + $mapKey = self::_parseItem($buf, $offset); + $mapVal = self::_parseItem($buf, $offset); + + if (!\is_int($mapKey) && !\is_string($mapKey)) { + throw new WebAuthnException('Can only use strings or integers as map keys', WebAuthnException::CBOR); + } + + $map[$mapKey] = $mapVal; // todo dup + } + return $map; + } + + protected static function _parseArray(ByteBuffer $buf, &$offset, $count) { + $arr = array(); + for ($i = 0; $i < $count; $i++) { + $arr[] = self::_parseItem($buf, $offset); + } + + return $arr; + } +} diff --git a/server/app-code/system/insecure_zone/php/WebAuthn.php b/server/app-code/system/insecure_zone/php/WebAuthn.php new file mode 100644 index 0000000..0da0aa6 --- /dev/null +++ b/server/app-code/system/insecure_zone/php/WebAuthn.php @@ -0,0 +1,677 @@ +_rpName = $rpName; + $this->_rpId = $rpId; + $this->_rpIdHash = \hash('sha256', $rpId, true); + ByteBuffer::$useBase64UrlEncoding = !!$useBase64UrlEncoding; + $supportedFormats = array('android-key', 'android-safetynet', 'apple', 'fido-u2f', 'none', 'packed', 'tpm'); + + if (!\function_exists('\openssl_open')) { + throw new WebAuthnException('OpenSSL-Module not installed'); + } + + if (!\in_array('SHA256', \array_map('\strtoupper', \openssl_get_md_methods()))) { + throw new WebAuthnException('SHA256 not supported by this openssl installation.'); + } + + // default: all format + if (!is_array($allowedFormats)) { + $allowedFormats = $supportedFormats; + } + $this->_formats = $allowedFormats; + + // validate formats + $invalidFormats = \array_diff($this->_formats, $supportedFormats); + if (!$this->_formats || $invalidFormats) { + throw new WebAuthnException('invalid formats on construct: ' . implode(', ', $invalidFormats)); + } + } + + /** + * add a root certificate to verify new registrations + * @param string $path file path of / directory with root certificates + * @param array|null $certFileExtensions if adding a direction, all files with provided extension are added. default: pem, crt, cer, der + */ + public function addRootCertificates($path, $certFileExtensions=null) { + if (!\is_array($this->_caFiles)) { + $this->_caFiles = []; + } + if ($certFileExtensions === null) { + $certFileExtensions = array('pem', 'crt', 'cer', 'der'); + } + $path = \rtrim(\trim($path), '\\/'); + if (\is_dir($path)) { + foreach (\scandir($path) as $ca) { + if (\is_file($path . DIRECTORY_SEPARATOR . $ca) && \in_array(\strtolower(\pathinfo($ca, PATHINFO_EXTENSION)), $certFileExtensions)) { + $this->addRootCertificates($path . DIRECTORY_SEPARATOR . $ca); + } + } + } else if (\is_file($path) && !\in_array(\realpath($path), $this->_caFiles)) { + $this->_caFiles[] = \realpath($path); + } + } + + /** + * Returns the generated challenge to save for later validation + * @return ByteBuffer + */ + public function getChallenge() { + return $this->_challenge; + } + + /** + * generates the object for a key registration + * provide this data to navigator.credentials.create + * @param string $userId + * @param string $userName + * @param string $userDisplayName + * @param int $timeout timeout in seconds + * @param bool|string $requireResidentKey 'required', if the key should be stored by the authentication device + * Valid values: + * true = required + * false = preferred + * string 'required' 'preferred' 'discouraged' + * @param bool|string $requireUserVerification indicates that you require user verification and will fail the operation + * if the response does not have the UV flag set. + * Valid values: + * true = required + * false = preferred + * string 'required' 'preferred' 'discouraged' + * @param bool|null $crossPlatformAttachment true for cross-platform devices (eg. fido usb), + * false for platform devices (eg. windows hello, android safetynet), + * null for both + * @param array $excludeCredentialIds a array of ids, which are already registered, to prevent re-registration + * @return \stdClass + */ + public function getCreateArgs($userId, $userName, $userDisplayName, $timeout=20, $requireResidentKey=false, $requireUserVerification=false, $crossPlatformAttachment=null, $excludeCredentialIds=[]) { + + $args = new \stdClass(); + $args->publicKey = new \stdClass(); + + // relying party + $args->publicKey->rp = new \stdClass(); + $args->publicKey->rp->name = $this->_rpName; + $args->publicKey->rp->id = $this->_rpId; + + $args->publicKey->authenticatorSelection = new \stdClass(); + $args->publicKey->authenticatorSelection->userVerification = 'preferred'; + + // validate User Verification Requirement + if (\is_bool($requireUserVerification)) { + $args->publicKey->authenticatorSelection->userVerification = $requireUserVerification ? 'required' : 'preferred'; + + } else if (\is_string($requireUserVerification) && \in_array(\strtolower($requireUserVerification), ['required', 'preferred', 'discouraged'])) { + $args->publicKey->authenticatorSelection->userVerification = \strtolower($requireUserVerification); + } + + // validate Resident Key Requirement + if (\is_bool($requireResidentKey) && $requireResidentKey) { + $args->publicKey->authenticatorSelection->requireResidentKey = true; + $args->publicKey->authenticatorSelection->residentKey = 'required'; + + } else if (\is_string($requireResidentKey) && \in_array(\strtolower($requireResidentKey), ['required', 'preferred', 'discouraged'])) { + $requireResidentKey = \strtolower($requireResidentKey); + $args->publicKey->authenticatorSelection->residentKey = $requireResidentKey; + $args->publicKey->authenticatorSelection->requireResidentKey = $requireResidentKey === 'required'; + } + + // filte authenticators attached with the specified authenticator attachment modality + if (\is_bool($crossPlatformAttachment)) { + $args->publicKey->authenticatorSelection->authenticatorAttachment = $crossPlatformAttachment ? 'cross-platform' : 'platform'; + } + + // user + $args->publicKey->user = new \stdClass(); + $args->publicKey->user->id = new ByteBuffer($userId); // binary + $args->publicKey->user->name = $userName; + $args->publicKey->user->displayName = $userDisplayName; + + // supported algorithms + $args->publicKey->pubKeyCredParams = []; + + if (function_exists('sodium_crypto_sign_verify_detached') || \in_array('ed25519', \openssl_get_curve_names(), true)) { + $tmp = new \stdClass(); + $tmp->type = 'public-key'; + $tmp->alg = -8; // EdDSA + $args->publicKey->pubKeyCredParams[] = $tmp; + unset ($tmp); + } + + if (\in_array('prime256v1', \openssl_get_curve_names(), true)) { + $tmp = new \stdClass(); + $tmp->type = 'public-key'; + $tmp->alg = -7; // ES256 + $args->publicKey->pubKeyCredParams[] = $tmp; + unset ($tmp); + } + + $tmp = new \stdClass(); + $tmp->type = 'public-key'; + $tmp->alg = -257; // RS256 + $args->publicKey->pubKeyCredParams[] = $tmp; + unset ($tmp); + + // if there are root certificates added, we need direct attestation to validate + // against the root certificate. If there are no root-certificates added, + // anonymization ca are also accepted, because we can't validate the root anyway. + $attestation = 'indirect'; + if (\is_array($this->_caFiles)) { + $attestation = 'direct'; + } + + $args->publicKey->attestation = \count($this->_formats) === 1 && \in_array('none', $this->_formats) ? 'none' : $attestation; + $args->publicKey->extensions = new \stdClass(); + $args->publicKey->extensions->exts = true; + $args->publicKey->timeout = $timeout * 1000; // microseconds + $args->publicKey->challenge = $this->_createChallenge(); // binary + + //prevent re-registration by specifying existing credentials + $args->publicKey->excludeCredentials = []; + + if (is_array($excludeCredentialIds)) { + foreach ($excludeCredentialIds as $id) { + $tmp = new \stdClass(); + $tmp->id = $id instanceof ByteBuffer ? $id : new ByteBuffer($id); // binary + $tmp->type = 'public-key'; + $tmp->transports = array('usb', 'nfc', 'ble', 'hybrid', 'internal'); + $args->publicKey->excludeCredentials[] = $tmp; + unset ($tmp); + } + } + + return $args; + } + + /** + * generates the object for key validation + * Provide this data to navigator.credentials.get + * @param array $credentialIds binary + * @param int $timeout timeout in seconds + * @param bool $allowUsb allow removable USB + * @param bool $allowNfc allow Near Field Communication (NFC) + * @param bool $allowBle allow Bluetooth + * @param bool $allowHybrid allow a combination of (often separate) data-transport and proximity mechanisms. + * @param bool $allowInternal allow client device-specific transport. These authenticators are not removable from the client device. + * @param bool|string $requireUserVerification indicates that you require user verification and will fail the operation + * if the response does not have the UV flag set. + * Valid values: + * true = required + * false = preferred + * string 'required' 'preferred' 'discouraged' + * @return \stdClass + */ + public function getGetArgs($credentialIds=[], $timeout=20, $allowUsb=true, $allowNfc=true, $allowBle=true, $allowHybrid=true, $allowInternal=true, $requireUserVerification=false) { + + // validate User Verification Requirement + if (\is_bool($requireUserVerification)) { + $requireUserVerification = $requireUserVerification ? 'required' : 'preferred'; + } else if (\is_string($requireUserVerification) && \in_array(\strtolower($requireUserVerification), ['required', 'preferred', 'discouraged'])) { + $requireUserVerification = \strtolower($requireUserVerification); + } else { + $requireUserVerification = 'preferred'; + } + + $args = new \stdClass(); + $args->publicKey = new \stdClass(); + $args->publicKey->timeout = $timeout * 1000; // microseconds + $args->publicKey->challenge = $this->_createChallenge(); // binary + $args->publicKey->userVerification = $requireUserVerification; + $args->publicKey->rpId = $this->_rpId; + + if (\is_array($credentialIds) && \count($credentialIds) > 0) { + $args->publicKey->allowCredentials = []; + + foreach ($credentialIds as $id) { + $tmp = new \stdClass(); + $tmp->id = $id instanceof ByteBuffer ? $id : new ByteBuffer($id); // binary + $tmp->transports = []; + + if ($allowUsb) { + $tmp->transports[] = 'usb'; + } + if ($allowNfc) { + $tmp->transports[] = 'nfc'; + } + if ($allowBle) { + $tmp->transports[] = 'ble'; + } + if ($allowHybrid) { + $tmp->transports[] = 'hybrid'; + } + if ($allowInternal) { + $tmp->transports[] = 'internal'; + } + + $tmp->type = 'public-key'; + $args->publicKey->allowCredentials[] = $tmp; + unset ($tmp); + } + } + + return $args; + } + + /** + * returns the new signature counter value. + * returns null if there is no counter + * @return ?int + */ + public function getSignatureCounter() { + return \is_int($this->_signatureCounter) ? $this->_signatureCounter : null; + } + + /** + * process a create request and returns data to save for future logins + * @param string $clientDataJSON binary from browser + * @param string $attestationObject binary from browser + * @param string|ByteBuffer $challenge binary used challange + * @param bool $requireUserVerification true, if the device must verify user (e.g. by biometric data or pin) + * @param bool $requireUserPresent false, if the device must NOT check user presence (e.g. by pressing a button) + * @param bool $failIfRootMismatch false, if there should be no error thrown if root certificate doesn't match + * @param bool $requireCtsProfileMatch false, if you don't want to check if the device is approved as a Google-certified Android device. + * @return \stdClass + * @throws WebAuthnException + */ + public function processCreate($clientDataJSON, $attestationObject, $challenge, $requireUserVerification=false, $requireUserPresent=true, $failIfRootMismatch=true, $requireCtsProfileMatch=true) { + $clientDataHash = \hash('sha256', $clientDataJSON, true); + $clientData = \json_decode($clientDataJSON); + $challenge = $challenge instanceof ByteBuffer ? $challenge : new ByteBuffer($challenge); + + // security: https://www.w3.org/TR/webauthn/#registering-a-new-credential + + // 2. Let C, the client data claimed as collected during the credential creation, + // be the result of running an implementation-specific JSON parser on JSONtext. + if (!\is_object($clientData)) { + throw new WebAuthnException('invalid client data', WebAuthnException::INVALID_DATA); + } + + // 3. Verify that the value of C.type is webauthn.create. + if (!\property_exists($clientData, 'type') || $clientData->type !== 'webauthn.create') { + throw new WebAuthnException('invalid type', WebAuthnException::INVALID_TYPE); + } + + // 4. Verify that the value of C.challenge matches the challenge that was sent to the authenticator in the create() call. + if (!\property_exists($clientData, 'challenge') || ByteBuffer::fromBase64Url($clientData->challenge)->getBinaryString() !== $challenge->getBinaryString()) { + throw new WebAuthnException('invalid challenge', WebAuthnException::INVALID_CHALLENGE); + } + + // 5. Verify that the value of C.origin matches the Relying Party's origin. + if (!\property_exists($clientData, 'origin') || !$this->_checkOrigin($clientData->origin)) { + throw new WebAuthnException('invalid origin', WebAuthnException::INVALID_ORIGIN); + } + + // Attestation + $attestationObject = new Attestation\AttestationObject($attestationObject, $this->_formats); + + // 9. Verify that the RP ID hash in authData is indeed the SHA-256 hash of the RP ID expected by the RP. + if (!$attestationObject->validateRpIdHash($this->_rpIdHash)) { + throw new WebAuthnException('invalid rpId hash', WebAuthnException::INVALID_RELYING_PARTY); + } + + // 14. Verify that attStmt is a correct attestation statement, conveying a valid attestation signature + if (!$attestationObject->validateAttestation($clientDataHash)) { + throw new WebAuthnException('invalid certificate signature', WebAuthnException::INVALID_SIGNATURE); + } + + // Android-SafetyNet: if required, check for Compatibility Testing Suite (CTS). + if ($requireCtsProfileMatch && $attestationObject->getAttestationFormat() instanceof Attestation\Format\AndroidSafetyNet) { + if (!$attestationObject->getAttestationFormat()->ctsProfileMatch()) { + throw new WebAuthnException('invalid ctsProfileMatch: device is not approved as a Google-certified Android device.', WebAuthnException::ANDROID_NOT_TRUSTED); + } + } + + // 15. If validation is successful, obtain a list of acceptable trust anchors + $rootValid = is_array($this->_caFiles) ? $attestationObject->validateRootCertificate($this->_caFiles) : null; + if ($failIfRootMismatch && is_array($this->_caFiles) && !$rootValid) { + throw new WebAuthnException('invalid root certificate', WebAuthnException::CERTIFICATE_NOT_TRUSTED); + } + + // 10. Verify that the User Present bit of the flags in authData is set. + $userPresent = $attestationObject->getAuthenticatorData()->getUserPresent(); + if ($requireUserPresent && !$userPresent) { + throw new WebAuthnException('user not present during authentication', WebAuthnException::USER_PRESENT); + } + + // 11. If user verification is required for this registration, verify that the User Verified bit of the flags in authData is set. + $userVerified = $attestationObject->getAuthenticatorData()->getUserVerified(); + if ($requireUserVerification && !$userVerified) { + throw new WebAuthnException('user not verified during authentication', WebAuthnException::USER_VERIFICATED); + } + + $signCount = $attestationObject->getAuthenticatorData()->getSignCount(); + if ($signCount > 0) { + $this->_signatureCounter = $signCount; + } + + // prepare data to store for future logins + $data = new \stdClass(); + $data->rpId = $this->_rpId; + $data->attestationFormat = $attestationObject->getAttestationFormatName(); + $data->credentialId = $attestationObject->getAuthenticatorData()->getCredentialId(); + $data->credentialPublicKey = $attestationObject->getAuthenticatorData()->getPublicKeyPem(); + $data->certificateChain = $attestationObject->getCertificateChain(); + $data->certificate = $attestationObject->getCertificatePem(); + $data->certificateIssuer = $attestationObject->getCertificateIssuer(); + $data->certificateSubject = $attestationObject->getCertificateSubject(); + $data->signatureCounter = $this->_signatureCounter; + $data->AAGUID = $attestationObject->getAuthenticatorData()->getAAGUID(); + $data->rootValid = $rootValid; + $data->userPresent = $userPresent; + $data->userVerified = $userVerified; + return $data; + } + + + /** + * process a get request + * @param string $clientDataJSON binary from browser + * @param string $authenticatorData binary from browser + * @param string $signature binary from browser + * @param string $credentialPublicKey string PEM-formated public key from used credentialId + * @param string|ByteBuffer $challenge binary from used challange + * @param int $prevSignatureCnt signature count value of the last login + * @param bool $requireUserVerification true, if the device must verify user (e.g. by biometric data or pin) + * @param bool $requireUserPresent true, if the device must check user presence (e.g. by pressing a button) + * @return boolean true if get is successful + * @throws WebAuthnException + */ + public function processGet($clientDataJSON, $authenticatorData, $signature, $credentialPublicKey, $challenge, $prevSignatureCnt=null, $requireUserVerification=false, $requireUserPresent=true) { + $authenticatorObj = new Attestation\AuthenticatorData($authenticatorData); + $clientDataHash = \hash('sha256', $clientDataJSON, true); + $clientData = \json_decode($clientDataJSON); + $challenge = $challenge instanceof ByteBuffer ? $challenge : new ByteBuffer($challenge); + + // https://www.w3.org/TR/webauthn/#verifying-assertion + + // 1. If the allowCredentials option was given when this authentication ceremony was initiated, + // verify that credential.id identifies one of the public key credentials that were listed in allowCredentials. + // -> TO BE VERIFIED BY IMPLEMENTATION + + // 2. If credential.response.userHandle is present, verify that the user identified + // by this value is the owner of the public key credential identified by credential.id. + // -> TO BE VERIFIED BY IMPLEMENTATION + + // 3. Using credential’s id attribute (or the corresponding rawId, if base64url encoding is + // inappropriate for your use case), look up the corresponding credential public key. + // -> TO BE LOOKED UP BY IMPLEMENTATION + + // 5. Let JSONtext be the result of running UTF-8 decode on the value of cData. + if (!\is_object($clientData)) { + throw new WebAuthnException('invalid client data', WebAuthnException::INVALID_DATA); + } + + // 7. Verify that the value of C.type is the string webauthn.get. + if (!\property_exists($clientData, 'type') || $clientData->type !== 'webauthn.get') { + throw new WebAuthnException('invalid type', WebAuthnException::INVALID_TYPE); + } + + // 8. Verify that the value of C.challenge matches the challenge that was sent to the + // authenticator in the PublicKeyCredentialRequestOptions passed to the get() call. + if (!\property_exists($clientData, 'challenge') || ByteBuffer::fromBase64Url($clientData->challenge)->getBinaryString() !== $challenge->getBinaryString()) { + throw new WebAuthnException('invalid challenge', WebAuthnException::INVALID_CHALLENGE); + } + + // 9. Verify that the value of C.origin matches the Relying Party's origin. + if (!\property_exists($clientData, 'origin') || !$this->_checkOrigin($clientData->origin)) { + throw new WebAuthnException('invalid origin', WebAuthnException::INVALID_ORIGIN); + } + + // 11. Verify that the rpIdHash in authData is the SHA-256 hash of the RP ID expected by the Relying Party. + if ($authenticatorObj->getRpIdHash() !== $this->_rpIdHash) { + throw new WebAuthnException('invalid rpId hash', WebAuthnException::INVALID_RELYING_PARTY); + } + + // 12. Verify that the User Present bit of the flags in authData is set + if ($requireUserPresent && !$authenticatorObj->getUserPresent()) { + throw new WebAuthnException('user not present during authentication', WebAuthnException::USER_PRESENT); + } + + // 13. If user verification is required for this assertion, verify that the User Verified bit of the flags in authData is set. + if ($requireUserVerification && !$authenticatorObj->getUserVerified()) { + throw new WebAuthnException('user not verificated during authentication', WebAuthnException::USER_VERIFICATED); + } + + // 14. Verify the values of the client extension outputs + // (extensions not implemented) + + // 16. Using the credential public key looked up in step 3, verify that sig is a valid signature + // over the binary concatenation of authData and hash. + $dataToVerify = ''; + $dataToVerify .= $authenticatorData; + $dataToVerify .= $clientDataHash; + + if (!$this->_verifySignature($dataToVerify, $signature, $credentialPublicKey)) { + throw new WebAuthnException('invalid signature', WebAuthnException::INVALID_SIGNATURE); + } + + $signatureCounter = $authenticatorObj->getSignCount(); + if ($signatureCounter !== 0) { + $this->_signatureCounter = $signatureCounter; + } + + // 17. If either of the signature counter value authData.signCount or + // previous signature count is nonzero, and if authData.signCount + // less than or equal to previous signature count, it's a signal + // that the authenticator may be cloned + if ($prevSignatureCnt !== null) { + if ($signatureCounter !== 0 || $prevSignatureCnt !== 0) { + if ($prevSignatureCnt >= $signatureCounter) { + throw new WebAuthnException('signature counter not valid', WebAuthnException::SIGNATURE_COUNTER); + } + } + } + + return true; + } + + /** + * Downloads root certificates from FIDO Alliance Metadata Service (MDS) to a specific folder + * https://fidoalliance.org/metadata/ + * @param string $certFolder Folder path to save the certificates in PEM format. + * @param bool $deleteCerts delete certificates in the target folder before adding the new ones. + * @return int number of cetificates + * @throws WebAuthnException + */ + public function queryFidoMetaDataService($certFolder, $deleteCerts=true) { + $url = 'https://mds.fidoalliance.org/'; + $raw = null; + if (\function_exists('curl_init')) { + $ch = \curl_init($url); + \curl_setopt($ch, CURLOPT_HEADER, false); + \curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + \curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); + \curl_setopt($ch, CURLOPT_USERAGENT, 'github.com/lbuchs/WebAuthn - A simple PHP WebAuthn server library'); + $raw = \curl_exec($ch); + \curl_close($ch); + } else { + $raw = \file_get_contents($url); + } + + $certFolder = \rtrim(\realpath($certFolder), '\\/'); + if (!is_dir($certFolder)) { + throw new WebAuthnException('Invalid folder path for query FIDO Alliance Metadata Service'); + } + + if (!\is_string($raw)) { + throw new WebAuthnException('Unable to query FIDO Alliance Metadata Service'); + } + + $jwt = \explode('.', $raw); + if (\count($jwt) !== 3) { + throw new WebAuthnException('Invalid JWT from FIDO Alliance Metadata Service'); + } + + if ($deleteCerts) { + foreach (\scandir($certFolder) as $ca) { + if (\substr($ca, -4) === '.pem') { + if (\unlink($certFolder . DIRECTORY_SEPARATOR . $ca) === false) { + throw new WebAuthnException('Cannot delete certs in folder for FIDO Alliance Metadata Service'); + } + } + } + } + + list($header, $payload, $hash) = $jwt; + $payload = Binary\ByteBuffer::fromBase64Url($payload)->getJson(); + + $count = 0; + if (\is_object($payload) && \property_exists($payload, 'entries') && \is_array($payload->entries)) { + foreach ($payload->entries as $entry) { + if (\is_object($entry) && \property_exists($entry, 'metadataStatement') && \is_object($entry->metadataStatement)) { + $description = $entry->metadataStatement->description ?? null; + $attestationRootCertificates = $entry->metadataStatement->attestationRootCertificates ?? null; + + if ($description && $attestationRootCertificates) { + + // create filename + $certFilename = \preg_replace('/[^a-z0-9]/i', '_', $description); + $certFilename = \trim(\preg_replace('/\_{2,}/i', '_', $certFilename),'_') . '.pem'; + $certFilename = \strtolower($certFilename); + + // add certificate + $certContent = $description . "\n"; + $certContent .= \str_repeat('-', \mb_strlen($description)) . "\n"; + + foreach ($attestationRootCertificates as $attestationRootCertificate) { + $attestationRootCertificate = \str_replace(["\n", "\r", ' '], '', \trim($attestationRootCertificate)); + $count++; + $certContent .= "\n-----BEGIN CERTIFICATE-----\n"; + $certContent .= \chunk_split($attestationRootCertificate, 64, "\n"); + $certContent .= "-----END CERTIFICATE-----\n"; + } + + if (\file_put_contents($certFolder . DIRECTORY_SEPARATOR . $certFilename, $certContent) === false) { + throw new WebAuthnException('unable to save certificate from FIDO Alliance Metadata Service'); + } + } + } + } + } + + return $count; + } + + // ----------------------------------------------- + // PRIVATE + // ----------------------------------------------- + + /** + * checks if the origin matchs the RP ID + * @param string $origin + * @return boolean + * @throws WebAuthnException + */ + private function _checkOrigin($origin) { + // https://www.w3.org/TR/webauthn/#rp-id + + // The origin's scheme must be https + if ($this->_rpId !== 'localhost' && \parse_url($origin, PHP_URL_SCHEME) !== 'https') { + return false; + } + + // extract host from origin + $host = \parse_url($origin, PHP_URL_HOST); + $host = \trim($host, '.'); + + // The RP ID must be equal to the origin's effective domain, or a registrable + // domain suffix of the origin's effective domain. + return \preg_match('/' . \preg_quote($this->_rpId) . '$/i', $host) === 1; + } + + /** + * generates a new challange + * @param int $length + * @return string + * @throws WebAuthnException + */ + private function _createChallenge($length = 32) { + if (!$this->_challenge) { + $this->_challenge = ByteBuffer::randomBuffer($length); + } + return $this->_challenge; + } + + /** + * check if the signature is valid. + * @param string $dataToVerify + * @param string $signature + * @param string $credentialPublicKey PEM format + * @return bool + */ + private function _verifySignature($dataToVerify, $signature, $credentialPublicKey) { + + // Use Sodium to verify EdDSA 25519 as its not yet supported by openssl + if (\function_exists('sodium_crypto_sign_verify_detached') && !\in_array('ed25519', \openssl_get_curve_names(), true)) { + $pkParts = []; + if (\preg_match('/BEGIN PUBLIC KEY\-+(?:\s|\n|\r)+([^\-]+)(?:\s|\n|\r)*\-+END PUBLIC KEY/i', $credentialPublicKey, $pkParts)) { + $rawPk = \base64_decode($pkParts[1]); + + // 30 = der sequence + // 2a = length 42 byte + // 30 = der sequence + // 05 = lenght 5 byte + // 06 = der OID + // 03 = OID length 3 byte + // 2b 65 70 = OID 1.3.101.112 curveEd25519 (EdDSA 25519 signature algorithm) + // 03 = der bit string + // 21 = length 33 byte + // 00 = null padding + // [...] = 32 byte x-curve + $okpPrefix = "\x30\x2a\x30\x05\x06\x03\x2b\x65\x70\x03\x21\x00"; + + if ($rawPk && \strlen($rawPk) === 44 && \substr($rawPk,0, \strlen($okpPrefix)) === $okpPrefix) { + $publicKeyXCurve = \substr($rawPk, \strlen($okpPrefix)); + + return \sodium_crypto_sign_verify_detached($signature, $dataToVerify, $publicKeyXCurve); + } + } + } + + // verify with openSSL + $publicKey = \openssl_pkey_get_public($credentialPublicKey); + if ($publicKey === false) { + throw new WebAuthnException('public key invalid', WebAuthnException::INVALID_PUBLIC_KEY); + } + + return \openssl_verify($dataToVerify, $signature, $publicKey, OPENSSL_ALGO_SHA256) === 1; + } +} diff --git a/server/app-code/system/insecure_zone/php/WebAuthnException.php b/server/app-code/system/insecure_zone/php/WebAuthnException.php new file mode 100644 index 0000000..f27eeec --- /dev/null +++ b/server/app-code/system/insecure_zone/php/WebAuthnException.php @@ -0,0 +1,28 @@ +connect_error) { + $success=0; + die("Connection failed: " . $conn->connect_error); +} + +try { + session_start(); + + // read get argument and post body + $fn = filter_input(INPUT_GET, 'fn'); + $requireResidentKey = !!filter_input(INPUT_GET, 'requireResidentKey'); + $userVerification = filter_input(INPUT_GET, 'userVerification', FILTER_SANITIZE_SPECIAL_CHARS); + + $userId = filter_input(INPUT_GET, 'userId', FILTER_SANITIZE_SPECIAL_CHARS); + $userName = filter_input(INPUT_GET, 'userName', FILTER_SANITIZE_SPECIAL_CHARS); + $userDisplayName = filter_input(INPUT_GET, 'userDisplayName', FILTER_SANITIZE_SPECIAL_CHARS); + + $userId = preg_replace('/[^0-9a-f]/i', '', $userId); + $userName = preg_replace('/[^0-9a-z]/i', '', $_SESSION["username"]); + $userDisplayName = preg_replace('/[^0-9a-z öüäéèàÖÜÄÉÈÀÂÊÎÔÛâêîôû]/i', '', $userDisplayName); + + $post = trim(file_get_contents('php://input')); + if ($post) { + $post = json_decode($post, null, 512, JSON_THROW_ON_ERROR); + } + + if ($fn !== 'getStoredDataHtml') { + + // Formats + $formats = []; + //if (filter_input(INPUT_GET, 'fmt_android-key')) { + $formats[] = 'android-key'; + //} + ///if (filter_input(INPUT_GET, 'fmt_android-safetynet')) { + $formats[] = 'android-safetynet'; + //} + //if (filter_input(INPUT_GET, 'fmt_apple')) { + $formats[] = 'apple'; + //} + //if (filter_input(INPUT_GET, 'fmt_fido-u2f')) { + $formats[] = 'fido-u2f'; + //} + //if (filter_input(INPUT_GET, 'fmt_none')) { + $formats[] = 'none'; + //} + //if (filter_input(INPUT_GET, 'fmt_packed')) { + $formats[] = 'packed'; + //} + //if (filter_input(INPUT_GET, 'fmt_tpm')) { + $formats[] = 'tpm'; + //} + + $rpId=$_SERVER['SERVER_NAME']; + + $typeUsb = true; + $typeNfc = true; + $typeBle = true; + $typeInt = true; + $typeHyb = true; + + // cross-platform: true, if type internal is not allowed + // false, if only internal is allowed + // null, if internal and cross-platform is allowed + $crossPlatformAttachment = null; + if (($typeUsb || $typeNfc || $typeBle || $typeHyb) && !$typeInt) { + $crossPlatformAttachment = true; + + } else if (!$typeUsb && !$typeNfc && !$typeBle && !$typeHyb && $typeInt) { + $crossPlatformAttachment = false; + } + + + // new Instance of the server library. + // make sure that $rpId is the domain name. + $WebAuthn = new lbuchs\WebAuthn\WebAuthn('WebAuthn Library', $rpId, $formats); + + // add root certificates to validate new registrations + //if (filter_input(INPUT_GET, 'solo')) { + $WebAuthn->addRootCertificates('rootCertificates/solo.pem'); + //} + //if (filter_input(INPUT_GET, 'apple')) { + $WebAuthn->addRootCertificates('rootCertificates/apple.pem'); + //} + //if (filter_input(INPUT_GET, 'yubico')) { + $WebAuthn->addRootCertificates('rootCertificates/yubico.pem'); + //} + //if (filter_input(INPUT_GET, 'hypersecu')) { + $WebAuthn->addRootCertificates('rootCertificates/hypersecu.pem'); + //} + //if (filter_input(INPUT_GET, 'google')) { + $WebAuthn->addRootCertificates('rootCertificates/globalSign.pem'); + $WebAuthn->addRootCertificates('rootCertificates/googleHardware.pem'); + //} + //if (filter_input(INPUT_GET, 'microsoft')) { + $WebAuthn->addRootCertificates('rootCertificates/microsoftTpmCollection.pem'); + //} + //if (filter_input(INPUT_GET, 'mds')) { + $WebAuthn->addRootCertificates('rootCertificates/mds'); + //} + + } + + // Handle different functions + if ($fn === 'getCreateArgs') { + $createArgs = $WebAuthn->getCreateArgs(\hex2bin($userId), $userName, $userDisplayName, 60*4, $requireResidentKey, $userVerification, $crossPlatformAttachment); + + header('Content-Type: application/json'); + print(json_encode($createArgs)); + + // save challange to session. you have to deliver it to processGet later. + $_SESSION['challenge'] = $WebAuthn->getChallenge(); + + } else if ($fn === 'getGetArgs') { + $ids = []; + + if ($requireResidentKey) { + if (!isset($_SESSION['registrations']) || !is_array($_SESSION['registrations']) || count($_SESSION['registrations']) === 0) { + throw new Exception('we do not have any registrations in session to check the registration'); + } + + } else { + // load registrations from session stored there by processCreate. + // normaly you have to load the credential Id's for a username + // from the database. + if (isset($_SESSION['registrations']) && is_array($_SESSION['registrations'])) { + foreach ($_SESSION['registrations'] as $reg) { + if ($reg->userId === $userId) { + $ids[] = $reg->credentialId; + } + } + } + + if (count($ids) === 0) { + throw new Exception('no registrations in session for userId ' . $userId); + } + } + + $getArgs = $WebAuthn->getGetArgs($ids, 60*4, $typeUsb, $typeNfc, $typeBle, $typeHyb, $typeInt, $userVerification); + + header('Content-Type: application/json'); + print(json_encode($getArgs)); + + // save challange to session. you have to deliver it to processGet later. + $_SESSION['challenge'] = $WebAuthn->getChallenge(); + } else if ($fn === 'processCreate') { + // Process create + $challenge = $_SESSION['challenge']; + $clientDataJSON = base64_decode($post->clientDataJSON); + $attestationObject = base64_decode($post->attestationObject); + + // Process create and store data in the database + $data = $WebAuthn->processCreate($clientDataJSON, $attestationObject, $challenge, $userVerification === 'required', true, false); + + // add user infos + $data->userId = $userId; + $data->userName = $userName; + $data->userDisplayName = $userDisplayName; + + // Store registration data in the database + $stmt = $conn->prepare("UPDATE users set user_hex_id = ?, credential_id = ?, public_key = ?, counter = ? WHERE username = ?"); + //$stmt = $conn->prepare("INSERT INTO users (user_hex_id, credential_id, public_key, counter) VALUES (?, ?, ?, ?)"); + //var_dump($data); + $stmt->execute([$userId, $data->credentialId, $data->credentialPublicKey, $data->signatureCounter,$userName]); + + $msg = 'registration success.'; + $return = new stdClass(); + $return->success = true; + $return->msg = $msg; + header('Content-Type: application/json'); + print(json_encode($return)); + log_action("PASSWD::PASSKEY::ADD","User ".$_SESSION["username"]." added a passkey.",$_SESSION["id"]); + } + +} catch (Throwable $ex) { + $return = new stdClass(); + $return->success = false; + $return->msg = $ex->getMessage(); + + header('Content-Type: application/json'); + print(json_encode($return)); +} +?> \ No newline at end of file diff --git a/server/app-code/system/insecure_zone/php/create_admin_backend.php b/server/app-code/system/insecure_zone/php/create_admin_backend.php new file mode 100644 index 0000000..269374f --- /dev/null +++ b/server/app-code/system/insecure_zone/php/create_admin_backend.php @@ -0,0 +1,187 @@ +connect_error) { + $success=0; + die("Connection failed: " . $conn->connect_error); +} +try { + session_start(); + + // read get argument and post body + $fn = filter_input(INPUT_GET, 'fn'); + $requireResidentKey = !!filter_input(INPUT_GET, 'requireResidentKey'); + $userVerification = filter_input(INPUT_GET, 'userVerification', FILTER_SANITIZE_SPECIAL_CHARS); + + $userId = filter_input(INPUT_GET, 'userId', FILTER_SANITIZE_SPECIAL_CHARS); + $userName = filter_input(INPUT_GET, 'userName', FILTER_SANITIZE_SPECIAL_CHARS); + $userDisplayName = filter_input(INPUT_GET, 'userDisplayName', FILTER_SANITIZE_SPECIAL_CHARS); + + $userId = preg_replace('/[^0-9a-f]/i', '', $userId); + $userName = preg_replace('/[^0-9a-z]/i', '', $userName); + $userDisplayName = preg_replace('/[^0-9a-z öüäéèàÖÜÄÉÈÀÂÊÎÔÛâêîôû]/i', '', $userDisplayName); + + $post = trim(file_get_contents('php://input')); + if ($post) { + $post = json_decode($post, null, 512, JSON_THROW_ON_ERROR); + } + + if ($fn !== 'getStoredDataHtml') { + + // Formats + $formats = []; + //if (filter_input(INPUT_GET, 'fmt_android-key')) { + $formats[] = 'android-key'; + //} + ///if (filter_input(INPUT_GET, 'fmt_android-safetynet')) { + $formats[] = 'android-safetynet'; + //} + //if (filter_input(INPUT_GET, 'fmt_apple')) { + $formats[] = 'apple'; + //} + //if (filter_input(INPUT_GET, 'fmt_fido-u2f')) { + $formats[] = 'fido-u2f'; + //} + //if (filter_input(INPUT_GET, 'fmt_none')) { + $formats[] = 'none'; + //} + //if (filter_input(INPUT_GET, 'fmt_packed')) { + $formats[] = 'packed'; + //} + //if (filter_input(INPUT_GET, 'fmt_tpm')) { + $formats[] = 'tpm'; + //} + + $rpId=$_SERVER['SERVER_NAME']; + + $typeUsb = true; + $typeNfc = true; + $typeBle = true; + $typeInt = true; + $typeHyb = true; + + // cross-platform: true, if type internal is not allowed + // false, if only internal is allowed + // null, if internal and cross-platform is allowed + $crossPlatformAttachment = null; + if (($typeUsb || $typeNfc || $typeBle || $typeHyb) && !$typeInt) { + $crossPlatformAttachment = true; + + } else if (!$typeUsb && !$typeNfc && !$typeBle && !$typeHyb && $typeInt) { + $crossPlatformAttachment = false; + } + + + // new Instance of the server library. + // make sure that $rpId is the domain name. + $WebAuthn = new lbuchs\WebAuthn\WebAuthn('WebAuthn Library', $rpId, $formats); + + // add root certificates to validate new registrations + //if (filter_input(INPUT_GET, 'solo')) { + $WebAuthn->addRootCertificates('rootCertificates/solo.pem'); + //} + //if (filter_input(INPUT_GET, 'apple')) { + $WebAuthn->addRootCertificates('rootCertificates/apple.pem'); + //} + //if (filter_input(INPUT_GET, 'yubico')) { + $WebAuthn->addRootCertificates('rootCertificates/yubico.pem'); + //} + //if (filter_input(INPUT_GET, 'hypersecu')) { + $WebAuthn->addRootCertificates('rootCertificates/hypersecu.pem'); + //} + //if (filter_input(INPUT_GET, 'google')) { + $WebAuthn->addRootCertificates('rootCertificates/globalSign.pem'); + $WebAuthn->addRootCertificates('rootCertificates/googleHardware.pem'); + //} + //if (filter_input(INPUT_GET, 'microsoft')) { + $WebAuthn->addRootCertificates('rootCertificates/microsoftTpmCollection.pem'); + //} + //if (filter_input(INPUT_GET, 'mds')) { + $WebAuthn->addRootCertificates('rootCertificates/mds'); + //} + + } + + // Handle different functions + if ($fn === 'getCreateArgs') { + $createArgs = $WebAuthn->getCreateArgs(\hex2bin($userId), $userName, $userDisplayName, 60*4, $requireResidentKey, $userVerification, $crossPlatformAttachment); + + header('Content-Type: application/json'); + print(json_encode($createArgs)); + + // save challange to session. you have to deliver it to processGet later. + $_SESSION['challenge'] = $WebAuthn->getChallenge(); + + } else if ($fn === 'getGetArgs') { + $ids = []; + + if ($requireResidentKey) { + if (!isset($_SESSION['registrations']) || !is_array($_SESSION['registrations']) || count($_SESSION['registrations']) === 0) { + throw new Exception('we do not have any registrations in session to check the registration'); + } + + } else { + // load registrations from session stored there by processCreate. + // normaly you have to load the credential Id's for a username + // from the database. + if (isset($_SESSION['registrations']) && is_array($_SESSION['registrations'])) { + foreach ($_SESSION['registrations'] as $reg) { + if ($reg->userId === $userId) { + $ids[] = $reg->credentialId; + } + } + } + + if (count($ids) === 0) { + throw new Exception('no registrations in session for userId ' . $userId); + } + } + + $getArgs = $WebAuthn->getGetArgs($ids, 60*4, $typeUsb, $typeNfc, $typeBle, $typeHyb, $typeInt, $userVerification); + + header('Content-Type: application/json'); + print(json_encode($getArgs)); + + // save challange to session. you have to deliver it to processGet later. + $_SESSION['challenge'] = $WebAuthn->getChallenge(); + } else if ($fn === 'processCreate') { + // Process create + $challenge = $_SESSION['challenge']; + $clientDataJSON = base64_decode($post->clientDataJSON); + $attestationObject = base64_decode($post->attestationObject); + + // Process create and store data in the database + $data = $WebAuthn->processCreate($clientDataJSON, $attestationObject, $challenge, $userVerification === 'required', true, false); + + // add user infos + $data->userId = $userId; + $data->userName = $userName; + $data->userDisplayName = $userDisplayName; + + // Store registration data in the database + $stmt = $conn->prepare("UPDATE users set user_hex_id = ?, credential_id = ?, public_key = ?, counter = ? WHERE username = ?"); + //$stmt = $conn->prepare("INSERT INTO users (user_hex_id, credential_id, public_key, counter) VALUES (?, ?, ?, ?)"); + //var_dump($data); + $stmt->execute([$userId, $data->credentialId, $data->credentialPublicKey, $data->signatureCounter,$userName]); + + $msg = 'registration success.'; + $return = new stdClass(); + $return->success = true; + $return->msg = $msg; + header('Content-Type: application/json'); + print(json_encode($return)); + } + +} catch (Throwable $ex) { + $return = new stdClass(); + $return->success = false; + $return->msg = $ex->getMessage(); + + header('Content-Type: application/json'); + print(json_encode($return)); +} +?> \ No newline at end of file diff --git a/server/app-code/system/insecure_zone/php/login.php b/server/app-code/system/insecure_zone/php/login.php new file mode 100644 index 0000000..3d59ed7 --- /dev/null +++ b/server/app-code/system/insecure_zone/php/login.php @@ -0,0 +1,366 @@ + + + + + + + + Cyberhex login page + + + + +
+
+
+
+
+
+

Login to Cyberhex

+
+
+
+
+ + +
+
+ + +
+
+
+
+ +
+
+
Or
+
+
+ +
+
+
+

+ + + connect_error) { + die("Connection failed: " . $conn->connect_error); + } + $sql = "SELECT * FROM users WHERE username = ?"; + $stmt = $conn->prepare($sql); + $stmt->bind_param("s", $username); + + // Execute the statement + $stmt->execute(); + + // Get the result + $result = $stmt->get_result(); + + // Check if the user exists and verify the password + if ($result->num_rows > 0) { + $row = $result->fetch_assoc(); + if($row["allow_pw_login"]==1){ + if (password_verify($password, $row['password'])) { + $_SESSION["username"]=htmlspecialchars($username); + $_SESSION["login"]=true; + $_SESSION["perms"]=$row["perms"]; + $_SESSION["email"]=$row["email"]; + $_SESSION["id"]=$row["id"]; + $_SESSION["telegram_id"]=$row["telegram_id"]; + $_SESSION["allow_pw_login"]=$row["allow_pw_login"]; + $_SESSION["send_login_message"]=$row["send_login_message"]; + $_SESSION["use_2fa"]=$row["use_2fa"]; + if($_SESSION["use_2fa"]=="1"){ + unset($_SESSION["login"]); //set the login state to false + $_SESSION["2fa_auth"]=true; + $pin=mt_rand(100000, 999999); + $_SESSION["pin"]=$pin; + $ip = $_SERVER['REMOTE_ADDR']; + send_to_user("[2FA-Pin]\nHello $username\nHere is your pin to log into cyberhex: $pin. If you did not try to log in please take steps to secure your account!\nIP: $ip\n",$username); + //send the user to 2fa auth page + echo ''; + }else{ + log_action("LOGIN::SUCCESS","User ".$_SESSION["username"]." logged in with password.",$_SESSION["id"]); + if($_SESSION["send_login_message"]=="1"){ + $ip = $_SERVER['REMOTE_ADDR']; + $username=$row["username"]; + send_to_user("[LOGIN WARNING]\nHello $username\nSomebody has logged into Cyberhex with your account.\nIf this was you, you can ignore this message. Else please take steps to secure your account!\nIP: $ip\n",$username); + } + echo ''; + } + exit(); + } else { + log_action("LOGIN::FAILURE","User ".$username." entered wrong password.",1); + echo ''; + } + } + else{ + echo ''; + } + } else { + log_action("LOGIN::FAILURE","User ".$username." entered unknown username.",1); + echo ''; + } + + + // Close the connection + $stmt->close(); + $conn->close(); + } + ?> + +
+
+
+
+
+
+ + diff --git a/server/app-code/system/insecure_zone/php/login_backend.php b/server/app-code/system/insecure_zone/php/login_backend.php new file mode 100644 index 0000000..8fe0a0f --- /dev/null +++ b/server/app-code/system/insecure_zone/php/login_backend.php @@ -0,0 +1,220 @@ +connect_error) { + $success=0; + die("Connection failed: " . $conn->connect_error); +} +include "../../../api/php/notifications/sendmessage.php"; //to send user notification on login +include "../../../api/php/log/add_server_entry.php"; //to log things +try { + session_start(); + + // read get argument and post body + $fn = filter_input(INPUT_GET, 'fn'); + $requireResidentKey = !!filter_input(INPUT_GET, 'requireResidentKey'); + $userVerification = filter_input(INPUT_GET, 'userVerification', FILTER_SANITIZE_SPECIAL_CHARS); + + $userId = filter_input(INPUT_GET, 'userId', FILTER_SANITIZE_SPECIAL_CHARS); + $userName = filter_input(INPUT_GET, 'userName', FILTER_SANITIZE_SPECIAL_CHARS); + $userDisplayName = filter_input(INPUT_GET, 'userDisplayName', FILTER_SANITIZE_SPECIAL_CHARS); + + $userId = preg_replace('/[^0-9a-f]/i', '', $userId); + $userName = preg_replace('/[^0-9a-z]/i', '', $userName); + $userDisplayName = preg_replace('/[^0-9a-z öüäéèàÖÜÄÉÈÀÂÊÎÔÛâêîôû]/i', '', $userDisplayName); + + $post = trim(file_get_contents('php://input')); + if ($post) { + $post = json_decode($post, null, 512, JSON_THROW_ON_ERROR); + } + + if ($fn !== 'getStoredDataHtml') { + + // Formats + $formats = []; + //if (filter_input(INPUT_GET, 'fmt_android-key')) { + $formats[] = 'android-key'; + //} + ///if (filter_input(INPUT_GET, 'fmt_android-safetynet')) { + $formats[] = 'android-safetynet'; + //} + //if (filter_input(INPUT_GET, 'fmt_apple')) { + $formats[] = 'apple'; + //} + //if (filter_input(INPUT_GET, 'fmt_fido-u2f')) { + $formats[] = 'fido-u2f'; + //} + //if (filter_input(INPUT_GET, 'fmt_none')) { + $formats[] = 'none'; + //} + //if (filter_input(INPUT_GET, 'fmt_packed')) { + $formats[] = 'packed'; + //} + //if (filter_input(INPUT_GET, 'fmt_tpm')) { + $formats[] = 'tpm'; + //} + + $rpId=$_SERVER['SERVER_NAME']; + + $typeUsb = true; + $typeNfc = true; + $typeBle = true; + $typeInt = true; + $typeHyb = true; + + // cross-platform: true, if type internal is not allowed + // false, if only internal is allowed + // null, if internal and cross-platform is allowed + $crossPlatformAttachment = null; + if (($typeUsb || $typeNfc || $typeBle || $typeHyb) && !$typeInt) { + $crossPlatformAttachment = true; + + } else if (!$typeUsb && !$typeNfc && !$typeBle && !$typeHyb && $typeInt) { + $crossPlatformAttachment = false; + } + + + // new Instance of the server library. + // make sure that $rpId is the domain name. + $WebAuthn = new lbuchs\WebAuthn\WebAuthn('WebAuthn Library', $rpId, $formats); + + // add root certificates to validate new registrations + //if (filter_input(INPUT_GET, 'solo')) { + $WebAuthn->addRootCertificates('rootCertificates/solo.pem'); + //} + //if (filter_input(INPUT_GET, 'apple')) { + $WebAuthn->addRootCertificates('rootCertificates/apple.pem'); + //} + //if (filter_input(INPUT_GET, 'yubico')) { + $WebAuthn->addRootCertificates('rootCertificates/yubico.pem'); + //} + //if (filter_input(INPUT_GET, 'hypersecu')) { + $WebAuthn->addRootCertificates('rootCertificates/hypersecu.pem'); + //} + //if (filter_input(INPUT_GET, 'google')) { + $WebAuthn->addRootCertificates('rootCertificates/globalSign.pem'); + $WebAuthn->addRootCertificates('rootCertificates/googleHardware.pem'); + //} + //if (filter_input(INPUT_GET, 'microsoft')) { + $WebAuthn->addRootCertificates('rootCertificates/microsoftTpmCollection.pem'); + //} + //if (filter_input(INPUT_GET, 'mds')) { + $WebAuthn->addRootCertificates('rootCertificates/mds'); + //} + + } + + // Handle different functions + if ($fn === 'getCreateArgs') { + // Get create arguments + $createArgs = $WebAuthn->getCreateArgs(\hex2bin($userId), $userName, $userDisplayName, 60*4, $requireResidentKey, $userVerification); + header('Content-Type: application/json'); + print(json_encode($createArgs)); + + // Save challenge to session or somewhere else if needed + } else if ($fn === 'getGetArgs') { + $ids = []; + + //get registrations form user table + //put credential id into session where userid = $userId + + $stmt = $conn->prepare("SELECT credential_id FROM users WHERE user_hex_id = ?"); + $stmt->bind_param("s", $userId); + $stmt->execute(); + $registration = $stmt->get_result(); + $row = $registration->fetch_assoc(); + + + if ($registration->num_rows <= 0) { + throw new Exception('User does not exist'); + } + + $_SESSION["registrations"]["credentialId"]=$row["credential_id"]; + + $ids[]=$row["credential_id"]; + $_SESSION["registrations"]["userId"]=$userId; + + $getArgs = $WebAuthn->getGetArgs($ids, 60*4, $typeUsb, $typeNfc, $typeBle, $typeHyb, $typeInt, $userVerification); + + header('Content-Type: application/json'); + print(json_encode($getArgs)); + + // save challange to session. you have to deliver it to processGet later. + $_SESSION['challenge'] = $WebAuthn->getChallenge(); + + }else if ($fn === 'processGet') { + // Process get + // Retrieve registration data from the database based on credential ID + $id = base64_decode($post->id); + $stmt = $conn->prepare("SELECT * FROM users WHERE credential_id = ?"); + $stmt->bind_param("s", $_SESSION["registrations"]["credentialId"]); + $stmt->execute(); + $registration = $stmt->get_result(); + $row = $registration->fetch_assoc(); + + if (!$registration) { + throw new Exception('Public Key for credential ID not found!'); + } + + $clientDataJSON = base64_decode($post->clientDataJSON); + $authenticatorData = base64_decode($post->authenticatorData); + $signature = base64_decode($post->signature); + $userHandle = base64_decode($post->userHandle); + $challenge = $_SESSION['challenge'] ?? ''; + $credentialPublicKey = $row['public_key']; + + // Process the get request + $WebAuthn->processGet($clientDataJSON, $authenticatorData, $signature, $credentialPublicKey, $challenge, null, $userVerification === 'required'); + + // Authentication success + //set sessionso user is authenticated + $_SESSION["username"]=htmlspecialchars($row["username"]); + $_SESSION["login"]=true; + $_SESSION["perms"]=$row["perms"]; + $_SESSION["id"]=$row["id"]; + $_SESSION["email"]=$row["email"]; + $_SESSION["telegram_id"]=$row["telegram_id"]; + $_SESSION["allow_pw_login"]=$row["allow_pw_login"]; + $_SESSION["send_login_message"]=$row["send_login_message"]; + $_SESSION["use_2fa"]=$row["use_2fa"]; + + $return = new stdClass(); + $return->success = true; + + if($_SESSION["use_2fa"]=="1"){ + unset($_SESSION["login"]); //set the login state to false + $_SESSION["2fa_auth"]=true; + $pin=mt_rand(100000, 999999); + $_SESSION["pin"]=$pin; + $ip = $_SERVER['REMOTE_ADDR']; + $username=$row["username"]; + send_to_user("[2FA-Pin]\nHello $username\nHere is your pin to log into cyberhex: $pin. If you did not try to log in please take steps to secure your account!\nIP: $ip\n",$username); + //send the user to 2fa auth page + $return->msg="send_to_2fa"; + }else{ + log_action("LOGIN::SUCCESS","User ".$_SESSION["username"]." logged in with passkey.",$_SESSION["id"]); + if($_SESSION["send_login_message"]=="1"){ + $ip = $_SERVER['REMOTE_ADDR']; + $username=$row["username"]; + send_to_user("[LOGIN WARNING]\nHello $username\nSomebody has logged into Cyberhex with your account.\nIf this was you, you can ignore this message. Else please take steps to secure your account!\nIP: $ip\n",$username); + } + } + + header('Content-Type: application/json'); + print(json_encode($return)); + } + +} catch (Throwable $ex) { + $return = new stdClass(); + $return->success = false; + $return->msg = $ex->getMessage(); + + header('Content-Type: application/json'); + print(json_encode($return)); +} + +?> diff --git a/server/app-code/system/insecure_zone/php/logout.php b/server/app-code/system/insecure_zone/php/logout.php new file mode 100644 index 0000000..2cda7df --- /dev/null +++ b/server/app-code/system/insecure_zone/php/logout.php @@ -0,0 +1,12 @@ + \ No newline at end of file diff --git a/server/app-code/system/insecure_zone/php/no_access.php b/server/app-code/system/insecure_zone/php/no_access.php new file mode 100644 index 0000000..5c3ea30 --- /dev/null +++ b/server/app-code/system/insecure_zone/php/no_access.php @@ -0,0 +1,28 @@ + + + + + + + Change Password + + + +
+
+
+
+
+

No Access

+
+
+ +
+
+
+
+
+ + diff --git a/server/app-code/system/secure_zone/php/add_user.php b/server/app-code/system/secure_zone/php/add_user.php new file mode 100644 index 0000000..ee9f169 --- /dev/null +++ b/server/app-code/system/secure_zone/php/add_user.php @@ -0,0 +1,231 @@ + + + + + + + + Change Password + + + +
+
+
+
+
+

Add a user

+
+
+
+
+ + +
+
+ + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
#DescriptionAllow/Deny
1Add user (Warning!)
2Delete/list/manage user (Warning!)
3View log
4Delete log
5Server Settings
6Client settings
7Database settings
8Add clients
9Delete/list clients
10View Incidents
11Manage Incidents
+ + +
+
+ + connect_error) { + die("Connection failed: " . $conn->connect_error); + } + $sql = "SELECT * FROM users WHERE username = ?"; + $stmt = $conn->prepare($sql); + $stmt->bind_param("s", $username); + + // Execute the statement + $stmt->execute(); + + // Get the result + $result = $stmt->get_result(); + $stmt->close(); + $conn->close(); + + + // Check if the user exists and verify the password + if ($result->num_rows > 0) { + echo ''; + + }else{ + $conn = new mysqli($DB_SERVERNAME, $DB_USERNAME, $DB_PASSWORD,$DB_DATABASE); + if ($conn->connect_error) { + $success=0; + die("Connection failed: " . $conn->connect_error); + } + $stmt = $conn->prepare("INSERT INTO users (email, username, password,perms,allow_pw_login,send_login_message,use_2fa) VALUES (?, ?, ?, ?,1,0,0)"); + $stmt->bind_param("ssss", $email, $username, $hash, $permissions); + + $email=htmlspecialchars($_POST["email"]); + $username=htmlspecialchars($_POST["username"]); + $password=$_POST["password"]; + $permissions=get_perm_str(); + $hash=password_hash($password, PASSWORD_BCRYPT); + + $stmt->execute(); + $stmt->close(); + $conn->close(); + echo ''; + + } + }elseif($block==1){ + echo ''; + } + ?> + +
+
+
+ + +
+
+ + diff --git a/server/app-code/system/secure_zone/php/index.php b/server/app-code/system/secure_zone/php/index.php new file mode 100644 index 0000000..d043041 --- /dev/null +++ b/server/app-code/system/secure_zone/php/index.php @@ -0,0 +1,137 @@ + + + + + + + + + Cyberhex (<?php echo(str_replace("_"," ",explode(".",$page))[0]); ?>) + + + + + +
+
+ + +
+

Home

+ +

User

+ + Log

"); + ?> + + Cyberhex settings

"); + ?> + + Clients

"); + ?> + + + Incidents

"); + ?> + + +
+ + +
+ + + +
+
+
+ + diff --git a/server/app-code/system/secure_zone/php/manage_user.php b/server/app-code/system/secure_zone/php/manage_user.php new file mode 100644 index 0000000..4663da3 --- /dev/null +++ b/server/app-code/system/secure_zone/php/manage_user.php @@ -0,0 +1,272 @@ + + + + + + + + Change Password + + +connect_error) { + die("Connection failed: " . $conn->connect_error); + } + $sql="SELECT * FROM users WHERE id=?"; + $stmt = $conn->prepare($sql); + $m_userid=htmlspecialchars($_GET["userid"]); + $stmt->bind_param("i", $m_userid); + $stmt->execute(); + // Get the result + $result = $stmt->get_result(); + $row = $result->fetch_assoc(); + $stmt->close(); + $m_username=$row["username"]; + $m_email=$row["email"]; + $m_permissions=$row["perms"]; + $conn->close(); +?> +
+
+
+
+
+

Manage a user

+
+
+
+
+ + +
+
+ + +
+ + + + + + + + + + + + + + '); + else + echo(''); + ?> + + + + + '); + else + echo(''); + ?> + + + + + '); + else + echo(''); + ?> + + + + + '); + else + echo(''); + ?> + + + + + '); + else + echo(''); + ?> + + + + + '); + else + echo(''); + ?> + + + + + '); + else + echo(''); + ?> + + + + + '); + else + echo(''); + ?> + + + + + '); + else + echo(''); + ?> + + + + + '); + else + echo(''); + ?> + + + + + '); + else + echo(''); + ?> + + +
#DescriptionAllow/Deny
1Add user (Warning!)
2Delete/list/manage user (Warning!)
3View log
4Delete log
5Server Settings
6Client settings
7Database settings
8Add clients
9Delete/list clients
10View Incidents
11Manage Incidents
+ + +
+
+ + connect_error) { + die("Connection failed: " . $conn->connect_error); + } + + $stmt = $conn->prepare("UPDATE users set email=?, username=?,perms=? WHERE id=?"); + $stmt->bind_param("sssi", $m_email, $m_username, $m_permissions,$m_userid); + + $m_email=htmlspecialchars($_POST["email"]); + $m_username=htmlspecialchars($_POST["username"]); + $m_permissions=get_perm_str(); + + $stmt->execute(); + $stmt->close(); + $conn->close(); + + echo(""); + }elseif($block==1){ + echo ''; + } + ?> + +
+
+
+ + +
+
+ + diff --git a/server/app-code/system/secure_zone/php/passwd.php b/server/app-code/system/secure_zone/php/passwd.php new file mode 100644 index 0000000..a998856 --- /dev/null +++ b/server/app-code/system/secure_zone/php/passwd.php @@ -0,0 +1,333 @@ + + + + + + + + Change Password + + + +
+
+
+
+
+

Change Password ()

+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ Or +
+ +
+
+ + connect_error) { + die("Connection failed: " . $conn->connect_error); + } + $sql = "SELECT * FROM users WHERE username = ?"; + $stmt = $conn->prepare($sql); + $stmt->bind_param("s", $username); + + // Execute the statement + $stmt->execute(); + + // Get the result + $result = $stmt->get_result(); + $stmt->close(); + $conn->close(); + + + // Check if the user exists and verify the password + if($new_password1===$new_password2){ + if ($result->num_rows > 0) { + $row = $result->fetch_assoc(); + if (password_verify($password, $row['password'])) { + //password correct update + // Create connection + $conn = new mysqli($DB_SERVERNAME, $DB_USERNAME, $DB_PASSWORD,$DB_DATABASE); + + // Check connection + if ($conn->connect_error) { + $success=0; + die("Connection failed: " . $conn->connect_error); + } + $stmt = $conn->prepare("UPDATE users set password = ? where username = ?"); + $stmt->bind_param("ss", $hash, $username); + $stmt->execute(); + $stmt->close(); + $conn->close(); + + echo '
'; + + } else { + + echo ''; + } + } else { + + echo ''; + } + }else{ + echo ''; + } + + } + ?> + +
+
+
+
+
+ + diff --git a/server/app-code/system/secure_zone/php/perms_functions.php b/server/app-code/system/secure_zone/php/perms_functions.php new file mode 100644 index 0000000..2fbbb96 --- /dev/null +++ b/server/app-code/system/secure_zone/php/perms_functions.php @@ -0,0 +1,71 @@ + \ No newline at end of file diff --git a/server/app-code/system/secure_zone/php/profile.php b/server/app-code/system/secure_zone/php/profile.php new file mode 100644 index 0000000..de5bb34 --- /dev/null +++ b/server/app-code/system/secure_zone/php/profile.php @@ -0,0 +1,167 @@ + +connect_error) { + $success=0; + die("Connection failed: " . $conn->connect_error); + } + $user_hex_id=bin2hex($username_new); + $stmt = $conn->prepare("UPDATE users set email = ?, username = ?, telegram_id = ?, allow_pw_login = ?, user_hex_id = ?, send_login_message = ?, use_2fa = ? where username = ?"); + $stmt->bind_param("sssisiis", $email, $username_new,$telegram_id, $pw_login,$user_hex_id, $send_login_message, $use_2fa, $username); + + $email=htmlspecialchars($_POST["email"]); + $username_new=htmlspecialchars($_POST["username"]); + $telegram_id=htmlspecialchars($_POST["telegram_id"]); + $stmt->execute(); + $stmt->close(); + $conn->close(); + $username=$username_new; + $_SESSION["username"]=$username; + $_SESSION["email"]=$email; + $_SESSION["telegram_id"]=$telegram_id; + $_SESSION["allow_pw_login"]=$pw_login; + $_SESSION["send_login_message"]=$send_login_message; + $_SESSION["use_2fa"]=$use_2fa; +} + +?> + + + + + + + Profile + + +
+
+
+
+
+

Your Profile ()

+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + "); + }else{ + echo(""); + } + ?> + +
+
+
+ + "); + }else{ + echo(""); + } + ?> + +
+
+
+ + "); + }else{ + echo(""); + } + ?> + +
+
+ +
+ '; + } + ?> +
+
+
+ +
+
+ + + diff --git a/server/app-code/system/secure_zone/php/user_list.php b/server/app-code/system/secure_zone/php/user_list.php new file mode 100644 index 0000000..59602ed --- /dev/null +++ b/server/app-code/system/secure_zone/php/user_list.php @@ -0,0 +1,127 @@ + + + + + + + + Change Password + + + +
+
+
+
+
+

User list

+
+
+ + connect_error) { + die("Connection failed: " . $conn->connect_error); + } + //delete user if requested + if(isset($_GET["delete"])){ + $userid=htmlspecialchars($_GET["delete"]); + $sql = "DELETE FROM users WHERE id = ?"; + $stmt = $conn->prepare($sql); + $stmt->bind_param("i", $userid); + // Execute the statement + $stmt->execute(); + $stmt->close(); + } + + + //get count of users + + $sql = "SELECT count(*) AS user_count FROM users"; + $stmt = $conn->prepare($sql); + // Execute the statement + $stmt->execute(); + // Get the result + $result = $stmt->get_result(); + $row = $result->fetch_assoc(); + $num_of_users=$row["user_count"]; + $stmt->close(); + + //now list of all the users => userid, username, email, perms, delete + // Create a connection + $conn = new mysqli($DB_SERVERNAME, $DB_USERNAME, $DB_PASSWORD, $DB_DATABASE); + + // Check the connection + if ($conn->connect_error) { + die("Connection failed: " . $conn->connect_error); + } + $last_id=-1; + //create the table header + echo(''); + echo(''); + echo(''); + echo(''); + echo(''); + echo(''); + echo(''); + while($num_of_users!=0){ + $sql = "SELECT * FROM users where id > $last_id"; + $stmt = $conn->prepare($sql); + // Execute the statement + $stmt->execute(); + // Get the result + $result = $stmt->get_result(); + $row = $result->fetch_assoc(); + $last_id=$row["id"]; + $username=$row["username"]; + $email=$row["email"]; + $perms=$row["perms"]; + if($last_id!=1){ //number 1 is the unauthenticated user + echo(''); + echo(''); + echo(''); + echo(''); + echo(''); + echo(''); + echo(''); + echo(''); + } + $stmt->close(); + $num_of_users--; + } + echo(''); + echo('
UseridUsernameEmailPermissionsEdit UserDelete user
'.$last_id.''.$username.''.$email.''.$perms.'managedelete
'); + $conn->close(); + ?> +
+
+
+
+
+ + diff --git a/server/app-code/system/secure_zone/php/welcome.php b/server/app-code/system/secure_zone/php/welcome.php new file mode 100644 index 0000000..ba84c41 --- /dev/null +++ b/server/app-code/system/secure_zone/php/welcome.php @@ -0,0 +1,39 @@ + + + + + + + + Cyberhex + + +

+
+
+
+
+
+

Dashboard

+
+
+
+
+
+
+
+ + diff --git a/server/docker-compose.yml b/server/docker-compose.yml new file mode 100644 index 0000000..abec619 --- /dev/null +++ b/server/docker-compose.yml @@ -0,0 +1,42 @@ +version: '3.3' + +services: + app-db: + image: yobasystems/alpine-mariadb:latest + container_name: app-db + environment: + MYSQL_ROOT_PASSWORD: 1234 + networks: + app-network: + ipv4_address: 192.168.178.2 + volumes: + - app-db-storage:/var/lib/mysql + + app-srv: + build: + context: . + dockerfile: srv_dockerfile + container_name: app-srv + networks: + app-network: + ipv4_address: 192.168.178.3 + ports: + - "8088:80" + - "4438:443" + depends_on: + - app-db + volumes: + - ./app-code:/var/www/html + - ./apache-conf:/etc/apache2/sites-available + - ./certs:/etc/apache2/certs +networks: + app-network: + driver: bridge + ipam: + driver: default + config: + - subnet: 192.168.178.0/24 + +volumes: + app-db-storage: + external: true diff --git a/server/install.sh b/server/install.sh new file mode 100644 index 0000000..925bb39 --- /dev/null +++ b/server/install.sh @@ -0,0 +1,2 @@ +#!/bin/bash +docker volume create app-db-storage diff --git a/server/install.txt b/server/install.txt new file mode 100644 index 0000000..6041882 --- /dev/null +++ b/server/install.txt @@ -0,0 +1,5 @@ +1: download the file docker-compose.yml and the folder cyberhex-code +2: create the cyberhex-db-storage volume +3: request some ssl certs via certbot form letsencrypt and put them into certs/ +sudo certbot certonly --manual --preferred-challenges=http --http-01-port=8080 +3: use docker-compose up to start the whole thing diff --git a/server/srv_dockerfile b/server/srv_dockerfile new file mode 100644 index 0000000..527c062 --- /dev/null +++ b/server/srv_dockerfile @@ -0,0 +1,16 @@ +# Extend the official PHP image +FROM php:apache + +# Install the mysqli extension +RUN docker-php-ext-install mysqli +RUN a2enmod ssl +RUN service apache2 restart +COPY ./cyberhex-code /var/www/html +RUN mkdir -p /var/www/html/install/ +RUN mkdir -p /var/www/html/database_srv +RUN mkdir -p /var/www/html/export +RUN mkdir -p /var/www/html/import +RUN chown -R www-data:www-data /var/www/html/export/ +RUN chown -R www-data:www-data /var/www/html/import/ +RUN chown -R www-data:www-data /var/www/html/install/ +RUN chown -R www-data:www-data /var/www/html/database_srv/ \ No newline at end of file