1use std::{collections::BTreeMap, future::Future, io};
2
3use serde::{Deserialize, Serialize};
4use thiserror::Error;
5use ulid::Ulid;
6
7use crate::{
8 Client, RequestError, RequestProgress,
9 config::{ConfigurationError, clear_rotation_key, read_pairing_from},
10 crypto::Session,
11 protocol::{self, Method, Response},
12 websocket::RelayExchange,
13};
14
15#[derive(Debug, Eq, PartialEq)]
19#[non_exhaustive]
20pub enum Secret {
21 #[non_exhaustive]
23 Environment {
24 description: Option<String>,
26 variables: Vec<String>,
30 },
31
32 #[non_exhaustive]
34 Ssh {
35 description: Option<String>,
37 public_key: String,
39 },
40
41 #[non_exhaustive]
43 Unknown {
44 description: Option<String>,
46 secret_type: String,
48 },
49}
50
51pub type Secrets = BTreeMap<String, Secret>;
55
56#[derive(Eq, PartialEq)]
58#[non_exhaustive]
59pub enum SecretUpload {
60 Environment {
62 name: String,
64 description: Option<String>,
66 variables: BTreeMap<String, String>,
68 },
69
70 Ssh {
72 name: String,
74 description: Option<String>,
76 private_key: String,
81 },
82}
83
84impl SecretUpload {
85 pub fn name(&self) -> &str {
87 match self {
88 Self::Environment { name, .. } | Self::Ssh { name, .. } => name,
89 }
90 }
91}
92
93#[derive(Clone, Copy, Debug, Eq, PartialEq)]
95pub enum SecretUploadMode {
96 Create,
98
99 Replace,
101
102 Update,
104}
105
106#[derive(Debug, Error)]
108#[non_exhaustive]
109pub enum SecretUploadError {
110 #[error(transparent)]
112 Request(#[from] RequestError),
113
114 #[error("the device rejected the secret upload: {message}")]
116 Rejected {
117 message: String,
119 },
120}
121
122impl From<ConfigurationError> for SecretUploadError {
123 fn from(error: ConfigurationError) -> Self {
124 Self::Request(error.into())
125 }
126}
127
128impl From<crate::websocket::Error> for SecretUploadError {
129 fn from(error: crate::websocket::Error) -> Self {
130 Self::Request(error.into())
131 }
132}
133
134impl Client {
135 pub async fn list_secrets<P>(
153 &self,
154 cancellation: impl Future<Output = ()>,
155 mut progress: P,
156 ) -> Result<Secrets, RequestError>
157 where
158 P: FnMut(RequestProgress),
159 {
160 tokio::pin!(cancellation);
161 progress(RequestProgress::Preparing);
162 self.maybe_rotate_psk()?;
163 let pairing_path = self.pairing_path();
164 let pairing = read_pairing_from(&pairing_path)?;
165 let request_id = Ulid::generate();
166 let plaintext = self
167 .encode(&ListRequest {
168 method: Method::SecretList,
169 })
170 .map_err(RequestError::other)?;
171 let mut session = Session::new(&pairing, &request_id).map_err(RequestError::other)?;
172 let request = session
173 .seal_request(&plaintext)
174 .map_err(RequestError::other)?;
175 let mut relay = RelayExchange::authenticated(self, &pairing, &request_id.to_string())?;
176
177 progress(RequestProgress::WaitingForDelivery);
178 let response = tokio::select! {
179 biased;
180 _ = cancellation.as_mut() => return Err(RequestError::Interrupted),
181 response = relay.request(&request, || {
182 progress(RequestProgress::WaitingForResponse);
183 }) => response?,
184 };
185 progress(RequestProgress::Completing);
186 let plaintext = session
187 .open_response(response)
188 .map_err(RequestError::other)?;
189 if let Some(rotation_key) = pairing.rotation_key() {
190 clear_rotation_key(&pairing_path, rotation_key)?;
191 }
192 let response: ListResponse =
193 match protocol::decode_response(&plaintext).map_err(RequestError::other)? {
194 Response::Message(response) => response,
195 Response::Error(error) => {
196 if let Some(completion) =
197 protocol::seal_error_completion(self, &mut session, &error)
198 {
199 let _ = relay.complete_briefly(&completion).await;
200 }
201 return Err(RequestError::DeviceRejected {
202 code: error.code,
203 message: error.message,
204 });
205 }
206 };
207 let plaintext = self.encode(&EmptyMessage {}).map_err(RequestError::other)?;
208 let completion = session
209 .seal_completion(&plaintext)
210 .map_err(RequestError::other)?;
211 let interrupted = tokio::select! {
212 biased;
213 _ = cancellation.as_mut() => true,
214 result = relay.complete(&completion) => {
215 result?;
216 false
217 }
218 };
219 if interrupted {
220 let _ = relay.complete_briefly(&completion).await;
221 }
222 progress(RequestProgress::Completed);
223
224 response
225 .secrets
226 .into_iter()
227 .map(|(name, secret)| Ok((name, secret.try_into()?)))
228 .collect::<io::Result<_>>()
229 .map_err(RequestError::other)
230 }
231
232 pub async fn upload_secret<P>(
254 &self,
255 secret: &SecretUpload,
256 mode: SecretUploadMode,
257 cancellation: impl Future<Output = ()>,
258 mut progress: P,
259 ) -> Result<(), SecretUploadError>
260 where
261 P: FnMut(RequestProgress),
262 {
263 tokio::pin!(cancellation);
264 progress(RequestProgress::Preparing);
265 self.maybe_rotate_psk()?;
266 let pairing_path = self.pairing_path();
267 let pairing = read_pairing_from(&pairing_path)?;
268 let request_id = Ulid::generate();
269 let request_payload = UploadRequest {
270 method: Method::SecretUpload,
271 mode: mode.into(),
272 secret: UploadSecretMessage::from(secret),
273 };
274 let plaintext = self.encode(&request_payload).map_err(RequestError::other)?;
275 let mut session = Session::new(&pairing, &request_id).map_err(RequestError::other)?;
276 let request = session
277 .seal_request(&plaintext)
278 .map_err(RequestError::other)?;
279 let mut relay = RelayExchange::authenticated(self, &pairing, &request_id.to_string())?;
280
281 progress(RequestProgress::WaitingForDelivery);
282 let response = tokio::select! {
283 biased;
284 _ = cancellation.as_mut() => {
285 return Err(RequestError::Interrupted.into());
286 }
287 response = relay.request(&request, || {
288 progress(RequestProgress::WaitingForResponse);
289 }) => response?,
290 };
291 progress(RequestProgress::Completing);
292 let plaintext = session
293 .open_response(response)
294 .map_err(RequestError::other)?;
295 if let Some(rotation_key) = pairing.rotation_key() {
296 clear_rotation_key(&pairing_path, rotation_key)?;
297 }
298 let response: UploadResult =
299 match protocol::decode_response(&plaintext).map_err(RequestError::other)? {
300 Response::Message(response) => response,
301 Response::Error(error) => {
302 if let Some(completion) =
303 protocol::seal_error_completion(self, &mut session, &error)
304 {
305 let _ = relay.complete_briefly(&completion).await;
306 }
307 return Err(RequestError::DeviceRejected {
308 code: error.code,
309 message: error.message,
310 }
311 .into());
312 }
313 };
314 let completion = self.encode(&response).map_err(RequestError::other)?;
315 let completion = session
316 .seal_completion(&completion)
317 .map_err(RequestError::other)?;
318 tokio::select! {
319 biased;
320 _ = cancellation.as_mut() => {},
321 _ = relay.complete_briefly(&completion) => {},
322 }
323 progress(RequestProgress::Completed);
324
325 match response {
326 UploadResult::Received => Ok(()),
327 UploadResult::Rejected { message } => Err(SecretUploadError::Rejected { message }),
328 }
329 }
330}
331
332#[derive(Serialize)]
333struct ListRequest {
334 method: Method,
335}
336
337#[derive(Serialize)]
338struct EmptyMessage {}
339
340#[derive(Deserialize)]
341struct ListResponse {
342 secrets: BTreeMap<String, ListedSecretMessage>,
343}
344
345#[derive(Serialize)]
346struct UploadRequest<'a> {
347 method: Method,
348 mode: SecretUploadModeMessage,
349 secret: UploadSecretMessage<'a>,
350}
351
352#[derive(Serialize)]
353#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
354enum SecretUploadModeMessage {
355 Create,
356 Replace,
357 Update,
358}
359
360impl From<SecretUploadMode> for SecretUploadModeMessage {
361 fn from(mode: SecretUploadMode) -> Self {
362 match mode {
363 SecretUploadMode::Create => Self::Create,
364 SecretUploadMode::Replace => Self::Replace,
365 SecretUploadMode::Update => Self::Update,
366 }
367 }
368}
369
370#[derive(Deserialize, Serialize)]
371#[serde(tag = "result", rename_all = "SCREAMING_SNAKE_CASE")]
372enum UploadResult {
373 Received,
374 Rejected { message: String },
375}
376
377#[derive(Deserialize, Serialize)]
378pub(crate) struct SecretMessage<T> {
379 #[serde(default, skip_serializing_if = "Option::is_none")]
380 pub(crate) description: Option<String>,
381 #[serde(flatten)]
382 pub(crate) contents: SecretContentsMessage<T>,
383}
384
385#[derive(Deserialize, Serialize)]
386#[serde(tag = "type", rename_all = "snake_case")]
387pub(crate) enum SecretContentsMessage<T> {
388 Environment { variables: T },
389 Ssh { public_key: String },
390}
391
392#[derive(Deserialize, Serialize)]
393pub(crate) struct EnvironmentVariableMessage {
394 pub(crate) value: String,
395}
396
397#[derive(Deserialize)]
398struct ListedSecretMessage {
399 #[serde(default)]
400 description: Option<String>,
401 #[serde(rename = "type")]
402 secret_type: String,
403 #[serde(flatten)]
404 metadata: BTreeMap<String, serde_json::Value>,
405}
406
407#[derive(Serialize)]
408#[serde(tag = "type", rename_all = "snake_case")]
409enum UploadSecretMessage<'a> {
410 Environment {
411 name: &'a str,
412 #[serde(skip_serializing_if = "Option::is_none")]
413 description: Option<&'a str>,
414 variables: BTreeMap<&'a str, UploadEnvironmentVariableMessage<'a>>,
415 },
416 Ssh {
417 name: &'a str,
418 #[serde(skip_serializing_if = "Option::is_none")]
419 description: Option<&'a str>,
420 private_key: &'a str,
421 },
422}
423
424#[derive(Serialize)]
425struct UploadEnvironmentVariableMessage<'a> {
426 value: &'a str,
427}
428
429impl<'a> From<&'a SecretUpload> for UploadSecretMessage<'a> {
430 fn from(secret: &'a SecretUpload) -> Self {
431 match secret {
432 SecretUpload::Environment {
433 name,
434 description,
435 variables,
436 } => Self::Environment {
437 name,
438 description: description.as_deref(),
439 variables: variables
440 .iter()
441 .map(|(name, value)| {
442 (name.as_str(), UploadEnvironmentVariableMessage { value })
443 })
444 .collect(),
445 },
446 SecretUpload::Ssh {
447 name,
448 description,
449 private_key,
450 } => Self::Ssh {
451 name,
452 description: description.as_deref(),
453 private_key,
454 },
455 }
456 }
457}
458
459impl TryFrom<ListedSecretMessage> for Secret {
460 type Error = io::Error;
461
462 fn try_from(mut secret: ListedSecretMessage) -> Result<Self, Self::Error> {
463 match secret.secret_type.as_str() {
464 "environment" => {
465 let variables = secret.metadata.remove("variables").ok_or_else(|| {
466 io::Error::other("environment secret metadata has no variables")
467 })?;
468 let mut variables: Vec<String> =
469 serde_json::from_value(variables).map_err(io::Error::other)?;
470 variables.sort();
471 variables.dedup();
472 Ok(Self::Environment {
473 description: secret.description,
474 variables,
475 })
476 }
477 "ssh" => Ok(Self::Ssh {
478 description: secret.description,
479 public_key: serde_json::from_value(
480 secret
481 .metadata
482 .remove("public_key")
483 .ok_or_else(|| io::Error::other("SSH secret metadata has no public key"))?,
484 )
485 .map_err(io::Error::other)?,
486 }),
487 _ => Ok(Self::Unknown {
488 description: secret.description,
489 secret_type: secret.secret_type,
490 }),
491 }
492 }
493}