Skip to content

Instantly share code, notes, and snippets.

@mbutler
Last active February 9, 2025 16:21
Show Gist options
  • Select an option

  • Save mbutler/24ba7d2b46d21aa0cef9aa760da9126d to your computer and use it in GitHub Desktop.

Select an option

Save mbutler/24ba7d2b46d21aa0cef9aa760da9126d to your computer and use it in GitHub Desktop.
Zalvek the Cipher, a portable character with a body and spirt, the public and the private
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Cryptographic Character: Signed Versioning</title>
<style>
/* Basic styling */
body {
font-family: sans-serif;
margin: 20px;
background: #f8f8f8;
}
.character-container {
max-width: 600px;
margin: auto;
background: #fff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 6px rgba(0,0,0,0.15);
}
.section {
margin-top: 20px;
border-top: 1px solid #ccc;
padding-top: 10px;
}
button {
margin: 5px;
padding: 10px 15px;
font-size: 1rem;
}
input[type="file"] {
margin: 5px 0;
}
textarea {
width: 100%;
box-sizing: border-box;
}
#status, #keyStatus, #challengeStatus, #versionStatus {
white-space: pre-wrap;
background: #eef;
padding: 10px;
border: 1px solid #ccd;
border-radius: 4px;
}
</style>
<!--
We store version data in meta tags:
- name="character-version"
- name="character-timestamp"
- name="version-signature"
- name="public-key"
The JavaScript will read/write these to sign or verify version info.
-->
<meta name="character-version" content="1" />
<meta name="character-timestamp" content="" />
<meta name="version-signature" content="" />
<meta name="public-key" content="" />
</head>
<body>
<div class="character-container">
<!-- Character Header -->
<div class="section">
<h1 id="charName">Zalvek the Cipher</h1>
<p id="charClass">Class: Ciphermage | Level: 3</p>
<p><strong>Public Key (Body):</strong></p>
<!-- Display the public key -->
<textarea id="publicKeyDisplay" rows="6" readonly></textarea>
</div>
<!-- Key Management Section -->
<div class="section">
<h2>Key Management</h2>
<button id="generateKeyBtn">Generate New Key Pair</button>
<button id="downloadPrivateKeyBtn" disabled>Download Private Key</button>
<br>
<label for="loadPrivateKey">Reload Private Key (JSON):</label>
<input type="file" id="loadPrivateKey" accept=".json">
<button id="verifyKeyPairBtn" disabled>Verify Key Pair</button>
<p id="keyStatus">Status messages will appear here.</p>
</div>
<!-- Challenge/Response Section -->
<div class="section">
<h2>Challenge/Response Demo</h2>
<p>
Generate a random challenge, sign it with your private key, and then verify the signature using the public key.
</p>
<button id="generateChallengeBtn">Generate Challenge</button>
<p>Challenge: <span id="challengeDisplay"></span></p>
<button id="signChallengeBtn" disabled>Sign Challenge</button>
<p>Signature:</p>
<!-- Use a textarea for the challenge signature -->
<textarea id="challengeSignatureDisplay" rows="4" style="width: 100%;" readonly></textarea>
<button id="verifyChallengeBtn" disabled>Verify Challenge Response</button>
<p id="challengeStatus">Challenge status messages will appear here.</p>
</div>
<!-- Signed Versioning Section -->
<div class="section">
<h2>Signed Versioning</h2>
<p>Version: <span id="versionNumber"></span></p>
<p>Timestamp: <span id="versionTimestamp"></span></p>
<p>Version Signature:</p>
<textarea id="versionSignatureDisplay" rows="4" style="width: 100%;" readonly></textarea>
<br>
<button id="signVersionBtn" disabled>Sign & Export New Version</button>
<button id="verifyVersionBtn" disabled>Verify Version Signature</button>
<p id="versionStatus">Version status messages will appear here.</p>
</div>
</div>
<!-- Inline JavaScript -->
<script>
// ===============
// GLOBALS
// ===============
let privateKey = null
let publicKeyPem = ""
let currentChallenge = ""
let currentSignature = ""
// We'll keep track of the version data from meta tags
let currentVersion = 1
let versionSignature = ""
let versionTimestamp = ""
// ===============
// ON LOAD
// ===============
window.addEventListener("load", () => {
// 1. Retrieve the public key from <meta> and set it in the textarea
const publicKeyMeta = document.querySelector('meta[name="public-key"]')
if (publicKeyMeta && publicKeyMeta.content) {
publicKeyPem = publicKeyMeta.content
document.getElementById("publicKeyDisplay").value = publicKeyPem
}
// 2. Read version info from meta tags
const versionMeta = document.querySelector('meta[name="character-version"]')
const timestampMeta = document.querySelector('meta[name="character-timestamp"]')
const signatureMeta = document.querySelector('meta[name="version-signature"]')
if (versionMeta && versionMeta.content) {
currentVersion = parseInt(versionMeta.content, 10) || 1
}
if (timestampMeta && timestampMeta.content) {
versionTimestamp = timestampMeta.content
}
if (signatureMeta && signatureMeta.content) {
versionSignature = signatureMeta.content
}
// 3. Update the displayed version info
updateVersionUI()
})
// Helper function to update version UI
function updateVersionUI() {
document.getElementById("versionNumber").innerText = currentVersion
document.getElementById("versionTimestamp").innerText = versionTimestamp
document.getElementById("versionSignatureDisplay").value = versionSignature
}
// ===============
// KEY GENERATION & MANAGEMENT
// ===============
async function generateKeyPair() {
try {
const keyPair = await crypto.subtle.generateKey(
{
name: "RSASSA-PKCS1-v1_5",
modulusLength: 4096,
publicExponent: new Uint8Array([1, 0, 1]),
hash: "SHA-512"
},
true,
["sign", "verify"]
)
privateKey = keyPair.privateKey
publicKeyPem = await exportPublicKeyAsPEM(keyPair.publicKey)
// Display the public key in the textarea
document.getElementById("publicKeyDisplay").value = publicKeyPem
document.getElementById("downloadPrivateKeyBtn").disabled = false
document.getElementById("signChallengeBtn").disabled = false
document.getElementById("verifyChallengeBtn").disabled = false
document.getElementById("verifyKeyPairBtn").disabled = false
document.getElementById("signVersionBtn").disabled = false
document.getElementById("versionStatus").innerText = ""
document.getElementById("keyStatus").innerText = "✅ Key pair generated successfully!"
} catch (error) {
console.error("Key generation error:", error)
document.getElementById("keyStatus").innerText = "❌ Error generating key pair."
}
}
async function exportPublicKeyAsPEM(publicKey) {
const exported = await crypto.subtle.exportKey("spki", publicKey)
const exportedAsString = btoa(String.fromCharCode(...new Uint8Array(exported)))
return `-----BEGIN PUBLIC KEY-----\n${exportedAsString}\n-----END PUBLIC KEY-----`
}
async function downloadPrivateKey() {
if (!privateKey) {
document.getElementById("keyStatus").innerText = "❌ No private key available. Generate a key pair first."
return
}
try {
const exportedPrivateKey = await crypto.subtle.exportKey("pkcs8", privateKey)
const privateKeyBase64 = btoa(String.fromCharCode(...new Uint8Array(exportedPrivateKey)))
const privateKeyJSON = JSON.stringify({
pem: `-----BEGIN PRIVATE KEY-----\n${privateKeyBase64}\n-----END PRIVATE KEY-----`
}, null, 2)
const blob = new Blob([privateKeyJSON], { type: "application/json" })
const link = document.createElement("a")
link.href = URL.createObjectURL(blob)
link.download = "spirit.json"
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
document.getElementById("keyStatus").innerText = "✅ Private key downloaded!"
} catch (error) {
console.error("Error exporting private key:", error)
document.getElementById("keyStatus").innerText = "❌ Error exporting private key."
}
}
document.getElementById("loadPrivateKey").addEventListener("change", event => {
const file = event.target.files[0]
if (!file) return
const reader = new FileReader()
reader.onload = async e => {
try {
const keyData = JSON.parse(e.target.result)
privateKey = await importPrivateKey(keyData.pem)
document.getElementById("keyStatus").innerText = "✅ Private key loaded successfully!"
document.getElementById("signChallengeBtn").disabled = false
document.getElementById("verifyChallengeBtn").disabled = false
document.getElementById("verifyKeyPairBtn").disabled = false
document.getElementById("signVersionBtn").disabled = false
} catch (err) {
console.error(err)
document.getElementById("keyStatus").innerText = "❌ Error loading private key."
}
}
reader.readAsText(file)
})
async function importPrivateKey(pem) {
const pemHeaderFooterRemoved = pem.replace(/-----[^-]+-----/g, "").replace(/\s/g, "")
const binaryDerString = atob(pemHeaderFooterRemoved)
const binaryDer = new Uint8Array(binaryDerString.length)
for (let i = 0; i < binaryDerString.length; i++) {
binaryDer[i] = binaryDerString.charCodeAt(i)
}
return crypto.subtle.importKey(
"pkcs8",
binaryDer.buffer,
{ name: "RSASSA-PKCS1-v1_5", hash: "SHA-512" },
true,
["sign"]
)
}
async function importPublicKey(pem) {
const pemHeaderFooterRemoved = pem.replace(/-----[^-]+-----/g, "").replace(/\s/g, "")
const binaryDerString = atob(pemHeaderFooterRemoved)
const binaryDer = new Uint8Array(binaryDerString.length)
for (let i = 0; i < binaryDerString.length; i++) {
binaryDer[i] = binaryDerString.charCodeAt(i)
}
return crypto.subtle.importKey(
"spki",
binaryDer.buffer,
{ name: "RSASSA-PKCS1-v1_5", hash: "SHA-512" },
true,
["verify"]
)
}
async function verifyKeyPair() {
if (!privateKey) {
document.getElementById("keyStatus").innerText = "❌ No private key loaded."
return
}
const testChallenge = "testchallenge"
try {
const signature = await signChallenge(testChallenge)
const valid = await verifyChallenge(testChallenge, signature)
if (valid) {
document.getElementById("keyStatus").innerText = "✅ The loaded private key matches the public key."
} else {
document.getElementById("keyStatus").innerText = "❌ The loaded private key does NOT match the public key."
}
} catch (err) {
document.getElementById("keyStatus").innerText = "❌ Error verifying key pair: " + err
}
}
document.getElementById("generateKeyBtn").addEventListener("click", generateKeyPair)
document.getElementById("downloadPrivateKeyBtn").addEventListener("click", downloadPrivateKey)
document.getElementById("verifyKeyPairBtn").addEventListener("click", verifyKeyPair)
// ===============
// CHALLENGE/RESPONSE
// ===============
function generateChallenge() {
currentChallenge = Math.random().toString(36).substring(2, 10)
document.getElementById("challengeDisplay").innerText = currentChallenge
document.getElementById("challengeStatus").innerText = "Challenge generated."
}
async function signChallenge(challenge) {
const encoder = new TextEncoder()
const challengeBuffer = encoder.encode(challenge)
if (!privateKey) {
throw new Error("No private key loaded.")
}
const signatureBuffer = await crypto.subtle.sign(
{ name: "RSASSA-PKCS1-v1_5" },
privateKey,
challengeBuffer
)
return btoa(String.fromCharCode(...new Uint8Array(signatureBuffer)))
}
async function verifyChallenge(challenge, signature) {
const encoder = new TextEncoder()
const challengeBuffer = encoder.encode(challenge)
const signatureBuffer = Uint8Array.from(atob(signature), c => c.charCodeAt(0))
const pubKey = await importPublicKey(publicKeyPem)
const valid = await crypto.subtle.verify(
{ name: "RSASSA-PKCS1-v1_5" },
pubKey,
signatureBuffer,
challengeBuffer
)
return valid
}
document.getElementById("generateChallengeBtn").addEventListener("click", () => {
generateChallenge()
document.getElementById("signChallengeBtn").disabled = false
})
document.getElementById("signChallengeBtn").addEventListener("click", async () => {
if (!currentChallenge) {
document.getElementById("challengeStatus").innerText = "❌ Please generate a challenge first."
return
}
try {
currentSignature = await signChallenge(currentChallenge)
document.getElementById("challengeSignatureDisplay").value = currentSignature
document.getElementById("challengeStatus").innerText = "Challenge signed."
} catch (err) {
console.error(err)
document.getElementById("challengeStatus").innerText = "❌ Error signing challenge: " + err
}
})
document.getElementById("verifyChallengeBtn").addEventListener("click", async () => {
if (!currentChallenge || !currentSignature) {
document.getElementById("challengeStatus").innerText = "❌ Missing challenge or signature."
return
}
try {
const valid = await verifyChallenge(currentChallenge, currentSignature)
if (valid) {
document.getElementById("challengeStatus").innerText = "✅ Challenge signature verified!"
} else {
document.getElementById("challengeStatus").innerText = "❌ Challenge signature verification failed."
}
} catch (err) {
console.error(err)
document.getElementById("challengeStatus").innerText = "❌ Error verifying challenge: " + err
}
})
// ===============
// SIGNED VERSIONING
// ===============
async function signAndExportNewVersion() {
if (!privateKey) {
document.getElementById("versionStatus").innerText = "❌ Private key not loaded."
return
}
try {
// 1. Increment version
currentVersion += 1
// 2. Generate a new timestamp
versionTimestamp = new Date().toISOString()
// 3. Create version metadata object
const versionData = {
version: currentVersion,
timestamp: versionTimestamp,
publicKey: publicKeyPem
}
// 4. Sign this metadata
const encoder = new TextEncoder()
const dataString = JSON.stringify(versionData)
const dataBuffer = encoder.encode(dataString)
const signatureBuffer = await crypto.subtle.sign(
{ name: "RSASSA-PKCS1-v1_5" },
privateKey,
dataBuffer
)
versionSignature = btoa(String.fromCharCode(...new Uint8Array(signatureBuffer)))
// 5. Update the meta tags
document.querySelector('meta[name="character-version"]').setAttribute("content", currentVersion)
document.querySelector('meta[name="character-timestamp"]').setAttribute("content", versionTimestamp)
document.querySelector('meta[name="version-signature"]').setAttribute("content", versionSignature)
// **Fix**: Make sure the public key is in the <meta> so it loads next time
document.querySelector('meta[name="public-key"]').setAttribute("content", publicKeyPem)
// 6. Update local UI
updateVersionUI()
// 7. Export updated HTML
exportUpdatedCharacterFile()
document.getElementById("versionStatus").innerText = `✅ Signed new version (${currentVersion}) and exported file.`
} catch (err) {
console.error("Error signing version:", err)
document.getElementById("versionStatus").innerText = "❌ Error signing version: " + err
}
}
function exportUpdatedCharacterFile() {
const updatedHTML = document.documentElement.outerHTML
const blob = new Blob([updatedHTML], { type: "text/html" })
// Example: name the file using the version number
const filename = `character_v${currentVersion}.html`
const link = document.createElement("a")
link.href = URL.createObjectURL(blob)
link.download = filename
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
}
async function verifyCurrentVersion() {
if (!publicKeyPem) {
document.getElementById("versionStatus").innerText = "❌ Public key not set."
return
}
if (!versionSignature) {
document.getElementById("versionStatus").innerText = "❌ No version signature found."
return
}
try {
// Re-create the version metadata object
const versionData = {
version: currentVersion,
timestamp: versionTimestamp,
publicKey: publicKeyPem
}
const dataString = JSON.stringify(versionData)
const encoder = new TextEncoder()
const dataBuffer = encoder.encode(dataString)
// Convert the stored signature from base64
const signatureBuffer = Uint8Array.from(
atob(versionSignature),
c => c.charCodeAt(0)
)
// Import the public key and verify
const pubKey = await importPublicKey(publicKeyPem)
const valid = await crypto.subtle.verify(
{ name: "RSASSA-PKCS1-v1_5" },
pubKey,
signatureBuffer,
dataBuffer
)
if (valid) {
document.getElementById("versionStatus").innerText = `✅ Version ${currentVersion} signature is valid.`
} else {
document.getElementById("versionStatus").innerText = `❌ Version ${currentVersion} signature is invalid.`
}
} catch (err) {
console.error("Error verifying version:", err)
document.getElementById("versionStatus").innerText = "❌ Error verifying version: " + err
}
}
document.getElementById("signVersionBtn").addEventListener("click", signAndExportNewVersion)
document.getElementById("verifyVersionBtn").addEventListener("click", verifyCurrentVersion)
</script>
</body>
</html>
@mbutler
Copy link
Copy Markdown
Author

