Skip to main content

kernoid_client/e2e/
vault.rs

1// SPDX-License-Identifier: MIT
2//! Vaults (RFC 0006, 6): directories whose content only their members read. A vault has an OpenPGP key of its own,
3//! made here; its secret key goes to the server only wrapped – encrypted to the key of each member (`VaultKey/get`,
4//! `/set`). Inside, files are encrypted to the vault's key before they are uploaded, and names are an id with the real
5//! name encrypted beside it (`kernoid:encryptedName`). Everything here needs the account's own key unlocked. Like the
6//! web app, a file is encrypted in one piece in memory (**OPEN**: in pieces, for very large files).
7
8use std::path::Path;
9
10use base64::{Engine, engine::general_purpose::STANDARD};
11use kernoid_e2e::{Kind, PublicKey, SecretKey, message};
12use serde::Deserialize;
13use serde_json::{Value, json};
14use tokio::io::AsyncWriteExt;
15
16use super::{E2e, OPENPGP, decode, list};
17use crate::{
18    Client, Error, FILENODE, Result,
19    files::{FileNode, Principal, media_type},
20};
21
22const USING: [&str; 2] = [FILENODE, OPENPGP];
23
24/// A random UUID (version 4), as the web app's `crypto.randomUUID()`.
25fn random_uuid() -> String {
26    let mut bytes = [0u8; 16];
27    getrandom::fill(&mut bytes).expect("the system's random numbers");
28    bytes[6] = (bytes[6] & 0x0f) | 0x40;
29    bytes[8] = (bytes[8] & 0x3f) | 0x80;
30    let hex: String = bytes.iter().map(|b| format!("{b:02x}")).collect();
31    format!("{}-{}-{}-{}-{}", &hex[..8], &hex[8..12], &hex[12..16], &hex[16..20], &hex[20..])
32}
33
34/// A name for a node inside a vault: an id, unique among its siblings.
35fn opaque_name() -> String {
36    format!("k-{}", random_uuid())
37}
38
39#[derive(Deserialize)]
40struct WrappedKey {
41    #[serde(default)]
42    sealed: Option<String>,
43    #[serde(default)]
44    members: Vec<String>,
45}
46
47impl E2e {
48    /// Whether vaults can be made here: the server has them and the tenant does not forbid them.
49    pub fn vaults_allowed(&self) -> bool {
50        self.available && self.settings.policy.vaults != "forbidden"
51    }
52
53    fn need_unlocked(&self) -> Result<&SecretKey> {
54        self.current.as_ref().ok_or_else(|| Error::E2e("unlock your key first".into()))
55    }
56
57    /// What the server keeps of a vault's key for this account, and who has a copy.
58    async fn wrapped(client: &Client, account: &str, vault: &str) -> Result<WrappedKey> {
59        let result = client.call(&USING, "VaultKey/get", json!({ "accountId": account, "ids": [vault] })).await?;
60        list::<WrappedKey>(&result)?.into_iter().next().ok_or_else(|| Error::E2e("no such vault".into()))
61    }
62
63    /// The vault's key: its wrapped secret fetched once and unwrapped with the account's key. `None` while no member
64    /// gave this account a copy.
65    async fn vault_key(&mut self, client: &Client, account: &str, vault: &str) -> Result<Option<SecretKey>> {
66        if let Some(key) = self.vaults.get(vault) {
67            return Ok(Some(key.clone()));
68        }
69        self.need_unlocked()?;
70        let Some(sealed) = Self::wrapped(client, account, vault).await?.sealed else {
71            return Ok(None);
72        };
73        let Ok(opened) = message::decrypt(&decode(&sealed)?, &self.secrets(), &[]) else {
74            return Ok(None);
75        };
76        let key = SecretKey::import(&opened.data)?;
77        self.vaults.insert(vault.to_owned(), key.clone());
78        Ok(Some(key))
79    }
80
81    async fn need_vault_key(&mut self, client: &Client, account: &str, vault: &str) -> Result<SecretKey> {
82        self.need_unlocked()?;
83        self.vault_key(client, account, vault)
84            .await?
85            .ok_or_else(|| Error::E2e("no member gave you this vault's key yet".into()))
86    }
87
88    /// Fills in the real names of nodes in vaults whose key is at hand; the others stay `locked`, named by their id.
89    pub async fn open_names(&mut self, client: &Client, account: &str, nodes: &mut [FileNode]) {
90        for node in nodes.iter_mut() {
91            let (Some(vault), Some(sealed)) = (node.vault.clone(), node.encrypted_name.clone()) else { continue };
92            node.locked = true;
93            if let Some(name) = self.open_name(client, account, &vault, &sealed).await {
94                node.name = name;
95                node.locked = false;
96            }
97        }
98    }
99
100    /// The real name of a node in a vault (`kernoid:encryptedName`) – `None` while the vault's key is not at hand.
101    pub async fn open_name(&mut self, client: &Client, account: &str, vault: &str, sealed: &str) -> Option<String> {
102        self.current.as_ref()?;
103        let key = self.vault_key(client, account, vault).await.ok().flatten()?;
104        decode(sealed)
105            .ok()
106            .and_then(|bytes| message::decrypt(&bytes, &[&key], &[]).ok())
107            .and_then(|opened| String::from_utf8(opened.data).ok())
108    }
109
110    /// A name for a node in a vault: the id it gets as `name` and the real name encrypted, for
111    /// `kernoid:encryptedName`.
112    pub async fn name_in(
113        &mut self,
114        client: &Client,
115        account: &str,
116        vault: &str,
117        name: &str,
118    ) -> Result<(String, String)> {
119        let key = self.need_vault_key(client, account, vault).await?;
120        Ok((opaque_name(), seal(&key.public(), name)?))
121    }
122
123    /// Content for a vault: encrypted to its key and signed with the account's, as a file's blob there is.
124    pub async fn seal_content(
125        &mut self,
126        client: &Client,
127        account: &str,
128        vault: &str,
129        data: Vec<u8>,
130    ) -> Result<Vec<u8>> {
131        let key = self.need_vault_key(client, account, vault).await?;
132        let signer = self.need_unlocked()?.clone();
133        let public = key.public();
134        tokio::task::spawn_blocking(move || message::encrypt(&data, &[&public], Some(&signer), false))
135            .await
136            .map_err(|error| Error::E2e(error.to_string()))?
137            .map_err(Into::into)
138    }
139
140    /// The content of a file of a vault, decrypted.
141    pub async fn open_content(
142        &mut self,
143        client: &Client,
144        account: &str,
145        vault: &str,
146        sealed: Vec<u8>,
147    ) -> Result<Vec<u8>> {
148        let key = self.need_vault_key(client, account, vault).await?;
149        let opened = tokio::task::spawn_blocking(move || message::decrypt(&sealed, &[&key], &[]))
150            .await
151            .map_err(|error| Error::E2e(error.to_string()))??;
152        Ok(opened.data)
153    }
154
155    /// Moves or renames a node within its vault: a new id for its name, the real name encrypted anew.
156    pub async fn move_in(
157        &mut self,
158        client: &Client,
159        account: &str,
160        vault: &str,
161        id: &str,
162        parent: &str,
163        name: &str,
164    ) -> Result<()> {
165        let (opaque, sealed) = self.name_in(client, account, vault, name).await?;
166        let update =
167            json!({ "update": { id: { "parentId": parent, "name": opaque, "kernoid:encryptedName": sealed } } });
168        client.set(&USING, "FileNode", account, update, json!({})).await?;
169        Ok(())
170    }
171
172    /// Makes a vault in `parent` (`None`: the top of the store): its key here, the secret wrapped for the account's own
173    /// key, the name encrypted. Returns its id.
174    pub async fn create_vault(
175        &mut self,
176        client: &Client,
177        account: &str,
178        parent: Option<&str>,
179        name: &str,
180    ) -> Result<String> {
181        let own = self.need_unlocked()?.public();
182        let address = format!("vault-{}@vault.invalid", random_uuid());
183        // A post-quantum key takes a moment to make.
184        let key = tokio::task::spawn_blocking(move || SecretKey::generate(Kind::Pqc, &[address.as_str()]))
185            .await
186            .map_err(|error| Error::E2e(error.to_string()))??;
187        let public = key.public();
188        let sealed = message::encrypt(key.export()?.as_bytes(), &[&own], None, false)?;
189        let create = json!({ "create": { "v": {
190            "@type": "Directory",
191            "parentId": parent,
192            "name": opaque_name(),
193            "kernoid:encryptedName": seal(&public, name)?,
194            "kernoid:newVault": { "publicKey": STANDARD.encode(public.to_bytes()?), "sealed": STANDARD.encode(sealed) },
195        } } });
196        let result = client.set(&USING, "FileNode", account, create, json!({})).await?;
197        let id = created_id(&result, "v")?;
198        self.vaults.insert(id.clone(), key);
199        Ok(id)
200    }
201
202    /// A directory inside a vault.
203    pub async fn create_directory_in(
204        &mut self,
205        client: &Client,
206        account: &str,
207        vault: &str,
208        parent: &str,
209        name: &str,
210    ) -> Result<()> {
211        self.make_directory_in(client, account, vault, parent, name).await.map(drop)
212    }
213
214    /// A directory inside a vault; returns its id.
215    pub async fn make_directory_in(
216        &mut self,
217        client: &Client,
218        account: &str,
219        vault: &str,
220        parent: &str,
221        name: &str,
222    ) -> Result<String> {
223        let (opaque, sealed) = self.name_in(client, account, vault, name).await?;
224        let create = json!({ "create": { "d": {
225            "@type": "Directory", "parentId": parent, "name": opaque, "kernoid:encryptedName": sealed,
226        } } });
227        let result = client.set(&USING, "FileNode", account, create, json!({})).await?;
228        created_id(&result, "d")
229    }
230
231    /// Renames a node in a vault: a new id for a name, the real one encrypted anew.
232    pub async fn rename_in(&mut self, client: &Client, account: &str, node: &FileNode, name: &str) -> Result<()> {
233        let vault = node.vault.as_deref().ok_or_else(|| Error::E2e("not in a vault".into()))?;
234        let key = self.need_vault_key(client, account, vault).await?;
235        let update = json!({ "update": { node.id.as_str(): {
236            "name": opaque_name(), "kernoid:encryptedName": seal(&key.public(), name)?,
237        } } });
238        client.set(&USING, "FileNode", account, update, json!({})).await?;
239        Ok(())
240    }
241
242    /// Uploads a file into a directory of a vault, encrypted here to the vault's key and signed with the account's;
243    /// `existing` (a file of the same real name) gets the new content.
244    #[allow(clippy::too_many_arguments)]
245    pub async fn upload_into(
246        &mut self,
247        client: &Client,
248        account: &str,
249        vault: &str,
250        parent: &str,
251        path: &Path,
252        existing: Option<&FileNode>,
253        progress: impl FnMut(u64, u64) + Send + 'static,
254    ) -> Result<()> {
255        let key = self.need_vault_key(client, account, vault).await?;
256        let signer = self.need_unlocked()?.clone();
257        let name = path
258            .file_name()
259            .and_then(|name| name.to_str())
260            .ok_or_else(|| Error::Protocol("the file name is not UTF-8".into()))?
261            .to_owned();
262        let data = tokio::fs::read(path).await?;
263        let modified =
264            tokio::fs::metadata(path).await?.modified().ok().map(|time| {
265                chrono::DateTime::<chrono::Utc>::from(time).to_rfc3339_opts(chrono::SecondsFormat::Secs, true)
266            });
267        let public = key.public();
268        let sealed = tokio::task::spawn_blocking(move || message::encrypt(&data, &[&public], Some(&signer), false))
269            .await
270            .map_err(|error| Error::E2e(error.to_string()))??;
271        let size = sealed.len() as u64;
272        let upload =
273            client.upload(account, "application/octet-stream", std::io::Cursor::new(sealed), size, progress).await?;
274        let kind = media_type(&name);
275        match existing.filter(|node| !node.is_directory()) {
276            Some(node) => {
277                let update = json!({ "update": { node.id.as_str(): {
278                    "blobId": upload.blob_id, "mediaType": kind, "modified": modified,
279                } } });
280                client.set(&USING, "FileNode", account, update, json!({})).await?;
281            }
282            None => {
283                let create = json!({ "create": { "f": {
284                    "@type": "File", "parentId": parent, "name": opaque_name(),
285                    "kernoid:encryptedName": seal(&key.public(), &name)?,
286                    "blobId": upload.blob_id, "mediaType": kind, "modified": modified,
287                } } });
288                client.set(&USING, "FileNode", account, create, json!({})).await?;
289            }
290        }
291        Ok(())
292    }
293
294    /// Downloads a file of a vault and decrypts it here; returns its content.
295    pub async fn download_from(&mut self, client: &Client, account: &str, node: &FileNode) -> Result<Vec<u8>> {
296        let vault = node.vault.as_deref().ok_or_else(|| Error::E2e("not in a vault".into()))?;
297        let blob = node.blob_id.as_deref().ok_or_else(|| Error::E2e("not a file".into()))?;
298        self.need_vault_key(client, account, vault).await?;
299        let mut sealed = Vec::new();
300        client.download(account, blob, "encrypted", "application/octet-stream", &mut sealed, &mut |_, _| {}).await?;
301        self.open_content(client, account, vault, sealed).await
302    }
303
304    /// Downloads a file of a vault, decrypted, into `path`.
305    pub async fn save_from(&mut self, client: &Client, account: &str, node: &FileNode, path: &Path) -> Result<()> {
306        let data = self.download_from(client, account, node).await?;
307        let mut file = tokio::fs::File::create(path).await?;
308        file.write_all(&data).await?;
309        file.flush().await?;
310        Ok(())
311    }
312
313    /// After sharing a vault: gives its key to every person shared with who has a key of their own and no copy yet.
314    /// Groups and people without a key wait (**OPEN**: a group's members, as in the web app). Returns those who wait.
315    pub async fn give_vault_keys(
316        &mut self,
317        client: &Client,
318        account: &str,
319        vault: &str,
320        people: &[Principal],
321    ) -> Result<Vec<Principal>> {
322        let key = self.need_vault_key(client, account, vault).await?;
323        let members = Self::wrapped(client, account, vault).await?.members;
324        let secret = key.export()?;
325        let mut waiting = Vec::new();
326        let mut update = serde_json::Map::new();
327        for person in people.iter().filter(|person| !members.contains(&person.id)) {
328            let keys: Vec<PublicKey> = match (person.kind.as_str(), &person.email) {
329                ("individual", Some(email)) => {
330                    self.lookup(client, std::slice::from_ref(email)).await?.into_values().flatten().collect()
331                }
332                _ => Vec::new(),
333            };
334            if keys.is_empty() {
335                waiting.push(person.clone());
336                continue;
337            }
338            let recipients: Vec<&PublicKey> = keys.iter().collect();
339            let wrapped = message::encrypt(secret.as_bytes(), &recipients, None, false)?;
340            update.insert(format!("members/{}", person.id), Value::String(STANDARD.encode(wrapped)));
341        }
342        if !update.is_empty() {
343            let change = json!({ "update": { vault: update } });
344            client.set(&USING, "VaultKey", account, change, json!({})).await?;
345        }
346        Ok(waiting)
347    }
348}
349
350/// A name encrypted to a vault's key, base64.
351fn seal(vault: &PublicKey, name: &str) -> Result<String> {
352    Ok(STANDARD.encode(message::encrypt(name.as_bytes(), &[vault], None, false)?))
353}
354
355/// The id of what a `…/set` made under `key`.
356fn created_id(result: &Value, key: &str) -> Result<String> {
357    result
358        .pointer(&format!("/created/{key}/id"))
359        .and_then(Value::as_str)
360        .map(str::to_owned)
361        .ok_or_else(|| Error::Protocol("the server made nothing".into()))
362}
363
364#[cfg(test)]
365mod tests {
366    use super::*;
367
368    #[test]
369    fn names_in_a_vault_are_random_ids() {
370        let (a, b) = (opaque_name(), opaque_name());
371        assert_ne!(a, b);
372        assert_eq!(a.len(), 2 + 36);
373        assert!(a.starts_with("k-") && a.as_bytes()[2 + 14] == b'4');
374    }
375}