0
$\begingroup$

(TL;DR at the end)

Let's say the plaintext hello is encrypted with AES (CFB mode). I noticed that decryption works no matter the given password! Of course if the password is wrong, the output will be garbage, but no error will be thrown (example in Python):

import Crypto, Crypto.Random, Crypto.Hash, Crypto.Cipher.AES
p = b'hello'
iv = Crypto.Random.new().read(Crypto.Cipher.AES.block_size)
cipher = Crypto.Cipher.AES.new(Crypto.Hash.SHA256.new(b'password1').digest(),
                          Crypto.Cipher.AES.MODE_CFB, iv).encrypt(p)
print(Crypto.Cipher.AES.new(Crypto.Hash.SHA256.new(b'wrongpassword').digest(),
                          Crypto.Cipher.AES.MODE_CFB, iv).decrypt(cipher))
# b'\xfdQ\xd7\xa3\x1b'

I read that HMAC etc. can be used to ensure integrity, or AES in GCM mode.

But to keep things simple, I wanted to know if this method is secure or not:

p = b'hello'
startingtag = b'504B0304'   # fixed constant, like "MZ" for DOS .exe files, or "%PDF-1.5" for PDF files
iv = Crypto.Random.new().read(Crypto.Cipher.AES.block_size)
cipher = Crypto.Cipher.AES.new(Crypto.Hash.SHA256.new(b'password1').digest(),
                          Crypto.Cipher.AES.MODE_CFB, iv).encrypt(startingtag + p)
s = Crypto.Cipher.AES.new(Crypto.Hash.SHA256.new(b'wrongpassword1').digest(),
                          Crypto.Cipher.AES.MODE_CFB, iv).decrypt(cipher)
if s[:len(startingtag)] == startingtag:
    print('ok good password, here is the plaintext:')
    print(s[len(startingtag):])
else:
    print('wrong password entered.')    

What does it do? A starting tag is encrypted along the plaintext. Then we check if the first bytes of the decrypted text matches this starting tag.

This is basically the method explained here. I had the same questionning than a comment there:

  • if the first block of the plaintext is a known value, can't an attacker easily get the key from looking at the first block of the ciphertext?

  • But on the other hand this seems true as well:

    Having a known header isn't a problem, otherwise encrypting something like a Microsoft Word document (that also has a known header) would be insecure.


TL;DR:

  • Is it secure to encrypt with AES CFB mode a plaintext for which the first bytes are known (e.g. %PDF-1.5 for a PDF document),
  • or does this give too much information to an attacker? Then does this mean it's a bad to encrypt documents with known headers with AES?
$\endgroup$
3
  • $\begingroup$ The Crypto.Random... line is pointless; just use os.urandom(128). It's the same. $\endgroup$
    – Legorooj
    Commented Dec 3, 2019 at 7:47
  • $\begingroup$ As advised in many answers, I finally used AES in GCM mode. Here is a working code: AES: how to detect that a bad password has been entered? directly inspired by PyCryptoDome's code sample. $\endgroup$
    – Basj
    Commented Dec 3, 2019 at 12:59
  • $\begingroup$ BTW, OpenPGP did something like this using CFB and some integrity checks, and it proved to cause some problems in their case... Not that your "scheme" seems vulnerable to the same kind of attacks, but just to remind everyone that it's best to be careful when doing this kind of things ;) $\endgroup$
    – Lery
    Commented Dec 4, 2019 at 23:26

3 Answers 3

3
$\begingroup$

Is it secure to encrypt with AES CFB mode a plaintext for which the first bytes are known (e.g. %PDF-1.5 for a PDF document)

Yes. Known (and even chosen) plaintext is a standard assumption in any moder cipher, including AES-CFB (and CTR, CBC, OFB...). It does not "give too much information to an attacker", and it is not "bad to encrypt documents with known headers with AES".

