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:
parent
27928ad833
commit
dc6054f09c
2 changed files with 66 additions and 10 deletions
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue