Good job releasing your project! It's a cool idea and surprisingly minimalist. That said, I've found a number of cryptographic flaws in the application source. This should not be used in instances where the encryption is mission-critical.
1) You generate a random key [0] and then feed it into PBKDF2 [1] to generate a 32-byte AES-GCM key. If you can generate 32 random bytes instead of 10 reduced-ASCII characters and a key stretch, just do that. PBKDF2 is for turning a password into a key, and it's far from the recommended algorithm nowadays; prefer scrypt if you need to do this sort of thing.
2) AES-GCM with random 12-byte nonces. Never use random IVs with GCM; this breaks the authentication [2] [3]. Given the pitfalls of AES-GCM with respect to random nonces, you might prefer switching to XSalsa20+Poly1305. The advantage of XSalsa is it has an extended nonce length, so you can use random nonces without fear.
3) Random key derivation with a restricted character set can make brute force attacks easier. You should have a 256-bit random key, and if you want that key to be within a certain character set, then encode the byte output from the CSPRNG using that character set.
4) 1fps achieves symmetric key distribution via a URL with a fragment identifier ("#") which IIRC is not sent to the server. Therefore it assumes you have a secure key distribution channel - the link contains the key, so it's important that only the intended recipient can view the part after the "#". If the server is truly malicious, it can deploy client-side Javascript to send the fragment to the server, allowing the server to access the key (and thus cleartext communication).
[0]: https://github.com/1fpsvideo/1fps/blob/main/1fps.go#L99
[1]: https://github.com/1fpsvideo/1fps/blob/main/1fps.go#L287
Those papers are a bit over my head. Could you please explain what's wrong with using random IVs here? What should we do instead (assuming we can only use GCM, and not switch to chacha)
There's two issues.
Background: the key+IV define a keystream which is xor-ed against the message. The same key+IV generate the same keystream. Thus you can XOR two cipher texts and reveal information from the two plaintext.
AES-GCM is authenticated encryption. To combat known-ciphertext-attacks, you want to have authenticated cipher texts. AES-GCM specifically is vulnerable to an attack with a reused IV to recover the authentication key. Allowing you to forge authentication tags and employ a KCA.
The solution, if you're stuck with aes, is to switch to XAES-GCM or better AES-GCM-SIV. Alternatively you must use a counter or checkes system to not reuse IV. Since this is in the context of 1fps, you could use unix timestamp + random bytes to reduce the chance of collisions.
Is the statement just that if you use a random value for a nonce rather than some guaranteed never-used-once value, it's possible to get a collision faster than the "natural" block collision complexity (half block size or something like that)?
It's a birthday attack principle. With only 96bits after roughly a billion messages with the key and random IVs, you start reaching realistic probabilities that you will reuse an IV
And how you will get a billion messages with 1 frame per second?
Not an expert, but this is my understanding.
1. It is necessary for nonces to never be re-used for a given key lest you open yourself to a certain class of attacks that can decode all messages using that key. This is specific to AES-GCM due to how it internally reuses nonces.
2. AES-GCM uses very small nonces, making the probability of randomly using the same nonce unacceptably as the number of messages encoded with a given key increases (as it would with each frame sent on 1fps).
You can avoid all this by using a different primitive with a longer nonce such as XSalsa (a version of Salsa with a 192-bit nonce)
In AES-GCM (simplified explanation), an encrypted message takes 3 inputs: plaintext, a symmetric encryption key (generally unique per-session, as in this program), and a 12-byte nonce (a.k.a. IV).
If an attacker intercepts 2 messages that were encrypted with the same key and the same nonce, they can reveal the authentication key used for the generation of authentication tags (auth tags), and they can then forge auth tags for any message. These auth tags are how a recipient verifies that the message was created by someone who knows the symmetric key that was used to encrypt the plaintext, and that it was not altered in transit.
More simply, it allows an attacker to alter the ciphertext of an encrypted message, and then forge an authentication tag so that the modification of the ciphertext could not be detected. It does not reveal the symmetric key that allows decryption of the ciphertext, or encryption of arbitrary plaintext.
If a random nonce is generated, there is a chance that it is the same as a random nonce that was generated earlier in the session. Since the nonce is 12 bytes, this chance is very small for any 2 random nonces (1 in 2^96), but the chance of a collision increases rapidly with the number of encrypted messages sent in a session (see the birthday problem). It still requires a large number of messages to be sent before the chance of a collision becomes significant: "after 2^28 encryptions the probability of a nonce collision will be around 0,2 % ... After 2^33 [encryptions] the probability will be more than 80 %"[0]
If this program is sending 1 message per second (1 FPS), it would take 8 years for 2^28 messages to be sent. I haven't looked at the code, it may well be sending many more messages than that.
The alternative to a random nonce is a "counter" nonce, which starts at 1 and increments with each message. The potential pitfall of counter nonces is that they can be harder to implement, as they require tracking and updating state. If the program ever fails to track or update this state correctly, nonce reuse will occur. A different counter must be used for each symmetric key (which should be randomly generated for each session).
EXTRA CREDIT: There is also information revealed about the plaintext of the 2 messages that used the same key and nonce - specifically, the XOR of the 2 plaintexts. While this doesn't directly reveal the plaintexts, if some information about one of the plaintexts is known, that can be used to reveal information about the other plaintext.
I learned most of this information from David Wong's Real-World Cryptography.
[0]: https://eprint.iacr.org/2016/475.pdf, section 3.3
I feel like there are so many pitfalls when designing this - is there something standard and trusted (would TLS work?) that you could build your application on top of?
I guess TLS has a dependency on the public key infrastructure (eg Let's Encrypt, or whoever issues wifey accepted certs). Which makes end to end encryption between users harder (most of this stuff is intended for server auth and encryption)?
But otherwise big +1 not to reimplement crypto when the are alternatives. Another option for secret key stuff might be ssh?
There is no requirement to use TLS with webPKI if you are making your own application (not the browser), you can use TLS with custom certificate mangement.
You still need to figure out how you handle trust and key authentication somehow, but that is true of all cryptographic protocols.
I assume there's TLS in the server connection already, but the encryption here is to make the communication unavailable to the server for decryption, so "bare" TLS does not solve the problem.
With TLS you need pubkeys you can trust (the certificate authority hierarchy provides that trust for the open Internet) or you're vulnerable to MITM. You could potentially share pubkeys using a similar out-of-band mechanism to that currently used for symmetric key distriubtion, and tunnel that TLS connection through the server's shared comms channel. That would work OK for two parties, but it becomes significantly more cumbersome if you want three or more, since each TLS session is a pairwise key exchange. Notably, however, this would not transit secret keys through server-controlled web pages where they could be available to Javascript. Something like Noise [0] might also be useful for a similar pubkey model.
Unfortunately, this kind of cryptography engineering is hard. Key distribution and exchange is hard. There isn't much of a way around learning the underlying material well enough to find this sort of issue yourself, but misuse-resistant libraries can help. Google's Tink [1] is misuse-resistant and provides a handful of blessed ways to do things such as key generation, but I'm not sure if it's suitable outside of cloud deployments with KMS solutions. nacl/secretbox handles straight encryption/decryption with sound primitives, but it still requires a correct means of key generation [2] and distribution.
[0]: http://www.noiseprotocol.org/noise.html
[1]: https://github.com/tink-crypto/tink-go
[2]: https://pkg.go.dev/golang.org/x/crypto/nacl/secretbox
It would be hard to do end-to-end TLS (where the server proxies the raw connection) because
(a) you can't share one TLS connection to the host between multiple clients; if you wanted multi-client support while preserving end-to-end TLS, the host would need to maintain a TLS connection with each client and waste bandwidth re-uploading the same image
(b) there is no client software requirement, so you would have to do the TLS decryption clientside in the browser (maybe via WASM) unless you're OK with having viewers download software
Agree. When people hear the adage "don't roll your own crypto", they often think it refers to crypto primitives only. In reality, it's also hard to design a secure crypto protocol, even if the underlying crypto primitives are secure.
Do you have a recommendation to address #4? That seems like an intrinsic problem for web apps, see also ProtonMail.
You're very right! Luckily, we can resolve the vulnerability in this instance, although it's a challenging problem to resolve in general webapps.
The technical explanation for our issue is that the client-side Javascript in our webapp is trusted. To quote the late Ross Anderson [0, pg. 13], "a trusted system or component is one whose failure can break the security policy." In this case, our security policy is that the server must not be capable of viewing our screenshots. Our goal is to make that trusted Javascript more trustworthy: that is, closer to a system that can't fail.
We're at an advantage in this case: there's an open-source application on GitHub with eyeballs[1] on it that users must run on their endpoint machines. Given that we already have source-available local code running, we could instead serve the UI from the local Go application and use CORS[2] to permit access to the remote server. If the local application is trustworthy, and we're only sending data (not fetching remote Javascript), then the local client UI is trustworthy and won't steal your keys. If users run binaries directly from 1fps (as opposed to building from source), then you would want some multi-party verification that those binaries correspond directly to the associated source [3].
Protonmail is almost surprising: it's supposed to be end-to-end encrypted, but it's not end-to-end encrypted in the presence of a malicious server. If, say, a government order compelled Protonmail to deploy a backdoor only when a particular client visited the site, most users would be unaffected and the likelihood of discovery would be low.
[0]: https://www.cl.cam.ac.uk/~rja14/book.html
[1]: https://en.wikipedia.org/wiki/Linus%27s_law
[2]: https://stackoverflow.com/a/45910902
[3]: https://en.wikipedia.org/wiki/Reproducible_builds
It seems the answer is "no" --- am I right to understand it that way?
Another attempt at compression: use a native app to serve Javascript for the web app so you don't have to trust any server
I don't mean to skip anything, it's just not clear to me how related some of it is, and if it's lengthy just because it is, or because I'm missing something thats very significant (ex. cite + name + page # to help say "something we trust is something we rely on", link to Wikipedia as source for "eyeballs make bugs shallow"
Links are just for reference, but the gist is: serve the webapp from the Go binary instead. The end-user already has to trust the Go binary, and if they need to they can look at the code once and confirm it's not vulnerable. I prefer this to browser extensions because the audit trail process from source to browser extension is less clear; even for open-source browser extensions, I still have to trust the author to not add code that isn't in the repository.
Meta / WhatsApp have developed their own solution for the whatsapp web client (whatsapp is end-to-end-encrypted): https://engineering.fb.com/2022/03/10/security/code-verify/
it takes the form of a browser extension the user downloads that will tell the user if the javascript code is what it is expected to be. it checks this by verifying the code's expected hash with an endpoint hosted by Cloudflare. Whatsapp can publish new versions to Cloudflare but they can't modify them.
In this case it makes it so that you are trusting Cloudflare instead of just WhatsApp, but (as an amateur), I don't see why this couldn't be adapted into a standard that works with something like a blockchain or certificate authorities (or even something like a git host to go along with public source code auditing?). I think something like this should become a standard and be built into browsers, but currently not a lot of companies are using any solution at all.
The only other implementation of a solution to this that I found, which I think is pretty similar, is Etesync's pgp signed webpages library + browser extension (https://stosb.com/blog/signed-web-pages/), which allows the developer to PGP sign web pages so you know the code has not been modified by a malicious server without the developers approval. So maybe you can use that in your project I guess, or there are probably some other solutions that I haven't found
I think this problem might be called "Code Verification" in cryptography, if you want to look more into it
Apart from having a local binary / extension / some bookmark URI magic I don't think so.
A "lighter" /alternative to a local binary is to a have a local index.html and use SRI when linking to the remote scripts [0]. But seems clunky as well...
[0]: https://developer.mozilla.org/en-US/docs/Web/Security/Subres...
That's pretty cool and this is exactly why I am here :) To have this kind of advice. I'll implement these changes as soon as I can.
This is such a healthy interaction, it makes me so happy to see people lifting each other up like this
Love to see things like this on HN.
You will still need to get the nonce and key generation right, but I'd recommend using Golang's nacl/secretbox [0] for a project such as this. It's designed to be relatively misuse-resistant compared to using underlying primitives directly, and under the hood it's XSalsa20+Poly1305 - so you can use random nonces with negligible collision risk.
[0]: https://pkg.go.dev/golang.org/x/crypto/nacl/secretbox
Thank you for sharing this and recommending XSalsa20+Poly1305. I have always been interested in cryptography, so learning about the many ways why one shouldn't roll their own crypto AND protocol is very cool.
Out of curiosity, is the primary reason you don't recommend fixing the nonce issue in this specific case due primarily to the pitfalls in doing so or is it more nuanced and related to the general issues mentioned in the articles above?
A naive perspective could be that one uses AES-GCM because it is used in so many places, such as TLS or SRTP, and someone who is not very well versed in cryptography assumes it can be the way to go.
AES-GCM has more issues than merely the nonce reuse in the context of random nonces. For instance, the short tag issue[0] leaks authentication (not encryption) keys after a probabilistic "forged" message.
In general, the move in modern cryptography engineering is to assume the end user does not know what they are doing. For GCM, you have to get the nonces right and you need the right tag length, and the design uses lookup tables so it's prone to timing attacks in many implementations.
Later on I didn't just recommend an algorithm but a specific implementation (at least if we can find a better method of symmetric key distribution): nacl/secretbox [1]. This is a cryptographic library designed to be misuse-resistant, a property of cryptographic designs that makes implementation errors more difficult. nacl is a few years behind the curve inasmuch as it arguably gives the end-user too much control over key generation, but it permits random nonces (being based upon XSalsa) and provides a simple API that is difficult to mess up.
AES-GCM is secure with a correct implementation, but to build a correct implementation you often need to know the specific library inputs and configuration settings to produce your desired outcome. Something like secretbox doesn't give you those options: you get one relatively secure configuration ... and that's it!
[0]: https://csrc.nist.gov/csrc/media/projects/block-cipher-techn...
[1]: https://pkg.go.dev/golang.org/x/crypto/nacl/secretbox
This is an excellent analysis! It's amazing what can be found with a minimal source code review.
With regard to point 4 (secure key distribution channel), as far as I can tell there is no good pki built into the browser, My point being. any pki tooling has to be shipped by the server and you have to trust the server to supply you honest tools. The saving grace is that this does not really matter and each domain could send you totally broken tools and only be able to steal keys produced for their domain.
footnote: there are client side certs, however because there is no tooling for them built into the browser usability sucks, I want to try to get public key auth working on my toy js application and the browser tooling for user generated keys sucks. I am tempted to use ssh keys(I like ssh keys), but will probably see if I can get hoba working. https://datatracker.ietf.org/doc/html/rfc7486 I got all excited about hoba when I first read about it, but am now a bit bitter when as found out that there is zero internal browser support.
Any tips on how/where one can learn more on these topics? I find cryptography fascinating, but whenever I've tried looking for some resources on my own, they all flew hilariously above my head, with dozens of acronyms and terms I've never even heard of before even as a native English speaker.
Came here to point out the PBKDF use in each frame but found this fantastic write up
"Never use random IVs with GCM; this breaks the authentication"
Why could one not use Encrypt-then-HMAC and HMAC-then-Decrypt with a random IV ?
(Serious question. It definitely sounds like you know what you are talking about, I just can't see what I am missing here)