Skip to content

Commit 900453f

Browse files
absurdfarcedkropachev
authored andcommitted
PYTHON-1351 Convert cryptography to an optional dependency (datastax#1164)
1 parent 59df663 commit 900453f

File tree

13 files changed

+423
-318
lines changed

13 files changed

+423
-318
lines changed
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
# Copyright DataStax, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from collections import namedtuple
16+
from functools import lru_cache
17+
18+
import logging
19+
import os
20+
21+
log = logging.getLogger(__name__)
22+
23+
from cassandra.cqltypes import _cqltypes
24+
from cassandra.policies import ColumnEncryptionPolicy
25+
26+
from cryptography.hazmat.primitives import padding
27+
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
28+
29+
AES256_BLOCK_SIZE = 128
30+
AES256_BLOCK_SIZE_BYTES = int(AES256_BLOCK_SIZE / 8)
31+
AES256_KEY_SIZE = 256
32+
AES256_KEY_SIZE_BYTES = int(AES256_KEY_SIZE / 8)
33+
34+
ColData = namedtuple('ColData', ['key','type'])
35+
36+
class AES256ColumnEncryptionPolicy(ColumnEncryptionPolicy):
37+
38+
# CBC uses an IV that's the same size as the block size
39+
#
40+
# TODO: Need to find some way to expose mode options
41+
# (CBC etc.) without leaking classes from the underlying
42+
# impl here
43+
def __init__(self, mode = modes.CBC, iv = os.urandom(AES256_BLOCK_SIZE_BYTES)):
44+
45+
self.mode = mode
46+
self.iv = iv
47+
48+
# ColData for a given ColDesc is always preserved. We only create a Cipher
49+
# when there's an actual need to for a given ColDesc
50+
self.coldata = {}
51+
self.ciphers = {}
52+
53+
def encrypt(self, coldesc, obj_bytes):
54+
55+
# AES256 has a 128-bit block size so if the input bytes don't align perfectly on
56+
# those blocks we have to pad them. There's plenty of room for optimization here:
57+
#
58+
# * Instances of the PKCS7 padder should be managed in a bounded pool
59+
# * It would be nice if we could get a flag from encrypted data to indicate
60+
# whether it was padded or not
61+
# * Might be able to make this happen with a leading block of flags in encrypted data
62+
padder = padding.PKCS7(AES256_BLOCK_SIZE).padder()
63+
padded_bytes = padder.update(obj_bytes) + padder.finalize()
64+
65+
cipher = self._get_cipher(coldesc)
66+
encryptor = cipher.encryptor()
67+
return encryptor.update(padded_bytes) + encryptor.finalize()
68+
69+
def decrypt(self, coldesc, encrypted_bytes):
70+
71+
cipher = self._get_cipher(coldesc)
72+
decryptor = cipher.decryptor()
73+
padded_bytes = decryptor.update(encrypted_bytes) + decryptor.finalize()
74+
75+
unpadder = padding.PKCS7(AES256_BLOCK_SIZE).unpadder()
76+
return unpadder.update(padded_bytes) + unpadder.finalize()
77+
78+
def add_column(self, coldesc, key, type):
79+
80+
if not coldesc:
81+
raise ValueError("ColDesc supplied to add_column cannot be None")
82+
if not key:
83+
raise ValueError("Key supplied to add_column cannot be None")
84+
if not type:
85+
raise ValueError("Type supplied to add_column cannot be None")
86+
if type not in _cqltypes.keys():
87+
raise ValueError("Type %s is not a supported type".format(type))
88+
if not len(key) == AES256_KEY_SIZE_BYTES:
89+
raise ValueError("AES256 column encryption policy expects a 256-bit encryption key")
90+
self.coldata[coldesc] = ColData(key, _cqltypes[type])
91+
92+
def contains_column(self, coldesc):
93+
return coldesc in self.coldata
94+
95+
def encode_and_encrypt(self, coldesc, obj):
96+
if not coldesc:
97+
raise ValueError("ColDesc supplied to encode_and_encrypt cannot be None")
98+
if not obj:
99+
raise ValueError("Object supplied to encode_and_encrypt cannot be None")
100+
coldata = self.coldata.get(coldesc)
101+
if not coldata:
102+
raise ValueError("Could not find ColData for ColDesc %s".format(coldesc))
103+
return self.encrypt(coldesc, coldata.type.serialize(obj, None))
104+
105+
def cache_info(self):
106+
return AES256ColumnEncryptionPolicy._build_cipher.cache_info()
107+
108+
def column_type(self, coldesc):
109+
return self.coldata[coldesc].type
110+
111+
def _get_cipher(self, coldesc):
112+
"""
113+
Access relevant state from this instance necessary to create a Cipher and then get one,
114+
hopefully returning a cached instance if we've already done so (and it hasn't been evicted)
115+
"""
116+
117+
try:
118+
coldata = self.coldata[coldesc]
119+
return AES256ColumnEncryptionPolicy._build_cipher(coldata.key, self.mode, self.iv)
120+
except KeyError:
121+
raise ValueError("Could not find column {}".format(coldesc))
122+
123+
# Explicitly use a class method here to avoid caching self
124+
@lru_cache(maxsize=128)
125+
def _build_cipher(key, mode, iv):
126+
return Cipher(algorithms.AES256(key), mode(iv))
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Copyright DataStax, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
try:
16+
import cryptography
17+
from cassandra.column_encryption._policies import *
18+
except ImportError:
19+
# Cryptography is not installed
20+
pass

cassandra/policies.py

Lines changed: 1 addition & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -17,30 +17,23 @@
1717
from functools import lru_cache
1818
from itertools import islice, cycle, groupby, repeat
1919
import logging
20-
import os
2120
from random import randint, shuffle
2221
from threading import Lock
2322
import socket
2423
import warnings
2524

26-
from cryptography.hazmat.primitives import padding
27-
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
25+
log = logging.getLogger(__name__)
2826

2927
from cassandra import WriteType as WT
30-
from cassandra.cqltypes import _cqltypes
3128

3229

3330
# This is done this way because WriteType was originally
3431
# defined here and in order not to break the API.
3532
# It may removed in the next mayor.
3633
WriteType = WT
3734

38-
3935
from cassandra import ConsistencyLevel, OperationTimedOut
4036

41-
log = logging.getLogger(__name__)
42-
43-
4437
class HostDistance(object):
4538
"""
4639
A measure of how "distant" a node is from the client, which
@@ -1397,7 +1390,6 @@ def _rethrow(self, *args, **kwargs):
13971390

13981391

13991392
ColDesc = namedtuple('ColDesc', ['ks', 'table', 'col'])
1400-
ColData = namedtuple('ColData', ['key','type'])
14011393

14021394
class ColumnEncryptionPolicy(object):
14031395
"""
@@ -1454,100 +1446,3 @@ def encode_and_encrypt(self, coldesc, obj):
14541446
statements.
14551447
"""
14561448
raise NotImplementedError()
1457-
1458-
AES256_BLOCK_SIZE = 128
1459-
AES256_BLOCK_SIZE_BYTES = int(AES256_BLOCK_SIZE / 8)
1460-
AES256_KEY_SIZE = 256
1461-
AES256_KEY_SIZE_BYTES = int(AES256_KEY_SIZE / 8)
1462-
1463-
class AES256ColumnEncryptionPolicy(ColumnEncryptionPolicy):
1464-
1465-
# CBC uses an IV that's the same size as the block size
1466-
#
1467-
# TODO: Need to find some way to expose mode options
1468-
# (CBC etc.) without leaking classes from the underlying
1469-
# impl here
1470-
def __init__(self, mode = modes.CBC, iv = os.urandom(AES256_BLOCK_SIZE_BYTES)):
1471-
1472-
self.mode = mode
1473-
self.iv = iv
1474-
1475-
# ColData for a given ColDesc is always preserved. We only create a Cipher
1476-
# when there's an actual need to for a given ColDesc
1477-
self.coldata = {}
1478-
self.ciphers = {}
1479-
1480-
def encrypt(self, coldesc, obj_bytes):
1481-
1482-
# AES256 has a 128-bit block size so if the input bytes don't align perfectly on
1483-
# those blocks we have to pad them. There's plenty of room for optimization here:
1484-
#
1485-
# * Instances of the PKCS7 padder should be managed in a bounded pool
1486-
# * It would be nice if we could get a flag from encrypted data to indicate
1487-
# whether it was padded or not
1488-
# * Might be able to make this happen with a leading block of flags in encrypted data
1489-
padder = padding.PKCS7(AES256_BLOCK_SIZE).padder()
1490-
padded_bytes = padder.update(obj_bytes) + padder.finalize()
1491-
1492-
cipher = self._get_cipher(coldesc)
1493-
encryptor = cipher.encryptor()
1494-
return encryptor.update(padded_bytes) + encryptor.finalize()
1495-
1496-
def decrypt(self, coldesc, encrypted_bytes):
1497-
1498-
cipher = self._get_cipher(coldesc)
1499-
decryptor = cipher.decryptor()
1500-
padded_bytes = decryptor.update(encrypted_bytes) + decryptor.finalize()
1501-
1502-
unpadder = padding.PKCS7(AES256_BLOCK_SIZE).unpadder()
1503-
return unpadder.update(padded_bytes) + unpadder.finalize()
1504-
1505-
def add_column(self, coldesc, key, type):
1506-
1507-
if not coldesc:
1508-
raise ValueError("ColDesc supplied to add_column cannot be None")
1509-
if not key:
1510-
raise ValueError("Key supplied to add_column cannot be None")
1511-
if not type:
1512-
raise ValueError("Type supplied to add_column cannot be None")
1513-
if type not in _cqltypes.keys():
1514-
raise ValueError("Type %s is not a supported type".format(type))
1515-
if not len(key) == AES256_KEY_SIZE_BYTES:
1516-
raise ValueError("AES256 column encryption policy expects a 256-bit encryption key")
1517-
self.coldata[coldesc] = ColData(key, _cqltypes[type])
1518-
1519-
def contains_column(self, coldesc):
1520-
return coldesc in self.coldata
1521-
1522-
def encode_and_encrypt(self, coldesc, obj):
1523-
if not coldesc:
1524-
raise ValueError("ColDesc supplied to encode_and_encrypt cannot be None")
1525-
if not obj:
1526-
raise ValueError("Object supplied to encode_and_encrypt cannot be None")
1527-
coldata = self.coldata.get(coldesc)
1528-
if not coldata:
1529-
raise ValueError("Could not find ColData for ColDesc %s".format(coldesc))
1530-
return self.encrypt(coldesc, coldata.type.serialize(obj, None))
1531-
1532-
def cache_info(self):
1533-
return AES256ColumnEncryptionPolicy._build_cipher.cache_info()
1534-
1535-
def column_type(self, coldesc):
1536-
return self.coldata[coldesc].type
1537-
1538-
def _get_cipher(self, coldesc):
1539-
"""
1540-
Access relevant state from this instance necessary to create a Cipher and then get one,
1541-
hopefully returning a cached instance if we've already done so (and it hasn't been evicted)
1542-
"""
1543-
1544-
try:
1545-
coldata = self.coldata[coldesc]
1546-
return AES256ColumnEncryptionPolicy._build_cipher(coldata.key, self.mode, self.iv)
1547-
except KeyError:
1548-
raise ValueError("Could not find column {}".format(coldesc))
1549-
1550-
# Explicitly use a class method here to avoid caching self
1551-
@lru_cache(maxsize=128)
1552-
def _build_cipher(key, mode, iv):
1553-
return Cipher(algorithms.AES256(key), mode(iv))

docs/.nav

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ upgrading
1111
user_defined_types
1212
dates-and-times
1313
cloud
14-
column_encryption
14+
column-encryption
1515
geo_types
1616
graph
1717
classic_graph

docs/column_encryption.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ when it's created.
2424
2525
import os
2626
27-
from cassandra.policies import ColDesc, AES256ColumnEncryptionPolicy, AES256_KEY_SIZE_BYTES
27+
from cassandra.policies import ColDesc
28+
from cassandra.column_encryption.policies import AES256ColumnEncryptionPolicy, AES256_KEY_SIZE_BYTES
2829
2930
key = os.urandom(AES256_KEY_SIZE_BYTES)
3031
cl_policy = AES256ColumnEncryptionPolicy()

docs/installation.rst

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,15 @@ To check if the installation was successful, you can run::
2626

2727
python -c 'import cassandra; print cassandra.__version__'
2828

29-
It should print something like "3.22.0".
29+
It should print something like "3.27.0".
3030

3131
.. _installation-datastax-graph:
3232

3333
(*Optional*) Graph
3434
---------------------------
3535
The driver provides an optional fluent graph API that depends on Apache TinkerPop (gremlinpython). It is
3636
not installed by default. To be able to build Gremlin traversals, you need to install
37-
the `graph` requirements::
37+
the `graph` extra::
3838

3939
pip install scylla-driver[graph]
4040

@@ -65,6 +65,27 @@ support this::
6565

6666
pip install scales
6767

68+
*Optional:* Column-Level Encryption (CLE) Support
69+
--------------------------------------------------
70+
The driver has built-in support for client-side encryption and
71+
decryption of data. For more, see :doc:`column-encryption`.
72+
73+
CLE depends on the Python `cryptography <https://cryptography.io/en/latest/>`_ module.
74+
When installing Python driver 3.27.0. the `cryptography` module is
75+
also downloaded and installed.
76+
If you are using Python driver 3.28.0 or later and want to use CLE, you must
77+
install the `cryptography <https://cryptography.io/en/latest/>`_ module.
78+
79+
You can install this module along with the driver by specifying the `cle` extra::
80+
81+
pip install cassandra-driver[cle]
82+
83+
Alternatively, you can also install the module directly via `pip`::
84+
85+
pip install cryptography
86+
87+
Any version of cryptography >= 35.0 will work for the CLE feature. You can find additional
88+
details at `PYTHON-1351 <https://datastax-oss.atlassian.net/browse/PYTHON-1351>`_
6889

6990
Speeding Up Installation
7091
^^^^^^^^^^^^^^^^^^^^^^^^

requirements.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1 @@
1-
cryptography >= 35.0
21
geomet>=0.1,<0.3

setup.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -421,11 +421,11 @@ def run_setup(extensions):
421421
'geomet>=0.1,<0.3',
422422
'pyyaml > 5.0',
423423
'six >=1.9',
424-
'cryptography>=35.0'
425424
]
426425

427426
_EXTRAS_REQUIRE = {
428-
'graph': ['gremlinpython==3.4.6']
427+
'graph': ['gremlinpython==3.4.6'],
428+
'cle': ['cryptography>=35.0']
429429
}
430430

431431
setup(
@@ -444,7 +444,8 @@ def run_setup(extensions):
444444
packages=[
445445
'cassandra', 'cassandra.io', 'cassandra.cqlengine', 'cassandra.graph',
446446
'cassandra.datastax', 'cassandra.datastax.insights', 'cassandra.datastax.graph',
447-
'cassandra.datastax.graph.fluent', 'cassandra.datastax.cloud', 'cassandra.scylla'
447+
'cassandra.datastax.graph.fluent', 'cassandra.datastax.cloud', 'cassandra.scylla',
448+
'cassandra.column_encryption'
448449
],
449450
keywords='cassandra,cql,orm,dse,graph',
450451
include_package_data=True,

test-datastax-requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
-r test-requirements.txt
22
kerberos
33
gremlinpython==3.4.6
4+
cryptography >= 35.0

0 commit comments

Comments
 (0)