earthsnake/earthsnake/identity.py

135 lines
3.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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 dont 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}'