Add crypto submodule that handles AES-256-CTR encryption.
authordrebs <drebs@leap.se>
Thu, 16 May 2013 15:10:23 +0000 (12:10 -0300)
committerdrebs <drebs@leap.se>
Thu, 16 May 2013 21:51:40 +0000 (18:51 -0300)
changes/feature_use-pycrypto-for-symmetric-encryption [new file with mode: 0644]
pkg/requirements.pip
setup.py
src/leap/common/crypto.py [new file with mode: 0644]
src/leap/common/tests/test_crypto.py [new file with mode: 0644]

diff --git a/changes/feature_use-pycrypto-for-symmetric-encryption b/changes/feature_use-pycrypto-for-symmetric-encryption
new file mode 100644 (file)
index 0000000..5448483
--- /dev/null
@@ -0,0 +1 @@
+  o Add AES-256 (CTR mode) encrypting/decrypting functions using PyCrypto.
index 01fe3ea..141c325 100644 (file)
@@ -5,5 +5,6 @@ pyopenssl
 python-dateutil
 autopep8
 python-gnupg
+PyCrypto
 
-https://protobuf-socket-rpc.googlecode.com/files/protobuf.socketrpc-1.3.2.tar.gz
\ No newline at end of file
+https://protobuf-socket-rpc.googlecode.com/files/protobuf.socketrpc-1.3.2.tar.gz
index 112736e..99fd25b 100644 (file)
--- a/setup.py
+++ b/setup.py
@@ -28,6 +28,8 @@ requirements = [
     "PyOpenSSL",
     "python-dateutil",
     "leap.soledad",
+    "python-gnupg",
+    "PyCrypto",
 ]
 
 
