469aefb873
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.
226 lines
7.4 KiB
Python
Executable File
226 lines
7.4 KiB
Python
Executable File
#!/usr/bin/env python
|
|
|
|
"""A simple chat server using Veilid's DHT."""
|
|
|
|
import argparse
|
|
import asyncio
|
|
import sys
|
|
|
|
import config
|
|
|
|
import veilid
|
|
|
|
QUIT = "QUIT"
|
|
NONCE_LENGTH = 24
|
|
|
|
|
|
async def noop_callback(*args, **kwargs):
|
|
"""In the real world, we'd use this to process interesting incoming events."""
|
|
|
|
return
|
|
|
|
|
|
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,
|
|
):
|
|
"""Read input, write it to the DHT, and print the response from the DHT."""
|
|
|
|
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, await encrypt("Hello from the world!"))
|
|
|
|
while True:
|
|
try:
|
|
msg = input("SEND> ")
|
|
except EOFError:
|
|
# Cat got your tongue? Hang up.
|
|
print("Closing the chat.")
|
|
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, 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
|
|
# purposes, not a great pattern. Instead, you'd want to use the
|
|
# callback function to handle events asynchronously.
|
|
while True:
|
|
# Try to get an updated version of the receiving subkey.
|
|
resp = await router.get_dht_value(key, recv_subkey, True)
|
|
if resp is None:
|
|
continue
|
|
|
|
# If the other party hasn't sent a newer message, try again.
|
|
if resp.seq == last_seq:
|
|
continue
|
|
|
|
msg = await decrypt(resp.data)
|
|
if msg == QUIT:
|
|
print("Other end closed the chat.")
|
|
return
|
|
|
|
print(f"RECV< {msg}")
|
|
last_seq = resp.seq
|
|
break
|
|
|
|
|
|
async def start(host: str, port: int, name: str):
|
|
"""Begin a conversation with a friend."""
|
|
|
|
conn = await veilid.json_api_connect(host, port, noop_callback)
|
|
|
|
keys = config.read_keys()
|
|
my_keypair = keys["self"]
|
|
their_key = keys["peers"][name]
|
|
|
|
members = [
|
|
veilid.DHTSchemaSMPLMember(my_keypair.key(), 1),
|
|
veilid.DHTSchemaSMPLMember(their_key, 1),
|
|
]
|
|
|
|
router = await (await conn.new_routing_context()).with_privacy()
|
|
crypto_system = await conn.get_crypto_system(veilid.CryptoKind.CRYPTO_KIND_VLD0)
|
|
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!")
|
|
|
|
# Close this key first. We'll reopen it for writing with our saved key.
|
|
await router.close_dht_record(record.key)
|
|
|
|
await router.open_dht_record(record.key, my_keypair)
|
|
|
|
try:
|
|
# Write to the 1st subkey and read from the 2nd.
|
|
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, 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 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, secret, 1, 0)
|
|
|
|
|
|
async def keygen(host: str, port: int):
|
|
"""Generate a keypair."""
|
|
|
|
conn = await veilid.json_api_connect(host, port, noop_callback)
|
|
|
|
crypto_system = await conn.get_crypto_system(veilid.CryptoKind.CRYPTO_KIND_VLD0)
|
|
async with crypto_system:
|
|
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_keypair
|
|
config.write_keys(keys)
|
|
|
|
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):
|
|
"""Add a friend's public key."""
|
|
|
|
keys = config.read_keys()
|
|
keys["peers"][name] = pubkey
|
|
config.write_keys(keys)
|
|
|
|
|
|
async def clean(host: str, port: int, key: str):
|
|
"""Delete a DHT key."""
|
|
|
|
conn = await veilid.json_api_connect(host, port, noop_callback)
|
|
|
|
router = await (await conn.new_routing_context()).with_privacy()
|
|
async with router:
|
|
await router.close_dht_record(key)
|
|
await router.delete_dht_record(key)
|
|
|
|
|
|
def handle_command_line(arglist: list[str]):
|
|
"""Process the command line.
|
|
|
|
This isn't the interesting part."""
|
|
|
|
parser = argparse.ArgumentParser(description="Veilid chat demonstration")
|
|
parser.add_argument("--host", default="localhost", help="Address of the Veilid server host.")
|
|
parser.add_argument("--port", type=int, default=5959, help="Port of the Veilid server.")
|
|
|
|
subparsers = parser.add_subparsers(required=True)
|
|
|
|
cmd_start = subparsers.add_parser("start", help=start.__doc__)
|
|
cmd_start.add_argument("name", help="Your friend's name")
|
|
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)
|
|
|
|
cmd_keygen = subparsers.add_parser("keygen", help=keygen.__doc__)
|
|
cmd_keygen.set_defaults(func=keygen)
|
|
|
|
cmd_add_friend = subparsers.add_parser("add-friend", help=add_friend.__doc__)
|
|
cmd_add_friend.add_argument("name", help="Your friend's name")
|
|
cmd_add_friend.add_argument("pubkey", help="Your friend's public key")
|
|
cmd_add_friend.set_defaults(func=add_friend)
|
|
|
|
cmd_clean = subparsers.add_parser("clean", help=clean.__doc__)
|
|
cmd_clean.add_argument("key", help="DHT key to delete")
|
|
cmd_clean.set_defaults(func=clean)
|
|
|
|
args = parser.parse_args(arglist)
|
|
kwargs = args.__dict__
|
|
func = kwargs.pop("func")
|
|
|
|
asyncio.run(func(**kwargs))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
handle_command_line(sys.argv[1:])
|