135 lines
3.7 KiB
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

"""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
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}}'
def __init__(
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 (
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') = name
self.verify_key = verify_key
self.sign_key = sign_key
def __str__(self) -> str:
return f'@{}.{base32_bytes_to_string(self.verify_key.to_bytes())}'
def __repr__(self) -> str:
return f'<Identity {self}>'
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)
def valid_name(cls, name: str) -> bool:
"""Validate an address name part"""
return bool(re.match(f'^{cls._NAME_PATTERN}$', name))
def valid_address(cls, address: str) -> bool:
"""Validate an author address"""
if not address.startswith('@'):
return False
address = address[1:]
name, verify_key_data = address.split('.')
except ValueError:
return False
verify_key_bytes = base32_string_to_bytes(verify_key_data)
except BaseException: # pylint: disable=broad-except
return False
return cls.valid_name(name)
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)
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)
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'{} {mnemonic}'