From 04354d79c9cd00b9d0cf4ba17fac0d9f4dc3fa0a Mon Sep 17 00:00:00 2001 From: Teknique Date: Wed, 2 Aug 2023 21:12:26 -0700 Subject: [PATCH 1/2] Cleanups and prepping for encryption This cleans up some types, moves some typing to loading and saving keys, uses clearer object names, and passes a crypto system into chatter. This lays the groundwork for adding encryption. --- veilid-python/demo/chat.py | 64 ++++++++++++++++++++---------------- veilid-python/demo/config.py | 11 +++++-- 2 files changed, 44 insertions(+), 31 deletions(-) diff --git a/veilid-python/demo/chat.py b/veilid-python/demo/chat.py index 4013dd8b..b5a5f0c7 100755 --- a/veilid-python/demo/chat.py +++ b/veilid-python/demo/chat.py @@ -19,17 +19,20 @@ async def noop_callback(*args, **kwargs): return -async def chatter(rc: veilid.api.RoutingContext, key: str, send_channel: int, recv_channel: int): +async def chatter( + router: veilid.api.RoutingContext, + crypto_system: veilid.CryptoSystem, + key: veilid.TypedKey, + send_subkey: veilid.ValueSubkey, + recv_subkey: veilid.ValueSubkey, +): """Read input, write it to the DHT, and print the response from the DHT.""" last_seq = -1 - send_subkey = veilid.types.ValueSubkey(send_channel) - recv_subkey = veilid.types.ValueSubkey(recv_channel) - # Prime the pumps. Especially when starting the conversation, this # causes the DHT key to propagate to the network. - await rc.set_dht_value(key, send_subkey, b"Hello from the world!") + await router.set_dht_value(key, send_subkey, b"Hello from the world!") while True: try: @@ -37,11 +40,11 @@ async def chatter(rc: veilid.api.RoutingContext, key: str, send_channel: int, re except EOFError: # Cat got your tongue? Hang up. print("Closing the chat.") - await rc.set_dht_value(key, send_subkey, QUIT) + await router.set_dht_value(key, send_subkey, QUIT) return # Write the input message to the DHT key. - await rc.set_dht_value(key, send_subkey, msg.encode()) + await router.set_dht_value(key, send_subkey, msg.encode()) # In the real world, don't do this. People may tease you for it. # This is meant to be easy to understand for demonstration @@ -49,7 +52,7 @@ async def chatter(rc: veilid.api.RoutingContext, key: str, send_channel: int, re # callback function to handle events asynchronously. while True: # Try to get an updated version of the receiving subkey. - resp = await rc.get_dht_value(key, recv_subkey, True) + resp = await router.get_dht_value(key, recv_subkey, True) if resp is None: continue @@ -72,30 +75,32 @@ async def start(host: str, port: int, name: str): conn = await veilid.json_api_connect(host, port, noop_callback) keys = config.read_keys() - my_key = veilid.KeyPair(keys["self"]) + my_keypair = keys["self"] + their_key = keys["peers"][name] members = [ - veilid.types.DHTSchemaSMPLMember(my_key.key(), 1), - veilid.types.DHTSchemaSMPLMember(keys["peers"][name], 1), + veilid.DHTSchemaSMPLMember(my_keypair.key(), 1), + veilid.DHTSchemaSMPLMember(their_key, 1), ] - router = await(await conn.new_routing_context()).with_privacy() - async with router: - rec = await router.create_dht_record(veilid.DHTSchema.smpl(0, members)) - print(f"New chat key: {rec.key}") + router = await (await conn.new_routing_context()).with_privacy() + crypto_system = await conn.get_crypto_system(veilid.CryptoKind.CRYPTO_KIND_VLD0) + async with router, crypto_system: + record = await router.create_dht_record(veilid.DHTSchema.smpl(0, members)) + print(f"New chat key: {record.key}") print("Give that to your friend!") # Close this key first. We'll reopen it for writing with our saved key. - await router.close_dht_record(rec.key) + await router.close_dht_record(record.key) - await router.open_dht_record(rec.key, veilid.KeyPair(keys["self"])) + await router.open_dht_record(record.key, my_keypair) try: # Write to the 1st subkey and read from the 2nd. - await chatter(router, rec.key, 0, 1) + await chatter(router, crypto_system, record.key, 0, 1) finally: - await router.close_dht_record(rec.key) - await router.delete_dht_record(rec.key) + await router.close_dht_record(record.key) + await router.delete_dht_record(record.key) async def respond(host: str, port: int, key: str): @@ -104,14 +109,15 @@ async def respond(host: str, port: int, key: str): conn = await veilid.json_api_connect(host, port, noop_callback) keys = config.read_keys() - my_key = veilid.KeyPair(keys["self"]) + my_keypair = keys["self"] - router = await(await conn.new_routing_context()).with_privacy() - async with router: - await router.open_dht_record(key, my_key) + router = await (await conn.new_routing_context()).with_privacy() + crypto_system = await conn.get_crypto_system(veilid.CryptoKind.CRYPTO_KIND_VLD0) + async with router, crypto_system: + await router.open_dht_record(key, my_keypair) # As the responder, we're writing to the 2nd subkey and reading from the 1st. - await chatter(router, key, 1, 0) + await chatter(router, crypto_system, key, 1, 0) async def keygen(host: str, port: int): @@ -121,17 +127,17 @@ async def keygen(host: str, port: int): crypto_system = await conn.get_crypto_system(veilid.CryptoKind.CRYPTO_KIND_VLD0) async with crypto_system: - my_key = await crypto_system.generate_key_pair() + my_keypair = await crypto_system.generate_key_pair() keys = config.read_keys() if keys["self"]: print("You already have a keypair.") sys.exit(1) - keys["self"] = my_key + keys["self"] = my_keypair config.write_keys(keys) - print(f"Your new public key is {my_key.key()}. Share it with your friends!") + print(f"Your new public key is {my_keypair.key()}. Share it with your friends!") async def add_friend(host: str, port: int, name: str, pubkey: str): @@ -147,7 +153,7 @@ async def clean(host: str, port: int, key: str): conn = await veilid.json_api_connect(host, port, noop_callback) - router = await(await conn.new_routing_context()).with_privacy() + router = await (await conn.new_routing_context()).with_privacy() async with router: await router.close_dht_record(key) await router.delete_dht_record(key) diff --git a/veilid-python/demo/config.py b/veilid-python/demo/config.py index 8479611e..12df6ccb 100644 --- a/veilid-python/demo/config.py +++ b/veilid-python/demo/config.py @@ -3,6 +3,8 @@ import json from pathlib import Path +import veilid + KEYFILE = Path(".demokeys") @@ -10,14 +12,19 @@ def read_keys() -> dict: """Load the stored keys from disk.""" try: - keydata = KEYFILE.read_text() + raw = KEYFILE.read_text() except FileNotFoundError: return { "self": None, "peers": {}, } - return json.loads(keydata) + keys = json.loads(raw) + if keys["self"] is not None: + keys["self"] = veilid.KeyPair(keys["self"]) + for name, pubkey in keys["peers"].items(): + keys["peers"][name] = veilid.PublicKey(pubkey) + return keys def write_keys(keydata: dict): From 469aefb873810e3d27272e65a963bf7e086a3206 Mon Sep 17 00:00:00 2001 From: Teknique Date: Wed, 2 Aug 2023 23:15:27 -0700 Subject: [PATCH 2/2] Encrypt messages between nodes Both sides of the chat now generate secrets based on their private key and the other side's public key. They encrypt each message with a nonce before sending it, and use the nonce in the decryption. This _does_ mean that the responder has to specify the starter's name now so that they can select the proper public key for encryption. --- veilid-python/demo/chat.py | 46 +++++++++++++++++++++++++++++--------- 1 file changed, 35 insertions(+), 11 deletions(-) diff --git a/veilid-python/demo/chat.py b/veilid-python/demo/chat.py index b5a5f0c7..7961c0cb 100755 --- a/veilid-python/demo/chat.py +++ b/veilid-python/demo/chat.py @@ -10,7 +10,8 @@ import config import veilid -QUIT = b"QUIT" +QUIT = "QUIT" +NONCE_LENGTH = 24 async def noop_callback(*args, **kwargs): @@ -23,6 +24,7 @@ async def chatter( router: veilid.api.RoutingContext, crypto_system: veilid.CryptoSystem, key: veilid.TypedKey, + secret: veilid.SharedSecret, send_subkey: veilid.ValueSubkey, recv_subkey: veilid.ValueSubkey, ): @@ -30,9 +32,24 @@ async def chatter( last_seq = -1 + async def encrypt(cleartext: str) -> bytes: + """Encrypt the message with the shared secret and a random nonce.""" + + nonce = await crypto_system.random_nonce() + encrypted = await crypto_system.crypt_no_auth(cleartext.encode(), nonce, secret) + return nonce.to_bytes() + encrypted + + async def decrypt(payload: bytes) -> str: + """Decrypt the payload with the shared secret and the payload's nonce.""" + + nonce = veilid.Nonce.from_bytes(payload[:NONCE_LENGTH]) + encrypted = payload[NONCE_LENGTH:] + cleartext = await crypto_system.crypt_no_auth(encrypted, nonce, secret) + return cleartext.decode() + # Prime the pumps. Especially when starting the conversation, this # causes the DHT key to propagate to the network. - await router.set_dht_value(key, send_subkey, b"Hello from the world!") + await router.set_dht_value(key, send_subkey, await encrypt("Hello from the world!")) while True: try: @@ -40,11 +57,11 @@ async def chatter( except EOFError: # Cat got your tongue? Hang up. print("Closing the chat.") - await router.set_dht_value(key, send_subkey, QUIT) + await router.set_dht_value(key, send_subkey, await encrypt(QUIT)) return # Write the input message to the DHT key. - await router.set_dht_value(key, send_subkey, msg.encode()) + await router.set_dht_value(key, send_subkey, await encrypt(msg)) # In the real world, don't do this. People may tease you for it. # This is meant to be easy to understand for demonstration @@ -60,11 +77,12 @@ async def chatter( if resp.seq == last_seq: continue - if resp.data == QUIT: + msg = await decrypt(resp.data) + if msg == QUIT: print("Other end closed the chat.") return - print(f"RECV< {resp.data.decode()}") + print(f"RECV< {msg}") last_seq = resp.seq break @@ -85,7 +103,9 @@ async def start(host: str, port: int, name: str): router = await (await conn.new_routing_context()).with_privacy() crypto_system = await conn.get_crypto_system(veilid.CryptoKind.CRYPTO_KIND_VLD0) - async with router, crypto_system: + async with crypto_system, router: + secret = await crypto_system.cached_dh(their_key, my_keypair.secret()) + record = await router.create_dht_record(veilid.DHTSchema.smpl(0, members)) print(f"New chat key: {record.key}") print("Give that to your friend!") @@ -97,27 +117,30 @@ async def start(host: str, port: int, name: str): try: # Write to the 1st subkey and read from the 2nd. - await chatter(router, crypto_system, record.key, 0, 1) + await chatter(router, crypto_system, record.key, secret, 0, 1) finally: await router.close_dht_record(record.key) await router.delete_dht_record(record.key) -async def respond(host: str, port: int, key: str): +async def respond(host: str, port: int, name: str, key: str): """Reply to a friend's chat.""" conn = await veilid.json_api_connect(host, port, noop_callback) keys = config.read_keys() my_keypair = keys["self"] + their_key = keys["peers"][name] router = await (await conn.new_routing_context()).with_privacy() crypto_system = await conn.get_crypto_system(veilid.CryptoKind.CRYPTO_KIND_VLD0) - async with router, crypto_system: + async with crypto_system, router: + secret = await crypto_system.cached_dh(their_key, my_keypair.secret()) + await router.open_dht_record(key, my_keypair) # As the responder, we're writing to the 2nd subkey and reading from the 1st. - await chatter(router, crypto_system, key, 1, 0) + await chatter(router, crypto_system, key, secret, 1, 0) async def keygen(host: str, port: int): @@ -175,6 +198,7 @@ def handle_command_line(arglist: list[str]): cmd_start.set_defaults(func=start) cmd_respond = subparsers.add_parser("respond", help=respond.__doc__) + cmd_respond.add_argument("name", help="Your friend's name") cmd_respond.add_argument("key", help="The chat's DHT key") cmd_respond.set_defaults(func=respond)