diff --git a/graphenecommon/message.py b/graphenecommon/message.py index eca37707..7aa9ba23 100644 --- a/graphenecommon/message.py +++ b/graphenecommon/message.py @@ -4,6 +4,7 @@ import re from binascii import hexlify, unhexlify +from datetime import datetime from graphenebase.ecdsa import sign_message, verify_message @@ -19,7 +20,7 @@ log = logging.getLogger(__name__) -class Message(AbstractBlockchainInstanceProvider): +class MessageV1(AbstractBlockchainInstanceProvider): """ Allow to sign and verify Messages that are sigend with a private key """ @@ -187,3 +188,169 @@ def verify(self, **kwargs): self.plain_message = message return True + + +class MessageV2(AbstractBlockchainInstanceProvider): + """ Allow to sign and verify Messages that are sigend with a private key + """ + + def __init__(self, message, *args, **kwargs): + self.define_classes() + assert self.account_class + assert self.publickey_class + + self.message = message + self.signed_by_account = None + self.signed_by_name = None + self.meta = None + self.plain_message = None + + def sign(self, account=None, **kwargs): + """ Sign a message with an account's memo key + + :param str account: (optional) the account that owns the bet + (defaults to ``default_account``) + :raises ValueError: If not account for signing is provided + + :returns: the signed message encapsulated in a known format + """ + if not account: + if "default_account" in self.blockchain.config: + account = self.blockchain.config["default_account"] + if not account: + raise ValueError("You need to provide an account") + + # Data for message + account = self.account_class(account, blockchain_instance=self.blockchain) + + # wif key + wif = self.blockchain.wallet.getPrivateKeyForPublicKey( + account["options"]["memo_key"] + ) + + payload = [ + "from", + account["name"], + "key", + account["options"]["memo_key"], + "time", + str(datetime.utcnow()), + "text", + self.message, + ] + enc_message = json.dumps(payload) + + # signature + signature = hexlify(sign_message(enc_message, wif)).decode("ascii") + + return dict(signed=enc_message, payload=payload, signature=signature) + + def verify(self, **kwargs): + """ Verify a message with an account's memo key + + :param str account: (optional) the account that owns the bet + (defaults to ``default_account``) + + :returns: True if the message is verified successfully + :raises InvalidMessageSignature if the signature is not ok + """ + assert isinstance(self.message, dict) + + payload = self.message.get("payload") + assert payload + payload_dict = {k[0]: k[1] for k in zip(payload[::2], payload[1::2])} + signature = self.message.get("signature") + + account_name = payload_dict.get("from").strip() + memo_key = payload_dict.get("key").strip() + + assert account_name + assert memo_key + + try: + self.publickey_class(memo_key, prefix=self.blockchain.prefix) + except Exception: + raise InvalidMemoKeyException("The memo key in the message is invalid") + + # Load account from blockchain + try: + account = self.account_class( + account_name, blockchain_instance=self.blockchain + ) + except AccountDoesNotExistsException: + raise AccountDoesNotExistsException( + "Could not find account {}. Are you connected to the right chain?".format( + account_name + ) + ) + + # Test if memo key is the same as on the blockchain + if not account["options"]["memo_key"] == memo_key: + raise WrongMemoKey( + "Memo Key of account {} on the Blockchain ".format(account["name"]) + + "differs from memo key in the message: {} != {}".format( + account["options"]["memo_key"], memo_key + ) + ) + + # Ensure payload and signed match + assert json.dumps(self.message.get("payload")) == self.message.get("signed") + + # Reformat message + enc_message = self.message.get("signed") + + # Verify Signature + pubkey = verify_message(enc_message, unhexlify(signature)) + + # Verify pubky + pk = self.publickey_class( + hexlify(pubkey).decode("ascii"), prefix=self.blockchain.prefix + ) + if format(pk, self.blockchain.prefix) != memo_key: + raise InvalidMessageSignature("The signature doesn't match the memo key") + + self.signed_by_account = account + self.signed_by_name = account["name"] + self.plain_message = payload_dict.get("text") + + return True + + +class Message(MessageV1, MessageV2): + supported_formats = (MessageV1, MessageV2) + valid_exceptions = ( + AccountDoesNotExistsException, + InvalidMessageSignature, + WrongMemoKey, + InvalidMemoKeyException, + ) + + def __init__(self, message, *args, **kwargs): + for _format in self.supported_formats: + try: + _format.__init__(self, message, *args, **kwargs) + return + except self.valid_exceptions as e: + raise e + except Exception: + pass + + def verify(self, **kwargs): + for _format in self.supported_formats: + try: + _format.verify(self, **kwargs) + return + except self.valid_exceptions as e: + raise e + except Exception: + pass + + def sign(self, account=None, **kwargs): + for _format in self.supported_formats: + try: + _format.sign(self, account=None, **kwargs) + return + except self.valid_exceptions as e: + raise e + except Exception: + pass diff --git a/tests/fixtures.py b/tests/fixtures.py index cd365105..561d92ff 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -82,7 +82,11 @@ from graphenecommon.committee import Committee as GCommittee from graphenecommon.block import Block as GBlock, BlockHeader as GBlockHeader from graphenecommon.blockchain import Blockchain as GBLockchain -from graphenecommon.message import Message as GMessage +from graphenecommon.message import ( + Message as GMessage, + MessageV1 as GMessageV1, + MessageV2 as GMessageV2, +) from graphenecommon.blockchainobject import ObjectCache, BlockchainObject from graphenecommon.price import Price as GPrice from graphenecommon.wallet import Wallet as GWallet @@ -258,6 +262,20 @@ def define_classes(self): self.publickey_class = PublicKey +@BlockchainInstance.inject +class MessageV1(GMessageV1): + def define_classes(self): + self.account_class = Account + self.publickey_class = PublicKey + + +@BlockchainInstance.inject +class MessageV2(GMessageV2): + def define_classes(self): + self.account_class = Account + self.publickey_class = PublicKey + + @BlockchainInstance.inject class Price(GPrice): def define_classes(self): diff --git a/tests/test_message.py b/tests/test_message.py index d06e406a..5c12e585 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- import unittest -from .fixtures import fixture_data, Message +from .fixtures import fixture_data, Message, MessageV1, MessageV2 +from pprint import pprint from graphenecommon.exceptions import InvalidMessageSignature @@ -40,3 +41,20 @@ def test_verify_message(self): "2034f601e175a25cf9f60a828650301f57c9efab53929b6a82fb413feb8a786fcb3ba4238dd8bece03aee38526ee363324d43944d4a3f9dc624fbe53ef5f0c9a5e\n" "-----END GRAPHENE SIGNED MESSAGE-----" ).verify() + + def test_v2_enc(self): + m = MessageV2("foobar") + c = m.sign(account="init0") + v = MessageV2(c) + v.verify() + + def test_v2andv1_enc(self): + m = MessageV2("foobar") + c = m.sign(account="init0") + v = Message(c) + v.verify() + + m = MessageV1("foobar") + c = m.sign(account="init0") + v = Message(c) + v.verify()