"""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'' return f'' 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 doesn’t 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