Skip to content

Commit

Permalink
bpov2: Changes to use wally.aes_cbc_with_ecdh_key() in protocol v2
Browse files Browse the repository at this point in the history
  • Loading branch information
JamieDriver committed Nov 29, 2023
1 parent a43f3b5 commit e54bdfa
Show file tree
Hide file tree
Showing 7 changed files with 130 additions and 113 deletions.
31 changes: 19 additions & 12 deletions client.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ def __init__(self, static_server_public_key):
self.static_server_public_key = static_server_public_key
self.ecdh_server_public_key = None

# returns ske, cke
def get_key_exchange(self):
return self.ecdh_server_public_key, self.public_key


# NOTE: protocol v1:
# Explicit 'hmac' fields, separate derived keys, and key-exchange handshake
class PINClientECDHv1(PINClientECDH):
def handshake(self, e_ecdh_server_public_key, static_server_signature):
ec_sig_verify(
self.static_server_public_key,
Expand All @@ -24,10 +32,6 @@ def handshake(self, e_ecdh_server_public_key, static_server_signature):
# Cache the shared secrets
self.generate_shared_secrets(e_ecdh_server_public_key)

# returns ske, cke
def get_key_exchange(self):
return self.ecdh_server_public_key, self.public_key

# Encrypt/sign/hmac the payload (ie. the pin secret)
def encrypt_request_payload(self, payload):
assert self.ecdh_server_public_key
Expand All @@ -48,22 +52,25 @@ def decrypt_response_payload(self, encrypted, hmac):
return decrypt(self.response_encryption_key, encrypted)


# NOTE: protocol v2:
# 'hmac' fields and derived keys implicit, and no key-exchange handshake required
class PINClientECDHv2(PINClientECDH):

def __init__(self, static_server_public_key, replay_counter):
super().__init__(static_server_public_key)

assert len(replay_counter) == 4
self.replay_counter = replay_counter
tweak = sha256(hmac_sha256(self.public_key, self.replay_counter))

# Derive and store the ecdh server public key (ske)
tweak = sha256(hmac_sha256(self.public_key, self.replay_counter))
self.ecdh_server_public_key = ec_public_key_bip341_tweak(
self.static_server_public_key, tweak, 0)

# Cache the shared secrets
self.generate_shared_secrets(self.ecdh_server_public_key)

# Encrypt/sign/hmac the payload (ie. the pin secret)
def encrypt_request_payload(self, payload):
encrypted = encrypt(self.request_encryption_key, payload)
hmac = hmac_sha256(self.request_hmac_key, self.public_key + self.replay_counter + encrypted)
return encrypted, hmac
return self.encrypt_with_ecdh(self.ecdh_server_public_key, self.LABEL_ORACLE_REQUEST,
payload)

def decrypt_response_payload(self, encrypted):
return self.decrypt_with_ecdh(self.ecdh_server_public_key, self.LABEL_ORACLE_RESPONSE,
encrypted)
21 changes: 11 additions & 10 deletions flaskserver.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
import json
import os
from flask import Flask, request, jsonify
from .server import PINServerECDH, PINServerECDHv2
from .server import PINServerECDH, PINServerECDHv1, PINServerECDHv2
from .pindb import PINDb
from wallycore import hex_from_bytes, hex_to_bytes, AES_KEY_LEN_256, \
AES_BLOCK_LEN
AES_BLOCK_LEN, HMAC_SHA256_LEN
from dotenv import load_dotenv

b2h = hex_from_bytes
Expand Down Expand Up @@ -43,7 +43,7 @@ def start_handshake_route():
app.logger.debug('Number of sessions {}'.format(len(sessions)))

# Create a new ephemeral server/session and get its signed pubkey
e_ecdh_server = PINServerECDH()
e_ecdh_server = PINServerECDHv1()
pubkey, sig = e_ecdh_server.get_signed_public_key()
ske = b2h(pubkey)

Expand All @@ -55,6 +55,7 @@ def start_handshake_route():
return jsonify({'ske': ske,
'sig': b2h(sig)})

# NOTE: explicit 'hmac' fields in protocol v1
def _complete_server_call_v1(pin_func, udata):
ske = udata['ske']
assert 'replay_counter' not in udata
Expand All @@ -70,8 +71,9 @@ def _complete_server_call_v1(pin_func, udata):
h2b(udata['hmac_encrypted_data']),
pin_func)

# Expecting to return an encrypted aes-key
# Expecting to return an encrypted aes-key with separate hmac
assert len(encrypted_key) == AES_KEY_LEN_256 + (2*AES_BLOCK_LEN)
assert len(hmac) == HMAC_SHA256_LEN

# Cleanup session
del sessions[ske]
Expand All @@ -81,24 +83,23 @@ def _complete_server_call_v1(pin_func, udata):
return jsonify({'encrypted_key': b2h(encrypted_key),
'hmac': b2h(hmac)})

# NOTE: 'hmac' data is appened to encrypted_data in protocol v2
def _complete_server_call_v2(pin_func, udata):
assert 'ske' not in udata
assert len(udata['replay_counter']) == 8
cke = h2b(udata['cke'])
replay_counter = h2b(udata['replay_counter'])
e_ecdh_server = PINServerECDHv2(replay_counter, cke)
encrypted_key, hmac = e_ecdh_server.call_with_payload(
encrypted_key = e_ecdh_server.call_with_payload(
cke,
h2b(udata['encrypted_data']),
h2b(udata['hmac_encrypted_data']),
pin_func)

# Expecting to return an encrypted aes-key
assert len(encrypted_key) == AES_KEY_LEN_256 + (2*AES_BLOCK_LEN)
# Expecting to return an encrypted aes-key with hmac appended
assert len(encrypted_key) == AES_KEY_LEN_256 + (2*AES_BLOCK_LEN) + HMAC_SHA256_LEN

# Return response
return jsonify({'encrypted_key': b2h(encrypted_key),
'hmac': b2h(hmac)})
return jsonify({'encrypted_key': b2h(encrypted_key)})

def _complete_server_call(pin_func):
try:
Expand Down
15 changes: 14 additions & 1 deletion lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from wallycore import AES_BLOCK_LEN, AES_FLAG_DECRYPT, AES_FLAG_ENCRYPT, \
aes_cbc, ec_private_key_verify, ec_public_key_from_private_key, ecdh, \
hmac_sha256
hmac_sha256, aes_cbc_with_ecdh_key


def encrypt(aes_key, plaintext):
Expand All @@ -19,6 +19,10 @@ def decrypt(aes_key, encrypted):

class E_ECDH(object):

# Labels used to derived child keys for aes_cbc_with_ecdh_key() call
LABEL_ORACLE_REQUEST = 'blind_oracle_request'.encode()
LABEL_ORACLE_RESPONSE = 'blind_oracle_response'.encode()

@classmethod
def _generate_private_key(cls):
counter = 4
Expand Down Expand Up @@ -50,3 +54,12 @@ def _derived(val):
self.request_hmac_key = _derived(1)
self.response_encryption_key = _derived(2)
self.response_hmac_key = _derived(3)

def decrypt_with_ecdh(self, public_key, label, encrypted):
return aes_cbc_with_ecdh_key(self.private_key, None, encrypted, public_key, label,
AES_FLAG_DECRYPT)

def encrypt_with_ecdh(self, public_key, label, plaintext):
iv = os.urandom(AES_BLOCK_LEN)
return aes_cbc_with_ecdh_key(self.private_key, iv, plaintext, public_key, label,
AES_FLAG_ENCRYPT)
31 changes: 16 additions & 15 deletions server.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,13 @@ def __init__(self):
super().__init__()
self.time_started = int(time.time())


# NOTE: protocol v1:
# Explicit 'hmac' fields, separate derived keys, and key-exchange handshake
class PINServerECDHv1(PINServerECDH):
def __init__(self):
super().__init__()

def get_signed_public_key(self):
return self.public_key, self._sign_with_static_key(self.public_key)

Expand Down Expand Up @@ -89,6 +96,8 @@ def call_with_payload(self, cke, encrypted, hmac, func):
return encrypted, hmac


# NOTE: protocol v2:
# 'hmac' fields and derived keys implicit, and no key-exchange handshake required
class PINServerECDHv2(PINServerECDH):

@classmethod
Expand All @@ -107,24 +116,16 @@ def __init__(self, replay_counter, cke):
self.replay_counter = replay_counter
self.private_key, self.public_key = self.generate_ec_key_pair(replay_counter, cke)

# Decrypt the received payload (ie. aes-key)
def decrypt_request_payload(self, cke, encrypted, hmac):
# Verify hmac received
hmac_calculated = hmac_sha256(self.request_hmac_key, cke + self.replay_counter + encrypted)
assert compare_digest(hmac, hmac_calculated)
def decrypt_request_payload(self, cke, encrypted):
return self.decrypt_with_ecdh(cke, self.LABEL_ORACLE_REQUEST, encrypted)

# Return decrypted data
return decrypt(self.request_encryption_key, encrypted)
def encrypt_response_payload(self, cke, payload):
return self.encrypt_with_ecdh(cke, self.LABEL_ORACLE_RESPONSE, payload)

# Function to deal with wrapper ecdh encryption.
# Calls passed function with unwrapped payload, and wraps response before
# returning. Separates payload handler func from wrapper encryption.
def call_with_payload(self, cke, encrypted, hmac, func):
self.generate_shared_secrets(cke)
payload = self.decrypt_request_payload(cke, encrypted, hmac)

# Call the passed function with the decrypted payload
def call_with_payload(self, cke, encrypted, func):
payload = self.decrypt_request_payload(cke, encrypted)
response = func(cke, payload, self._get_aes_pin_data_key(), self.replay_counter)

encrypted, hmac = self.encrypt_response_payload(response)
return encrypted, hmac
return self.encrypt_response_payload(cke, response)
27 changes: 14 additions & 13 deletions test/test_ecdh_v1.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,28 @@

import os

from ..client import PINClientECDH
from ..server import PINServerECDH
from ..client import PINClientECDHv1
from ..server import PINServerECDHv1


# Tests ECDHv1 wrapper without any reference to the pin/aes-key paylod stuff.
# Just testing the ECDH envelope/encryption in isolation, with misc bytearray()
# payloads (ie. any old str.encode()). Tests client/server handshake/pairing.
# NOTE: protocol v1: key-exchange handshake required
# NOTE: protocol v1:
# Explicit 'hmac' fields, separate derived keys, and key-exchange handshake
class ECDHv1Test(unittest.TestCase):

@classmethod
def setUpClass(cls):
PINServerECDH.load_private_key()
PINServerECDHv1.load_private_key()

# The server public key the client would know
with open(PINServerECDH.STATIC_SERVER_PUBLIC_KEY_FILE, 'rb') as f:
with open(PINServerECDHv1.STATIC_SERVER_PUBLIC_KEY_FILE, 'rb') as f:
cls.static_server_public_key = f.read()

# Make a new client and initialise with server handshake
def new_client_handshake(self, ske, sig):
client = PINClientECDH(self.static_server_public_key)
client = PINClientECDHv1(self.static_server_public_key)
client.handshake(ske, sig)
ske1, cke = client.get_key_exchange()
self.assertEqual(ske, ske1)
Expand All @@ -32,7 +33,7 @@ def _test_client_server_impl(self, client_request, server_response):

# A new server is created, which signs its newly-created ske with the
# static key (so the client can validate that the ske is genuine).
server = PINServerECDH()
server = PINServerECDHv1()
ske, sig = server.get_signed_public_key()

# They get sent to the client (eg. over network) which then validates
Expand Down Expand Up @@ -71,7 +72,7 @@ def test_client_server_happypath(self):

def test_call_with_payload(self):
# A new server and client
server = PINServerECDH()
server = PINServerECDHv1()
ske, sig = server.get_signed_public_key()
cke, client = self.new_client_handshake(ske, sig)

Expand All @@ -98,7 +99,7 @@ def _func(client_key, payload, aes_pin_data_key):

def test_multiple_calls(self):
# A new server and client
server = PINServerECDH()
server = PINServerECDHv1()
ske, sig = server.get_signed_public_key()
cke, client = self.new_client_handshake(ske, sig)

Expand All @@ -120,7 +121,7 @@ def test_multiple_calls(self):

def test_multiple_clients(self):
# A new server and several clients
server = PINServerECDH()
server = PINServerECDHv1()
ske, sig = server.get_signed_public_key()

# Server can persist and handle multiple calls provided each one is
Expand All @@ -143,7 +144,7 @@ def test_multiple_clients(self):

def test_bad_request_cke_throws(self):
# A new server and client
server = PINServerECDH()
server = PINServerECDHv1()
ske, sig = server.get_signed_public_key()
cke, client = self.new_client_handshake(ske, sig)

Expand Down Expand Up @@ -173,7 +174,7 @@ def _func(client_key, payload, aes_pin_data_key):

def test_bad_request_hmac_throws(self):
# A new server and client
server = PINServerECDH()
server = PINServerECDHv1()
ske, sig = server.get_signed_public_key()
cke, client = self.new_client_handshake(ske, sig)

Expand All @@ -200,7 +201,7 @@ def _func(client_key, payload, aes_pin_data_key):

def test_bad_response_hmac_throws(self):
# A new server and client
server = PINServerECDH()
server = PINServerECDHv1()
ske, sig = server.get_signed_public_key()
cke, client = self.new_client_handshake(ske, sig)

Expand Down
Loading

0 comments on commit e54bdfa

Please sign in to comment.