Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ env_logger = "0.11.10"
log = "0.4"

# JSON
base64 = "0.22"
serde = { version = "1", features = ["derive"] }
serde_json = { version = "1" }

Expand Down
19 changes: 18 additions & 1 deletion PRIVACY.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# OpenTubeX Sync Server Privacy Policy

Last updated: July 23, 2026
Last updated: August 26, 2026

This policy applies to the public OpenTubeX sync server at
[sync.d3sox.me](https://sync.d3sox.me). Other operators running this
Expand Down Expand Up @@ -30,6 +30,23 @@ never receives the passphrase or plaintext. The operator can still observe
account activity, request timing, collection names, and approximate data size.
The server cannot recover a lost privacy passphrase.

**Device pairing.** Secure device pairing temporarily stores a one-time session
ID, SHA-256 recipient-token hash, recipient public key, pairing-scoped device
identifiers, the receiving device's user-chosen display name, expiry time, and
an encrypted pairing payload. It adds the account ID when an authenticated
device claims the session. Sessions expire after two minutes and are deleted
when they are consumed or cancelled. Poll, consume, and cancel requests send
the raw recipient token in a request header; the server stores only its hash.
The server never receives the QR-only pairing secret, recipient private key,
privacy key, privacy passphrase, or login password. It creates a fresh
authentication token for the receiving device during the claim and therefore
knows that token. The approving device places the token inside the encrypted
relay payload together with the account name, privacy key, privacy salt, and
six-digit verification code. The server can drop or overwrite that ciphertext,
but it cannot decrypt it or forge a valid replacement without the QR-only
secret. A background task deletes expired sessions, normally within 30 seconds
after their two-minute expiry.

**Legacy compatibility.** The server retains plaintext endpoints for older or
non-OpenTubeX clients. Current OpenTubeX clients do not use them on this public
server. A client using these endpoints may send readable subscriptions, groups,
Expand Down
39 changes: 39 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,45 @@ for profiles, playback speeds, and sessions, 16 MiB for subscriptions and playli
bookmarks, and 64 MiB for playlists and history. The combined active encrypted
collections for one account cannot exceed 128 MiB.

### Secure device pairing

Capability `key_pairing: 1` advertises passwordless device pairing for
enhanced-privacy sync. A receiving device anonymously creates a pending
session, then shows a QR or text code. An already authenticated device claims
the session for its account and approves it. During the claim, the server mints
a fresh JWT for the receiving device. The approving device encrypts that JWT,
the account name, privacy key, privacy salt, and a six-digit verification code
before uploading one opaque relay payload.

The server stores the session ID, SHA-256 recipient-token hash, recipient public
key, pairing-scoped device IDs, receiving-device display name, expiry, account
ID after claim, and approved ciphertext. It never receives the QR-only secret,
recipient private key, privacy key, or privacy passphrase. Poll, consume, and
cancel requests send the raw recipient token in a request header; the server
stores only its hash. The server created the fresh JWT and therefore knows that
token. It can drop or overwrite the encrypted transfer, but it cannot decrypt
it or forge a valid replacement without the QR-only secret.

Sessions expire after two minutes, and a background task normally deletes them
within another 30 seconds. The server permits at most 10,000 active pairing
sessions globally and five claimed sessions per account. Authenticated pairing
requests are limited to 120 per account per minute, while anonymous creation
uses the server's address-based request limiter. Claim and approval accept an
identical retry after success. Consumption atomically returns and deletes the
ciphertext, and cancellation deletes the session.

The endpoints are:

- anonymous `POST /v1/pairing` to create a session with a recipient-token hash
- recipient-token `GET /v1/pairing/{id}` to inspect its metadata and state
- authenticated `POST /v1/pairing/{id}/claim` to bind it to an account and mint a fresh JWT
- authenticated `PUT /v1/pairing/{id}` to approve it with an opaque ciphertext
- recipient-token `POST /v1/pairing/{id}/consume` to atomically consume it
- recipient-token `DELETE /v1/pairing/{id}` to cancel it

The protocol, threat model, fixed serialization, and interoperability vector
live in the OpenTubeX client repository at `docs/sync-key-pairing-v1.md`.

## Development

### Running
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DROP TABLE pairing_session;
16 changes: 16 additions & 0 deletions migrations/postgres/2026-08-26-000000-0000_key_pairing/up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
CREATE TABLE pairing_session(
id VARCHAR PRIMARY KEY NOT NULL,
version SMALLINT NOT NULL,
account_id VARCHAR,
recipient_public_key VARCHAR NOT NULL,
recipient_device_id VARCHAR NOT NULL,
recipient_device_name VARCHAR NOT NULL,
recipient_token_hash VARCHAR NOT NULL,
approving_device_id VARCHAR,
encrypted_payload TEXT,
expires_at BIGINT NOT NULL,
CONSTRAINT FK__pairing_session__account FOREIGN KEY(account_id) REFERENCES account(id) ON DELETE CASCADE
);

CREATE INDEX pairing_session_account_expires_idx
ON pairing_session(account_id, expires_at);
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DROP TABLE pairing_session;
16 changes: 16 additions & 0 deletions migrations/sqlite/2026-08-26-000000-0000_key_pairing/up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
CREATE TABLE pairing_session(
id VARCHAR PRIMARY KEY NOT NULL,
version SMALLINT NOT NULL,
account_id VARCHAR,
recipient_public_key VARCHAR NOT NULL,
recipient_device_id VARCHAR NOT NULL,
recipient_device_name VARCHAR NOT NULL,
recipient_token_hash VARCHAR NOT NULL,
approving_device_id VARCHAR,
encrypted_payload TEXT,
expires_at BIGINT NOT NULL,
CONSTRAINT FK__pairing_session__account FOREIGN KEY(account_id) REFERENCES account(id) ON DELETE CASCADE
);

CREATE INDEX pairing_session_account_expires_idx
ON pairing_session(account_id, expires_at);
1 change: 1 addition & 0 deletions src/database.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ pub mod account;
pub mod channel;
pub mod channel_playback_speed;
pub mod encrypted_sync;
pub mod pairing;
pub mod playlist;
pub mod playlist_bookmark;
pub mod public_playlist;
Expand Down
Loading