3
$\begingroup$

General

My goal is to encrypt some strings and store them in a database.

I am using RSA OAEP in Go (Golang):

rsa.EncryptOAEP(sha256.New(), rng, rsaPublickey, plaintext, nil)
rsa.DecryptOAEP(sha256.New(), rng, rsaPrivatekey, ciphertext, nil)

I generated a public/private key pair with 4096 bits.

My understanding of RSA is very basic. I read some similar questions on stack overflow that say that RSA OAEP adds padding to ensure that a different ciphertext is generated for the same plaintext for security purposes.

I am more confident with using AES because if you supply the same symmetric key and the same initialization vector you will always get the same ciphertext.

P.S: I understand that RSA and AES have different use cases when it comes to sending data over the network and I should not treat them the same, but for my personal use case I want to use them for encryption/decryption of strings.

Question

  1. Since different ciphertexts are generated for the same plaintext, is there a guarantee that I will be able to decrypt these RSA OAEP ciphertexts in the future ?

Implementations change, new libraries, functions & new versions of programming language runtimes are used, etc.

I am indifferent to the RSA OAEP implementation that will be used for decryption (Go, Java, Python, openssl, ...) as long as I can get the plaintext from the ciphertext.

The Go documentation says Encryption and decryption of a given message must use the same hash function. In my case this hash function is SHA256.

Additionally the Go documentation says The random parameter is used as a source of entropy to ensure that encrypting the same message twice doesn't result in the same ciphertext. I haven't seen this random parameter (rng in my case) used in other programming languages. It seems to be specific to the Go RSA OAEP implementation.

So if I use an RSA OAEP decryption implementation with the same:

  • Private key
  • Hash function

I should be able to get the plaintext from all the encrypted strings. From what I see these 2 things seem to be the only requirements needed for decryption. Is this correct ? The answer is probably yes, but if possible, it would be nice to receive some more information about this to increase my confidence.

$\endgroup$

3 Answers 3

3
$\begingroup$

Every asymmetric encryption method must return different ciphertexts for every encryption of the same plaintext. Otherwise, given a ciphertext and the public key but not the corresponding private key, an attacker could try to guess the plaintext: the adversary could validate guesses by trying the encryption and comparing the actual ciphertext with the ciphertext they calculated.

To ensure that every encryption returns a different ciphertext, the result of the encryption needs to depend on a value that cannot be found or guessed from just the ciphertext, the plaintext and the public key. A random value is the only practical choice. Depending on the choices of the library designer, the way to generate that random value may be either an internal detail of the implementation or a parameter of the encryption interface. Doing it internally makes the interface easier to use, which is generally better, but having it as a parameter has some advantages: it allows repeating a sequence of operations for auditing or debugging and getting the exact same outputs, and it allows the implementation to not depend on global state such as the RNG state (a necessity in purely functional languages).

Interfaces to symmetric encryption are typically different because the security characteristics are different and the common usage is different. Symmetric encryption is often used in protocols where the same key is used with many messages, and the protocol specifies what unique input (IV/nonce) to use when encrypting and decrypting (this allows to not store or transmit this unique input). With RSA-OAEP, the random nonce is encoded inside the ciphertext anyway. Symmetric encryption typically doesn't need the nonce to be secret, whereas we've seen that it's important for asymmetric encryption. Symmetric encryption can actually do without a nonce: you get deterministic encryption, where the ciphertext only depends on the secret key and the plaintext. Deterministic encryption makes it apparent when the same plaintext is encrypted twice, but since an attacker doesn't have the encryption key (which is the same as the decryption key), this can't be leveraged to guess the plaintext for an unknown ciphertext. As we've seen, it would be a major problem for asymmetric encryption since the attacker has the encryption key.

You can't verify that an asymmetric encryption succeeded by comparing the result with a known value. You need to apply the decryption process (which requires having the private key). (And similarly, many asymmetric signature methods are randomized and so you can't check that a signature is correct by comparing it to a known value — however, it is possible to design a secure, deterministic asymmetric signature mechanism, for example PSS is one if you use an empty salt.)

RSA-OAEP has a precise specification. To decrypt, you only need to specify:

  • the hash function used for the label;
  • the mask generation function, which in practice is always MGF1 with the same hash function;
  • the private key;
  • the ciphertext.

With these inputs, the decryption procedure specifies how to convert the ciphertext to a number, apply the RSA private-key operation to obtain another number, transform that number into a padded-and-masked plaintext, decompose that and unmask the masked part to obtain the ciphertext. (Optionally a label can be used — but you can decrypt without knowing the label in advance.) Conversely, the encryption procedure takes 5 inputs: a hash function, a mask generation function, a public key, a plaintext and a random seed. (Plus an optional label which affects the masking-and-padding.) Given these inputs, it specifies how to turn the random seed into a mask, apply that mask to the plaintext and pad the result, transform the padded-and-masked plaintext into a number, apply the RSA public-key operation to obtain another number, and convert that number into a string which is the ciphertext. Two implementations of RSA-OAEP encryption will always produce the same ciphertext if given the same inputs including the random number (which is normally not passed separately by the caller, but obtained directly by the implementation). And two implementations of RSA-OAEP decryption will always produce the same plaintext if given the same inputs.

