Skip to main content

aranya_daemon_api/
service.rs

1#![allow(clippy::disallowed_macros)] // tarpc uses unreachable
2
3use core::{error, fmt, hash::Hash, time::Duration};
4
5pub use aranya_crypto::tls::CipherSuiteId;
6use aranya_crypto::{
7    dangerous::spideroak_crypto::hex::Hex,
8    default::DefaultEngine,
9    id::IdError,
10    subtle::{Choice, ConstantTimeEq},
11    zeroize::{Zeroize, ZeroizeOnDrop},
12    EncryptionPublicKey, Engine,
13};
14use aranya_id::custom_id;
15pub use aranya_policy_text::{text, InvalidText, Text};
16use aranya_util::{error::ReportExt, Addr};
17use buggy::Bug;
18pub use semver::Version;
19use serde::{Deserialize, Serialize};
20
21pub mod afc;
22pub mod quic_sync;
23
24#[cfg(feature = "afc")]
25pub use self::afc::*;
26pub use self::quic_sync::*;
27
28/// CE = Crypto Engine
29pub type CE = DefaultEngine;
30/// CS = Cipher Suite
31pub type CS = <DefaultEngine as Engine>::CS;
32
33/// An error returned by the API.
34// TODO: add more error variants as needed for control flow.
35#[derive(Serialize, Deserialize, Debug)]
36pub enum Error {
37    /// The requested resource does not exist.
38    DoesNotExist(String),
39    /// Any other error.
40    Other(String),
41}
42
43impl Error {
44    pub fn from_msg(err: &str) -> Self {
45        Self::Other(err.into())
46    }
47
48    pub fn from_err<E: error::Error>(err: E) -> Self {
49        Self::Other(ReportExt::report(&err).to_string())
50    }
51}
52
53impl From<Bug> for Error {
54    fn from(err: Bug) -> Self {
55        Self::from_err(err)
56    }
57}
58
59impl From<anyhow::Error> for Error {
60    fn from(err: anyhow::Error) -> Self {
61        Self::Other(format!("{err:?}"))
62    }
63}
64
65impl From<InvalidText> for Error {
66    fn from(err: InvalidText) -> Self {
67        Self::Other(format!("{err:?}"))
68    }
69}
70
71impl From<semver::Error> for Error {
72    fn from(err: semver::Error) -> Self {
73        Self::from_err(err)
74    }
75}
76
77impl From<IdError> for Error {
78    fn from(err: IdError) -> Self {
79        Self::from_err(err)
80    }
81}
82
83impl fmt::Display for Error {
84    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
85        match self {
86            Self::DoesNotExist(msg) | Self::Other(msg) => msg.fmt(f),
87        }
88    }
89}
90
91impl error::Error for Error {}
92
93pub type Result<T, E = Error> = core::result::Result<T, E>;
94
95custom_id! {
96    /// The Device ID.
97    pub struct DeviceId;
98}
99
100custom_id! {
101    /// The Team ID (a.k.a Graph ID).
102    pub struct TeamId;
103}
104
105custom_id! {
106    /// A label ID.
107    pub struct LabelId;
108}
109
110custom_id! {
111    /// A role ID.
112    pub struct RoleId;
113}
114
115custom_id! {
116    /// An identifier for any object with a unique Aranya ID defined in the policy.
117    pub struct ObjectId;
118}
119
120/// A numerical rank used for authorization in the rank-based hierarchy.
121///
122/// Higher-ranked objects can operate on lower-ranked objects.
123#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
124pub struct Rank(i64);
125
126impl Rank {
127    /// Creates a new rank from a raw value.
128    pub const fn new(value: i64) -> Self {
129        Self(value)
130    }
131
132    /// Returns the raw rank value.
133    pub const fn value(self) -> i64 {
134        self.0
135    }
136}
137
138impl fmt::Display for Rank {
139    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
140        self.0.fmt(f)
141    }
142}
143
144impl From<i64> for Rank {
145    fn from(value: i64) -> Self {
146        Self::new(value)
147    }
148}
149
150#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
151pub struct Role {
152    /// Uniquely identifies the role.
153    pub id: RoleId,
154    /// The role's friendly name.
155    pub name: Text,
156    /// The author of the role.
157    pub author_id: DeviceId,
158    /// Is this a default role?
159    pub default: bool,
160}
161
162/// A device's public key bundle.
163#[derive(Clone, Serialize, Deserialize, Eq, PartialEq)]
164pub struct PublicKeyBundle {
165    pub identity: Vec<u8>,
166    pub signing: Vec<u8>,
167    pub encryption: Vec<u8>,
168}
169
170impl fmt::Debug for PublicKeyBundle {
171    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
172        f.debug_struct("PublicKeyBundle")
173            .field("identity", &Hex::new(&*self.identity))
174            .field("signing", &Hex::new(&*self.signing))
175            .field("encryption", &Hex::new(&*self.encryption))
176            .finish()
177    }
178}
179
180// Note: any fields added to this type should be public
181/// A configuration for adding a team in the daemon.
182#[derive(Debug, Serialize, Deserialize)]
183pub struct AddTeamConfig {
184    pub team_id: TeamId,
185    pub quic_sync: Option<AddTeamQuicSyncConfig>,
186}
187
188// Note: any fields added to this type should be public
189/// A configuration for creating a team in the daemon.
190#[derive(Debug, Serialize, Deserialize)]
191pub struct CreateTeamConfig {
192    pub quic_sync: Option<CreateTeamQuicSyncConfig>,
193}
194
195/// A label.
196#[derive(Clone, Debug, Hash, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
197pub struct Label {
198    pub id: LabelId,
199    pub name: Text,
200    pub author_id: DeviceId,
201}
202
203/// A PSK IKM.
204#[derive(Clone, Serialize, Deserialize)]
205pub struct Ikm([u8; SEED_IKM_SIZE]);
206
207impl Ikm {
208    /// Provides access to the raw IKM bytes.
209    #[inline]
210    pub fn raw_ikm_bytes(&self) -> &[u8; SEED_IKM_SIZE] {
211        &self.0
212    }
213}
214
215impl From<[u8; SEED_IKM_SIZE]> for Ikm {
216    fn from(value: [u8; SEED_IKM_SIZE]) -> Self {
217        Self(value)
218    }
219}
220
221impl ConstantTimeEq for Ikm {
222    fn ct_eq(&self, other: &Self) -> Choice {
223        self.0.ct_eq(&other.0)
224    }
225}
226
227impl ZeroizeOnDrop for Ikm {}
228impl Drop for Ikm {
229    fn drop(&mut self) {
230        self.0.zeroize()
231    }
232}
233
234impl fmt::Debug for Ikm {
235    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
236        f.debug_struct("Ikm").finish_non_exhaustive()
237    }
238}
239
240/// A secret.
241#[derive(Clone, Serialize, Deserialize)]
242pub struct Secret(Box<[u8]>);
243
244impl Secret {
245    /// Provides access to the raw secret bytes.
246    #[inline]
247    pub fn raw_secret_bytes(&self) -> &[u8] {
248        &self.0
249    }
250}
251
252impl<T> From<T> for Secret
253where
254    T: Into<Box<[u8]>>,
255{
256    fn from(value: T) -> Self {
257        Self(value.into())
258    }
259}
260
261impl ConstantTimeEq for Secret {
262    fn ct_eq(&self, other: &Self) -> Choice {
263        self.0.ct_eq(&other.0)
264    }
265}
266
267impl ZeroizeOnDrop for Secret {}
268impl Drop for Secret {
269    fn drop(&mut self) {
270        self.0.zeroize()
271    }
272}
273
274impl fmt::Debug for Secret {
275    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
276        f.debug_struct("Secret").finish_non_exhaustive()
277    }
278}
279
280/// Configuration values for syncing with a peer
281#[derive(Clone, Debug, Serialize, Deserialize)]
282pub struct SyncPeerConfig {
283    /// The interval at which syncing occurs. If None, the peer will not be periodically synced.
284    pub interval: Option<Duration>,
285    /// Determines whether the peer will be scheduled for an immediate sync when added.
286    pub sync_now: bool,
287    /// Determines if the peer should be synced with when a hello message is received
288    /// indicating they have a head that we don't have
289    #[cfg(feature = "preview")]
290    pub sync_on_hello: bool,
291}
292
293/// Valid channel operations for a label assignment.
294#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
295pub enum ChanOp {
296    /// The device can only receive data in channels with this
297    /// label.
298    RecvOnly,
299    /// The device can only send data in channels with this
300    /// label.
301    SendOnly,
302    /// The device can send or receive data in channels with this
303    /// label.
304    SendRecv,
305}
306
307/// Permissions that can be granted to a role.
308///
309/// # Stability
310///
311/// New permissions may be added to the end of this enum without breaking
312/// backward compatibility. Existing permissions will not be removed or
313/// renamed.
314///
315/// # Deprecation
316///
317/// Deprecated variants are marked with `#[deprecated]` and will emit
318/// compiler warnings when used. They remain in the enum for backward
319/// compatibility — see the deprecation note on each variant for the
320/// migration path.
321// This enum is re-exported as `aranya_client::Permission`. New
322// permissions may be added as the policy evolves, so `non_exhaustive`
323// ensures downstream match statements continue to compile.
324#[non_exhaustive]
325#[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
326pub enum Perm {
327    // # Team management
328    //
329    // The role can add a device to the team.
330    AddDevice,
331    // The role can remove a device from the team.
332    RemoveDevice,
333    // The role can terminate the team.
334    TerminateTeam,
335
336    // # Rank
337    //
338    // The role can change the rank of objects.
339    ChangeRank,
340
341    // # Roles
342    //
343    // The role can create a role.
344    CreateRole,
345    // The role can delete a role.
346    DeleteRole,
347    // The role can assign a role to other devices.
348    AssignRole,
349    // The role can revoke a role from other devices.
350    RevokeRole,
351    // The role can change permissions on roles.
352    ChangeRolePerms,
353    // The role can set up default roles.
354    SetupDefaultRole,
355
356    // # Labels
357    //
358    // The role can create a label.
359    CreateLabel,
360    // The role can delete a label.
361    DeleteLabel,
362    // The role can assign a label to a device.
363    AssignLabel,
364    // The role can revoke a label from a device.
365    RevokeLabel,
366
367    // # AFC
368    //
369    // The role can use AFC.
370    CanUseAfc,
371    // The role can create a unidirectional AFC channel.
372    CreateAfcUniChannel,
373}
374
375// TODO(jdygert): tarpc does not cfg return types properly.
376#[cfg(not(feature = "afc"))]
377use afc_stub::{AfcReceiveChannelInfo, AfcSendChannelInfo, AfcShmInfo};
378#[cfg(not(feature = "afc"))]
379mod afc_stub {
380    #[derive(Debug, serde::Serialize, serde::Deserialize)]
381    pub enum Never {}
382    pub type AfcShmInfo = Never;
383    pub type AfcSendChannelInfo = Never;
384    pub type AfcReceiveChannelInfo = Never;
385}
386
387#[tarpc::service]
388pub trait DaemonApi {
389    //
390    // Misc
391    //
392
393    /// Returns the daemon's version.
394    async fn version() -> Result<Version>;
395    /// Gets local address the Aranya sync server is bound to.
396    async fn aranya_local_addr() -> Result<Addr>;
397
398    /// Gets the public key bundle for this device
399    async fn get_public_key_bundle() -> Result<PublicKeyBundle>;
400    /// Gets the public device id.
401    async fn get_device_id() -> Result<DeviceId>;
402    /// Returns the trace ID received in the current RPC context.
403    ///
404    /// Intended for test/debug validation of client<->daemon trace propagation.
405    #[cfg(feature = "test-utils")]
406    async fn test_trace_id() -> Result<String>;
407
408    //
409    // Syncing
410    //
411
412    /// Adds the peer for automatic periodic syncing.
413    async fn add_sync_peer(addr: Addr, team: TeamId, config: SyncPeerConfig) -> Result<()>;
414    /// Sync with peer immediately.
415    async fn sync_now(addr: Addr, team: TeamId, cfg: Option<SyncPeerConfig>) -> Result<()>;
416
417    /// Subscribe to hello notifications from a sync peer.
418    #[cfg(feature = "preview")]
419    async fn sync_hello_subscribe(
420        peer: Addr,
421        team: TeamId,
422        graph_change_debounce: Duration,
423        duration: Duration,
424        schedule_delay: Duration,
425    ) -> Result<()>;
426
427    /// Unsubscribe from hello notifications from a sync peer.
428    #[cfg(feature = "preview")]
429    async fn sync_hello_unsubscribe(peer: Addr, team: TeamId) -> Result<()>;
430
431    /// Removes the peer from automatic syncing.
432    async fn remove_sync_peer(addr: Addr, team: TeamId) -> Result<()>;
433    /// add a team to the local device store that was created by someone else. Not an aranya action/command.
434    async fn add_team(cfg: AddTeamConfig) -> Result<()>;
435
436    /// Remove a team from local device storage.
437    async fn remove_team(team: TeamId) -> Result<()>;
438
439    /// Create a new graph/team with the current device as the owner.
440    async fn create_team(cfg: CreateTeamConfig) -> Result<TeamId>;
441    /// Close the team.
442    async fn close_team(team: TeamId) -> Result<()>;
443
444    /// Encrypts the team's syncing PSK(s) for the peer.
445    async fn encrypt_psk_seed_for_peer(
446        team: TeamId,
447        peer_enc_pk: EncryptionPublicKey<CS>,
448    ) -> Result<WrappedSeed>;
449
450    //
451    // Device onboarding
452    //
453
454    /// Adds a device to the team with an optional initial role and
455    /// explicit rank.
456    async fn add_device_to_team(
457        team: TeamId,
458        keys: PublicKeyBundle,
459        initial_role: Option<RoleId>,
460        rank: Rank,
461    ) -> Result<()>;
462    /// Remove device from the team.
463    async fn remove_device_from_team(team: TeamId, device: DeviceId) -> Result<()>;
464    /// Returns all the devices on the team.
465    async fn devices_on_team(team: TeamId) -> Result<Box<[DeviceId]>>;
466    /// Returns the device's public key bundle.
467    async fn device_public_key_bundle(team: TeamId, device: DeviceId) -> Result<PublicKeyBundle>;
468
469    //
470    // Role creation
471    //
472
473    /// Configures the team with default roles from policy.
474    ///
475    /// It returns the default roles that were created.
476    async fn setup_default_roles(team: TeamId) -> Result<Box<[Role]>>;
477    /// Creates a new role with the given rank.
478    async fn create_role(team: TeamId, role_name: Text, rank: Rank) -> Result<Role>;
479    /// Deletes a role.
480    async fn delete_role(team: TeamId, role_id: RoleId) -> Result<()>;
481    /// Returns the current team roles.
482    async fn team_roles(team: TeamId) -> Result<Box<[Role]>>;
483
484    //
485    // Role management
486    //
487
488    /// Adds a permission to a role.
489    async fn add_perm_to_role(team: TeamId, role: RoleId, perm: Perm) -> Result<()>;
490    /// Removes a permission from a role.
491    async fn remove_perm_from_role(team: TeamId, role: RoleId, perm: Perm) -> Result<()>;
492    /// Queries all permissions assigned to a role.
493    async fn query_role_perms(team: TeamId, role: RoleId) -> Result<Vec<Perm>>;
494    /// Changes the rank of an object (device or label).
495    ///
496    /// Note: Role ranks cannot be changed after creation. This maintains the
497    /// invariant that `role_rank > device_rank` for all devices assigned to
498    /// the role. To effectively change a role's rank, create a new role with
499    /// matching permissions at the desired rank, assign the new role to the
500    /// devices that had the old role, then delete the old role.
501    async fn change_rank(
502        team: TeamId,
503        object_id: ObjectId,
504        old_rank: Rank,
505        new_rank: Rank,
506    ) -> Result<()>;
507    /// Queries the rank of an object.
508    async fn query_rank(team: TeamId, object_id: ObjectId) -> Result<Rank>;
509
510    /// Queries the generation counter for a device.
511    #[cfg(feature = "test-utils")]
512    #[cfg_attr(docsrs, doc(cfg(feature = "test-utils")))]
513    async fn query_device_generation(team: TeamId, device_id: DeviceId) -> Result<Option<i64>>;
514
515    //
516    // Role assignment
517    //
518
519    /// Assign a role to a device.
520    async fn assign_role(team: TeamId, device: DeviceId, role: RoleId) -> Result<()>;
521    /// Revoke a role from a device.
522    async fn revoke_role(team: TeamId, device: DeviceId, role: RoleId) -> Result<()>;
523    /// Changes the assigned role of a device.
524    async fn change_role(
525        team: TeamId,
526        device: DeviceId,
527        old_role: RoleId,
528        new_role: RoleId,
529    ) -> Result<()>;
530    /// Returns the role assigned to the device.
531    async fn device_role(team: TeamId, device: DeviceId) -> Result<Option<Role>>;
532
533    //
534    // Label creation
535    //
536
537    /// Creates a label with an explicit rank.
538    async fn create_label(team: TeamId, name: Text, rank: Rank) -> Result<LabelId>;
539    /// Delete a label.
540    async fn delete_label(team: TeamId, label_id: LabelId) -> Result<()>;
541    /// Returns a specific label.
542    async fn label(team: TeamId, label: LabelId) -> Result<Label>;
543    /// Returns all labels on the team.
544    async fn labels(team: TeamId) -> Result<Vec<Label>>;
545
546    //
547    // Label assignments
548    //
549
550    /// Assigns a label to a device.
551    async fn assign_label_to_device(
552        team: TeamId,
553        device: DeviceId,
554        label: LabelId,
555        op: ChanOp,
556    ) -> Result<()>;
557    /// Revokes a label from a device.
558    async fn revoke_label_from_device(team: TeamId, device: DeviceId, label: LabelId)
559        -> Result<()>;
560    /// Returns all labels assigned to the device.
561    async fn labels_assigned_to_device(team: TeamId, device: DeviceId) -> Result<Box<[Label]>>;
562
563    /// Gets AFC shared-memory configuration info.
564    #[cfg(feature = "afc")]
565    #[cfg_attr(docsrs, doc(cfg(feature = "afc")))]
566    async fn afc_shm_info() -> Result<AfcShmInfo>;
567    /// Create a send-only AFC channel.
568    #[cfg(feature = "afc")]
569    #[cfg_attr(docsrs, doc(cfg(feature = "afc")))]
570    async fn create_afc_channel(
571        team: TeamId,
572        peer_id: DeviceId,
573        label_id: LabelId,
574    ) -> Result<AfcSendChannelInfo>;
575    /// Delete a AFC channel.
576    #[cfg(feature = "afc")]
577    #[cfg_attr(docsrs, doc(cfg(feature = "afc")))]
578    async fn delete_afc_channel(chan: AfcLocalChannelId) -> Result<()>;
579    /// Accept a receive-only AFC channel by processing a peer's ctrl message.
580    #[cfg(feature = "afc")]
581    #[cfg_attr(docsrs, doc(cfg(feature = "afc")))]
582    async fn accept_afc_channel(team: TeamId, ctrl: AfcCtrl) -> Result<AfcReceiveChannelInfo>;
583}