M1: persistent vault, local unlock key, sync client, test vectors
kv-store: SQLite (bundled) and in-memory Store holding only ciphertext envelopes, with base copies for three-way merges and a 10-version history. kv-ffi: VaultCore::open(path); persisted account record; local unlock key (enable/unlock_with_local_key/disable); async sync API (configure_server, set_device_signer, register_account, login, sync_now, logout, devices); DeviceSigner foreign trait; IntegrityAlarm/PinMismatch/... error variants. kv-sync: reqwest + rustls (TLS 1.3 only, ring) with SPKI pinning; OPAQUE client on kv-proto's OpaqueSuite; RFC 9421 signing per kv_proto::httpsig; pull/push with If-Match, field-level three-way merge, edit-beats-delete; signed manifest (canonical CBOR, Merkle root, prev chain, Ed25519) with rollback/fork/Merkle checks as hard integrity alarms. Tested against a wiremock fake server, plus an #[ignore] test against kestrove-server. kv-crypto: gen-vectors example and tests reproducing kestrove-spec/vectors. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -1,24 +1,72 @@
|
||||
//! Sync engine (milestone M1, kestrove-spec/docs/03).
|
||||
//!
|
||||
//! Responsibilities:
|
||||
//! * `Transport`: HTTPS with rustls, TLS 1.3 only, SPKI pinning (primary + backup pin).
|
||||
//! * OPAQUE login via `opaque-ke` (ristretto255 / SHA-512, identity KSF because the input is already Argon2id-stretched).
|
||||
//! * RFC 9421 request signing with the device key. The key lives in the platform keystore, so signing is
|
||||
//! * [`transport`]: HTTPS with rustls, TLS 1.3 only, SPKI pinning (primary + backup pin).
|
||||
//! * [`opaque`]: OPAQUE login via `opaque-ke` (ristretto255 / SHA-512, identity KSF because the input is already
|
||||
//! Argon2id-stretched).
|
||||
//! * [`signing`]: RFC 9421 request signing with the device key. The key lives in the platform keystore, so signing is
|
||||
//! a callback into platform code ([`DeviceSigner`]).
|
||||
//! * Pull, then push with `If-Match`, then three-way field merge on 412, then manifest verify and publish.
|
||||
//! * [`engine`]: pull, then push with `If-Match`, then three-way field merge on 412 ([`merge`]), then manifest verify
|
||||
//! and publish ([`manifest`]).
|
||||
|
||||
pub mod client;
|
||||
pub mod engine;
|
||||
pub mod manifest;
|
||||
pub mod merge;
|
||||
pub mod opaque;
|
||||
pub mod signing;
|
||||
#[cfg(feature = "test-server")]
|
||||
pub mod test_server;
|
||||
pub mod transport;
|
||||
pub mod wire;
|
||||
|
||||
pub use client::{ApiClient, LoginOutcome};
|
||||
pub use engine::{SyncContext, SyncReport};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum SyncError {
|
||||
#[error("network: {0}")]
|
||||
Network(String),
|
||||
#[error("server rejected request: {0}")]
|
||||
Server(String),
|
||||
#[error("integrity alarm on vault {0}: manifest rollback or tampering")]
|
||||
Integrity(Uuid),
|
||||
#[error("server certificate does not match the pinned keys")]
|
||||
PinMismatch,
|
||||
#[error("configuration: {0}")]
|
||||
Config(String),
|
||||
#[error("server returned {status} {code}: {message}")]
|
||||
Server { status: u16, code: String, message: String },
|
||||
#[error("unexpected server response: {0}")]
|
||||
Protocol(String),
|
||||
#[error("integrity alarm on vault {vault_id}: {reason}")]
|
||||
Integrity { vault_id: Uuid, reason: String },
|
||||
#[error("not logged in")]
|
||||
NotAuthenticated,
|
||||
#[error("wrong master password or Secret Key")]
|
||||
BadCredentials,
|
||||
#[error("authentication failed: {0}")]
|
||||
Auth(String),
|
||||
#[error("device signer: {0}")]
|
||||
Signer(String),
|
||||
#[error("local storage: {0}")]
|
||||
Store(String),
|
||||
#[error("crypto: {0}")]
|
||||
Crypto(String),
|
||||
#[error("not found")]
|
||||
NotFound,
|
||||
/// HTTP 412: the item changed on the server since `If-Match`.
|
||||
#[error("revision conflict")]
|
||||
RevisionConflict,
|
||||
/// HTTP 409.
|
||||
#[error("version conflict")]
|
||||
VersionConflict,
|
||||
/// Internal: the manifest is newer than our cursor; pull again.
|
||||
#[error("manifest is ahead of the pulled changes")]
|
||||
ManifestAhead,
|
||||
}
|
||||
|
||||
impl From<kv_store::StoreError> for SyncError {
|
||||
fn from(e: kv_store::StoreError) -> Self {
|
||||
SyncError::Store(e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Implemented by each platform: Secure Enclave, StrongBox, TPM or software fallback.
|
||||
@@ -31,8 +79,16 @@ pub trait DeviceSigner: Send + Sync {
|
||||
}
|
||||
|
||||
/// Server endpoint plus pinned SPKI SHA-256 hashes (base64).
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ServerConfig {
|
||||
pub base_url: String,
|
||||
pub spki_pins: Vec<String>,
|
||||
/// Extra trust anchors (DER), e.g. a private CA for a self-hosted server. Empty in normal use.
|
||||
pub extra_roots_der: Vec<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl ServerConfig {
|
||||
pub fn new(base_url: impl Into<String>, spki_pins: Vec<String>) -> Self {
|
||||
Self { base_url: base_url.into(), spki_pins, extra_roots_der: vec![] }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user