219 lines
5.8 KiB
Rust
219 lines
5.8 KiB
Rust
//! 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<S: Serializer>(bytes: &[u8], s: S) -> Result<S::Ok, S::Error> {
|
|
s.serialize_str(&URL_SAFE_NO_PAD.encode(bytes))
|
|
}
|
|
|
|
pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Vec<u8>, 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<u8>,
|
|
#[serde(with = "b64")]
|
|
pub ct: Vec<u8>,
|
|
/// HPKE encapsulated key (HPKE envelopes only).
|
|
#[serde(with = "b64", default, skip_serializing_if = "Vec::is_empty")]
|
|
pub enc: Vec<u8>,
|
|
}
|
|
|
|
#[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<u8>,
|
|
}
|
|
|
|
#[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<u8>,
|
|
}
|
|
|
|
#[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<u8>,
|
|
#[serde(with = "b64")]
|
|
pub enc_pk: Vec<u8>,
|
|
#[serde(with = "b64")]
|
|
pub sig_pk: Vec<u8>,
|
|
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<u8>,
|
|
pub device: DeviceEnroll,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct LoginStartResponse {
|
|
pub login_id: String,
|
|
#[serde(with = "b64")]
|
|
pub ke2: Vec<u8>,
|
|
pub kdf: KdfParams,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct LoginFinish {
|
|
pub login_id: String,
|
|
#[serde(with = "b64")]
|
|
pub ke3: Vec<u8>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub totp: Option<String>,
|
|
}
|
|
|
|
#[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<String>,
|
|
pub expires_at: Option<String>,
|
|
}
|
|
|
|
#[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<Envelope>,
|
|
pub payload: Option<Envelope>,
|
|
pub updated_by: Uuid,
|
|
pub updated_at: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct Changes {
|
|
pub items: Vec<ItemRecord>,
|
|
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<u8>,
|
|
#[serde(with = "b64")]
|
|
pub signature: Vec<u8>,
|
|
}
|
|
|
|
#[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::<Envelope>(&json).unwrap(), env);
|
|
}
|
|
}
|