Initial scaffold for kestrove-core
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,218 @@
|
||||
//! 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user