Refactor feed code
This commit is contained in:
parent
837fd5e38b
commit
e2ec8313dc
66
ssb/feed.py
66
ssb/feed.py
@ -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')
|
||||||
|
28
ssb/keys.py
28
ssb/keys.py
@ -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)
|
|
@ -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)
|
||||||
|
@ -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:
|
||||||
|
Loading…
Reference in New Issue
Block a user