#!/usr/bin/env python3 """ otp_nostr_30078.py — example: encrypt data with OTP, wrap in a Nostr kind 30078 event, sign it with n_signer, and print the signed event for publishing. Workflow: 1. Call n_signer's `otp_encrypt` verb to encrypt plaintext with the bound OTP pad. 2. Build a Nostr kind 30078 (replaceable parameterized) event with the ASCII-armored ciphertext as the `content` field. 3. Call n_signer's `sign_event` verb to sign the event with the secp256k1 key. 4. Print the signed event JSON, ready to publish to Nostr relays. This is a demo — it does not actually publish to a relay. To publish, send the signed event to your preferred Nostr relay using a library like nostr-tools, nostril, or nak. Usage: python3 examples/otp_nostr_30078.py "Your secret message here" Requirements: - n_signer running with --otp-pad-dir / --otp-pad bound, and a secp256k1 role (e.g. "main") available for sign_event. - This script connects to n_signer via stdio (one process per request). See plans/otp_nostr_integration.md for the full design. """ import base64 import hashlib import json import os import struct import subprocess import sys import time NSIGNER = "./build/nsigner" PAD_DIR = "/media/user/Music/pads" PAD_SPEC = "333e9902db839d9d" MNEMONIC_FILE = ".test_mnemonic" MNEMONIC_TMP = ".test_mnemonic_otp_30078.tmp" def send_framed(proc, obj): payload = json.dumps(obj).encode() proc.stdin.write(struct.pack(">I", len(payload))) proc.stdin.write(payload) proc.stdin.flush() def recv_framed(proc): """Read a framed response, skipping any banner text on stdout.""" buf = b"" while True: b = proc.stdout.read(1) if not b: return None buf = (buf + b)[-4:] if len(buf) < 4: continue (length,) = struct.unpack(">I", buf) if 1 <= length <= 1024 * 1024: peek = proc.stdout.read(1) if peek == b"{": body = peek + proc.stdout.read(length - 1) return json.loads(body.decode()) else: buf = (buf + peek)[-4:] def run_one_request(req_obj): """Run nsigner in stdio mode for a single framed request/response.""" shell_cmd = ( f"exec 3<{MNEMONIC_TMP}; " f"exec {NSIGNER} --listen stdio --mnemonic-fd 3 " f"--otp-pad-dir {PAD_DIR} --otp-pad {PAD_SPEC} " f"--otp-allow-blkback --allow-all" ) proc = subprocess.Popen( ["bash", "-c", shell_cmd], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) import time as _time _time.sleep(1.0) if proc.poll() is not None: err = proc.stderr.read().decode() print(f"ERROR: nsigner exited early (code {proc.returncode})") print(f"stderr: {err}") return None send_framed(proc, req_obj) resp = recv_framed(proc) proc.stdin.close() try: proc.wait(timeout=5) except subprocess.TimeoutExpired: proc.kill() return resp def compute_event_id(event): """Compute the Nostr event ID (SHA-256 of the canonical serialized event).""" # Nostr event serialization: [0, pubkey, created_at, kind, tags, content] serialized = json.dumps([ 0, event["pubkey"], event["created_at"], event["kind"], event["tags"], event["content"], ], separators=(",", ":"), ensure_ascii=False) return hashlib.sha256(serialized.encode()).hexdigest() def main(): plaintext = " ".join(sys.argv[1:]) if len(sys.argv) > 1 else "Secret OTP message" print(f"Plaintext: {plaintext}") # Prepare the mnemonic temp file. with open(MNEMONIC_FILE) as f: mnemonic = f.read().strip() with open(MNEMONIC_TMP, "w") as f: f.write(mnemonic + "\n") try: # Step 1: Get the public key for the "main" role print("\n=== Step 1: get_public_key ===") resp = run_one_request({ "id": "1", "method": "get_public_key", "params": [{"role": "main"}], }) if resp is None or "result" not in resp: print("ERROR: get_public_key failed") print(f"Response: {resp}") return 1 # The result is a plain hex string for secp256k1 backward compat. pubkey_hex = resp["result"].strip('"') print(f"Public key: {pubkey_hex}") # Step 2: Encrypt the plaintext with OTP print("\n=== Step 2: otp_encrypt ===") pt_b64 = base64.b64encode(plaintext.encode()).decode() resp = run_one_request({ "id": "2", "method": "otp_encrypt", "params": [pt_b64, {"encoding": "ascii"}], }) if resp is None or "result" not in resp: print("ERROR: otp_encrypt failed") print(f"Response: {resp}") return 1 enc_result = json.loads(resp["result"]) ciphertext = enc_result["ciphertext"] pad_chksum = enc_result["pad_chksum"] pad_offset = enc_result["pad_offset_after"] print(f"Pad checksum: {pad_chksum}") print(f"Pad offset after encrypt: {pad_offset}") print(f"Ciphertext (first 60 chars): {ciphertext[:60]}...") # Step 3: Build the Nostr kind 30078 event print("\n=== Step 3: Build kind 30078 event ===") # Use a unique d-tag based on the pad checksum and offset. d_tag = f"otp-{pad_chksum[:16]}-{pad_offset}" event = { "pubkey": pubkey_hex, "created_at": int(time.time()), "kind": 30078, "tags": [ ["d", d_tag], ["otp-pad", pad_chksum[:16]], ["otp-version", "v0.0.2-otp"], ["otp-encoding", "ascii"], ], "content": ciphertext, } # Compute the event ID. event_id = compute_event_id(event) event["id"] = event_id print(f"Event ID: {event_id}") print(f"d-tag: {d_tag}") # Step 4: Sign the event with n_signer print("\n=== Step 4: sign_event ===") # sign_event expects the event JSON as the first param (without id/sig). # The signer computes the id and signature internally. event_for_signing = { "pubkey": event["pubkey"], "created_at": event["created_at"], "kind": event["kind"], "tags": event["tags"], "content": event["content"], } resp = run_one_request({ "id": "3", "method": "sign_event", "params": [json.dumps(event_for_signing), {"role": "main"}], }) if resp is None or "result" not in resp: print("ERROR: sign_event failed") print(f"Response: {resp}") return 1 sig = resp["result"].strip('"') event["sig"] = sig print(f"Signature: {sig[:60]}...") # Step 5: Print the signed event print("\n=== Signed Nostr event (ready to publish) ===") print(json.dumps(event, indent=2)) print(f"\nTo publish: send this event to a Nostr relay.") print(f"To decrypt: call otp_decrypt with the content field.") return 0 finally: try: os.unlink(MNEMONIC_TMP) except OSError: pass if __name__ == "__main__": sys.exit(main())