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

issue-166 selecting encryption engine based on queryable parameter #499

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/data_types.rst
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ EncryptedType

.. autoclass:: EncryptedType

.. autoclass:: StringEncryptedType

JSONType
--------

Expand Down
140 changes: 103 additions & 37 deletions sqlalchemy_utils/types/encrypted/encrypted_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ class AesEngine(EncryptionDecryptionBaseEngine):
for a row based on the value of an encrypted column. Use AesEngine
instead, since that allows you to perform such searches.

If you don't need to search by the value of an encypted column, the
If you don't need to search by the value of an encrypted column, the
AesGcmEngine provides better security.
"""

Expand Down Expand Up @@ -141,7 +141,7 @@ class AesGcmEngine(EncryptionDecryptionBaseEngine):
for a row based on the value of an encrypted column. Use AesEngine
instead, since that allows you to perform such searches.

If you don't need to search by the value of an encypted column, the
If you don't need to search by the value of an encrypted column, the
AesGcmEngine provides better security.
"""

Expand Down Expand Up @@ -199,7 +199,16 @@ def decrypt(self, value):


class FernetEngine(EncryptionDecryptionBaseEngine):
"""Provide Fernet encryption and decryption methods."""
"""
Provide Fernet encryption and decryption methods.

You should NOT use this FernetEngine if you want to be able to search
for a row based on the value of an encrypted column. Use AesEngine
instead, since that allows you to perform such searches.

If you don't need to search by the value of an encrypted column, the
AesGcmEngine or FernetEngine provides better security.
"""

def _initialize_engine(self, parent_class_key):
self.secret_key = base64.urlsafe_b64encode(parent_class_key)
Expand All @@ -225,15 +234,40 @@ def decrypt(self, value):

class StringEncryptedType(TypeDecorator, ScalarCoercible):
"""
EncryptedType provides a way to encrypt and decrypt values,
StringEncryptedType provides a way to encrypt and decrypt values,
to and from databases, that their type is a basic SQLAlchemy type.
For example Unicode, String or even Boolean.
On the way in, the value is encrypted and on the way out the stored value
is decrypted.

EncryptedType needs Cryptography_ library in order to work.
StringEncryptedType needs Cryptography_ library in order to work.

The `queryable` parameter sets whether it will be possible to search for
a row based on the value of an encrypted column. This is convenient, but
the encryption used can leak information (see below warning).

By default, when `queryable` is `True` the `AesEngine` will be used for
encryption, and when `queryable` is `False` the `FernetEngine` will be
used. Other engines can be specified by passing the encryption engine
class via the `engine` parameter, however the engine chosen must be
compatible with the value of `queryable`.

.. warning::
Information can be leaked when using an encryption engine that
supports searching for a row based on the value of an encrypted
column. These engines use a static IV per key - see
https://security.stackexchange.com/a/1097 for why this can be
problematic.

If a row does not need to be searchable in this way, then it is
strongly recommended to set the `queryable` parameter to `False`
so that a more robust encryption method is used.

The `padding` parameter is passed to the `AesEngine` if it is used, so if
another engine is used (`queryable` is set to `False`) then `padding` is
ignored.

When declaring a column which will be of type EncryptedType
When declaring a column which will be of type StringEncryptedType
it is better to be as precise as possible and follow the pattern
below.

Expand All @@ -242,14 +276,14 @@ class StringEncryptedType(TypeDecorator, ScalarCoercible):
::


a_column = sa.Column(EncryptedType(sa.Unicode,
secret_key,
FernetEngine))
a_column = sa.Column(StringEncryptedType(sa.Unicode,
secret_key,
False))

another_column = sa.Column(EncryptedType(sa.Unicode,
secret_key,
AesEngine,
'pkcs5'))
another_column = sa.Column(StringEncryptedType(sa.Unicode,
secret_key,
True,
padding='pkcs5'))


A more complete example is given below.
Expand All @@ -262,8 +296,7 @@ class StringEncryptedType(TypeDecorator, ScalarCoercible):
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

from sqlalchemy_utils import EncryptedType
from sqlalchemy_utils.types.encrypted.encrypted_type import AesEngine
from sqlalchemy_utils import StringEncryptedType

secret_key = 'secretkey1234'
# setup
Expand All @@ -275,22 +308,22 @@ class StringEncryptedType(TypeDecorator, ScalarCoercible):
class User(Base):
__tablename__ = "user"
id = sa.Column(sa.Integer, primary_key=True)
username = sa.Column(EncryptedType(sa.Unicode,
secret_key,
AesEngine,
'pkcs5'))
access_token = sa.Column(EncryptedType(sa.String,
secret_key,
AesEngine,
'pkcs5'))
is_active = sa.Column(EncryptedType(sa.Boolean,
secret_key,
AesEngine,
'zeroes'))
number_of_accounts = sa.Column(EncryptedType(sa.Integer,
username = sa.Column(StringEncryptedType(sa.Unicode,
secret_key,
True,
padding='pkcs5'))
access_token = sa.Column(StringEncryptedType(sa.String,
secret_key,
AesEngine,
'oneandzeroes'))
False,
AesGcmEngine))
is_active = sa.Column(StringEncryptedType(sa.Boolean,
secret_key,
True,
padding='zeroes'))
number_of_accounts = sa.Column(StringEncryptedType(sa.Integer,
secret_key,
True,
padding='oneandzeroes'))


sa.orm.configure_mappers()
Expand Down Expand Up @@ -343,19 +376,19 @@ def get_key():
class User(Base):
__tablename__ = 'user'
id = sa.Column(sa.Integer, primary_key=True)
username = sa.Column(EncryptedType(
username = sa.Column(StringEncryptedType(
sa.Unicode, get_key))

"""

