M .gitignore => .gitignore +1 -0
@@ 4,4 4,5 @@ composer.phar
php-cs-fixer.phar
makefile
tools/
+vendor/
*.sqlite3=
\ No newline at end of file
M README.md => README.md +1 -1
@@ 1,6 1,6 @@
# MeBo-es
MeBo is a simple message board system on the Web.
-Developed in english, but with texts in spanish. Multilingual support could come later.
+Developed in English, but with texts in Spanish. Multilingual support could come later.
Forked by ~eapl from
[sourcehut - mercurial repo](https://hg.sr.ht/~m15o/mebo)
A composer.json => composer.json +5 -0
@@ 0,0 1,5 @@
+{
+ "require": {
+ "lbuchs/webauthn": "^1.1"
+ }
+}
A composer.lock => composer.lock +64 -0
@@ 0,0 1,64 @@
+{
+ "_readme": [
+ "This file locks the dependencies of your project to a known state",
+ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
+ "This file is @generated automatically"
+ ],
+ "content-hash": "2b57a79cfde46bf4e89be9e1d7d07a87",
+ "packages": [
+ {
+ "name": "lbuchs/webauthn",
+ "version": "v1.1.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/lbuchs/WebAuthn.git",
+ "reference": "4780c7b017ccc74a023c6ae05b5847e478f5b97d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/lbuchs/WebAuthn/zipball/4780c7b017ccc74a023c6ae05b5847e478f5b97d",
+ "reference": "4780c7b017ccc74a023c6ae05b5847e478f5b97d",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.1"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "lbuchs\\WebAuthn\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Lukas Buchs",
+ "role": "Developer"
+ }
+ ],
+ "description": "A simple PHP WebAuthn (FIDO2) server library",
+ "homepage": "https://github.com/lbuchs/webauthn",
+ "keywords": [
+ "Authentication",
+ "webauthn"
+ ],
+ "support": {
+ "issues": "https://github.com/lbuchs/WebAuthn/issues",
+ "source": "https://github.com/lbuchs/WebAuthn/tree/v1.1.3"
+ },
+ "time": "2022-11-21T08:46:34+00:00"
+ }
+ ],
+ "packages-dev": [],
+ "aliases": [],
+ "minimum-stability": "stable",
+ "stability-flags": [],
+ "prefer-stable": false,
+ "prefer-lowest": false,
+ "platform": [],
+ "platform-dev": [],
+ "plugin-api-version": "2.3.0"
+}
A public/login_passkey.php => public/login_passkey.php +53 -0
@@ 0,0 1,53 @@
+<?php
+require 'includes/app.php';
+
+$errors = [];
+$email = '';
+
+if ($_SERVER['REQUEST_METHOD'] === 'POST') {
+ $email = $_POST['email'];
+ $password = $_POST['password'];
+
+ Validate::isEmail($email) or $errors[] = "Wrong email";
+ Validate::isPassword($password) or $errors[] = "Wrong password";
+
+ if (!count($errors)) {
+ if ($member = $BBS->getUser()->login($email, $password, $errors)) {
+ $BBS->getSession()->login($member['id']);
+ redirect('index.php');
+ }
+ }
+}
+?>
+<?php include 'includes/header.php'; ?>
+<script src="webauthn.js"></script>
+
+<h1>Inicia sesión</h1>
+
+<?php form_errors($errors) ?>
+
+<form method="POST" class="column">
+ <div id="login">
+ <button type="button" onclick="checkRegistration()">Login with Passkey</button>
+ </div>
+</form>
+
+<hr>
+
+<form action="<?= $_SERVER['PHP_SELF'] ?>" method="post">
+ <?php include 'includes/csrf.php' ?>
+ <div class="form-group">
+ <label for="form-name">Correo:</label>
+ <input type="email" name="email" value="<?=$email?>" class="form-control" />
+ </div>
+
+ <div class="form-group">
+ <label for="form-password">Contraseña:</label>
+ <input type="password" name="password" class="form-control" required/>
+ </div>
+
+ <input type="submit" value="Inicia sesión"/>
+ <p><a href="password-lost.php">¿Olvidaste tu contraseña anoche?</a></p>
+</form>
+
+<?php include 'includes/footer.php'; ?>
A public/webauthn.js => public/webauthn.js +190 -0
@@ 0,0 1,190 @@
+/**
+ * Creates a new FIDO2 registration
+ * @returns {undefined}
+ */
+function newRegistration() {
+ if (!window.fetch || !navigator.credentials || !navigator.credentials.create) {
+ window.alert('Your browser is not supported. Look for one with WebAuthn or Passkey support.')
+ return
+ }
+
+ // Get default args
+ window.fetch(
+ 'webauthn.php?fn=createArgs',
+ {method:'GET',cache:'no-cache'}
+ ).then(function(response) {
+ return response.json()
+
+ // Convert Base64 to ArrayBuffer
+ }).then(function(json) {
+ // Error handling
+ if (json.success === false) {
+ throw new Error(json.msg);
+ }
+
+ // Replace binary Base64 data with ArrayBuffer.
+ // Another way to do this is the reviver function of JSON.parse()
+ recursiveBase64StrToArrayBuffer(json);
+ return json;
+
+ // Create credentials
+ }).then(function(createCredentialArgs) {
+ //console.log(createCredentialArgs);
+ return navigator.credentials.create(createCredentialArgs);
+
+ // Convert to base64
+ }).then(function(cred) {
+ return {
+ clientDataJSON:
+ cred.response.clientDataJSON
+ ? arrayBufferToBase64(cred.response.clientDataJSON)
+ : null,
+ attestationObject:
+ cred.response.attestationObject
+ ? arrayBufferToBase64(cred.response.attestationObject)
+ : null,
+ };
+
+ // Transfer to server
+ }).then(function(AuthenticatorAttestationResponse) {
+ AuthenticatorAttestationResponse.masterPwd = document.getElementById('password').value
+ AuthenticatorAttestationResponse = JSON.stringify(AuthenticatorAttestationResponse)
+
+ return window.fetch('webauthn.php?fn=processCreate', {method:'POST', body: AuthenticatorAttestationResponse, cache:'no-cache'})
+
+ // Convert to JSON
+ }).then(function(response) {
+ return response.json();
+
+ // Analyze response
+ }).then(function(json) {
+ if (json.success) {
+ //reloadServerPreview();
+ window.alert(json.msg || 'Registration success');
+ //console.log(json)
+ // TODO: Redirect to somewhere else
+ } else {
+ throw new Error(json.msg);
+ }
+
+ // Catch errors
+ }).catch(function(err) {
+ //reloadServerPreview();
+ window.alert(err.message || 'Unknown error occured');
+ });
+}
+
+/**
+ * Checks a FIDO2 registration
+ * @returns {undefined}
+ */
+function checkRegistration() {
+ if (!window.fetch || !navigator.credentials || !navigator.credentials.create) {
+ window.alert('Your browser is not supported. Look for one with WebAuthn or Passkey support.')
+ return;
+ }
+
+ // Get default args
+ window.fetch('webauthn.php?fn=getGetArgs', { method: 'GET', cache: 'no-cache' }).then(function (response) {
+ console.log(response.body)
+ return response.json();
+
+ // Convert Base64 to ArrayBuffer
+ }).then(function(json) {
+
+ // Error handling
+ if (json.success === false) {
+ throw new Error(json.msg)
+ }
+
+ // Replace binary base64 data with ArrayBuffer. a other way to do this
+ // is the reviver function of JSON.parse()
+ recursiveBase64StrToArrayBuffer(json)
+ console.log(json)
+ return json
+
+ // Create credentials
+ }).then(function (getCredentialArgs) {
+ console.log('Geting navigator credentials')
+ return navigator.credentials.get(getCredentialArgs)
+
+ // Convert to Base64
+ }).then(function (cred) {
+ console.log('Cred:')
+ console.log(cred)
+ return {
+ id: cred.rawId ? arrayBufferToBase64(cred.rawId) : null,
+ clientDataJSON: cred.response.clientDataJSON ? arrayBufferToBase64(cred.response.clientDataJSON) : null,
+ authenticatorData: cred.response.authenticatorData ? arrayBufferToBase64(cred.response.authenticatorData) : null,
+ signature: cred.response.signature ? arrayBufferToBase64(cred.response.signature) : null,
+ userHandle: cred.response.userHandle ? arrayBufferToBase64(cred.response.userHandle) : null
+ };
+
+ // Transfer to server
+ }).then(JSON.stringify).then(function (AuthenticatorAttestationResponse) {
+ return window.fetch('webauthn.php?fn=processGet', {method:'POST', body: AuthenticatorAttestationResponse, cache:'no-cache'});
+
+ // Convert to json
+ }).then(function(response) {
+ return response.json();
+
+ // Analyze response
+ }).then(function(json) {
+ if (json.success) {
+ //window.alert(json.msg || 'Login success');
+ window.location.replace('index.php');
+ } else {
+ throw new Error(json.msg);
+ }
+
+ // Catch errors
+ }).catch(function (err) {
+ console.log('Error catched')
+ window.alert(err.message || 'unknown error occured');
+ });
+}
+
+/**
+ * Convert RFC 1342-like Base64 strings to ArrayBuffer
+ * @param {mixed} obj
+ * @returns {undefined}
+ */
+function recursiveBase64StrToArrayBuffer(obj) {
+ let prefix = '=?BINARY?B?';
+ let suffix = '?=';
+ if (typeof obj === 'object') {
+ for (let key in obj) {
+ if (typeof obj[key] === 'string') {
+ let str = obj[key];
+ if (str.substring(0, prefix.length) === prefix && str.substring(str.length - suffix.length) === suffix) {
+ str = str.substring(prefix.length, str.length - suffix.length);
+
+ let binary_string = window.atob(str);
+ let len = binary_string.length;
+ let bytes = new Uint8Array(len);
+ for (let i = 0; i < len; i++) {
+ bytes[i] = binary_string.charCodeAt(i);
+ }
+ obj[key] = bytes.buffer;
+ }
+ } else {
+ recursiveBase64StrToArrayBuffer(obj[key]);
+ }
+ }
+ }
+}
+
+/**
+ * Convert a ArrayBuffer to Base64
+ * @param {ArrayBuffer} buffer
+ * @returns {String}
+ */
+function arrayBufferToBase64(buffer) {
+ let binary = '';
+ let bytes = new Uint8Array(buffer);
+ let len = bytes.byteLength;
+ for (let i = 0; i < len; i++) {
+ binary += String.fromCharCode(bytes[i]);
+ }
+ return window.btoa(binary);
+}<
\ No newline at end of file
A public/webauthn.php => public/webauthn.php +330 -0
@@ 0,0 1,330 @@
+<?php
+/*
+ * Copyright (C) 2018 Lukas Buchs
+ * license https://github.com/lbuchs/WebAuthn/blob/master/LICENSE MIT
+ *
+ * Server test script for WebAuthn library. Saves new registrations in session.
+ *
+ * JAVASCRIPT | SERVER
+ * ------------------------------------------------------------
+ *
+ * REGISTRATION
+ *
+ * window.fetch -----------------> getCreateArgs
+ * |
+ * navigator.credentials.create <-------------'
+ * |
+ * '-------------------------> processCreate
+ * |
+ * alert ok or fail <----------------'
+ *
+ * ------------------------------------------------------------
+ *
+ * VALIDATION
+ *
+ * window.fetch ------------------> getGetArgs
+ * |
+ * navigator.credentials.get <----------------'
+ * |
+ * '-------------------------> processGet
+ * |
+ * alert ok or fail <----------------'
+ *
+ * ------------------------------------------------------------
+ */
+
+require_once '../vendor/autoload.php';
+
+define('TIMEOUT', 20);
+define('REGISTRATIONS_PATH', 'registrations.bin');
+
+//$config = parse_ini_file('.config');
+//$masterPassword = $config['master_password'];
+
+// '123'
+$masterPassword = '$argon2i$v=19$m=65536,t=4,p=1$M0dRcmxtVEc0STJpZkQ0TA$Kfk3l5Fjv3gG5E8gJqemuEOw5QUCttBg+LtylyBRXYU';
+
+try {
+ session_start();
+
+ // Read get argument and post body
+ $fn = filter_input(INPUT_GET, 'fn');
+
+ $post = trim(file_get_contents('php://input'));
+ if ($post) {
+ $post = json_decode($post);
+ }
+
+ if ($fn !== 'getStoredDataHtml') {
+ $formats = array();
+ $formats[] = 'android-key';
+ $formats[] = 'android-safetynet';
+ $formats[] = 'apple';
+ $formats[] = 'packed';
+ $formats[] = 'tpm';
+
+ // Types selected on front end
+ $typeUsb = true;
+ $typeNfc = true;
+ $typeBle = true;
+ $typeInt = 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) && !$typeInt) {
+ $crossPlatformAttachment = true;
+
+ } else if (!$typeUsb && !$typeNfc && !$typeBle && $typeInt) {
+ $crossPlatformAttachment = false;
+ }
+
+ // Relying party
+ //$rpId = 'eapl.mx';
+ //$rpId = '127.0.0.1:8000';
+ $rpId = 'https://localhost';
+
+ // New Instance of the server library. Make sure that $rpId is the domain name.
+ $WebAuthn = new lbuchs\WebAuthn\WebAuthn('WebAuthn Library', $rpId, $formats);
+ }
+
+ // ------------------------------------
+ // Request for create arguments
+ // ------------------------------------
+ if ($fn === 'createArgs') {
+ // This is a random userId
+ $userId = '6f333511393d6e784e550218b66c3e09';
+ $userName = 'master';
+ $userDisplayName = 'master';
+ $requiresResidentKey = true; // Client-side discoverable Credential
+ $userVerification = true;
+ $crossPlatformAttachment = null;
+
+ // Check parameters here:
+ // https://github.com/lbuchs/WebAuthn/blob/master/src/WebAuthn.php#L125
+ $createArgs = $WebAuthn->getCreateArgs(
+ \hex2bin($userId), $userName, $userDisplayName, TIMEOUT, $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();
+
+ // ------------------------------------
+ // Request for get arguments
+ // ------------------------------------
+ } else if ($fn === 'getGetArgs') {
+ $registrations = array();
+ if (file_exists(REGISTRATIONS_PATH)) {
+ $registrations = unserialize(file_get_contents(REGISTRATIONS_PATH));
+ }
+
+ $ids = array();
+ foreach ($registrations as $key => $value) {
+ $ids[] = $value->credentialId;
+ }
+
+ $userVerification = true;
+
+ $getArgs = $WebAuthn->getGetArgs(
+ $ids, TIMEOUT, $typeUsb, $typeNfc, $typeBle, $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();
+
+ // ------------------------------------
+ // Process create
+ // ------------------------------------
+ } else if ($fn === 'processCreate') {
+ // Check parameters here:
+ // https://github.com/lbuchs/WebAuthn/blob/master/src/WebAuthn.php#L277
+ $clientDataJSON = base64_decode($post->clientDataJSON);
+ $attestationObject = base64_decode($post->attestationObject);
+ $postMasterPassword = $post->masterPwd;
+ $challenge = $_SESSION['challenge'];
+
+ $requireUserVerification = true;
+ $requireUserPresent = true;
+ $failIfRootMismatch = false;
+
+ if (!password_verify($postMasterPassword, $masterPassword)) {
+ throw new Exception("Incorrect Master Password!");
+ }
+
+ // processCreate returns data to be stored for future logins.
+ // in this example we store it in the PHP session.
+
+ // Normaly you have to store the data in a database connected
+ // with the user name.
+ $data = $WebAuthn->processCreate(
+ $clientDataJSON, $attestationObject, $challenge,
+ $requireUserVerification, $requireUserPresent, $failIfRootMismatch
+ );
+
+ // Add user info
+ //$data->userId = $userId;
+ //$data->userName = $userName;
+ //$data->userDisplayName = $userDisplayName;
+
+ /*
+ // Data in $data
+ // See https://github.com/lbuchs/WebAuthn/blob/master/src/WebAuthn.php#L357
+ $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;
+ */
+
+ // Load previous registrations if they exist
+ $registrations = array();
+
+ if (file_exists(REGISTRATIONS_PATH)) {
+ $registrations = unserialize(file_get_contents(REGISTRATIONS_PATH));
+ }
+
+ // chunk_split(string $body, int $chunklen = 76, string $end = "\r\n"): string
+ $credentialId = chunk_split(bin2hex($data->credentialId), 64, '');
+ $registrations[$credentialId] = $data;
+
+ // We are saving the registrations in a binary file
+ file_put_contents(REGISTRATIONS_PATH, serialize($registrations));
+
+ $msg = 'Registration success.';
+ if ($data->rootValid === false) {
+ $msg = 'Registration OK, but certificate does not match any of the selected root CA.';
+ }
+
+ $return = new stdClass();
+ $return->success = true;
+ $return->msg = $msg;
+
+ header('Content-Type: application/json');
+ print(json_encode($return));
+
+ // ------------------------------------
+ // Proccess get
+ // ------------------------------------
+ } else if ($fn === 'processGet') {
+ $clientDataJSON = base64_decode($post->clientDataJSON);
+ $authenticatorData = base64_decode($post->authenticatorData);
+ $signature = base64_decode($post->signature);
+ $userHandle = base64_decode($post->userHandle);
+ $id = base64_decode($post->id);
+ $challenge = $_SESSION['challenge'];
+
+ // Looking up correspondending Public key of the Credential ID
+ // you should also validate that only IDs of the given User name
+ // are taken for the login.
+
+ $registrations = array();
+ if (file_exists(REGISTRATIONS_PATH)) {
+ $registrations = unserialize(file_get_contents(REGISTRATIONS_PATH));
+ }
+
+ $credentialPublicKey = null;
+
+ foreach ($registrations as $registration) {
+ if ($registration->credentialId === $id) {
+ $credentialPublicKey = $registration->credentialPublicKey;
+ }
+ }
+
+ if ($credentialPublicKey === null) {
+ throw new Exception('Public Key for credential ID not found!');
+ }
+
+ // If we have resident key, we have to verify that the userHandle
+ // is the provided userId at registration
+ if ($requireResidentKey && $userHandle !== hex2bin($reg->userId)) {
+ throw new \Exception('userId doesnt match (is ' . bin2hex($userHandle) . ' but expect ' . $reg->userId . ')');
+ }
+
+ // Process the get request. throws WebAuthnException if it fails
+ $WebAuthn->processGet($clientDataJSON, $authenticatorData, $signature, $credentialPublicKey, $challenge, null, $userVerification === 'required');
+
+ $return = new stdClass();
+ $return->success = true;
+
+ $_SESSION['valid_session'] = true;
+
+ header('Content-Type: application/json');
+ print(json_encode($return));
+
+ // ------------------------------------
+ // Proccess clear registrations
+ // ------------------------------------
+ } else if ($fn === 'clearRegistrations') {
+ $_SESSION['registrations'] = null;
+ $_SESSION['challenge'] = null;
+
+ $return = new stdClass();
+ $return->success = true;
+ $return->msg = 'all registrations deleted';
+
+ header('Content-Type: application/json');
+ print(json_encode($return));
+
+ // ------------------------------------
+ // Display stored data as HTML
+ // ------------------------------------
+ } else if ($fn === 'getStoredDataHtml') {
+ $html = '<!DOCTYPE html>' . "\n";
+ $html .= '<html><head><style>tr:nth-child(even){background-color: #f2f2f2;}</style></head>';
+ $html .= '<body style="font-family:sans-serif">';
+ if (isset($_SESSION['registrations']) && is_array($_SESSION['registrations'])) {
+ $html .= '<p>There are ' . count($_SESSION['registrations']) . ' registrations in this session:</p>';
+ foreach ($_SESSION['registrations'] as $reg) {
+ $html .= '<table style="border:1px solid black;margin:10px 0;">';
+ foreach ($reg as $key => $value) {
+
+ if (is_bool($value)) {
+ $value = $value ? 'yes' : 'no';
+ } else if (is_null($value)) {
+ $value = 'null';
+ } else if (is_object($value)) {
+ $value = chunk_split(strval($value), 64);
+ } else if (is_string($value) && strlen($value) > 0 && htmlspecialchars($value) === '') {
+ $value = chunk_split(bin2hex($value), 64);
+ }
+
+ if ($key === 'credentialId' || $key === 'AAGUID') {
+ $value = chunk_split(bin2hex($value), 64);
+ }
+
+ $html .= '<tr><td>' . htmlspecialchars($key) . '</td><td style="font-family:monospace;">' . nl2br(htmlspecialchars($value)) . '</td>';
+ }
+ $html .= '</table>';
+ }
+ } else {
+ $html .= '<p>There are no registrations in this session.</p>';
+ }
+ $html .= '</body></html>';
+
+ header('Content-Type: text/html');
+ print $html;
+ }
+} catch (Throwable $ex) {
+ $return = new stdClass();
+ $return->success = false;
+ $return->msg = $ex->getMessage();
+
+ header('Content-Type: application/json');
+ print('Error: ' . json_encode($return));
+}<
\ No newline at end of file