Use MultiFernet for encryption
Facilitates key rotation and uses more secure encryption than what sqlalchemy-utils does. Fixes #117 and #119.
This commit is contained in:
@ -13,9 +13,7 @@ from cryptography.hazmat.backends import default_backend
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy import event, Integer, ForeignKey, String, DateTime, PassiveDefault, func, Column, Text, Boolean
|
||||
|
||||
from sqlalchemy_utils import EncryptedType
|
||||
|
||||
from lemur.utils import get_key
|
||||
from lemur.utils import Vault
|
||||
from lemur.database import db
|
||||
from lemur.plugins.base import plugins
|
||||
|
||||
@ -213,7 +211,7 @@ class Certificate(db.Model):
|
||||
id = Column(Integer, primary_key=True)
|
||||
owner = Column(String(128))
|
||||
body = Column(Text())
|
||||
private_key = Column(EncryptedType(String, get_key))
|
||||
private_key = Column(Vault)
|
||||
status = Column(String(128))
|
||||
deleted = Column(Boolean, index=True)
|
||||
name = Column(String(128))
|
||||
|
@ -72,7 +72,7 @@ SECRET_KEY = '{flask_secret_key}'
|
||||
|
||||
# You should consider storing these separately from your config
|
||||
LEMUR_TOKEN_SECRET = '{secret_token}'
|
||||
LEMUR_ENCRYPTION_KEY = '{encryption_key}'
|
||||
LEMUR_ENCRYPTION_KEYS = '{encryption_key}'
|
||||
|
||||
# this is a list of domains as regexes that only admins can issue
|
||||
LEMUR_RESTRICTED_DOMAINS = []
|
||||
@ -171,7 +171,9 @@ def generate_settings():
|
||||
settings file.
|
||||
"""
|
||||
output = CONFIG_TEMPLATE.format(
|
||||
encryption_key=base64.b64encode(os.urandom(KEY_LENGTH)),
|
||||
# we use Fernet.generate_key to make sure that the key length is
|
||||
# compatible with Fernet
|
||||
encryption_key=Fernet.generate_key(),
|
||||
secret_token=base64.b64encode(os.urandom(KEY_LENGTH)),
|
||||
flask_secret_key=base64.b64encode(os.urandom(KEY_LENGTH)),
|
||||
)
|
||||
|
@ -12,9 +12,8 @@
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy import Column, Integer, String, Text, ForeignKey
|
||||
|
||||
from sqlalchemy_utils import EncryptedType
|
||||
from lemur.database import db
|
||||
from lemur.utils import get_key
|
||||
from lemur.utils import Vault
|
||||
from lemur.models import roles_users
|
||||
|
||||
|
||||
@ -23,7 +22,7 @@ class Role(db.Model):
|
||||
id = Column(Integer, primary_key=True)
|
||||
name = Column(String(128), unique=True)
|
||||
username = Column(String(128))
|
||||
password = Column(EncryptedType(String, get_key))
|
||||
password = Column(Vault)
|
||||
description = Column(Text)
|
||||
authority_id = Column(Integer, ForeignKey('authorities.id'))
|
||||
user_id = Column(Integer, ForeignKey('users.id'))
|
||||
|
@ -21,7 +21,7 @@ SECRET_KEY = 'I/dVhOZNSMZMqrFJa5tWli6VQccOGudKerq3eWPMSzQNmHHVhMAQfQ=='
|
||||
|
||||
# You should consider storing these separately from your config
|
||||
LEMUR_TOKEN_SECRET = 'test'
|
||||
LEMUR_ENCRYPTION_KEY = 'jPd2xwxgVGXONqghHNq7/S761sffYSrT3UAgKwgtMxbqa0gmKYCfag=='
|
||||
LEMUR_ENCRYPTION_KEYS = 'o61sBLNBSGtAckngtNrfVNd8xy8Hp9LBGDstTbMbqCY='
|
||||
|
||||
# this is a list of domains as regexes that only admins can issue
|
||||
LEMUR_RESTRICTED_DOMAINS = []
|
||||
|
@ -5,17 +5,93 @@
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
import six
|
||||
from flask import current_app
|
||||
from cryptography.fernet import Fernet, MultiFernet
|
||||
import sqlalchemy.types as types
|
||||
|
||||
|
||||
def get_key():
|
||||
def get_keys():
|
||||
"""
|
||||
Gets the current encryption key
|
||||
Gets the encryption keys.
|
||||
|
||||
This supports multiple keys to facilitate key rotation. The first
|
||||
key in the list is used to encrypt. Decryption is attempted with
|
||||
each key in succession.
|
||||
|
||||
:return:
|
||||
"""
|
||||
|
||||
# when running lemur create_config, this code needs to work despite
|
||||
# the fact that there is not a current_app with a config at that point
|
||||
try:
|
||||
return current_app.config.get('LEMUR_ENCRYPTION_KEY').strip()
|
||||
except RuntimeError:
|
||||
print("No Encryption Key Found")
|
||||
return ''
|
||||
keys = current_app.config.get('LEMUR_ENCRYPTION_KEYS')
|
||||
except:
|
||||
print("no encryption keys")
|
||||
return []
|
||||
|
||||
# this function is expected to return a list of keys, but we want
|
||||
# to let people just specify a single key
|
||||
if not isinstance(keys, list):
|
||||
keys = [keys]
|
||||
|
||||
# make sure there is no accidental whitespace
|
||||
keys = [key.strip() for key in keys]
|
||||
|
||||
return keys
|
||||
|
||||
|
||||
class Vault(types.TypeDecorator):
|
||||
"""
|
||||
A custom SQLAlchemy column type that transparently handles encryption.
|
||||
|
||||
This uses the MultiFernet from the cryptography package to faciliate
|
||||
key rotation. That class handles encryption and signing.
|
||||
|
||||
Fernet uses AES in CBC mode with 128-bit keys and PKCS7 padding. It
|
||||
uses HMAC-SHA256 for ciphertext authentication. Initialization
|
||||
vectors are generated using os.urandom().
|
||||
"""
|
||||
|
||||
# required by SQLAlchemy. defines the underlying column type
|
||||
impl = types.Binary
|
||||
|
||||
def process_bind_param(self, value, dialect):
|
||||
"""
|
||||
Encrypt values on the way into the database.
|
||||
|
||||
MultiFernet.encrypt uses the first key in the list.
|
||||
"""
|
||||
|
||||
# we assume that the user's keys are already Fernet keys (32 byte
|
||||
# keys that have been base64 encoded).
|
||||
self.keys = [Fernet(key) for key in get_keys()]
|
||||
|
||||
# we only support strings and they should be of type bytes for Fernet
|
||||
if not isinstance(value, six.string_types):
|
||||
return None
|
||||
|
||||
value = bytes(value)
|
||||
|
||||
return MultiFernet(self.keys).encrypt(value)
|
||||
|
||||
def process_result_value(self, value, dialect):
|
||||
"""
|
||||
Decrypt values on the way out of the database.
|
||||
|
||||
MultiFernet tries each key until one works.
|
||||
"""
|
||||
|
||||
# we assume that the user's keys are already Fernet keys (32 byte
|
||||
# keys that have been base64 encoded).
|
||||
self.keys = [Fernet(key) for key in get_keys()]
|
||||
|
||||
# if the value is not a string we aren't going to try to decrypt
|
||||
# it. this is for the case where the column is null
|
||||
if not isinstance(value, six.string_types):
|
||||
return None
|
||||
|
||||
# TODO this may raise an InvalidToken exception in certain
|
||||
# cases. Should we handle that?
|
||||
# https://cryptography.io/en/latest/fernet/#cryptography.fernet.Fernet.decrypt
|
||||
return MultiFernet(self.keys).decrypt(value)
|
||||
|
Reference in New Issue
Block a user