impl = String

def __init__(self, type_in=None, key=None,
engine=None, padding=None, **kwargs):
queryable=False, engine=None, padding=None, **kwargs):
"""Initialization."""
if not cryptography:
raise ImproperlyConfigured(
"'cryptography' is required to use EncryptedType"
"'cryptography' is required to use StringEncryptedType"
)
super(StringEncryptedType, self).__init__(**kwargs)
# set the underlying type
Expand All @@ -365,12 +398,35 @@ def __init__(self, type_in=None, key=None,
type_in = type_in()
self.underlying_type = type_in
self._key = key
if not engine:
engine = AesEngine
self.engine = engine()
if isinstance(self.engine, AesEngine):

# If queryable, only the AesEngine is supported.
if queryable:
if engine not in (None, AesEngine):
raise ValueError(
"When `queryable` is `True` the encryption engine must be "
"`AesEngine` (the default). See the documentation for "
"security warnings."
)
self.engine = AesEngine()
self.engine._set_padding_mechanism(padding)

# The AesEngine cannot be used when queryable is True.
elif engine is AesEngine:
raise ValueError(
"When `queryable` is `False` the encryption engine should not "
"be `AesEngine` (the default), since it is less secure than "
"alternatives. See the documentation for security warnings."
)

# By now, queryable is True, so default engine to FernetEngine.
elif engine is None:
self.engine = FernetEngine()

# By now, queryable is True and we know it's not AesEngine, so we can
# instantiate whichever engine was chosen by the caller.
else:
self.engine = engine()

@property
def key(self):
return self._key
Expand Down Expand Up @@ -449,6 +505,16 @@ def _coerce(self, value):


class EncryptedType(StringEncryptedType):
"""
See the `StringEncryptedType` baseclass for details.

The base class includes important security notes that should
be read before using encryption.

The `EncryptedType` class will change implementation from
'LargeBinary' to 'String' in a future version. Use
`StringEncryptedType` to use the 'String' implementation.
"""
impl = LargeBinary

def __init__(self, *args, **kwargs):
Expand Down
12 changes: 12 additions & 0 deletions tests/types/test_encrypted.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,76 +29,87 @@ class User(Base):
username = sa.Column(EncryptedType(
sa.Unicode,
test_key,
encryption_engine is AesEngine,
encryption_engine,
padding_mechanism)
)

access_token = sa.Column(EncryptedType(
sa.String,
test_key,
encryption_engine is AesEngine,
encryption_engine,
padding_mechanism)
)

is_active = sa.Column(EncryptedType(
sa.Boolean,
test_key,
encryption_engine is AesEngine,
encryption_engine,
padding_mechanism)
)

accounts_num = sa.Column(EncryptedType(
sa.Integer,
test_key,
encryption_engine is AesEngine,
encryption_engine,
padding_mechanism)
)

phone = sa.Column(EncryptedType(
PhoneNumberType,
test_key,
encryption_engine is AesEngine,
encryption_engine,
padding_mechanism)
)

color = sa.Column(EncryptedType(
ColorType,
test_key,
encryption_engine is AesEngine,
encryption_engine,
padding_mechanism)
)

date = sa.Column(EncryptedType(
sa.Date,
test_key,
encryption_engine is AesEngine,
encryption_engine,
padding_mechanism)
)

time = sa.Column(EncryptedType(
sa.Time,
test_key,
encryption_engine is AesEngine,
encryption_engine,
padding_mechanism)
)

datetime = sa.Column(EncryptedType(
sa.DateTime,
test_key,
encryption_engine is AesEngine,
encryption_engine,
padding_mechanism)
)

enum = sa.Column(EncryptedType(
sa.Enum('One', name='user_enum_t'),
test_key,
encryption_engine is AesEngine,
encryption_engine,
padding_mechanism)
)

json = sa.Column(EncryptedType(
JSONType,
test_key,
encryption_engine is AesEngine,
encryption_engine,
padding_mechanism)
)
Expand Down Expand Up @@ -254,6 +265,7 @@ class Team(Base):
name = sa.Column(EncryptedType(
sa.Unicode,
lambda: self._team_key,
encryption_engine is AesEngine,
encryption_engine,
padding_mechanism)
)
Expand Down