| 1 |
# synckit-client |
| 2 |
|
| 3 |
End-to-end encrypted cloud sync SDK for Rust applications, built for the [MNW SyncKit](https://makenot.work) server. |
| 4 |
|
| 5 |
All row data and binary blobs are encrypted client-side (XChaCha20-Poly1305) before leaving the device. The server only ever stores ciphertext. |
| 6 |
|
| 7 |
## Features |
| 8 |
|
| 9 |
- **E2E encryption** -- XChaCha20-Poly1305 with Argon2id key derivation (64 MB, 3 iterations) |
| 10 |
- **OS keychain integration** -- master key cached in macOS Keychain, Linux secret-service, or Windows Credential Manager |
| 11 |
- **Blob encryption** -- binary files encrypted with fixed 40-byte overhead (no base64 expansion) |
| 12 |
- **Retry with backoff** -- transient failures (network, 5xx, 429) retried up to 3 times with exponential delay |
| 13 |
- **OAuth2 PKCE** -- browser-based auth flow alongside email/password |
| 14 |
- **Token expiry detection** -- client-side JWT check with 30-second buffer |
| 15 |
|
| 16 |
## Quick Start |
| 17 |
|
| 18 |
```rust |
| 19 |
use synckit_client::{SyncKitClient, SyncKitConfig, ChangeEntry, ChangeOp}; |
| 20 |
use chrono::Utc; |
| 21 |
|
| 22 |
let client = SyncKitClient::new(SyncKitConfig { |
| 23 |
server_url: "https://makenot.work".into(), |
| 24 |
api_key: "your-api-key".into(), |
| 25 |
}); |
| 26 |
|
| 27 |
// Authenticate |
| 28 |
let (user_id, app_id) = client.authenticate("user@example.com", "password").await?; |
| 29 |
|
| 30 |
// Set up encryption (first device) |
| 31 |
client.setup_encryption_new("password").await?; |
| 32 |
|
| 33 |
// Register this device |
| 34 |
let device = client.register_device("MacBook Pro", "macos").await?; |
| 35 |
|
| 36 |
// Push encrypted data |
| 37 |
let cursor = client.push(device.id, vec![ |
| 38 |
ChangeEntry { |
| 39 |
table: "tasks".into(), |
| 40 |
op: ChangeOp::Insert, |
| 41 |
row_id: uuid::Uuid::new_v4().to_string(), |
| 42 |
timestamp: Utc::now(), |
| 43 |
data: Some(serde_json::json!({"title": "Buy milk"})), |
| 44 |
}, |
| 45 |
]).await?; |
| 46 |
|
| 47 |
// Pull and auto-decrypt |
| 48 |
let (changes, cursor, has_more) = client.pull(device.id, 0).await?; |
| 49 |
``` |
| 50 |
|
| 51 |
## Crate Structure |
| 52 |
|
| 53 |
|
| 54 |
|
| 55 |
| `lib.rs` | Crate root, re-exports, doc example | |
| 56 |
| `client.rs` | `SyncKitClient` -- HTTP methods, retry logic, token expiry detection | |
| 57 |
| `crypto.rs` | Key derivation (Argon2id), key wrapping, per-entry and per-blob encrypt/decrypt | |
| 58 |
| `error.rs` | `SyncKitError` enum (10 variants: HTTP, server, JSON, crypto, keychain, auth) | |
| 59 |
| `keystore.rs` | OS keychain read/write/delete, feature-gated with no-op stubs | |
| 60 |
| `types.rs` | Wire protocol types (`ChangeEntry`, `ChangeOp`, `Device`, `SyncStatus`) | |
| 61 |
|
| 62 |
## Feature Flags |
| 63 |
|
| 64 |
|
| 65 |
|
| 66 |
| `keychain` | on | OS keychain storage via the `keyring` crate. Disable with `default-features = false` for headless/CI environments. | |
| 67 |
|
| 68 |
## Security Properties |
| 69 |
|
| 70 |
- **Server-zero-knowledge** -- the server never receives the plaintext master key or user data |
| 71 |
- **Key zeroization** -- volatile writes clear the master key from memory on drop |
| 72 |
- **Random salt per wrap** -- re-wrapping with the same password produces a different envelope |
| 73 |
- **Minimum ciphertext validation** -- decryption rejects inputs shorter than 40 bytes (24-byte nonce + 16-byte tag) |
| 74 |
- **No key material in logs** -- tracing events never include key bytes or ciphertext |
| 75 |
|
| 76 |
## License |
| 77 |
|
| 78 |
PolyForm Noncommercial 1.0.0 |
| 79 |
|