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(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 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>, origin: Option<Uuid>,
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        self.events.doc_written(target_id, Actor::User(origin));
170        self.add_doc_event(activity::DocEvent::Write(target_id, get_time().0))
171            .await?;
172
173        Ok(hmac)
174    }
175
176    pub(crate) async fn cleanup(&self) -> LbResult<()> {
177        let tx = self.ro_tx().await;
178        let db = tx.db();
179
180        let tree = db.base_metadata.stage(&db.local_metadata);
181
182        let base_files = tree.base.all_files()?.into_iter();
183        let local_files = tree.staged.all_files()?.into_iter();
184
185        let file_hmacs = base_files
186            .chain(local_files)
187            .filter_map(|f| f.document_hmac().map(|hmac| (*f.id(), *hmac)))
188            .collect::<HashSet<_>>();
189
190        self.docs.retain(file_hmacs).await?;
191
192        drop(tx);
193
194        Ok(())
195    }
196}
197
198fn compress_encrypt_document(
199    key: &AESKey, content: &[u8],
200) -> LbResult<(DocumentHmac, EncryptedDocument)> {
201    let hmac: DocumentHmac = {
202        let mut mac = HmacSha256::new_from_slice(key)
203            .map_err(|err| LbErrKind::Unexpected(format!("hmac creation error: {err:?}")))?;
204        mac.update(content);
205        mac.finalize().into_bytes()
206    }
207    .into();
208    let compressed = compression_service::compress(content)?;
209    let encrypted = symkey::encrypt(key, &compressed)?;
210    Ok((hmac, encrypted))
211}
212
213fn decrypt_decompress_document(
214    key: &AESKey, encrypted: &EncryptedDocument,
215) -> LbResult<DecryptedDocument> {
216    let compressed = symkey::decrypt(key, encrypted)?;
217    let doc = compression_service::decompress(&compressed)?;
218    Ok(doc)
219}