refactor cryptutils

Cryptography can be a black art, rather than rolling our own crypto
functions use the ones provided by the Python Cryptography module.

- `encrypt()` and `decrypt()`
    - ECB is insecure and the Python Cryptography module recommends[1]
    against using it.  The Python Cryptography module offers Fernet[2]
    for encrypting values using a secret_key.

    - Djangos `settings.SECRET_KEY` exists for things like encrypting
      passwords, use it instead of Modoboas own `secret_key`.

    - `settings.SECRET_KEY` is truncated to 32 characters as Fernet
      requires a key 32 characters long.

- `random_key()` uses Dangos get_random_string() method, the same
code used to generate settings.SECRET_KEY

- add tests for cryptutil methods

[1] https://cryptography.io/en/latest/hazmat/primitives/symmetric-encryption/?highlight=ecb#insecure-modes
[2] https://cryptography.io/en/latest/fernet/
This commit is contained in:
Andrew Fyfe
2018-01-17 22:49:17 +00:00
parent 958f2e0ad2
commit e8ab4e2912
2 changed files with 71 additions and 42 deletions
+33 -42
View File
@@ -1,58 +1,49 @@
# coding: utf-8
# -*- coding: utf-8 -*-
"""Crypto related utilities."""
from __future__ import unicode_literals
import base64
import random
import string
from django.utils.encoding import force_bytes, force_text
from django.conf import settings
from django.utils.crypto import get_random_string
from django.utils.encoding import smart_bytes, smart_text
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
from modoboa.parameters import tools as param_tools
from cryptography.fernet import Fernet
def random_key(l=16):
"""Generate a random key.
:param integer l: the key's length
:return: a string
"""
punctuation = """!#$%&'()*+,-./:;<=>?@[]^_`{|}~"""
population = string.digits + string.ascii_letters + punctuation
while True:
key = "".join(random.sample(population * l, l))
if len(key) == l:
return key
def random_key(length=16):
"""Generate a new key used to encrypt user passwords in session storage."""
chars = 'abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)'
return get_random_string(length, chars)
def encrypt(clear):
key = force_bytes(
param_tools.get_global_parameter("secret_key", app="core"))
backend = default_backend()
cipher = Cipher(algorithms.AES(key), modes.ECB(), backend=backend)
encryptor = cipher.encryptor()
block_size = algorithms.AES.block_size
clear = force_bytes(clear)
if len(clear) % block_size:
clear += b" " * (block_size - len(clear) % block_size)
ct = encryptor.update(clear) + encryptor.finalize()
return force_text(base64.b64encode(ct))
def _get_fernet():
"""Create a Fernet instance."""
secret_key = base64.urlsafe_b64encode(smart_bytes(settings.SECRET_KEY[:32]))
return Fernet(secret_key)
def decrypt(ct):
backend = default_backend()
key = param_tools.get_global_parameter("secret_key", app="core")
cipher = Cipher(
algorithms.AES(force_bytes(key)), modes.ECB(), backend=backend)
ct = base64.b64decode(force_bytes(ct))
decryptor = cipher.decryptor()
clear = decryptor.update(ct) + decryptor.finalize()
return force_text(clear.rstrip(b" "))
def encrypt(clear_value):
"""Encrypt a value using secret_key."""
return smart_text(_get_fernet().encrypt(smart_bytes(clear_value)))
def decrypt(encrypted_value):
"""Decrypt a value using secret_key."""
return smart_text(_get_fernet().decrypt(smart_bytes(encrypted_value)))
def get_password(request):
return decrypt(request.session["password"])
"""
Retrieve and decrypt the users password from session storage.
This is used by modoboa-webmail to allow the user to send/receive e-mails
without having to ask the user for a password on each connection to the
postfix/dovecot server.
"""
try:
return decrypt(request.session["password"])
except KeyError:
return None
+38
View File
@@ -0,0 +1,38 @@
# -*- coding: utf-8 -*-
"""Tests for crypto utility functions."""
from __future__ import unicode_literals
from django.test import SimpleTestCase
from django.test.utils import override_settings
from modoboa.lib import cryptutils
@override_settings(SECRET_KEY="!8o(-dbbl3e+*bh7nx-^xysdt)1gso*%@4ze4-9_9o+i&amp"
";t--u_")
class CryptUtilsTest(SimpleTestCase):
"""Tests for modoboa.lib.cryptutils"""
def test_valid_secret_key_generated(self):
"""Generate a 32 byte key"""
secret_key = cryptutils.random_key(32)
self.assertEqual(len(secret_key), 32)
def test_encrypt(self):
"""Encrypt a string"""
value = "test"
expected_output = "test"
output = cryptutils.encrypt(value)
output = cryptutils.decrypt(output)
self.assertEqual(output, expected_output)
def test_decrypt(self):
"""Decrypt a string"""
value = "gAAAAABaXSWk4Via-aYP7diek9MmfknUiEY8szrgxIytdXrbfc"\
"YlbOYiNG01zqkn3a06P1xPXe5XD2SP4UrIvCWzhLs-FO19Cw=="
expected_output = "test"
output = cryptutils.decrypt(value)
self.assertEqual(output, expected_output)