diff --git a/src/leap/common/crypto.py b/src/leap/common/crypto.py
new file mode 100644 (file)
index 0000000..f49933b
--- /dev/null
@@ -0,0 +1,114 @@
+# -*- coding: utf-8 -*-
+# crypto.py
+# Copyright (C) 2013 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
+from Crypto.Cipher import AES
+from Crypto.Random import random
+from Crypto.Util import Counter
+from leap.common.check import leap_assert, leap_assert_type
+
+
+#
+# encryption methods
+#
+
+class EncryptionMethods(object):
+    """
+    Representation of encryption methods that can be used.
+    """
+
+    AES_256_CTR = 'aes-256-ctr'
+
+
+class UnknownEncryptionMethod(Exception):
+    """
+    Raised when trying to encrypt/decrypt with unknown method.
+    """
+    pass
+
+
+#
+# encrypt/decrypt functions
+#
+
+# In the future, we might want to implement other encryption schemes and
+# possibly factor out the actual encryption/decryption routines of the
+# following functions to specific classes, while not changing the API.
+
+def encrypt_sym(data, key, method=EncryptionMethods.AES_256_CTR):
+    """
+    Encrypt C{data} with C{key}, using C{method} encryption method.
+
+    @param data: The data to be encrypted.
+    @type data: str
+    @param key: The key used to encrypt C{data} (must be 256 bits long).
+    @type key: str
+    @param method: The encryption method to use.
+    @type method: str
+
+    @return: A tuple with the initial value and the encrypted data.
+    @rtype: (long, str)
+    """
+    leap_assert_type(key, str)
+
+    # AES-256 in CTR mode
+    if method == EncryptionMethods.AES_256_CTR:
+        leap_assert(
+            len(key) == 32,  # 32 x 8 = 256 bits.
+            'Wrong key size: %s bits (must be 256 bits long).' % (len(key)*8))
+        iv = random.getrandbits(256)
+        ctr = Counter.new(128, initial_value=iv)
+        cipher = AES.new(key=key, mode=AES.MODE_CTR, counter=ctr)
+        return iv, cipher.encrypt(data)
+
+    # raise if method is unknown
+    raise UnknownEncryptionMethod('Unkwnown method: %s' % method)
+
+
+def decrypt_sym(data, key, method=EncryptionMethods.AES_256_CTR, **kwargs):
+    """
+    Decrypt C{data} with C{key} using C{method} encryption method.
+
+    @param data: The data to be decrypted with prepended IV.
+    @type data: str
+    @param key: The key used to decrypt C{data} (must be 256 bits long).
+    @type key: str
+    @param method: The encryption method to use.
+    @type method: str
+    @param kwargs: Other parameters specific to each encryption method.
+    @type kwargs: long
+
+    @return: The decrypted data.
+    @rtype: str
+    """
+    leap_assert_type(key, str)
+
+    # AES-256 in CTR mode
+    if method == EncryptionMethods.AES_256_CTR:
+        # assert params
+        leap_assert(
+            len(key) == 32,  # 32 x 8 = 256 bits.
+            'Wrong key size: %s (must be 256 bits long).' % len(key))
+        leap_assert(
+            'iv' in kwargs,
+            'AES-256-CTR needs an initial value given as.')
+        ctr = Counter.new(128, initial_value=kwargs['iv'])
+        cipher = AES.new(key, AES.MODE_CTR, counter=ctr)
+        return cipher.decrypt(data)
+
+    # raise if method is unknown
+    raise UnknownEncryptionMethod('Unkwnown method: %s' % method)
diff --git a/src/leap/common/tests/test_crypto.py b/src/leap/common/tests/test_crypto.py
new file mode 100644 (file)
index 0000000..b704c05
--- /dev/null
@@ -0,0 +1,80 @@
+## -*- coding: utf-8 -*-
+# test_crypto.py
+# Copyright (C) 2013 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
+"""
+Tests for the crypto submodule.
+"""
+
+
+from leap.common.testing.basetest import BaseLeapTest
+from leap.common import crypto
+from Crypto import Random
+
+
+class CryptoTestCase(BaseLeapTest):
+
+    def setUp(self):
+        pass
+
+    def tearDown(self):
+        pass
+
+    def test_encrypt_decrypt_sym(self):
+        # generate 256-bit key
+        key = Random.new().read(32)
+        iv, cyphertext = crypto.encrypt_sym(
+            'data', key,
+            method=crypto.EncryptionMethods.AES_256_CTR)
+        self.assertTrue(cyphertext is not None)
+        self.assertTrue(cyphertext != '')
+        self.assertTrue(cyphertext != 'data')
+        plaintext = crypto.decrypt_sym(
+            cyphertext, key, iv=iv,
+            method=crypto.EncryptionMethods.AES_256_CTR)
+        self.assertEqual('data', plaintext)
+
+    def test_decrypt_with_wrong_iv_fails(self):
+        key = Random.new().read(32)
+        iv, cyphertext = crypto.encrypt_sym(
+            'data', key,
+            method=crypto.EncryptionMethods.AES_256_CTR)
+        self.assertTrue(cyphertext is not None)
+        self.assertTrue(cyphertext != '')
+        self.assertTrue(cyphertext != 'data')
+        iv += 1
+        plaintext = crypto.decrypt_sym(
+            cyphertext, key, iv=iv,
+            method=crypto.EncryptionMethods.AES_256_CTR)
+        self.assertNotEqual('data', plaintext)
+
+    def test_decrypt_with_wrong_key_fails(self):
+        key = Random.new().read(32)
+        iv, cyphertext = crypto.encrypt_sym(
+            'data', key,
+            method=crypto.EncryptionMethods.AES_256_CTR)
+        self.assertTrue(cyphertext is not None)
+        self.assertTrue(cyphertext != '')
+        self.assertTrue(cyphertext != 'data')
+        wrongkey = Random.new().read(32)  # 256-bits key
+        # ensure keys are different in case we are extremely lucky
+        while wrongkey == key:
+            wrongkey = Random.new().read(32)
+        plaintext = crypto.decrypt_sym(
+            cyphertext, wrongkey, iv=iv,
+            method=crypto.EncryptionMethods.AES_256_CTR)
+        self.assertNotEqual('data', plaintext)