True, having a known plaintext at start to allow a test of the password makes password search a little simpler. But only a little, and we customarily ignore that little. One reason (stated in the question) is that actual plaintext typically contains recognizable sequences. And when not, it is typically compressible, when the incorrect plaintext obtained for the wrong password is not, which allows automated recognition of having the correct password anyway.

Another problem is that CFB is weak at detecting message alteration. Using a modern, authenticated encryption mode such as AES-GCM would solve that. It turns out this is readily available in PyCryptodome's Crypto.Cipher.

However, there is a different, serious problem with the code !

It has poor security because the Crypto.Hash.SHA256.new(b'password1') part of it builds the AES key from a password using a standard cryptographic hash (SHA-256). That conversion from password to key is fast, and that allows to test passwords at high speed. This makes the system very vulnerable to password crackers (which essentially test plausible passwords at high speed, including short ones, and those from or inspired from a dictionary of common passwords/words).

I use a random Initialization Vector, so doesn't this help (against the above problem)?

No, because that does not appreciably slow down testing a password.

That issue has plagued password-based encryption since the origin of that, and progress in computing (faster CPUs, GPUs, FPGAs, ASICs..) directly worsen it, to the point that nowadays, very practically, if users can memorize a password, and a standard hash is used to turn it into a key, then the system is very weak (much to the pleasure of Three Letters Agencies and more casual/greedy attackers, which routinely use password crackers, and succeed).

In the 1990 we started to get iterated password-to-key derivation functions, like PBKDF2 of RFC 2898, which essentially slow down conversion from password to key by requiring many hashes. Initially "many" was at least a thousand, but nowadays we'd need hundred thousands just to keep with the progress of technology. Until about 2015, that was the industrial state of the art (what your mobile phone or computer used).

