DeadLock Technical Specification
This document describes how DeadLock authentication works at the implementation level. Everything you need to add DeadLock to your own application is here — the architecture, the cryptographic design, database schema, code examples, and security considerations. No license, no SDK, no dependency. Just the pattern.
The Lookup Problem
With traditional authentication, you look up a user by their email address, then verify the password hash. With DeadLock there is no username — just a phrase. The system must find the user from the phrase alone. You cannot use bcrypt for this because bcrypt is intentionally non-deterministic (each hash includes a random salt). Two bcrypt hashes of the same input produce different outputs.
The solution is a dual-hash architecture: one deterministic hash for lookup, one slow hash for verification.
Architecture: Dual-Hash Design
A deterministic keyed hash using a server-side secret. The same input always produces the same output, enabling fast key-value lookup. The server secret means that even with full database access, an attacker cannot compute the HMAC without the key.
A slow, salted hash stored on the user record. After HMAC lookup finds the user, bcrypt verifies the phrase is correct. 12 rounds of key stretching makes GPU-accelerated attacks impractical even if both the database and server secret are compromised.
Both hashes must pass for authentication to succeed. Compromising the database alone gives the attacker HMAC hashes they cannot reverse (no key) and bcrypt hashes that would take millennia to brute-force. Compromising the server secret alone gives them nothing without the database. Both layers must fall simultaneously.
Step 1: Normalization
Before hashing, the phrase must be normalized so that trivial variations (capitalization, extra spaces) do not prevent login. The normalization is simple and must be applied identically at set time and login time:
# Python import re def normalize_deadlock(phrase: str) -> str: """Lowercase, strip whitespace, collapse multiple spaces to single.""" return re.sub(r'\s+', ' ', phrase.strip().lower())
// JavaScript function normalizeDeadlock(phrase) { return phrase.trim().toLowerCase().replace(/\s+/g, ' '); }
This means "The Coffee Shop", "the coffee shop", and "the coffee shop" all normalize to the coffee shop. The user only needs to remember the words.
Emoji in Passphrases
DeadLock phrases support full Unicode emoji. A phrase like my πΆ loves π every friday night is valid and significantly harder to brute-force than ASCII alone. Emoji are preserved through normalization — lowercase and whitespace collapsing do not affect them.
Two rules keep emoji phrases practical:
A phrase must contain at least 3 words of 2+ alphabetic characters. This prevents all-emoji phrases like "πΆππΈπ " which would be impossible to remember reliably and vulnerable to emoji dictionary attacks.
At least one word must appear between consecutive emoji. This forces emoji to augment natural language rather than cluster together. my πΆ loves π is valid. my πΆπ loves dinner is not.
# Python β Emoji validation import re EMOJI_PATTERN = re.compile( '[\U0001F600-\U0001F64F\U0001F300-\U0001F5FF\U0001F680-\U0001F6FF' '\U0001F900-\U0001F9FF\U0001FA00-\U0001FA6F\U0001FA70-\U0001FAFF' '\U00002702-\U000027B0\U00002600-\U000026FF\U00002764\U00002B50]' ) def validate_emoji_rules(phrase: str) -> str | None: """Returns error message or None if valid.""" words = re.findall(r'[a-zA-Z]{2,}', phrase) if len(words) < 3: return "Phrase needs at least 3 real words" # Check for consecutive emoji without a word between them tokens = re.split(r'\s+', phrase.strip()) last_was_emoji = False for token in tokens: has_emoji = bool(EMOJI_PATTERN.search(token)) has_word = bool(re.search(r'[a-zA-Z]{2,}', token)) if has_emoji and not has_word: if last_was_emoji: return "Need at least one word between emoji" last_was_emoji = True else: last_was_emoji = False return None
Adding even two emoji to a phrase explodes the search space. With ~3,600 common emoji characters mixed into natural language, an attacker must now brute-force a combined ASCII + Unicode character set — effectively multiplying the keyspace by orders of magnitude per emoji position.
Step 2: HMAC-SHA256 Lookup Index
Generate a deterministic key from the normalized phrase using HMAC-SHA256 with your server secret. This key is used as a database index to find the user.
# Python import hmac, hashlib def deadlock_hmac(normalized: str, server_secret: str) -> str: """Generate deterministic lookup key from normalized phrase.""" return hmac.new( server_secret.encode(), normalized.encode(), hashlib.sha256 ).hexdigest()
// Node.js const crypto = require('crypto'); function deadlockHmac(normalized, serverSecret) { return crypto .createHmac('sha256', serverSecret) .update(normalized) .digest('hex'); }
The server secret must be stored securely outside the database — in an environment variable, a secrets manager, or a file on disk that the application reads at startup. If it lives in the database, it defeats the purpose.
Step 3: AI Strength Review (Learning Blocklist)
Before a phrase is hashed and stored, it passes through an AI evaluator that checks for guessability. The AI reviews each phrase for verbatim famous quotes, song lyrics, movie lines, sports slogans, proverbs, and other well-known phrases that an attacker could dictionary-attack. The AI evaluates only — it never suggests alternatives. The phrase must come from the user's own memory, not from AI.
Every rejected phrase is added to a persistent blocklist keyed by its HMAC hash (the plaintext phrase is never stored). Future attempts with the same phrase are rejected instantly from the blocklist with zero API overhead. The blocklist grows smarter over time as more weak phrases are attempted.
If the AI service is unavailable (network error, timeout, missing API key), the phrase is allowed through. Security should never lock users out of setting a passphrase due to a third-party dependency failure. The dual-hash architecture and minimum length requirement are the primary defenses — the AI review is an additional layer.
# Python β AI strength check with learning blocklist import json, httpx def check_phrase_strength(normalized: str, hmac_key: str, db, api_key: str) -> dict: """Returns {"ok": True} or {"ok": False, "reason": "..."}""" # Check blocklist first (instant, no API call) blocked = db.get(f"deadlock_blocklist:{hmac_key}") if blocked: return {"ok": False, "reason": blocked["reason"]} # Call AI evaluator try: resp = httpx.post( "https://api.anthropic.com/v1/messages", headers={ "x-api-key": api_key, "anthropic-version": "2023-06-01", "content-type": "application/json", }, json={ "model": "claude-haiku-4-5-20251001", "max_tokens": 150, "messages": [{"role": "user", "content": f"Evaluate: {normalized}"}], "system": STRENGTH_PROMPT, }, timeout=15.0, ) result = resp.json()["content"][0]["text"].strip() except Exception: return {"ok": True} # Fail open if result.upper().startswith("PASS"): return {"ok": True} # Extract reason and add to blocklist reason = result.split(":", 1)[1].strip() if ":" in result else "Too guessable" db.set(f"deadlock_blocklist:{hmac_key}", { "reason": reason, "added_at": datetime.utcnow().isoformat() }) return {"ok": False, "reason": reason}
The AI prompt instructs the evaluator to default to PASS and only reject phrases it is 100% certain are verbatim well-known phrases. Personal phrases about pets, family, places, dates, hobbies, and memories are always approved regardless of structure. The evaluator must never suggest alternative phrases — DeadLock's security model depends on the phrase coming from human memory, not AI generation.
Step 4: bcrypt Verification Hash
Store a bcrypt hash of the normalized phrase on the user record. This is your second layer — even if an attacker obtains the HMAC key and reverses the lookup index, they still face bcrypt.
# Python β Setting a DeadLock phrase import bcrypt, hashlib def prehash(password: str) -> bytes: """SHA-256 prehash for phrases exceeding bcrypt's 72-byte limit.""" raw = password.encode() if len(raw) <= 72: return raw return hashlib.sha256(raw).hexdigest().encode() def set_deadlock(user_id, phrase, server_secret, db, api_key=None): normalized = normalize_deadlock(phrase) # Enforce minimum length if len(normalized) < 20: raise ValueError("Phrase must be at least 20 characters") # Validate emoji rules emoji_err = validate_emoji_rules(normalized) if emoji_err: raise ValueError(emoji_err) # Generate lookup key hmac_key = deadlock_hmac(normalized, server_secret) # Check for collision (do NOT reveal this to the user) if db.lookup_exists(f"deadlock:{hmac_key}"): raise ValueError("Could not set phrase. Please try a different one.") # AI strength review + learning blocklist if api_key: result = check_phrase_strength(normalized, hmac_key, db, api_key) if not result["ok"]: raise ValueError(result["reason"]) # Hash for verification (prehash handles >72 byte phrases) phrase_hash = bcrypt.hashpw( prehash(normalized), bcrypt.gensalt(rounds=12) ).decode() # Store both db.set_user_field(user_id, "deadlock_hash", phrase_hash) db.set_user_field(user_id, "deadlock_hmac_key", hmac_key) db.set_lookup(f"deadlock:{hmac_key}", user_id)
Step 5: Login Flow
The login flow is: normalize → HMAC lookup → bcrypt verify → create session. No 2FA step. The phrase is the entire authentication.
# Python β DeadLock login def deadlock_login(phrase, server_secret, db): normalized = normalize_deadlock(phrase) if len(normalized) < 20: return None # Reject silently # Step 1: HMAC lookup hmac_key = deadlock_hmac(normalized, server_secret) user_id = db.get_lookup(f"deadlock:{hmac_key}") if not user_id: return None # No user found β generic error # Step 2: bcrypt verify user = db.get_user(user_id) stored_hash = user.get("deadlock_hash") if not stored_hash: return None if not bcrypt.checkpw(prehash(normalized), stored_hash.encode()): return None # Wrong phrase β same generic error # Step 3: Create session (skip 2FA entirely) session_token = db.create_session(user_id) return session_token
Every failure path returns the same generic error. Never indicate whether the phrase exists, whether the user exists, or what specifically failed. The attacker learns nothing from a failed attempt.
Database Schema
You need two storage structures: a field on the user record for the bcrypt hash, and a separate lookup index mapping HMAC keys to user IDs.
-- SQL (PostgreSQL / MySQL) -- Add to your users table ALTER TABLE users ADD COLUMN deadlock_hash VARCHAR(72) DEFAULT NULL; ALTER TABLE users ADD COLUMN deadlock_hmac_key VARCHAR(64) DEFAULT NULL; -- Lookup index table CREATE TABLE deadlock_index ( hmac_key VARCHAR(64) PRIMARY KEY, user_id INTEGER NOT NULL REFERENCES users(id) ); -- Unique constraint prevents collision at the DB level CREATE UNIQUE INDEX idx_deadlock_hmac ON deadlock_index(hmac_key);
# Key-Value Store (Redis, DynamoDB, etc.) # Lookup index deadlock:{hmac_sha256_hex} → { "user_id": 123 } # User record fields user:{user_id}.deadlock_hash → "$2b$12$..." user:{user_id}.deadlock_hmac_key → "a1b2c3..."
Removing a DeadLock Phrase
When a user removes or changes their phrase, you must delete the old HMAC lookup index and clear the hash from the user record. If changing, delete the old index before creating the new one.
# Python β Remove DeadLock def remove_deadlock(user_id, db): user = db.get_user(user_id) old_key = user.get("deadlock_hmac_key") # Delete lookup index if old_key: db.delete_lookup(f"deadlock:{old_key}") # Clear user fields db.set_user_field(user_id, "deadlock_hash", None) db.set_user_field(user_id, "deadlock_hmac_key", None)
Security Considerations
The HMAC server secret is the crown jewel. Store it in a secrets manager, a Docker secret, or an environment variable — never in the database and never in source control. If the secret is rotated, all existing HMAC lookup indexes become invalid. Plan for this.
Even though brute force is mathematically futile, rate limit login attempts anyway. It prevents resource exhaustion attacks and adds defense in depth. We use 10 attempts per 5 minutes per IP.
Every failure — wrong phrase, no user found, account disabled — must return the same generic error. Never confirm or deny whether a phrase exists in the system. Never return a different HTTP status code for "phrase not found" vs "phrase wrong." Same response, every time.
If two users attempt to set the same phrase, the second attempt must fail with a generic error. Never say "this phrase is already in use" — that confirms the phrase exists and is a security leak. Use a message like "Could not set phrase. Please try a different one."
Enforce a minimum of 20 characters on both client and server. This ensures a baseline entropy floor even for the laziest users. Reject short phrases before they reach the hash functions.
bcrypt truncates input at 72 bytes. For ASCII text, that is 72 characters. For UTF-8 with emoji, each emoji can be 4 bytes — an emoji-heavy phrase can exceed 72 bytes quickly. We solve this with a SHA-256 prehash: if the encoded phrase exceeds 72 bytes, hash it with SHA-256 first, then pass the hex digest (64 bytes, always under 72) to bcrypt. The HMAC layer always uses the full phrase regardless. Apply the same prehash at both set time and login time.
The search space of a DeadLock phrase makes automated brute force impossible. You do not need CAPTCHA for bot protection on the login form. This means no Google reCAPTCHA, no data harvested from your users during authentication, and no third-party dependency on your login path.
Complete Flow
Setting a DeadLock phrase: User input: "My πΆ Loves π Every Friday Night" ↓ Normalize: "my πΆ loves π every friday night" ↓ Validate: emoji rules (3+ words, word gap between emoji) → OK ↓ HMAC-SHA256(normalized, server_secret) → hmac_key ↓ Check: does hmac_key already exist in lookup index? ↓ No Check blocklist: deadlock_blocklist[hmac_key] exists? ↓ No (not previously rejected) AI strength review → PASS ↓ Prehash if >72 bytes: SHA-256(normalized) → prehashed bcrypt(prehashed, rounds=12) → hash ↓ Store: user.deadlock_hash = hash user.deadlock_hmac_key = hmac_key deadlock_index[hmac_key] = user_id Setting a weak phrase (rejected): User input: "to be or not to be that is the question" ↓ Normalize → HMAC → No collision ↓ AI strength review → FAIL: This is a famous Shakespeare quote ↓ Add HMAC to blocklist with reason (plaintext never stored) ↓ Return rejection to user (Next attempt with same phrase: instant reject from blocklist, no API call) Login with DeadLock: User input: "my πΆ loves π every friday night" ↓ Normalize: "my πΆ loves π every friday night" ↓ HMAC-SHA256(normalized, server_secret) → hmac_key ↓ Lookup: deadlock_index[hmac_key] → user_id ↓ Found Prehash if >72 bytes: SHA-256(normalized) → prehashed bcrypt.verify(prehashed, user.deadlock_hash) → True ↓ Create session → return token (skip 2FA entirely)
Entropy Analysis
For a phrase of length L drawn from a character set of size C:
Classical entropy: bits = L × log2(C) Quantum (Grover): effective_bits = bits / 2 Example: "the parking lot behind the library floods" (42 chars) Conservative charset: 95 printable ASCII Classical: 42 × log2(95) = 42 × 6.57 = ~276 bits Quantum: 276 / 2 = ~138 bits At 10^12 ops/sec (theoretical quantum computer): 2^138 / 10^12 = ~3.5 × 10^29 seconds = ~11 sextillion years With Unicode/emoji (charset ~150,000): Classical: 42 × log2(150000) = 42 × 17.19 = ~722 bits This is beyond meaningfully quantifiable.
Quantum computing is upon us, and we need simpler security that can beat it.