$\endgroup$
2
  • $\begingroup$ Added a section at the end to make it even more clear how the standard defines the ciphertext, hope you agree, otherwise revert. I'll delete this comment if/when you react :) $\endgroup$
    – Maarten Bodewes
    Commented Aug 16, 2022 at 9:23
  • $\begingroup$ "Deterministic encryption makes it apparent when the same plaintext is encrypted twice, but since an attacker doesn't have the encryption key (which is the same as the decryption key), this can't be leveraged to guess the plaintext for an unknown ciphertext." I just wanted to point out that if the data is drawn from a relatively small distribution, an adversary might use statistics to infer plaintext from repeated ciphertext. e.g., if 10% of an encrypted "handedness" value is 0, and the remaining 90% is 1, I might infer that 0 is the ciphertext for "left-handed" and 1 is "right-handed". $\endgroup$ Commented May 3, 2023 at 15:22
1
$\begingroup$

That sounds right.

Specifically in OAEP a one-use random seed is created by the encryptor that determines a mask for the padded message and the seed itself is then masked and included in the payload. The decryptor does not have to be able to recreate the seed in the same way as the encryptor as they can take the masked seed from the decrypted payload and should be able to recover the seed by removing the mask.

Of course the payload recovered needs to be same as the payload sent so that the decryptor must have the private key associated with the public key used by the encryptor. Likewise, the decryptor must be able to reconstruct the same mask as used by the encryptor and so must use the same Mask Derviation Function (MDF). In your implementation it sounds like a cryptographic hash function is being used as the MDF and so the same hash must be used by encryptor and decryptor.

Provided that the ciphertext is unaltered and the decryptor has both the decryption key and the function used to generate the mask, recovery is assured.

$\endgroup$
4
  • $\begingroup$ Thanks for some clarification. However, I think the hashes generated by the encryptor and decryptor are not the same. A new random or pseudo-random hash is generated every time encryption or decryption happens. The same hash function - SHA256 - is used, but a different hash value is generated each time. This hash value is used in some way, but I don't know how. Apparently the decryptor doesn't need the original hash value that the encryptor computed, it just needs to know the hash function used. $\endgroup$
    – Subzero123
    Commented Aug 11, 2022 at 17:34
  • $\begingroup$ That's fine because both encryptor and decryptor will be using the same hash function on the same input for the same payload-ciphertext instance, and so obtain the same value. Different inputs will occur for different instances but the same encryptor and decryptor will still create identical outputs in any individual instance. $\endgroup$
    – Daniel S
    Commented Aug 11, 2022 at 17:40
  • $\begingroup$ “Mask derivation function” might be more appropriate since this is a pseudorandom process, but “mask generation function (MGF)” is an established term when it comes to OAEP. All implementations use MGF1, which is a specific construction parametrized by a hash function (not the hash function itself). $\endgroup$ Commented Aug 16, 2022 at 18:54
  • $\begingroup$ @Subzero123 A hash function is deterministic: it always produces the same output from a given input. OAEP does use a hash function internally, and part of the input to that hash function is a value that's generated randomly when encrypting, and stored (somewhat indirectly) inside the ciphertext. See the specification if you want to know exactly how it's done. $\endgroup$ Commented Aug 16, 2022 at 18:57
1
$\begingroup$

I haven't seen this random parameter (rng in my case) used in other programming languages.

Python has a defaultable argument and also for the 'legacy' PKCS1-v1_5 padding.

OpenSSL has a (fairly complicated) RNG scheme internal to the library. Normally you don't specify anything and it uses defaults, but you can optionally make separate calls to configure/tailor it before doing an operation that uses it, such as RSA encryption either OAEP or v1_5.

WebCrypto (in browser javascript) similarly has a builtin RNG that is implicitly used when needed. (For javascript run on nodejs, the builtin 'crypto' module -- and for recent versions also a WebCrypto shim on top of it -- is actually a wrapper for OpenSSL, so see above.)

Java uses a single 'facade' class javax.crypto.Cipher for all ciphers and cipher modes, regardless of what parameters (including randomness) they need. As you can see in that javadoc there are multiple overloads of init which correspond to different use-cases, some including an RNG and some not; if you use one without, e.g. init(int,Key,AlgorithmParameterSpec) for an instance that needs randomness (like RSA-OAEP) it uses a default RNG as described.

$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.