1. 97
  1.  

  2. 33

    My hat to the author for taking the time to not shame the dev and providing context around how “common” the issue is. I really like this article, How to learn cryptography as a programmer and A furry’s guide to end-to-end encryption. Very informative and accessible.

    1. 8

      Agreed, it’s all too common for articles like this to have a very condescending tone. Well done, article!

      One thing I think the article is missing is that it doesn’t mention OTR (https://en.wikipedia.org/wiki/Off-the-Record_Messaging) which solves the specific problem the RSA-using developer was trying to address.

      1. 2

        I believe the double ratchet system used in Signal (which is recommended in the article) is a direct descendent of OTR.

    2. 10

      In my opinion, we should stop shipping cryptography interfaces that…

      • Mix symmetric and asymmetric cryptography in the same API
      • Allow developers to encrypt directly with asymmetric primitives
      • Force developers to manage their own nonces/initialization vectors
      • Allow public/private keys to easily get confused (e.g. lack of type safety)

      Ironically, Libsodium is guilty of 3 of those items: crypto_box() does both key exchange and authenticated encryption, nonces must be managed manually most of the time (though by default they can just be random), and it’s very easy to swap arguments by mistake. Not that they can help it: it’s a C library after all. My own Monocypher also forces nonce management on users, and arguments are easily swapped.

      On the other hand, I have a plea for binding authors: for the love of what is holly please mitigate those problems when porting our libraries to your language. Especially if your standard library provides a cryptographically secure RNG.

      1. 2

        When I bound libsodium for Monte, I found these problems too. I wasn’t able to remove nonce management either, but I did orient keys to try to make misuse a little harder. This PyCon 2016 demonstration program shows off the API. It works, but I’m definitely less-than-enthused with the nonce management. (Also, yes, the demonstration uses symmetric techniques with asymmetric keys. Not great for posterity.)

      2. 8

        I appreciate the intentions behind this post, but as a cursory introduction to a common problem in cryptography, I worry that this article muddies together a number of concepts, and I’m taking the time to write a correction here given how this have been upvoted to the top of Lobsters and could therefore mislead some developers.

        This design completely lacks forward secrecy. This is the same reason that PGP encryption sucks.

        This is just bizarre, because it strongly implies that the project whose cryptography the author is criticizing, “Zuccnet”, “completely lacks” forward secrecy because it uses RSA. But RSA is a primitive for public key encryption. Forward secrecy, on the other hand, is a property of a cryptographic protocol. Using RSA or not using RSA doesn’t have direct bearing on whether or not you obtain forward secrecy. RSA itself cannot possibly “lack” or “offer” forward secrecy, and constructing an argument based on this logic makes no sense:

        1. Were I to replace RSA usage with AES-CBC, AES-GCM, XSasla20-Poly1305, etc. — none of that would grant me or take away forward secrecy.
        2. Were I to follow the author’s advice and encrypt symmetric keys using RSA, that wouldn’t grant me forward secrecy, either, if I don’t have a protocol that manages the way those keys are generated/derived, used and refreshed.
        3. Even if I were to use an authenticated key exchange as the author later suggests, that itself doesn’t guarantee forward secrecy, either! It simply guarantees, as the name suggests, an authenticated key exchange step for the protocol.

        I think that it would be better for the author here to more clearly distinguish between RSA as a primitive and the design of the protocol they are criticizing, to avoid misleading new readers. It’s important to understand that RSA does not affect forward secrecy and vice versa. The conflation with PGP further muddies the comparison and mixes together a bunch of contexts that in reality aren’t very closely related.

        Some cryptography libraries let you treat RSA as a block cipher in ECB mode and encrypt each chunk independently. This is an incredibly stupid API deign choice: […]

        Calling this an “incredibly stupid design choice” doesn’t make sense to me, because the supposed “design choice” itself has been fundamentally misunderstood and is being miscommunicated. The author here is almost certainly referring to RSA constructions being referred to as, for example, RSA/ECB/OAEPWithSHA1AndMGF1Padding. This is a naming scheme that was first promoted in Java and that has found itself copied into a tiny number other, largely Java-inspired frameworks.

        As noted in Java documentation and in ample references around the web, it is highly misleading to refer to how RSA Encryption is used as “ECB mode”. The “ECB” here doesn’t actually mean anything — it’s just a stand-in for there not being a a real block cipher mode of operation, and was likely added as part of the naming scheme for ciphers so that asymmetric ciphers are referred to in a way that structurally is similar to that of symmetric block ciphers (eg. AES/CBC/PKCS5PADDING).

        Working around [the lack of forward secrecy] requires an Authenticated Key Exchange (AKE)

        Some popular protocols, such as Signal or the Noise Protocol Framework, do establish some forward secrecy (and post-compromise security) via an AKE, but this doesn’t mean that an AKE is required to obtain forward secrecy. In the case of Signal, the majority of the forward secrecy and post-compromise guarantees are actually not even guaranteed by the AKE at all but by the subsequent ratcheting mechanism, with the AKE only setting the stage for that and offering forward secrecy for session initialization only.

        Protocols can achieve forward secrecy via periodic key rotation or other mechanisms that don’t implicate an AKE, and this could be preferable depending on the use case scenario and execution context.

        Finally, the “Recommendations” section contains pieces of advice that all seem to conflict with one another:

        • RSA is for encrypting symmetric keys, not entire messages. Pass it on.

        • Consider not using RSA.

        • Instead, if you find yourself needing to encrypt a message with RSA, remind yourself that RSA is for encrypting symmetric keys, not messages. And then plan your protocol design accordingly.

        • You should use RSA-KEM instead of what I’ve sketched out […]

        If you’re the party planning the protocol design, then why would you find yourself needing to encrypt a message with RSA? If it’s better not to use RSA at all, then why is the article’s subheading mentioning that “RSA is for encrypting symmetric keys”? If one were to use a KEM, why would they use an RSA-based KEM?

        I think the article is better off just providing a simpler, more coherent recommendation that leads people away from RSA entirely. As it is, I could read this article as a new cryptography engineer and walk away with four conflicting recommendations.


        As others have noted, this post is commendable for not shaming the developer of “Zuccnet” and trying to raise the bar against common cryptography mistakes, so I’d like to congratulate the author their intentions but wish more time was spent on a polished execution. If folks are interested, I’d like to suggest some readings on protocol design that could serve as a more coherent reference on how to think about protocols, primitives, etc. (yes, they’re from ePrint, but they’re not harder to read than this blog post, I promise!):

        1. 3

          I mostly agree with you Nadim but I cannot think of a way to do PFS with RSA.

          Except for very scientific constructions like having a million RSA keys and throwing away all the used ones. The problem is that you cannot really hash an RSA key to a new key. That’s why 0-RTT PFS for TLS is so cool. But it requires puncturable encryption.

          So, practically speaking, I would agree that using RSA encryption means you don’t get PFS.

          1. [Comment removed by author]

            1. [Comment removed by author]

              1. [Comment removed by author]

          2. 2
            1. If you try to encrypt a message longer than 256 bytes with a 2048-bit RSA public key, it will fail. (Bytes matter here, not characters, even for English speakers–because emoji.)
            2. This design completely lacks forward secrecy. This is the same reason that PGP encryption sucks.

            Could these tradeoffs be worth it if it means the system is really simple and easy to understand?

            1. 12

              The first one, no. Breaking on large messages is a serious usability pain-point, and doing a hybrid public key encryption is 100% worth the additional complexity.

              The second one, YES! If you make the threat model clear, then eliminating forward secrecy greatly simplifies your protocol. (Implementing X3DH requires an online server to hand out “one-time pre-keys” to be totally safe.) At worst, you’re as bad off as PGP encryption (except, if you follow the advice in my blog, you’re probably going to end up using an authenticated encryption construction rather than CAST5-YOLO).

              1. 1

                The first one, no. Breaking on large messages is a serious usability pain-point, and doing a hybrid public key encryption is 100% worth the additional complexity.

                Isn’t it something people are quite used to though? Both SMS and tweets have a character limit.

                But let’s say we do want to go with the simplest secure model, without forward secrecy but no character limit. So hybrid encryption but not X3DH. What library functions would the smart developer use?

                1. 5

                  If they’re using libsodium? crypto_box_seal() and crypto_box_seal_open(). Problem solved for them.

                  If they’re using OpenSSL (or one of the native wrappers), something like this:

                  type SealedMessage = {cipher: Buffer, tag: Buffer, wrappedKey: buffer};
                  const DOMAIN_SEPARATION_AES = Buffer.from('AES-256-CTR');
                  const DOMAIN_SEPARATION_HMAC = Buffer.from('HMAC-SHA256');
                  
                  function hmacSha256(msg: string|Buffer, key: Buffer): Buffer {
                      const hmac = crypto.createHmac('sha256', key);
                      hmac.update(msg);
                      return hmac.digest();
                  }
                  
                  function seal(msg: string|Buffer, recipientPublicKey: Buffer): SealedMessage {
                      // Generate and wrap the primary key 
                      // (which is split into two keys: one for AES, one for HMAC)
                      const key = crypto.randomBytes(32);
                      const aesKey = hmacSha256(Buffer.concat([key, DOMAIN_SEPARATION_AES]), key);
                      const macKey = hmacSha256(Buffer.concat([key, DOMAIN_SEPARATION_HMAC]), key);
                      const rsaCiphertext = crypto.publicEncrypt(
                          {
                              key: recipientPublicKey,
                              padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
                              oaepHash: "sha256",
                          },
                          key
                      );
                      
                      // Encrypt the data
                      const nonce = crypto.randomBytes(16);
                      const aes = crypto.createCipheriv('aes-256-ctr', aesKey, nonce);
                      const ciphertext = Buffer.concat([
                          nonce, 
                          aes.update(Buffer.from(string)), 
                          aes.finish()
                      ]);
                      
                      // Authenticate the data
                      const tag = hmacSha256(ciphertext, macKey);
                      
                      return {
                          cipher: ciphertext,
                          tag: tag,
                          wrappedKey: rsaCiphertext
                      };
                  }
                  
                  function unseal(sealed: SealedMessage, secretKey: Buffer): Buffer {
                      const key = crypto.privateDecrypt(
                          {
                              key: secretKey,
                              padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
                              oaepHash: "sha256"
                          },
                          sealed.wrappedKey
                      );
                      const aesKey = hmacSha256(Buffer.concat([key, DOMAIN_SEPARATION_AES]), key);
                      const macKey = hmacSha256(Buffer.concat([key, DOMAIN_SEPARATION_HMAC]), key);
                      const nonce = sealed.cipher.slice(0, 16); // AES-CTR nonce size
                      const ciphertext = sealed.cipher.slice(16);
                      if (!crypto.timingSafeEqual(sealed.tag, hmacSha256(ciphertext, macKey)) {
                          throw new Error("Integrity check failed");
                      }
                      const aes = crypto.createDecipheriv('aes-256-ctr', aesKey, nonce);
                      return Buffer.concat([aes.update(ciphertext), aes.final()]);
                  }
                  

                  (This is why “just use libsodium” is so much better.)

                  1. 1

                    Please consider using Pastebin for code; Lobsters renders code in a larger-appearing font than text in its comment section and doesn’t seem to fold it away properly, creating a wall of text that makes it harder to scroll through comments.

                    1. 1

                      I somewhat agree, but I don’t think that there’s a good pastebin which is free to Lobsters without signup and also allows posts to persist. (The Reputation Problem disincentivizes such a service; it would be open to abuse.) It would be cool if Lobsters had the ability to click to expand/hide long code snippets.

                      1. 1

                        Definitely the best solution would be for Lobsters to fix code rendering in comments.

                        1. 4

                          We have an issue tracking this if anyone wants to pick up the work

                      2. 1

                        For what it’s worth, that comment looks ok to me (Chrome on Windows).

                2. 2

                  If you are okay with giving up on security (e.g. for educational purposes) then it could be worth it.

                  In practice absolutely not.

                  1. 1

                    Giving up on security is too vague, sorry. Can eve read my messages? No? Then I think I’m pretty safe.

                    1. 2

                      Maybe bfiedler refers to the second point, meaning if Eve compromises Alice’s private key, then Eve can read past, present and future messages. My personal opinion is that this should be default for any secure messaging system.

                3. 2

                  RSA is also really hard to get right and has a million gotchas. Aside from side-channel issues, problems range from flawed padding to poor key generation. There is also the question of key strength since people keep downwardly adjusting the estimate for just how strong 2048 and 4096-bit keys really are assuming you did everything else right.