In the age of AI-driven phishing, voice cloning, and automated social engineering, traditional authentication is fundamentally broken. Passwords are leaked, SMS 2FA is intercepted, and even biometrics can be spoofed.
But there is one thing an AI, no matter how powerful, can never steal: a secret that has never touched the internet.
Enter CryptoLogin.
In this tutorial, we will build a Zero-Knowledge DataVault from scratch. Your master_secret (e.g., a 32+ character phrase based on family nicknames, kept on a piece of paper in your wallet) will never leave your device. The server will never see it, store it, or even know what it is.
Let's build it.
๐๏ธ The Zero-Knowledge Architecture
Unlike traditional systems that send a password (or its hash) to the server
for verification, CryptoLogin reverses the logic. The server holds no secrets.
It only issues mathematical challenges that only the holder of the
master_secret can solve.
Here is the 3-step authentication flow:
Local Derivation: Your device derives a unique user_id from your master_secret using PBKDF2-HMAC-SHA512 (100,000 iterations). The secret never leaves your machine.
Server Challenge: Your device sends the user_id to the server. The server generates a random cryptographic challenge (nonce) and sends it back.
Local HMAC Signing: Your device signs the challenge locally using HMAC-SHA256 and the derived key. It sends only the signature to the server. The server verifies it in constant time.
The Result? If a hacker breaches the server tomorrow, they will only find hex-encoded user_ids and random challenges. Zero passwords. Zero emails. Zero risk.
๐ Step 1: Spin Up the Auth Server (2 Commands)
The CryptoLogin ecosystem is designed for frictionless developer experience. No complex configurations, no heavy dependencies.
Open your terminal and run:
# 1. Install the server package
pip install cryptologin[server]
# 2. Initialize the project (automatically creates `cryptologin_db`)
cryptologin init
# 3. Start the server (defaults to http://localhost:8000)
cryptologin run
โ Thatโs it. Your Zero-Knowledge authentication server is now running and ready to accept challenges.
๐ Step 2: Basic Zero-Knowledge Authentication (Node.js)
Letโs create a simple client to prove the concept.
mkdir basic-auth && cd basic-auth
npm init -y
npm install cryptologin-client
Create an index.js file:
import { createClient } from "cryptologin-client";
// Your 32+ char master secret. In production, read this from a local file or hardware key.
const MASTER_SECRET = "uncle-jean-1985-marseille-secret-32";
const client = createClient({ baseURL: "http://localhost:8000/api/v1" });
async function demo() {
try {
// 1. Register (Derives user_id locally via PBKDF2)
await client.register(MASTER_SECRET, { username: "demo-user" });
console.log("โ
Registered locally.\n");
// 2. Login (Solves the server's HMAC challenge locally)
const session = await client.login(MASTER_SECRET);
console.log(`๐ SUCCESS! Authenticated. Session: ${session.sessionId}`);
console.log("๐ช The server NEVER saw your master_secret.");
} catch (error) {
console.error("โ Error:", error.message);
}
}
demo();
Run it with node index.js. You will see the local derivation and HMAC signing happen in a fraction of a second.
๐๏ธ Step 3: Build the End-to-End Encrypted DataVault
Authentication is great, but what about storing sensitive data? We will now build a DataVault where data is encrypted locally before being sent to the server, using AES-256-GCM (the military-grade standard).
The Vault Server (Python)
Create vault_server.py. Notice the CORS middleware, allowing our local browser demo to communicate with it securely.
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
import sqlite3, json
app = FastAPI()
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])
db = sqlite3.connect("vault.db", check_same_thread=False)
db.execute("CREATE TABLE IF NOT EXISTS vault (label TEXT PRIMARY KEY, ciphertext TEXT)")
class VaultItem(BaseModel):
label: str
ciphertext: dict
@app.post("/api/v1/vault/store")
async def store_item(item: VaultItem):
db.execute("INSERT OR REPLACE INTO vault (label, ciphertext) VALUES (?, ?)",
(item.label, json.dumps(item.ciphertext)))
db.commit()
return {"status": "stored"}
@app.get("/api/v1/vault/retrieve/{label}")
async def retrieve_item(label: str):
row = db.execute("SELECT ciphertext FROM vault WHERE label = ?", (label,)).fetchone()
if not row: raise HTTPException(404, "Item not found")
return {"label": label, "ciphertext": json.loads(row[0])}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="127.0.0.1", port=8001)
Run it:
python vault_server.py
The Local Encryption Client (Node.js)
Create client.js in the same folder:
import { createClient } from "cryptologin-client";
import { webcrypto } from "crypto";
const MASTER_SECRET = "uncle-jean-1985-marseille-secret-32";
const client = createClient({ baseURL: "http://localhost:8000/api/v1" });
const VAULT_URL = "http://localhost:8001/api/v1/vault";
// ๐ Derive AES-256 key locally
async function deriveKey(secret) {
const enc = new TextEncoder();
const keyMaterial = await webcrypto.subtle.importKey("raw", enc.encode(secret), "PBKDF2", false, ["deriveKey"]);
return webcrypto.subtle.deriveKey(
{ name: "PBKDF2", salt: enc.encode("cryptologin-datavault-salt-v1"), iterations: 100000, hash: "SHA-256" },
keyMaterial, { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"]
);
}
// ๐ Encrypt locally
async function encryptData(secret, plaintext) {
const key = await deriveKey(secret);
const iv = webcrypto.getRandomValues(new Uint8Array(12));
const ciphertext = await webcrypto.subtle.encrypt({ name: "AES-GCM", iv }, key, new TextEncoder().encode(plaintext));
return { iv: Buffer.from(iv).toString("base64"), data: Buffer.from(ciphertext).toString("base64") };
}
// ๐ Decrypt locally
async function decryptData(secret, encrypted) {
const key = await deriveKey(secret);
const plaintext = await webcrypto.subtle.decrypt(
{ name: "AES-GCM", iv: Buffer.from(encrypted.iv, "base64") },
key, Buffer.from(encrypted.data, "base64")
);
return new TextDecoder().decode(plaintext);
}
async function runVault() {
console.log("๐ [DataVault] E2E Encryption Test...\n");
// 1. Auth (with auto-register fallback)
let session;
try { session = await client.login(MASTER_SECRET); }
catch {
await client.register(MASTER_SECRET, { username: "demo-user" });
session = await client.login(MASTER_SECRET);
}
console.log("โ
Authenticated.\n");
// 2. Local Encryption
const secretMsg = "My Swiss account number is CH93 0076 2011 6238 5295 745 796 100.";
console.log("๐ Original:", secretMsg);
const encrypted = await encryptData(MASTER_SECRET, secretMsg);
console.log("\n๐ Encrypted payload sent to server:\n", encrypted);
// 3. Store on blind server
await fetch(`${VAULT_URL}/store`, {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ label: "bank-account", ciphertext: encrypted })
});
console.log("\n๐พ Stored on server. The server only sees random noise.");
// 4. Retrieve & Local Decryption
const res = await fetch(`${VAULT_URL}/retrieve/bank-account`);
const data = await res.json();
const decrypted = await decryptData(MASTER_SECRET, data.ciphertext);
console.log("\n๐ Decrypted locally:", decrypted);
console.log("\nโจ PROVEN: The server NEVER saw the plaintext.");
}
runVault().catch(console.error);
Run "node client.js". Watch the magic happen: the message is encrypted locally, sent as gibberish, and perfectly reconstructed on your machine.
๐ Try the Visual Demo Yourself
Don't just take my word for it. I built a standalone, dependency-free HTML demo so you can see the Web Crypto API in action right in your browser.
๐ Live Demo Page
Type a secret, encrypt a message, and watch the server blindly store the Base64 ciphertext. Then, decrypt it locally.
๐ก Conclusion: The Secret in Your Wallet
We live in an era where AI can clone voices, generate perfect phishing emails, and automate social engineering at an industrial scale. Passwords, SMS codes, and even biometrics are becoming attack vectors.
But there is one thing no AI can ever steal: what you keep in your pocket.
A 32-character master_secret based on family nicknames, an intimate secret phrase, kept on a piece of paper in your wallet. No cloud. No network. No possible leak.
CryptoLogin doesn't reinvent security. It rehabilitates it. It brings control back to where it should have never left: in your hands.
"Not your keys, not your crypto. Not your master_secret, not your account."
๐ Resources & Links
- ๐ป Full Tutorial Source Code: github.com/erabytse/cryptologin-tutorial
- ๐ง Main CryptoLogin Repo: github.com/erabytse/CryptoLogin
- ๐ PyPI (Server): pypi.org/project/cryptologin/
- ๐ฆ npm (Client SDK): npmjs.com/package/cryptologin-client
If you found this tutorial valuable, please โญ star the GitHub repository and - share it with your dev community. Let's build a safer internet, together.



Top comments (1)
So, letโs recap. Weโve set up a zero-knowledge authentication server in five minutes. Weโve created an end-to-end encrypted DataVault. And weโve proven that even in the event of a complete database breach, your data remains mathematically unreadable.
But beyond the technical aspects, thereโs a philosophy.
In an age where AI can clone any voice, generate any phishing scam, automate any attackโฆ the one thing it will never be able to steal is what you keep in your pocket.
A piece of paper. A 32-character master_secret. Your familyโs nicknames. A personal phrase that only you know.
CryptoLogin isnโt reinventing security. Itโs restoring it. Itโs bringing control back to where it should never have left: into your hands.
So, give it a go. Audit the code. Give it a star on GitHub. And join us in this Zero-Knowledge revolution.
Because in the age of AI, the most advanced technology there isโฆ might simply be a piece of paper in your wallet.
Thank you for reading. See you soon for the next article.