Skip to content

rel-s/stringphone

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

6 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

🥫🪢🥫 stringphone

Send messages, secrets & tokens, and small files effortlessly and seamlessly directly from the terminal via e2e encrypted, ephemeral messaging over a metadata-blind relay. Data is encrypted on the client using your existing SSH ed25519 keys, and kept on the server only for a few minutes. The server cannot read messages, identify participants, or determine who is talking to whom.

demo

Install

brew tap rel-s/stringphone
brew install stringphone

Quick start

# Pair with Bob
stringphone pair bob --github bobs-github-account # or, use the key directly "ssh-ed25519 AAAA..."
alias bob="stringphone bob"

# Alice sends a message
echo "SECRET_TOKEN: x-123..." | bob

# Receive anything from bob
bob > ~/.zshrc.bobs-config

CLI usage

stringphone <peer>                      # auto: send if stdin piped, recv otherwise
stringphone --send <peer>               # explicit send (reads stdin)
stringphone --recv <peer>               # explicit recv (writes to stdout)
stringphone pair <name> <pubkey>        # pair with a public key string
stringphone pair <name> --github <user> # pair via GitHub public keys
stringphone peers                       # list paired peers

Auto-detection: if stdin is piped, stringphone sends. If stdin is a terminal, it receives. Use --send/--recv to override.

Pairing

Pairing stores a peer's SSH ed25519 public key locally. You can pair by providing the key directly or by importing from GitHub:

# Direct
stringphone pair alice "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI..."

# From GitHub (uses first ed25519 key found at github.com/<user>.keys)
stringphone pair alice --github alice-github-username

Peer keys are stored as files in ~/.config/stringphone/peers/<name>.pub.

Configuration

Config file: ~/.config/stringphone/config.toml

# Relay server URL
server = "https://relay.example.com"

# Path to your SSH ed25519 private key
identity_key = "~/.ssh/id_ed25519"

All fields are optional. Defaults:

Field Default Description
server https://relay.string-phone.com Relay server URL
identity_key ~/.ssh/id_ed25519 SSH private key path (~ is expanded)
verbose false Enable verbose stderr logging

Config directory: ~/.config/stringphone/ on all platforms.

Server

You don't have to run the server yourself - there's an online server maintained by me configured as the default.

The server is limited by default to 5 KiB max message size, and a 5m TTL after which messages are deleted.

API

Endpoint Method Description
POST /mailbox/:id POST Store encrypted message (raw bytes)
GET /mailbox/:id GET Retrieve message (raw bytes, empty if none)
GET /health GET Returns {"status":"ok"}

Mailbox IDs are 64-character hex strings. POST returns 201, 413 (too large), 429 (mailbox full), or 400 (bad ID). GET returns the raw message bytes (empty body if no message). No authentication headers are required — mailbox IDs are unguessable because they include the DH shared secret.


Cryptography

Overview

stringphone uses your existing SSH ed25519 keys to establish a shared secret with each peer via X25519 Diffie-Hellman. Messages are encrypted with ChaCha20-Poly1305. The relay server handles only opaque identifiers and ciphertext.

Protocol in detail (post-pairing)

Step 1: Shared secret

Both parties compute the same shared secret independently:

Alice:  shared = X25519(alice_private, bob_public)
Bob:    shared = X25519(bob_private, alice_public)
→ Both produce the same 32-byte shared secret

This is a standard ECDH exchange. Neither party transmits their private key.

Step 2: Mailbox ID

mailbox_id = BLAKE3(shared_secret || sender_pubkey || recipient_pubkey)

Mailbox IDs are directional: alice→bob ≠ bob→alice.

Because the DH shared secret is included in the hash, an observer who knows both public keys cannot discover the mailbox. This makes sure no third party can overwrite the mailbox contents (even with invalid data).

Step 3: Encryption key

enc_key = HKDF-SHA256(
    ikm  = shared_secret,
    salt = mailbox_id,
    info = "stringphone-v1-msg"
)

The mailbox ID is used as salt, binding the key to the specific sender→recipient direction.

Step 4: Encrypt

nonce     = 12 random bytes
envelope  = [8-byte timestamp] [plaintext]
ciphertext = ChaCha20-Poly1305(key=enc_key, nonce=nonce, plaintext=envelope)

ChaCha20-Poly1305 provides authenticated encryption: the ciphertext is confidential and tamper-proof. The 16-byte Poly1305 tag ensures any modification is detected on decryption.

The timestamp allows us to protect against replay attacks. We keep track of the hashes of last seen messages in ~/.config/stringphone/seen; this way we can reject identical message hashes, and reject messages with old timestamps. Rejecting old timestamps allows us to prune the last seen message hashes.

Wire format

[0x01] [12-byte nonce] [ciphertext + 16-byte tag]
  ^version

Total overhead: 37 bytes per message (1 version + 12 nonce + 8 timestamp + 16 auth tag).

What the server knows

The relay server sees:

  • Mailbox IDs: opaque 32-byte hashes. The server uses them as routing keys.
  • Ciphertext blobs: opaque encrypted bytes. The server stores and forwards them.
  • Timing: when messages are posted and retrieved.
  • Size: ciphertext length (plaintext length + 29 bytes overhead).
  • IP addresses: client IPs connecting to the server. Can be hidden using VPNs/Tor/so on.

Troubleshooting

"only ed25519 keys are supported"

stringphone requires ed25519 SSH keys. RSA and ECDSA keys cannot be used because the protocol relies on X25519 Diffie-Hellman, which requires Curve25519 keys. If you only have an RSA key, generate an ed25519 key alongside it:

ssh-keygen -t ed25519

Your existing RSA key continues to work for SSH.

About

E2E encrypted ephemeral CLI messenger

Resources

Stars

Watchers

Forks

Packages