Skip to main content

lb_rs/service/
documents.rs

1use std::collections::HashSet;
2
3use crate::LocalLb;
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 LocalLb {
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);
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 info: Option<(DocumentHmac, AESKey, Option<EncryptedDocument>)> = {
72            let tx = self.ro_tx().await;
73            let db = tx.db();
74            let mut tree = (&db.base_metadata).to_staged(&db.local_metadata).to_lazy();
75
76            let file = tree.find(&id)?;
77            validate::is_document(file)?;
78            let hmac = file.document_hmac().copied();
79
80            if tree.calculate_deleted(&id)? {
81                return Err(LbErrKind::FileNonexistent.into());
82            }
83
84            match hmac {
85                Some(hmac) => {
86                    let key = tree.decrypt_key(&id, &self.keychain)?;
87                    let local_blob = if self.docs.exists(id, Some(hmac)) {
88                        Some(self.docs.get(id, Some(hmac)).await?)
89                    } else {
90                        None
91                    };
92                    Some((hmac, key, local_blob))
93                }
94                None => None,
95            }
96        };
97
98        // do decrypt + decompress without holding the lock; fetch from the
99        // server first if the blob wasn't already local.
100        let (hmac, doc) = match info {
101            None => (None, vec![]),
102            Some((hmac, key, local_blob)) => {
103                let encrypted = match local_blob {
104                    Some(blob) => blob,
105                    // todo: if document not found -- need to trigger a pull
106                    None => self.fetch_doc(id, hmac).await?,
107                };
108                let doc = decrypt_decompress_document(&key, &encrypted)?;
109                (Some(hmac), doc)
110            }
111        };
112
113        if user_activity {
114            self.add_doc_event(activity::DocEvent::Read(id, get_time().0))
115                .await?;
116        }
117
118        Ok((hmac, doc))
119    }
120
121    #[instrument(level = "debug", skip(self, content), err(Debug))]
122    pub async fn safe_write(
123        &self, id: Uuid, old_hmac: Option<DocumentHmac>, content: Vec<u8>,
124    ) -> LbResult<DocumentHmac> {
125        // get info so we can do operations while not holding lock
126        let (target_id, key) = {
127            let tx = self.ro_tx().await;
128            let db = tx.db();
129            let mut tree = (&db.base_metadata).to_staged(&db.local_metadata).to_lazy();
130            let file = tree.find(&id)?;
131            if file.document_hmac() != old_hmac.as_ref() {
132                return Err(LbErrKind::ReReadRequired.into());
133            }
134            let target_id = match file.file_type() {
135                FileType::Document | FileType::Folder => id,
136                FileType::Link { target } => target,
137            };
138            validate::is_document(tree.find(&target_id)?)?;
139            (target_id, tree.decrypt_key(&target_id, &self.keychain)?)
140        };
141
142        // do the operations
143        let (hmac, encrypted) = compress_encrypt_document(&key, &content)?;
144        let encrypted_size = encrypted.value.len();
145        self.docs
146            .insert_pending(target_id, hmac, &encrypted)
147            .await?;
148
149        // commit the result
150        {
151            let mut tx = self.begin_tx().await;
152            let db = tx.db();
153            let mut tree = (&db.base_metadata)
154                .to_staged(&mut db.local_metadata)
155                .to_lazy();
156            self.docs.promote_pending(target_id, hmac).await?;
157            if tree.find(&id)?.document_hmac() != old_hmac.as_ref() {
158                return Err(LbErrKind::ReReadRequired.into());
159            }
160            tree.overwrite_document_hmac(
161                &target_id,
162                Some(hmac),
163                Some(encrypted_size),
164                &self.keychain,
165            )?;
166            tx.end();
167        }
168
169        // todo: when workspace isn't the only writer, this arg needs to be exposed
170        // this will happen when lb-fs is integrated into an app and shares an lb-rs with ws
171        // or it will happen when there are multiple co-operative core processes.
172        self.events.doc_written(target_id, Actor::User);
173        self.add_doc_event(activity::DocEvent::Write(target_id, get_time().0))
174            .await?;
175
176        Ok(hmac)
177    }
178
179    pub(crate) async fn cleanup(&self) -> LbResult<()> {
180        let tx = self.ro_tx().await;
181        let db = tx.db();
182
183        let tree = db.base_metadata.stage(&db.local_metadata);
184
185        let base_files = tree.base.all_files()?.into_iter();
186        let local_files = tree.staged.all_files()?.into_iter();
187
188        let file_hmacs = base_files
189            .chain(local_files)
190            .filter_map(|f| f.document_hmac().map(|hmac| (*f.id(), *hmac)))
191            .collect::<HashSet<_>>();
192
193        self.docs.retain(file_hmacs).await?;
194
195        drop(tx);
196
197        Ok(())
198    }
199}
200
201fn compress_encrypt_document(
202    key: &AESKey, content: &[u8],
203) -> LbResult<(DocumentHmac, EncryptedDocument)> {
204    let hmac: DocumentHmac = {
205        let mut mac = HmacSha256::new_from_slice(key)
206            .map_err(|err| LbErrKind::Unexpected(format!("hmac creation error: {err:?}")))?;
207        mac.update(content);
208        mac.finalize().into_bytes()
209    }
210    .into();
211    let compressed = compression_service::compress(content)?;
212    let encrypted = symkey::encrypt(key, &compressed)?;
213    Ok((hmac, encrypted))
214}
215
216fn decrypt_decompress_document(
217    key: &AESKey, encrypted: &EncryptedDocument,
218) -> LbResult<DecryptedDocument> {
219    let compressed = symkey::decrypt(key, encrypted)?;
220    let doc = compression_service::decompress(&compressed)?;
221    Ok(doc)
222}