Encryption with Encryption Key returns different output with the same data

I experienced a rather strange outcome while using the anvil.secrets.encrypt_with_key(key, value) function. I intended to use this to encrypt and decrypt user passwords in my database.

What I expected was that when I encrypt a password using a particular key, I should be able to get the same ciphered text everytime, but that wasn’t the case. When I executed the snippet of code below, I got different results encrypting the same password twice using the same encryption key.

def encrypt_password(password):
  return anvil.secrets.encrypt_with_key('ENCRYPTION_KEY', password)   

def decrypt_password(encrypted_password):
  return anvil.secrets.decrypt_with_key('ENCRYPTION_KEY', encrypted_password)   

#Testing
print("Encrypted password: ", encrypt_password('0000'))  
print("Encrypted password: ", encrypt_password('0000'))  

Maybe I have something wrong in the implementation. Please can someone take a look and help me resolve this issue.
Thanks.

the code seems fine. What’s important is that you will always get the same decrypt value based on using the same encryption key.

And you do.

The details of the type of encryption can be found here: https://anvil.works/docs/security/encrypting-secret-data

I guess some further reading into the algorithm will lead to a deeper understanding of the nuance you describe.

2 Likes

Thanks @stucork.

I do get the same value when I decrypt with the same key but that doesn’t really help me. For authentication, I would need to encrypt the entered password and compare with the encrypted password in my database. If I get different outputs when encrypting the same password value using the same encryption key, it just doesn’t work in this scenario.

couldn’t you instead test the decrypted password against the decrypted password?

I have so many entries in my database. For this to work I would have to decrypt every single entry and compare.

It would be far simpler to just encrypt the entered password, then query my database looking for a match.

1 Like

Maybe you could provide more details about the query you’re doing here?

It’s a simple login query.
My anvil app connects to an external database using pymysql. Find the sample query below:


...

SQL = 'SELECT * FROM users WHERE (username=%s) AND (password=%s)'

vals = (username, encrypted_password)

cursor.execute(sql, vals)

user = cursor.fetchone()

...

is the username not unique here? in which case you don’t need the password for the initial query.

Hi @Agobin,

If you’re doing password authentication, this is a somewhat dangerous way to do it! Is there a reason you’re not using the Users Service, which has this all built in?

This post got long, and it still doesn’t cover all the considerations in building password-based authentication. This is something with lots of sharp edges, and I strongly, strongly recommend using the Users service if you can.


The layout you’re proposing uses symmetric encryption for the passwords, which means you (or any administrative user) can decrypt any password you want! This generally isn’t what your users expect from you, and is generally considered bad hygiene.

Instead, you should be using a strong one-way function, such as bcrypt – “one way” means that you can’t simply decrypt it. If you have the password, you can check it, but that’s all. However, bcrypt isn’t repeatable: if you give it the same input twice, you’ll get two different outputs, so you still can’t use it in the sort of query you’re doing.

(This is for really good reasons! If it produced the same output every time for the same password, it would be easy to spot which users in your database had the same password. Plus, you could compute an up-front table of the hashes of, say, the 100 million most common passwords, which would make it easy to break the hashes if they ever leaked. This is one reason why using “ordinary” hashing algorithms, such as SHA256, isn’t a good idea for password storage. So instead, bcrypt uses a “salt”: a random value that’s different every time, so that encoding the same password twice gives different results. bcrypt also does a deliberately slow computation, so that brute-forcing the hashes – by trying every password in the world – is slow.)

So, how do you use the bcrypt module? Again, check the docs, but here’s an example:

import bcrypt

password = b"super secret password"
hashed = bcrypt.hashpw(password, bcrypt.gensalt())

# Now store the hashed password, not the original!
# When it's time to check the password, retrieve the hash from the database
# (use the username as your selection criterion, not the hash), and do:

if bcrypt.checkpw(password, hashed):
  print("It matched!")
else:
  print("It didn't match")

The bcrypt module is available in every server-side environment, including Basic Python.

(Note that bcrypt is expecting a bytes object, not a str object. If you’re giving it a string, you’ll want to do password.decode().)


This is not a complete guide to password authentication; there are a lot more questions than just the hashing algorithm, and lots of subtleties, and getting it wrong can lead to moderate-to-severe security vulnerabilities. This is why we built a robust password implementation into the Users Service, and we recommend you use that rather than rolling your own!

Again, please use the Users Service if you can!

5 Likes

Thanks very much @stucork and @meredydd for your wonderful contributions, I really appreciate the support. I can’t use the Users service because of some regulations concerning the application.

I have definitely learned more about the subject now. Thanks again.

1 Like

Interesting, I never thought about laws affacting authentication.

In your case, this might help
https://anvil.works/docs/how-to/custom-user-auth

A post was split to a new topic: Client-side bcrypt