Refactor feed code

This commit is contained in:
Pedro Ferreira 2017-08-05 18:21:14 +02:00
parent 837fd5e38b
commit e2ec8313dc
4 changed files with 74 additions and 55 deletions

View File

@ -5,33 +5,69 @@ from hashlib import sha256
from simplejson import dumps, loads from simplejson import dumps, loads
from ssb.util import tag
OrderedMsg = namedtuple('OrderedMsg', ('previous', 'author', 'sequence', 'timestamp', 'hash', 'content')) OrderedMsg = namedtuple('OrderedMsg', ('previous', 'author', 'sequence', 'timestamp', 'hash', 'content'))
class NoPrivateKeyException(Exception):
pass
def to_ordered(data): def to_ordered(data):
smsg = OrderedMsg(**data) smsg = OrderedMsg(**data)
return OrderedDict((k, getattr(smsg, k)) for k in smsg._fields) return OrderedDict((k, getattr(smsg, k)) for k in smsg._fields)
class Feed(object):
def __init__(self, public_key):
self.public_key = public_key
@property
def id(self):
return tag(self.public_key).decode('ascii')
def sign(self):
raise NoPrivateKeyException('Cannot use remote identity to sign (no private key!)')
class LocalFeed(Feed):
def __init__(self, private_key):
self.private_key = private_key
@property
def public_key(self):
return self.private_key.verify_key
def sign(self, msg):
return self.private_key.sign(msg).signature
class Message(object): class Message(object):
def __init__(self, keypair, content, timestamp=None, previous=None): def __init__(self, feed, content, signature, sequence=1, timestamp=None, previous=None):
self.keypair = keypair self.feed = feed
self.content = content self.content = content
self.signature = signature
self.previous = previous self.previous = previous
self.sequence = (self.previous.sequence + 1) if self.previous else 1 if self.previous:
self.sequence = self.previous.sequence + 1
else:
self.sequence = sequence
self.timestamp = int(time.time() * 1000) if timestamp is None else timestamp self.timestamp = int(time.time() * 1000) if timestamp is None else timestamp
@classmethod @classmethod
def parse(cls, data, keypair): def parse(cls, data, feed):
obj = loads(data, object_pairs_hook=OrderedDict) obj = loads(data, object_pairs_hook=OrderedDict)
msg = cls(keypair, obj['content'], timestamp=obj['timestamp']) msg = cls(feed, obj['content'], timestamp=obj['timestamp'])
return msg, obj['signature'] return msg, obj['signature']
def to_dict(self, add_signature=True): def to_dict(self, add_signature=True):
obj = to_ordered({ obj = to_ordered({
'previous': self.previous.key if self.previous else None, 'previous': self.previous.key if self.previous else None,
'author': self.keypair.tag, 'author': self.feed.id,
'sequence': self.sequence, 'sequence': self.sequence,
'timestamp': self.timestamp, 'timestamp': self.timestamp,
'hash': 'sha256', 'hash': 'sha256',
@ -42,12 +78,6 @@ class Message(object):
obj['signature'] = self.signature obj['signature'] = self.signature
return obj return obj
@property
def signature(self):
# ensure ordering of keys and indentation of 2 characters, like ssb-keys
data = dumps(self.to_dict(add_signature=False), indent=2)
return (b64encode(bytes(self.keypair.sign(data.encode('ascii')))) + b'.sig.ed25519').decode('ascii')
def verify(self, signature): def verify(self, signature):
return self.signature == signature return self.signature == signature
@ -59,3 +89,15 @@ class Message(object):
@property @property
def key(self): def key(self):
return '%' + self.hash return '%' + self.hash
class LocalMessage(Message):
def __init__(self, feed, content, signature=None, sequence=1, timestamp=None, previous=None):
super(LocalMessage, self).__init__(feed, content, signature, sequence, timestamp, previous)
if signature is None:
self.signature = self.sign()
def sign(self):
# ensure ordering of keys and indentation of 2 characters, like ssb-keys
data = dumps(self.to_dict(add_signature=False), indent=2)
return (b64encode(bytes(self.feed.sign(data.encode('ascii')))) + b'.sig.ed25519').decode('ascii')

View File

@ -1,28 +0,0 @@
from base64 import b64encode
from nacl.signing import SigningKey
def tag(key):
"""Create tag from publick key."""
return b'@' + b64encode(bytes(key)) + b'.ed25519'
class KeyPair(object):
def __init__(self, seed=None):
self.private_key = SigningKey.generate() if seed is None else SigningKey(seed)
self.public_key = self.private_key.verify_key
@property
def tag(self):
return tag(self.public_key).decode('ascii')
@property
def private_tag(self):
return tag(self.private_key)
def sign(self, data):
return self.private_key.sign(data).signature
def __repr__(self):
return "<KeyPair {0.tag}>".format(self)

View File

@ -2,29 +2,29 @@ from base64 import b64decode
from collections import OrderedDict from collections import OrderedDict
import pytest import pytest
from nacl.signing import SigningKey
from ssb.feed import Message from ssb.feed import LocalMessage, LocalFeed
from ssb.keys import KeyPair
@pytest.fixture() @pytest.fixture()
def keypair(): def feed():
secret = b64decode('Mz2qkNOP2K6upnqibWrR+z8pVUI1ReA1MLc7QMtF2qQ=') secret = b64decode('Mz2qkNOP2K6upnqibWrR+z8pVUI1ReA1MLc7QMtF2qQ=')
return KeyPair(secret) return LocalFeed(SigningKey(secret))
def test_keypair(): def test_feed():
secret = b64decode('Mz2qkNOP2K6upnqibWrR+z8pVUI1ReA1MLc7QMtF2qQ=') secret = b64decode('Mz2qkNOP2K6upnqibWrR+z8pVUI1ReA1MLc7QMtF2qQ=')
kp = KeyPair(secret) feed = LocalFeed(SigningKey(secret))
assert bytes(kp.private_key) == secret assert bytes(feed.private_key) == secret
assert bytes(kp.public_key) == b64decode('I/4cyN/jPBbDsikbHzAEvmaYlaJK33lW3UhWjNXjyrU=') assert bytes(feed.public_key) == b64decode('I/4cyN/jPBbDsikbHzAEvmaYlaJK33lW3UhWjNXjyrU=')
assert kp.tag == '@I/4cyN/jPBbDsikbHzAEvmaYlaJK33lW3UhWjNXjyrU=.ed25519' assert feed.id == '@I/4cyN/jPBbDsikbHzAEvmaYlaJK33lW3UhWjNXjyrU=.ed25519'
def test_message(keypair): def test_message(feed):
m1 = Message(keypair, OrderedDict([ m1 = LocalMessage(feed, OrderedDict([
('type', 'about'), ('type', 'about'),
('about', keypair.tag), ('about', feed.id),
('name', 'neo'), ('name', 'neo'),
('description', 'The Chosen One') ('description', 'The Chosen One')
]), timestamp=1495706260190) ]), timestamp=1495706260190)
@ -35,9 +35,9 @@ def test_message(keypair):
'lPsQ9P10OgeyH6u0unFgiI2wV/RQ7Q2x2ebxnXYCzsJ055TBMXphRADTKhOMS2EkUxXQ9k3amj5fnWPudGxwBQ==.sig.ed25519' 'lPsQ9P10OgeyH6u0unFgiI2wV/RQ7Q2x2ebxnXYCzsJ055TBMXphRADTKhOMS2EkUxXQ9k3amj5fnWPudGxwBQ==.sig.ed25519'
assert m1.key == '%xRDqws/TrQmOd4aEwZ32jdLhP873ZKjIgHlggPR0eoo=.sha256' assert m1.key == '%xRDqws/TrQmOd4aEwZ32jdLhP873ZKjIgHlggPR0eoo=.sha256'
m2 = Message(keypair, OrderedDict([ m2 = LocalMessage(feed, OrderedDict([
('type', 'about'), ('type', 'about'),
('about', keypair.tag), ('about', feed.id),
('name', 'morpheus'), ('name', 'morpheus'),
('description', 'Dude with big jaw') ('description', 'Dude with big jaw')
]), previous=m1, timestamp=1495706447426) ]), previous=m1, timestamp=1495706447426)

View File

@ -1,6 +1,6 @@
import os import os
import yaml import yaml
from base64 import b64decode from base64 import b64decode, b64encode
from nacl.signing import SigningKey from nacl.signing import SigningKey
@ -9,6 +9,11 @@ class ConfigException(Exception):
pass pass
def tag(key):
"""Create tag from publick key."""
return b'@' + b64encode(bytes(key)) + b'.ed25519'
def load_ssb_secret(): def load_ssb_secret():
"""Load SSB keys from ~/.ssb""" """Load SSB keys from ~/.ssb"""
with open(os.path.expanduser('~/.ssb/secret')) as f: with open(os.path.expanduser('~/.ssb/secret')) as f: