1#[cfg(all(not(target_arch = "wasm32"), feature = "config"))]
2pub mod config;
3
4use chrono::NaiveDateTime;
5use serde::{Deserialize, Serialize};
6use serde_with::base64::Base64;
7use serde_with::serde_as;
8use std::sync::Arc;
9
10#[cfg(not(target_arch = "wasm32"))]
11use std::path::Path;
12
13#[serde_as]
15#[derive(Deserialize, Serialize, Clone, Debug)]
16pub struct EncryptedPayload {
17 #[serde_as(as = "Base64")]
18 pub payload: Vec<u8>,
19}
20
21#[derive(Deserialize, Serialize, Clone, Debug)]
23pub struct SecretId(pub String);
24
25#[serde_as]
27#[derive(Deserialize, Serialize, Clone, Debug)]
28pub struct CreateSecretRequest {
29 #[serde_as(as = "Base64")]
30 pub ciphertext: Vec<u8>,
31 pub expires_at: Option<NaiveDateTime>,
32 pub max_views: Option<i32>,
33}
34
35#[derive(Deserialize, Serialize, Clone, Debug)]
41pub struct Payload {
42 pub version: u8,
43 pub payload: Vec<u8>,
44}
45
46impl Payload {
47 pub fn new(full_secret: &FullSecretV1) -> Result<Self, postcard::Error> {
48 let payload = full_secret.to_bytes()?;
49 Ok(Self {
50 payload,
51 version: 1,
52 })
53 }
54 pub fn to_bytes(&self) -> Result<Vec<u8>, postcard::Error> {
55 postcard::to_stdvec(self)
56 }
57
58 pub fn from_bytes(bytes: Vec<u8>) -> Result<Self, postcard::Error> {
59 postcard::from_bytes(&bytes)
60 }
61
62 pub fn to_full_secret(&self) -> Result<FullSecretV1, postcard::Error> {
63 let secret = FullSecretV1::from_bytes(&self.payload)?;
64 Ok(secret)
65 }
66}
67
68#[derive(Deserialize, Serialize, Clone, Debug)]
69pub struct FullSecretV1 {
70 pub text: String,
71 pub files: Vec<SecretFileV1>,
72}
73
74#[derive(Deserialize, Serialize, Clone, Debug)]
75pub struct SecretFileV1 {
76 filename: String,
77 content: Vec<u8>,
78}
79
80impl SecretFileV1 {
81 #[cfg(not(target_arch = "wasm32"))]
82 pub fn new(filename: &str, content: Vec<u8>) -> Result<Self, Box<dyn std::error::Error>> {
83 let safe_filename = sanitize_filename(filename)?;
84
85 if content.is_empty() {
86 return Err("File is empty".into());
87 }
88
89 Ok(Self {
90 filename: safe_filename,
91 content,
92 })
93 }
94
95 #[must_use]
96 pub fn filename(&self) -> &str {
97 &self.filename
98 }
99
100 #[must_use]
101 pub fn content(&self) -> &[u8] {
102 &self.content
103 }
104
105 #[cfg(not(target_arch = "wasm32"))]
106 pub fn write(&self, path: &Path) -> Result<(), std::io::Error> {
107 std::fs::write(path, self.content())?;
108 Ok(())
109 }
110}
111
112impl FullSecretV1 {
113 fn to_bytes(&self) -> Result<Vec<u8>, postcard::Error> {
114 postcard::to_stdvec(self)
115 }
116
117 fn from_bytes(bytes: &[u8]) -> Result<Self, postcard::Error> {
118 postcard::from_bytes(bytes)
119 }
120
121 pub fn to_payload(&self) -> Result<Payload, postcard::Error> {
122 let payload = self.to_bytes()?;
123 Ok(Payload {
124 version: 1,
125 payload,
126 })
127 }
128
129 #[must_use]
130 pub fn total_files(&self) -> usize {
131 self.files.len()
132 }
133
134 #[must_use]
135 pub fn into_shared(self) -> FullSecret {
136 FullSecret {
137 text: Arc::new(self.text),
138 files: self
139 .files
140 .into_iter()
141 .map(|f| SecretFile {
142 filename: f.filename,
143 content: Arc::new(f.content),
144 })
145 .collect(),
146 }
147 }
148}
149
150#[derive(Clone, Debug)]
151pub struct FullSecret {
152 pub text: Arc<String>,
153 pub files: Vec<SecretFile>,
154}
155
156#[derive(Clone, Debug)]
157pub struct SecretFile {
158 pub filename: String,
159 pub content: Arc<Vec<u8>>,
160}
161
162impl FullSecret {
163 #[must_use]
164 pub fn total_files(&self) -> usize {
165 self.files.len()
166 }
167}
168
169impl SecretFile {
170 #[must_use]
171 pub fn filename(&self) -> &str {
172 &self.filename
173 }
174
175 #[must_use]
176 pub fn content(&self) -> &[u8] {
177 &self.content
178 }
179
180 #[cfg(not(target_arch = "wasm32"))]
181 pub fn write(&self, path: &Path) -> Result<(), std::io::Error> {
182 std::fs::write(path, self.content())?;
183 Ok(())
184 }
185}
186
187#[cfg(not(target_arch = "wasm32"))]
189pub fn sanitize_filename(name: &str) -> Result<String, &'static str> {
190 let path = Path::new(name);
191
192 if path.components().count() != 1 {
193 return Err("Invalid filename (must not contain path separators)");
194 }
195
196 let file_name = path.file_name().ok_or("Invalid filename")?;
197
198 let file_name = file_name.to_str().ok_or("Filename is not valid UTF-8")?;
199
200 if file_name.is_empty() || file_name == "." || file_name == ".." {
201 return Err("Invalid filename");
202 }
203
204 Ok(file_name.to_string())
205}