Skip to content
Snippets Groups Projects

Draft: #164 Add encryption/decryption for transaction comment in helpers

Open Draft: #164 Add encryption/decryption for transaction comment in helpers
Open Vincent Texier requested to merge encrypt_comments into dev
Files
2
+ 205
0
 
"""
 
Copyright 2014-2021 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 random
 
from hashlib import scrypt
 
 
import base58
 
from libnacl import (
 
crypto_box_beforenm,
 
crypto_sign_ed25519_pk_to_curve25519,
 
crypto_box_seed_keypair,
 
)
 
 
from duniterpy.key import SigningKey
 
 
TRANSACTION_ENCRYPTION_MAGIC_VALUE = 0x27B6
 
TRANSACTION_ENCRYPTION_MESSAGE_TYPE_FOR_HUMAN = 0x00
 
TRANSACTION_ENCRYPTION_MESSAGE_TYPE_FOR_MACHINE = 0x01
 
 
 
def random_bytes(size: int) -> bytes:
 
"""
 
Return random bytes of given length
 
 
:param size: Size of nonce in bytes
 
:return:
 
"""
 
return bytearray(random.getrandbits(8) for _ in range(size))
 
 
 
def byte_xor(bytes1, bytes2):
 
"""
 
XOR two byte strings
 
 
:param bytes1: First string
 
:param bytes2: Second string
 
:return:
 
"""
 
return bytes([_a ^ _b for _a, _b in zip(bytes1, bytes2)])
 
 
 
def encrypt_comment(
 
comment: str,
 
signing_key: SigningKey,
 
pubkey: str,
 
message_type: int = TRANSACTION_ENCRYPTION_MESSAGE_TYPE_FOR_HUMAN,
 
) -> str:
 
"""
 
Official Duniter Transaction Comment Encryption
 
 
Comment to encrypt maximum length is 170 characters.
 
 
see RFC 0017 https://git.duniter.org/documents/rfcs/blob/tx_comment_encrypt/rfc/0017_transaction_comment_encryption.md
 
 
:param comment: Message to encrypt
 
:param signing_key: SigningKey instance of the sender
 
:param pubkey: Base58 receiver public key
 
:param message_type: Code defining the type of message (default=TRANSACTION_ENCRYPTION_MESSAGE_TYPE_HUMAN_HUMAN)
 
:return:
 
"""
 
version = 0x01
 
comment_as_bytes = comment.encode("utf-8")
 
comment_length = len(comment_as_bytes)
 
assert comment_length <= 170
 
 
nonce = random_bytes(16)
 
extra_bytes_length = random.randint(0, 170 - comment_length)
 
extra_bytes = random_bytes(extra_bytes_length)
 
encrypted_comment_length = comment_length + extra_bytes_length + 1
 
 
# convert keys to curve25519
 
x25519_pubkey = crypto_sign_ed25519_pk_to_curve25519(base58.b58decode(pubkey))
 
# _, x25519_seckey = crypto_sign_seed_keypair(signing_key.seed)
 
_, x25519_seckey = crypto_box_seed_keypair(signing_key.seed)
 
 
# compute password
 
password = crypto_box_beforenm(x25519_pubkey, x25519_seckey[:32])
 
 
# compute key
 
key = scrypt(
 
password=password, salt=nonce, n=1024, r=8, p=1, dklen=encrypted_comment_length
 
)
 
 
# encrypt with XOR
 
assert len(
 
comment_length.to_bytes(1, "big") + comment_as_bytes + extra_bytes
 
) == len(key)
 
bytes_to_encrypt = (
 
comment_length.to_bytes(1, "big") + comment_as_bytes + extra_bytes
 
)
 
encrypted_comment = byte_xor(bytes_to_encrypt, key)
 
 
data = (
 
TRANSACTION_ENCRYPTION_MAGIC_VALUE.to_bytes(2, "big")
 
+ version.to_bytes(1, "big")
 
+ message_type.to_bytes(1, "big")
 
+ nonce
 
+ encrypted_comment
 
)
 
 
# debug
 
# print(f"nonce: {nonce.hex()}")
 
# print(f"extra_bytes: {extra_bytes.hex()}")
 
# print(f"password: {password.hex()}")
 
# print(f"key: {key.hex()}")
 
 
# encode as b64 string with no padding
 
# remove padding manually (padding is always added by Python b64encode implementation)
 
# see https://bugs.python.org/issue29427
 
return base64.b64encode(data).rstrip(b"=").decode("utf-8")
 
 
 
def decrypt_comment(comment: str, signing_key: SigningKey, pubkey: str) -> str:
 
"""
 
