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

135 lines
3.7 KiB

"""Identity management"""
import re
from typing import Optional
import bip39
from ed25519 import SigningKey, VerifyingKey, create_keypair
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}}'
_PATTERN = f'^@{_NAME_PATTERN}\\.{_KEY_PATTERN}$'
_SECRET_PATTERN = f'^{_KEY_PATTERN}$'
def __init__(
self,
name: str,
verify_key: Optional[VerifyingKey] = 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.get_verifying_key().to_bytes() != verify_key.to_bytes()
):
raise ValidationError('Signing and verifying keys don’t match')
if sign_key and not verify_key:
verify_key = sign_key.get_verifying_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:
return f'@{self.name}.{base32_bytes_to_string(self.verify_key.to_bytes())}'
def __repr__(self) -> str:
return f'<Identity {self}>'
@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 = VerifyingKey(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)
VerifyingKey(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, verify_key = create_keypair()
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.to_seed()
mnemonic = bip39.encode_bytes(seed)
return f'{self.name} {mnemonic}'