1use crate::error::StorageError;
9use base64::engine::general_purpose::URL_SAFE_NO_PAD as B64;
10use base64::Engine;
11use doido_core::Result;
12use hmac::digest::KeyInit;
13use hmac::{Hmac, Mac};
14use serde::{Deserialize, Serialize};
15use sha2::Sha256;
16use std::time::{Duration, SystemTime, UNIX_EPOCH};
17
18type HmacSha256 = Hmac<Sha256>;
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
22#[serde(rename_all = "snake_case")]
23pub enum Disposition {
24 #[default]
26 Inline,
27 Attachment,
29}
30
31impl Disposition {
32 pub fn header(&self, filename: &str) -> String {
34 let kind = match self {
35 Disposition::Inline => "inline",
36 Disposition::Attachment => "attachment",
37 };
38 format!("{kind}; filename=\"{}\"", filename.replace('"', ""))
39 }
40}
41
42#[derive(Debug, Serialize, Deserialize)]
44struct Payload {
45 data: String,
47 purpose: String,
49 #[serde(skip_serializing_if = "Option::is_none")]
51 exp: Option<u64>,
52}
53
54fn now_secs() -> u64 {
55 SystemTime::now()
56 .duration_since(UNIX_EPOCH)
57 .map(|d| d.as_secs())
58 .unwrap_or(0)
59}
60
61#[derive(Clone)]
63pub struct Signer {
64 secret: Vec<u8>,
65}
66
67impl Signer {
68 pub fn new(secret: impl Into<Vec<u8>>) -> Self {
70 Self {
71 secret: secret.into(),
72 }
73 }
74
75 pub fn from_env() -> Self {
78 match std::env::var("DOIDO_SECRET_KEY_BASE") {
79 Ok(s) if !s.is_empty() => Self::new(s.into_bytes()),
80 _ => {
81 doido_core::tracing::warn!(
82 "DOIDO_SECRET_KEY_BASE unset; using an insecure development signing key"
83 );
84 Self::new(b"doido-storage-development-secret".to_vec())
85 }
86 }
87 }
88
89 fn mac(&self, message: &[u8]) -> Vec<u8> {
90 let mut mac =
91 HmacSha256::new_from_slice(&self.secret).expect("HMAC accepts keys of any length");
92 mac.update(message);
93 mac.finalize().into_bytes().to_vec()
94 }
95
96 pub fn sign(&self, data: &str, purpose: &str, expires_in: Option<Duration>) -> String {
98 let payload = Payload {
99 data: data.to_string(),
100 purpose: purpose.to_string(),
101 exp: expires_in.map(|d| now_secs() + d.as_secs()),
102 };
103 let json = serde_json::to_vec(&payload).expect("payload serializes");
104 let message = B64.encode(&json);
105 let sig = B64.encode(self.mac(message.as_bytes()));
106 format!("{message}--{sig}")
107 }
108
109 pub fn verify(&self, token: &str, purpose: &str) -> Result<String> {
112 let (message, sig) = token
113 .split_once("--")
114 .ok_or_else(|| StorageError::InvalidSignature("malformed token".into()))?;
115
116 let expected = self.mac(message.as_bytes());
117 let provided = B64
118 .decode(sig)
119 .map_err(|_| StorageError::InvalidSignature("bad signature encoding".into()))?;
120 let mut mac =
122 HmacSha256::new_from_slice(&self.secret).expect("HMAC accepts keys of any length");
123 mac.update(message.as_bytes());
124 mac.verify_slice(&provided)
125 .map_err(|_| StorageError::InvalidSignature("signature mismatch".into()))?;
126 let _ = expected; let json = B64
129 .decode(message)
130 .map_err(|_| StorageError::InvalidSignature("bad payload encoding".into()))?;
131 let payload: Payload = serde_json::from_slice(&json)
132 .map_err(|_| StorageError::InvalidSignature("bad payload".into()))?;
133
134 if payload.purpose != purpose {
135 return Err(StorageError::InvalidSignature("purpose mismatch".into()).into());
136 }
137 if let Some(exp) = payload.exp {
138 if now_secs() > exp {
139 return Err(StorageError::InvalidSignature("token expired".into()).into());
140 }
141 }
142 Ok(payload.data)
143 }
144}