Skip to main content

lb_rs/service/
documents.rs

1use std::collections::HashSet;
2
3use crate::Lb;
4use crate::model::clock::get_time;
5use crate::model::crypto::{AESKey, DecryptedDocument, EncryptedDocument};
6use crate::model::errors::{LbErrKind, LbResult};
7use crate::model::file_like::FileLike;
8use crate::model::file_metadata::{DocumentHmac, FileType};
9use crate::model::secret_filename::HmacSha256;
10use crate::model::tree_like::TreeLike;
11use crate::model::{compression_service, symkey, validate};
12use hmac::{Mac, NewMac};
13use uuid::Uuid;
14
15use super::activity;
16use super::events::Actor;
17
18impl Lb {
19    #[instrument(level = "debug", skip(self), err(Debug))]
20    pub async fn read_document(
21        &self, id: Uuid, user_activity: bool,
22    ) -> LbResult<DecryptedDocument> {
23        let (_, doc) = self.read_document_with_hmac(id, user_activity).await?;
24        Ok(doc)
25    }
26
27    #[instrument(level = "debug", skip(self, content), err(Debug))]
28    pub async fn write_document(&self, id: Uuid, content: &[u8]) -> LbResult<()> {
29        // get info so we can do operations while not holding lock
30        let (id, key) = {
31            let tx = self.ro_tx().await;
32            let db = tx.db();
33            let mut tree = (&db.base_metadata).to_staged(&db.local_metadata).to_lazy();
34            let id = match tree.find(&id)?.file_type() {
35                FileType::Document | FileType::Folder => id,
36                FileType::Link { target } => target,
37            };
38            validate::is_document(tree.find(&id)?)?;
39            (id, tree.decrypt_key(&id, &self.keychain)?)
40        };
41
42        // do the operations
43        let (hmac, encrypted) = compress_encrypt_document(&key, content)?;
44        let encrypted_size = encrypted.value.len();
45        self.docs.insert_pending(id, hmac, &encrypted).await?;
46
47        // commit the result
48        {
49            let mut tx = self.begin_tx().await;
50            let db = tx.db();
51            let mut tree = (&db.base_metadata)
52                .to_staged(&mut db.local_metadata)
53                .to_lazy();
54            self.docs.promote_pending(id, hmac).await?;
55            tree.overwrite_document_hmac(&id, Some(hmac), Some(encrypted_size), &self.keychain)?;
56            tx.end();
57        }
58
59        self.events.doc_written(id, Actor::User(None));
60        self.add_doc_event(activity::DocEvent::Write(id, get_time().0))
61            .await?;
62
63        Ok(())
64    }
65
66    #[instrument(level = "debug", skip(self), err(Debug))]
67    pub async fn read_document_with_hmac(
68        &self, id: Uuid, user_activity: bool,
69    ) -> LbResult<(Option<DocumentHmac>, DecryptedDocument)> {
70        // get info + on-disk bytes so we can decrypt without holding the lock
71        let mut retried = false;
72        let info: Option<(DocumentHmac, AESKey, Option<EncryptedDocument>)> = loop {
73            let tx = self.ro_tx().await;
74            let db = tx.db();
75            let mut tree = (&db.base_metadata).to_staged(&db.local_metadata).to_lazy();
76
77            let file = tree.find(&id)?;
78            validate::is_document(file)?;
79            let hmac = file.document_hmac().copied();
80
81            if tree.calculate_deleted(&id)? {
82                return Err(LbErrKind::FileNonexistent.into());
83            }
84
85            match hmac {
86                Some(hmac) => {
87                    let key = tree.decrypt_key(&id, &self.keychain)?;
88                    let local_blob = self.docs.maybe_get(id, Some(hmac)).await?;
89                    if local_blob.is_none() && !retried {
90                        drop(tx);
91                        // catch up our local db and try to read again
92                        self.begin_tx().await.end();
93                        retried = true;
94                        continue;
95                    }
96                    break Some((hmac, key, local_blob));
97                }
98                None => break None,
99            }
100        };
101
102        // do decrypt + decompress without holding the lock; fetch from the
103        // server first if the blob wasn't already local.
104        let (hmac, doc) = match info {
105            None => (None, vec![]),
106            Some((hmac, key, local_blob)) => {
107                let encrypted = match local_blob {
108                    Some(blob) => blob,
109                    // todo: if document not found -- need to trigger a pull
110                    None => self.fetch_doc(id, hmac).await?,
111                };
112                let doc = decrypt_decompress_document(&key, &encrypted)?;
113                (Some(hmac), doc)
114            }
115        };
116
117        if user_activity {
118            self.add_doc_event(activity::DocEvent::Read(id, get_time().0))
119                .await?;
120        }
121
122        Ok((hmac, doc))
123    }
124
125    #[instrument(level = "debug", skip(self, content), err(Debug))]
126    pub async fn safe_write(
127        &self, id: Uuid, old_hmac: Option<DocumentHmac>, content: Vec<u8>, origin: Option<Uuid>,
128    ) -> LbResult<DocumentHmac> {
129        // get info so we can do operations while not holding lock
130        let (target_id, key) = {
131            let tx = self.ro_tx().await;
132            let db = tx.db();
133            let mut tree = (&db.base_metadata).to_staged(&db.local_metadata).to_lazy();
134            let file = tree.find(&id)?;
135            if file.document_hmac() != old_hmac.as_ref() {
136                return Err(LbErrKind::ReReadRequired.into());
137            }
138            let target_id = match file.file_type() {
139                FileType::Document | FileType::Folder => id,
140                FileType::Link { target } => target,
141            };
142            validate::is_document(tree.find(&target_id)?)?;
143            (target_id, tree.decrypt_key(&target_id, &self.keychain)?)
144        };
145
146        // do the operations
147        let (hmac, encrypted) = compress_encrypt_document(&key, &content)?;
148        let encrypted_size = encrypted.value.len();
149        self.docs
150            .insert_pending(target_id, hmac, &encrypted)
151            .await?;
152
153        // commit the result
154        {
155            let mut tx = self.begin_tx().await;
156            let db = tx.db();
157            let mut tree = (&db.base_metadata)
158                .to_staged(&mut db.local_metadata)
159                .to_lazy();
160            self.docs.promote_pending(target_id, hmac).await?;
161            if tree.find(&id)?.document_hmac() != old_hmac.as_ref() {
162                return Err(LbErrKind::ReReadRequired.into());
163            }
164            tree.overwrite_document_hmac(
165                &target_id,
166                Some(hmac),
167                Some(encrypted_size),
168                &self.keychain,
169            )?;
170            tx.end();
171        }
172
173        self.events.doc_written(target_id, Actor::User(origin));
174        self.add_doc_event(activity::DocEvent::Write(target_id, get_time().0))
175            .await?;
176
177        Ok(hmac)
178    }
179
180    pub(crate) async fn cleanup(&self) -> LbResult<()> {
181        let tx = self.ro_tx().await;
182        let db = tx.db();
183
184        let tree = db.base_metadata.stage(&db.local_metadata);
185
186        let base_files = tree.base.all_files()?.into_iter();
187        let local_files = tree.staged.all_files()?.into_iter();
188
189        let file_hmacs = base_files
190            .chain(local_files)
191            .filter_map(|f| f.document_hmac().map(|hmac| (*f.id(), *hmac)))
192            .collect::<HashSet<_>>();
193
194        self.docs.retain(file_hmacs).await?;
195
196        drop(tx);
197
198        Ok(())
199    }
200}
201
202fn compress_encrypt_document(
203    key: &AESKey, content: &[u8],
204) -> LbResult<(DocumentHmac, EncryptedDocument)> {
205    let hmac: DocumentHmac = {
206        let mut mac = HmacSha256::new_from_slice(key)
207            .map_err(|err| LbErrKind::Unexpected(format!("hmac creation error: {err:?}")))?;
208        mac.update(content);
209        mac.finalize().into_bytes()
210    }
211    .into();
212    let compressed = compression_service::compress(content)?;
213    let encrypted = symkey::encrypt(key, &compressed)?;
214    Ok((hmac, encrypted))
215}
216
217fn decrypt_decompress_document(
218    key: &AESKey, encrypted: &EncryptedDocument,
219) -> LbResult<DecryptedDocument> {
220    let compressed = symkey::decrypt(key, encrypted)?;
221    let doc = compression_service::decompress(&compressed)?;
222    Ok(doc)
223}