mbutler commented Feb 6, 2025

Cryptographic Character: Self-Sovereign HTML Identity

1) Project Overview

This project demonstrates how to create a self-contained, cryptographic “character” stored in a single HTML file. Rather than relying on centralized servers or accounts, each character file has its public key (the “body”) embedded, while the private key (the “spirit”) can be downloaded separately. The character can sign and verify data offline, ensuring trust without traditional logins or blockchain infrastructure.

Core Principles

  • Self-Sovereign Identity: Each character uses a cryptographic key pair (RSA-4096). The public key (body) is embedded in the HTML, and the private key (spirit) is stored by the user.
  • Single-File Portability: A character is distributed as a single HTML file, containing all necessary logic.
  • Offline-First: The file can be opened locally (via file://), and all cryptographic operations work without an internet connection.
  • No Centralized Verification: Challenge-response mechanics prove ownership of the private key without relying on external services.
  • Self-Signed Versioning: Each new version of the character HTML file is cryptographically tied to its previous version.

2) Implementation Details

  1. Key Pair Generation:

    • The character supports generating an RSA key pair (4096-bit) via the Web Crypto API.
    • The public key is displayed in a <textarea> and also embedded in <meta> tags for reloading.
    • The private key remains in memory, never automatically saved.
  2. Downloading/Reloading the Private Key:

    • The user can export their private key ("spirit.json").
    • Later, they can reload it for cryptographic signing.
  3. Challenge/Response Mechanism:

    • A random challenge is generated in the browser.
    • The user signs the challenge with their private key.
    • Verification uses the embedded public key.
  4. Signed Versioning:

    • A signAndExportNewVersion() function increments the version, timestamps it, and signs the resulting JSON metadata.
    • The <meta> tags are updated with the new version, timestamp, and Base64-encoded signature.
    • Finally, the file is exported (e.g., character_v2.html), containing updated <meta> data.
    • Verification involves re-creating the metadata and ensuring the signature matches.

