Skip to content

Commit 1c6c739

Browse files
author
laodouya
authored
Merge pull request #2 from CovenantSQL/feature/e2ee
Add end to end encryption
2 parents 07ba9a5 + fea5822 commit 1c6c739

File tree

6 files changed

+177
-18
lines changed

6 files changed

+177
-18
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
.vscode
2+
.idea
3+
venv
24
*.swp

pycovenantsql/__init__.py

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import sys
21
from ._compat import PY2
32
from .converters import escape_dict, escape_sequence, escape_string
43
from .constants import FIELD_TYPE
@@ -38,18 +37,18 @@ def __hash__(self):
3837

3938
# TODO it's in pep249 find out meaning and usage of this
4039
# https://www.python.org/dev/peps/pep-0249/#string
41-
STRING = DBAPISet([FIELD_TYPE.ENUM, FIELD_TYPE.STRING,
42-
FIELD_TYPE.VAR_STRING])
43-
BINARY = DBAPISet([FIELD_TYPE.BLOB, FIELD_TYPE.LONG_BLOB,
44-
FIELD_TYPE.MEDIUM_BLOB, FIELD_TYPE.TINY_BLOB])
45-
NUMBER = DBAPISet([FIELD_TYPE.DECIMAL, FIELD_TYPE.DOUBLE, FIELD_TYPE.FLOAT,
46-
FIELD_TYPE.INT24, FIELD_TYPE.LONG, FIELD_TYPE.LONGLONG,
47-
FIELD_TYPE.TINY, FIELD_TYPE.YEAR])
48-
DATE = DBAPISet([FIELD_TYPE.DATE, FIELD_TYPE.NEWDATE])
49-
TIME = DBAPISet([FIELD_TYPE.TIME])
40+
STRING = DBAPISet([FIELD_TYPE.ENUM, FIELD_TYPE.STRING,
41+
FIELD_TYPE.VAR_STRING])
42+
BINARY = DBAPISet([FIELD_TYPE.BLOB, FIELD_TYPE.LONG_BLOB,
43+
FIELD_TYPE.MEDIUM_BLOB, FIELD_TYPE.TINY_BLOB])
44+
NUMBER = DBAPISet([FIELD_TYPE.DECIMAL, FIELD_TYPE.DOUBLE, FIELD_TYPE.FLOAT,
45+
FIELD_TYPE.INT24, FIELD_TYPE.LONG, FIELD_TYPE.LONGLONG,
46+
FIELD_TYPE.TINY, FIELD_TYPE.YEAR])
47+
DATE = DBAPISet([FIELD_TYPE.DATE, FIELD_TYPE.NEWDATE])
48+
TIME = DBAPISet([FIELD_TYPE.TIME])
5049
TIMESTAMP = DBAPISet([FIELD_TYPE.TIMESTAMP, FIELD_TYPE.DATETIME])
51-
DATETIME = TIMESTAMP
52-
ROWID = DBAPISet()
50+
DATETIME = TIMESTAMP
51+
ROWID = DBAPISet()
5352

5453

5554
def Binary(x):
@@ -59,6 +58,7 @@ def Binary(x):
5958
else:
6059
return bytes(x)
6160

