From fb2bf05057a1ab1b1b28ad912bccd5f5b0d0c23a Mon Sep 17 00:00:00 2001 From: Sergi Delgado Segura Date: Wed, 5 Jun 2019 16:44:43 +0100 Subject: [PATCH] Adds blob encryption on both sides --- .gitignore | 1 + pisa-btc/apps/blob.py | 43 +++++++++++++++++++++++ pisa-btc/apps/messages.py | 1 - pisa-btc/apps/pisa-cli.py | 62 ++++++++++++++++++++++++++------- pisa-btc/pisa/api.py | 3 +- pisa-btc/pisa/appointment.py | 8 +++-- pisa-btc/pisa/encrypted_blob.py | 23 ++++++++++++ pisa-btc/pisa/inspector.py | 36 +++++++++---------- pisa-btc/pisa/pisad.py | 4 +-- pisa-btc/pisa/watcher.py | 7 ++-- 10 files changed, 146 insertions(+), 42 deletions(-) create mode 100644 pisa-btc/apps/blob.py delete mode 100644 pisa-btc/apps/messages.py create mode 100644 pisa-btc/pisa/encrypted_blob.py diff --git a/.gitignore b/.gitignore index f062cad..eba0f38 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ bitcoin.conf *__pycache__ .pending* pisa.log +pisa-btc/apps/*.json .\#* build/ diff --git a/pisa-btc/apps/blob.py b/pisa-btc/apps/blob.py new file mode 100644 index 0000000..5d28abb --- /dev/null +++ b/pisa-btc/apps/blob.py @@ -0,0 +1,43 @@ +import hashlib +from binascii import hexlify +from cryptography.hazmat.primitives.ciphers.aead import AESGCM + +SUPPORTED_HASH_FUNCTIONS = ['SHA256'] +SUPPORTED_CYPHERS = ['AES-GCM-128'] + + +class Blob: + def __init__(self, data, cypher, hash_function): + self.data = data + self.cypher = cypher + self.hash_function = hash_function + + # FIXME: We only support SHA256 for now + if self.hash_function.upper() not in SUPPORTED_HASH_FUNCTIONS: + raise Exception('Hash function not supported ({}). Supported Hash functions: {}' + .format(self.hash_function, SUPPORTED_HASH_FUNCTIONS)) + + # FIXME: We only support SHA256 for now + if self.cypher.upper() not in SUPPORTED_CYPHERS: + raise Exception('Cypher not supported ({}). Supported cyphers: {}'.format(self.hash_function, + SUPPORTED_CYPHERS)) + + def encrypt(self, tx_id): + # Transaction to be encrypted + # FIXME: The blob data should contain more things that just the transaction. Leaving like this for now. + tx = self.data.encode() + + # FIXME: tx_id should not be necessary (can be derived from tx SegWit-like). Passing it for now + # Extend the key using SHA256 as a KDF + tx_id = tx_id.encode() + extended_key = hashlib.sha256(tx_id[:16]).digest() + + # The 16 MSB of the extended key will serve as the AES GCM 128 secret key. The 16 LSB will serve as the IV. + sk = extended_key[:16] + nonce = extended_key[16:] + + # Encrypt the data + aesgcm = AESGCM(sk) + encrypted_blob = hexlify(aesgcm.encrypt(nonce=nonce, data=tx, associated_data=None)).decode() + + return encrypted_blob diff --git a/pisa-btc/apps/messages.py b/pisa-btc/apps/messages.py deleted file mode 100644 index b57d1ed..0000000 --- a/pisa-btc/apps/messages.py +++ /dev/null @@ -1 +0,0 @@ -wrong_txid = "You should provide the 16 MSB (in hex) of the txid you'd like to be monitored." \ No newline at end of file diff --git a/pisa-btc/apps/pisa-cli.py b/pisa-btc/apps/pisa-cli.py index 2cd0817..e4b3c52 100644 --- a/pisa-btc/apps/pisa-cli.py +++ b/pisa-btc/apps/pisa-cli.py @@ -1,16 +1,41 @@ -from multiprocessing.connection import Client +import requests +import re +import os +import json from getopt import getopt from sys import argv +import logging +from conf import CLIENT_LOG_FILE + +from apps.blob import Blob from apps import PISA_API_SERVER, PISA_API_PORT -import apps.messages as msg -import re + commands = ['add_appointment'] +def build_appointment(tx, tx_id, start_block, end_block, dispute_delta): + locator = tx_id[:16] + + cipher = "AES-GCM-128" + hash_function = "SHA256" + + # FIXME: The blob data should contain more things that just the transaction. Leaving like this for now. + blob = Blob(tx, cipher, hash_function) + + # FIXME: tx_id should not be necessary (can be derived from tx SegWit-like). Passing it for now + encrypted_blob = blob.encrypt(tx_id) + + appointment = {"locator": locator, "start_block": start_block, "end_block": end_block, + "dispute_delta": dispute_delta, "encrypted_blob": encrypted_blob, "cipher": cipher, "hash_function": + hash_function} + + return appointment + + def check_txid_format(txid): - if len(txid) != 32: - raise Exception("txid does not matches the expected size (16-byte / 32 hex chars). " + msg.wrong_txid) + if len(txid) != 64: + raise Exception("txid does not matches the expected size (32-byte / 64 hex chars).") return re.search(r'^[0-9A-Fa-f]+$', txid) is not None @@ -25,6 +50,12 @@ def show_usage(): if __name__ == '__main__': opts, args = getopt(argv[1:], '', commands) + # Configure logging + logging.basicConfig(format='%(asctime)s %(message)s', level=logging.INFO, handlers=[ + logging.FileHandler(CLIENT_LOG_FILE), + logging.StreamHandler() + ]) + # Get args if len(args) > 0: command = args[0] @@ -35,18 +66,25 @@ if __name__ == '__main__': if command in commands: if len(args) != 2: - raise Exception("txid missing. " + msg.wrong_txid) + raise Exception("Path to appointment_data.json missing.") - arg = args[1] - valid_locator = check_txid_format(arg) + if not os.path.isfile(args[1]): + raise Exception("Can't find file " + args[1]) + + appointment_data = json.load(open(args[1])) + valid_locator = check_txid_format(appointment_data.get('tx_id')) if valid_locator: - conn = Client((PISA_API_SERVER, PISA_API_PORT)) + pisa_url = "http://{}:{}".format(PISA_API_SERVER, PISA_API_PORT) + appointment = build_appointment(appointment_data.get('tx'), appointment_data.get('tx_id'), + appointment_data.get('start_time'), appointment_data.get('end_time'), + appointment_data.get('dispute_delta')) - # Argv could be undefined, but we only have one command so it's safe for now - conn.send((command, arg)) + r = requests.post(url=pisa_url, json=json.dumps(appointment)) + + logging.info("[Client] {} (code: {})".format(r.text, r.status_code)) else: - raise ValueError("The provided locator is not valid. " + msg.wrong_txid) + raise ValueError("The provided locator is not valid.") else: show_usage() diff --git a/pisa-btc/pisa/api.py b/pisa-btc/pisa/api.py index 4896c03..5edcb48 100644 --- a/pisa-btc/pisa/api.py +++ b/pisa-btc/pisa/api.py @@ -16,8 +16,7 @@ def add_appointment(): logging.info('[API] connection accepted from {}:{}'.format(remote_addr, remote_port)) # Check content type once if properly defined - # FIXME: Temporary patch until Paddy set's the client properly - request_data = json.loads(request.form['data']) + request_data = json.loads(request.get_json()) appointment = inspector.inspect(request_data, debug) if appointment: diff --git a/pisa-btc/pisa/appointment.py b/pisa-btc/pisa/appointment.py index ea97062..6db08b3 100644 --- a/pisa-btc/pisa/appointment.py +++ b/pisa-btc/pisa/appointment.py @@ -1,13 +1,15 @@ +from pisa.encrypted_blob import EncryptedBlob # Basic appointment structure class Appointment: - def __init__(self, locator, start_time, end_time, dispute_delta, encrypted_blob, cypher): + def __init__(self, locator, start_time, end_time, dispute_delta, encrypted_blob, cipher, hash_function): self.locator = locator self.start_time = start_time self.end_time = end_time self.dispute_delta = dispute_delta - self.encrypted_blob = encrypted_blob - self.cipher = cypher + self.encrypted_blob = EncryptedBlob(encrypted_blob) + self.cipher = cipher + self.hash_function = hash_function diff --git a/pisa-btc/pisa/encrypted_blob.py b/pisa-btc/pisa/encrypted_blob.py new file mode 100644 index 0000000..6bf5e0a --- /dev/null +++ b/pisa-btc/pisa/encrypted_blob.py @@ -0,0 +1,23 @@ +import hashlib +from binascii import unhexlify +from cryptography.hazmat.primitives.ciphers.aead import AESGCM + + +class EncryptedBlob: + def __init__(self, data): + self.data = data + + def decrypt(self, key): + # Extend the key using SHA256 as a KDF + extended_key = hashlib.sha256(key).digest() + + # The 16 MSB of the extended key will serve as the AES GCM 128 secret key. The 16 LSB will serve as the IV. + sk = extended_key[:16] + nonce = extended_key[16:] + + # Decrypt + aesgcm = AESGCM(sk) + data = unhexlify(self.data.encode) + raw_tx = aesgcm.decrypt(nonce=nonce, data=data, associated_data=None) + + return raw_tx diff --git a/pisa-btc/pisa/inspector.py b/pisa-btc/pisa/inspector.py index 86eaa6d..e11a134 100644 --- a/pisa-btc/pisa/inspector.py +++ b/pisa-btc/pisa/inspector.py @@ -10,41 +10,41 @@ class Inspector: appointment = None - locator = data.get('txlocator') - start_time = data.get('startblock') - end_time = data.get('endblock') - - # Missing for now + locator = data.get('locator') + start_time = data.get('start_block') + end_time = data.get('end_block') dispute_delta = data.get('dispute_delta') - - # FIXME: this will be eventually be replaced, here for testing now - encrypted_blob = data.get('rawtx') - # encrypted_blob = data.get('encrypted_blob') - + encrypted_blob = data.get('encrypted_blob') cipher = data.get('cipher') + hash_function = data.get('hash_function') if self.check_locator(locator, debug) and self.check_start_time(start_time, debug) and \ self.check_end_time(end_time, debug) and self.check_delta(dispute_delta, debug) and \ - self.check_blob(encrypted_blob, debug) and self.check_cipher(cipher, debug): - appointment = Appointment(locator, start_time, end_time, dispute_delta, encrypted_blob, cipher) + self.check_blob(encrypted_blob, debug) and self.check_cipher(cipher, debug) and \ + self.check_cipher(hash_function, debug): + appointment = Appointment(locator, start_time, end_time, dispute_delta, encrypted_blob, cipher, + hash_function) return appointment # FIXME: Define checks def check_locator(self, locator, debug): - return True + return locator is not None def check_start_time(self, start_time, debug): - return True + return start_time is not None def check_end_time(self, end_time, debug): - return True + return end_time is not None def check_delta(self, dispute_delta, debug): - return True + return dispute_delta is not None def check_blob(self, encrypted_blob, debug): - return True + return encrypted_blob is not None def check_cipher(self, cipher, debug): - return True + return cipher is not None + + def check_hash_function(self, hash_function, debug): + return hash_function is not None diff --git a/pisa-btc/pisa/pisad.py b/pisa-btc/pisa/pisad.py index 458da08..b84c4c6 100644 --- a/pisa-btc/pisa/pisad.py +++ b/pisa-btc/pisa/pisad.py @@ -1,7 +1,7 @@ import logging from sys import argv from getopt import getopt -from conf import LOG_FILE +from conf import SERVER_LOG_FILE from threading import Thread from pisa.api import start_api @@ -15,7 +15,7 @@ if __name__ == '__main__': # Configure logging logging.basicConfig(format='%(asctime)s %(message)s', level=logging.INFO, handlers=[ - logging.FileHandler(LOG_FILE), + logging.FileHandler(SERVER_LOG_FILE), logging.StreamHandler() ]) diff --git a/pisa-btc/pisa/watcher.py b/pisa-btc/pisa/watcher.py index 58b31a5..a230ffc 100644 --- a/pisa-btc/pisa/watcher.py +++ b/pisa-btc/pisa/watcher.py @@ -121,10 +121,9 @@ class Watcher: for appointment_pos, appointment in enumerate(self.appointments.get(locator)): try: dispute_txid = locator + k - rawtx = decrypt_tx(appointment.encrypted_blob, k, appointment.cipher) - - txid = bitcoin_cli.decoderawtransaction(rawtx).get('txid') - matches.append((locator, appointment_pos, dispute_txid, txid, rawtx)) + raw_tx = appointment.encrypted_blob.decrypt(k) + txid = bitcoin_cli.decoderawtransaction(raw_tx).get('txid') + matches.append((locator, appointment_pos, dispute_txid, txid, raw_tx)) if debug: logging.error("[Watcher] match found for {}:{}! {}".format(locator, appointment_pos,