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