wasmcloud_secrets_client/
lib.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
use async_nats::HeaderMap;
use nkeys::XKey;
use wasmcloud_secrets_types::{
    Secret, SecretRequest, SecretResponse, RESPONSE_XKEY, WASMCLOUD_HOST_XKEY,
};

/// Default API version of the secrets API implementation in wasmCloud
const DEFAULT_API_VERSION: &str = "v1alpha1";

/// Errors that can be returned during creation/use of a [`Client`]
#[derive(Debug, thiserror::Error)]
pub enum SecretClientError {
    #[error("failed to convert server xkey: {0}")]
    ConvertServerXkey(String),
    #[error("failed to parse server xkey: {0}")]
    ParseServerXkey(nkeys::error::Error),
    #[error("failed to fetch server xkey: {0}")]
    RequestServerXkey(async_nats::RequestError),
    #[error("invalid xkey: {0}")]
    InvalidXkey(nkeys::error::Error),
    #[error("failed to seal secret request: {0}")]
    SealSecretRequest(nkeys::error::Error),
    #[error("failed to send secret request: {0}")]
    SendSecretRequest(async_nats::RequestError),
    #[error("failed to serialize secret request: {0}")]
    SerializeSecretRequest(serde_json::error::Error),
    #[error("failed to parse xkey from server response: {0}")]
    ParseServerResponseXkey(nkeys::error::Error),
    #[error("failed to open secret response: {0}")]
    OpenSecretResponse(nkeys::error::Error),
    #[error("failed to deserialize secret response: {0}")]
    DeserializeSecretResponse(serde_json::error::Error),
    #[error("server error: {0}")]
    Server(String),
    #[error("missing secret: {0}")]
    MissingSecret(String),
}

/// Topic on which secrets can be requested.
///
/// This topic is normally a *prefix* from which other requests can be made,
/// for example retrieving a secret or a server xkey.
#[derive(Debug)]
struct SecretsTopic(String);

impl SecretsTopic {
    pub(crate) fn new(prefix: &str, backend: &str, api_version: Option<&str>) -> Self {
        let version = api_version.unwrap_or(DEFAULT_API_VERSION);
        Self(format!("{}.{}.{}", prefix, version, backend))
    }

    pub fn get(&self) -> String {
        format!("{}.{}", self.0, "get")
    }

    pub fn server_xkey(&self) -> String {
        format!("{}.{}", self.0, "server_xkey")
    }
}

/// NATS client that can be used to interact with secrets
#[derive(Debug)]
pub struct Client {
    /// NATS client to use to make requests
    client: async_nats::Client,
    /// Topic on which secrets-related requests can be made
    topic: SecretsTopic,
    /// Server Xkey (retrieved at client creation time)
    server_xkey: XKey,
}

impl Client {
    /// Create a new [`Client`], negotiating a server Xkey along the way
    pub async fn new(
        backend: &str,
        prefix: &str,
        nats_client: async_nats::Client,
    ) -> Result<Self, SecretClientError> {
        Self::new_with_version(backend, prefix, nats_client, None).await
    }

    /// Create a new [`Client`] with a specific API version
    pub async fn new_with_version(
        backend: &str,
        prefix: &str,
        nats_client: async_nats::Client,
        api_version: Option<&str>,
    ) -> Result<Self, SecretClientError> {
        let secrets_topic = SecretsTopic::new(prefix, backend, api_version);

        // Fetch server XKey so we can use it to encrypt requests to the server.
        let resp = nats_client
            .request(secrets_topic.server_xkey(), "".into())
            .await
            .map_err(SecretClientError::RequestServerXkey)?;
        let s = std::str::from_utf8(&resp.payload)
            .map_err(|e| SecretClientError::ConvertServerXkey(e.to_string()))?;
        let server_xkey = XKey::from_public_key(s).map_err(SecretClientError::ParseServerXkey)?;

        Ok(Self {
            client: nats_client,
            topic: secrets_topic,
            server_xkey,
        })
    }

    /// Retrieve a given secret
    pub async fn get(
        &self,
        secret_request: SecretRequest,
        request_xkey: XKey,
    ) -> Result<Secret, SecretClientError> {
        // Ensure the provided xkey can be used for sealing
        if let Err(e) = request_xkey.seed() {
            return Err(SecretClientError::InvalidXkey(e));
        }

        let request = serde_json::to_string(&secret_request)
            .map_err(SecretClientError::SerializeSecretRequest)?;
        let encrypted_request = request_xkey
            .seal(request.as_bytes(), &self.server_xkey)
            .map_err(SecretClientError::SealSecretRequest)?;

        let response = self
            .client
            .request_with_headers(
                self.topic.get(),
                self.request_headers(request_xkey.public_key()),
                encrypted_request.into(),
            )
            .await
            .map_err(SecretClientError::SendSecretRequest)?;

        let headers = response.headers.unwrap_or_default();
        // Check whether we got a 'Server-Response-Key' header, signifying an
        // encrypted payload. Otherwise assume that we received an error and
        // we handle that instead.
        let Some(response_xkey_header) = headers.get(RESPONSE_XKEY) else {
            let sr: SecretResponse = serde_json::from_slice(&response.payload)
                .map_err(SecretClientError::DeserializeSecretResponse)?;

            if let Some(error) = sr.error {
                return Err(SecretClientError::Server(error.to_string()));
            }
            return Err(SecretClientError::Server(
                "unhandled server error (the server errored without explanation)".into(),
            ));
        };

        let response_xkey = XKey::from_public_key(response_xkey_header.as_str())
            .map_err(SecretClientError::ParseServerResponseXkey)?;

        let decrypted = request_xkey
            .open(&response.payload, &response_xkey)
            .map_err(SecretClientError::OpenSecretResponse)?;

        let sr: SecretResponse = serde_json::from_slice(&decrypted)
            .map_err(SecretClientError::DeserializeSecretResponse)?;

        sr.secret.ok_or_else(|| {
            SecretClientError::MissingSecret(format!(
                "no secret found with name [{}]",
                secret_request.key
            ))
        })
    }

    /// Generate NATS request headers
    fn request_headers(&self, pubkey: String) -> HeaderMap {
        let mut headers = HeaderMap::new();
        headers.insert(WASMCLOUD_HOST_XKEY, pubkey.as_str());
        headers
    }
}