We are slowly moving to memory-intensive (or memory-hard) password-to-key functions. The pioneer was bcrypt (which almost accidentally required sizable memory), then scrypt (theorizing the use of large memory and multiple CPUs when available). The Password Hashing Competition gave us Argon2 (which unfortunately fails to catch AFAIK, perhaps because it's complex and has many options). There is also Balloon (which does not catch either, perhaps because it was not in the PHC).

In any case, it is nowadays inadequate and grossly incompetent to turn a password into an encryption key using anything lesser than an iterated hash with many thousand iterations; and it is highly recommendable to additionally make that process require sizable memory.

Could you include an example of code with which you would replace AES.new(SHA256.new(b'password1').digest(), AES.MODE_CFB, iv).encrypt(p) to make use of another hash?

Crypto.SE is not for code recommendations; and we need another kind of hash. One variously called entropy-stretching key derivation function, password-based hash, or some mix of that. It must be purposely as slow as possible for the attacker, yet fast enough for the application. Also, it will have a salt input, and it is a good idea to feed that with the IV/nonce, of at least server+user ID (this was missing in an earlier version of this answer).

The good news is that we no longer have to choose between several evils (see earlier version of this answer): PyCryptodome now has entropy-stretching key derivation functions, including scrypt which is memory-hard. This is the obvious choice. Be sure to up the N parameter as much as bearable.

Sample Python code there, illustrating how PyCryptodome's scrypt can turn a password into a key of AES-GCM, with GCM's nounce also scrypt's salt.

Caveat: nothing in this post should be construed as an endorsement of PyCryptodome's security.

$\endgroup$
3
  • $\begingroup$ Thanks. I'll probably use pycryptodome.readthedocs.io/en/latest/src/cipher/… then (there is code sample there). $\endgroup$
    – Basj
    Commented Dec 3, 2019 at 12:41
  • $\begingroup$ @Basj: another update: scrypt is now in Crypto!! That makes things a lot easier. $\endgroup$
    – fgrieu
    Commented Dec 3, 2019 at 13:23
  • 1
    $\begingroup$ @Basj It's a different package, but cryptography has a cipher recipe called fernet (available here) that includes all of your requirments. It is authenticated (via AES-CBC and HMAC) and raises an error if the decrypt fail for whatever reason. Have a play with it and see if you can use it or adapt it to your needs. $\endgroup$
    – Legorooj
    Commented Dec 4, 2019 at 1:36
1
$\begingroup$

As secure as any. Known headers aren't going to decrease security, but using SHA256 as a KDF will. Use Scrypt; it's many times more secure. And iv = Crypto.Random.new().read(Crypto.Cipher.AES.block_size) is better as iv = os.urandom(128). It's the same underlying function, and AES' block size doesn't change.

$\endgroup$
4
  • $\begingroup$ Thank you for your answer. Known headers aren't going to decrease security: I was in doubt because it's a case in which an attacker knows both plaintext and ciphertext for a few bytes (the header), so it gives some information. $\endgroup$
    – Basj
    Commented Dec 3, 2019 at 8:02
  • 1
    $\begingroup$ @Basj some but not enough to attack a mode like CFB. It'll give minimal decrease is security. $\endgroup$
    – Legorooj
    Commented Dec 3, 2019 at 8:03
  • $\begingroup$ PS: For completeness, could you include an example of code with which you would replace AES.new(SHA256.new(b'password1').digest(), AES.MODE_CFB, iv).encrypt(p) to make use of Scrypt? (possibly with the same library Crypto to avoid having to install another one). $\endgroup$
    – Basj
    Commented Dec 3, 2019 at 8:03
  • $\begingroup$ Will do by tomorrow $\endgroup$
    – Legorooj
    Commented Dec 3, 2019 at 8:06
0
$\begingroup$

You may notice that the length of the output of the encryption function is the same as the length of the plaintext.

That suggests an inherent problem. In order to verify something, we need something else to compare it to. But that something else hasn’t been attached to the ciphertext (or else the length would be different) and wasn’t returned separately either.

The reason is that it doesn’t exist. AES-CFB is not an authenticated mode. The data can be freely tampered with, and there is no way to detect this, because no additional information was stored.

AES-CFB is just a building block and very rarely something you should use on its own. It’s also a pretty unconventional mode.

If you want to encrypt data, you should generally use an authenticated mode such as AES-GCM (that can also be tricky to use correctly) or the XChaCha20-Poly1305 construction.

There is another problem here. Passwords are predictable. The key space is pretty small. They can be brute forced. So, they should never be used directly as a key, even after hashing them once with a conventional hash function.

Your first step should be to use a password hashing function, such as bcrypt. That function will return a key you can use then for authenticated encryption.

$\endgroup$
5
  • $\begingroup$ Thank you for your answer. In fact I don't want to check the data integrity / "if the data has been tampered", I only want to inform the user when he entered the wrong password, so it's a bit different. $\endgroup$
    – Basj
    Commented Dec 2, 2019 at 21:38
  • $\begingroup$ Passwords [...] should never be used directly as a key, even after hashing them once with a conventional hash function.: I use a random initialization vector (iv), so doesn't this help in this case? $\endgroup$
    – Basj
    Commented Dec 2, 2019 at 21:39
  • $\begingroup$ In order to inform the user that the password is wrong (not necessarily a good idea, btw) you still need to compare what you stored with something else. That something else can be a keyed hash of the (encrypted) password, which is exactly what an authentication tag usually is. $\endgroup$ Commented Dec 2, 2019 at 21:51
  • $\begingroup$ The random IV doesn’t help at all. If the database is breached, that IV will be leaked along with the encrypted password. Look for password hashing functions instead (bcrypt, scrypt, yescript, argon2...) $\endgroup$ Commented Dec 2, 2019 at 21:53
  • $\begingroup$ Do you see a problem in the proposed way to do it (compare startingtag = b'504B0304' with decrypt(encrypt(startingtag), real_password), user_input_password) $\endgroup$
    – Basj
    Commented Dec 2, 2019 at 21:53

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.