Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Use MultiFernet for encryption #123

Merged
merged 1 commit into from
Oct 14, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
16 changes: 11 additions & 5 deletions docs/administration/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -87,17 +87,23 @@ Basic Configuration
>>> secret_key = secret_key + ''.join(random.choice(string.digits) for x in range(6))


.. data:: LEMUR_ENCRYPTION_KEY
.. data:: LEMUR_ENCRYPTION_KEYS
:noindex:

The LEMUR_ENCRYPTION_KEY is used to encrypt data at rest within Lemur's database. Without this key Lemur will refuse
to start.
The LEMUR_ENCRYPTION_KEYS is used to encrypt data at rest within Lemur's database. Without a key Lemur will refuse
to start. Multiple keys can be provided to facilitate key rotation. The first key in the list is used for
encryption and all keys are tried for decryption until one works. Each key must be 32 URL safe base-64 encoded bytes.

See `LEMUR_TOKEN_SECRET` for methods of secure secret generation.
Running lemur create_config will securely generate a key for your configuration file.
If you would like to generate your own, we recommend the following method:

>>> import os
>>> import base64
>>> base64.urlsafe_b64encode(os.urandom(32))

::

LEMUR_ENCRYPTION_KEY = 'supersupersecret'
LEMUR_ENCRYPTION_KEYS = ['1YeftooSbxCiX2zo8m1lXtpvQjy27smZcUUaGmffhMY=', 'LAfQt6yrkLqOK5lwpvQcT4jf2zdeTQJV1uYeh9coT5s=']


Certificate Default Options
Expand Down
4 changes: 2 additions & 2 deletions docs/faq.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ Frequently Asked Questions
Common Problems
---------------

In my startup logs I see *'Aborting... Lemur cannot locate db encryption key, is LEMUR_ENCRYPTION_KEY set?'*
You likely have not correctly configured **LEMUR_ENCRYPTION_KEY**. See
In my startup logs I see *'Aborting... Lemur cannot locate db encryption key, is LEMUR_ENCRYPTION_KEYS set?'*
You likely have not correctly configured **LEMUR_ENCRYPTION_KEYS**. See
:doc:`administration/index` for more information.


Expand Down
6 changes: 2 additions & 4 deletions lemur/certificates/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,7 @@
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

Expand Down Expand Up @@ -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))
Expand Down
6 changes: 4 additions & 2 deletions lemur/manage.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@

# 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 = []
Expand Down Expand Up @@ -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)),
)
Expand Down
5 changes: 2 additions & 3 deletions lemur/roles/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand All @@ -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'))
Expand Down
2 changes: 1 addition & 1 deletion lemur/tests/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@

# 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 = []
Expand Down
88 changes: 82 additions & 6 deletions lemur/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)