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