Encrypt passwords stored in Redis

Using symmetric encryption in the `cryptography`'s `Fernet` class,
we can ensure that no one can snoop the passwords simply by having access
to the Redis store.

An encryption key is sent to the secret receiver, along with the 32 character
Redis key that identifies the secret, which is needed to decrypt the password.
This commit is contained in:
Samuel Dion-Girardeau 2017-05-10 22:53:50 -04:00
parent 27928ad833
commit dc6054f09c
2 changed files with 66 additions and 10 deletions

View file

@ -4,3 +4,4 @@ MarkupSafe==0.18
Werkzeug==0.9.4 Werkzeug==0.9.4
itsdangerous==0.23 itsdangerous==0.23
redis==2.8.0 redis==2.8.0
cryptography==1.8.1

View file

@ -8,11 +8,14 @@ from redis.exceptions import ConnectionError
from flask import abort, Flask, render_template, request from flask import abort, Flask, render_template, request
from cryptography.fernet import Fernet
SNEAKY_USER_AGENTS = ('Slackbot', 'facebookexternalhit', 'Twitterbot', SNEAKY_USER_AGENTS = ('Slackbot', 'facebookexternalhit', 'Twitterbot',
'Facebot', 'WhatsApp', 'SkypeUriPreview') 'Facebot', 'WhatsApp', 'SkypeUriPreview')
SNEAKY_USER_AGENTS_RE = re.compile('|'.join(SNEAKY_USER_AGENTS)) SNEAKY_USER_AGENTS_RE = re.compile('|'.join(SNEAKY_USER_AGENTS))
NO_SSL = os.environ.get('NO_SSL', False) NO_SSL = os.environ.get('NO_SSL', False)
TOKEN_SEPARATOR = '~'
app = Flask(__name__) app = Flask(__name__)
@ -49,20 +52,72 @@ def check_redis_alive(fn):
return inner return inner
def encrypt(password):
"""
Take a password string, encrypt it with Fernet symmetric encryption,
and return the result (bytes), with the decryption key (bytes)
"""
encryption_key = Fernet.generate_key()
fernet = Fernet(encryption_key)
encrypted_password = fernet.encrypt(password.encode('utf-8'))
return encrypted_password, encryption_key
def decrypt(password, decryption_key):
"""
Decrypt a password (bytes) using the provided key (bytes),
and return the plain-text password (bytes).
"""
fernet = Fernet(decryption_key)
return fernet.decrypt(password)
def parse_token(token):
token_fragments = token.split(TOKEN_SEPARATOR, 1) # Split once, not more.
storage_key = token_fragments[0]
try:
decryption_key = token_fragments[1].encode('utf-8')
except IndexError:
decryption_key = None
return storage_key, decryption_key
@check_redis_alive @check_redis_alive
def set_password(password, ttl): def set_password(password, ttl):
key = uuid.uuid4().hex """
redis_client.setex(key, ttl, password) Encrypt and store the password for the specified lifetime.
return key
Returns a token comprised of the key where the encrypted password
is stored, and the decryption key.
"""
storage_key = uuid.uuid4().hex
encrypted_password, encryption_key = encrypt(password)
redis_client.setex(storage_key, ttl, encrypted_password)
encryption_key = encryption_key.decode('utf-8')
token = TOKEN_SEPARATOR.join([storage_key, encryption_key])
return token
@check_redis_alive @check_redis_alive
def get_password(key): def get_password(token):
password = redis_client.get(key) """
From a given token, return the initial password.
If the token is tilde-separated, we decrypt the password fetched from Redis.
If not, the password is simply returned as is.
"""
storage_key, decryption_key = parse_token(token)
password = redis_client.get(storage_key)
redis_client.delete(storage_key)
if password is not None: if password is not None:
password = password.decode('utf-8')
redis_client.delete(key) if decryption_key is not None:
return password password = decrypt(password, decryption_key)
return password.decode('utf-8')
def empty(value): def empty(value):
@ -104,13 +159,13 @@ def index():
@app.route('/', methods=['POST']) @app.route('/', methods=['POST'])
def handle_password(): def handle_password():
ttl, password = clean_input() ttl, password = clean_input()
key = set_password(password, ttl) token = set_password(password, ttl)
if NO_SSL: if NO_SSL:
base_url = request.url_root base_url = request.url_root
else: else:
base_url = request.url_root.replace("http://", "https://") base_url = request.url_root.replace("http://", "https://")
link = base_url + key link = base_url + token
return render_template('confirm.html', password_link=link) return render_template('confirm.html', password_link=link)