WIGGWIGG Security Whitepaper
Version: 1.0.2 · Date: 2026-09-10
WIGGWIGG is a privacy-first phone-number and identity product. This document describes, in technical detail, how we protect your data: what we cannot read, what we can read and why, the cryptography we use, and where our guarantees end. It is written for a skeptical, technical reader.
Our cryptographic core is open source at github.com/WiggWiggDev/crypto (the
@wiggwigg/crypto package): the key-derivation, sealing, and signing primitives
this document describes are auditable independently of the closed application.
1. Summary and stance
- Zero-knowledge by design. Your password, vault entries, identities and contacts are encrypted on your device with keys we never hold. Message content is sealed once stored to the account communication key, which supported clients open after you sign in and unlock, and we destroy our copy of the sealing key (section 5, which also names a small set of private-beta messages that are not sealed yet). For a sealed message we hold no key, and we cannot be compelled to produce a key we don’t have. Read that as written: it is a claim about a message at rest, and section 5 sets out what happens before the seal.
- Open-core, not “open source.” We open-sourced the cryptographic core so you can audit how keys and data are protected. The rest of the application (the operational and abuse-defense surfaces) is closed. We never imply otherwise.
- The edges. Phone numbers and routing metadata are server-readable because phone service requires it. A message you send passes through us in the clear on its way to the carrier, and picture messages are checked for known illegal material before they are stored: when a check matches we keep the picture, and that copy outlives your account. We are not end-to-end-encrypted in transit (the phone network predates that). We do not yet have forward secrecy or post-quantum protection. The options and exceptions that move a piece of data from one tier to another are in section 4; the standing limits are in sections 5, 10 and 11.
- Self-attested today, externally audited later. This document is backed by open source code and a published vulnerability-disclosure policy. A third-party cryptographic and application audit is planned; we will publish it when complete and will not claim it before then.
2. Threat model and trust boundaries
We design the cryptographic storage boundary against a fully malicious server, not merely an honest-but-curious one: assume our infrastructure may be compromised, subpoenaed, or operated by someone who should not see your data, and that it may actively lie to your device rather than just watch. The stored-data guarantee assumes that the client code performing the encryption is authentic and untampered. A live attacker who controls delivery of the web app could change the code served to a later session and try to capture plaintext or keys; remotely delivered web code cannot be independent of its delivery server. With an authentic, untampered client, a copy of our stored data and server-side keys cannot open the zero-knowledge ciphertext because the opening keys live on your device. That is the threat model the 2026 USENIX Security cryptographic analysis of password managers argues is the correct one (Zero Knowledge (About) Encryption, Scarlata et al.), and the one we hold ourselves to; the residuals we still carry under it are stated in section 10, not left out.
| Boundary | What we assume | What it can see |
|---|---|---|
| Your device | Trusted; client-side crypto runs here | Everything you store (it holds your keys) |
| Your password / recovery phrase | Secret, known only to you | Nothing leaves the device |
| WIGGWIGG servers | May be compromised, compelled, or malicious | Server-encrypted operational data (section 4); stored zero-knowledge ciphertext remains closed when authentic, untampered client code encrypted it, but an active web-delivery compromise can target later sessions |
| Database | Protected by row-level security and triggers | Ciphertext only, for zero-knowledge data |
| Telecom carrier (Telnyx) | A “dumb pipe” that learns only what routing requires | Call/SMS metadata and content in transit (like all phone service) |
| Payment processors (Stripe, Blockonomics) | Data-minimized | Card-billing essentials, or a Bitcoin address and amount; never your vault or identities |
| The illegal-content detectors | Trusted only with the comparison input they need | A one-way fingerprint of each picture message, computed on our own servers. No detector ever receives the picture (section 5). Running the check is our decision; only confirmed-illegal material creates the preservation and reporting duties described there |
| The reporting authority | Receives what the law obliges us to report | Only for material confirmed illegal, the picture itself, with the report. Pictures we keep under the second class in section 5 are not reported |
We do not defend against a compromised endpoint (malware on your own device, or a malicious browser extension), an active compromise that changes the web app delivered to a later session, or metadata inherent to placing a phone call or SMS through the public network. See section 10.
3. Cryptographic architecture
All primitives below come from audited libraries
(@noble/curves,
@noble/hashes,
@scure/bip39). We do not roll our own.
- Key derivation: your password is stretched with Argon2id (128 MiB memory, 3 iterations). It is memory-hard and resistant to GPU/ASIC brute-force. The same parameters are byte-for-byte identical across web and mobile so your keys match on every device.
- Vault-key indirection: the password-derived master key wraps a separate, per-account vault key, and your data is encrypted under the vault key. Changing your password re-wraps the vault key; the key your data is encrypted under does not change. (Your entries are still rewritten under fresh nonces on a password change. What stays put is the key, not the ciphertext.)
- Symmetric encryption: AES-256-GCM (authenticated) for vault entries, identities, contacts, message content, and server-side operational fields.
- Public-key sealing: inbound message content is encrypted with AES-256-GCM under a one-time key, which is itself sealed to the account communication X25519 public key (X25519-ECDH, HKDF-SHA256, XChaCha20-Poly1305: a libsodium-style sealed box). If the account has no X25519 key on file yet, the session key is wrapped with RSA-2048-OAEP instead, the legacy scheme; each blob records which was used. Blobs written before that marker existed leave it blank, and a reader treats blank as the RSA scheme. Both are zero-knowledge (section 5), and retiring RSA is on the roadmap (section 11).
- Authentication: proving you know your password without revealing it (section 6).
- Key separation: every derived key gets its own domain tag (HKDF-SHA256 under
a distinct
infostring, or HMAC-SHA256 over a distinct label), so compromising one never yields another. Two families deliberately do not come from the master key: the vault key is random and merely wrapped by it, and the recovery keys come from your BIP39 phrase (section 7). - Crypto agility: every account-level scheme carries a version, and the vault, identity, contact and message rows record the key version they were written under, so we can rotate a primitive without breaking stored data. A few smaller server-side fields (contact labels, vault shares, the stored number on a call-log row) do not carry one yet, so rotating those means re-encrypting rather than dispatching. This is how the post-quantum upgrade in section 11 lands without a rewrite.
4. Data classification
What is and isn’t zero-knowledge:
The tiers are named for who can read the data, because that is the only distinction that changes what a leak or a warrant would expose. The app puts the same names on every field badge, so a badge in the product and a row in this table mean the same thing.
| Tier | Data | Who can read it |
|---|---|---|
| Only you | Vault entries, contacts, the personal content of your identities, optional custom calling-device names, SMS/MMS content written through the current sealing method (once stored: see section 5 for the check that runs first, the one case where we keep a picture, and the private-beta rows that are not sealed yet), the other party’s number in your message log when written through that method, E911 addresses (our stored copy), the anti-phishing marker seed (phrase/colors/avatar) | You, through a supported client and account-bound keys |
| Mixed protection | Voicemail audio, caller number and transcript field; the other party’s number in a call log; MMS attachment bytes and filename | Sealed to the account communication key when a usable key resolves. Voicemail is server-readable while Listen-by-phone is on or when the account has no usable key. A call-log number becomes server-readable when there is no usable key or its lookup fails, so a live call is not interrupted. Current MMS attachment writes seal the bytes and filename to the account communication key, but retained private-beta rows and object versions can remain server-readable or not yet be individually attributable. |
| You and WIGGWIGG | Phone numbers (routing), including the verified number attached to an identity; SIP credentials; billing metadata; call/SMS cost and carrier metadata; call detail records; automatic-reply text; the intros your callers record; the text and audio of your voicemail greeting, of the announcement we play to you on pickup and of the prompt your callers hear before they identify themselves; and, while the carrier still holds it, the link we use to re-fetch a picture message | Server can decrypt (required to operate) |
| You and WIGGWIGG support | Support ticket subject, body and attachment filenames | Sealed to you and to a support key, so your ticket can be answered |
| You and WIGGWIGG (support attachments) | The contents of a file you attach to a ticket | Encrypted at rest, and readable by our servers: support can open the file to help you. Unlike the ticket’s subject, body and filename, the file itself is not sealed to you. Sealing it is on the roadmap |
| No one | Search indexes over your vault, identities and contacts, keyed on your device | One-way keyed HMAC under a key we do not hold |
| You and WIGGWIGG (lookup indexes) | The equality indexes we need to route and to match a number, over phone numbers, blocked numbers and recognized contacts | One-way keyed HMAC under a key we hold, so we can recompute but not read back. See the honest paragraph below |
| Third-Party Transit | The number you are texting or calling, and your card details, as they are handed to the carrier or the payment processor | Plaintext to that provider, by necessity |
| Unencrypted | Account ID (a public username), most timestamps, primary and foreign keys, is_admin, active calling-device routing metadata, and a longer tail of operational fields (message and attachment sizes and types, plan and product names on an invoice, tax province, credit balances) | Anyone with DB access |
After the current Privacy Statement is accepted, WIGGWIGG automatically registers up to 10 active browsers or mobile devices per account when the person signs in and unlocks their data, so those devices can receive calls. The random recognition code is stored only on the device that creates it. It reaches our server transiently when that device asks to be recognised, but is neither logged nor persisted there. We retain a keyed, account-scoped one-way fingerprint instead. While a registration is live, the server can read the account link, a random internal id, browser-or-mobile class, account-local display ordinal, associated phone ids, lifecycle state, and registration, readiness, and lifecycle activity timestamps. For each phone number, the server can also read whether routing includes all registered devices or a custom subset, and it can infer the selected device links needed for inbound routing. That selection applies only to incoming ringing; an otherwise authorized registered device can still place calls when it is not selected to ring. A custom device name is encrypted under the client’s vault key, so we cannot read it.
A historical calling-device decline or withdrawal remains a server-readable paused routing state until explicit reactivation after recent authentication. No separate date is retained for that state change. Removing one device immediately stops new calls to its registration. Provider cleanup can take longer. Only after every provider endpoint is confirmed gone does the database permit the device to become terminal; that transition erases the class, ordinal, phone links, timestamps, and encrypted custom name. The remaining anti-reuse record contains only the account link, opaque internal id, keyed fingerprint with its key version, and revoked status. It is immutable and survives only until account deletion or an earlier retirement of that fingerprint key. While the tombstone exists, the old recognition code cannot create a fresh registration. Official WIGGWIGG clients generate a new code when registering again. A separate account-scoped event ledger containing only a random internal id, the account link, and the attempt time enforces the rolling one-hour and 24-hour registration limits and is swept after its 24-hour window.
Registration asks for no country declaration. During Early Access, phone numbers, the seven-day Canadian-number trial, and anything paid require a Canadian province confirmed for taxes. Phone-trial activation and card, Bitcoin, or credit checkout separately validate a province the person selected and confirmed. That province is retained for tax calculation; Stripe’s card form may separately collect a postal code.
The carve-outs: phone numbers are server-readable because the network must route your calls.
Our stored copy of your E911 address is zero-knowledge, and our servers never decrypt it. Getting it there is the honest part: to make 911 work at all, your device sends us the address in readable form once, at setup, so we can validate it and register it with our carrier, who keeps it to route emergency calls. Dispatch uses their copy. We do not keep ours in the clear, but we did see it in that moment, and no amount of client-side encryption changes that.
Voicemail content is zero-knowledge when a usable account communication key resolves, unless you turn on “Allow listening by phone” for a number (off by default, and set per number rather than per account). With it on, new voicemail for that number is stored in a form our servers can read, so that it can be played back to you over the phone line, and there is no sealed copy alongside it. It is not encrypted under a key of ours either: it is a plain recording, protected at rest by our storage provider and by nothing else. The caller’s number on those voicemails becomes server-readable too. Even with listening by phone off, if the account has no usable communication key, we save new voicemail in that same server-readable form rather than discard it; the server logs that security downgrade. Once a usable key is available, turning listening by phone off returns new voicemail to account-key sealing immediately. It does not re-seal voicemail we already hold, either on the server or during the automatic web pass. Older server-readable voicemail stays that way until separate maintenance can prove that the exact stored object was deleted after a successful re-seal. Leave listening by phone off unless you need phone playback: it is the single largest thing you can choose to give away here.
Your contacts are zero-knowledge unless you turn on “Contact recognition on calls” (off by default), which stores a keyed hash of each contact’s number so we can recognize a known caller or sender on an incoming call or text, which makes those numbers recoverable by our servers while it’s on. Turning it off erases those hashes from our live systems, and routine encrypted backups age out within about a month.
Two more, which are not toggles but consequences of how the feature works:
- Your line greetings, announcements and caller prompts are server-readable, as text and as audio, because the phone network has to play them during the call and the caller has no account or key.
- The intros your callers record are server-readable, always. The phone network plays that clip back to you when the call rings, which requires plaintext we can recover. The caller has no account and no key with us, so a recording sealed to you alone is not possible here.
- Automatic replies are server-readable, because our server is what writes and sends them on your behalf.
These are deliberate trade-offs for a working phone service. We state them so you can price them.
An honest word about blind indexes: a keyed HMAC of a low-entropy value stays enumerable for whoever holds the key: the phone-number space can be walked by brute force. That protection is aimed at a database leak without the key; it does not hold against a compromise of the live server, which holds it.
Some server-encrypted fields are bound to their context (SIP credentials and TOTP seeds are cryptographically tied to their owner) so a ciphertext cannot be moved between records. We are frank about the scope: that binding does not yet cover every server field, and phone numbers, notably, are not there yet. Extending it to every server field, plus per-user key derivation, is on our published roadmap.
5. Communications: “server-blind,” not forward-secret
Every SMS, MMS, and voicemail written through our current sealing method is sealed to the account communication public key (X25519, or the RSA-2048 fallback of section 3 if the account has no X25519 key yet), whether you sent it or received it. On the X25519 path we generate a sender keypair, use it once, and destroy it the instant the message is stored; the RSA fallback encrypts straight to your account’s public key, so there is no sender key in the first place. Either way the result is the same: for a sealed message we hold no key that can read it: not at rest, not later, not under a warrant served on us. We can be compelled to produce the encrypted bytes; we cannot be compelled to produce a key we do not have.
That sentence is scoped, and we mean it that way. It is a claim about a message already sealed and at rest, not a blanket promise about everything a warrant reaches. Here is the whole exception list, including the parts that are least comfortable to write.
A small set of communications kept from the private beta is still under our older server-side encryption. We can read those at rest, and the sentence above does not cover them. It is not only message bodies: most of them are the other party’s number in a call log, and the rest are voicemail audio and caller numbers, an MMS attachment, and SMS bodies. They belong to a handful of accounts whose retained rows have not completed the supported web re-sealing pass since account communication keys shipped. A mobile sign-in can provision the account key but does not start that pass; today, only signing in and unlocking in a supported web client automatically starts it. The bounded pass can re-seal up to 50 eligible SMS rows. It does not migrate private-beta voicemail, legacy call-log numbers, MMS attachments, retained object versions, or every remaining SMS row in one pass. Those excluded copies stay server-readable until later passes or separate maintenance. In particular, the voicemail migration is withheld because the old rows do not retain the exact object version needed to prove that the server-readable audio was destroyed. We keep this exception published until every affected row and every retained copy has converged.
Before that pass reads any retained plaintext, the web client must open a short-lived, one-time challenge sealed to the account’s exact current X25519 communication key. The server stores only the challenge digest, consumes a successful proof once, and rechecks the same public-key fingerprint while each row is replaced. If the private-key wrapper is damaged, belongs to another key, expires, is replayed, or races a key change, the pass stops and the server-readable source remains in place.
The current iOS notification path holds a broader key than a preview needs. After
the device’s first unlock, the app mirrors the raw account communication keypair into
an AfterFirstUnlockThisDeviceOnly shared keychain so the notification service
extension can decrypt content while the device is locked. It does this even when the
notification display setting is Nothing. That copy survives vault lock and device
lock, and it can open all history sealed to the account key, not only notification
previews. The app attempts to delete the copy at sign-out, but sign-out does not fail
closed or retry if deletion fails. The keychain item stays on that device and is not
uploaded to WIGGWIGG. Android uses a separately derived notification-display key whose
scope is limited to previews. We do not generalize that narrower Android property to
iOS, and replacing the iOS copy with a narrow key plus verified deletion remains a
client-security follow-up.
Before the seal, a message passes through us in the clear. A message you send reaches our servers as plaintext, because we have to hand it to the carrier. We seal it when we store it and keep no key afterwards, but we did hold it in memory in between. It is sealed at rest, not end-to-end (section 11).
We check picture messages against known-illegal-image databases, and that check reads the picture. Running the check is our decision, not a duty the law imposes on us; what the law compels is what follows a confirmed match, below. It runs in memory, on our own servers, before storage. What reaches a detector is a one-way fingerprint, never the picture: each detector compares that fingerprint against its database and answers. We use two, one Canadian and one American, and neither receives an image. When a picture is clean we do not keep the picture, and we keep no fingerprint of it. What stays is a verdict row recording that a check ran and found nothing, erased within 90 days (section 9).
When a check matches, we keep the picture itself. There are two kinds of match, and they are not the same, so we separate them here rather than let the harsher one speak for both.
For material confirmed to be illegal, the law obliges us to notify a police officer. The image is written to a sealed quarantine under legal hold, and it goes with our report to the RCMP’s National Child Exploitation Crime Centre in Ottawa. That report stays in Canada.
For the second kind, the Canadian detector flags a picture as harmful to children without placing it in its confirmed-illegal category. We block it and preserve it the same way, and no law required us to keep it. That is our decision, and we would rather name it as ours than shelter it under the word “required”. The category is not a finding that the picture is legal: it is kept deliberately cautious so it holds across many countries, and the Canadian definition is broader. Nothing is reported automatically, but if a review concludes the material is criminal in Canada, we report it.
Both kinds are held the same way once kept: we can open and view the picture, deleting your account does not remove it, and a warrant reaches it. We do not put an end date on either copy, and that too is our choice rather than a legal requirement. Both apply to a picture someone else sent you as much as to one you sent. This is the one category where we hold your content outright rather than a key to it, and we would rather say so here than have you discover it.
The carve-outs from section 4 also carry a key we hold: voicemail while listening by phone is on or when the account has no usable communication key, the intros your callers record, automatic-reply text, and your line greetings, announcements and caller prompts as text and audio. For up to thirty days we also keep a link we can use to re-fetch a picture from the carrier.
Metadata is not content and is not sealed: call detail records (who called whom, when, how long) are inherent to telephony. We keep them, subject to the retention described in section 9. Under the current method, the other party’s number in your message log is sealed with the message. In a call log, that number is sealed when a usable account communication key resolves. If the account has no usable key, or if the call-log key lookup fails, we store that number under the server key so the live call is not interrupted and log the downgrade. A server-side lookup index over the same number sits beside either form (section 4’s honest paragraph applies here too); the record that a call or a message happened is not sealed at all.
What this is not: this is not forward secrecy. Your messages are sealed to your account’s long-term communication key, which is the same key every device you unlock uses to read your history. If your private key were compromised, your stored history could be decrypted. We optimize for server-blindness (a server or warrant cannot read your content), which is the achievable and meaningful property for stored, re-readable message history. True forward secrecy conflicts with multi-device history and is not something we claim. Post-quantum hardening of this seal is on our roadmap (section 11).
Carrier-network transit (SS7/SIP) is visible to carriers like all phone service. For end-to-end encryption in transit, use Signal or WhatsApp with your WIGGWIGG number.
6. Authentication
Your password never leaves your device. New accounts, and private-beta accounts that have completed migration, sign in with an Ed25519 challenge-response: your device derives a signing key from your master key (Argon2id), the server stores the matching public key, and you prove possession by signing a single-use, 60-second server challenge. For these accounts, the stored public key cannot be replayed to sign in.
Some private-beta accounts still retain a static login verifier until an up-to-date WIGGWIGG client completes migration during a successful sign-in. For those accounts, the stored verifier is bearer-equivalent: if it leaks, someone else could sign in to the account. It does not reveal the password or vault key. After the server records the client-proved public key, the static verifier is no longer accepted for login and future sign-ins use the challenge-response path.
MFA: WebAuthn/passkeys (phishing-resistant) and TOTP. Anti-phishing: before you type your full password, the site proves itself with a per-account marker (phrase, colors, avatar, optional audio) rendered on your device from a seed we hold only in encrypted form. The seed is sealed under a key derived from the first characters of your password, the same characters that gate the reveal (HMAC-SHA256 partial-password check), so the server never holds a marker it could render, and a database leak alone cannot forge your marker without brute-forcing those characters against the key-stretching function. That function is PBKDF2-SHA256 here rather than the Argon2id of section 3: this check has to run before your vault unlocks, so it stays pinned to the older one.
One honest limit. The protection is bounded by that few-character entropy rather than unbreakable, so what it really defeats is mass forgery from a stolen database, the scenario an attacker exploits after a breach.
7. Account recovery
Recovery uses a 24-word BIP39 phrase (256 bits of entropy) that you hold and we never see. From it your device derives three values, each domain-separated from the others: an X25519 key, which your key material is sealed to; an Ed25519 signing key, which authorizes a password change with a signature over a single-use server challenge; and an older symmetric verifier, which is what gates entry to the recovery flow today.
We store the two public keys, a hash of the verifier, and a sealed blob we cannot open. Let us be clear about that verifier, because it is the weakest part of this section: unlike the two keys, it is a shared secret rather than a public one, and your device sends it to us at setup and again on every recovery attempt. A leak of our database does not yield it, since we keep only a hash. But we do see it in the moment it is sent, which is exactly the property the two keys are designed not to have. Retiring it in favour of the Ed25519 challenge-response, which sends no secret at all, is on the roadmap (section 11).
That blob seals your vault key, not your master key. The distinction matters: the vault key doesn’t change when you change your password, so the recovery envelope never has to be automatically re-sealed. An automatic re-seal is exactly the handle a malicious server would reach for, substituting its own recovery key so it can open the envelope; sealing a stable key means the handle isn’t there. A password change started from recovery is authorized by an Ed25519 signature from your phrase over a single-use, two-minute server challenge, and your device verifies that the recovered key actually decrypts your data before it re-encrypts anything.
The trusted recovery key is also pinned inside your encrypted preferences, which the server cannot read: if the key it presents doesn’t match the one your device pinned, the operation stops and asks you to confirm your phrase instead of sealing blindly under a server-supplied key. That pinning is on the web app today. An account created on the phone is pinned the first time you sign in on the web; until then, re-sealing your recovery key asks you to confirm your phrase rather than trusting the key the server offered.
One practical limit, stated plainly rather than discovered at the worst moment: recovery is completed in the web app. The phone app sets your phrase up and hands you to the website to use it.
If you lose both your password and your recovery phrase, your zero-knowledge data is unrecoverable by design: we cannot decrypt it for you. There is no administrative flow that opens your account for you: the one administrative action that touches your password forces a reset, and completing it still requires your recovery phrase.
8. Key management and rotation
Server-side keys live in a managed secret store, validated on startup. The operational field key is a versioned ring wrapped by a managed KMS key; the blind-index key and the per-user metadata seed are single managed secrets rather than rings. Each scheme (KDF, vault-key, asymmetric seal, server key) carries a version column designed for zero-downtime rotation: new writes use the current version, and readers treat the stored version as a fast path rather than a truth claim: if it doesn’t decrypt, they try every key in the ring (safely, because the GCM auth tag guarantees only the right key yields valid plaintext) and a fallback win increments a drift counter rather than being silently absorbed. That counter lives in the process today and is not yet exported to our metrics, which is a gap we are closing. The integrity guarantee is the retirement check below, not the version tag. The columns that wrap your keys, meaning the master-key wrap, the vault-key wrap and both private-key wraps, are protected by database triggers that refuse to null or blank them.
In practice a full server-key rotation runs in a maintenance window today rather than hot: both versions live in the ring, so everything decrypts throughout, and the old version is only retired after a check that refuses to drop it while a single row still depends on it. For the field-encryption ring that check actually decrypts a sample before it lets go. The blind-index key is checked more weakly, by counting version stamps on the tables that carry one; a check that re-derives an index, and stamps for the tables still missing them, are in progress. What’s described here is the mechanism, not a routine operation, and we won’t claim hot rotation until it has been exercised end to end.
9. Application and infrastructure security
- Row-level security: the database itself refuses to hand one account another’s
rows, and the application connects as a role that neither owns those tables nor
can bypass their policies, so the policies bind it. On the most sensitive tables
(vault, identities, messages, contacts, consents, recovery keys) the policy calls
a context function that raises when the request context is missing, so a
forgotten wrap fails loudly instead of quietly returning nothing; those tables
also carry
FORCE ROW LEVEL SECURITY, which subjects the table’s owner as well. On the remaining protected tables a missing context yields no rows rather than an error. Two static guards run before a change lands: one requires every account-scoped table to carry a policy or a written, reviewed exemption, and one rejects a query issued outside a bound context. Some tables are deliberately exempt (pre-authentication lookups, provider webhooks that must resolve an owner before it is known, and trust-and-safety records), each with its reason recorded. Raising the weaker tables to the stronger mechanism is not finished. - Transport: TLS everywhere, with HTTP redirected to HTTPS at every edge; security headers (CSP/HSTS), where the content-security policy is enforced by the applications rather than at the CDN; CSRF defended via SameSite cookies and a strict origin allowlist.
- Network: each auxiliary service runs in its own VPC, reached over a dedicated point-to-point peering connection, and no NAT gateway is ever shared between them.
- Encryption at rest: the database, and the buckets holding your content, evidence and audit logs, are encrypted with customer-managed AWS KMS keys; the cache and the access-log buckets are encrypted with provider-managed keys. All of it sits underneath the application-layer and zero-knowledge encryption above.
- Defense in depth: a managed web application firewall fronts the application’s public edge, with continuous threat detection, control-plane audit logging, flow logs on the application networks, and encrypted, versioned backups. Our static marketing site is served by a different provider and does not sit behind that firewall; it holds no account data.
- Observability is pseudonymous by default: our application database stores no raw IP address. Every IP we record is hashed under a server secret, per account and per day, which a database leak alone cannot reverse. The CDNs that serve your app and your media are deliberately configured to keep no access logs at all. The load balancers and the firewall in front of them do log connection IP addresses, as any internet service does, and our cloud provider’s control-plane audit trail retains the source address of administrative API calls for several years, because the point of an audit trail is that it cannot be trimmed. Telemetry is designed to carry no user identity, and the server-only records we keep for safety and routing (the verdicts of content checks that found nothing, carrier-lookup cache, spam-velocity counters) are purged on a 90-day sweep. Your own call and message history is not on that clock: it stays until you close your account. Be precise about what deleting a message does today: it removes the message from your apps, and the row stays on our side until your account is closed. There is no delete control for your call history yet. Both are on the roadmap; until then we would rather you knew.
- We erase what we no longer need to hold. An expired phone verification does not keep your number: past a short grace window, we reduce the record to the ownership proof the anti-takeover protection needs (a keyed hash, not the number) and erase the encrypted number itself along with the verification PIN. What we don’t keep, a compromise can’t reveal.
- We don’t keep your user agent. The full string your browser sends is a low-entropy fingerprint, so hashing it obscures it rather than removing it. We reduce it at collection to a coarse label from a fixed 49-value allowlist, like “Chrome / macOS”: no version, no device model, no key. Whatever could single you out is discarded before anything is written. That is data minimization, not anonymization: the label still sits on a row carrying your account id.
- Provider data-minimization: the carrier learns only what routing requires. The card processor (Stripe) identifies you to us only by an opaque identifier, never a name, email, phone number or address; it does see what you bought, because a line item on a receipt has to say what it is (“Phone Number”, a plan change, an add-on) along with the tax jurisdiction that applies. We read a completed payment back from them to confirm it, and we ask nothing else about you. The Bitcoin processor (Blockonomics) sees only a payment address and an amount.
10. Limitations and non-goals
We state these plainly:
- Endpoint compromise. Malware or a malicious extension on your device can read your data after you unlock it. Client-side encryption cannot defend a compromised client. On current iOS builds, the notification-extension copy described in section 5 also leaves the account communication key available after the first device unlock while the vault or device is locked, until sign-out deletion succeeds.
- No forward secrecy on stored messages (section 5).
- Vault integrity. Each entry is sealed to its own identity: a server can neither read one nor move it into another field or another account. For the collection we hand back, an authenticated manifest under your vault key (section 11) is active: your device detects if a server omits an entry or serves an older version of one. The second layer, per-entry version binding (“zk2”, section 11), is active too: new accounts have it from the start. An existing account moves over in one pass the next time it signs in on the web. If you only ever use the phone app, your entries move over one at a time as you edit them, so until then some of them stay on the first binding, without that second layer. Two honest limits remain: both clients block on a failed check rather than merely warning, but on the web there is a brief window before that gate mounts, and a whole-vault rollback on a fresh device with no local history remains possible, a residual every stateless zero-knowledge vault shares.
- Recovery pinning does not yet cover the recovery signing key (section 7): a server that substituted it would cause a denial of service at recovery time, not a read of your data.
- No post-quantum protection yet (section 11): a future quantum adversary could decrypt today’s X25519-sealed data (“harvest now, decrypt later”).
- Phone-service metadata (who called whom, when, for how long) is inherent to the public network and visible to carriers, and we cannot make the carriers’ copy private. On our side we hold your call and message history until you close your account (see section 9 for what deleting a message does and does not do today).
- Closing your account is not instant, and not total. There is a 30-day grace period, and signing back in during it cancels the deletion. After that, personal data is permanently deleted except invoices and billing records held at least 7 years under Canadian tax law, showing amount, date, taxes, payment-method type and product with no identity data, communications or encrypted content; the verdict of any content check that flagged something and the preserved copy for either match class in section 5 (only the confirmed-illegal class is legally compelled); and security audit logs held up to 7 years for service security.
- Server-readable operational data (phone numbers, billing) is not zero-knowledge (section 4), because it is required to operate.
What a full server compromise learns
Take the worst case seriously, because it is our threat model (section 2): an adversary who controls our servers also holds the server-side keys. The inventory below describes what the stored data and those keys reveal before the attacker changes any client code. On the web, an active adversary could also deliver an altered client and target later sessions, as section 2 states.
They read your phone numbers and SIP credentials, the link between your account, your identities and your numbers, your call and SMS metadata for as long as you have kept it, active calling-device routing metadata, and the account link, opaque event id, and attempt time in the registration-limit ledger until its 24-hour sweep. They can enumerate the blind indexes, including the keyed fingerprints of live and revoked recognition codes (section 4’s honest paragraph), but cannot reverse a high-entropy recognition code from its fingerprint. For any private-beta account that has not completed the authentication migration in section 6, they also obtain a static login verifier that could let them sign in to that account and read its server-readable data. They do not read from the stored copy your vault, the content of your identities, your contacts, your custom calling-device names, or your message content, apart from the private-beta communications named in section 5, provided authentic and untampered client code encrypted that data. Those opening keys are yours, held on our side only in a form we cannot open, and we cannot be compelled to produce a key we do not hold. This is section 4’s boundary, stated from the attacker’s side, and it is why we minimize and expire (sections 9 and 11): the only data a compromise can never reveal is the data we didn’t keep.
11. Roadmap
- End-to-end encrypted messaging between WIGGWIGG users. Under our current method, we seal your messages at rest to the account communication key, which makes the sealed copy unreadable to us. The small private-beta set described in section 5 remains readable by our servers until its re-sealing is complete. It is not end-to-end encryption: a message crossing the public phone network is visible to carriers in transit, and no phone service can change that. We intend to add end-to-end encrypted messaging between WIGGWIGG accounts, where the plaintext never exists outside the two devices, in the way Signal does it. It is not built yet, and until it ships we will not describe anything we do as end-to-end encrypted. For a conversation that needs that property today, use Signal.
- Canada-only infrastructure. What we store, we store in Canada. The parts we do not own do not all work that way: telephone traffic passes through our carrier’s US infrastructure, and payments through our processor. We intend to move to Canadian providers end to end as we grow. The supply of Canadian carriers and processors that meet our other requirements is thin today, so we are not there, and we will not claim to be until every hop is.
- Post-quantum: hybrid X25519 + ML-KEM-768 key wrapping for the message seal and recovery wrap, so an attacker must break both. This directly addresses harvest-now-decrypt-later for long-term stored data.
- Auth: evaluate an asymmetric PAKE (OPAQUE).
- Recovery: retire the symmetric recovery verifier (section 7) so entering recovery, like signing in, sends us no secret at all; pin the recovery signing key as well as the sealing key; and bring recovery itself, and recovery-key pinning, to the phone app.
- Vault integrity: both layers are active. The first, an authenticated manifest under your vault key (the encrypted packing list of your entries plus an anti-rollback counter), makes a server-side deletion or rollback detectable. The second (“zk2”), active as well, cryptographically binds each entry’s version to the manifest’s, so a server can’t quietly roll a single entry you haven’t touched back to an older version either. Two things remain on the roadmap here: closing the short window on the web between a page rendering and the integrity gate mounting, and bringing the one-pass migration to the phone app, which today rewrites an entry to the new binding as you edit it rather than all at once.
- Retire RSA-2048 (the communications fallback seal in section 3, and admin support encryption) in favor of the X25519 sealed box.
- Broaden context-binding (AAD) and per-user key derivation across all server-encrypted fields.
- Third-party audit: a planned external cryptographic and application review.
12. Verification and disclosure
- Audit the crypto: github.com/WiggWiggDev/crypto. The KDF, seal, signing, and recovery primitives are exactly what this document describes. Open source proves the design and the client-side primitives; it does not, by itself, prove our servers run them unmodified, so reproducible client builds and published bundle hashes are on the roadmap.
- Report a vulnerability: see our vulnerability-disclosure policy (or the machine-readable security.txt). We offer a good-faith safe-harbor for responsible disclosure and aim to acknowledge within one business day.
This document is versioned and dated. If reality and this document ever disagree, that is a bug in one of them. Tell us.