$max) $v = mb_substr($v, 0, $max);
return preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', '', $v);
}
/**
* Clean a public key from any format to raw key content.
* Handles: PEM (any type), SSH format, hex, base64, raw paste.
*/
function cleanPublicKey(string $raw): string {
$key = trim($raw);
if (empty($key)) return '';
// Strip PEM headers/footers (-----BEGIN ... -----, -----END ... -----)
$key = preg_replace('/-----BEGIN [A-Z\s]+-----/', '', $key);
$key = preg_replace('/-----END [A-Z\s]+-----/', '', $key);
// Strip SSH prefix (ssh-ed25519, ssh-rsa, ecdsa-sha2-nistp256, etc.)
$key = preg_replace('/^(ssh-\S+|ecdsa-\S+)\s+/', '', trim($key));
// Strip SSH trailing comment (user@host)
$key = preg_replace('/\s+\S+@\S+\s*$/', '', $key);
$key = preg_replace('/\s+[^\s=+\/]+\s*$/', '', $key);
// Remove all whitespace and newlines
$key = preg_replace('/\s+/', '', $key);
return $key;
}
// ============================================
// IDENTICON GENERATION VIA CLI
// ============================================
function generateIdenticonBase64(string $input, int $size): ?string {
if (!is_executable(IDENTICONS_CLI)) return null;
$cmd = IDENTICONS_CLI . ' -input ' . escapeshellarg($input) .
' -size ' . intval($size) . ' -transparent -format base64 2>&1';
$out = trim(shell_exec($cmd) ?? '');
if (empty($out) || !preg_match('/^[A-Za-z0-9+\/=]+$/', $out)) return null;
return $out;
}
// ============================================
// FACE HEADER FOLDING (RFC 2822)
// ============================================
function foldFaceHeader(string $base64): string {
$maxFirst = 72 - strlen('Face: ');
$maxCont = 72 - 1;
$lines = ['Face: ' . substr($base64, 0, $maxFirst)];
$offset = $maxFirst;
while ($offset < strlen($base64)) {
$lines[] = ' ' . substr($base64, $offset, $maxCont);
$offset += $maxCont;
}
return implode("\n", $lines);
}
// ============================================
// ED25519 KEYPAIR GENERATION
// ============================================
function generateKeypair(): array {
$kp = sodium_crypto_sign_keypair();
$sk = sodium_crypto_sign_secretkey($kp);
$pk = sodium_crypto_sign_publickey($kp);
$result = [
'publicKey' => base64_encode($pk),
'secretKey' => base64_encode($sk),
'created' => gmdate('c'),
];
// Wipe sensitive material from memory
sodium_memzero($sk);
sodium_memzero($kp);
return $result;
}
// ============================================
// ED25519 SIGNATURE VERIFICATION
// ============================================
function verifySignature(string $message, string $signatureB64, string $pubkeyB64): bool {
$sig = base64_decode($signatureB64, true);
$pk = base64_decode($pubkeyB64, true);
if ($sig === false || $pk === false) return false;
if (strlen($sig) !== 64 || strlen($pk) !== 32) return false;
return sodium_crypto_sign_verify_detached($sig, $message, $pk);
}
// ============================================
// STATIC IDENTITY PAGE GENERATOR
// ============================================
function buildIdentityPageHTML(
string $username, string $email, string $pubkey,
string $hash, string $identicon256, string $faceBase64
): string {
$u = htmlspecialchars($username, ENT_QUOTES, 'UTF-8');
$e = htmlspecialchars($email, ENT_QUOTES, 'UTF-8');
$p = htmlspecialchars($pubkey, ENT_QUOTES, 'UTF-8');
$h = htmlspecialchars($hash, ENT_QUOTES, 'UTF-8');
$faceHeader = htmlspecialchars(foldFaceHeader($faceBase64), ENT_QUOTES, 'UTF-8');
$vfaceHeaders = htmlspecialchars(
"From: {$username} <{$email}>\n" .
"Ed25519-Pub: {$pubkey}\n" .
"Ed25519-Sig: [sign message body with your private key]\n" .
"Identity-Hash: {$hash}\n" .
foldFaceHeader($faceBase64),
ENT_QUOTES, 'UTF-8'
);
return <<
VFACE Identity — {$u}
VFACE Identity
VFACE Headers for Usenet / Email:
Copy these headers into your client. Replace the signature line after signing your message body.
{$vfaceHeaders}
How to verify this identity:
1. Concatenate this string:
Click to select all — then copy
2. Calculate SHA256 of that string — it should match the Identity Hash above.
3. Go to
identicons.virebent.art and use the Analyze tab to verify the identicon, or the Create tab to regenerate it from the same input.
4. For signed messages: verify the Ed25519 signature with the public key above.
5. Check the
.ots file alongside this page for Bitcoin timestamp proof (
opentimestamps.org ).
HTML;
}
// ============================================
// CH1FFR3PUNK PALETTES (for analysis)
// ============================================
const PRIMARY_PALETTE = [
[0x00, 0xbf, 0x93], [0x2d, 0xcc, 0x70], [0x42, 0xe4, 0x53], [0xf1, 0xc4, 0x0f],
[0xe6, 0x7f, 0x22], [0xff, 0x94, 0x4e], [0xe8, 0x4c, 0x3d], [0x35, 0x98, 0xdb],
[0x9a, 0x59, 0xb5], [0xef, 0x3e, 0x96], [0xdf, 0x21, 0xb9], [0x7d, 0xc2, 0xd2],
[0x16, 0xa0, 0x86], [0x27, 0xae, 0x61], [0x24, 0xc3, 0x33], [0x1c, 0xab, 0xbb],
];
const SECONDARY_PALETTE = [
[0x34, 0x49, 0x5e], [0x95, 0xa5, 0xa5], [0xd2, 0x54, 0x00], [0xc1, 0x39, 0x2b],
[0x29, 0x7f, 0xb8], [0x8d, 0x44, 0xad], [0xbe, 0x12, 0x7e], [0xe5, 0x23, 0x83],
[0x27, 0xae, 0x61], [0x24, 0xc3, 0x33], [0xd9, 0xd9, 0x21], [0xf3, 0x9c, 0x11],
[0xff, 0x55, 0x00], [0x1c, 0xab, 0xbb], [0x23, 0x23, 0x23], [0x7e, 0x8c, 0x8d],
];
const BACKGROUNDS = [
[255, 255, 255], [243, 245, 247], [236, 240, 241],
];
function colorsMatch(array $a, array $b, int $tolerance = 5): bool {
return abs($a['r'] - $b['r']) <= $tolerance &&
abs($a['g'] - $b['g']) <= $tolerance &&
abs($a['b'] - $b['b']) <= $tolerance;
}
function colorDistance(array $a, array $b): float {
return sqrt(pow($a[0] - $b[0], 2) + pow($a[1] - $b[1], 2) + pow($a[2] - $b[2], 2));
}
function analyzeIdenticon(string $filePath): array {
$result = [
'valid' => false, 'dimensions' => null, 'cell_size' => null,
'symmetric' => false, 'colors_found' => [],
'primary_color' => null, 'secondary_color' => null,
'background' => null, 'bg_match' => false,
'primary_match' => false, 'secondary_match' => false,
'palette_score' => 0, 'errors' => [], 'dataurl' => null,
];
$info = @getimagesize($filePath);
if ($info === false) { $result['errors'][] = 'Not a valid image file.'; return $result; }
if ($info[2] !== IMAGETYPE_PNG) { $result['errors'][] = 'Image must be PNG format.'; return $result; }
$width = $info[0]; $height = $info[1];
$result['dimensions'] = "{$width}x{$height}";
if ($width !== $height) { $result['errors'][] = 'Image is not square.'; return $result; }
if ($width < 10) { $result['errors'][] = 'Image too small.'; return $result; }
$spriteSize = 5;
$cellSize = intdiv($width, $spriteSize);
$margin = intdiv($width - $spriteSize * $cellSize, 2);
$result['cell_size'] = $cellSize;
$img = @imagecreatefrompng($filePath);
if (!$img) { $result['errors'][] = 'Failed to load PNG.'; return $result; }
if (!imageistruecolor($img)) { imagepalettetotruecolor($img); }
imagealphablending($img, false);
imagesavealpha($img, true);
ob_start(); imagepng($img); $pngData = ob_get_clean();
$result['dataurl'] = 'data:image/png;base64,' . base64_encode($pngData);
$grid = []; $colorMap = [];
for ($row = 0; $row < $spriteSize; $row++) {
$grid[$row] = [];
for ($col = 0; $col < $spriteSize; $col++) {
$px = min($col * $cellSize + $margin + intdiv($cellSize, 2), $width - 1);
$py = min($row * $cellSize + $margin + intdiv($cellSize, 2), $width - 1);
$rgba = imagecolorat($img, $px, $py);
$r = ($rgba >> 16) & 0xFF; $g = ($rgba >> 8) & 0xFF;
$b = $rgba & 0xFF; $a = ($rgba >> 24) & 0x7F;
$grid[$row][$col] = ['r' => $r, 'g' => $g, 'b' => $b, 'a' => $a];
if ($a < 64) { $colorMap["{$r},{$g},{$b}"] = [$r, $g, $b]; }
}
}
$symmetryOk = true;
for ($row = 0; $row < 5; $row++) {
for ($pair = 0; $pair < 2; $pair++) {
if (!colorsMatch($grid[$row][$pair], $grid[$row][4 - $pair], 5)) {
$symmetryOk = false; break 2;
}
}
}
$result['symmetric'] = $symmetryOk;
$result['colors_found'] = array_values($colorMap);
$bgColor = null; $fgColors = [];
$cornerColor = $grid[0][0];
foreach (BACKGROUNDS as $bg) {
if (colorsMatch(['r'=>$bg[0],'g'=>$bg[1],'b'=>$bg[2],'a'=>0], $cornerColor, 5)) {
$result['bg_match'] = true; $bgColor = $bg; break;
}
}
$hasTransparent = false;
for ($row = 0; $row < 5; $row++) {
for ($col = 0; $col < 5; $col++) {
if ($grid[$row][$col]['a'] >= 64) { $hasTransparent = true; break 2; }
}
}
if ($hasTransparent) { $result['bg_match'] = true; $result['background'] = 'transparent'; }
elseif ($bgColor) { $result['background'] = sprintf('#%02x%02x%02x', $bgColor[0], $bgColor[1], $bgColor[2]); }
foreach ($colorMap as $key => $rgb) {
if ($bgColor && colorsMatch(['r'=>$rgb[0],'g'=>$rgb[1],'b'=>$rgb[2],'a'=>0],
['r'=>$bgColor[0],'g'=>$bgColor[1],'b'=>$bgColor[2],'a'=>0], 5)) continue;
$fgColors[] = $rgb;
}
foreach ($fgColors as $fg) {
foreach (PRIMARY_PALETTE as $idx => $pal) {
if (colorDistance($fg, $pal) < 10) {
$result['primary_match'] = true;
$result['primary_color'] = ['rgb'=>$fg, 'hex'=>sprintf('#%02x%02x%02x',$fg[0],$fg[1],$fg[2]), 'index'=>$idx];
break 2;
}
}
}
foreach ($fgColors as $fg) {
if ($result['primary_color'] && colorDistance($fg, $result['primary_color']['rgb']) < 10) continue;
foreach (SECONDARY_PALETTE as $idx => $pal) {
if (colorDistance($fg, $pal) < 10) {
$result['secondary_match'] = true;
$result['secondary_color'] = ['rgb'=>$fg, 'hex'=>sprintf('#%02x%02x%02x',$fg[0],$fg[1],$fg[2]), 'index'=>$idx];
break 2;
}
}
}
$score = 0;
if ($result['symmetric']) $score += 30;
if ($result['bg_match']) $score += 20;
if ($result['primary_match']) $score += 25;
if ($result['secondary_match']) $score += 25;
$result['palette_score'] = $score;
$result['valid'] = ($result['symmetric'] && $result['bg_match'] &&
$result['primary_match'] && $result['secondary_match'] &&
count($colorMap) >= 2 && count($colorMap) <= 4);
imagedestroy($img);
return $result;
}
// ============================================
// PROCESS REQUESTS
// ============================================
$tab = $_POST['tab'] ?? 'create';
$result = null;
$verifyRes = null;
$anaResult = null;
$error = null;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$tab = $_POST['tab'] ?? 'create';
// ---- TAB: CREATE ----
if ($tab === 'create') {
$username = sanitize($_POST['username'] ?? '', 64);
$email = sanitize($_POST['email'] ?? '', 254);
$keyMode = $_POST['key_mode'] ?? 'generate';
$rawPubkey = sanitize($_POST['pubkey'] ?? '', 8192);
if (empty($username)) { $error = 'Pick a username.'; }
elseif (empty($email)) { $error = 'Enter an email address.'; }
elseif ($keyMode === 'existing' && empty($rawPubkey)) {
$error = 'Paste your public key.';
}
if (!$error) {
$keypair = null;
if ($keyMode === 'generate') {
if (!function_exists('sodium_crypto_sign_keypair')) {
$error = 'Server does not support Ed25519 (sodium extension missing).';
} else {
$keypair = generateKeypair();
$pubkey = $keypair['publicKey'];
}
} else {
$pubkey = cleanPublicKey($rawPubkey);
if (empty($pubkey)) {
$error = 'Could not extract a valid key from your input.';
}
}
$keyCleaned = isset($pubkey) && $rawPubkey !== $pubkey;
}
if (!$error) {
$input = $username . '|' . $email . '|' . $pubkey;
$hash = hash('sha256', $input);
$ico48 = generateIdenticonBase64($input, 48);
$ico256 = generateIdenticonBase64($input, 256);
if (!$ico48 || !$ico256) {
$error = 'Failed to generate identicon (binary not found).';
} else {
$result = [
'username' => $username,
'email' => $email,
'pubkey' => $pubkey,
'keypair' => $keypair,
'keyCleaned' => $keyCleaned ?? false,
'input' => $input,
'hash' => $hash,
'ico48' => $ico48,
'ico256' => $ico256,
'face' => foldFaceHeader($ico48),
'headers' => "From: {$username} <{$email}>\n" .
"Ed25519-Pub: {$pubkey}\n" .
"Ed25519-Sig: [sign message body with your private key]\n" .
"Identity-Hash: {$hash}\n" .
foldFaceHeader($ico48),
'identityPage' => buildIdentityPageHTML(
$username, $email, $pubkey, $hash, $ico256, $ico48
),
];
}
}
// ---- TAB: VERIFY ----
} elseif ($tab === 'verify') {
$vRawPubkey = sanitize($_POST['v_pubkey'] ?? '', 8192);
$vSig = sanitize($_POST['v_signature'] ?? '', 1024);
$vBody = $_POST['v_body'] ?? '';
$vUser = sanitize($_POST['v_username'] ?? '', 64);
$vEmail = sanitize($_POST['v_email'] ?? '', 254);
$vFace = sanitize($_POST['v_face'] ?? '', 8192);
$vPubkey = cleanPublicKey($vRawPubkey);
if (empty($vPubkey) || empty($vSig) || empty($vBody)) {
$error = 'Public key, signature, and message body are required.';
}
if (!$error) {
$sigValid = verifySignature($vBody, $vSig, $vPubkey);
$hashCheck = null;
$icoCheck = null;
$icoGenerated = null;
if (!empty($vUser) && !empty($vEmail)) {
$expectedInput = $vUser . '|' . $vEmail . '|' . $vPubkey;
$expectedHash = hash('sha256', $expectedInput);
$hashCheck = $expectedHash;
$icoGenerated = generateIdenticonBase64($expectedInput, 48);
if (!empty($vFace) && $icoGenerated) {
$cleanFace = trim(preg_replace('/^Face:\s*/m', '', $vFace));
$cleanFace = preg_replace('/\s+/', '', $cleanFace);
$icoCheck = ($cleanFace === $icoGenerated);
}
}
$verifyRes = [
'sig_valid' => $sigValid,
'hash_expected' => $hashCheck,
'ico_match' => $icoCheck,
'ico_generated' => $icoGenerated,
];
}
// ---- TAB: ANALYZE ----
} elseif ($tab === 'analyze') {
if (!isset($_FILES['identicon_file']) || $_FILES['identicon_file']['error'] !== UPLOAD_ERR_OK) {
$uploadErr = $_FILES['identicon_file']['error'] ?? UPLOAD_ERR_NO_FILE;
$error = match ($uploadErr) {
UPLOAD_ERR_NO_FILE => 'No file uploaded.',
UPLOAD_ERR_INI_SIZE,
UPLOAD_ERR_FORM_SIZE => 'File too large.',
default => 'Upload failed (error code: ' . $uploadErr . ').',
};
} else {
$file = $_FILES['identicon_file'];
if ($file['size'] > MAX_UPLOAD_SIZE) {
$error = 'File exceeds maximum size of ' . (MAX_UPLOAD_SIZE / 1024) . ' KB.';
} elseif ($file['size'] === 0) {
$error = 'Uploaded file is empty.';
} else {
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($file['tmp_name']);
if ($mime !== 'image/png') {
$error = 'Only PNG files are accepted (detected: ' . htmlspecialchars($mime) . ').';
} else {
$anaResult = analyzeIdenticon($file['tmp_name']);
if (!empty($anaResult['errors'])) {
$error = implode(' ', $anaResult['errors']);
}
// Optional: compare with identity
$anaUser = sanitize($_POST['ana_username'] ?? '', 64);
$anaEmail = sanitize($_POST['ana_email'] ?? '', 254);
$anaRawKey = sanitize($_POST['ana_pubkey'] ?? '', 8192);
$anaKey = cleanPublicKey($anaRawKey);
if (!empty($anaUser) && !empty($anaEmail) && !empty($anaKey) && $anaResult && !$error) {
$compareInput = $anaUser . '|' . $anaEmail . '|' . $anaKey;
$compareHash = hash('sha256', $compareInput);
// Read raw uploaded file and base64 it
$uploadedB64 = base64_encode(file_get_contents($file['tmp_name']));
// Generate at both sizes and try matching
$compareIco48 = generateIdenticonBase64($compareInput, 48);
$compareIco256 = generateIdenticonBase64($compareInput, 256);
$match = ($compareIco48 && $uploadedB64 === $compareIco48) ||
($compareIco256 && $uploadedB64 === $compareIco256);
$dims = explode('x', $anaResult['dimensions'] ?? '48x48');
$showSize = (intval($dims[0]) > 48) ? 256 : 48;
$showIco = ($showSize === 256) ? $compareIco256 : $compareIco48;
$anaResult['comparison'] = [
'hash' => $compareHash,
'generated' => $showIco,
'match' => $match,
];
}
}
}
}
}
}
// Preserve form values
$fUser = htmlspecialchars($_POST['username'] ?? '', ENT_QUOTES, 'UTF-8');
$fEmail = htmlspecialchars($_POST['email'] ?? '', ENT_QUOTES, 'UTF-8');
$fPubkey = htmlspecialchars($_POST['pubkey'] ?? '', ENT_QUOTES, 'UTF-8');
$fKeyMode = $_POST['key_mode'] ?? 'generate';
?>
VFACE — Your Identity, Cryptographically Yours
VFACE
Your pseudonym, cryptographically yours.
VFACE gives you a persistent identity that nobody can steal or fake.
Pick a name, get a unique visual fingerprint tied to your key.
Anyone can verify it's really you — without knowing who you are.
How does it work? Read the full explanation →
Reset
Create Identity
Analyze
Verify Signatures
= htmlspecialchars($error) ?>
Your VFACE Identity
This is your face.
Name
= htmlspecialchars($result['username']) ?>
Email
= htmlspecialchars($result['email']) ?>
Public Key
= htmlspecialchars($result['pubkey']) ?>
Your key was normalized — headers, prefixes, and formatting were removed.
The cleaned version above is what defines your identity.
Always use this exact value when verifying.
Identity Hash
= htmlspecialchars($result['hash']) ?>
Save your key pair now. This is the only time you'll see your private key.
Download the file below and store it somewhere safe. If you lose it, this identity is gone forever.
Nobody — including this server — has a copy.
= htmlspecialchars(json_encode([
'algorithm' => 'Ed25519',
'publicKey' => $result['keypair']['publicKey'],
'secretKey' => $result['keypair']['secretKey'],
'created' => $result['keypair']['created'],
'username' => $result['username'],
'email' => $result['email'],
'identityHash' => $result['hash'],
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)) ?>
Copy
VFACE Headers
Copy these into your Usenet/email client.
Replace the signature line after signing your message body.
Copy
What's next?
1
Save your key pair (if generated above).
Store the JSON file on an encrypted drive. Consider a YubiKey for maximum security.
2
Publish your identity page.
Host the static HTML page below on your website or .onion service. It's your public identity card.
3
Timestamp it on Bitcoin.
Run ots stamp identity-page.html to anchor your identity on the blockchain.
This proves when you first claimed this name — nobody can backdate a fake.
4
Start signing messages.
Use your private key to sign every message you send. Recipients verify with your public key.
Download Identity Page
A ready-to-publish static HTML page. Upload it to your web server
and timestamp it with OpenTimestamps for first-claim proof.
= htmlspecialchars($result['identityPage']) ?>
Copy
Upload an identicon image to check if it's a valid
Ch1ffr3punk
identicon. Optionally enter identity details to check if the image belongs
to a specific identity.
Analyze Image
Analysis
= 50) {
$verdictClass = 'unknown';
$verdictText = 'Partial match — some identicon properties detected (score: ' . $score . '/100)';
} else {
$verdictClass = 'fail';
$verdictText = 'Not a valid Ch1ffr3punk identicon (score: ' . $score . '/100)';
}
?>
= htmlspecialchars($verdictText) ?>
Horizontal symmetry (5×5 grid, mirrored columns)
Background: = $anaResult['background'] ? htmlspecialchars($anaResult['background']) : 'not recognized' ?>
Primary color:
= $anaResult['primary_color']['hex'] ?> (palette index = $anaResult['primary_color']['index'] ?>)
no match in primary palette
Secondary color:
= $anaResult['secondary_color']['hex'] ?> (palette index = $anaResult['secondary_color']['index'] ?>)
no match in secondary palette
Dimensions
= htmlspecialchars($anaResult['dimensions']) ?>
Cell Size
= $anaResult['cell_size'] ?>px
Unique Colors
= count($anaResult['colors_found']) ?>
Identity Comparison
This identicon matches the identity provided.
This identicon does NOT match the identity provided.
Identity Hash
= htmlspecialchars($anaResult['comparison']['hash']) ?>
Uploaded
Expected
Advanced — verify an Ed25519 signed message.
Paste the details from the message headers to check if the signature is genuine.
Public key (from Ed25519-Pub header)
= htmlspecialchars($_POST['v_pubkey'] ?? '') ?>
Signature (from Ed25519-Sig header)
Message body (the signed text)
= htmlspecialchars($_POST['v_body'] ?? '') ?>
Verify Signature
Verification Result
Signature is valid — this message is authentic.
Signature is INVALID — this message may be forged.
Identity Hash
= htmlspecialchars($verifyRes['hash_expected']) ?>
Face header matches the expected identicon.
Face header does NOT match — possible impersonation.
Expected identicon for this identity