1use std::{collections::BTreeMap, future::Future, io};
2
3use serde::{Deserialize, Serialize};
4use thiserror::Error;
5use ulid::Ulid;
6
7use crate::{
8 Client, RequestError,
9 config::{ConfigurationError, clear_rotation_key, read_pairing_from},
10 crypto::Session,
11 pairing::RotationError,
12 protocol::{self, Method, Response},
13 websocket::RelayExchange,
14};
15
16#[derive(Debug, Eq, PartialEq)]
20#[non_exhaustive]
21pub enum Secret {
22 #[non_exhaustive]
24 Environment {
25 description: Option<String>,
27 variables: Vec<String>,
31 },
32
33 #[non_exhaustive]
35 Ssh {
36 description: Option<String>,
38 public_key: String,
40 },
41
42 #[non_exhaustive]
44 Unknown {
45 description: Option<String>,
47 secret_type: String,
49 },
50}
51
52pub type Secrets = BTreeMap<String, Secret>;
56
57#[derive(Eq, PartialEq)]
59#[non_exhaustive]
60pub enum SecretUpload {
61 Environment {
63 name: String,
65 description: Option<String>,
67 variables: BTreeMap<String, String>,
69 },
70
71 Ssh {
73 name: String,
75 description: Option<String>,
77 private_key: String,
79 },
80}
81
82impl SecretUpload {
83 pub fn name(&self) -> &str {
85 match self {
86 Self::Environment { name, .. } | Self::Ssh { name, .. } => name,
87 }
88 }
89}
90
91#[derive(Clone, Copy, Debug, Eq, PartialEq)]
93pub enum SecretUploadMode {
94 Create,
96
97 Replace,
99
100 Update,
102}
103
104#[derive(Clone, Copy, Debug, Eq, PartialEq)]
111#[non_exhaustive]
112pub enum SecretListProgress {
113 Preparing,
115
116 WaitingForDelivery,
118
119 WaitingForResponse,
121
122 Completing,
124
125 Completed,
127}
128
129#[derive(Clone, Copy, Debug, Eq, PartialEq)]
136#[non_exhaustive]
137pub enum SecretUploadProgress {
138 Preparing,
140
141 WaitingForDelivery,
143
144 WaitingForResponse,
146
147 Completing,
149
150 Completed,
152}
153
154#[derive(Debug, Error)]
156#[non_exhaustive]
157pub enum SecretUploadError {
158 #[error(transparent)]
160 Request(#[from] RequestError),
161
162 #[error("the device rejected the secret upload: {message}")]
164 Rejected {
165 message: String,
167 },
168}
169
170impl From<ConfigurationError> for SecretUploadError {
171 fn from(error: ConfigurationError) -> Self {
172 Self::Request(error.into())
173 }
174}
175
176impl From<crate::websocket::Error> for SecretUploadError {
177 fn from(error: crate::websocket::Error) -> Self {
178 Self::Request(error.into())
179 }
180}
181
182impl Client {
183 pub async fn list_secrets<P>(
201 &self,
202 cancellation: impl Future<Output = ()>,
203 mut progress: P,
204 ) -> Result<Secrets, RequestError>
205 where
206 P: FnMut(SecretListProgress),
207 {
208 tokio::pin!(cancellation);
209 progress(SecretListProgress::Preparing);
210 self.prepare_request()?;
211 let pairing_path = self.pairing_path()?;
212 let pairing = read_pairing_from(&pairing_path)?;
213 let request_id = Ulid::generate();
214 let plaintext = self
215 .encode(&ListRequest {
216 method: Method::SecretList,
217 })
218 .map_err(RequestError::other)?;
219 let mut session = Session::new(&pairing, &request_id).map_err(RequestError::other)?;
220 let request = session
221 .seal_request(&plaintext)
222 .map_err(RequestError::other)?;
223 let mut relay = RelayExchange::authenticated(&pairing, &request_id.to_string())?;
224
225 progress(SecretListProgress::WaitingForDelivery);
226 let response = tokio::select! {
227 biased;
228 _ = cancellation.as_mut() => return Err(RequestError::Interrupted),
229 response = relay.request(&request, || {
230 progress(SecretListProgress::WaitingForResponse);
231 }) => response?,
232 };
233 progress(SecretListProgress::Completing);
234 let plaintext = session
235 .open_response(response)
236 .map_err(RequestError::other)?;
237 if let Some(rotation_key) = pairing.rotation_key() {
238 clear_rotation_key(&pairing_path, rotation_key)?;
239 }
240 let response: ListResponse =
241 match protocol::decode_response(&plaintext).map_err(RequestError::other)? {
242 Response::Message(response) => response,
243 Response::Error(error) => {
244 if let Some(completion) =
245 protocol::seal_error_completion(self, &mut session, &error)
246 {
247 let _ = relay.complete_briefly(&completion).await;
248 }
249 return Err(RequestError::DeviceRejected {
250 code: error.code,
251 message: error.message,
252 });
253 }
254 };
255 let plaintext = self.encode(&EmptyMessage {}).map_err(RequestError::other)?;
256 let completion = session
257 .seal_completion(&plaintext)
258 .map_err(RequestError::other)?;
259 let interrupted = tokio::select! {
260 biased;
261 _ = cancellation.as_mut() => true,
262 result = relay.complete(&completion) => {
263 result?;
264 false
265 }
266 };
267 if interrupted {
268 let _ = relay.complete_briefly(&completion).await;
269 }
270 progress(SecretListProgress::Completed);
271
272 response
273 .secrets
274 .into_iter()
275 .map(|(name, secret)| Ok((name, secret.try_into()?)))
276 .collect::<io::Result<_>>()
277 .map_err(RequestError::other)
278 }
279
280 pub async fn upload_secret<P>(
302 &self,
303 secret: &SecretUpload,
304 mode: SecretUploadMode,
305 cancellation: impl Future<Output = ()>,
306 mut progress: P,
307 ) -> Result<(), SecretUploadError>
308 where
309 P: FnMut(SecretUploadProgress),
310 {
311 tokio::pin!(cancellation);
312 progress(SecretUploadProgress::Preparing);
313 self.prepare_request()?;
314 let pairing_path = self.pairing_path()?;
315 let pairing = read_pairing_from(&pairing_path)?;
316 let request_id = Ulid::generate();
317 let request_payload = UploadRequest {
318 method: Method::SecretUpload,
319 mode: mode.into(),
320 secret: UploadSecretMessage::from(secret),
321 };
322 let plaintext = self.encode(&request_payload).map_err(RequestError::other)?;
323 let mut session = Session::new(&pairing, &request_id).map_err(RequestError::other)?;
324 let request = session
325 .seal_request(&plaintext)
326 .map_err(RequestError::other)?;
327 let mut relay = RelayExchange::authenticated(&pairing, &request_id.to_string())?;
328
329 progress(SecretUploadProgress::WaitingForDelivery);
330 let response = tokio::select! {
331 biased;
332 _ = cancellation.as_mut() => {
333 return Err(RequestError::Interrupted.into());
334 }
335 response = relay.request(&request, || {
336 progress(SecretUploadProgress::WaitingForResponse);
337 }) => response?,
338 };
339 progress(SecretUploadProgress::Completing);
340 let plaintext = session
341 .open_response(response)
342 .map_err(RequestError::other)?;
343 if let Some(rotation_key) = pairing.rotation_key() {
344 clear_rotation_key(&pairing_path, rotation_key)?;
345 }
346 let response: UploadResult =
347 match protocol::decode_response(&plaintext).map_err(RequestError::other)? {
348 Response::Message(response) => response,
349 Response::Error(error) => {
350 if let Some(completion) =
351 protocol::seal_error_completion(self, &mut session, &error)
352 {
353 let _ = relay.complete_briefly(&completion).await;
354 }
355 return Err(RequestError::DeviceRejected {
356 code: error.code,
357 message: error.message,
358 }
359 .into());
360 }
361 };
362 let completion = self.encode(&response).map_err(RequestError::other)?;
363 let completion = session
364 .seal_completion(&completion)
365 .map_err(RequestError::other)?;
366 tokio::select! {
367 biased;
368 _ = cancellation.as_mut() => {},
369 _ = relay.complete_briefly(&completion) => {},
370 }
371 progress(SecretUploadProgress::Completed);
372
373 match response {
374 UploadResult::Received => Ok(()),
375 UploadResult::Rejected { message } => Err(SecretUploadError::Rejected { message }),
376 }
377 }
378
379 fn prepare_request(&self) -> Result<(), RequestError> {
380 self.maybe_rotate_psk().map_err(|error| match error {
381 RotationError::Configuration(error) => RequestError::Configuration(error),
382 RotationError::Other(error) => RequestError::Other(error),
383 })?;
384 Ok(())
385 }
386}
387
388#[derive(Serialize)]
389struct ListRequest {
390 method: Method,
391}
392
393#[derive(Serialize)]
394struct EmptyMessage {}
395
396#[derive(Deserialize)]
397struct ListResponse {
398 secrets: BTreeMap<String, ListedSecretMessage>,
399}
400
401#[derive(Serialize)]
402struct UploadRequest<'a> {
403 method: Method,
404 mode: SecretUploadModeMessage,
405 secret: UploadSecretMessage<'a>,
406}
407
408#[derive(Serialize)]
409#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
410enum SecretUploadModeMessage {
411 Create,
412 Replace,
413 Update,
414}
415
416impl From<SecretUploadMode> for SecretUploadModeMessage {
417 fn from(mode: SecretUploadMode) -> Self {
418 match mode {
419 SecretUploadMode::Create => Self::Create,
420 SecretUploadMode::Replace => Self::Replace,
421 SecretUploadMode::Update => Self::Update,
422 }
423 }
424}
425
426#[derive(Deserialize, Serialize)]
427#[serde(tag = "result", rename_all = "SCREAMING_SNAKE_CASE")]
428enum UploadResult {
429 Received,
430 Rejected { message: String },
431}
432
433#[derive(Deserialize, Serialize)]
434pub(crate) struct SecretMessage<T> {
435 #[serde(default, skip_serializing_if = "Option::is_none")]
436 pub(crate) description: Option<String>,
437 #[serde(flatten)]
438 pub(crate) contents: SecretContentsMessage<T>,
439}
440
441#[derive(Deserialize, Serialize)]
442#[serde(tag = "type", rename_all = "snake_case")]
443pub(crate) enum SecretContentsMessage<T> {
444 Environment { variables: T },
445 Ssh { public_key: String },
446}
447
448#[derive(Deserialize, Serialize)]
449pub(crate) struct EnvironmentVariableMessage {
450 pub(crate) value: String,
451}
452
453#[derive(Deserialize)]
454struct ListedSecretMessage {
455 #[serde(default)]
456 description: Option<String>,
457 #[serde(rename = "type")]
458 secret_type: String,
459 #[serde(flatten)]
460 metadata: BTreeMap<String, serde_json::Value>,
461}
462
463#[derive(Serialize)]
464#[serde(tag = "type", rename_all = "snake_case")]
465enum UploadSecretMessage<'a> {
466 Environment {
467 name: &'a str,
468 #[serde(skip_serializing_if = "Option::is_none")]
469 description: Option<&'a str>,
470 variables: BTreeMap<&'a str, UploadEnvironmentVariableMessage<'a>>,
471 },
472 Ssh {
473 name: &'a str,
474 #[serde(skip_serializing_if = "Option::is_none")]
475 description: Option<&'a str>,
476 private_key: &'a str,
477 },
478}
479
480#[derive(Serialize)]
481struct UploadEnvironmentVariableMessage<'a> {
482 value: &'a str,
483}
484
485impl<'a> From<&'a SecretUpload> for UploadSecretMessage<'a> {
486 fn from(secret: &'a SecretUpload) -> Self {
487 match secret {
488 SecretUpload::Environment {
489 name,
490 description,
491 variables,
492 } => Self::Environment {
493 name,
494 description: description.as_deref(),
495 variables: variables
496 .iter()
497 .map(|(name, value)| {
498 (name.as_str(), UploadEnvironmentVariableMessage { value })
499 })
500 .collect(),
501 },
502 SecretUpload::Ssh {
503 name,
504 description,
505 private_key,
506 } => Self::Ssh {
507 name,
508 description: description.as_deref(),
509 private_key,
510 },
511 }
512 }
513}
514
515impl TryFrom<ListedSecretMessage> for Secret {
516 type Error = io::Error;
517
518 fn try_from(mut secret: ListedSecretMessage) -> Result<Self, Self::Error> {
519 match secret.secret_type.as_str() {
520 "environment" => {
521 let variables = secret.metadata.remove("variables").ok_or_else(|| {
522 io::Error::other("environment secret metadata has no variables")
523 })?;
524 let mut variables: Vec<String> =
525 serde_json::from_value(variables).map_err(io::Error::other)?;
526 variables.sort();
527 variables.dedup();
528 Ok(Self::Environment {
529 description: secret.description,
530 variables,
531 })
532 }
533 "ssh" => Ok(Self::Ssh {
534 description: secret.description,
535 public_key: serde_json::from_value(
536 secret
537 .metadata
538 .remove("public_key")
539 .ok_or_else(|| io::Error::other("SSH secret metadata has no public key"))?,
540 )
541 .map_err(io::Error::other)?,
542 }),
543 _ => Ok(Self::Unknown {
544 description: secret.description,
545 secret_type: secret.secret_type,
546 }),
547 }
548 }
549}