File: document.py

package info (click to toggle)
python-duniterpy 1.1.1-3
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 1,228 kB
  • sloc: python: 10,624; makefile: 182; sh: 17
file content (154 lines) | stat: -rw-r--r-- 4,559 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
# Copyright  2014-2022 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 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 Exception("Signature is not defined, can not check signature")

        verifying_key = VerifyingKey(pubkey)

        return verifying_key.check_signature(self.raw(), self.signature)