Notebook 04 — Full Alice↔Bob session via CLI#
We spawn two real CLI processes sharing an inbox directory, and watch five roundtrip messages.
import os, subprocess, tempfile, shutil
from pathlib import Path
workdir = Path(tempfile.mkdtemp(prefix="pqmsg_nb04_"))
alice_home = workdir / "alice"; alice_home.mkdir()
bob_home = workdir / "bob"; bob_home.mkdir()
shared_inbox = workdir / "shared_inbox"; shared_inbox.mkdir()
(alice_home / "inbox").symlink_to(shared_inbox, target_is_directory=True)
(bob_home / "inbox").symlink_to(shared_inbox, target_is_directory=True)
print("workdir:", workdir)
workdir: /tmp/pqmsg_nb04_aie40oxj
def run(home, *args):
env = os.environ.copy()
env["PQMSG_HOME"] = str(home)
r = subprocess.run(["pqmsg"] + list(args), env=env, capture_output=True, text=True, timeout=60)
return r
print(run(alice_home, "init", "--name", "alice").stdout.strip())
print(run(bob_home, "init", "--name", "bob").stdout.strip())
Generated identity for 'alice' at /tmp/pqmsg_nb04_aie40oxj/alice/identity.json
Share your contact file: pqmsg export-contact --output /tmp/alice.pub
Generated identity for 'bob' at /tmp/pqmsg_nb04_aie40oxj/bob/identity.json
Share your contact file: pqmsg export-contact --output /tmp/bob.pub
alice_pub = workdir / "alice.pub"
bob_pub = workdir / "bob.pub"
run(alice_home, "export-contact", "--output", str(alice_pub))
run(bob_home, "export-contact", "--output", str(bob_pub))
run(alice_home, "import-contact", str(bob_pub), "--as", "bob")
run(bob_home, "import-contact", str(alice_pub), "--as", "alice")
print("contacts exchanged")
contacts exchanged
for i in range(5):
msg = f"round {i}: hello from alice"
r = run(alice_home, "send", "bob", msg)
print("ALICE:", r.stdout.strip())
r = run(bob_home, "recv")
print("BOB: ", r.stdout.strip())
ALICE: sent message #0 to bob (1827 bytes)
BOB: [from alice, msg #0]: round 0: hello from alice
ALICE: sent message #1 to bob (335 bytes)
BOB: [from alice, msg #1]: round 1: hello from alice
ALICE: sent message #2 to bob (335 bytes)
BOB: [from alice, msg #2]: round 2: hello from alice
ALICE: sent message #3 to bob (335 bytes)
BOB: [from alice, msg #3]: round 3: hello from alice
ALICE: sent message #4 to bob (335 bytes)
BOB: [from alice, msg #4]: round 4: hello from alice
Each “ALICE” line advances the Alice-to-Bob chain key; each “BOB” line advances Bob’s Alice-to-Bob receive chain. Both sides stay in lockstep.#
print("--- Alice session state ---")
print(run(alice_home, "show-keys", "bob").stdout)
print("--- Bob session state ---")
print(run(bob_home, "show-keys", "alice").stdout)
--- Alice session state ---
peer: bob
send_index: 5
recv_index: 0
chain_key_send (first 8 bytes): 94bb948fa3c7e2d7
chain_key_recv (first 8 bytes): 7bfd97682b67aa32
--- Bob session state ---
peer: alice
send_index: 0
recv_index: 5
chain_key_send (first 8 bytes): 7bfd97682b67aa32
chain_key_recv (first 8 bytes): 94bb948fa3c7e2d7
shutil.rmtree(workdir, ignore_errors=True)
print("cleaned", workdir)
cleaned /tmp/pqmsg_nb04_aie40oxj
What you just saw#
Two independent OS processes, each with its own
~/.pq-messengerdirectory.A shared file-queue “network”.
Five encrypted-and-decrypted round trips with a single hybrid X3DH handshake up front.
Monotonically advancing send/recv indices — the symmetric ratchet in action.
For a real deployment you’d want the full Double Ratchet (DH re-keying), proper mutual authentication (signatures + published prekeys), and a server for offline delivery. Those are the next levels of complexity, intentionally left for the reader.