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