mirror of
https://github.com/sudosylabs/vnidrop.git
synced 2026-08-11 05:09:55 +02:00
525 lines
21 KiB
Markdown
525 lines
21 KiB
Markdown
# Design — Saved devices and targeted transfers
|
|
|
|
Status: **proposed for the experimental 0.3.x line**.
|
|
|
|
This document supersedes the previous device-history design. The implementation
|
|
currently on `feat/device-history` is an unreleased prototype. Its database and
|
|
wire formats are not compatibility commitments and may be replaced.
|
|
|
|
The feature lets two VniDrop installations remember one another after a
|
|
successful transfer, with explicit consent on both devices. A saved device can
|
|
then request a new transfer without another invitation, QR scan, or NFC tap.
|
|
The receiver must still approve every transfer.
|
|
|
|
The Rust core, protocol, persistence, credential-storage integration, and
|
|
platform contracts are the first delivery scope. Product UI is intentionally
|
|
deferred to a separate design and implementation session.
|
|
|
|
---
|
|
|
|
## 1. Vocabulary and invariants
|
|
|
|
### Saved device
|
|
|
|
A `SavedDevice` is a remote VniDrop **app-installation identity**. It is not a
|
|
person, account, address-book contact, or reliably identifiable piece of
|
|
physical hardware.
|
|
|
|
The identity is the remote iroh endpoint identity. A reinstall or unrecoverable
|
|
endpoint-key loss creates a new identity and requires a new successful transfer
|
|
and mutual consent. Display names, platform hints, IP addresses, and physical
|
|
device properties must never merge identities.
|
|
|
|
### Device relationship
|
|
|
|
A `DeviceRelationship` is a mutually acknowledged relationship between two
|
|
saved-device identities. It contains two directional grants: one issued in
|
|
each direction. The relationship is usable only after both grants have been
|
|
acknowledged.
|
|
|
|
### Targeted transfer
|
|
|
|
A `TargetedTransfer` is an immutable one-sender, one-receiver transfer. It is a
|
|
separate domain from the existing invitation-based `Share`, which may serve
|
|
multiple receivers.
|
|
|
|
The following invariants are mandatory:
|
|
|
|
- Saving a device requires a fully completed authenticated transfer and
|
|
explicit consent on both devices.
|
|
- Remembering a device never authorizes automatic receipt. Every targeted
|
|
transfer requires explicit receiver approval.
|
|
- A targeted transfer has exactly one sender identity, one receiver identity,
|
|
one transfer ID, and one immutable manifest.
|
|
- Authorization is bound to the selected receiver. A leaked capability or
|
|
ticket must not authorize any other identity.
|
|
- Relays may forward end-to-end encrypted traffic according to the active
|
|
network profile, but VniDrop has no intermediary file store, relationship
|
|
service, delivery queue, push service, or account system.
|
|
- Existing invitation-based transfers retain their current behavior and domain
|
|
model.
|
|
|
|
---
|
|
|
|
## 2. Goals and non-goals
|
|
|
|
### Goals
|
|
|
|
- Send to a previously saved device without exchanging another invitation.
|
|
- Make mutual consent cryptographically enforceable rather than a UI promise.
|
|
- Keep receiver approval mandatory for each new transfer.
|
|
- Give forget, revoke, block, cancellation, and deletion immediate local
|
|
security effect even when the peer is offline.
|
|
- Persist accepted interrupted transfers so they can resume when both devices
|
|
are online again.
|
|
- Protect endpoint identity keys and relationship secrets with platform-backed
|
|
credential storage.
|
|
- Provide versioned, typed Rust and UniFFI contracts that every platform can
|
|
exercise before UI work begins.
|
|
|
|
### Non-goals
|
|
|
|
- Automatic acceptance or unattended writes to a receiver's device.
|
|
- Offline store-and-forward, automatic peer polling, background inboxes, or
|
|
push notifications.
|
|
- Server-side device discovery, relationship storage, history synchronization,
|
|
backup, export, or restoration onto another installation.
|
|
- Presence indicators or a promise that a suspended mobile application is
|
|
reachable.
|
|
- Groups or a multi-recipient variant of `TargetedTransfer`.
|
|
- Associating several saved devices with a person or account.
|
|
- UI screens, navigation, wording, and presentation architecture in this phase.
|
|
|
|
---
|
|
|
|
## 3. Network and privacy model
|
|
|
|
Saved-device operations use the same configured iroh network profile as
|
|
ordinary transfers:
|
|
|
|
- `Automatic` may use configured/default relays and direct paths.
|
|
- Custom-relay modes remain restricted to their configured relays and fallback
|
|
policy.
|
|
- `LocalOnly` must not silently enable public discovery or a relay.
|
|
|
|
An endpoint ID authenticates a peer; it is not, by itself, a routable address.
|
|
Address discovery and file transport may use a relay. VniDrop and the endpoints
|
|
still provide end-to-end authentication and encryption, so the relay cannot
|
|
decrypt content or authorize a recipient. A relay may observe transport
|
|
metadata such as network addresses, timing, and volume. VniDrop must not claim
|
|
that relayed traffic is anonymous, metadata-free, or relay-free.
|
|
|
|
VniDrop does not upload a transfer for later delivery. The sender and receiver
|
|
cores must both be reachable while an offer is negotiated. A relay cannot wake
|
|
a terminated or suspended application. The first release therefore reports a
|
|
typed unavailable or timeout result when the receiver's core cannot answer.
|
|
|
|
Current direct address candidates may be exchanged over an authenticated
|
|
connection and cached for the connection or a short local lifetime. The app
|
|
must not accumulate a historical IP-address log.
|
|
|
|
---
|
|
|
|
## 4. Identity and credential custody
|
|
|
|
The endpoint private key and all relationship capability secrets are protected
|
|
by platform-backed credential storage:
|
|
|
|
| Platform | Required protection |
|
|
|---|---|
|
|
| Apple | Keychain with a non-synchronizing, device-appropriate accessibility class |
|
|
| Android | Keystore-backed encryption; only ciphertext may live outside Keystore |
|
|
| Windows | DPAPI scoped to the current user |
|
|
| Linux | Secret Service/libsecret |
|
|
|
|
There is no plaintext fallback.
|
|
|
|
Rust owns identity use, cryptographic operations, relationship state, and
|
|
authorization. Platforms provide a narrow secure-secret-store adapter. Public
|
|
bindings exchange opaque handles and typed outcomes, never raw grants, pairing
|
|
tokens, or private keys.
|
|
|
|
If the endpoint identity key is temporarily unavailable, networking is
|
|
temporarily unavailable because VniDrop cannot authenticate as the same
|
|
endpoint. If the endpoint key is available but relationship grants are not,
|
|
ordinary invitation transfers remain available while saved-device operations
|
|
fail closed. Neither case may generate a replacement identity automatically.
|
|
|
|
### 4.1 Legacy endpoint-key migration
|
|
|
|
Migration of an existing endpoint key must be recoverable:
|
|
|
|
1. Read the legacy key.
|
|
2. Write it to protected storage.
|
|
3. Read it back and prove that it derives the same endpoint ID.
|
|
4. Commit a storage-version marker.
|
|
5. Only then remove the legacy copy.
|
|
|
|
A crash at any step must preserve at least one valid copy and must not change
|
|
the endpoint identity. Confirmed unrecoverable loss or an explicit identity
|
|
reset is required before replacement.
|
|
|
|
Secrets must not synchronize through platform cloud backup. Restored metadata
|
|
without its device-bound secrets reconciles to disabled relationships, never a
|
|
cloned identity.
|
|
|
|
---
|
|
|
|
## 5. Pairing eligibility
|
|
|
|
Only a **fully completed authenticated transfer** creates pairing eligibility.
|
|
A handshake, partial download, failed export, cancellation, decline, or failed
|
|
transfer does not qualify. Either the sender or receiver may initiate pairing
|
|
after a qualifying transfer.
|
|
|
|
During the qualifying transfer, the peers establish a cryptographic,
|
|
single-use pairing eligibility capability bound to:
|
|
|
|
- Both endpoint identities.
|
|
- The qualifying transfer/session.
|
|
- The saved-device protocol version.
|
|
- A 24-hour local expiry.
|
|
|
|
The capability becomes usable only after the transfer reaches its durable
|
|
completed state. It is stored locally in encrypted form without filenames or a
|
|
transfer-history record. It is deleted when consumed, declined, expired,
|
|
forgotten, blocked, or reset.
|
|
|
|
Requests without valid eligibility are silently rejected. This prevents a
|
|
modified stranger from generating unsolicited pairing prompts.
|
|
|
|
---
|
|
|
|
## 6. Mutual-consent protocol
|
|
|
|
The protocol uses explicit pending states rather than exposing partial contacts
|
|
as usable saved devices:
|
|
|
|
- `PendingOutgoing`
|
|
- `PendingIncoming`
|
|
- `Saved`
|
|
|
|
The normal exchange is:
|
|
|
|
1. Alice locally chooses to remember Bob after a qualifying transfer.
|
|
2. Alice sends a token-bound pairing request.
|
|
3. Bob explicitly consents.
|
|
4. Alice and Bob exchange fresh directional grants.
|
|
5. Alice acknowledges Bob's grant.
|
|
6. Both sides activate the relationship as `Saved` only after the mutual
|
|
exchange is acknowledged.
|
|
|
|
Failure before activation remains a bounded pending operation and cannot be
|
|
used to initiate a transfer. Pending operations expire and are recoverable or
|
|
cleaned after crashes.
|
|
|
|
If both devices initiate simultaneously, the protocol deterministically merges
|
|
the attempts using the endpoint identities and the transfer-bound eligibility
|
|
capability. It creates one relationship and one active grant per direction,
|
|
without duplicate prompts or rows.
|
|
|
|
Declining consumes the eligibility for that qualifying transfer. It cannot
|
|
prompt again. A later completed transfer may establish new eligibility, but
|
|
another request still requires fresh local initiation.
|
|
|
|
---
|
|
|
|
## 7. Directional grants
|
|
|
|
Each direction has one active, high-entropy capability bound to:
|
|
|
|
- Issuer endpoint identity.
|
|
- Holder endpoint identity.
|
|
- Relationship generation.
|
|
- Minimum negotiated protocol generation.
|
|
|
|
Proof uses the authenticated iroh channel plus established, domain-separated
|
|
cryptographic primitives, challenge binding, and replay protection. Display
|
|
names, addresses, and transfer IDs alone are never authentication. The protocol
|
|
must have independent, reviewable test vectors.
|
|
|
|
Relationships do not expire merely through inactivity. They remain until
|
|
forget, block, explicit revocation, identity loss, or reset. Long-unseen devices
|
|
may later be represented as inactive by UI, but inactivity does not silently
|
|
remove permission.
|
|
|
|
Activating a replacement grant first makes the prior relationship generation
|
|
locally invalid. Exactly one generation is active per direction. Minimal
|
|
non-secret revocation tombstones are retained for as long as an old generation
|
|
could otherwise be replayed; tombstones contain no names, filenames, transfer
|
|
history, or capability material.
|
|
|
|
An established relationship records its minimum supported protocol generation
|
|
and must never silently downgrade below it.
|
|
|
|
---
|
|
|
|
## 8. Forget, block, and identity replacement
|
|
|
|
### Forget
|
|
|
|
Forget makes the local relationship and its grants unusable immediately,
|
|
cancels active or resumable targeted transfers for that relationship, removes
|
|
relationship secrets and metadata, and sends a signed/bound best-effort remote
|
|
revocation when possible. Correctness never depends on remote delivery.
|
|
|
|
An independently approved invitation transfer already in progress may continue
|
|
because it belongs to the existing share domain.
|
|
|
|
### Block
|
|
|
|
Block is identity-wide and immediate. It rejects or cancels current and future
|
|
traffic from the blocked endpoint across:
|
|
|
|
- Pairing and grant operations.
|
|
- Targeted offers and transfers.
|
|
- Ordinary invitation handshakes and transfers.
|
|
- Revocation and probing endpoints, except for indistinguishable rejection
|
|
needed to avoid exposing block state.
|
|
|
|
Blocking deletes active relationship grants but retains the minimal identity
|
|
deny record and replay tombstones. Unblocking removes only the deny rule. It
|
|
does not restore grants, relationships, or cancelled transfers. Saving the
|
|
device again requires another qualifying transfer and fresh mutual consent.
|
|
|
|
A peer reinstall produces a new endpoint identity. It is never linked to the
|
|
old device by name, address, or platform. The old saved entry remains
|
|
unavailable until forgotten; the new identity follows the complete first-
|
|
transfer and consent flow.
|
|
|
|
---
|
|
|
|
## 9. Targeted-transfer model
|
|
|
|
`TargetedTransfer` is not an access mode on an ordinary share. It has its own
|
|
protocol types, repository records, authorization rules, and public APIs.
|
|
Internal blob storage, import, hashing, streaming, and output-sink machinery may
|
|
be reused.
|
|
|
|
The following fields are immutable after creation:
|
|
|
|
- Transfer ID.
|
|
- Sender endpoint identity.
|
|
- Receiver endpoint identity.
|
|
- Manifest identity and content hashes.
|
|
- File count and total size.
|
|
|
|
Sending identical content to several saved devices creates independent
|
|
targeted transfers. Internal blobs may be deduplicated, but approval, progress,
|
|
cancellation, retry, authorization, and durable state remain independent.
|
|
|
|
The durable state machine is:
|
|
|
|
```text
|
|
Preparing -> Offering -> AwaitingApproval -> Approved -> Connecting
|
|
-> Transferring -> Completed
|
|
\-> Interrupted -> Connecting
|
|
|
|
Terminal alternatives: Declined, Cancelled, Failed, Deleted
|
|
```
|
|
|
|
Rust centrally validates transitions. Platform code invokes typed operations
|
|
and consumes snapshots/events; it cannot fabricate states.
|
|
|
|
---
|
|
|
|
## 10. Offer and approval protocol
|
|
|
|
An offer is online-only and bounded:
|
|
|
|
1. The sender creates an immutable targeted transfer for one saved-device
|
|
identity.
|
|
2. The peers authenticate the saved relationship and negotiate the targeted-
|
|
transfer protocol version.
|
|
3. The sender submits a bounded offer containing a stable transfer ID and an
|
|
authenticated manifest summary, but no reusable ordinary-share ticket.
|
|
4. The receiver validates all framing, limits, identity bindings, relay-policy
|
|
compatibility, and manifest claims before surfacing approval.
|
|
5. The receiver explicitly approves or declines.
|
|
6. On approval, the sender issues authorization bound to the exact transfer,
|
|
manifest, and receiver endpoint.
|
|
7. The receiver pulls the content through the existing safe streaming and
|
|
output-sink machinery.
|
|
|
|
The approved authorization covers the exact manifest, content hashes, sizes,
|
|
sender, receiver, transfer ID, and protocol generation. Any mismatch or content
|
|
mutation invalidates the transfer and requires a new transfer ID and approval.
|
|
A leaked capability must fail when presented by another endpoint.
|
|
|
|
Every operation is idempotent. Replaying the same pairing request, offer,
|
|
approval, acknowledgement, cancellation, or completion returns the existing
|
|
result and cannot create duplicate prompts, grants, authorizations, or rows.
|
|
|
|
Declining rejects only that transfer. It neither forgets nor blocks the sender.
|
|
|
|
---
|
|
|
|
## 11. Online, interruption, and deletion semantics
|
|
|
|
An unapproved offer exists only in a bounded live-session queue. Sender
|
|
cancellation, decline, timeout, disconnect, or core restart removes it. There
|
|
is no sender-held offline offer, receiver polling loop, background inbox, or
|
|
automatic retry that can produce a later prompt.
|
|
|
|
After approval, the transfer and its recipient-scoped authorization are
|
|
durable. Interruption retains verified progress and may resume when both devices
|
|
are online again. Resuming the same immutable transfer does not request another
|
|
approval. Changed content or metadata requires a new transfer.
|
|
|
|
Cancellation before approval withdraws the offer. Cancellation after approval
|
|
stops authorization and active streaming synchronously before asynchronous
|
|
cleanup. It affects only that transfer.
|
|
|
|
Deletion must make authorization unusable, stop content service for that
|
|
transfer, remove resumable state, and clean related secrets. Remote cleanup is
|
|
best-effort; immediate durable local denial is mandatory.
|
|
|
|
Several separately approved targeted transfers may run concurrently between
|
|
the same devices under existing global stream and resource limits.
|
|
|
|
---
|
|
|
|
## 12. Local data and consistency
|
|
|
|
The private application database may contain only the relationship and transfer
|
|
metadata needed for the feature, including:
|
|
|
|
- Endpoint identity/public identifier.
|
|
- User-owned local label and untrusted platform/name hints.
|
|
- Pending/saved/blocked/revoked state and state revision.
|
|
- Opaque secure-store handles.
|
|
- Protocol and relationship generation.
|
|
- Last successful authenticated contact time.
|
|
- Minimal replay and revocation tombstones.
|
|
- Durable targeted-transfer state after approval.
|
|
|
|
It must not become a transfer-history log. Pairing does not justify retaining
|
|
filenames, previous IP addresses, or lists of past transfers.
|
|
|
|
Credential-store and SQLite updates cannot share a native transaction. Use
|
|
recoverable staged transitions:
|
|
|
|
1. Write secret material under a versioned opaque handle.
|
|
2. Verify the protected write.
|
|
3. Commit metadata referencing that handle in a non-active state.
|
|
4. Finalize activation.
|
|
|
|
Startup reconciliation removes orphaned secrets and disables metadata whose
|
|
required secrets are missing. Revocation becomes locally effective before any
|
|
network notification. Relationship mutations are serialized per remote
|
|
endpoint, while unrelated devices proceed concurrently. Database, relationship,
|
|
and credential-store guards must never be held across network awaits.
|
|
|
|
---
|
|
|
|
## 13. Core and platform contract
|
|
|
|
The Rust core exposes separate typed models and operations for:
|
|
|
|
- Pairing eligibility and pending pairing requests.
|
|
- Listing, renaming, forgetting, blocking, and unblocking saved devices.
|
|
- Creating and submitting targeted transfers.
|
|
- Approving, declining, cancelling, resuming, and deleting transfers.
|
|
- Querying durable state and current capability availability.
|
|
- Subscribing to typed events carrying stable IDs and monotonic state revisions.
|
|
|
|
Bindings must not expose raw secrets or generic state mutation. Events are
|
|
wake-up notifications, not authoritative storage. They may be delivered at
|
|
least once; consumers deduplicate by stable ID and revision, then query current
|
|
state after reconnect or restart.
|
|
|
|
Failures remain typed where callers can act differently, including:
|
|
|
|
- Device unavailable or offer timeout.
|
|
- Protocol incompatibility or forbidden downgrade.
|
|
- Revoked or blocked relationship.
|
|
- Relay-policy incompatibility.
|
|
- Secure storage locked, unavailable, missing, or corrupted.
|
|
- Approval decline, cancellation, interruption, and invalid transition.
|
|
|
|
Production errors and diagnostics must not expose endpoint IDs, direct
|
|
addresses, tickets, grants, pairing capabilities, filenames, or secret-store
|
|
payloads.
|
|
|
|
---
|
|
|
|
## 14. Limits and hostile-peer handling
|
|
|
|
A saved relationship proves a remote app identity and permits it to request
|
|
approval. It does not make remote metadata, filenames, paths, sizes, messages,
|
|
or content trusted.
|
|
|
|
The feature reuses all existing filesystem safety, output-sink, no-overwrite,
|
|
ticket validation, and resource-limit invariants. Before approval it also
|
|
enforces:
|
|
|
|
- One unresolved offer per sender identity.
|
|
- A bounded global pending-offer queue.
|
|
- Strict request, manifest, metadata, file-count, and size limits.
|
|
- Connection, pairing, offer, approval, and acknowledgement timeouts.
|
|
- Per-identity cooldown after repeated malformed traffic or declines.
|
|
- Silent rejection of unauthenticated, ineligible, blocked, or invalid traffic.
|
|
- A configurable `CoreLimits.max_saved_devices`, defaulting to 256.
|
|
|
|
These are control-plane and local-resource protections. They do not impose a
|
|
quota on accepted transfers, files, bytes, or bandwidth.
|
|
|
|
VniDrop cannot protect against a compromised or unlocked endpoint, malicious
|
|
files the receiver knowingly accepts, operating-system credential compromise,
|
|
network traffic analysis, or a reinstalled peer appearing under a new identity.
|
|
|
|
---
|
|
|
|
## 15. Compatibility and release policy
|
|
|
|
Saved devices and targeted transfers use explicit, versioned protocol
|
|
capabilities. A peer without compatible support cannot be paired or receive a
|
|
targeted transfer and falls back to the existing invitation flow. A targeted
|
|
transfer must never be reinterpreted as an ordinary share for compatibility.
|
|
|
|
The feature is gated as experimental in the 0.3.x line. The wire protocol is
|
|
versioned from its first merge. Removing the experimental gate requires:
|
|
|
|
- Stable migrations from every released database version.
|
|
- Compatible Apple, Android, Windows, and Linux credential-store adapters.
|
|
- Rust and platform contract coverage.
|
|
- Stable downgrade, revocation, recovery, and lifecycle behavior.
|
|
- No regression in invitation-based multi-recipient transfers.
|
|
|
|
The unreleased `feat/device-history` schema, held offers, polling behavior,
|
|
expiring grants, `Contact` terminology, Apple-only feature UI, and ordinary-
|
|
share offer authorization are prototype artifacts. They may be removed without
|
|
a migration. Useful low-level cryptographic, repository, protocol, and test
|
|
patterns may be retained only after they are checked against this design.
|
|
|
|
---
|
|
|
|
## 16. Verification requirements
|
|
|
|
Rust tests must deterministically cover:
|
|
|
|
- Mutual consent, decline, simultaneous initiation, timeouts, and lost
|
|
acknowledgements.
|
|
- Pairing eligibility after completion and rejection after every non-completed
|
|
outcome.
|
|
- Replay, malformed input, spoofed identity, blocking, revocation, grant
|
|
rotation, and protocol downgrade.
|
|
- Recipient-bound authorization and rejection of leaked capabilities.
|
|
- Direct, relay, custom-relay, local-only, and incompatible-profile behavior.
|
|
- Restart and recovery at every durable state.
|
|
- Cancellation, deletion, forget, and block during active streaming.
|
|
- Credential-store failure and crash-point reconciliation.
|
|
- Concurrent independent targeted transfers.
|
|
- Existing invitation-based multi-recipient behavior remaining unchanged.
|
|
|
|
Each platform secure-storage adapter requires contract coverage for create,
|
|
read, update, delete, locked/unavailable behavior, migration, device-bound
|
|
persistence, orphan cleanup, and redaction. Platform harnesses must prove that
|
|
secrets do not appear in generated bindings, logs, diagnostics, or ordinary
|
|
database columns.
|
|
|
|
The core/platform foundation is complete only when these contracts are
|
|
implemented, documented, exposed through typed UniFFI APIs, and pass the
|
|
relevant Rust and platform checks. UI polish is not part of that completion
|
|
boundary.
|