wasmcloud_secrets_client/
lib.rs

1use async_nats::HeaderMap;
2use nkeys::XKey;
3use wasmcloud_secrets_types::{
4    Secret, SecretRequest, SecretResponse, RESPONSE_XKEY, WASMCLOUD_HOST_XKEY,
5};
6
7/// Default API version of the secrets API implementation in wasmCloud
8const DEFAULT_API_VERSION: &str = "v1alpha1";
9
10/// Errors that can be returned during creation/use of a [`Client`]
11#[derive(Debug, thiserror::Error)]
12pub enum SecretClientError {
13    #[error("failed to convert server xkey: {0}")]
14    ConvertServerXkey(String),
15    #[error("failed to parse server xkey: {0}")]
16    ParseServerXkey(nkeys::error::Error),
17    #[error("failed to fetch server xkey: {0}")]
18    RequestServerXkey(async_nats::RequestError),
19    #[error("invalid xkey: {0}")]
20    InvalidXkey(nkeys::error::Error),
21    #[error("failed to seal secret request: {0}")]
22    SealSecretRequest(nkeys::error::Error),
23    #[error("failed to send secret request: {0}")]
24    SendSecretRequest(async_nats::RequestError),
25    #[error("failed to serialize secret request: {0}")]
26    SerializeSecretRequest(serde_json::error::Error),
27    #[error("failed to parse xkey from server response: {0}")]
28    ParseServerResponseXkey(nkeys::error::Error),
29    #[error("failed to open secret response: {0}")]
30    OpenSecretResponse(nkeys::error::Error),
31    #[error("failed to deserialize secret response: {0}")]
32    DeserializeSecretResponse(serde_json::error::Error),
33    #[error("server error: {0}")]
34    Server(String),
35    #[error("missing secret: {0}")]
36    MissingSecret(String),
37}
38
39/// Topic on which secrets can be requested.
40///
41/// This topic is normally a *prefix* from which other requests can be made,
42/// for example retrieving a secret or a server xkey.
43#[derive(Debug)]
44struct SecretsTopic(String);
45
46impl SecretsTopic {
47    pub(crate) fn new(prefix: &str, backend: &str, api_version: Option<&str>) -> Self {
48        let version = api_version.unwrap_or(DEFAULT_API_VERSION);
49        Self(format!("{prefix}.{version}.{backend}"))
50    }
51
52    pub fn get(&self) -> String {
53        format!("{}.{}", self.0, "get")
54    }
55
56    pub fn server_xkey(&self) -> String {
57        format!("{}.{}", self.0, "server_xkey")
58    }
59}
60
61/// NATS client that can be used to interact with secrets
62#[derive(Debug)]
63pub struct Client {
64    /// NATS client to use to make requests
65    client: async_nats::Client,
66    /// Topic on which secrets-related requests can be made
67    topic: SecretsTopic,
68    /// Server Xkey (retrieved at client creation time)
69    server_xkey: XKey,
70}
71
72impl Client {
73    /// Create a new [`Client`], negotiating a server Xkey along the way
74    pub async fn new(
75        backend: &str,
76        prefix: &str,
77        nats_client: async_nats::Client,
78    ) -> Result<Self, SecretClientError> {
79        Self::new_with_version(backend, prefix, nats_client, None).await
80    }
81
82    /// Create a new [`Client`] with a specific API version
83    pub async fn new_with_version(
84        backend: &str,
85        prefix: &str,
86        nats_client: async_nats::Client,
87        api_version: Option<&str>,
88    ) -> Result<Self, SecretClientError> {
89        let secrets_topic = SecretsTopic::new(prefix, backend, api_version);
90
91        // Fetch server XKey so we can use it to encrypt requests to the server.
92        let resp = nats_client
93            .request(secrets_topic.server_xkey(), "".into())
94            .await
95            .map_err(SecretClientError::RequestServerXkey)?;
96        let s = std::str::from_utf8(&resp.payload)
97            .map_err(|e| SecretClientError::ConvertServerXkey(e.to_string()))?;
98        let server_xkey = XKey::from_public_key(s).map_err(SecretClientError::ParseServerXkey)?;
99
100        Ok(Self {
101            client: nats_client,
102            topic: secrets_topic,
103            server_xkey,
104        })
105    }
106
107    /// Retrieve a given secret
108    pub async fn get(
109        &self,
110        secret_request: SecretRequest,
111        request_xkey: XKey,
112    ) -> Result<Secret, SecretClientError> {
113        // Ensure the provided xkey can be used for sealing
114        if let Err(e) = request_xkey.seed() {
115            return Err(SecretClientError::InvalidXkey(e));
116        }
117
118        let request = serde_json::to_string(&secret_request)
119            .map_err(SecretClientError::SerializeSecretRequest)?;
120        let encrypted_request = request_xkey
121            .seal(request.as_bytes(), &self.server_xkey)
122            .map_err(SecretClientError::SealSecretRequest)?;
123
124        let response = self
125            .client
126            .request_with_headers(
127                self.topic.get(),
128                self.request_headers(request_xkey.public_key()),
129                encrypted_request.into(),
130            )
131            .await
132            .map_err(SecretClientError::SendSecretRequest)?;
133
134        let headers = response.headers.unwrap_or_default();
135        // Check whether we got a 'Server-Response-Key' header, signifying an
136        // encrypted payload. Otherwise assume that we received an error and
137        // we handle that instead.
138        let Some(response_xkey_header) = headers.get(RESPONSE_XKEY) else {
139            let sr: SecretResponse = serde_json::from_slice(&response.payload)
140                .map_err(SecretClientError::DeserializeSecretResponse)?;
141
142            if let Some(error) = sr.error {
143                return Err(SecretClientError::Server(error.to_string()));
144            }
145            return Err(SecretClientError::Server(
146                "unhandled server error (the server errored without explanation)".into(),
147            ));
148        };
149
150        let response_xkey = XKey::from_public_key(response_xkey_header.as_str())
151            .map_err(SecretClientError::ParseServerResponseXkey)?;
152
153        let decrypted = request_xkey
154            .open(&response.payload, &response_xkey)
155            .map_err(SecretClientError::OpenSecretResponse)?;
156
157        let sr: SecretResponse = serde_json::from_slice(&decrypted)
158            .map_err(SecretClientError::DeserializeSecretResponse)?;
159
160        sr.secret.ok_or_else(|| {
161            SecretClientError::MissingSecret(format!(
162                "no secret found with name [{}]",
163                secret_request.key
164            ))
165        })
166    }
167
168    /// Generate NATS request headers
169    fn request_headers(&self, pubkey: String) -> HeaderMap {
170        let mut headers = HeaderMap::new();
171        headers.insert(WASMCLOUD_HOST_XKEY, pubkey.as_str());
172        headers
173    }
174}