Nenyok publishes its client-side encryption code for public review. The server relay code remains private to reduce attack surface — but it has no ability to decrypt your messages. Here we explain exactly what is open, what the limits are, and how to audit it.
Full AES-256-GCM logic, PBKDF2 key derivation, random IV per message. Runs in your browser.
The UI code — what you see is what runs. Inspectable in DevTools at any time.
Forwards encrypted blobs only — cannot decrypt. Kept private to reduce infrastructure attack surface.
Infrastructure details not disclosed to prevent targeted attacks.
The server delivers the JavaScript to your browser on each visit. If the server were compromised, it could serve modified JS. This is a fundamental limitation of any browser-based app. To mitigate: check the source in DevTools before sensitive conversations, or use the browser's built-in crypto verification (see below).
Nenyok is designed for a specific threat model. It is not a universal security solution.
ISP, network admin, or anyone intercepting traffic cannot read messages — only encrypted ciphertext is transmitted.
The relay server sees only encrypted blobs. It has no key and cannot decrypt messages.
Nothing is stored on disk. Messages exist only in RAM while both users are connected, then discarded.
If your device has malware, keyloggers, or a malicious browser extension — all bets are off.
A short or guessable code (under 12 chars, dictionary words) can be brute-forced. Use a random passphrase.
A server-side attacker could serve different code. Always verify JS in DevTools for sensitive use.
HTTPS prevents MITM on the connection itself. Key fingerprint (shown in chat) lets you verify the session manually.
The server knows when connections happen and connection IP. It does not know who you are or what you say.
The core cryptographic functions from chat.js. This is what actually runs in your browser.
// ── Key derivation ──────────────────────────────────────────
// PBKDF2-SHA256 · 200,000 iterations · 16-byte random salt
// The salt is random per session — different each time
// Both peers derive the same key independently
async function deriveKey(code, salt) {
const enc = new TextEncoder();
const baseKey = await crypto.subtle.importKey(
'raw', enc.encode(code), 'PBKDF2', false, ['deriveKey']
);
return crypto.subtle.deriveKey(
{ name: 'PBKDF2', hash: 'SHA-256', iterations: 200000, salt: salt.buffer },
baseKey,
{ name: 'AES-GCM', length: 256 },
true, // exportable for fingerprint computation
['encrypt', 'decrypt']
);
}
// ── Key fingerprint ──────────────────────────────────────────
// Short visual hash of the derived key
// Both users see the same fingerprint if keys match
// Verify it verbally to detect MITM
async function computeFingerprint(key) {
const raw = await crypto.subtle.exportKey('raw', key);
const hash = await crypto.subtle.digest('SHA-256', raw);
const arr = Array.from(new Uint8Array(hash));
return arr.slice(0, 6).map(b => b.toString(16).toUpperCase().padStart(2,'0')).join(':');
}
// ── Encryption ───────────────────────────────────────────────
// Random 12-byte IV per message — never reused
// AES-256-GCM provides both confidentiality and integrity
async function encryptMessage(key, text) {
const iv = crypto.getRandomValues(new Uint8Array(12));
const encrypted = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv },
key,
new TextEncoder().encode(text)
);
return { iv: Array.from(iv), data: Array.from(new Uint8Array(encrypted)) };
}
// ── Decryption ───────────────────────────────────────────────
// Returns null if decryption fails (wrong key or tampered data)
// GCM tag validates integrity — no silent corruption
async function decryptMessage(key, payload) {
try {
const decrypted = await crypto.subtle.decrypt(
{ name: 'AES-GCM', iv: new Uint8Array(payload.iv) },
key,
new Uint8Array(payload.data)
);
return new TextDecoder().decode(decrypted);
} catch {
return null; // wrong key or integrity check failed
}
}
You don't need to trust us — verify directly in your browser.
In Chrome/Firefox: F12 → Sources tab → nenyok.com → chat.js. Compare the deriveKey, encryptMessage, decryptMessage functions with what is shown on this page.
Open DevTools → Network → WS. You will see only JSON blobs with iv and data arrays — never your message text.
When connected, both users see a key fingerprint in the status bar (e.g. A1:B2:C3:D4:E5:F6). Call or text your contact and confirm the fingerprints match. If they don't — someone is in the middle.
In DevTools Console, type window.crypto.subtle — it should return a SubtleCrypto object. All Nenyok crypto goes through this browser-native API, not a third-party library.
Scope: client-side encryption, session handling, key derivation, WebSocket protocol.
Disclosure: Please report privately — do not post publicly until resolved.
Contact: info@nenyok.com
Response SLA: acknowledgement within 48 hours, resolution timeline communicated within 5 business days.
We credit researchers who report valid vulnerabilities.
Messages are never written to disk. They exist in server RAM only while both users are in the same room.
IP addresses and connection timestamps appear in server process logs. Logs are not persisted long-term and are not shared with third parties.
20 messages/second per connection, 20 room joins/minute per IP. Protects against abuse.
The server rejects any third connection to an occupied room. No silent observers possible.