Skip to main content

vial_shared/
lib.rs

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/// Received via HTTPS from the server then decrypted to Payload struct
14#[serde_as]
15#[derive(Deserialize, Serialize, Clone, Debug)]
16pub struct EncryptedPayload {
17    #[serde_as(as = "Base64")]
18    pub payload: Vec<u8>,
19}
20
21/// Gets returned by the server when creating a new secret
22#[derive(Deserialize, Serialize, Clone, Debug)]
23pub struct SecretId(pub String);
24
25/// Sent to the server when creating a new secret
26#[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/// Once the bytes gets decrypted, should be deserialized to this struct.
36/// Similarly, before encrypting, this struct is serialized to bytes.
37///
38/// After deserializing, the bytes can be deserialized to `FullSecretV1` struct.
39/// Similarly, payload is generated from the serialized `FullSecretV1` struct.
40#[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/// Helper function to sanitize a filename from potentially unsafe characters
188#[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}