Skip to content

Commit 6c3b8dc

Browse files
absurdfarcedkropachev
authored andcommitted
PYTHON-1351 Convert cryptography to an optional dependency (datastax#1164)
1 parent c07a06b commit 6c3b8dc

File tree

11 files changed

+400
-317
lines changed

11 files changed

+400
-317
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/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: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ 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
(*Optional*) Compression Support
3232
--------------------------------

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: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -416,12 +416,12 @@ def run_setup(extensions):
416416

417417
dependencies = [
418418
'geomet>=0.1,<0.3',
419-
'pyyaml > 5.0',
420-
'cryptography>=35.0'
419+
'pyyaml > 5.0'
421420
]
422421

423422
_EXTRAS_REQUIRE = {
424-
'graph': ['gremlinpython==3.4.6']
423+
'graph': ['gremlinpython==3.4.6'],
424+
'cle': ['cryptography>=35.0']
425425
}
426426

427427
setup(
@@ -440,7 +440,8 @@ def run_setup(extensions):
440440
packages=[
441441
'cassandra', 'cassandra.io', 'cassandra.cqlengine', 'cassandra.graph',
442442
'cassandra.datastax', 'cassandra.datastax.insights', 'cassandra.datastax.graph',
443-
'cassandra.datastax.graph.fluent', 'cassandra.datastax.cloud', 'cassandra.scylla'
443+
'cassandra.datastax.graph.fluent', 'cassandra.datastax.cloud', 'cassandra.scylla',
444+
'cassandra.column_encryption'
444445
],
445446
keywords='cassandra,cql,orm,dse,graph',
446447
include_package_data=True,

0 commit comments

Comments
 (0)