Skip to content

Commit a1c6bf7

Browse files
absurdfarcedkropachev
authored andcommitted
PYTHON-1350 Store IV along with encrypted text when using column-level encryption (datastax#1160)
1 parent 7e47772 commit a1c6bf7

File tree

3 files changed

+105
-31
lines changed

3 files changed

+105
-31
lines changed

cassandra/column_encryption/_policies.py

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -35,15 +35,27 @@
3535

3636
class AES256ColumnEncryptionPolicy(ColumnEncryptionPolicy):
3737

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
38+
# Fix block cipher mode for now. IV size is a function of block cipher used
39+
# so fixing this avoids (possibly unnecessary) validation logic here.
40+
mode = modes.CBC
41+
42+
# "iv" param here expects a bytearray that's the same size as the block
43+
# size for AES-256 (128 bits or 16 bytes). If none is provided a new one
44+
# will be randomly generated, but in this case the IV should be recorded and
45+
# preserved or else you will not be able to decrypt any data encrypted by this
46+
# policy.
47+
def __init__(self, iv=None):
48+
49+
# CBC uses an IV that's the same size as the block size
50+
#
51+
# Avoid defining IV with a default arg in order to stay away from
52+
# any issues around the caching of default args
4653
self.iv = iv
54+
if self.iv:
55+
if not len(self.iv) == AES256_BLOCK_SIZE_BYTES:
56+
raise ValueError("This policy uses AES-256 with CBC mode and therefore expects a 128-bit initialization vector")
57+
else:
58+
self.iv = os.urandom(AES256_BLOCK_SIZE_BYTES)
4759

4860
# ColData for a given ColDesc is always preserved. We only create a Cipher
4961
# when there's an actual need to for a given ColDesc
@@ -64,11 +76,13 @@ def encrypt(self, coldesc, obj_bytes):
6476

6577
cipher = self._get_cipher(coldesc)
6678
encryptor = cipher.encryptor()
67-
return encryptor.update(padded_bytes) + encryptor.finalize()
79+
return self.iv + encryptor.update(padded_bytes) + encryptor.finalize()
6880

69-
def decrypt(self, coldesc, encrypted_bytes):
81+
def decrypt(self, coldesc, bytes):
7082

71-
cipher = self._get_cipher(coldesc)
83+
iv = bytes[:AES256_BLOCK_SIZE_BYTES]
84+
encrypted_bytes = bytes[AES256_BLOCK_SIZE_BYTES:]
85+
cipher = self._get_cipher(coldesc, iv=iv)
7286
decryptor = cipher.decryptor()
7387
padded_bytes = decryptor.update(encrypted_bytes) + decryptor.finalize()
7488

@@ -108,19 +122,18 @@ def cache_info(self):
108122
def column_type(self, coldesc):
109123
return self.coldata[coldesc].type
110124

111-
def _get_cipher(self, coldesc):
125+
def _get_cipher(self, coldesc, iv=None):
112126
"""
113127
Access relevant state from this instance necessary to create a Cipher and then get one,
114128
hopefully returning a cached instance if we've already done so (and it hasn't been evicted)
115129
"""
116-
117130
try:
118131
coldata = self.coldata[coldesc]
119-
return AES256ColumnEncryptionPolicy._build_cipher(coldata.key, self.mode, self.iv)
132+
return AES256ColumnEncryptionPolicy._build_cipher(coldata.key, iv or self.iv)
120133
except KeyError:
121134
raise ValueError("Could not find column {}".format(coldesc))
122135

123136
# Explicitly use a class method here to avoid caching self
124137
@lru_cache(maxsize=128)
125-
def _build_cipher(key, mode, iv):
126-
return Cipher(algorithms.AES256(key), mode(iv))
138+
def _build_cipher(key, iv):
139+
return Cipher(algorithms.AES256(key), AES256ColumnEncryptionPolicy.mode(iv))

tests/integration/standard/column_encryption/test_policies.py

Lines changed: 56 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
from cassandra.policies import ColDesc
2121

2222
from cassandra.column_encryption.policies import AES256ColumnEncryptionPolicy, \
23-
AES256_KEY_SIZE_BYTES
23+
AES256_KEY_SIZE_BYTES, AES256_BLOCK_SIZE_BYTES
2424

2525
def setup_module():
2626
use_singledc()
@@ -32,25 +32,28 @@ def _recreate_keyspace(self, session):
3232
session.execute("CREATE KEYSPACE foo WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '1'}")
3333
session.execute("CREATE TABLE foo.bar(encrypted blob, unencrypted int, primary key(unencrypted))")
3434

35+
def _create_policy(self, key, iv = None):
36+
cl_policy = AES256ColumnEncryptionPolicy()
37+
col_desc = ColDesc('foo','bar','encrypted')
38+
cl_policy.add_column(col_desc, key, "int")
39+
return (col_desc, cl_policy)
40+
3541
def test_end_to_end_prepared(self):
3642

3743
# We only currently perform testing on a single type/expected value pair since CLE functionality is essentially
3844
# independent of the underlying type. We intercept data after it's been encoded when it's going out and before it's
3945
# encoded when coming back; the actual types of the data involved don't impact us.
40-
expected = 12345
41-
expected_type = "int"
46+
expected = 0
4247

4348
key = os.urandom(AES256_KEY_SIZE_BYTES)
44-
cl_policy = AES256ColumnEncryptionPolicy()
45-
col_desc = ColDesc('foo','bar','encrypted')
46-
cl_policy.add_column(col_desc, key, expected_type)
47-
49+
(_, cl_policy) = self._create_policy(key)
4850
cluster = TestCluster(column_encryption_policy=cl_policy)
4951
session = cluster.connect()
5052
self._recreate_keyspace(session)
5153

5254
prepared = session.prepare("insert into foo.bar (encrypted, unencrypted) values (?,?)")
53-
session.execute(prepared, (expected,expected))
55+
for i in range(100):
56+
session.execute(prepared, (i, i))
5457

5558
# A straight select from the database will now return the decrypted bits. We select both encrypted and unencrypted
5659
# values here to confirm that we don't interfere with regular processing of unencrypted vals.
@@ -66,20 +69,19 @@ def test_end_to_end_prepared(self):
6669

6770
def test_end_to_end_simple(self):
6871

69-
expected = 67890
70-
expected_type = "int"
72+
expected = 1
7173

7274
key = os.urandom(AES256_KEY_SIZE_BYTES)
73-
cl_policy = AES256ColumnEncryptionPolicy()
74-
col_desc = ColDesc('foo','bar','encrypted')
75-
cl_policy.add_column(col_desc, key, expected_type)
76-
75+
(col_desc, cl_policy) = self._create_policy(key)
7776
cluster = TestCluster(column_encryption_policy=cl_policy)
7877
session = cluster.connect()
7978
self._recreate_keyspace(session)
8079

8180
# Use encode_and_encrypt helper function to populate date
82-
session.execute("insert into foo.bar (encrypted, unencrypted) values (%s,%s)",(cl_policy.encode_and_encrypt(col_desc, expected), expected))
81+
for i in range(1,100):
82+
self.assertIsNotNone(i)
83+
encrypted = cl_policy.encode_and_encrypt(col_desc, i)
84+
session.execute("insert into foo.bar (encrypted, unencrypted) values (%s,%s)", (encrypted, i))
8385

8486
# A straight select from the database will now return the decrypted bits. We select both encrypted and unencrypted
8587
# values here to confirm that we don't interfere with regular processing of unencrypted vals.
@@ -92,3 +94,42 @@ def test_end_to_end_simple(self):
9294
(encrypted,unencrypted) = session.execute(prepared, [expected]).one()
9395
self.assertEquals(expected, encrypted)
9496
self.assertEquals(expected, unencrypted)
97+
98+
def test_end_to_end_different_cle_contexts(self):
99+
100+
expected = 2
101+
102+
key = os.urandom(AES256_KEY_SIZE_BYTES)
103+
104+
# Simulate the creation of two AES256 policies at two different times. Python caches
105+
# default param args at function definition time so a single value will be used any time
106+
# the default val is used. Upshot is that within the same test we'll always have the same
107+
# IV if we rely on the default args, so manually introduce some variation here to simulate
108+
# what actually happens if you have two distinct sessions created at two different times.
109+
iv1 = os.urandom(AES256_BLOCK_SIZE_BYTES)
110+
(col_desc1, cl_policy1) = self._create_policy(key, iv=iv1)
111+
cluster1 = TestCluster(column_encryption_policy=cl_policy1)
112+
session1 = cluster1.connect()
113+
self._recreate_keyspace(session1)
114+
115+
# Use encode_and_encrypt helper function to populate date
116+
for i in range(1,100):
117+
self.assertIsNotNone(i)
118+
encrypted = cl_policy1.encode_and_encrypt(col_desc1, i)
119+
session1.execute("insert into foo.bar (encrypted, unencrypted) values (%s,%s)", (encrypted, i))
120+
session1.shutdown()
121+
cluster1.shutdown()
122+
123+
# Explicitly clear the class-level cache here; we're trying to simulate a second connection from a completely new process and
124+
# that would entail not re-using any cached ciphers
125+
AES256ColumnEncryptionPolicy._build_cipher.cache_clear()
126+
cache_info = cl_policy1.cache_info()
127+
self.assertEqual(cache_info.currsize, 0)
128+
129+
iv2 = os.urandom(AES256_BLOCK_SIZE_BYTES)
130+
(_, cl_policy2) = self._create_policy(key, iv=iv2)
131+
cluster2 = TestCluster(column_encryption_policy=cl_policy2)
132+
session2 = cluster2.connect()
133+
(encrypted,unencrypted) = session2.execute("select encrypted, unencrypted from foo.bar where unencrypted = %s allow filtering", (expected,)).one()
134+
self.assertEquals(expected, encrypted)
135+
self.assertEquals(expected, unencrypted)

tests/unit/column_encryption/test_policies.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,23 @@ def test_add_column_invalid_key_size_raises(self):
5555
with self.assertRaises(ValueError):
5656
policy.add_column(coldesc, os.urandom(key_size), "blob")
5757

58+
def test_add_column_invalid_iv_size_raises(self):
59+
def test_iv_size(iv_size):
60+
policy = AES256ColumnEncryptionPolicy(iv = os.urandom(iv_size))
61+
policy.add_column(coldesc, os.urandom(AES256_KEY_SIZE_BYTES), "blob")
62+
policy.encrypt(coldesc, os.urandom(128))
63+
64+
coldesc = ColDesc('ks1','table1','col1')
65+
for iv_size in range(1,AES256_BLOCK_SIZE_BYTES - 1):
66+
with self.assertRaises(ValueError):
67+
test_iv_size(iv_size)
68+
for iv_size in range(AES256_BLOCK_SIZE_BYTES + 1,(2 * AES256_BLOCK_SIZE_BYTES) - 1):
69+
with self.assertRaises(ValueError):
70+
test_iv_size(iv_size)
71+
72+
# Finally, confirm that the expected IV size has no issue
73+
test_iv_size(AES256_BLOCK_SIZE_BYTES)
74+
5875
def test_add_column_null_coldesc_raises(self):
5976
with self.assertRaises(ValueError):
6077
policy = AES256ColumnEncryptionPolicy()
@@ -125,6 +142,9 @@ def test_decrypt_unknown_column(self):
125142
policy.decrypt(ColDesc('ks2','table2','col2'), encrypted_bytes)
126143

127144
def test_cache_info(self):
145+
# Exclude any interference from tests above
146+
AES256ColumnEncryptionPolicy._build_cipher.cache_clear()
147+
128148
coldesc1 = ColDesc('ks1','table1','col1')
129149
coldesc2 = ColDesc('ks2','table2','col2')
130150
coldesc3 = ColDesc('ks3','table3','col3')

0 commit comments

Comments
 (0)