From 94eb71488d334f12d069631b5f7cf433930b6ecd Mon Sep 17 00:00:00 2001 From: timotheyca Date: Wed, 5 Aug 2020 03:28:24 +0300 Subject: [PATCH] added v25 --- .gitignore | 5 + .idea/.gitignore | 8 ++ .idea/dataSources.xml | 19 +++ .idea/inspectionProfiles/Project_Default.xml | 19 +++ .../inspectionProfiles/profiles_settings.xml | 6 + .idea/misc.xml | 7 + .idea/modules.xml | 8 ++ .idea/v25.iml | 10 ++ .idea/vcs.xml | 6 + v25/__init__.py | 0 v25/messaging/__init__.py | 0 v25/messaging/message.py | 122 ++++++++++++++++ v25/messaging/subject.py | 39 +++++ v25/storage/__init__.py | 0 v25/storage/dbstorage.py | 135 ++++++++++++++++++ v25/storage/secstorage.py | 35 +++++ v25/storage/storage.py | 25 ++++ v25/web/__init__.py | 0 v25/web/client/__init__.py | 0 v25/web/client/remotestorage.py | 46 ++++++ v25/web/server/__init__.py | 0 v25/web/server/api.py | 66 +++++++++ 22 files changed, 556 insertions(+) create mode 100644 .idea/.gitignore create mode 100644 .idea/dataSources.xml create mode 100644 .idea/inspectionProfiles/Project_Default.xml create mode 100644 .idea/inspectionProfiles/profiles_settings.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/v25.iml create mode 100644 .idea/vcs.xml create mode 100644 v25/__init__.py create mode 100644 v25/messaging/__init__.py create mode 100644 v25/messaging/message.py create mode 100644 v25/messaging/subject.py create mode 100644 v25/storage/__init__.py create mode 100644 v25/storage/dbstorage.py create mode 100644 v25/storage/secstorage.py create mode 100644 v25/storage/storage.py create mode 100644 v25/web/__init__.py create mode 100644 v25/web/client/__init__.py create mode 100644 v25/web/client/remotestorage.py create mode 100644 v25/web/server/__init__.py create mode 100644 v25/web/server/api.py diff --git a/.gitignore b/.gitignore index 476572e..7a24582 100644 --- a/.gitignore +++ b/.gitignore @@ -202,3 +202,8 @@ fabric.properties # Android studio 3.1+ serialized cache file .idea/caches/build_file_checksums.ser + + +# Others +*.db +/test*.py diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..73f69e0 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml new file mode 100644 index 0000000..1960207 --- /dev/null +++ b/.idea/dataSources.xml @@ -0,0 +1,19 @@ + + + + + sqlite.xerial + true + org.sqlite.JDBC + jdbc:sqlite:D:\source\PyCharm\v25\test.db + + + file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.31.1/license.txt + + + file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.31.1/sqlite-jdbc-3.31.1.jar + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..e91148c --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,19 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..5cf8ffc --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..e275cb5 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/v25.iml b/.idea/v25.iml new file mode 100644 index 0000000..74d515a --- /dev/null +++ b/.idea/v25.iml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/v25/__init__.py b/v25/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/v25/messaging/__init__.py b/v25/messaging/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/v25/messaging/message.py b/v25/messaging/message.py new file mode 100644 index 0000000..6c54723 --- /dev/null +++ b/v25/messaging/message.py @@ -0,0 +1,122 @@ +import json +from time import time +from typing import Optional, Tuple + +import nacl.encoding +import nacl.public +import nacl.secret +import nacl.signing +import nacl.utils + +from v25.messaging.encoding import Encoding +from v25.messaging.subject import Subject, PrivateSubject + +KEY_SIZE = 80 +SFROM = 0 +STO = 1 + + +class Message: + def __init__(self, sfrom: Subject, sto: Subject, idnonce: bytes, timestamp: Optional[float], + editnonce: Optional[bytes], pcontent: Optional[bytes], econtent: Optional[bytes], + flags: str): + self.sfrom = sfrom + self.sto = sto + self.idnonce = idnonce + self.timestamp = timestamp + self.editnonce = editnonce + self.pcontent = pcontent + self.econtent = econtent + self.flags = flags + + @classmethod + def send(cls, sfrom: PrivateSubject, sto: Subject, pcontent: bytes): + return cls(sfrom, sto, Encoding.nonce(), time(), + Encoding.nonce(), pcontent, None, + '').sealed() + + @classmethod + def loads(cls, s: str): + d = json.loads(s) + return cls( + Subject.loads(d['sf']), Subject.loads(d['st']), Encoding.decode(d['in']), d['ts'], + Encoding.decode(d['en']), None, Encoding.decode(d['ec']), + d['fl'] + ) + + @property + def pair(self): + return self.sfrom, self.sto + + def seal(self) -> bytes: + assert isinstance(self.sfrom, PrivateSubject) + scontent: nacl.signing.SignedMessage = self.sfrom.skey.sign(self.pcontent) + self.encrypt(scontent) + return self.econtent + + def encrypt(self, scontent: bytes): + key = nacl.utils.random(nacl.secret.SecretBox.KEY_SIZE) + self.econtent = (nacl.public.SealedBox(self.sfrom.pkey).encrypt(key) + + nacl.public.SealedBox(self.sto.pkey).encrypt(key) + + nacl.secret.SecretBox(key).encrypt(scontent)) + + def sealed(self) -> 'Message': + self.seal() + return self + + def dumps(self) -> str: + return json.dumps({ + 'sf': self.sfrom.dumps(), + 'st': self.sto.dumps(), + 'in': Encoding.encode(self.idnonce), + 'ts': self.timestamp, + 'en': Encoding.encode(self.editnonce), + 'ec': Encoding.encode(self.econtent), + 'fl': self.flags, + }) + + def unseal(self, s: int) -> bytes: + scontent = self.decrypt(s) + self.pcontent = self.sfrom.vkey.verify(scontent) + return self.pcontent + + def decrypt(self, s: int) -> bytes: + subject = self.pair[s] + assert isinstance(subject, PrivateSubject) + key: bytes = nacl.public.SealedBox(subject.ekey).decrypt( + [self.econtent[:KEY_SIZE], self.econtent[KEY_SIZE:2 * KEY_SIZE]][s]) + return nacl.secret.SecretBox(key).decrypt(self.econtent[2 * KEY_SIZE:]) + + def unsealed(self, s: int): + self.unseal(s) + return self + + def edit(self, pcontent: bytes) -> 'Message': + return Message(self.sfrom, self.sto, self.idnonce, None, + Encoding.nonce(), pcontent, None, + self.flags.replace('', '')).sealed() + + def edited(self, other: 'Message'): + return self.pair == other.pair and self.idnonce == other.idnonce + + def editt(self, content: bytes) -> Tuple['Message', 'Message']: + return self, self.edit(content) + + def delete(self): + return Message(self.sfrom, self.sto, self.idnonce, None, + None, None, None, + '') + + def flags_(self, flags: str): + return Message(self.sfrom, self.sto, self.idnonce, None, + self.editnonce, None, None, + flags) + + def alter(self, sfrom: Optional[Subject] = None, sto: Optional[Subject] = None): + if sfrom is not None: + assert self.sfrom == sfrom + self.sfrom = sfrom + if sto is not None: + assert self.sto == sto + self.sto = sto + return self diff --git a/v25/messaging/subject.py b/v25/messaging/subject.py new file mode 100644 index 0000000..5832b83 --- /dev/null +++ b/v25/messaging/subject.py @@ -0,0 +1,39 @@ +import nacl.public +import nacl.pwhash +import nacl.signing + +from v25.messaging.encoding import Encoding + + +class Subject: + def __init__(self, vkey: bytes): + self.vkey = nacl.signing.VerifyKey(vkey) + self.pkey: nacl.public.PublicKey = self.vkey.to_curve25519_public_key() + + def __eq__(self, other): + if not isinstance(other, Subject): + return False + return self.vkey == other.vkey + + def __hash__(self): + return hash(self.vkey.encode()) + + def dumps(self) -> str: + return Encoding.encode(self.vkey) + + @classmethod + def loads(cls, s: str): + return Subject(Encoding.decode(s)) + + +class PrivateSubject(Subject): + def __init__(self, pwd: str): + salt = nacl.pwhash.argon2id.SALTBYTES * b'\0' + self.seed = nacl.pwhash.argon2id.kdf( + nacl.public.PrivateKey.SEED_SIZE, pwd.encode(), salt, + + nacl.pwhash.OPSLIMIT_INTERACTIVE, + nacl.pwhash.MEMLIMIT_INTERACTIVE) + self.skey = nacl.signing.SigningKey(self.seed) + self.ekey: nacl.public.PrivateKey = self.skey.to_curve25519_private_key() + super().__init__(bytes(self.skey.verify_key)) diff --git a/v25/storage/__init__.py b/v25/storage/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/v25/storage/dbstorage.py b/v25/storage/dbstorage.py new file mode 100644 index 0000000..1fd9eab --- /dev/null +++ b/v25/storage/dbstorage.py @@ -0,0 +1,135 @@ +from typing import Tuple, Optional, Iterable + +from nacl.bindings import crypto_sign_PUBLICKEYBYTES +from sqlalchemy import create_engine, LargeBinary, Column, REAL, BLOB, String, or_, and_, BigInteger +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker, Query + +from v25.messaging.encoding import NONCE_SIZE, Encoding +from v25.messaging.message import Message +from v25.messaging.subject import Subject +from v25.storage.storage import AbstractStorage + +Base = declarative_base() + + +class MsgSgn(Base): + __tablename__ = 'msgsgns' + + sf = Column(LargeBinary(crypto_sign_PUBLICKEYBYTES), primary_key=True) + st = Column(LargeBinary(crypto_sign_PUBLICKEYBYTES), primary_key=True) + idn = Column(LargeBinary(NONCE_SIZE), primary_key=True) + + +class Msg(Base): + __tablename__ = 'msgs' + + sf = Column(LargeBinary(crypto_sign_PUBLICKEYBYTES), primary_key=True) + st = Column(LargeBinary(crypto_sign_PUBLICKEYBYTES), primary_key=True) + idn = Column(LargeBinary(NONCE_SIZE), primary_key=True) + ts = Column(REAL, nullable=False, index=True) + en = Column(LargeBinary(NONCE_SIZE), nullable=False) + ec = Column(BLOB, nullable=False) + flags = Column(String, nullable=False) + oid = Column(BigInteger, autoincrement=True, index=True, unique=True, nullable=False) + + def sgn(self): + return MsgSgn(sf=self.sf, st=self.st, idn=self.idn) + + def __repr__(self): + return f"{type(self).__name__}(" \ + f"sf={self.sf!r}, st={self.st!r}, idn={self.idn!r}, ts={self.ts!r}," \ + f" en={self.en!r}, ec={self.ec!r}," \ + f" flags={self.flags!r}" \ + f")" + + @classmethod + def from_message(cls, m: Message): + # noinspection PyArgumentList + return cls(sf=m.sfrom.vkey.encode(), st=m.sto.vkey.encode(), idn=m.idnonce, ts=m.timestamp, + en=m.editnonce, ec=m.econtent, + flags=m.flags) + + def to_message(self): + return Message(Subject(self.sf), Subject(self.st), self.idn, self.ts, + self.en, None, self.ec, self.flags) + + +class DBStorage(AbstractStorage): + def __init__(self, *args): + self.engine = create_engine(*args, echo=False) + Base.metadata.create_all(self.engine) + self.Session = sessionmaker(bind=self.engine) + + def check(self, s: Subject) -> dict: + return {'allowed': None} + + def push(self, m: Message) -> None: + session = self.Session() + msg = Msg.from_message(m) + session.add(msg) + session.add(msg.sgn()) + session.commit() + session.close() + + def edit(self, old: Message, new: Message): + assert old.edited(new) + session = self.Session() + msg = self.one_alike(session, old) + assert msg.en == old.editnonce + msgn = Msg.from_message(new) + msg.en = msgn.en + msg.ec = msgn.ec + msg.flags = msgn.flags + session.commit() + session.close() + + @staticmethod + def one_alike(session, m: Message) -> Msg: + return session.query(Msg).filter_by(sf=m.sfrom.vkey.encode(), st=m.sto.vkey.encode(), idn=m.idnonce).one() + + def delete(self, m: Message): + session = self.Session() + session.delete(self.one_alike(session, m)) + session.commit() + session.close() + + def pull(self, pair: Tuple[Subject, Subject], params: Optional[dict] = None) -> Iterable[Message]: + if params is None: + params = {} + session = self.Session() + query: Query = session.query(Msg).filter(or_( + and_( + Msg.sf == pair[0].vkey.encode(), + Msg.st == pair[1].vkey.encode(), + ), + and_( + Msg.sf == pair[1].vkey.encode(), + Msg.st == pair[0].vkey.encode(), + ), + )) + if 'ts' in params: + if '>' in params: + query = query.filter(Msg.ts > params['ts']['>']) + if '<' in params: + query = query.filter(Msg.ts < params['ts']['<']) + if params.get('before'): + query = query.filter(Msg.oid < self.one_alike( + session, + Message(pair[0], pair[1], Encoding.decode(params['before']), None, + None, None, None, + '')).oid) + for flag in params.get('flags', ()): + query = query.filter(Msg.flags.contains(flag)) + query = query.order_by(Msg.oid.desc()) + if 'limit' in params: + query = query.limit(params['limit']) + return map(Msg.to_message, query.from_self().order_by(Msg.oid)) + + def flags(self, m: Message, flags: str): + session = self.Session() + msg: Msg = self.one_alike(session, m) + assert msg.en == m.editnonce + msg.flags = flags + session.commit() + session.close() diff --git a/v25/storage/secstorage.py b/v25/storage/secstorage.py new file mode 100644 index 0000000..c3101c7 --- /dev/null +++ b/v25/storage/secstorage.py @@ -0,0 +1,35 @@ +from typing import Tuple, Optional, Iterable + +from v25.messaging.message import Message +from v25.messaging.subject import Subject +from v25.storage.storage import AbstractStorage + + +class SecureStorage(AbstractStorage): + def __init__(self, storage: AbstractStorage, subject: Subject): + self.storage = storage + self.subject = subject + + def check(self, s: Subject) -> dict: + assert self.subject == s + return self.storage.check(s) + + def push(self, m: Message) -> None: + assert self.subject == m.sfrom + return self.storage.push(m) + + def edit(self, old: Message, new: Message): + assert self.subject == old.sfrom + return self.storage.edit(old, new) + + def delete(self, m: Message): + assert self.subject in m.pair + return self.storage.delete(m) + + def pull(self, pair: Tuple[Subject, Subject], params: Optional[dict] = None) -> Iterable[Message]: + assert self.subject in pair + return self.storage.pull(pair, params) + + def flags(self, m: Message, flags: str): + assert self.subject in m.pair + return self.storage.flags(m, flags) diff --git a/v25/storage/storage.py b/v25/storage/storage.py new file mode 100644 index 0000000..e27aaf6 --- /dev/null +++ b/v25/storage/storage.py @@ -0,0 +1,25 @@ +from abc import ABC +from typing import Tuple, Optional, List, Iterable + +from v25.messaging.message import Message +from v25.messaging.subject import Subject + + +class AbstractStorage(ABC): + def check(self, s: Subject) -> dict: + raise NotImplementedError + + def push(self, m: Message) -> None: + raise NotImplementedError + + def edit(self, old: Message, new: Message): + raise NotImplementedError + + def delete(self, m: Message): + raise NotImplementedError + + def pull(self, pair: Tuple[Subject, Subject], params: Optional[dict] = None) -> Iterable[Message]: + raise NotImplementedError + + def flags(self, m: Message, flags: str): + raise NotImplementedError diff --git a/v25/web/__init__.py b/v25/web/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/v25/web/client/__init__.py b/v25/web/client/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/v25/web/client/remotestorage.py b/v25/web/client/remotestorage.py new file mode 100644 index 0000000..04cdf50 --- /dev/null +++ b/v25/web/client/remotestorage.py @@ -0,0 +1,46 @@ +from json import dumps +from typing import Tuple, Optional, Iterable + +import requests + +from v25.messaging.encoding import Encoding +from v25.messaging.message import Message +from v25.messaging.subject import Subject, PrivateSubject +from v25.storage.storage import AbstractStorage + + +class RemoteStorage(AbstractStorage): + def __init__(self, url: str, subject: PrivateSubject): + self.url = url + self.subject = subject + + def api(self, s: str): + return f'{self.url}/{s}' + + def req(self, api: str, o: object): + source = dumps(o) + req = requests.post(self.api(api), json={ + 'source': source, + 'subject': self.subject.dumps(), + 'signature': Encoding.encode(self.subject.skey.sign(source.encode()).signature), + }) + return req.json() + + def check(self, s: Subject) -> dict: + return self.req('check', s.dumps()) + + def push(self, m: Message) -> None: + self.req('push', m.dumps()) + + def edit(self, old: Message, new: Message): + self.req('edit', {'old': old.dumps(), 'new': new.dumps()}) + + def delete(self, m: Message): + self.req('delete', m.dumps()) + + def pull(self, pair: Tuple[Subject, Subject], params: Optional[dict] = None) -> Iterable[Message]: + return map(Message.loads, + self.req('pull', {'params': params, 'pair': [subject.dumps() for subject in pair]})) + + def flags(self, m: Message, flags: str): + self.req('flags', {'m': m.dumps(), 'flags': flags}) diff --git a/v25/web/server/__init__.py b/v25/web/server/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/v25/web/server/api.py b/v25/web/server/api.py new file mode 100644 index 0000000..366f401 --- /dev/null +++ b/v25/web/server/api.py @@ -0,0 +1,66 @@ +from json import loads +from typing import Tuple + +from flask import Flask, jsonify, request + +from v25.messaging.encoding import Encoding +from v25.messaging.message import Message +from v25.messaging.subject import Subject +from v25.storage.secstorage import SecureStorage +from v25.storage.storage import AbstractStorage + + +class API(Flask): + def __init__(self, import_name, storage: AbstractStorage): + self.storage = storage + super().__init__(import_name) + self.routes() + + def allowed(self, s: Subject): + return 'allowed' in self.storage.check(s) + + def ss(self): + d = request.json + source: str = d['source'] + subject = Subject.loads(d['subject']) + assert self.allowed(subject) + subject.vkey.verify(source.encode(), Encoding.decode(d['signature'])) + return loads(source), SecureStorage(self.storage, subject) + + def routes(self): + app = self + + @app.route('/') + def root(): + return "V25API" + + @app.route('/check', methods=['POST']) + def check(): + d, storage = self.ss() + return jsonify(storage.check(Subject.loads(d))) + + @app.route('/push', methods=['POST']) + def push(): + d, storage = self.ss() + return jsonify(storage.push(Message.loads(d))) + + @app.route('/edit', methods=['POST']) + def edit(): + d, storage = self.ss() + return jsonify(storage.edit(Message.loads(d['old']), Message.loads(d['new']))) + + @app.route('/delete', methods=['POST']) + def delete(): + d, storage = self.ss() + return jsonify(storage.delete(Message.loads(d))) + + @app.route('/pull', methods=['POST']) + def pull(): + d, storage = self.ss() + pair: Tuple[Subject, Subject] = Subject.loads(d['pair'][0]), Subject.loads(d['pair'][1]) + return jsonify(list(map(Message.dumps, storage.pull(pair, d['params'])))) + + @app.route('/flags', methods=['POST']) + def flags(): + d, storage = self.ss() + return jsonify(storage.flags(Message.loads(d['m']), d['flags']))