# MeshVault

A native Mac app and iPhone photo-backup app backed by a Go peer-to-peer storage service. Connect devices with Tailscale, choose folders on your Mac, and keep your own recovery key. Peers hold encrypted data for multiple independent users.

## Start on this Mac

1. Open **`build/MeshVault.app`**. It starts its bundled Go service.
2. Select **Create or import a user**. Save the `mv1_…` recovery key offline. It cannot be reset. The app also keeps it in this Mac's Keychain.
3. Choose **Add folder** and select the folders to sync.
4. Connect Tailscale. **Network** shows this node's address, normally `http://100.x.y.z:7443`. If Tailscale was disconnected when the service started, choose **Restart service**.
5. Run MeshVault on two more devices. Copy the first node's **network admission key** into their Network settings. Add each other's addresses and save on each node.
6. Use **File library** to check actual copy counts and restore files. The default protection target is **3/3 complete copies**.

The service checks selected folders every 30 seconds while running and unlocked. It detects changed bytes even when size and modification time stay the same. Additions, changes, and deletion markers are encrypted and replicated. Earlier versions remain available in **Version history**. Restore explicitly chooses a destination and refuses to overwrite existing files.

This is continuous backup to the network with explicit restores. It does **not** automatically apply other devices' edits to local folders. Each selected source folder has a separate identity, preventing independent devices from overwriting each other's folders. Upload never changes your source files.

## Connect an iPhone

1. Open **`apps/MeshVault.xcodeproj`** in Xcode. Select **MeshVaultPhotos** and choose your Apple development team under Signing & Capabilities. Change the bundle identifier if required by your team.
2. Build and run on your iPhone (iOS 17+). Install and connect Tailscale on the phone.
3. On the Mac: **Users & keys → Pair iPhone**. Scan the code in the phone app, or paste the connection code.
4. Grant access to all photos or a chosen selection, then tap **Back up photos**. Videos can be included. Original resources and Live Photo components are retained.

Photos are encrypted on the phone before upload. The app sends them to reachable configured nodes, commits each record after its chunks are present, and saves encrypted progress for retries. It reports uploads awaiting the full replication target. Include all storage node addresses for failover; re-pair after changing the network key or node list.

Background processing uses Apple's scheduler: iOS decides when it runs. For the initial backup, leave the app open and the phone unlocked. iCloud originals may need to download first. The app never deletes phone photos, and deleting a photo does not delete its backup. Restore photo files through the Mac's File library.

## Two different keys

| Key | Purpose | Storage |
| --- | --- | --- |
| User recovery key, `mv1_…` | Derives that user's encryption/signing keys | User's offline backup; Keychain on personal devices; unlocked client's memory |
| Network admission key, 64 hex characters | Lets approved nodes/clients communicate | Private node configuration and paired devices |

Give the network key to storage nodes. Give a user's recovery key only to that user's own devices. Storage-only nodes need no user key. Network membership cannot decrypt anybody's files. A pairing code contains both keys and must be kept private.

Multiple users have separate encrypted namespaces. Labels are local conveniences, not login accounts. A Mac with a saved key is a trusted endpoint for that user. People sharing one physical Mac should use separate macOS accounts: app profiles do not isolate people who share the same unlocked OS account.

## Node failure and recovery

Every node maintains a full encrypted mirror. The protection target counts distinct node identities with a signed record and every encrypted chunk. Surviving nodes exchange inventories and repair missing/corrupted objects every 30 seconds. A replacement node can join with the network key and peer addresses, without any user keys. Import a user's recovery key to browse and restore that user's files. No coordinator is required.

Three healthy copies can survive two permanent node losses **after replication finishes**, provided the remaining copy is intact. Losing the sole current copy before replication completes loses that data. Counts are recent observations, not real-time guarantees. Use separate physical devices; three processes on one disk do not protect against that disk dying.

## Build and test

Requirements: Go 1.26+, macOS 14+ for the Mac app, and Xcode with an iOS SDK for the phone app. The Go service has no external dependencies. XcodeGen only regenerates the checked-in project.

