//! Wire format for the Kestrove sync API (see kestrove-spec/docs/03 and api/openapi.yaml). //! //! Everything here is either public metadata or opaque ciphertext. The server //! depends on this crate and nothing else from the core. use serde::{Deserialize, Serialize}; use uuid::Uuid; pub const FORMAT_VERSION: u8 = 1; pub const API_VERSION: u32 = 1; /// Minimum Argon2id parameters a client accepts (anti-downgrade, docs/02). pub const MIN_KDF_M_KIB: u32 = 64 * 1024; pub const MIN_KDF_T: u32 = 3; /// base64url (no padding) serde helper for byte fields. pub mod b64 { use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; use serde::{Deserialize, Deserializer, Serializer}; pub fn serialize(bytes: &[u8], s: S) -> Result { s.serialize_str(&URL_SAFE_NO_PAD.encode(bytes)) } pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result, D::Error> { let s = String::deserialize(d)?; URL_SAFE_NO_PAD.decode(s).map_err(serde::de::Error::custom) } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum Alg { /// XChaCha20-Poly1305 with a 24-byte random nonce. #[serde(rename = "xc20p")] XChaCha20Poly1305, /// HPKE base mode, DHKEM(X25519, HKDF-SHA256), HKDF-SHA256, ChaCha20-Poly1305. #[serde(rename = "hpke-x25519-chacha")] HpkeX25519ChaCha, } /// Every encrypted object on the wire and on disk (docs/02 "Envelope format"). #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct Envelope { pub v: u8, pub alg: Alg, /// Id of the key that opens this envelope. pub kid: Uuid, #[serde(with = "b64", default, skip_serializing_if = "Vec::is_empty")] pub n: Vec, #[serde(with = "b64")] pub ct: Vec, /// HPKE encapsulated key (HPKE envelopes only). #[serde(with = "b64", default, skip_serializing_if = "Vec::is_empty")] pub enc: Vec, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct KdfParams { pub alg: String, pub m_kib: u32, pub t: u32, pub p: u32, #[serde(with = "b64")] pub salt: Vec, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum Platform { Ios, Macos, Android, Windows, Linux, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ServerInfo { pub version: String, pub api: u32, pub single_tenant: bool, pub setup_open: bool, pub min_client: String, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DeviceEnroll { pub device_id: Uuid, pub name: String, pub platform: Platform, /// SEC1 uncompressed P-256 public key. #[serde(with = "b64")] pub p256_pk: Vec, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SetupRequest { pub setup_token: String, pub account_id: Uuid, pub kdf: KdfParams, #[serde(with = "b64")] pub opaque_registration_upload: Vec, #[serde(with = "b64")] pub enc_pk: Vec, #[serde(with = "b64")] pub sig_pk: Vec, pub wrapped_keyset: Envelope, pub device: DeviceEnroll, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LoginStart { pub account_id: Uuid, #[serde(with = "b64")] pub ke1: Vec, pub device: DeviceEnroll, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LoginStartResponse { pub login_id: String, #[serde(with = "b64")] pub ke2: Vec, pub kdf: KdfParams, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LoginFinish { pub login_id: String, #[serde(with = "b64")] pub ke3: Vec, #[serde(default, skip_serializing_if = "Option::is_none")] pub totp: Option, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum LoginStatus { Ok, PendingApproval, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LoginFinishResponse { pub status: LoginStatus, pub session: Option, pub expires_at: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Vault { pub vault_id: Uuid, pub head_revision: u64, pub sealed_key: Envelope, pub meta: Envelope, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ItemPut { pub wrapped_item_key: Envelope, pub payload: Envelope, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ItemRecord { pub item_id: Uuid, pub revision: u64, pub deleted: bool, pub wrapped_item_key: Option, pub payload: Option, pub updated_by: Uuid, pub updated_at: String, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Changes { pub items: Vec, pub head_revision: u64, pub more: bool, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Manifest { pub version: u64, /// Canonical CBOR of the manifest body (docs/03 "Integrity"). #[serde(with = "b64")] pub body: Vec, #[serde(with = "b64")] pub signature: Vec, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ApiError { pub code: String, pub message: String, } #[cfg(test)] mod tests { use super::*; #[test] fn envelope_json_roundtrip_uses_b64url() { let env = Envelope { v: FORMAT_VERSION, alg: Alg::XChaCha20Poly1305, kid: Uuid::nil(), n: vec![0xff; 24], ct: vec![1, 2, 3], enc: vec![], }; let json = serde_json::to_string(&env).unwrap(); assert!(json.contains("\"alg\":\"xc20p\"")); assert!(!json.contains("enc")); assert!(!json.contains('+') && !json.contains('/')); assert_eq!(serde_json::from_str::(&json).unwrap(), env); } }