api-auth
Source: crates/lvcs-api/src/auth.rs.
/// The first line the daemon sends on a host peer stream (decision 300):
/// the push `challenge`. A device's `hello` signs it ([`hello_message`]),
/// so its proof can't be replayed on another stream or to another host.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ChallengePush {
/// A fresh nonce, lowercase hex.
pub nonce: String,
/// The host peer's id.
pub host_peer: String,
}
/// A device's proof of its key in `hello` on a host peer stream
/// (decision 300): `key` (`ID_ENCODING`) and its Ed25519 `signature`
/// (lowercase hex) over [`hello_message`].
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct DeviceProof {
pub key: String,
pub signature: String,
}
/// What a device signs in `hello`: `HELLO_PROOF_CONTEXT`, the challenge's
/// `nonce`, the `host_peer` id, and the `root` the `hello` names (empty
/// for none), one per line.
#[must_use]
pub fn hello_message(nonce: &str, host_peer: &str, root: Option<&str>) -> String {
format!(
"{HELLO_PROOF_CONTEXT}\n{nonce}\n{host_peer}\n{}",
root.unwrap_or("")
)
}
/// What a pairing link carries after `#pair=` (decision 300), as
/// unpadded base64url of its JSON: the host peer to dial, the project
/// root to name in `hello`, and the pairing's nonce for `auth.pair`.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PairLink {
/// The link's version, 1.
pub v: u8,
/// The host peer's id.
pub peer: String,
/// The host peer's WebRTC-direct addresses, each ending in
/// `/p2p/<peer>`.
pub addrs: Vec<String>,
/// The project root.
pub root: String,
/// The project's id, which the tracker finds the host peer by
/// (decision 327).
pub project: String,
/// The tracker the host peer announces to, for a browser that can
/// reach none of `addrs`; none when it announces to none.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tracker: Option<String>,
/// The STUN servers the host peer asks, which the browser asks too.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub stun: Vec<String>,
/// The pairing's nonce.
pub nonce: String,
}
/// `auth.login` (decision 300): start pairing a device for the caller.
/// The device gets `read` on `read` (every path when empty), and with
/// `write` also `message`, so it may coordinate (ask, answer, hold, say,
/// run), never edit; it signs those records itself (the push `sign`).
/// `alias` names the device (decision 330): only the owner gives one, it
/// is shaped as `grant`'s alias is, and one already granted to any
/// participant or device, revoked or not, is refused `INVALID_PARAMS`
/// with a free alias in `next`. Answered at once with the nonce; the
/// pairing expires after `PAIRING_TTL_SECS`.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct AuthLoginParams {
#[serde(default, skip_serializing_if = "core::ops::Not::not")]
pub write: bool,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub read: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub alias: Option<String>,
}
/// What a public link carries after `#` (decision 337): `p=<project>@<peer>`,
/// the project's id and the host peer that serves it, then `&t=<tracker>`
/// when its tracker isn't `TRACKER_URL`, and `&s=<stun>,...` when its STUN
/// servers aren't `STUN_URLS`. Anyone who opens it reads the project
/// anonymously, through that tracker, as a `read` grant to anyone allows;
/// the host peer's id pins the daemon whose signed answer the browser
/// takes, so a peer that announces the same project id serves it nothing.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReadLink {
/// The project's id.
pub project: String,
/// The host peer's id.
pub peer: String,
/// The tracker the host peer announces to.
pub tracker: String,
/// The STUN servers it asks.
pub stun: Vec<String>,
}
impl ReadLink {
/// The link's fragment, without its `#`.
#[must_use]
pub fn fragment(&self) -> String {
let mut out = format!("p={}@{}", self.project, self.peer);
if self.tracker != TRACKER_URL {
out.push_str("&t=");
out.push_str(&self.tracker);
}
if self.stun.join(",") != STUN_URLS {
out.push_str("&s=");
out.push_str(&self.stun.join(","));
}
out
}
/// The link in the fragment `fragment` (with or without its `#`);
/// `None` when it is no public link.
#[must_use]
pub fn parse(fragment: &str) -> Option<Self> {
let fragment = fragment.strip_prefix('#').unwrap_or(fragment);
let mut link: Option<Self> = None;
let mut tracker = None;
let mut stun = None;
for part in fragment.split('&') {
match part.split_once('=')? {
("p", value) => {
let (project, peer) = value.split_once('@')?;
let valid = |text: &str| {
!text.is_empty() && text.bytes().all(|b| b.is_ascii_alphanumeric())
};
if !valid(project) || !valid(peer) {
return None;
}
link = Some(Self {
project: project.to_owned(),
peer: peer.to_owned(),
tracker: TRACKER_URL.to_owned(),
stun: Vec::new(),
});
}
("t", value) => tracker = Some(value.to_owned()),
("s", value) => stun = Some(value.to_owned()),
_ => {}
}
}
let mut link = link?;
if let Some(tracker) = tracker {
link.tracker = tracker;
}
link.stun = stun
.as_deref()
.unwrap_or(STUN_URLS)
.split(',')
.filter(|url| !url.is_empty())
.map(str::to_owned)
.collect();
Some(link)
}
}
/// `auth.login` result. `lvcs auth login` builds the link to open from
/// it and the host's `status`: `APP_URL`, then `#pair=` and the
/// [`PairLink`].
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AuthLoginResult {
/// The pairing's nonce, for `auth.wait`, `auth.confirm`, and the link.
pub nonce: String,
/// When the pairing expires, in Unix milliseconds.
pub expires_ms: u64,
}
/// `auth.wait` (decision 300): wait for a device to claim pairing
/// `nonce`; answered then, or refused once it expired.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct AuthWaitParams {
pub nonce: String,
}
/// `auth.wait` result: the device that claimed the pairing.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AuthWaitResult {
/// Its key, `ID_ENCODING`.
pub device: String,
/// The name it gave.
pub name: String,
/// The code it shows: confirm only when the two match.
pub code: String,
/// The alias it gets unless `auth.confirm` names another: the login's
/// `alias`, else `<name>-<n>`; absent when the login's participant
/// isn't the owner, which gives no alias.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub alias: Option<String>,
}
/// `auth.confirm` (decision 300): accept (`accept`) or decline the
/// device that claimed pairing `nonce`. Accepting signs its device grant
/// as the caller, under a permanent alias when the caller is the owner:
/// the login's `alias`, else this `alias` (decision 330, checked as
/// `auth.login` checks it, and a refused one leaves the pairing to
/// confirm again), else `<name>-<n>`; answered as `grant` is
/// (`GrantResult`). Declining answers `{}`.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct AuthConfirmParams {
pub nonce: String,
pub accept: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub alias: Option<String>,
}
/// `auth.pair`'s result once the device's grant renders.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AuthPaired {
/// The device grant's id.
pub grant: String,
/// The device's key, `ID_ENCODING`.
pub device: String,
/// Its alias, absent when the issuer isn't the owner.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub alias: Option<String>,
/// Whether it may coordinate.
pub write: bool,
/// The paths it reads.
pub read: Vec<String>,
}
/// `auth.pair` (decision 300): the device whose `hello` proved its key
/// claims pairing `nonce`, under `name` (at most `DEVICE_NAME_MAX_BYTES`
/// of lowercase letters, digits, and `-`). Answered once the pairing is
/// confirmed, refused when it is declined or expires; meanwhile the push
/// `pairing` gives the code to show.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct AuthPairParams {
pub nonce: String,
pub name: String,
}
/// The push `pairing` (decision 300): the code a claiming device shows,
/// the one `lvcs auth login` shows in the terminal.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PairingPush {
pub nonce: String,
pub code: String,
}
/// The push `sign` (decision 300): a write device's coordination record
/// (a thread change or a gossip message) is signed by the device itself,
/// since the daemon holds no device key. The device answers with
/// `auth.sign`, its Ed25519 signature over `input` (lowercase hex bytes);
/// a record whose signature never comes is never published.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SignPush {
/// The signing request's id, for `auth.sign`.
pub request: u64,
/// What to sign, lowercase hex.
pub input: String,
}
/// `auth.sign` (decision 300): the device's `signature` (lowercase hex)
/// over the input of the push `sign` with id `request`. Answered `{}`;
/// refused when no such request waits, or the signature isn't the
/// device's over it.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct AuthSignParams {
pub request: u64,
pub signature: String,
}
/// `auth.status` (decision 300): the caller's paired devices.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct AuthStatusParams {}
/// `auth.status` result.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AuthStatusResult {
#[serde(default)]
pub devices: Vec<PairedDevice>,
}
/// One live device grant the caller issued.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PairedDevice {
/// The grant's id.
pub grant: String,
/// The device's key, `ID_ENCODING`.
pub device: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub alias: Option<String>,
pub write: bool,
pub read: Vec<String>,
/// When it was paired, in Unix seconds.
pub paired_s: i64,
/// When this daemon last saw it on a host peer stream since it
/// started, in Unix milliseconds.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub last_seen_ms: Option<u64>,
}
/// `auth.logout` (decision 300): revoke the device `device` (its alias, or
/// its key or a prefix of it) and every grant of its lineage, as
/// `revoke` does; its open streams close. Answered with `RevokeResult`.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct AuthLogoutParams {
pub device: String,
}