The SSH host key has changed on 8 April, 2022 to this one: SHA256:573uTBSeh74kvOo0HJXi5ijdzRm8me27suzNEDlGyrQ
Python implementation of [Earthstar](https://earthstar-project.org/)
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
earthsnake/earthsnake/identity.py

171 lines
4.8 KiB

"""Identity management"""
import re
from typing import Optional
import bip39
from nacl.encoding import RawEncoder
from nacl.exceptions import BadSignatureError
from nacl.signing import SigningKey, VerifyKey
from .base32 import base32_bytes_to_string, base32_string_to_bytes
from .exc import ValidationError
from .types import ALPHA_LOWER, ALPHA_LOWER_OR_DIGIT, B32_CHAR
class Identity:
"""Class representing and Earthstar identity
.. code-block:: python
identity = Identity()
mnemonic = identity.mnemonic
identity = Identity.from_mnemonic(mnemonic)
"""
_KEY_PATTERN = f'b[{B32_CHAR}]{{52}}'
_NAME_PATTERN = f'[{ALPHA_LOWER}][{ALPHA_LOWER_OR_DIGIT}]{{3}}'
_SECRET_PATTERN = f'^{_KEY_PATTERN}$'
PATTERN = f'^@{_NAME_PATTERN}\\.{_KEY_PATTERN}$'
def __init__(
self,
name: str,
verify_key: Optional[VerifyKey] = None,
sign_key: Optional[SigningKey] = None,
) -> None:
if not self.valid_name(name):
raise ValidationError(f'Invalid name: {name}')
if sign_key and verify_key and sign_key.verify_key != verify_key:
raise ValidationError('Signing and verifying keys don’t match')
if sign_key and not verify_key:
verify_key = sign_key.verify_key
if not verify_key:
raise ValidationError('At least verify_key must be present')
self.name = name
self.verify_key = verify_key
self.sign_key = sign_key
def __str__(self) -> str:
key = base32_bytes_to_string(self.verify_key.encode(RawEncoder))
return f'@{self.name}.{key}'
def __repr__(self) -> str:
if self.sign_key:
return f'<Identity (signer) {self}>'
return f'<Identity {self}>'
def __eq__(self, other: object) -> bool:
if isinstance(other, str):
return str(self) == other
if isinstance(other, Identity):
return str(self) == str(other)
raise TypeError(
'Don’t know how to compare {self.__class__.__name__} and {other.__class__.__name__}'
)
@classmethod
def from_address(cls, address: str) -> 'Identity':
"""Load an identity from an author address"""
if not cls.valid_address(address):
raise ValidationError(f'Invalid address {address}')
address = address[1:]
name, verify_key_data = address.split('.')
verify_key_bytes = base32_string_to_bytes(verify_key_data)
verify_key = VerifyKey(verify_key_bytes)
return cls(name, verify_key=verify_key)
@classmethod
def valid_name(cls, name: str) -> bool:
"""Validate an address’ name part"""
return bool(re.match(f'^{cls._NAME_PATTERN}$', name))
@classmethod
def valid_address(cls, address: str) -> bool:
"""Validate an author address"""
if not address.startswith('@'):
return False
address = address[1:]
try:
name, verify_key_data = address.split('.')
except ValueError:
return False
try:
verify_key_bytes = base32_string_to_bytes(verify_key_data)
VerifyKey(verify_key_bytes)
except BaseException: # pylint: disable=broad-except
return False
return cls.valid_name(name)
@classmethod
def generate(cls, name: str) -> 'Identity':
"""Generate a new entity"""
sign_key = SigningKey.generate()
verify_key = sign_key.verify_key
return cls(name, verify_key=verify_key, sign_key=sign_key)
@classmethod
def from_mnemonic(cls, mnemonic: str) -> 'Identity':
"""Load an identity from a mnemonic"""
name, mnemonic = mnemonic.split(' ', 1)
seed = bip39.decode_phrase(mnemonic)
sign_key = SigningKey(seed)
return cls(name, sign_key=sign_key)
@property
def mnemonic(self) -> Optional[str]:
"""Convert the identify to a BIP39 mnemonic
If the identity doesnt have a stored signing (secret) key, it returns None."""
if self.sign_key is None:
return None
seed = self.sign_key.encode()
mnemonic = bip39.encode_bytes(seed)
return f'{self.name} {mnemonic}'
def sign(self, data: str) -> str:
"""Sign data"""
if not self.sign_key:
raise TypeError('This identity doesn’t have a signing key')
signed_message = self.sign_key.sign(data.encode('utf-8'))
return base32_bytes_to_string(signed_message.signature)
def verify(self, data: str, signature: str) -> bool:
"""Verify if data is signed by us"""
signature_bytes = base32_string_to_bytes(signature)
try:
self.verify_key.verify(data.encode('utf-8'), signature_bytes)
except BadSignatureError:
return False
return True