--- /dev/null
+# -*- 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
--- /dev/null
+# -*- 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)
--- /dev/null
+# -*- 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()
--- /dev/null
+# -*- 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)