```sh
make check                    # go vet + race-enabled tests
make build                    # build/meshvault Go node
make mac                      # native Mac app + bundled service
bash scripts/check-crypto.sh  # Swift ↔ Go crypto interoperability
python3 scripts/smoke-test.py # real processes: upload, node death, recovery
make ios                      # regenerate Xcode project + simulator build
```

Network tests need permission to open local loopback sockets. All fixtures and test identities are synthetic.

The implementation machine's Xcode has a `DVTDownloads` / `IDESimulatorFoundation` framework mismatch that prevents `xcodebuild` from starting a simulator build. Both phone variants were compiled and linked directly with the installed compiler/SDK instead:

```sh
bash scripts/build-ios-direct.sh simulator
bash scripts/build-ios-direct.sh device
```

These produce `build/ios-simulator/MeshVault.app` and **unsigned** `build/ios-device/MeshVault.app`. Physical-device installation requires Apple provisioning/signing. Normal Xcode builds require repairing the local framework mismatch. Photos access and background execution have not been exercised on a physical iPhone here.

## Go nodes on macOS or Linux

```sh
go build -trimpath -o meshvault ./cmd/meshvault
./meshvault serve --data /path/to/private-node-data
```

The listener selects a local Tailscale address on port 7443. Explicit settings are supported:

```sh
./meshvault serve \
  --data /path/to/private-node-data \
  --listen 100.80.10.20:7443 \
  --network-key-file /path/to/private-network-key-file \
  --peers http://100.80.10.21:7443,http://100.80.10.22:7443 \
  --capacity-gb 100 --replicas 3
```

The key file contains the admission key copied from the first Mac; keep it private (`chmod 600`). Configuration is persisted in the data directory. The default quota is **100 GiB per node**, across all users; `--capacity-gb 0` explicitly removes the quota. Full nodes report an error and do not acknowledge additional copies.

The local management API binds only to `127.0.0.1` at a random port, requires a fresh token from private `connection.json`, and rejects browser-origin requests. The peer API binds only to a specific Tailscale IP. `--dev` allows loopback test nodes; no wildcard or ordinary LAN bind is accepted.

Tailscale is installed separately. Permit intended nodes and clients to reach TCP 7443 in your tailnet access rules. HTTP inside Tailscale is intentional: the VPN encrypts transport, and files have independent application encryption. No central storage server or MeshVault-operated relay exists.

## Running after login

Closing the Mac app does not stop its Go storage process. `scripts/install-login-service.sh` creates an opt-in LaunchAgent and prints how to load it after login. Stop an existing manually launched service before loading it; a data-directory lock prevents concurrent writers. The Mac app reuses an existing service.

The Go service never saves user recovery keys. After restarting it, open the Mac app to unlock saved users from Keychain before folder uploads resume. Ciphertext replication works without unlocking users. **Lock & forget on this Mac** removes the saved Keychain entry and locks the service user; keep an offline key first.

## Current boundaries

- This is an implemented prototype, not independently audited storage software.
- Full mirroring requires each storage node to fit the whole encrypted network. There is no erasure coding, selective placement, or bandwidth throttling.
- Versions and uncommitted encrypted chunks are retained. No garbage collection or pruning is implemented; plan disk space accordingly. Inventory integrity checks read stored objects, so this targets small personal networks.
- Remote file names, content, hashes, labels, and timestamps are encrypted. Peers can still observe opaque owner IDs, sizes, object counts, chunk associations, and traffic timing.
- Symlinks and special files are skipped. Permissions, extended attributes, ACLs, Finder tags, and empty-directory metadata are not backed up. Changing files are retried; there is no cross-file filesystem snapshot.
- All signed versions survive partitions. The UI selects the newest timestamp per source folder/path; it does not merge contents. Compromised members can deny service or fill quota. Encryption/signatures protect confidentiality/authenticity, not availability against malicious infrastructure.
- Key rotation, user-key revocation, password recovery, App Store distribution, and audited production hardening are outside this version.

See [Protocol](docs/PROTOCOL.md) and [Verification](docs/VERIFICATION.md).
