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