Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

clippio

Lightweight, secure, remote clipboard. Push text or files from one machine and pull them onto another's local clipboard, through a server that only ever sees encrypted bytes.

Payloads are end-to-end encrypted by the CLI: the server stores and returns opaque ciphertext and never holds the shared secret or plaintext. Access to the server is a separate x-api-key, and each key gets its own single-entry slot.

Layout

cmd/clippio-server   the relay (HTTP)
cmd/clippio          the CLI client
internal/model       shared ClipModel
internal/crypto      scrypt + AES-128-GCM (client only)
internal/server      in-memory per-key store + handlers
internal/config      ~/.config/clippio/config.json
internal/clipboard   local clipboard access
internal/cli         cobra commands + REPL

Build

go build ./... only compiles to the build cache and produces no files. To get runnable binaries, build the commands to a directory:

go build -o bin/ ./cmd/...   # produces bin/clippio and bin/clippio-server
go test ./...

Or install them onto your PATH ($(go env GOPATH)/bin):

go install ./cmd/...

You can also run either command directly without building:

go run ./cmd/clippio-server
go run ./cmd/clippio --help

Server

CLIPPIO_API_KEYS=key1,key2 go run ./cmd/clippio-server

Environment:

Variable Default Meaning
CLIPPIO_API_KEYS (bootstrap only) Comma-separated keys used to seed the key file the first time it is created.
CLIPPIO_KEYS_FILE clippio-keys.json Path to the persisted key file (default is in the current directory).
CLIPPIO_ONESHOT false When true, each clip is cleared once it is read.
CLIPPIO_ADDR :8080 Listen address.
CLIPPIO_CLI_PATH ./cli Directory of precompiled CLI artifacts to serve.
CLIPPIO_MAX_BYTES 10485760 (10 MiB) Max raw payload size for a paste; larger uploads get 413.

The valid x-api-key set is persisted to CLIPPIO_KEYS_FILE so rotations survive a restart. That file is authoritative: CLIPPIO_API_KEYS only seeds it when it does not yet exist, so a key removed by rotation stays gone (it will not come back from the env on the next start). To reset the key set, delete the file and restart. Clip payloads themselves stay in memory and are lost on restart.

Docker

The image builds the server and cross-compiles the full CLI matrix into /cli (GET /discover serves it): linux, darwin, and windows × amd64/arm64, packaged as .zip for windows and .tar.gz otherwise.

docker build -t clippio-server .
docker run -d -p 8080:8080 \
  -e CLIPPIO_API_KEYS=your-root-key \
  -v clippio-data:/data \
  clippio-server

The container runs as a nonroot user on a distroless base and stores the key file on the /data volume (CLIPPIO_KEYS_FILE=/data/clippio-keys.json), so CLIPPIO_API_KEYS is only needed on the first start. For multi-arch server images, build with buildx, e.g. docker buildx build --platform linux/amd64,linux/arm64 -t clippio-server ..

Local run (systemd --user)

./serve.sh <bootstrap-api-key> builds everything into serve/ and installs a --user service on port 8088 (the key is required):

serve/clippio-server            native server binary
serve/cli/                      the CLI matrix (served by /discover)
serve/data/                     persistent key file
serve/clippio-server.service    generated unit, symlinked into ~/.config/systemd/user/

Then manage it with systemd:

systemctl --user enable --now clippio-server   # start + start on login
systemctl --user status clippio-server
journalctl --user -u clippio-server -f
systemctl --user restart clippio-server        # after re-running ./serve.sh

The unit seeds serve/data/clippio-keys.json with the key you pass, on first start only — once that file exists it is authoritative, so to re-seed with a different key, delete it and restart. Everything under serve/ is disposable; re-run ./serve.sh any time. To keep the service running after you log out, loginctl enable-linger $USER.

API:

  • POST /paste — body ClipModel JSON, header x-api-key200 / 401 / 413 (payload over CLIPPIO_MAX_BYTES).
  • GET /copy — header x-api-key200 ClipModel / 404 / 401.
  • PUT /rotate — header x-api-key200 {"key": "<new>"} / 401. Replaces the caller's key with a freshly generated one, moving its stored clip to the new key. The old key stops working immediately. A prefix is preserved (see below), so jack:AAA… rotates to jack:BBB….
  • POST /invite — body {"prefix": "jack"}, header x-api-key200 {"key": "jack:<token>"} / 400 / 401 / 403. Seeds a new key under the given prefix and returns it.
  • DELETE /revoke — body {"prefix": "jack"}, header x-api-key200 {"revoked": <n>} / 400 / 401 / 403. Removes every key under the prefix (and clears their clips). Root keys only.
  • GET /discover — no auth → 200 {"files":[{"name","url","size","sha256"}, …]}. Lists the precompiled CLI artifacts in CLIPPIO_CLI_PATH with absolute download URLs and SHA-256 sums. The manifest is built once at startup.
  • GET /cli/{name} — no auth → the file bytes, or 404. Serves a single artifact. Only files present in the startup manifest are served.

Hosting the CLI

Drop precompiled tarballs into CLIPPIO_CLI_PATH (default ./cli), named however you like — the convention is clippio-<os>-<arch>.tgz:

cli/
  clippio-linux-amd64.tgz
  clippio-darwin-arm64.tgz

A new user with nothing installed can then bootstrap the client with curl:

curl -s https://clip.example.com/discover
# pick the url matching your platform, then:
curl -sO https://clip.example.com/cli/clippio-linux-amd64.tgz
# verify against the sha256 from /discover:
sha256sum clippio-linux-amd64.tgz
tar xzf clippio-linux-amd64.tgz

The manifest (names, sizes, sha256 sums) is computed once when the server starts, so add or replace artifacts, then restart to publish them. Discovery and download are unauthenticated (the artifacts are generic client binaries); everything else still requires an x-api-key.

Keys and prefixes

A key is prefixed when it contains a : — the part before it is the prefix (a label, e.g. jack). Keys without a : are root keys; the bootstrap keys from CLIPPIO_API_KEYS are root.

  • Only root keys may invite and revoke. Invited keys are always prefixed and cannot invite or revoke, giving a simple two-level hierarchy (root → named users). Revoke removes every key under a prefix at once, so a user's rotated keys are all cut off together.
  • The invite prefix must be at least 3 characters and contain no : or whitespace.
  • Rotation preserves the prefix: a prefixed key rotates to a new token under the same prefix; a root key rotates to a new root key.

The random token is base32 over 16 random bytes, truncated to 16 characters (80 bits of entropy).

Run behind TLS. Payloads are encrypted end-to-end, but the x-api-key travels in a request header, so terminate TLS in front of the server (reverse proxy) for real deployments. The server itself speaks plain HTTP.

nginx defaults to a 1 MB request body, so raise client_max_body_size or it will reject pastes with 413 before they reach clippio. Because the payload is base64-in-JSON, the HTTP body is ~1.34× the raw payload, so set it a bit above CLIPPIO_MAX_BYTES (for the 10 MiB default, ~14m):

server {
    # ...
    client_max_body_size 14m;   # ~1.34x the 10 MiB CLIPPIO_MAX_BYTES payload cap
    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host              $host;
        proxy_set_header X-Forwarded-Proto $scheme;   # so /discover emits https URLs
    }
}

CLI

Configure once (stored at ~/.config/clippio/config.json, mode 0600). Run it with no flags to be walked through every field:

$ clippio conf
clippio server url: https://clip.example.com
api key: ********
confirm api key: ********
shared secret (cipher): ************
confirm shared secret (cipher): ************
saved config to ~/.config/clippio/config.json

The url is prompted plainly; the two secrets are prompted with echo disabled and a confirmation entry, so they never land in your shell history or the process arguments. Leave any prompt blank to keep its current value.

Each field can still be supplied as a flag to skip its prompt — convenient for the url, and the only non-interactive way to set the secrets (scripting):

clippio conf --url https://clip.example.com --key <x-api-key> --cipher <secret>

A non-interactive stdin leaves any unflagged field unchanged. Any field can also be overridden per-invocation with CLIPPIO_URL, CLIPPIO_KEY, CLIPPIO_CIPHER.

Send to remote (paste):

clippio paste "some text"     # literal text
clippio paste ./report.pdf    # a file (bytes + filename)
clippio paste                 # current local clipboard contents

Fetch from remote (copy):

clippio copy

Rotate the api key (rotate):

clippio rotate

Generates a new key server-side, saves it to the local config so this machine keeps working, and copies it to the local clipboard so it can be distributed to your other machines. The key itself is not printed (only echoed as a fallback if no clipboard is available). The previous key is invalidated immediately, and a prefixed key keeps its prefix (jack:…jack:…).

Invite a new user (invite, root keys only):

$ clippio invite jack
invite key copied to clipboard

The new key is placed on your clipboard to hand to that user (echoed only if no clipboard is available); it does not change your own config. The prefix must be at least 3 characters.

Revoke a user (revoke, root keys only):

$ clippio revoke jack
revoked 2 key(s) under prefix "jack"

Removes every key under that prefix (all of jack's keys, including rotated ones) and clears their clips. Root keys have no prefix, so they are never caught by a revoke.

  • Text clips are placed on the local clipboard.
  • File clips are written to the current directory under their original filename (OS clipboards can't portably hold arbitrary file bytes).

Naming follows the flow: you paste your local clipboard to the remote, then on the other machine copy from the remote and paste locally as usual.

Interactive session (REPL)

Run clippio with no arguments:

clippio> paste "hello"
clippio> copy
clippio> help
clippio> exit

Ctrl-C cancels the current line; Ctrl-D or exit quits.

Encryption

key  = scrypt(cipher, salt, N=32768, r=8, p=1, 16 bytes)
blob = base64( salt(16) || nonce(12) || AES-128-GCM(payload) )

A random salt and nonce are generated per message and travel with the ciphertext. GCM is authenticated, so a wrong secret or a tampered payload fails copy cleanly instead of returning garbage.

Local clipboard dependency

Reading/writing the local clipboard needs a helper installed:

  • Linux/X11: xclip or xsel
  • Linux/Wayland: wl-clipboard
  • macOS/Windows: built in

File paste/copy do not touch the clipboard and work without these.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages