]> git.datanom.net - securemail.git/commitdiff
Reorder
authorMichael Rasmussen <mir@datanom.net>
Mon, 13 Aug 2018 20:51:55 +0000 (22:51 +0200)
committerMichael Rasmussen <mir@datanom.net>
Mon, 13 Aug 2018 20:51:55 +0000 (22:51 +0200)
Signed-off-by: Michael Rasmussen <mir@datanom.net>
app/backend/__init__.py [new file with mode: 0644]
app/backend/config.py [new file with mode: 0644]
app/backend/cryptonize.py [new file with mode: 0644]
app/backend/db.py [new file with mode: 0644]
app/backend/user.py [new file with mode: 0644]

diff --git a/app/backend/__init__.py b/app/backend/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/app/backend/config.py b/app/backend/config.py
new file mode 100644 (file)
index 0000000..bd71642
--- /dev/null
@@ -0,0 +1,37 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2018  Michael Rasmussen <mir@datanom.net>
+# 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 <https://www.gnu.org/licenses/>.
+
+######   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 (file)
index 0000000..7a598d3
--- /dev/null
@@ -0,0 +1,111 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2018  Michael Rasmussen <mir@datanom.net>
+# 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 <https://www.gnu.org/licenses/>.
+
+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 (file)
index 0000000..93eea09
--- /dev/null
@@ -0,0 +1,202 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2018  Michael Rasmussen <mir@datanom.net>
+# 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 <https://www.gnu.org/licenses/>.
+
+# 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 (file)
index 0000000..a4ab2f7
--- /dev/null
@@ -0,0 +1,142 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2018  Michael Rasmussen <mir@datanom.net>
+# 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 <https://www.gnu.org/licenses/>.
+
+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)
This page took 0.049835 seconds and 5 git commands to generate.