public function generateRegistrationOptions($username) {
$rpEntity = new PublicKeyCredentialRpEntity($this->rpName, $this->rpId);
+
+ // Convert username to a valid user ID (must be ArrayBuffer or ArrayBufferView)
+ $userId = hash('sha256', $username, true); // Binary SHA-256 hash
+
$userEntity = new PublicKeyCredentialUserEntity(
- $username,
- $username,
- $username
+ $username, // name
+ $userId, // id (must be binary)
+ $username // displayName
);
$challenge = random_bytes(32);
$_SESSION['challenge'] = base64_encode($challenge);
$_SESSION['username'] = $username;
+ $_SESSION['user_id_hash'] = bin2hex($userId); // Store for later verification
+ // Note: We use standard base64 for JSON, not base64url
$creationOptions = PublicKeyCredentialCreationOptions::create(
$rpEntity,
$challenge = base64_decode($_SESSION['challenge']);
$username = $_SESSION['username'];
+ $userIdHash = isset($_SESSION['user_id_hash']) ? hex2bin($_SESSION['user_id_hash']) : hash('sha256', $username, true);
unset($_SESSION['challenge']);
unset($_SESSION['username']);
+ unset($_SESSION['user_id_hash']);
try {
error_log("WebAuthn register: decoding JSON response");
if ($attestationData === null) {
error_log("WebAuthn register: JSON decode failed. Error: " . json_last_error_msg());
+ error_log("WebAuthn register: Raw response: " . substr($attestationResponse, 0, 500));
return false;
}
+ // Validate required fields
+ $requiredFields = ['id', 'rawId', 'response', 'type'];
+ foreach ($requiredFields as $field) {
+ if (!isset($attestationData[$field])) {
+ error_log("WebAuthn register: Missing required field: $field");
+ return false;
+ }
+ }
+
+ // Validate response fields
+ $responseFields = ['attestationObject', 'clientDataJSON'];
+ foreach ($responseFields as $field) {
+ if (!isset($attestationData['response'][$field])) {
+ error_log("WebAuthn register: Missing required response field: $field");
+ return false;
+ }
+ }
+
error_log("WebAuthn register: JSON decoded successfully");
// Convert base64 back to binary
}
}
- public function generateAuthenticationOptions() {
+ public function generateAuthenticationOptions($userId) {
$challenge = random_bytes(32);
$_SESSION['challenge'] = base64_encode($challenge);
$rpEntity = new PublicKeyCredentialRpEntity($this->rpName, $this->rpId);
- return PublicKeyCredentialRequestOptions::create($rpEntity, $challenge);
+ // Convert userId to binary format if it's not already
+ if (is_numeric($userId)) {
+ $userIdBinary = hash('sha256', strval($userId), true);
+ } else {
+ $userIdBinary = hash('sha256', $userId, true);
+ }
+
+ return PublicKeyCredentialRequestOptions::create($rpEntity, $challenge, null, 'public-key', null, null, $userIdBinary);
}
public function authenticate($assertionResponse, $storedPublicKey) {
<?php
+error_reporting(E_ALL & ~E_DEPRECATED);
// register.php
session_start();
require_once __DIR__ . '/../include/Database.php';
require_once __DIR__ . '/../include/WebAuthnManager.php';
+// Set CORS headers for WebAuthn
+header("Access-Control-Allow-Origin: https://dw.nothing2do.fr");
+header("Access-Control-Allow-Credentials: true");
+header("Access-Control-Allow-Headers: Content-Type");
+header("Access-Control-Allow-Methods: POST, OPTIONS");
+
+// Handle preflight requests
+if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
+ http_response_code(200);
+ exit();
+}
+
$db = new Database();
$pdo = $db->connect();
$webAuthnManager = new WebAuthnManager();
// Helper function to convert ArrayBuffer to Base64
function arrayBufferToBase64(buffer) {
try {
- const bytes = new Uint8Array(buffer);
- let binary = '';
- for (let i = 0; i < bytes.byteLength; i++) {
- binary += String.fromCharCode(bytes[i]);
+ // Handle ArrayBuffer directly
+ if (buffer instanceof ArrayBuffer) {
+ buffer = new Uint8Array(buffer);
+ }
+
+ // Check if it's already a Uint8Array
+ if (!(buffer instanceof Uint8Array)) {
+ throw new Error('Input is not an ArrayBuffer or Uint8Array');
}
- return btoa(binary);
+
+ // Chunked conversion to avoid stack overflow
+ const chunkSize = 0x8000; // 32KB chunks
+ let result = '';
+
+ for (let i = 0; i < buffer.length; i += chunkSize) {
+ const chunk = buffer.subarray(i, i + chunkSize);
+ const binaryString = String.fromCharCode.apply(null, chunk);
+ result += btoa(binaryString);
+ }
+
+ return result;
} catch (error) {
console.error('Error converting ArrayBuffer to Base64:', error);
- console.error('Buffer:', buffer);
- console.error('Buffer type:', typeof buffer);
- console.error('Buffer length:', buffer ? buffer.byteLength : 'N/A');
- throw error;
+ console.error('Buffer type:', buffer ? buffer.constructor.name : 'null');
+ console.error('Buffer length:', buffer ? buffer.length || buffer.byteLength : 'N/A');
+
+ // Try alternative method
+ try {
+ let binary = '';
+ const bytes = new Uint8Array(buffer);
+ const len = bytes.byteLength;
+
+ for (let i = 0; i < len; i++) {
+ binary += String.fromCharCode(bytes[i]);
+ }
+
+ return btoa(binary);
+ } catch (fallbackError) {
+ console.error('Fallback method also failed:', fallbackError);
+ throw new Error('Failed to convert ArrayBuffer to Base64: ' + error.message);
+ }
+ }
+ }
+
+ // Helper function to convert base64 to Uint8Array
+ function base64ToUint8Array(base64) {
+ try {
+ // Handle both standard base64 and base64url
+ let processedBase64 = base64.replace(/-/g, '+').replace(/_/g, '/');
+
+ // Add padding if needed
+ const padLength = processedBase64.length % 4;
+ const paddedBase64 = padLength ? processedBase64 + '==='.slice(padLength) : processedBase64;
+
+ // Convert to binary string
+ const binaryString = atob(paddedBase64);
+
+ // Convert to Uint8Array
+ const bytes = new Uint8Array(binaryString.length);
+ for (let i = 0; i < binaryString.length; i++) {
+ bytes[i] = binaryString.charCodeAt(i);
+ }
+
+ return bytes;
+ } catch (error) {
+ console.error('Error in base64ToUint8Array:', error);
+ console.error('Input:', base64);
+ console.error('Input length:', base64 ? base64.length : 'N/A');
+ throw new Error('Failed to convert base64 to Uint8Array: ' + error.message);
+ }
+ }
+
+ // Helper function to convert string to Uint8Array
+ function stringToUint8Array(str) {
+ try {
+ // If it's already in the right format, return as-is
+ if (str instanceof ArrayBuffer) {
+ return new Uint8Array(str);
+ }
+
+ // If it's a hex string (from PHP hash)
+ if (/^[0-9a-f]+$/i.test(str)) {
+ const matches = str.match(/.{1,2}/g);
+ const bytes = new Uint8Array(matches.length);
+ for (let i = 0; i < matches.length; i++) {
+ bytes[i] = parseInt(matches[i], 16);
+ }
+ return bytes;
+ }
+
+ // If it's base64
+ try {
+ const binaryString = atob(str);
+ const bytes = new Uint8Array(binaryString.length);
+ for (let i = 0; i < binaryString.length; i++) {
+ bytes[i] = binaryString.charCodeAt(i);
+ }
+ return bytes;
+ } catch (e) {
+ // Fallback: treat as regular string
+ const encoder = new TextEncoder();
+ return encoder.encode(str);
+ }
+ } catch (error) {
+ console.error('Error converting string to Uint8Array:', error);
+ throw new Error('Failed to convert user ID: ' + error.message);
}
}
+ // Helper function to validate Base64 string
+ function isValidBase64(str) {
+ try {
+ // Check if string is valid Base64
+ if (!/^[A-Za-z0-9+/]+={0,2}$/.test(str)) {
+ return false;
+ }
+
+ // Try to decode it
+ const decoded = atob(str);
+ return decoded.length > 0;
+ } catch (e) {
+ return false;
+ }
+ }
+
+ // Helper function to convert credential to a format suitable for server
+ function prepareCredentialForServer(credential) {
+ try {
+ console.log('Preparing credential for server...');
+
+ // Use the browser's native conversion where possible
+ const response = credential.response;
+
+ // For rawId, use credential.id which is already base64url
+ const rawIdBase64 = credential.id;
+
+ // Convert ArrayBuffer data using a more reliable method
+ const arrayBufferToBase64Safe = (buffer) => {
+ try {
+ // Method 1: Using FileReader (most reliable)
+ return new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.onload = () => {
+ const result = reader.result;
+ // Remove data URL prefix if present
+ const base64 = result.split(',')[1] || result;
+ resolve(base64);
+ };
+ reader.onerror = reject;
+ reader.readAsDataURL(new Blob([buffer]));
+ });
+ } catch (e) {
+ // Fallback to manual conversion
+ const bytes = new Uint8Array(buffer);
+ let binary = '';
+ for (let i = 0; i < bytes.byteLength; i++) {
+ binary += String.fromCharCode(bytes[i]);
+ }
+ return btoa(binary);
+ }
+ };
+
+ return Promise.all([
+ arrayBufferToBase64Safe(response.attestationObject),
+ arrayBufferToBase64Safe(response.clientDataJSON)
+ ])
+ .then(([attestationObjectBase64, clientDataJSONBase64]) => {
+ return {
+ id: credential.id,
+ rawId: rawIdBase64,
+ type: credential.type,
+ response: {
+ attestationObject: attestationObjectBase64,
+ clientDataJSON: clientDataJSONBase64,
+ transports: response.getTransports ? response.getTransports() : []
+ }
+ };
+ })
+ .catch(error => {
+ console.error('Error in promise chain:', error);
+ throw new Error('Failed to prepare credential data: ' + error.message);
+ });
+ }
+
document.addEventListener('DOMContentLoaded', function() {
const form = document.getElementById('registrationForm');
const usernameInput = document.getElementById('username');
webauthnMessage.textContent = 'Veuillez toucher votre YubiKey...';
// Convert the registration options to the format expected by the browser
+ console.log('Original registration options:', registrationOptions);
+
const publicKey = {
challenge: Uint8Array.from(atob(registrationOptions.challenge), c => c.charCodeAt(0)),
rp: registrationOptions.rp,
- user: registrationOptions.user,
+ user: {
+ ...registrationOptions.user,
+ id: (() => {
+ const id = registrationOptions.user.id;
+ console.log('User ID type:', typeof id, 'value:', id);
+
+ if (id instanceof ArrayBuffer) {
+ return new Uint8Array(id);
+ }
+
+ // Try base64 first (this is what PHP sends)
+ try {
+ return base64ToUint8Array(id);
+ } catch (e) {
+ console.warn('base64 conversion failed, trying other methods:', e.message);
+ }
+
+ // Try hex string
+ try {
+ if (/^[0-9a-f]+$/i.test(id)) {
+ const matches = id.match(/.{1,2}/g);
+ const bytes = new Uint8Array(matches.length);
+ for (let i = 0; i < matches.length; i++) {
+ bytes[i] = parseInt(matches[i], 16);
+ }
+ return bytes;
+ }
+ } catch (e) {
+ console.warn('Hex conversion failed:', e.message);
+ }
+
+ // Try regular base64
+ try {
+ const binaryString = atob(id);
+ const bytes = new Uint8Array(binaryString.length);
+ for (let i = 0; i < binaryString.length; i++) {
+ bytes[i] = binaryString.charCodeAt(i);
+ }
+ return bytes;
+ } catch (e) {
+ console.warn('Base64 conversion failed:', e.message);
+ }
+
+ // Last resort: treat as UTF-8 string
+ const encoder = new TextEncoder();
+ return encoder.encode(id);
+ })()
+ },
pubKeyCredParams: registrationOptions.pubKeyCredParams,
authenticatorSelection: registrationOptions.authenticatorSelection,
timeout: registrationOptions.timeout,
attestation: registrationOptions.attestation
};
+
+ console.log('Converted publicKey options:', publicKey);
// Call the WebAuthn API
+ console.log('Calling WebAuthn API with options:', registrationOptions);
const credential = await navigator.credentials.create({ publicKey });
if (credential) {
- // Send the attestation response to the server
- // Convert credential to a format that can be sent to the server
- const rawIdBase64 = arrayBufferToBase64(credential.rawId);
- const attestationObjectBase64 = arrayBufferToBase64(credential.response.attestationObject);
- const clientDataJSONBase64 = arrayBufferToBase64(credential.response.clientDataJSON);
-
- console.log('Raw ID (Base64):', rawIdBase64);
- console.log('Attestation Object (Base64):', attestationObjectBase64);
- console.log('Client Data JSON (Base64):', clientDataJSONBase64);
-
- // Vérifier que les données Base64 sont valides
- if (!rawIdBase64 || !attestationObjectBase64 || !clientDataJSONBase64) {
- throw new Error('Données Base64 invalides');
- }
-
- const attestationResponse = {
- id: credential.id,
- rawId: rawIdBase64,
- response: {
- attestationObject: attestationObjectBase64,
- clientDataJSON: clientDataJSONBase64
- },
- type: credential.type
- };
-
- console.log('Attestation response:', attestationResponse);
+ console.log('WebAuthn credential received:', credential);
+ console.log('Credential ID:', credential.id);
+ console.log('Credential type:', credential.type);
+ console.log('Credential rawId:', credential.rawId);
+ console.log('Credential response:', credential.response);
+ // Send the attestation response to the server
try {
+ console.log('Preparing credential for server (async)...');
+ const attestationResponse = await prepareCredentialForServer(credential);
+ console.log('Prepared attestation response:', attestationResponse);
+
const attestationResponseJSON = JSON.stringify(attestationResponse);
console.log('Attestation response JSON:', attestationResponseJSON);
const response = await fetch('register.php', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/x-www-form-urlencoded',
- },
- body: `attestationResponse=${encodeURIComponent(attestationResponseJSON)}`
- });
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ },
+ body: `attestationResponse=${encodeURIComponent(attestationResponseJSON)}`
+ });
const responseText = await response.text();
console.log('Server response (step 2 - attestation):', responseText);