earthsnake/earthsnake/identity.py

178 lines
4.9 KiB
Python
Raw Normal View History

2022-04-11 13:45:45 +00:00
"""Identity management"""
import re
from typing import Optional
import bip39
2022-05-06 09:39:23 +00:00
from nacl.encoding import RawEncoder
from nacl.exceptions import BadSignatureError
from nacl.signing import SigningKey, VerifyKey
2022-04-11 13:45:45 +00:00
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}$'
2022-04-11 13:45:45 +00:00
def __init__(
self,
name: str,
2022-05-06 09:39:23 +00:00
verify_key: Optional[VerifyKey] = None,
2022-04-11 13:45:45 +00:00
sign_key: Optional[SigningKey] = None,
) -> None:
if not self.valid_name(name):
raise ValidationError(f'Invalid name: {name}')
2022-05-06 09:39:23 +00:00
if sign_key and verify_key and sign_key.verify_key != verify_key:
2022-04-11 13:45:45 +00:00
raise ValidationError('Signing and verifying keys dont match')
if sign_key and not verify_key:
2022-05-06 09:39:23 +00:00
verify_key = sign_key.verify_key
2022-04-11 13:45:45 +00:00
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:
2022-05-06 09:39:23 +00:00
key = base32_bytes_to_string(self.verify_key.encode(RawEncoder))
return f'@{self.name}.{key}'
2022-04-11 13:45:45 +00:00
def __repr__(self) -> str:
if self.sign_key:
return f'<Identity (signer) {self}>'
2022-04-11 13:45:45 +00:00
return f'<Identity {self}>'
2022-05-04 11:52:20 +00:00
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__}'
)
2022-04-11 13:45:45 +00:00
@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)
2022-05-06 09:39:23 +00:00
verify_key = VerifyKey(verify_key_bytes)
2022-04-11 13:45:45 +00:00
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)
2022-05-06 09:39:23 +00:00
VerifyKey(verify_key_bytes)
2022-04-11 13:45:45 +00:00
except BaseException: # pylint: disable=broad-except
return False
return cls.valid_name(name)
@classmethod
def generate(cls, name: str) -> 'Identity':
"""Generate a new entity"""
2022-05-06 09:39:23 +00:00
sign_key = SigningKey.generate()
verify_key = sign_key.verify_key
2022-04-11 13:45:45 +00:00
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 doesnt have a stored signing (secret) key, it returns None."""
if self.sign_key is None:
return None
2022-05-06 09:39:23 +00:00
seed = self.sign_key.encode()
2022-04-11 13:45:45 +00:00
mnemonic = bip39.encode_bytes(seed)
return f'{self.name} {mnemonic}'
2022-05-09 15:17:04 +00:00
@property
def can_sign(self) -> bool:
return bool(self.sign_key)
def sign(self, data: str) -> str:
"""Sign data"""
if not self.sign_key:
raise TypeError('This identity doesnt have a signing key')
2022-05-06 09:39:23 +00:00
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:
2022-05-06 09:39:23 +00:00
self.verify_key.verify(data.encode('utf-8'), signature_bytes)
except BadSignatureError:
return False
return True
def hash(self) -> int:
return hash(str(self))