Official Duniter Transaction Comment Decryption
 
 
see RFC 0017 https://git.duniter.org/documents/rfcs/blob/tx_comment_encrypt/rfc/0017_transaction_comment_encryption.md
 
 
:param comment: Encrypted message
 
:param signing_key: SigningKey instance of the receiver
 
:param pubkey: Public key of the sender
 
:return:
 
"""
 
# add padding (required by Python b64decode implementation)
 
# see https://bugs.python.org/issue29427
 
data = base64.b64decode(comment + "==")
 
data_length = len(data)
 
assert data_length >= 3
 
 
# extract data format and version
 
magic_value = data[:2]
 
version = data[2]
 
 
if (
 
magic_value == TRANSACTION_ENCRYPTION_MAGIC_VALUE.to_bytes(2, "big")
 
and version == 1
 
):
 
assert data_length >= 21
 
# message_type = data[3]
 
nonce = data[4:20]
 
encrypted_comment = data[20:]
 
else:
 
raise NotImplementedError("encrypted comment data format not supported")
 
 
# convert keys to curve25519
 
x25519_pubkey = crypto_sign_ed25519_pk_to_curve25519(base58.b58decode(pubkey))
 
# _, x25519_seckey = crypto_sign_seed_keypair(signing_key.seed)
 
_, x25519_seckey = crypto_box_seed_keypair(signing_key.seed)
 
# compute password
 
password = crypto_box_beforenm(x25519_pubkey, x25519_seckey[:32])
 
# compute key
 
key = scrypt(
 
password=password, salt=nonce, n=1024, r=8, p=1, dklen=data_length - 20
 
)
 
 
# decrypt with XOR
 
assert len(encrypted_comment) == len(key)
 
decrypted_bytes = byte_xor(encrypted_comment, key)
 
 
# extract data
 
message_length = decrypted_bytes[0]
 
decrypted_comment = decrypted_bytes[1 : message_length + 1] # type: bytes
 
 
return decrypted_comment.decode("utf-8")
 
 
 
if __name__ == "__main__":
 
 
sender_signing_key = SigningKey.from_credentials("alice", "alice")
 
receiver_signing_key = SigningKey.from_credentials("bob", "bob")
 
 
comment_ = "My taylor is rich ? Isn't it ? Un été 42..."
 
encrypted_comment_ = encrypt_comment(
 
comment_,
 
sender_signing_key,
 
receiver_signing_key.pubkey,
 
TRANSACTION_ENCRYPTION_MESSAGE_TYPE_FOR_HUMAN,
 
)
 
decrypted_comment_ = decrypt_comment(
 
encrypted_comment_, receiver_signing_key, sender_signing_key.pubkey
 
)
 
 
print(f"Sender: secretID=alice, password=alice -> {sender_signing_key.pubkey}")
 
print(f"Seed: {sender_signing_key.seed.hex()}")
 
print(f"Receiver: secretID=bob, password=bob {receiver_signing_key.pubkey}")
 
print(f"Seed: {receiver_signing_key.seed.hex()}")
 
print(f"Comment: {comment_}")
 
print(f"Encrypted comment: {encrypted_comment_}")
 
print(f"Decrypted comment: {decrypted_comment_}")
 
 
assert comment_ == decrypted_comment_
Loading