The argument is old. In 2011, Matasano Security published a piece titled "Javascript Cryptography Considered Harmful" that became the standard reference for why browser-based crypto was a bad idea. Reading it now is instructive, because roughly half of it has been fixed and the important half has not.
What got fixed
The 2011 critique bundled several distinct problems. Several were genuinely solved.
- Missing primitives. Hand-rolled JavaScript implementations of AES and SHA-2 were the norm, with predictable results. The Web Crypto API now provides vetted native implementations, including non-extractable keys that script can use but cannot read out, and WebAssembly allows shipping an audited library rather than a reimplementation.
- Unreliable randomness. Early code seeded from clocks and mouse movement.
crypto.getRandomValuesnow bridges to the platform generator, which is the same source native code uses. Our piece on secure randomness covers why that distinction is not cosmetic. - Plaintext transport. In 2011, plenty of sites still served scripts over HTTP, so any network attacker could rewrite the crypto code in flight. Universal TLS and HSTS preloading closed that specific hole.
A browser today can perform the cryptography correctly. That was the easier problem.
What did not get fixed
The remaining issue is that you cannot verify the program you are running, because you receive a new copy every time and nobody else necessarily receives the same one.
When you install a desktop or mobile application, the artifact is signed, distributed through a channel that serves the same bytes to everyone, and it sits on your machine unchanged between updates. If a vendor shipped a backdoored version, it would go to every user at once, and anyone comparing hashes would see it. That visibility is the actual security property, and it comes from the distribution model rather than from the code.
A web application inverts each of those. The server decides what to send after seeing who is asking. It can serve one bundle to the general public and a different one to a single account, on a single request, and revert immediately afterward. No other user observes it. The user targeted has no baseline to compare against, because the code is regenerated on every load anyway.
This is not an assertion that any provider does this. It is a statement about what the architecture permits and what the user can check. In a native distribution model, targeting one user requires a detectable act. In a web delivery model, it requires a conditional in a request handler. The threat models that care about compelled or coerced providers are exactly the ones this distinction matters for.
What an audit covers
A security audit examines a version. For a native client, the version audited is close to the version users run, and reproducible builds can tie the published binary to the reviewed source. For a web client, the audited snapshot may be replaced dozens of times before the report is published, with no signature, no version pin, and no mechanism by which a user can tell which revision their browser just executed.
This is why security-focused messengers tend to distribute installable applications even for desktop use, rather than a browser-based client. It is a distribution decision more than a cryptographic one.
The partial mitigations
| Mechanism | What it covers | What it misses |
|---|---|---|
| Subresource Integrity | Third-party scripts matching a declared hash | The HTML that declares the hashes, which the same server writes |
| Content Security Policy | Injected and third-party script execution | First-party code, which is the concern here |
| Extension-based verification | Comparing delivered code against a published manifest | Users who do not install the extension, which is most of them |
| Certificate Transparency | Mis-issued certificates for the origin | Code served legitimately by the real origin |
The third row is the most ambitious of these. In 2022, Meta shipped a browser extension called Code Verify for its web messaging client, which compared a hash of the loaded code against a manifest published to an independent third party. The design is sound and the reach is the problem: verification that requires installing a separate extension only protects the small fraction of users who install it, and the extension itself becomes another component requiring trust and updates.
Standards work on signed web bundles and integrity manifests aims at making this native to the platform. Until a browser refuses to run an application whose bundle does not match a publicly logged hash, the property native apps get from code signing and binary transparency has no web equivalent.
Two smaller differences worth knowing
Memory hygiene. JavaScript gives you no way to reliably zero a buffer. Strings are immutable, the garbage collector copies objects around, and key material may persist in memory long after use. Typed arrays and non-extractable Web Crypto keys reduce the exposure without eliminating it. Native code can at least attempt it.
Environment malleability. The page runs inside an environment shared with extensions and with anything else the browser permits. A browser extension with page access can read the DOM of your webmail before encryption or after decryption, which is the point our piece on extension security makes at length. A native app is not immune to a compromised device, but the attack surface is narrower and better isolated.
Where this leaves web clients
Not condemned. A web client is the difference between having access to your encrypted account from a borrowed machine and not having it, and for a large number of people the realistic alternative is not a native encrypted app, it is an unencrypted provider.
The right framing is a layered one. A web client is appropriate when your adversary is a network observer, a mass-collection pipeline, or a server operator who reads stored data. It is the wrong tool when your adversary can compel the provider to target you specifically, because that is the case the delivery model cannot defend. Anyone whose threat model includes targeted state-level pressure should be using an installed, signed client and verifying it, and that includes users of ours: Haven ships a web client, and it carries the same property described here as every other web client does.
The question worth asking a provider is not whether their web app uses end-to-end encryption. It is what a user can do to check that the code running in their tab today is the code that was reviewed. If the answer is nothing, that is the real specification, and it should be weighed against what the alternative actually is for that user.