KENNI
You can't trust a voice anymore. So I built an app that lets people prove it's really them — and open-sourced the whole thing.
The thing that finally bugged me enough
A while ago I read one of those stories that keeps showing up now: someone gets a call from their kid, the kid is crying, says they're in trouble and need money right now. Except it wasn't the kid. It was an AI clone of their voice, scraped from a few seconds of a video, and the person on the other end lost a lot of money. These aren't rare anymore. Voice cloning went from 'research demo' to 'app you can download' in about two years.
What got me is that all our usual proof of identity is basically gone. A voice used to be enough. A face on a video call used to be enough. Now none of it is. And the advice everyone gives — 'agree on a secret family password' — is cute but falls apart the moment someone forgets it or it gets overheard. I wanted something that actually holds up, and that a non-technical person could use without knowing what a public key is. That became KENNI.
The core idea: stop trusting the channel, trust the key
The whole trick is to move trust off the thing that can be faked (the voice, the chat, the caller ID) and onto something that can't: a private key that only lives on your contact's phone. You and the people you care about exchange keys once — ideally in person, by scanning each other's QR code. After that, whenever you're not sure who you're really talking to, you tap their contact and ask their phone to prove it. Their phone signs a fresh challenge, your phone checks the signature against the key you saved. If it matches, it's really them. If someone's impersonating them, they physically cannot produce that signature.
No accounts. No phone numbers. No email. Your identity is just a keypair, and the app is built so you never really have to think about that.
How the identity actually works
When you first open the app it generates 128 bits of randomness and shows you 12 words — standard BIP39, the same recovery-phrase idea crypto wallets use. Everything else is derived from those 12 words with HKDF: an Ed25519 keypair for signing (this is your identity), an X25519 keypair for agreeing on shared secrets with contacts, and a symmetric key for encrypted backups. Because it's all derived deterministically, those 12 words are a complete backup — type them into a fresh phone and your identity is back, fully offline, no server involved.
The seed lives in the iOS Keychain. If you want it backed up, it goes into your personal iCloud Keychain (end-to-end encrypted, Apple can't read it either) — but that's a choice you make during onboarding, before anything syncs, because I didn't want to sync people's keys off their device without asking first. Everything sensitive sits behind Face ID.
Two ways to verify, because real life isn't always online
There are two flows, and they cover the two situations you actually find yourself in.
| Live check (online) | Offline codes | |
|---|---|---|
| When | You both have internet | No signal, on a call, anywhere |
| What happens | You send a silent request, their phone asks 'is this really you?', they confirm or deny | Your app shows a 6-digit code, they read back a 6-digit answer only their phone can compute |
| Proof | Ed25519 signature over a one-time nonce | HMAC over a secret only the two of you share |
| Can be faked? | No | No |
The offline one is my favorite because it needs zero connectivity. When you exchanged keys, both phones quietly computed the same shared secret (X25519 key agreement — the secret never gets transmitted, both sides just derive it). So later, on a call with no internet, your app can show a challenge and only their phone can produce the right response:
// Both phones hold the same pairwise secret from the key exchange.
// A shows a random 6-digit challenge; B computes the answer:
let msg = Data("kenni/v1/offline".utf8) + Data(challenge.utf8) + Data("response".utf8)
let mac = HMAC<SHA256>.authenticationCode(for: msg, using: SymmetricKey(data: secret))
// -> truncate to 6 digits, B reads it aloud, A's app checks it.An impostor can't compute that response without the shared secret, which never left either device. Six digits with only a couple of allowed attempts is more than enough for a live phone call.
Why the server can't cheat (and why that matters)
There is a backend, but it's deliberately dumb. All it does is deliver push notifications, hand off exchange links, and hold an optional encrypted backup blob it can't read. It never sees names, photos, or contacts — those only ever live on devices. Every verification is checked on your phone against the key you personally saved, not against anything the server says. So even if someone fully owned my server, they couldn't forge a confirmation or invent a fake contact. The server is a mailman, not a notary.
Every request to the API is also signed with your identity key and timestamped, so the server can tell you own the key without you ever having an account. It's a nice property — there's literally no password to steal, no user table to leak.
It's open source — please poke holes in it
Here's the part I care about most: a security app you can't inspect is just asking for trust, which is exactly the thing the app is supposed to replace. So both pieces are public — the SwiftUI app and the Vapor backend. If you know your way around crypto and you find something wrong, I genuinely want to hear about it. The design is simple on purpose, so it should be easy to read through.
- kenni-ios — the iPhone app (SwiftUI, CryptoKit, no third-party dependencies)
- kenni-api — the relay backend (Swift, Vapor, Postgres, dockerized for self-hosting)
It's free, there are no ads, and there's no tracking. I built it because I want my own family to have it on their phones. If it stops one person from getting scammed by a fake version of someone they love, it was worth the late nights.