You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

169 lines
4.7 KiB

"""Identity management"""
import re
from typing import Optional
import bip39
from ed25519 import BadSignatureError, 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:
if self.sign_key:
return f'<Identity (signer) {self}>'
return f'<Identity {self}>'
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(
'Dont know how to compare {self.__class__.__name__} and {other.__class__.__name__}'
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}'
def sign(self, data: str) -> str:
"""Sign data"""
if not self.sign_key:
raise TypeError('This identity doesnt have a signing key')
return base32_bytes_to_string(self.sign_key.sign(data.encode('utf-8')))
def verify(self, data: str, signature: str) -> bool:
"""Verify if data is signed by us"""
signature_bytes = base32_string_to_bytes(signature)
self.verify_key.verify(signature_bytes, data.encode('utf-8'))
except BadSignatureError:
return False
return True