Astroport.ONE/venv/lib/python3.11/site-packages/duniterpy/documents/document.py
2024-03-01 16:15:45 +01:00

161 lines
4.5 KiB
Python

# Copyright 2014-2024 Vincent Texier <vit@free.fr>
#
# DuniterPy is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# DuniterPy is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import base64
import hashlib
import logging
import re
from typing import Any, Dict, Optional, Pattern, Type, TypeVar
from ..constants import SIGNATURE_REGEX
from ..key import SigningKey, VerifyingKey
class SignatureException(Exception):
pass
class MalformedDocumentError(Exception):
"""
Malformed document exception
"""
def __init__(self, field_name: str) -> None:
"""
Init exception instance
:param field_name: Name of the wrong field
"""
super().__init__(f"Could not parse field {field_name}")
# required to type hint cls in classmethod
DocumentType = TypeVar("DocumentType", bound="Document")
class Document:
re_version = re.compile("Version: ([0-9]+)\n")
re_currency = re.compile("Currency: ([^\n]+)\n")
re_signature = re.compile(f"({SIGNATURE_REGEX})\n")
fields_parsers: Dict[str, Pattern] = {
"Version": re_version,
"Currency": re_currency,
"Signature": re_signature,
}
def __init__(self, version: int, currency: str) -> None:
"""
Init Document instance
:param version: Version of the Document
:param currency: Name of the currency
"""
self.version = version
self.currency = currency
self.signature: Optional[str] = None
def __eq__(self, other: Any) -> bool:
"""
Check Document instances equality
"""
if not isinstance(other, Document):
return NotImplemented
return (
self.version == other.version
and self.currency == other.currency
and self.signature == other.signature
)
def __hash__(self) -> int:
return hash(
(
self.version,
self.currency,
self.signature,
)
)
@classmethod
def parse_field(cls: Type[DocumentType], field_name: str, line: str) -> Any:
"""
Parse a document field with regular expression and return the value
:param field_name: Name of the field
:param line: Line string to parse
:return:
"""
try:
match = cls.fields_parsers[field_name].match(line)
if match is None:
raise AttributeError
value = match.group(1)
except AttributeError:
raise MalformedDocumentError(field_name) from AttributeError
return value
def sign(self, key: SigningKey) -> None:
"""
Sign the current document with key
:param key: Libnacl key instance
"""
signature = base64.b64encode(key.signature(bytes(self.raw(), "ascii")))
logging.debug("Signature:\n%s", signature.decode("ascii"))
self.signature = signature.decode("ascii")
def raw(self) -> str:
"""
Returns the raw document in string format
"""
raise NotImplementedError("raw() is not implemented")
def signed_raw(self) -> str:
"""
:return:
"""
if self.signature is None:
raise MalformedDocumentError(
"Signature is not defined, can not create signed raw format"
)
return f"{self.raw()}{self.signature}\n"
@property
def sha_hash(self) -> str:
"""
Return uppercase hex sha256 hash from signed raw document
:return:
"""
return hashlib.sha256(self.signed_raw().encode("ascii")).hexdigest().upper()
def check_signature(self, pubkey: str) -> bool:
"""
Check if the signature is from pubkey
:param pubkey: Base58 public key
:return:
"""
if self.signature is None:
raise SignatureException(
"Signature is not defined, can not check signature"
)
verifying_key = VerifyingKey(pubkey)
return verifying_key.check_signature(self.raw(), self.signature)