From 5bc1fc471eb0a72f4420b9189c990026e570d52a Mon Sep 17 00:00:00 2001 From: Michael Rasmussen Date: Mon, 13 Aug 2018 22:51:55 +0200 Subject: [PATCH] Reorder Signed-off-by: Michael Rasmussen --- app/backend/__init__.py | 0 app/backend/config.py | 37 +++++++ app/backend/cryptonize.py | 111 +++++++++++++++++++++ app/backend/db.py | 202 ++++++++++++++++++++++++++++++++++++++ app/backend/user.py | 142 +++++++++++++++++++++++++++ 5 files changed, 492 insertions(+) create mode 100644 app/backend/__init__.py create mode 100644 app/backend/config.py create mode 100644 app/backend/cryptonize.py create mode 100644 app/backend/db.py create mode 100644 app/backend/user.py diff --git a/app/backend/__init__.py b/app/backend/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/backend/config.py b/app/backend/config.py new file mode 100644 index 0000000..bd71642 --- /dev/null +++ b/app/backend/config.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2018 Michael Rasmussen + +# This file is part of SecureMail. + +# SecureMail 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. +# +# SecureMail 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 SecureMail. If not, see . + +###### REQUIREMENTS ####### +# python3-nacl # +# python3-mysqldb # +# python3-psycopg2 # +# python3-apsw # +# mysql, postgresql or sqlite # +############################### + +#DBTYPE = "mysql" +#DBHOST = "localhost" # default value +#DBPORT = 3306 # default value +#DBTYPE = "postgresql" +#DBHOST = "localhost" # default value +#DBPORT = 5432 # default value +DBTYPE = "sqlite" +#DBUID = "backend" # default value +#DBPWD = "clV77B2ZJQxr" # default value +DBNAME = "securemail" # if DBTYPE is sqlite: ./DBNAME + .db diff --git a/app/backend/cryptonize.py b/app/backend/cryptonize.py new file mode 100644 index 0000000..7a598d3 --- /dev/null +++ b/app/backend/cryptonize.py @@ -0,0 +1,111 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2018 Michael Rasmussen + +# This file is part of SecureMail. + +# SecureMail 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. +# +# SecureMail 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 SecureMail. If not, see . + +from nacl import __version__ as NACL_VERSION +from nacl.secret import SecretBox +from nacl.public import PrivateKey, Box +from nacl.utils import random, EncryptedMessage +from nacl.encoding import HexEncoder +import nacl.hash + +class Cryptonize: + """ + Encrypt and decrypt objects + """ + + def symmetric_encrypt(self, key, plain): + skey = self.sanitize_key(key) + box = SecretBox(skey) + if NACL_VERSION < "1.1.0": + nonce = random(SecretBox.NONCE_SIZE) + cipher = box.encrypt(plain, nonce) + else: + cipher = box.encrypt(plain) + box = skey = None + + return cipher + + def symmetric_decrypt(self, key, cipher): + skey = self.sanitize_key(key) + box = SecretBox(skey) + plain = box.decrypt(cipher) + box = skey = None + + return plain + + def asymmetric_encrypt(self, privkey, pubkey, plain): + if not isinstance(plain, bytes): + plain = plain.encode('utf-8') + box = Box(privkey, pubkey) + if NACL_VERSION < "1.1.0": + nonce = random(Box.NONCE_SIZE) + cipher = box.encrypt(plain, nonce) + else: + cipher = box.encrypt(plain) + box = None + + return cipher + + def asymmetric_decrypt(self, privkey, pubkey, cipher): + if not isinstance(cipher, bytes): + cipher = cipher.encode('utf-8') + box = Box(privkey, pubkey) + plain = box.decrypt(cipher) + box = None + + return plain + + def get_random_key(self): + return random(SecretBox.KEY_SIZE) + + def sanitize_key(self, key): + if not isinstance(key, bytes): + key = key.encode('utf-8') + size = len(key) + if size < SecretBox.KEY_SIZE: + """ We must pad """ + newkey = key + bytes(SecretBox.KEY_SIZE - size) + elif size > SecretBox.KEY_SIZE: + newkey = key[:SecretBox.KEY_SIZE] + else: + newkey = key + + + return newkey + + def get_key_pair(self): + privkey = PrivateKey.generate() + pubkey = privkey.public_key + + return (privkey, pubkey) + + def generate_hash(self, key): + if not isinstance(key, bytes): + key = key.encode('utf-8') + HASHER = nacl.hash.sha512 + digest = HASHER(key, encoder=HexEncoder) + + return digest.decode() + + def create_EncryptedMessage(self, payload): + nonce = payload[:SecretBox.NONCE_SIZE] + ciphertext = payload[SecretBox.NONCE_SIZE:] + + return EncryptedMessage._from_parts( + nonce, ciphertext, nonce + ciphertext) diff --git a/app/backend/db.py b/app/backend/db.py new file mode 100644 index 0000000..93eea09 --- /dev/null +++ b/app/backend/db.py @@ -0,0 +1,202 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2018 Michael Rasmussen + +# This file is part of SecureMail. + +# SecureMail 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. +# +# SecureMail 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 SecureMail. If not, see . + +# sqlite +sqlite_sql = """create table account ( +id int auto_increment, +token char(128) unique not null, +cipher text not null, +primary key (id))""" + +# mysql +mysql_sql = """create table account ( +id int auto_increment, +token char(128) unique not null, +cipher text not null, +primary key (id))""" + +# postgresql +postgresql_sql = """create table account ( +id serial, +token char(128) unique not null, +cipher bytea not null, +primary key (id))""" + +import base64 +from config import DBTYPE, DBNAME +try: + from config import DBUID +except ImportError: + DBUID = 'backend' +try: + from config import DBPWD +except ImportError: + DBPWD = 'clV77B2ZJQxr' +try: + from config import DBHOST +except ImportError: + DBHOST = 'localhost' +try: + from config import DBPORT +except ImportError: + if DBTYPE == 'mysql': + DBPORT = 3306 + elif DBTYPE == 'postgresql': + DBPORT = 5432 +from cryptonize import Cryptonize + +class Singleton: + def __init__(self, klass): + self.klass = klass + self.instance = None + + def __call__(self, *args, **kwargs): + if self.instance == None: + self.instance = self.klass(*args, **kwargs) + return self.instance + +@Singleton +class DB: + conn = None + + def get_connection(self): + if self.conn is None: + if DBTYPE == 'mysql': + import MySQLdb + self.conn = MySQLdb.connect(host=DBHOST, port=DBPORT, user=DBUID, passwd=DBPWD, db=DBNAME) + elif DBTYPE == 'postgresql': + import psycopg2 + self.conn = psycopg2.connect(host=DBHOST, port=DBPORT, user=DBUID, password=DBPWD, dbname=DBNAME) + elif DBTYPE == 'sqlite': + import apsw + self.conn = apsw.Connection('./{0}.db'.format(DBNAME)) + else: + raise ValueError('{0}: Unsupported database'.format(DBTYPE)) + return self.conn + + def __del__(self): + if self.conn is not None: + self.conn.close() + +class DBInterface: + @staticmethod + def load_user(key): + conn = DB().get_connection() + cursor = conn.cursor() + cursor.execute("select a.cipher from account a where token = '{0}'".format(key)) + row = cursor.fetchone() + if row is None: + obj = None + else: + c = Cryptonize() + msg = base64.b64decode(row[0]) + obj = c.create_EncryptedMessage(msg) + cursor.close() + + return obj + + @staticmethod + def store_user(key, cipher): + if DBTYPE == 'mysql': + from MySQLdb import Error as DBError + elif DBTYPE == 'postgresql': + from psycopg2 import Error as DBError + elif DBTYPE == 'sqlite': + from apsw import Error as DBError + conn = DB().get_connection() + cursor = conn.cursor() + raw = base64.b64encode(cipher) + try: + if DBTYPE != 'sqlite': + cursor.execute("insert into account(token, cipher) values(%s, %s)", (key, raw)) + conn.commit() + else: + cursor.execute('begin') + cursor.execute("insert into account(token, cipher) values(?, ?)", (key, raw)) + cursor.execute('commit') + except DBError as e: + print (e) + if DBTYPE != 'sqlite': + conn.rollback() + else: + cursor.execute('rollback') + raise e + finally: + cursor.close() + + @staticmethod + def create_database(): + if DBTYPE == 'mysql': + from MySQLdb import Error as DBError + elif DBTYPE == 'postgresql': + from psycopg2 import Error as DBError + elif DBTYPE == 'sqlite': + from apsw import Error as DBError + conn = DB().get_connection() + cursor = conn.cursor() + try: + if DBTYPE != 'sqlite': + if DBTYPE == 'mysql': + sql = mysql_sql + elif DBTYPE == 'postgresql': + sql = postgresql_sql + cursor.execute(sql) + conn.commit() + else: + cursor.execute('begin') + cursor.execute(sqlite_sql) + cursor.execute('commit') + except DBError as e: + if DBTYPE != 'sqlite': + conn.rollback() + else: + cursor.execute('rollback') + raise e + finally: + cursor.close() + +def main(): + from optparse import OptionParser + + usage = "usage: %prog [options] arg" + parser = OptionParser(usage) + parser.add_option("-c", "--create", action="store_true", dest="create", + help="Create tables in database using config.py", default=False) + parser.add_option("-v", "--verbose", action="store_true", dest="verbose", + help="Run in verbose mode", default=False) + (options, args) = parser.parse_args() + + if options.create: + try: + if options.verbose: + print("Creating empty database") + print("Database Engine: {0}".format(DBTYPE)) + if DBTYPE != 'sqlite': + print("Database Host: {0}".format(DBHOST)) + print("Database Port: {0}".format(DBPORT)) + else: + print("Database File: ./{0}.db".format(DBNAME)) + DBInterface.create_database() + print("Database created") + except Exception as e: + print("Creating database failed!") + print(e) + +if __name__ == '__main__': + main() diff --git a/app/backend/user.py b/app/backend/user.py new file mode 100644 index 0000000..a4ab2f7 --- /dev/null +++ b/app/backend/user.py @@ -0,0 +1,142 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2018 Michael Rasmussen + +# This file is part of SecureMail. + +# SecureMail 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. +# +# SecureMail 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 SecureMail. If not, see . + +import pickle +from db import DBInterface as DBI +from cryptonize import Cryptonize +from nacl.public import PublicKey + +class NoSuchUser(Exception): + pass + +class User: + """ + Class implementing the backend users + """ + def __init__(self, key=None): + if key is not None: + self.load(key) + else: + self.pubkeys = {} + + def store(self, key): + crypto = Cryptonize() + cipher = crypto.symmetric_encrypt(key, pickle.dumps(self)) + DBI.store_user(crypto.generate_hash(key), cipher) + + def load(self, key): + crypto = Cryptonize() + cipher = DBI.load_user(crypto.generate_hash(key)) + if cipher is None: + raise NoSuchUser('{0}: User not found'.format(key)) + plain = crypto.symmetric_decrypt(key, cipher) + try: + obj = pickle.loads(plain) + self.__dict__.update(obj.__dict__) + except pickle.UnpicklingError as e: + raise e + + def add_pubkey(self, email, key): + if email not in self.pubkeys: + self.pubkeys[email] = key.encode() + else: + raise KeyError('{0}: Exists'.format(email)) + + def update_pubkey(self, email, key): + self.pubkeys[email] = key.encode() + + def delete_pubkey(self, email): + if email in self.pubkeys: + del self.pubkeys[email] + + def get_pubkey(self, email): + if email in self.pubkeys: + key = self.pubkeys[email] + key = PublicKey(key) + else: + key = None + + return key + + @property + def name(self): + return self._name + + @name.setter + def name(self, name): + self._name = name + + @property + def email(self): + return self._email + + @email.setter + def email(self, email): + self._email = email + + @property + def pubkeys(self): + return self._pubkeys + + @pubkeys.setter + def pubkeys(self, pubkeys): + if type(pubkeys) is not type({}): + raise ValueError('Not dictionary') + self._pubkeys = pubkeys + +if __name__ == '__main__': + try: + u = User('test') + for attr, value in u.__dict__.items(): + print ('{0}: {1}'.format(attr, value)) + print ('{0} - {1} - {2}'.format(u.name, u.email, u.pubkeys)) + key = '' + for i in range(40): + key += '{0}'.format(i) + u = User() + u.name = 'testname1' + u.email = 'testname1@securemail.icu' + u.pubkeys = {'test': 'some test', 'test1': 'some test 1'} + try: + u.store(key) + except: + u = User(key) + for attr, value in u.__dict__.items(): + print ('{0}: {1}'.format(attr, value)) + print ('{0} - {1} - {2}'.format(u.name, u.email, u.pubkeys)) + c = Cryptonize() + keypair1 = c.get_key_pair() + keypair2 = c.get_key_pair() + try: + u.add_pubkey('test', keypair2[1]) + except KeyError: + u.update_pubkey('test', keypair2[1]) + message = "Kill all humans æøåÅØÆ" + print ("Message to encrypt: {0}".format(message)) + encrypted = c.asymmetric_encrypt(keypair1[0], u.get_pubkey('test'), message) + print ("Message encrypted: {0}".format(encrypted)) + plaintext = c.asymmetric_decrypt(keypair2[0], keypair1[1], encrypted) + print("Message decrypted: {0}".format(plaintext.decode())) + except NoSuchUser: + u = User() + u.name = 'testname' + u.email = 'testname@securemail.icu' + u.store('test') + except Exception as e: + print (e) -- 2.39.2