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