Skip to content

Instantly share code, notes, and snippets.

@williamzujkowski
Created November 18, 2025 01:43
Show Gist options
  • Select an option

  • Save williamzujkowski/a69980ca2a6261aaabb00b84d3b8d853 to your computer and use it in GitHub Desktop.

Select an option

Save williamzujkowski/a69980ca2a6261aaabb00b84d3b8d853 to your computer and use it in GitHub Desktop.
Zero-Knowledge Authentication Client - Browser JavaScript
// Zero-Knowledge Authentication Client (Browser)
// Generates ZK-SNARK proofs without transmitting password
async function register(username, password) {
// Generate ZK circuit keys
const { publicKey, privateKey } = await generateZKKeys();
// Hash password locally
const passwordHash = await sha256(password);
// Encrypt private key with password (stored in browser)
const encryptedKey = await encryptKey(privateKey, password);
localStorage.setItem('zk_private_key', encryptedKey);
// Send only public key to server
const response = await fetch('/api/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
username: username,
public_key: publicKey,
password_hash: passwordHash
})
});
return await response.json();
}
async function login(username, password) {
// Get user's public key from server
const publicKeyResp = await fetch(`/api/public-key/${username}`);
const { publicKey, expectedHash } = await publicKeyResp.json();
// Retrieve encrypted private key from browser storage
const encryptedKey = localStorage.getItem('zk_private_key');
const privateKey = await decryptKey(encryptedKey, password);
// Generate ZK-SNARK proof locally (password never sent)
const proof = await generateZKProof({
public: {
username_hash: await sha256(username),
expected_hash: expectedHash
},
private: {
password: password // Stays in browser, never transmitted
}
}, privateKey);
// Send only proof to server
const authResp = await fetch('/api/auth/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
username: username,
proof: proof // ~400 bytes, no password info
})
});
const { access_token } = await authResp.json();
localStorage.setItem('jwt_token', access_token);
return access_token;
}
async function sha256(message) {
const msgBuffer = new TextEncoder().encode(message);
const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer);
return Array.from(new Uint8Array(hashBuffer))
.map(b => b.toString(16).padStart(2, '0'))
.join('');
}
async function generateZKKeys() {
// Actual implementation would use zksnark.js library
// Simplified for demonstration
return {
publicKey: crypto.randomUUID(),
privateKey: crypto.randomUUID()
};
}
async function generateZKProof(inputs, privateKey) {
// Actual implementation would use zksnark.js prove()
// Returns ~400 byte proof
return new Uint8Array(400);
}
async function encryptKey(key, password) {
// Encrypt private key with password using AES-GCM
const passwordKey = await crypto.subtle.importKey(
'raw',
new TextEncoder().encode(password),
{ name: 'PBKDF2' },
false,
['deriveKey']
);
const aesKey = await crypto.subtle.deriveKey(
{ name: 'PBKDF2', salt: crypto.getRandomValues(new Uint8Array(16)), iterations: 100000, hash: 'SHA-256' },
passwordKey,
{ name: 'AES-GCM', length: 256 },
false,
['encrypt']
);
const iv = crypto.getRandomValues(new Uint8Array(12));
const encrypted = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv: iv },
aesKey,
new TextEncoder().encode(key)
);
return btoa(String.fromCharCode(...new Uint8Array(encrypted)));
}
async function decryptKey(encryptedKey, password) {
// Decrypt private key with password
// Implementation mirrors encryptKey
return "decrypted_key"; // Simplified
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment