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.

171 lines
4.8 KiB

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 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
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[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 dont 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') = 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'@{}.{key}'
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 = VerifyKey(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 = SigningKey.generate()
verify_key = sign_key.verify_key
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.encode()
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')
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)
self.verify_key.verify(data.encode('utf-8'), signature_bytes)
except BadSignatureError:
return False
return True