added v25

This commit is contained in:
AF 2020-08-05 03:28:24 +03:00
parent f3637d2081
commit 94eb71488d
22 changed files with 556 additions and 0 deletions

5
.gitignore vendored
View File

@ -202,3 +202,8 @@ fabric.properties
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
# Others
*.db
/test*.py

8
.idea/.gitignore vendored Normal file
View File

@ -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/

19
.idea/dataSources.xml Normal file
View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="test" uuid="c31e2821-78f4-457d-a96f-a4f0fc76708c">
<driver-ref>sqlite.xerial</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
<jdbc-url>jdbc:sqlite:D:\source\PyCharm\v25\test.db</jdbc-url>
<libraries>
<library>
<url>file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.31.1/license.txt</url>
</library>
<library>
<url>file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.31.1/sqlite-jdbc-3.31.1.jar</url>
</library>
</libraries>
</data-source>
</component>
</project>

View File

@ -0,0 +1,19 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="PyPackageRequirementsInspection" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoredPackages">
<value>
<list size="1">
<item index="0" class="java.lang.String" itemvalue="nacl" />
</list>
</value>
</option>
</inspection_tool>
<inspection_tool class="SpellCheckingInspection" enabled="false" level="TYPO" enabled_by_default="false">
<option name="processCode" value="true" />
<option name="processLiterals" value="true" />
<option name="processComments" value="true" />
</inspection_tool>
</profile>
</component>

View File

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

7
.idea/misc.xml Normal file
View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JavaScriptSettings">
<option name="languageLevel" value="ES6" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.8 (v25)" project-jdk-type="Python SDK" />
</project>

8
.idea/modules.xml Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/v25.iml" filepath="$PROJECT_DIR$/.idea/v25.iml" />
</modules>
</component>
</project>

10
.idea/v25.iml Normal file
View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/venv" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

6
.idea/vcs.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

0
v25/__init__.py Normal file
View File

View File

122
v25/messaging/message.py Normal file
View File

@ -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,
'<unread><unedited>').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('<unedited>', '<edited>')).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

39
v25/messaging/subject.py Normal file
View File

@ -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))

0
v25/storage/__init__.py Normal file
View File

135
v25/storage/dbstorage.py Normal file
View File

@ -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()

35
v25/storage/secstorage.py Normal file
View File

@ -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)

25
v25/storage/storage.py Normal file
View File

@ -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

0
v25/web/__init__.py Normal file
View File

View File

View File

@ -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})

View File

66
v25/web/server/api.py Normal file
View File

@ -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']))