From 821a3b9f1f80e0858cb1e4729a45ef371b21ba12 Mon Sep 17 00:00:00 2001
From: vtexier <vit@free.fr>
Date: Sun, 17 Feb 2019 18:00:49 +0100
Subject: [PATCH] [enh] #78 Add Ascii Armor encrypted and signed message
 creation and example

---
 duniterpy/key/__init__.py                     |   5 +-
 duniterpy/key/ascii_armor.py                  | 123 ++++++++++++++++++
 .../save_ascii_armor_encrypted_message.py     |  35 +++++
 3 files changed, 161 insertions(+), 2 deletions(-)
 create mode 100644 duniterpy/key/ascii_armor.py
 create mode 100644 examples/save_ascii_armor_encrypted_message.py

diff --git a/duniterpy/key/__init__.py b/duniterpy/key/__init__.py
index 1f2e92d5..05cc9c2b 100644
--- a/duniterpy/key/__init__.py
+++ b/duniterpy/key/__init__.py
@@ -1,3 +1,4 @@
-from .signing_key import SigningKey, ScryptParams
+from .signing_key import SigningKey
 from .verifying_key import VerifyingKey
-from .encryption_key import SecretKey, PublicKey
+from .encryption_key import SecretKey, PublicKey, SCRYPT_PARAMS, SEED_LENGTH
+from .ascii_armor import AsciiArmor
diff --git a/duniterpy/key/ascii_armor.py b/duniterpy/key/ascii_armor.py
new file mode 100644
index 00000000..a6b5d578
--- /dev/null
+++ b/duniterpy/key/ascii_armor.py
@@ -0,0 +1,123 @@
+import base64
+import libnacl
+from typing import Optional, List
+
+from duniterpy.key import SigningKey, PublicKey, SCRYPT_PARAMS, SEED_LENGTH
+
+# Headers constants
+BEGIN_MESSAGE_HEADER = "-----BEGIN DUNITER MESSAGE-----"
+END_MESSAGE_HEADER = "-----END DUNITER MESSAGE-----"
+BEGIN_SIGNATURE_HEADER = "-----BEGIN DUNITER SIGNATURE-----"
+END_SIGNATURE_HEADER = "-----END DUNITER SIGNATURE-----"
+
+# Version field values
+AA_MESSAGE_VERSION = "Python Libnacl " + libnacl.__version__
+AA_SIGNATURE_VERSION = "Python Libnacl " + libnacl.__version__
+
+
+class AsciiArmor:
+    """
+    Class to handle writing and reading of ascii armor messages
+    """
+
+    @staticmethod
+    def encrypt(message: str, pubkey: str, signing_keys: Optional[List[SigningKey]] = None,
+                message_comment: Optional[str] = None, signatures_comment: Optional[str] = None) -> str:
+        """
+        Encrypt a message in ascii armor format, optionally signing it
+
+        :param message: Utf-8 message
+        :param pubkey: Public key of recipient for encryption
+        :param signing_keys: Optional list of SigningKey instances
+        :param message_comment: Optional message comment field
+        :param signatures_comment: Optional signatures comment field
+        :return:
+        """
+        pubkey_instance = PublicKey(pubkey)
+        base64_encrypted_message = base64.b64encode(pubkey_instance.encrypt_seal(message))  # type: bytes
+        script_field = AsciiArmor._get_scrypt_field()
+
+        # create block with headers
+        ascii_armor_msg = """
+{begin_message_header}
+Version: {version}
+{script_field}
+""".format(begin_message_header=BEGIN_MESSAGE_HEADER, version=AA_MESSAGE_VERSION,
+           script_field=script_field)
+
+        # add message comment if specified
+        if message_comment:
+            ascii_armor_msg += AsciiArmor._get_comment_field(message_comment)
+
+        # add encrypted message
+        ascii_armor_msg += """
+{base64_encrypted_message}
+""".format(base64_encrypted_message=base64_encrypted_message.decode('utf-8'))
+
+        # if no signature...
+        if signing_keys is None:
+            # add message tail
+            ascii_armor_msg += END_MESSAGE_HEADER
+        else:
+            # add signature blocks and close block on last signature
+            count = 1
+            for signing_key in signing_keys:
+                ascii_armor_msg += AsciiArmor._get_signature_block(message, signing_key, count == len(signing_keys),
+                                                                   signatures_comment)
+                count += 1
+
+        return ascii_armor_msg
+
+    @staticmethod
+    def _get_scrypt_field():
+        """
+        Return the Scrypt field
+
+        :return:
+        """
+        return "Scrypt: N={0};r={1};p={2};len={3}".format(SCRYPT_PARAMS['N'], SCRYPT_PARAMS['r'], SCRYPT_PARAMS['p'],
+                                                          SEED_LENGTH)
+
+    @staticmethod
+    def _get_comment_field(comment: str) -> str:
+        """
+        Return a comment field
+
+        :param comment: Comment text
+        :return:
+        """
+        return "Comment: {comment}\n".format(comment=comment)
+
+    @staticmethod
+    def _get_signature_block(message: str, signing_key: SigningKey, close_block: bool = True,
+                             comment: Optional[str] = None) -> str:
+        """
+        Return a signature block
+
+        :param message: Message (not encrypted!) to sign
+        :param signing_key: The libnacl SigningKey instance of the keypair
+        :param close_block: Optional flag to close the signature block with the signature tail header
+        :param comment: Optional comment field content
+        :return:
+        """
+        script_param_field = AsciiArmor._get_scrypt_field()
+        base64_signature = base64.b64encode(signing_key.signature(message))
+
+        block = """{begin_signature_header}
+Version: {version}
+Scrypt: {script_params}
+""".format(begin_signature_header=BEGIN_SIGNATURE_HEADER, version=AA_SIGNATURE_VERSION,
+            script_params=script_param_field)
+
+        # add message comment if specified
+        if comment:
+            block += AsciiArmor._get_comment_field(comment)
+
+        block += """
+{base64_signature}
+""".format(base64_signature=base64_signature.decode('utf-8'))
+
+        if close_block:
+            block += END_SIGNATURE_HEADER
+
+        return block
diff --git a/examples/save_ascii_armor_encrypted_message.py b/examples/save_ascii_armor_encrypted_message.py
new file mode 100644
index 00000000..abbc6268
--- /dev/null
+++ b/examples/save_ascii_armor_encrypted_message.py
@@ -0,0 +1,35 @@
+import getpass
+from duniterpy import __version__
+
+from duniterpy.key import AsciiArmor, SigningKey
+
+################################################
+
+AA_ENCRYPTED_MESSAGE_FILENAME = 'duniter_aa_encrypted_message.txt'
+
+if __name__ == '__main__':
+    # Ask public key of the recipient
+    pubkeyBase58 = input("Enter public key of the message recipient: ")
+
+    # prompt hidden user entry
+    salt = getpass.getpass("Enter your passphrase (salt): ")
+
+    # prompt hidden user entry
+    password = getpass.getpass("Enter your password: ")
+
+    # init SigningKey instance
+    signing_key = SigningKey.from_credentials(salt, password)
+
+    # Enter the message
+    message = input("Enter your message: ")
+
+    comment = "generated by Duniterpy {0}".format(__version__)
+    # Encrypt the message, only the recipient secret key will be able to decrypt the message
+    encrypted_message = AsciiArmor.encrypt(message, pubkeyBase58, [signing_key], message_comment=comment,
+                                           signatures_comment=comment)
+
+    # Save encrypted message in a file
+    with open(AA_ENCRYPTED_MESSAGE_FILENAME, 'w') as file_handler:
+        file_handler.write(encrypted_message)
+
+    print("Ascii Armor Encrypted message saved in file ./{0}".format(AA_ENCRYPTED_MESSAGE_FILENAME))
-- 
GitLab