$msg]); exit; } if ($_SERVER['REQUEST_METHOD'] !== 'POST') fail('POST only', 405); if (!is_executable(IDENTICONS_CLI)) fail('engine unavailable', 503); $username = trim($_POST['username'] ?? ''); $email = trim($_POST['email'] ?? ''); $pubkey = trim($_POST['pubkey'] ?? ''); // Validation: public fields only. No '|' (identity-string separator) allowed inside. if ($username === '' || mb_strlen($username) > 64 || strpos($username, '|') !== false) fail('bad username'); if ($email === '' || mb_strlen($email) > 128 || strpos($email, '|') !== false) fail('bad email'); if (!preg_match('#^[A-Za-z0-9+/=]{1,128}$#', $pubkey)) fail('bad pubkey'); // Identity string = exact format consumed by identicons-cli / identicons.virebent.art $input = $username . '|' . $email . '|' . $pubkey; function identiconB64(string $input, int $size): ?string { $cmd = IDENTICONS_CLI . ' -input ' . escapeshellarg($input) . ' -size ' . intval($size) . ' -transparent -format base64 2>/dev/null'; $out = trim(shell_exec($cmd) ?? ''); return ($out !== '' && preg_match('#^[A-Za-z0-9+/=]+$#', $out)) ? $out : null; } // Single-line Face: header (no folding) so the base64 stays clean in the // saved identity JSON and after downstream RFC unfolding. ~222 octets < 998. function foldFaceHeader(string $base64): string { return 'Face: ' . $base64; } $face48 = identiconB64($input, 48); $face256 = identiconB64($input, 256); if ($face48 === null || $face256 === null) fail('generation failed', 500); echo json_encode([ 'hash' => hash('sha256', $input), 'face48' => $face48, // raw base64 PNG (48x48) -> Face: header 'faceHeader' => foldFaceHeader($face48), // ready-to-paste folded Face: header 'preview' => 'data:image/png;base64,' . $face256, // 256x256 dataurl for display ]);