Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F4343
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Flag For Later
Award Token
Size
23 KB
Referenced Files
None
Subscribers
None
View Options
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
Details
Attached
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)
Attached To
Mode
rLANK Encrypted Communications
Attached
Detach File
Event Timeline
Log In to Comment