61+
6262
def Connect(*args, **kwargs):
6363
"""
6464
Connect to the database; see connections.Connection.__init__() for
@@ -69,6 +69,7 @@ def Connect(*args, **kwargs):
6969

7070

7171
from . import connections as _orig_conn
72+
7273
if _orig_conn.Connection.__init__.__doc__ is not None:
7374
Connect.__doc__ = _orig_conn.Connection.__init__.__doc__
7475
del _orig_conn
@@ -80,6 +81,7 @@ def get_client_info(): # for MySQLdb compatibility
8081
version = VERSION[:3]
8182
return '.'.join(map(str, version))
8283

84+
8385
connect = Connection = Connect
8486

8587
NULL = "NULL"

pycovenantsql/e2ee.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
from Crypto.Cipher import AES
2+
from Crypto import Random
3+
import hashlib
4+
from binascii import hexlify, unhexlify
5+
6+
BLOCK_SIZE = AES.block_size # Bytes
7+
8+
salt = unhexlify("3fb8877d37fdc04e4a4765EFb8ab7d36")
9+
10+
11+
class PaddingError(Exception):
12+
"""Exception raised for errors in the padding.
13+
14+
Attributes:
15+
message -- explanation of the error
16+
"""
17+
18+
def __init__(self, message):
19+
self.message = message
20+
21+
22+
pad = lambda s: s + ((BLOCK_SIZE - len(s) % BLOCK_SIZE) *
23+
chr(BLOCK_SIZE - len(s) % BLOCK_SIZE)).encode('ascii')
24+
25+
26+
def unpad(s):
27+
in_len = len(s)
28+
if in_len == 0:
29+
raise PaddingError("empty input")
30+
pad_char = s[-1]
31+
if pad_char > BLOCK_SIZE:
32+
raise PaddingError("padding length > 16")
33+
for i in s[in_len - pad_char:]:
34+
if i != pad_char:
35+
raise PaddingError("unexpected padding char")
36+
return s[:-pad_char]
37+
38+
39+
# kdf does 2 times sha256 and takes the first 16 bytes
40+
def kdf(raw_key):
41+
"""
42+
kdf does 2 times sha256 and takes the first 16 bytes
43+
:param raw_key:
44+
:return:
45+
"""
46+
return hashlib.sha256(hashlib.sha256(raw_key + salt).digest()).digest()[:16]
47+
48+
49+
def encrypt(raw, password):
50+
"""
51+
encrypt encrypts data with given password by AES-128-CBC PKCS#7, iv will be placed
52+
at head of cipher data.
53+
54+
:param raw: input raw byte array
55+
:param password: password byte array
56+
:return: encrypted byte array
57+
"""
58+
iv = Random.new().read(AES.block_size)
59+
cipher = AES.new(kdf(password), AES.MODE_CBC, iv)
60+
return iv + cipher.encrypt(pad(raw))
61+
62+
63+
def decrypt(enc, password):
64+
"""
65+
decrypt decrypts data with given password by AES-128-CBC PKCS#7. iv will be read from
66+
the head of raw.
67+
68+
:param enc: input encrypted byte array
69+
:param password: password byte array
70+
:return: decrypted byte array
71+
"""
72+
iv = enc[:16]
73+
cipher = AES.new(kdf(password), AES.MODE_CBC, iv)
74+
return unpad(cipher.decrypt(enc[16:]))

pycovenantsql/tests/test_connection.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import datetime
22
import sys
33
import time
4-
import unittest2
54
import pycovenantsql
65
from pycovenantsql.tests import base
76
from pycovenantsql._compat import text_type
@@ -41,6 +40,7 @@ def __exit__(self, exc_type, exc_value, traceback):
4140
if self._created:
4241
self._c.execute("DROP USER %s" % self._user)
4342

43+
4444
class TestConnection(base.PyCovenantSQLTestCase):
4545

4646
def test_largedata(self):
@@ -70,6 +70,7 @@ def test_context(self):
7070
self.assertEqual(1,cur.fetchone()[0])
7171
cur.execute('drop table test')
7272

73+
7374
# A custom type and function to escape it
7475
class Foo(object):
7576
value = "bar"

pycovenantsql/tests/test_e2ee.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# coding: utf-8
2+
3+
import unittest
4+
from pycovenantsql.e2ee import encrypt, decrypt, unpad, PaddingError
5+
from binascii import hexlify, unhexlify
6+
7+
# Test cases for all implementations.
8+
# Because iv is random, so Encrypted data is not always the same,
9+
# but Decrypt(possibleEncrypted) will get raw.
10+
cases = [
11+
{
12+
"raw": "11",
13+
"pass": ";#K]As9C*6L",
14+
"possibleEncrypted": "a372ea2c158a2f99d386e309db4355a659a7a8dd3986fd1d94f7604256061609",
15+
},
16+
{
17+
"raw": "111282C128421286712857128C2128EF" +
18+
"128B7671283C128571287512830128EC" +
19+
"128391281A1312849128381281E1286A" +
20+
"12871128621287A9D12857128C412886" +
21+
"128FD12834128DA128F5",
22+
"pass": "",
23+
"possibleEncrypted": "1bfb6a7fda3e3eb1e14c9afd0baefe86" +
24+
"c90979101f179db7e48a0fa7617881e8" +
25+
"f752c59fb512bb86b8ed69c5644bf2dc" +
26+
"30fbcd3bf79fb20342595c84fad00e46" +
27+
"2fab3e51266492a3d5d085e650c1e619" +
28+
"6278d7f5185c263440ec6fd940ffbb85",
29+
},
30+
{
31+
"raw": "11",
32+
"pass": "'K]\"#'pi/1/JD2",
33+
"possibleEncrypted": "a83d152777ce3a1c0710b03676ae867c86ab0a47b3ca080f825683ac1079eb41",
34+
},
35+
{
36+
"raw": "11111111111111111111111111111111",
37+
"pass": "",
38+
"possibleEncrypted": "7dda438c4256a63c62d6816617fcbf9c" +
39+
"7773b9b4f87902b7253848ba2b0ed0ba" +
40+
"f70a3ac976a835b7bc3008e9ba43da74",
41+
},
42+
{
43+
"raw": "11111111111111111111111111111111",
44+
"pass": "youofdas1312",
45+
"possibleEncrypted": "cab07967cf377dbc010fbf5f84d12bcb" +
46+
"6f8b188e6965738cf9007a671b4bfeb9" +
47+
"f52257aac3808048c341dcaa1c125ca7",
48+
},
49+
{
50+
"raw": "11111111111111111111111111",
51+
"pass": "空のBottle😄",
52+
"possibleEncrypted": "4384874473945c5b70519ad5ace6305ef6b78c60c3c694add08a8b81899c4171",
53+
},
54+
]
55+
56+
57+
class TestE2ee(unittest.TestCase):
58+
def test_enc_dec(self):
59+
i = 0
60+
for case in cases:
61+
print("Case: #" + str(i))
62+
i += 1
63+
enc = encrypt(unhexlify(case["raw"]), case["pass"].encode())
64+
dec = decrypt(enc, case["pass"].encode())
65+
self.assertEqual(unhexlify(case["raw"]), dec)
66+
dec2 = decrypt(unhexlify(case["possibleEncrypted"]), case["pass"].encode())
67+
self.assertEqual(unhexlify(case["raw"]), dec2)
68+
69+
def test_unpad_error(self):
70+
self.assertEqual(
71+
unpad(unhexlify("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa01")),
72+
unhexlify("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
73+
)
74+
self.assertEqual(
75+
unpad(unhexlify("aaaaaaaaaaaaaaaaaaaaaaaaaaaa0202")),
76+
unhexlify("aaaaaaaaaaaaaaaaaaaaaaaaaaaa")
77+
)
78+
self.assertRaisesRegex(PaddingError, "unexpected padding char", unpad, unhexlify("aaaaaaaaaaaaaaaaaaaaaaaaaaaa0102"))
79+
self.assertRaisesRegex(PaddingError, "padding length > 16", unpad, unhexlify("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"))
80+
self.assertRaisesRegex(PaddingError, "empty input", unpad, unhexlify(""))

setup.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
install_requires=[
1919
"requests",
2020
"arrow",
21+
"pycrypto",
2122
],
2223
classifiers=[
2324
'Development Status :: 2 - Pre-Alpha',
@@ -32,10 +33,9 @@
3233
'Intended Audience :: Developers',
3334
'Topic :: Database',
3435
],
35-
keywords=("CovenantSQL","driver","database"),
36+
keywords=("CovenantSQL", "driver", "database"),
3637

37-
author = "laodouya",
38-
author_email = "jin.xu@CovenantSQL.io",
39-
license = "Apache 2.0 Licence",
38+
author="laodouya",
39+
author_email="jin.xu@CovenantSQL.io",
40+
license="Apache 2.0 Licence",
4041
)
41-

0 commit comments

Comments
 (0)