3) How It Works

  1. Offline, Single-File Execution:
    • You open the HTML file in any modern browser.
    • All cryptographic operations occur locally using the Web Crypto API.
  2. Key Management:
    • The user clicks "Generate Key Pair", which produces a new RSA key pair.
    • The file updates <textarea id="publicKeyDisplay"> with the public key.
    • The private key is stored in a JavaScript variable until the user downloads it.
  3. Challenge/Response Demo:
    • A user can create a random challenge.
    • If the private key is loaded, the challenge can be signed.
    • The signature is then verified using the same file’s public key.
  4. Versioning:
    • Each version is numbered and time-stamped.
    • A JSON payload (version, timestamp, public key) is signed using the private key.
    • <meta> tags are updated with the resulting signature.
    • The user downloads a new HTML file, e.g. character_v2.html, preserving offline identity.

4) Technical Details

  1. RSA Key Generation

    const keyPair = await crypto.subtle.generateKey(
      {
        name: "RSASSA-PKCS1-v1_5",
        modulusLength: 4096,
        publicExponent: new Uint8Array([1, 0, 1]),
        hash: "SHA-512"
      },
      true,
      ["sign", "verify"]
    )
    • Creates a public/private key pair suitable for signing (sign) and verification (verify).
  2. Exporting the Public Key

    const exported = await crypto.subtle.exportKey("spki", publicKey)
    const exportedAsString = btoa(String.fromCharCode(...new Uint8Array(exported)))
    const publicKeyPem = `-----BEGIN PUBLIC KEY-----\n${exportedAsString}\n-----END PUBLIC KEY-----`
  3. Private Key Management

    • Exporting the private key uses pkcs8 format:
      const exportedPrivateKey = await crypto.subtle.exportKey("pkcs8", privateKey)
      // Convert to Base64, package in JSON
    • The user downloads spirit.json. Reloading it imports the private key (importPrivateKey()).
  4. Challenge/Response

    async function signChallenge(challenge) {
      const encoder = new TextEncoder()
      const challengeBuffer = encoder.encode(challenge)
      const signatureBuffer = await crypto.subtle.sign(
        { name: "RSASSA-PKCS1-v1_5" },
        privateKey,
        challengeBuffer
      )
      return btoa(String.fromCharCode(...new Uint8Array(signatureBuffer)))
    }
    • The verifying side re-imports the public key to call crypto.subtle.verify(...).
  5. Signed Versioning

    • The function signAndExportNewVersion() increments currentVersion, timestamps it, and builds a JSON payload:
      const versionData = {
        version: currentVersion,
        timestamp: versionTimestamp,
        publicKey: publicKeyPem
      }
    • This payload is then signed with the private key.
    • <meta> tags (character-version, character-timestamp, version-signature, public-key) are updated.
    • The entire HTML (document.documentElement.outerHTML) is exported as character_vX.html.

5) Future Improvements

  1. Full Version History:

    • Instead of storing only the latest version number and timestamp, embed a JSON ledger of all versions in the HTML. This would enable a complete timeline of changes, though it increases file size.
  2. Branch Merging:

    • If a user has multiple divergent versions, merging them is currently manual. A more advanced system could track merges by referencing previous version hashes.
  3. Encryption / Secret Data:

    • Add optional encryption of certain fields, requiring the private key not only for signing but also for decryption.
  4. Decentralized Discovery:

    • Characters can be shared peer-to-peer. For multi-user scenarios, tools like Gun.js or WebRTC could help discover or exchange these HTML files.
  5. Alternative Algorithms:

    • RSA-4096 is robust but not quantum-safe. Exploring elliptic-curve (e.g., P-256, Ed25519) or post-quantum signatures could future-proof the project.

This proof-of-concept shows that self-sovereign, cryptographically-secured identities can be packaged in a single HTML file, offline and without reliance on central servers. By combining challenge/response, private key management, and self-signed versioning, the system ensures each character is both verifiable and portable—paving the way for decentralized identity in games, creative projects, or beyond.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment