Skip to main content

iroh_services/
api_secret.rs

1use std::{
2    collections::BTreeSet,
3    env::VarError,
4    fmt::{self, Display},
5    str::FromStr,
6};
7
8use anyhow::{Context, anyhow};
9use iroh::{EndpointAddr, EndpointId, SecretKey, TransportAddr};
10use iroh_tickets::{ParseError, Ticket};
11use serde::{Deserialize, Serialize};
12
13/// The secret material used to connect your services.iroh.computer project. The
14/// value of these should be treated like any other API key: guard them carefully.
15#[derive(Debug, Clone)]
16pub struct ApiSecret {
17    /// ED25519 secret used to construct rcans from
18    pub secret: SecretKey,
19    /// The iroh-services endpoint to direct requests to
20    pub remote: EndpointAddr,
21}
22
23impl Display for ApiSecret {
24    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
25        write!(f, "{}", Ticket::serialize(self))
26    }
27}
28
29#[derive(Serialize, Deserialize)]
30struct Variant0EndpointAddr {
31    endpoint_id: EndpointId,
32    addrs: BTreeSet<TransportAddr>,
33}
34
35/// Wire format for [`Ticket`].
36#[derive(Serialize, Deserialize)]
37enum TicketWireFormat {
38    Variant0(Variant0ServicesTicket),
39}
40
41#[derive(Serialize, Deserialize)]
42struct Variant0ServicesTicket {
43    secret: SecretKey,
44    addr: Variant0EndpointAddr,
45}
46
47impl Ticket for ApiSecret {
48    // KIND is the constant that's added to the front of a serialized ticket
49    // string. It should be a short, human readable string
50    const KIND: &'static str = "services";
51
52    fn to_bytes(&self) -> Vec<u8> {
53        let data = TicketWireFormat::Variant0(Variant0ServicesTicket {
54            secret: self.secret.clone(),
55            addr: Variant0EndpointAddr {
56                endpoint_id: self.remote.id,
57                addrs: self.remote.addrs.clone(),
58            },
59        });
60        postcard::to_stdvec(&data).expect("postcard serialization failed")
61    }
62
63    fn from_bytes(bytes: &[u8]) -> Result<Self, ParseError> {
64        let res: TicketWireFormat = postcard::from_bytes(bytes)?;
65        let TicketWireFormat::Variant0(Variant0ServicesTicket { secret, addr }) = res;
66        Ok(Self {
67            secret,
68            remote: EndpointAddr {
69                id: addr.endpoint_id,
70                addrs: addr.addrs.clone(),
71            },
72        })
73    }
74}
75
76impl FromStr for ApiSecret {
77    type Err = ParseError;
78
79    fn from_str(s: &str) -> Result<Self, Self::Err> {
80        iroh_tickets::Ticket::deserialize(s)
81    }
82}
83
84impl ApiSecret {
85    /// Creates a new ticket.
86    pub fn new(secret: SecretKey, remote: impl Into<EndpointAddr>) -> Self {
87        Self {
88            secret,
89            remote: remote.into(),
90        }
91    }
92
93    /// Read an Api Secret from a given environment variable
94    pub fn from_env_var(env_var: &str) -> anyhow::Result<Self> {
95        match std::env::var(env_var) {
96            Ok(ticket_string) => Self::from_str(&ticket_string)
97                .context(format!("invalid api secret at env var {env_var}")),
98            Err(VarError::NotPresent) => Err(anyhow!("{env_var} environment variable is not set")),
99            Err(VarError::NotUnicode(e)) => Err(anyhow!(
100                "{env_var} environment variable is not valid unicode: {:?}",
101                e
102            )),
103        }
104    }
105
106    /// The [`EndpointAddr`] of the provider for this ticket.
107    pub fn addr(&self) -> &EndpointAddr {
108        &self.remote
109    }
110}