Page MenuHomePhorge

No OneTemporary

diff --git a/.gitignore b/.gitignore
index e5b27a7..70d29c3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,9 +1,12 @@
*.swp
*.egg-info/
*.pyc
/build/
/dist/
+/test/
+
+/local
__pycache__
diff --git a/CHANGELOG b/CHANGELOG
index 2b3662f..ea63d13 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,11 +1,13 @@
Copyright (C) 2022 LANNOCC (Shawn A. Wilson)
@%@~LICENSE:MIT~@%@
+saw_061722_1 - Beginning crypto handler v1 (label registration).
+
saw_061622_1 - Beginning node protocol v1 (ping-pong).
saw_061422_3 - Use os.makedirs() for the user_data_dir (creates parents).
saw_061422_2 - Now have a simple `lank` command.
saw_061422_1 - Initial commit (barebones pip project skeleton).
diff --git a/lank/__version__.py b/lank/__version__.py
index b7b6da0..63797b7 100644
--- a/lank/__version__.py
+++ b/lank/__version__.py
@@ -1,2 +1,2 @@
-__version__ = '0.1.3'
+__version__ = '0.1.4'
diff --git a/lank/cmd.py b/lank/cmd.py
index 2e68b6b..24e46f6 100644
--- a/lank/cmd.py
+++ b/lank/cmd.py
@@ -1,69 +1,77 @@
def main(args):
print('LANK: ', end='')
cmd = args[0] if args else 'help'
args = args[1:]
if cmd in SET:
cmd = SET[cmd]
print(cmd[1])
cmd[0](args)
else:
print(f'unknown command: {cmd}')
print('Try `lank help` for a list of commands.')
def help(args):
print('Listening Anchor Nodes for K')
print()
print('USAGE:')
print(' lank <command>')
print()
print('The following commands are available:')
for name, cmd in SET.items():
print(f' {name} - {cmd[1]}')
def version(args):
from . import __version__
print(f'Installed version is {__version__}')
def dbinfo(args):
from .config import DB
from .db import VERSION
from os.path import getsize
print(f' db file: {DB}')
print(f' version: {VERSION}')
print(f' size: {getsize(DB)} (bytes)')
+def register(args):
+ from .crypto import get_handler
+
+ get_handler().register()
+
+
def node(args):
from .node.cmd import main as node_main
node_main(args)
def peer(args):
from .peer.cmd import main as peer_main
peer_main(args)
SET = {
'help': (help,
'help for this program'),
'version': (version,
'version information'),
'dbinfo': (dbinfo,
'database information'),
+ 'register': (register,
+ 'register a new label'),
'node': (node,
'node commands'),
'peer': (peer,
'peer commands'),
}
diff --git a/lank/crypto/__init__.py b/lank/crypto/__init__.py
new file mode 100644
index 0000000..43146bb
--- /dev/null
+++ b/lank/crypto/__init__.py
@@ -0,0 +1,36 @@
+from abc import ABC, abstractmethod
+
+
+VERSION = 1
+
+cache = { }
+
+
+def get_handler(version=None):
+ if not version:
+ version = VERSION
+
+ if version not in cache:
+ try:
+ exec(f'from .v{version} import Handler as Crypto_v{version}')
+ exec(f'cache[{version}] = Crypto_v{version}')
+
+ except ModuleNotFoundError:
+ cache[version] = None
+
+ handler = cache[version]
+
+ if not handler:
+ raise ValueError(f'crypto version {version}')
+
+ return handler()
+
+
+class Handler(ABC):
+ def __init__(self):
+ pass
+
+ @abstractmethod
+ def register(self):
+ raise NotImplemented()
+
diff --git a/lank/crypto/v1.py b/lank/crypto/v1.py
new file mode 100644
index 0000000..9834be1
--- /dev/null
+++ b/lank/crypto/v1.py
@@ -0,0 +1,168 @@
+from . import Handler as Base
+import lank.db as ldb
+
+from cryptography.hazmat.primitives import serialization, hashes
+from cryptography.hazmat.primitives.asymmetric import rsa, padding
+from password_strength import PasswordPolicy
+#from password_strength.tests import (
+# Length, Uppercase, Numbers, Special, NonLetters, Strength)
+
+from getpass import getpass
+import sys
+
+
+PASS_POLICY = PasswordPolicy.from_names(
+ length=8, uppercase=1, numbers=1, special=1, #nonletters=1,
+ strength=0.5)
+
+#KEY_SIZE = 15360 # too slow!
+KEY_SIZE = 4096
+KEY_PUBLIC_EXPONENT = 65537
+KEY_ENCODING = serialization.Encoding.PEM
+KEY_ENCRYPTED_FORMAT = serialization.PrivateFormat.PKCS8
+KEY_OPEN_FORMAT = serialization.PrivateFormat.TraditionalOpenSSL
+KEY_PUBLIC_FORMAT = serialization.PublicFormat.SubjectPublicKeyInfo
+
+SIGN_PAD = padding.PSS
+SIGN_PAD_MGF = padding.MGF1
+SIGN_PAD_MGF_HASH = hashes.SHA256
+SIGN_PAD_SALT_LENGTH = SIGN_PAD.MAX_LENGTH
+SIGN_HASH = hashes.SHA256
+SIGN_MESSAGE = b'Spread love everywhere you go. ' \
+ + b'Let no one ever come to you without leaving happier.' # --Mother Teresa
+
+
+# Basic process from:
+# https://dev.to/aaronktberry/generating-encrypted-key-pairs-in-python-69b
+
+class Handler(Base):
+ def __init__(self):
+ super().__init__()
+
+ def register(self):
+ print()
+ print('Create/update a label with a new key pair for signing.')
+ print()
+
+ label = input('Label: ')
+ if label: label = label.strip()
+ if not label:
+ print('ABORTED')
+ return
+
+ label_id = ldb.get_label_by_name(label)
+ if label_id:
+ print(' A label with that name already exists.')
+ label_id = label_id['id']
+ registration = ldb.find_signed(label_id, ldb.NAME_REGISTER, limit=1)
+ assert registration is not None
+ registration = registration[0]
+ #FIXME: check signed version
+
+ try:
+ priv_key = serialization.load_pem_private_key(
+ registration['address'], password=None)
+
+ print(' ERROR: The existing key is OPEN and must remain so.')
+ print('ABORTED')
+ return
+
+ except TypeError:
+ pass
+
+ password = getpass('Existing Password: ')
+ if not password:
+ print('ABORTED')
+ return
+
+ try:
+ priv_key = serialization.load_pem_private_key(
+ registration['address'], password=password.encode())
+
+ except ValueError as e:
+ if e.args: e = ' | '.join(e.args)
+ print(f' ERROR: {e}')
+ print('ABORTED')
+ return
+
+ password = getpass('New Password: ')
+
+ else:
+ password = getpass('Password: ')
+
+ if password:
+ results = PASS_POLICY.test(password)
+ if label.lower() in password.lower():
+ results.append('Contains Label Name')
+ if results:
+ print(' WARNING: You have entered a WEAK PASSWORD.')
+ print(' This makes it VERY LIKELY somebody will STEAL it.')
+ print(' The following tests FAILED:')
+ for result in results:
+ print(f' - {result}')
+ print(' Proceed with CAUTION!')
+
+ confirm = getpass('Confirm Password: ')
+ if confirm != password:
+ print('ABORTED (passwords do not match)')
+ return
+
+ else:
+ print(' WARNING: Empty password creates an OPEN key pair.')
+ print(' This means EVERYBODY is allowed to control the label')
+ print(' FOREVER and CANNOT BE UNDONE. Proceed with CAUTION!')
+
+ agree = input('Type AGREE to continue: ')
+ if agree != 'AGREE':
+ print('ABORTED')
+ return
+
+ print()
+ print('Generating key pair...', end='')
+ sys.stdout.flush()
+
+ priv_key = rsa.generate_private_key(
+ key_size=KEY_SIZE,
+ public_exponent=KEY_PUBLIC_EXPONENT)
+
+ if password:
+ priv_key_pem = priv_key.private_bytes(
+ encoding=KEY_ENCODING,
+ format=KEY_ENCRYPTED_FORMAT,
+ encryption_algorithm=serialization.BestAvailableEncryption(
+ password.encode()))
+
+ else:
+ priv_key_pem = priv_key.private_bytes(
+ encoding=KEY_ENCODING,
+ format=KEY_OPEN_FORMAT,
+ encryption_algorithm=serialization.NoEncryption())
+
+ pub_key_pem = priv_key.public_key().public_bytes(
+ encoding=KEY_ENCODING,
+ format=KEY_PUBLIC_FORMAT)
+
+ print(' [done]')
+ print('Creating signature...', end='')
+ sys.stdout.flush()
+
+ signature = priv_key.sign(
+ SIGN_MESSAGE,
+ SIGN_PAD(
+ mgf=SIGN_PAD_MGF(SIGN_PAD_MGF_HASH()),
+ salt_length=SIGN_PAD_SALT_LENGTH),
+ SIGN_HASH())
+
+ print(' [done]')
+ print('Saving to database...', end='')
+ sys.stdout.flush()
+
+ with ldb.Transaction():
+ if not label_id:
+ label_id = ldb.insert_label(label)
+
+ signed_id = ldb.insert_signed(label_id, ldb.NAME_REGISTER, 'FIXME',
+ priv_key_pem + pub_key_pem, signature, 1)
+
+ print(' [done]')
+
diff --git a/lank/db.py b/lank/db.py
index 15d4537..1ed29cd 100644
--- a/lank/db.py
+++ b/lank/db.py
@@ -1,116 +1,306 @@
from .config import DB
import sqlite3
-from datetime import datetime
+from contextlib import AbstractContextManager
-VERSION = 1
+VERSION = 2
# meta table entries
META_VERSION = 'db_version'
+# name table entries
+NAME_REGISTER = 10
+
#print(f'databasing "{DB}" ...')
con = sqlite3.connect(DB, isolation_level=None,
detect_types=sqlite3.PARSE_DECLTYPES)
con.row_factory = sqlite3.Row
sqlite3.register_adapter(bool, int)
sqlite3.register_converter('BOOLEAN', lambda v: bool(int(v)))
cur = con.cursor()
#cur.execute('.dbconfig defensive on')
cur.execute('PRAGMA journal_mode=WAL')
cur.execute('PRAGMA synchronous=NORMAL')
cur.execute('PRAGMA temp_store=MEMORY')
#cur.execute('PRAGMA locking_mode=EXCLUSIVE')
cur.execute('PRAGMA foreign_keys=ON')
def cur_fetch():
for row in cur:
return row # returns first row only
return None # if there were no rows
def cur_fetchcol(name):
row = cur_fetch()
if not row:
return None
return row[name]
def cur_fetchall():
rows = [ ]
for row in cur:
rows.append(row)
return rows
def close():
cur.execute('VACUUM')
con.close()
+class Transaction(AbstractContextManager):
+ def __init__(self):
+ super().__init__()
+
+ def __enter__(self):
+ cur.execute('BEGIN')
+ return self
+
+ def __exit__(self, exc_type=None, exc_value=None, traceback=None):
+ if exc_type or exc_value or traceback:
+ cur.execute('ROLLBACK')
+ else:
+ cur.execute('COMMIT')
+ return None
+
+
###
### META
###
cur.execute('''
CREATE TABLE IF NOT EXISTS meta (
name TEXT PRIMARY KEY,
value TEXT
)
''')
cur.executemany('''
INSERT OR IGNORE INTO meta (
name,
value
)
VALUES (
?, ?
)
''', [
(META_VERSION, VERSION),
])
def get_meta(name):
cur.execute('''
SELECT value
FROM meta
WHERE name = ?
''', (
name,
))
return cur_fetchcol('value')
def set_meta(name, value):
cur.execute('''
UPDATE meta
SET value = ?
WHERE name = ?
''', (
value,
name
))
def _upgrade_if_needed_():
v = int(get_meta(META_VERSION))
if v < VERSION:
from .patch import upgrade
- upgrade(DB, cur, cur_fetchall, set_meta, META_VERSION, v, VERSION)
+ upgrade(DB, close, cur, cur_fetchall,
+ set_meta, META_VERSION, v, VERSION)
_upgrade_if_needed_() # pause here and patch the db before continuing
###
-### FOO
+### LABEL
+###
+
+cur.execute('''
+ CREATE TABLE IF NOT EXISTS label (
+ id INTEGER PRIMARY KEY,
+ name TEXT NOT NULL COLLATE NOCASE UNIQUE
+ )
+''')
+
+def insert_label(name):
+ cur.execute('''
+ INSERT INTO label (
+ name
+ )
+ VALUES (
+ ?
+ )
+ ''', (
+ name,
+ ))
+
+ return cur.lastrowid
+
+def get_label(label_id):
+ cur.execute('''
+ SELECT *
+ FROM label
+ WHERE id = ?
+ ''', (
+ label_id,
+ ))
+
+ return cur_fetch()
+
+def get_label_by_name(name):
+ cur.execute('''
+ SELECT *
+ FROM label
+ WHERE name = ?
+ ''', (
+ name,
+ ))
+
+ return cur_fetch()
+
+def list_labels():
+ cur.execute('''
+ SELECT *
+ FROM label
+ ORDER BY name
+ ''')
+
+ return cur_fetchall()
+
+
+###
+### NAME
+###
+
+cur.execute('''
+ CREATE TABLE IF NOT EXISTS name (
+ id INTEGER PRIMARY KEY,
+ name TEXT NOT NULL COLLATE NOCASE UNIQUE
+ )
+''')
+
+cur.executemany('''
+ INSERT OR IGNORE INTO name (
+ id,
+ name
+ )
+ VALUES (
+ ?, ?
+ )
+''', [
+ (NAME_REGISTER, 'registration'),
+])
+
+def get_name(name_id):
+ cur.execute('''
+ SELECT *
+ FROM name
+ WHERE id = ?
+ ''', (
+ name_id,
+ ))
+
+ return cur_fetch()
+
+def list_names():
+ cur.execute('''
+ SELECT *
+ FROM name
+ ORDER BY name
+ ''')
+
+ return cur_fetchall()
+
+
+###
+### SIGNED
###
+cur.execute('''
+ CREATE TABLE IF NOT EXISTS signed (
+ id INTEGER PRIMARY KEY,
+ label INTEGER NOT NULL,
+ name INTEGER NOT NULL,
+ key TEXT NOT NULL,
+ address TEXT NOT NULL,
+ signature BLOB NOT NULL,
+ version INTEGER NOT NULL,
+
+ FOREIGN KEY (label) REFERENCES label (id),
+ FOREIGN KEY (name) REFERENCES name (id)
+ )
+''')
+
+cur.execute('''
+ CREATE INDEX IF NOT EXISTS signed_label_name_key ON signed (
+ label,
+ name,
+ key
+ )
+''')
+
+cur.execute('''
+ CREATE INDEX IF NOT EXISTS signed_name_key ON signed (
+ name,
+ key
+ )
+''')
+
+def insert_signed(label, name, key, address, signature, version):
+ cur.execute('''
+ INSERT INTO signed (
+ label,
+ name,
+ key,
+ address,
+ signature,
+ version
+ )
+ VALUES (
+ ?, ?, ?, ?, ?, ?
+ )
+ ''', (
+ label,
+ name,
+ key,
+ address,
+ signature,
+ version
+ ))
+
+ return cur.lastrowid
+
+def find_signed(label, name, limit=None):
+ sql = '''
+ SELECT *
+ FROM signed
+ WHERE label = ? AND name = ?
+ ORDER BY id DESC
+ '''
+
+ if limit:
+ sql += f'LIMIT {limit}'
+
+ cur.execute(sql, (label, name))
+
+ return cur_fetchall()
+
#print('db ready :-)')
diff --git a/lank/node/__init__.py b/lank/node/__init__.py
index 276800b..75655b1 100644
--- a/lank/node/__init__.py
+++ b/lank/node/__init__.py
@@ -1,66 +1,66 @@
from .protocol import get_handler
from gevent.pool import Pool
from gevent.server import StreamServer
from socket import timeout
DEFAULT_PORT = 42024
-HELLO = b'HOLANK'
+HELLO = b'\x04\x02\x00HOLANK\x00\x02\x04'
HELLO_SIZE = len(HELLO)
HELLO_TIMEOUT = 9 # seconds
class Server:
def __init__(self):
self.port = DEFAULT_PORT
self.pool = Pool()
self.server = StreamServer(('0.0.0.0', self.port), self.handle,
spawn=self.pool)
self.buffer = bytearray(HELLO_SIZE)
def run(self):
self.server.serve_forever()
def handle(self, sock, addr):
print(f' + connection from {addr}')
sock.settimeout(HELLO_TIMEOUT)
try:
read = sock.recv_into(self.buffer)
if read == HELLO_SIZE:
if self.buffer == HELLO:
try:
protocol = get_handler(sock, addr)
if protocol:
try:
protocol.server()
print(f' - finished {addr}')
except BrokenPipeError:
print(f' - closed {addr} [BROKEN PIPE]')
else:
print(f' - closed {addr} [CLIENT ABORT]')
except ValueError:
print(f' - terminated {addr} [PROTOCOL VERSION]')
else:
print(f' - terminated {addr} [BAD HELLO]')
elif read:
print(f' - terminated {addr} [BAD HELLO]')
else:
print(f' - closed {addr} [CLIENT ABORT]')
except timeout:
print(f' - terminated {addr} [HELLO TIMEOUT]')
diff --git a/lank/node/test.py b/lank/node/test.py
index 152d652..af52df4 100644
--- a/lank/node/test.py
+++ b/lank/node/test.py
@@ -1,59 +1,59 @@
from . import DEFAULT_PORT, HELLO
from .protocol.v1 import Handler, Ping, Pong
from gevent import socket
import sys
def begin(txt):
print(f' {txt}...', end='')
sys.stdout.flush()
def end():
print(' [done]')
class TestClient:
def __init__(self):
pass
def run(self):
addr = ('localhost', DEFAULT_PORT)
begin('connecting')
sock = socket.create_connection(addr)
end()
begin('sending HELLO v1')
sock.sendall(HELLO + b'\x01')
end()
begin('instantiating protocol handler')
handler = Handler(sock, addr)
end()
begin('ping-pong')
for i in range(999):
ping = Ping()
- print('>', end='')
+ print('<', end='')
sys.stdout.flush()
handler.send(ping)
msg = handler.recv()
- print('<', end='')
+ print('>', end='')
sys.stdout.flush()
if isinstance(msg, Pong):
if msg.nonce != ping.nonce:
print()
print(f' NONCE: sent {ping.nonce}, got {msg.nonce}')
break
else:
print()
print(f' {msg}')
break
end()
diff --git a/lank/patch/__init__.py b/lank/patch/__init__.py
index 661fd0b..b3b62ed 100644
--- a/lank/patch/__init__.py
+++ b/lank/patch/__init__.py
@@ -1,66 +1,68 @@
import sys
-def upgrade(dbfile, cur, fetchall, set_meta, meta_version, from_ver, to_ver):
+def upgrade(dbfile, close, cur, fetchall,
+ set_meta, meta_version, from_ver, to_ver):
print()
print()
print('#################################')
print(' DATABASE UPGRADE REQUIRED')
print('---------------------------------')
print(f' database file: {dbfile}')
print(f' your version: {from_ver}')
print(f' needed version: {to_ver}')
print('---------------------------------')
print('The database must be patched')
print('before it can be used by this')
print('release.')
print()
print('! WARNING: It is recommended that')
print('you make a backup copy of the')
print('database file before continuing!!')
print()
print('Patches will apply incrementally.')
print('---------------------------------')
print(' USE <CTRL>-C TO EXIT')
print('#################################')
v = from_ver
while v < to_ver:
v += 1
try:
apply_patch(cur, fetchall, v)
set_meta(meta_version, v)
print('SUCCESS')
except KeyboardInterrupt:
print('ANCELLED BY USER') # not a typo (piggy back off ^C)
+ close()
sys.exit(1)
except Exception as e:
print('FAILED')
raise RuntimeError(f'Failed to apply version {v} patch', e)
def apply_patch(cur, fetchall, version):
print()
print(f'Hit <ENTER> to apply version {version}...')
input()
- exec(f'from .patch.v{version} import patch as patch_v{version}')
+ exec(f'from .v{version} import patch as patch_v{version}')
cur.execute('PRAGMA foreign_keys=OFF')
cur.execute('BEGIN')
exec(f'patch_v{version}(cur)')
cur.execute('PRAGMA foreign_key_check')
fails = fetchall()
if fails:
print('!!! FOREIGN KEY FAILURE !!!')
for fail in fails:
print(f' table={fail[0]}, id={fail[1]}, column={fail[2]}')
raise RuntimeError('one or more foreign key constraints failed')
cur.execute('COMMIT')
cur.execute('PRAGMA foreign_keys=ON')
cur.execute('VACUUM')
diff --git a/lank/patch/v2.py b/lank/patch/v2.py
new file mode 100644
index 0000000..c89a9eb
--- /dev/null
+++ b/lank/patch/v2.py
@@ -0,0 +1,62 @@
+
+
+NAME_REGISTER = 10
+
+
+def patch(cur):
+ cur.execute('''
+ CREATE TABLE label (
+ id INTEGER PRIMARY KEY,
+ name TEXT NOT NULL COLLATE NOCASE UNIQUE
+ )
+ ''')
+
+ cur.execute('''
+ CREATE TABLE name (
+ id INTEGER PRIMARY KEY,
+ name TEXT NOT NULL COLLATE NOCASE UNIQUE
+ )
+ ''')
+
+ cur.executemany('''
+ INSERT INTO name (
+ id,
+ name
+ )
+ VALUES (
+ ?, ?
+ )
+ ''', [
+ (NAME_REGISTER, 'registration'),
+ ])
+
+ cur.execute('''
+ CREATE TABLE signed (
+ id INTEGER PRIMARY KEY,
+ label INTEGER NOT NULL,
+ name INTEGER NOT NULL,
+ key TEXT NOT NULL,
+ address TEXT NOT NULL,
+ signature BLOB NOT NULL,
+ version INTEGER NOT NULL,
+
+ FOREIGN KEY (label) REFERENCES label (id),
+ FOREIGN KEY (name) REFERENCES name (id)
+ )
+ ''')
+
+ cur.execute('''
+ CREATE INDEX signed_label_name_key ON signed (
+ label,
+ name,
+ key
+ )
+ ''')
+
+ cur.execute('''
+ CREATE INDEX signed_name_key ON signed (
+ name,
+ key
+ )
+ ''')
+
diff --git a/setup.py b/setup.py
index bd471bd..09b2e68 100644
--- a/setup.py
+++ b/setup.py
@@ -1,57 +1,59 @@
import setuptools
def run():
setuptools.setup(
name='lank',
version = findversion('.', 'lank'),
author='LANNOCC (Shawn A. Wilson)',
author_email='lannocc@yahoo.com',
url='https://github.com/lannocc/lank',
packages=setuptools.find_packages(),
description='Listening Anchor Nodes for K',
long_description_content_type='text/markdown',
long_description=open('README.md').read(),
classifiers=[
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
],
install_requires=[
'appdirs',
'bidict',
+ 'cryptography',
'gevent',
+ 'password-strength',
],
entry_points = {
'console_scripts': [
'lank=lank.__main__:run',
'lank-node=lank.node.__main__:run',
'lank-peer=lank.peer.__main__:run',
],
},
)
def findversion(root, name):
'''versioning strategy taken from
http://stackoverflow.com/a/7071358/7203060'''
import re
from os.path import join
vfile = join(root, name, "__version__.py")
vmatch = re.search(r'^__version__ *= *["\']([^"\']*)["\']',
open(vfile, "rt").read(), re.M)
if vmatch:
version = vmatch.group(1)
print ("Found %s version %s" % (name, version))
return version
else:
raise RuntimeError("Expecting a version string in %s." % (vfile))
if __name__ == '__main__':
run()

File Metadata

Mime Type
text/x-diff
Expires
Mon, Sep 14, 4:41 AM (1 w, 4 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3591
Default Alt Text
(23 KB)

Event Timeline