sos_database/entity/
folder.rs

1use crate::{Error, Result};
2use async_sqlite::rusqlite::{
3    CachedStatement, Connection, Error as SqlError, OptionalExtension, Row,
4    Transaction,
5};
6use async_sqlite::Client;
7use sos_core::crypto::Seed;
8use sos_core::{
9    commit::CommitHash, crypto::AeadPack, decode, encode, SecretId,
10    UtcDateTime, VaultCommit, VaultEntry, VaultFlags, VaultId,
11};
12use sos_vault::{Summary, Vault};
13use sql_query_builder as sql;
14use std::collections::HashMap;
15use std::ops::Deref;
16use std::result::Result as StdResult;
17
18fn folder_select_columns(sql: sql::Select) -> sql::Select {
19    sql.select(
20        r#"
21            folders.folder_id,
22            folders.created_at,
23            folders.modified_at,
24            folders.identifier,
25            folders.name,
26            folders.salt,
27            folders.meta,
28            folders.seed,
29            folders.version,
30            folders.cipher,
31            folders.kdf,
32            folders.flags
33        "#,
34    )
35}
36
37fn secret_select_columns(sql: sql::Select) -> sql::Select {
38    sql.select(
39        r#"
40            secret_id,
41            created_at,
42            modified_at,
43            identifier,
44            commit_hash,
45            meta,
46            secret 
47        "#,
48    )
49}
50
51/// Folder row from the database.
52#[doc(hidden)]
53#[derive(Debug, Default)]
54pub struct FolderRow {
55    pub row_id: i64,
56    created_at: String,
57    modified_at: String,
58    identifier: String,
59    name: String,
60    salt: Option<String>,
61    meta: Option<Vec<u8>>,
62    seed: Option<Vec<u8>>,
63    version: i64,
64    cipher: String,
65    kdf: String,
66    flags: Vec<u8>,
67}
68
69impl FolderRow {
70    /// Create a new folder row to insert.
71    pub async fn new_insert(vault: &Vault) -> Result<Self> {
72        let meta = if let Some(meta) = vault.header().meta() {
73            Some(encode(meta).await?)
74        } else {
75            None
76        };
77        let salt = vault.salt().cloned();
78        let seed = vault.seed().map(|s| s.as_ref().to_vec());
79        Self::new_insert_parts(vault.summary(), salt, meta, seed)
80    }
81
82    /// Create a new folder row to be inserted from parts.
83    pub fn new_insert_parts(
84        summary: &Summary,
85        salt: Option<String>,
86        meta: Option<Vec<u8>>,
87        seed: Option<Vec<u8>>,
88    ) -> Result<Self> {
89        Ok(Self {
90            created_at: UtcDateTime::default().to_rfc3339()?,
91            modified_at: UtcDateTime::default().to_rfc3339()?,
92            identifier: summary.id().to_string(),
93            name: summary.name().to_string(),
94            salt,
95            meta,
96            seed,
97            version: *summary.version() as i64,
98            cipher: summary.cipher().to_string(),
99            kdf: summary.kdf().to_string(),
100            flags: summary.flags().bits().to_le_bytes().to_vec(),
101            ..Default::default()
102        })
103    }
104
105    /// Create a new folder row to update.
106    pub async fn new_update(vault: &Vault) -> Result<Self> {
107        let summary = vault.summary();
108        let meta = if let Some(meta) = vault.header().meta() {
109            Some(encode(meta).await?)
110        } else {
111            None
112        };
113        let salt = vault.salt().cloned();
114        let seed = vault.seed().map(|s| s.as_ref().to_vec());
115        Ok(Self {
116            modified_at: UtcDateTime::default().to_rfc3339()?,
117            identifier: summary.id().to_string(),
118            name: summary.name().to_string(),
119            salt,
120            meta,
121            seed,
122            version: *summary.version() as i64,
123            cipher: summary.cipher().to_string(),
124            kdf: summary.kdf().to_string(),
125            flags: summary.flags().bits().to_le_bytes().to_vec(),
126            ..Default::default()
127        })
128    }
129}
130
131impl<'a> TryFrom<&Row<'a>> for FolderRow {
132    type Error = SqlError;
133    fn try_from(row: &Row<'a>) -> StdResult<Self, Self::Error> {
134        Ok(FolderRow {
135            row_id: row.get(0)?,
136            created_at: row.get(1)?,
137            modified_at: row.get(2)?,
138            identifier: row.get(3)?,
139            name: row.get(4)?,
140            salt: row.get(5)?,
141            meta: row.get(6)?,
142            seed: row.get(7)?,
143            version: row.get(8)?,
144            cipher: row.get(9)?,
145            kdf: row.get(10)?,
146            flags: row.get(11)?,
147        })
148    }
149}
150
151/// Folder record from the database.
152#[derive(Debug, Clone)]
153pub struct FolderRecord {
154    /// Row identifier.
155    pub row_id: i64,
156    /// Created date and time.
157    pub created_at: UtcDateTime,
158    /// Modified date and time.
159    pub modified_at: UtcDateTime,
160    /// Key derivation salt.
161    pub salt: Option<String>,
162    /// Folder meta data.
163    pub meta: Option<AeadPack>,
164    /// Optional seed entropy.
165    pub seed: Option<Seed>,
166    /// Folder summary.
167    pub summary: Summary,
168}
169
170impl FolderRecord {
171    /// Convert from a folder row.
172    pub async fn from_row(value: FolderRow) -> Result<Self> {
173        let created_at = UtcDateTime::parse_rfc3339(&value.created_at)?;
174        let modified_at = UtcDateTime::parse_rfc3339(&value.modified_at)?;
175        let folder_id: VaultId = value.identifier.parse()?;
176        let version: u16 = value.version.try_into()?;
177        let cipher = value.cipher.parse()?;
178        let kdf = value.kdf.parse()?;
179        let bytes: [u8; 8] = value.flags.as_slice().try_into()?;
180        let bits = u64::from_le_bytes(bytes);
181        let flags = VaultFlags::from_bits(bits)
182            .ok_or(sos_vault::Error::InvalidVaultFlags)?;
183
184        let salt = value.salt;
185
186        let meta = if let Some(meta) = &value.meta {
187            Some(decode(meta).await?)
188        } else {
189            None
190        };
191
192        let seed = if let Some(seed) = value.seed {
193            let seed: [u8; Seed::SIZE] = seed.as_slice().try_into()?;
194            Some(Seed(seed))
195        } else {
196            None
197        };
198
199        let summary =
200            Summary::new(version, folder_id, value.name, cipher, kdf, flags);
201
202        Ok(FolderRecord {
203            row_id: value.row_id,
204            created_at,
205            modified_at,
206            salt,
207            meta,
208            seed,
209            summary,
210        })
211    }
212
213    /// Convert a folder record into a vault.
214    pub fn into_vault(&self) -> Result<Vault> {
215        let mut vault: Vault = self.summary.clone().into();
216        vault.header_mut().set_meta(self.meta.clone());
217        vault.header_mut().set_salt(self.salt.clone());
218        vault.header_mut().set_seed(self.seed);
219        Ok(vault)
220    }
221}
222
223/// Secret row from the database.
224#[doc(hidden)]
225#[derive(Debug, Default)]
226pub struct SecretRow {
227    pub row_id: i64,
228    created_at: String,
229    modified_at: String,
230    identifier: String,
231    commit: Vec<u8>,
232    meta: Vec<u8>,
233    secret: Vec<u8>,
234}
235
236impl SecretRow {
237    /// Create a new secret row for insertion.
238    pub async fn new(
239        secret_id: &SecretId,
240        commit: &CommitHash,
241        entry: &VaultEntry,
242    ) -> Result<Self> {
243        let VaultEntry(meta, secret) = entry;
244        let meta = encode(meta).await?;
245        let secret = encode(secret).await?;
246        Ok(Self {
247            created_at: UtcDateTime::default().to_rfc3339()?,
248            modified_at: UtcDateTime::default().to_rfc3339()?,
249            identifier: secret_id.to_string(),
250            commit: commit.as_ref().to_vec(),
251            meta,
252            secret,
253            ..Default::default()
254        })
255    }
256
257    /// Secret identifier.
258    pub fn identifier(&self) -> &str {
259        &self.identifier
260    }
261
262    /// Commit hash.
263    pub fn commit(&self) -> &[u8] {
264        &self.commit
265    }
266
267    /// Meta data bytes.
268    pub fn meta_bytes(&self) -> &[u8] {
269        &self.meta
270    }
271
272    /// Secret data bytes.
273    pub fn secret_bytes(&self) -> &[u8] {
274        &self.secret
275    }
276}
277
278impl<'a> TryFrom<&Row<'a>> for SecretRow {
279    type Error = SqlError;
280    fn try_from(row: &Row<'a>) -> StdResult<Self, Self::Error> {
281        Ok(SecretRow {
282            row_id: row.get(0)?,
283            created_at: row.get(1)?,
284            modified_at: row.get(2)?,
285            identifier: row.get(3)?,
286            commit: row.get(4)?,
287            meta: row.get(5)?,
288            secret: row.get(6)?,
289        })
290    }
291}
292
293/// Secret record from the database.
294#[doc(hidden)]
295#[derive(Debug)]
296pub struct SecretRecord {
297    pub row_id: i64,
298    pub created_at: UtcDateTime,
299    pub modified_at: UtcDateTime,
300    pub secret_id: VaultId,
301    pub commit: VaultCommit,
302}
303
304impl SecretRecord {
305    /// Convert from a secret row.
306    pub async fn from_row(value: SecretRow) -> Result<Self> {
307        let created_at = UtcDateTime::parse_rfc3339(&value.created_at)?;
308        let modified_at = UtcDateTime::parse_rfc3339(&value.modified_at)?;
309        let secret_id: SecretId = value.identifier.parse()?;
310        let commit_hash = CommitHash(value.commit.as_slice().try_into()?);
311        let meta: AeadPack = decode(&value.meta).await?;
312        let secret: AeadPack = decode(&value.secret).await?;
313        let commit = VaultCommit(commit_hash, VaultEntry(meta, secret));
314
315        Ok(SecretRecord {
316            row_id: value.row_id,
317            created_at,
318            modified_at,
319            secret_id,
320            commit,
321        })
322    }
323}
324
325/// Folder entity.
326pub struct FolderEntity<'conn, C>
327where
328    C: Deref<Target = Connection>,
329{
330    conn: &'conn C,
331}
332
333impl<'conn> FolderEntity<'conn, Box<Connection>> {
334    /// Query to find all secrets in a folder.
335    pub fn find_all_secrets_query() -> sql::Select {
336        secret_select_columns(sql::Select::new())
337            .from("folder_secrets")
338            .where_clause("folder_id=?1")
339    }
340
341    /// Compute the vault for a folder in the database.
342    pub async fn compute_folder_vault(
343        client: &Client,
344        folder_id: &VaultId,
345    ) -> Result<Vault> {
346        let folder_id = *folder_id;
347
348        let (folder_row, secret_rows) = client
349            .conn_and_then(move |conn| {
350                let folder_entity = FolderEntity::new(&conn);
351                let folder_row = folder_entity.find_one(&folder_id)?;
352                let secret_rows =
353                    folder_entity.load_secrets(folder_row.row_id)?;
354                Ok::<_, Error>((folder_row, secret_rows))
355            })
356            .await?;
357
358        let folder_record = FolderRecord::from_row(folder_row).await?;
359        let mut vault = folder_record.into_vault()?;
360        for row in secret_rows {
361            let record = SecretRecord::from_row(row).await?;
362            vault.insert_entry(record.secret_id, record.commit);
363        }
364        Ok(vault)
365    }
366}
367
368impl<'conn> FolderEntity<'conn, Transaction<'conn>> {
369    /// Create a folder and the secrets in a vault.
370    ///
371    /// If a folder with the same identifier already exists
372    /// it is updated and any existing secrets are deleted
373    /// before inserting the new collection of secrets in the vault.
374    pub async fn upsert_folder_and_secrets(
375        client: &Client,
376        account_id: i64,
377        vault: &Vault,
378    ) -> Result<(i64, HashMap<SecretId, i64>)> {
379        let folder_id = *vault.id();
380
381        let meta = if let Some(meta) = vault.header().meta() {
382            Some(encode(meta).await?)
383        } else {
384            None
385        };
386        let salt = vault.salt().cloned();
387        let seed = vault.seed().map(|s| s.as_ref().to_vec());
388
389        let folder_row =
390            FolderRow::new_insert_parts(vault.summary(), salt, meta, seed)?;
391
392        let mut secret_rows = Vec::new();
393        for (secret_id, commit) in vault.iter() {
394            let VaultCommit(commit, entry) = commit;
395            secret_rows.push(SecretRow::new(secret_id, commit, entry).await?);
396        }
397
398        client
399            .conn_mut_and_then(move |conn| {
400                let tx = conn.transaction()?;
401                let folder_entity = FolderEntity::new(&tx);
402
403                let folder_id = if let Some(row) =
404                    folder_entity.find_optional(&folder_id)?
405                {
406                    folder_entity.update_folder(&folder_id, &folder_row)?;
407                    folder_entity.delete_all_secrets(row.row_id)?;
408                    row.row_id
409                } else {
410                    folder_entity.insert_folder(account_id, &folder_row)?
411                };
412
413                let secret_ids = folder_entity.insert_folder_secrets(
414                    folder_id,
415                    secret_rows.as_slice(),
416                )?;
417                tx.commit()?;
418                Ok::<_, Error>((folder_id, secret_ids))
419            })
420            .await
421    }
422
423    /// Replace all secrets for a folder using a transaction.
424    pub async fn replace_all_secrets(
425        client: Client,
426        folder_id: &VaultId,
427        vault: &Vault,
428    ) -> Result<()> {
429        let folder_id = *folder_id;
430        let mut insert_secrets = Vec::new();
431        for (secret_id, commit) in vault.iter() {
432            let VaultCommit(commit, entry) = commit;
433            insert_secrets
434                .push(SecretRow::new(secret_id, commit, entry).await?);
435        }
436
437        let folder_update_row = FolderRow::new_update(vault).await?;
438        client
439            .conn_mut(move |conn| {
440                let tx = conn.transaction()?;
441                let folder = FolderEntity::new(&tx);
442                let folder_row = folder.find_one(&folder_id)?;
443                folder.delete_all_secrets(folder_row.row_id)?;
444                for secret_row in insert_secrets {
445                    folder.insert_secret_by_row_id(
446                        folder_row.row_id,
447                        &secret_row,
448                    )?;
449                }
450                folder.update_folder(&folder_id, &folder_update_row)?;
451                tx.commit()?;
452                Ok(())
453            })
454            .await
455            .map_err(Error::from)?;
456        Ok(())
457    }
458}
459
460impl<'conn, C> FolderEntity<'conn, C>
461where
462    C: Deref<Target = Connection>,
463{
464    /// Create a new folder entity.
465    pub fn new(conn: &'conn C) -> Self {
466        Self { conn }
467    }
468
469    fn select_folder<'a>(
470        &'a self,
471        use_identifier: bool,
472    ) -> StdResult<CachedStatement<'a>, SqlError> {
473        let query = folder_select_columns(sql::Select::new()).from("folders");
474
475        let query = if use_identifier {
476            query.where_clause("identifier = ?1")
477        } else {
478            query.where_clause("folder_id = ?1")
479        };
480        self.conn.prepare_cached(&query.as_string())
481    }
482
483    /// Find a folder in the database.
484    pub fn find_one(
485        &self,
486        // FIXME: require account_id?
487        folder_id: &VaultId,
488    ) -> StdResult<FolderRow, SqlError> {
489        let mut stmt = self.select_folder(true)?;
490        stmt
491            .query_row([folder_id.to_string()], |row| row.try_into())
492    }
493
494    /// Find an optional folder in the database.
495    pub fn find_optional(
496        &self,
497        // FIXME: require account_id?
498        folder_id: &VaultId,
499    ) -> StdResult<Option<FolderRow>, SqlError> {
500        let mut stmt = self.select_folder(true)?;
501        stmt
502            .query_row([folder_id.to_string()], |row| {
503                let row: FolderRow = row.try_into()?;
504                Ok(row)
505            })
506            .optional()
507    }
508
509    /// Find a folder in the database by primary key.
510    pub fn find_by_row_id(
511        &self,
512        folder_id: i64,
513    ) -> StdResult<FolderRow, SqlError> {
514        let mut stmt = self.select_folder(false)?;
515        stmt.query_row([folder_id], |row| row.try_into())
516    }
517
518    /// Try to find a login folder for an account.
519    pub fn find_login_folder(&self, account_id: i64) -> Result<FolderRow> {
520        self
521            .find_login_folder_optional(account_id)?
522            .ok_or_else(|| Error::NoLoginFolder(account_id))
523    }
524
525    /// Try to find an optional login folder for an account.
526    pub fn find_login_folder_optional(
527        &self,
528        account_id: i64,
529    ) -> StdResult<Option<FolderRow>, SqlError> {
530        let query = folder_select_columns(sql::Select::new())
531            .from("folders")
532            .left_join(
533                "account_login_folder login ON folders.folder_id = login.folder_id",
534            )
535            .where_clause("folders.account_id=?1")
536            .where_and("login.account_id=?1");
537
538        let mut stmt = self.conn.prepare_cached(&query.as_string())?;
539        stmt
540            .query_row([account_id], |row| row.try_into())
541            .optional()
542    }
543
544    /// Try to find a device folder for an account.
545    pub fn find_device_folder(
546        &self,
547        account_id: i64,
548    ) -> StdResult<Option<FolderRow>, SqlError> {
549        let query = folder_select_columns(sql::Select::new())
550            .from("folders")
551            .left_join(
552                "account_device_folder device ON folders.folder_id = device.folder_id",
553            )
554            .where_clause("folders.account_id=?1")
555            .where_and("device.account_id=?1");
556
557        let mut stmt = self.conn.prepare_cached(&query.as_string())?;
558        stmt
559            .query_row([account_id], |row| row.try_into())
560            .optional()
561    }
562
563    /// List user folders for an account.
564    ///
565    /// Does not include the identity and device folders.
566    pub fn list_user_folders(
567        &self,
568        account_id: i64,
569    ) -> Result<Vec<FolderRow>> {
570        let query = folder_select_columns(sql::Select::new())
571            .from("folders")
572            .left_join(
573                "account_login_folder login ON folders.folder_id = login.folder_id",
574            )
575            .left_join(
576                "account_device_folder device ON folders.folder_id = device.folder_id",
577            )
578            .where_clause("folders.account_id=?1")
579            .where_and("login.folder_id IS NULL")
580            .where_and("device.folder_id IS NULL");
581
582        let mut stmt = self.conn.prepare_cached(&query.as_string())?;
583
584        fn convert_row(row: &Row<'_>) -> Result<FolderRow> {
585            Ok(row.try_into()?)
586        }
587
588        let rows = stmt.query_and_then([account_id], convert_row)?;
589        let mut folders = Vec::new();
590        for row in rows {
591            folders.push(row?);
592        }
593        Ok(folders)
594    }
595
596    /// Update the name of a folder.
597    pub fn update_name(&self, folder_id: &VaultId, name: &str) -> Result<()> {
598        let modified_at = UtcDateTime::default().to_rfc3339()?;
599        let query = sql::Update::new()
600            .update("folders")
601            .set("name = ?1, modified_at = ?2")
602            .where_clause("identifier = ?3");
603        let mut stmt = self.conn.prepare_cached(&query.as_string())?;
604        stmt.execute((name, modified_at, folder_id.to_string()))?;
605        Ok(())
606    }
607
608    /// Update the folder flags.
609    pub fn update_flags(
610        &self,
611        folder_id: &VaultId,
612        flags: &VaultFlags,
613    ) -> Result<()> {
614        let flags = flags.bits().to_le_bytes();
615        let modified_at = UtcDateTime::default().to_rfc3339()?;
616        let query = sql::Update::new()
617            .update("folders")
618            .set("flags = ?1, modified_at = ?2")
619            .where_clause("identifier = ?3");
620        let mut stmt = self.conn.prepare_cached(&query.as_string())?;
621        stmt.execute((flags, modified_at, folder_id.to_string()))?;
622        Ok(())
623    }
624
625    /// Update the folder meta data.
626    pub fn update_meta(
627        &self,
628        folder_id: &VaultId,
629        meta: &[u8],
630    ) -> Result<()> {
631        let modified_at = UtcDateTime::default().to_rfc3339()?;
632        let query = sql::Update::new()
633            .update("folders")
634            .set("meta = ?1, modified_at = ?2")
635            .where_clause("identifier = ?3");
636        let mut stmt = self.conn.prepare_cached(&query.as_string())?;
637        stmt.execute((meta, modified_at, folder_id.to_string()))?;
638        Ok(())
639    }
640
641    /// Create the folder entity in the database.
642    pub fn insert_folder(
643        &self,
644        account_id: i64,
645        folder_row: &FolderRow,
646    ) -> StdResult<i64, SqlError> {
647        let query = sql::Insert::new()
648            .insert_into(
649                r#"
650                folders
651                (
652                    account_id,
653                    created_at,
654                    modified_at,
655                    identifier,
656                    name,
657                    salt,
658                    meta,
659                    seed,
660                    version,
661                    cipher,
662                    kdf,
663                    flags
664                )
665            "#,
666            )
667            .values("(?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12)");
668
669        let mut stmt = self.conn.prepare_cached(&query.as_string())?;
670        stmt.execute((
671            &account_id,
672            &folder_row.created_at,
673            &folder_row.modified_at,
674            &folder_row.identifier,
675            &folder_row.name,
676            &folder_row.salt,
677            &folder_row.meta,
678            &folder_row.seed,
679            &folder_row.version,
680            &folder_row.cipher,
681            &folder_row.kdf,
682            &folder_row.flags,
683        ))?;
684
685        Ok(self.conn.last_insert_rowid())
686    }
687
688    /// Update the folder entity in the database.
689    pub fn update_folder(
690        &self,
691        folder_id: &VaultId,
692        folder_row: &FolderRow,
693    ) -> StdResult<(), SqlError> {
694        let query = sql::Update::new()
695            .update("folders")
696            .set(
697                r#"
698                    modified_at = ?1,
699                    identifier = ?2,
700                    name = ?3,
701                    salt = ?4,
702                    meta = ?5,
703                    seed = ?6,
704                    version = ?7,
705                    cipher = ?8,
706                    kdf = ?9,
707                    flags = ?10
708                 "#,
709            )
710            .where_clause("identifier=?11");
711        let mut stmt = self.conn.prepare_cached(&query.as_string())?;
712        stmt.execute((
713            &folder_row.modified_at,
714            &folder_row.identifier,
715            &folder_row.name,
716            &folder_row.salt,
717            &folder_row.meta,
718            &folder_row.seed,
719            &folder_row.version,
720            &folder_row.cipher,
721            &folder_row.kdf,
722            &folder_row.flags,
723            folder_id.to_string(),
724        ))?;
725
726        Ok(())
727    }
728
729    /// Create folder secret rows.
730    pub fn insert_folder_secrets(
731        &self,
732        folder_id: i64,
733        rows: &[SecretRow],
734    ) -> Result<HashMap<SecretId, i64>> {
735        let mut secret_ids = HashMap::new();
736        for secret_row in rows {
737            let identifier: SecretId = secret_row.identifier.parse()?;
738            let secret_id =
739                self.insert_secret_by_row_id(folder_id, secret_row)?;
740            secret_ids.insert(identifier, secret_id);
741        }
742        Ok(secret_ids)
743    }
744
745    /// Create folder secret.
746    pub fn insert_secret(
747        &self,
748        folder_id: &VaultId,
749        secret_row: &SecretRow,
750    ) -> StdResult<i64, SqlError> {
751        let row = self.find_one(folder_id)?;
752        self.insert_secret_by_row_id(row.row_id, secret_row)
753    }
754
755    /// Insert a secret using the folder row id.
756    pub fn insert_secret_by_row_id(
757        &self,
758        folder_id: i64,
759        secret_row: &SecretRow,
760    ) -> StdResult<i64, SqlError> {
761        // NOTE: we have to use an upsert here as auto merge
762        // NOTE: can try to create secrets that already exist
763        // NOTE: so we handle the conflict situation
764        let query = sql::Insert::new()
765            .insert_into("folder_secrets (folder_id, identifier, commit_hash, meta, secret, created_at, modified_at)")
766            .values("(?1, ?2, ?3, ?4, ?5, ?6, ?7)")
767            .on_conflict(
768            r#"
769                (identifier)
770                DO UPDATE SET
771                    folder_id=excluded.folder_id,
772                    commit_hash=excluded.commit_hash,
773                    meta=excluded.meta,
774                    secret=excluded.secret,
775                    modified_at=excluded.modified_at
776            "#);
777        let mut stmt = self.conn.prepare_cached(&query.as_string())?;
778        stmt.execute((
779            &folder_id,
780            &secret_row.identifier,
781            &secret_row.commit,
782            &secret_row.meta,
783            &secret_row.secret,
784            &secret_row.created_at,
785            &secret_row.modified_at,
786        ))?;
787        Ok(self.conn.last_insert_rowid())
788    }
789
790    /// Find a folder secret.
791    pub fn find_secret(
792        &self,
793        folder_id: &VaultId,
794        secret_id: &SecretId,
795    ) -> StdResult<Option<SecretRow>, SqlError> {
796        let row = self.find_one(folder_id)?;
797        let query = secret_select_columns(sql::Select::new())
798            .from("folder_secrets")
799            .where_clause("folder_id=?1")
800            .where_and("identifier=?2");
801
802        let mut stmt = self.conn.prepare_cached(&query.as_string())?;
803        stmt
804            .query_row((row.row_id, secret_id.to_string()), |row| {
805                let row: SecretRow = row.try_into()?;
806                Ok(row)
807            })
808            .optional()
809    }
810
811    /// Update a folder secret.
812    pub fn update_secret(
813        &self,
814        folder_id: &VaultId,
815        secret_row: &SecretRow,
816    ) -> Result<bool> {
817        let modified_at = UtcDateTime::default().to_rfc3339()?;
818        let row = self.find_one(folder_id)?;
819        let query = sql::Update::new()
820            .update("folder_secrets")
821            .set(
822                r#"
823
824                    modified_at=?1,
825                    commit_hash=?2,
826                    meta=?3, 
827                    secret=?4
828                 "#,
829            )
830            .where_clause("folder_id=?5")
831            .where_and("identifier = ?6");
832
833        let mut stmt = self.conn.prepare_cached(&query.as_string())?;
834        let affected_rows = stmt.execute((
835            modified_at,
836            &secret_row.commit,
837            &secret_row.meta,
838            &secret_row.secret,
839            row.row_id,
840            &secret_row.identifier,
841        ))?;
842        Ok(affected_rows > 0)
843    }
844
845    /// Load secret rows.
846    pub fn load_secrets(&self, folder_row_id: i64) -> Result<Vec<SecretRow>> {
847        let query = secret_select_columns(sql::Select::new())
848            .from("folder_secrets")
849            .where_clause("folder_id=?1");
850        let mut stmt = self.conn.prepare_cached(&query.as_string())?;
851
852        fn convert_row(row: &Row<'_>) -> Result<SecretRow> {
853            Ok(row.try_into()?)
854        }
855
856        let rows = stmt.query_and_then([folder_row_id], convert_row)?;
857        let mut secrets = Vec::new();
858        for row in rows {
859            secrets.push(row?);
860        }
861        Ok(secrets)
862    }
863
864    /// List secret ids.
865    pub fn list_secret_ids(
866        &self,
867        folder_id: &VaultId,
868    ) -> Result<Vec<SecretId>> {
869        let folder = self.find_one(folder_id)?;
870        let query = sql::Select::new()
871            .select("identifier")
872            .from("folder_secrets")
873            .where_clause("folder_id=?1");
874        let mut stmt = self.conn.prepare_cached(&query.as_string())?;
875
876        fn convert_row(row: &Row<'_>) -> Result<SecretId> {
877            let id: String = row.get(0)?;
878            Ok(id.parse()?)
879        }
880
881        let rows = stmt.query_and_then([folder.row_id], convert_row)?;
882        let mut secrets = Vec::new();
883        for row in rows {
884            secrets.push(row?);
885        }
886        Ok(secrets)
887    }
888
889    /// Delete a folder.
890    pub fn delete_folder(
891        &self,
892        folder_id: &VaultId,
893    ) -> StdResult<bool, SqlError> {
894        let row = self.find_one(folder_id)?;
895        let query = sql::Delete::new()
896            .delete_from("folders")
897            .where_clause("folder_id = ?1");
898        let mut stmt = self.conn.prepare_cached(&query.as_string())?;
899        let affected_rows = stmt.execute([row.row_id])?;
900        Ok(affected_rows > 0)
901    }
902
903    /// Delete folder secret.
904    pub fn delete_secret(
905        &self,
906        folder_id: &VaultId,
907        secret_id: &SecretId,
908    ) -> StdResult<bool, SqlError> {
909        let row = self.find_one(folder_id)?;
910        let query = sql::Delete::new()
911            .delete_from("folder_secrets")
912            .where_clause("folder_id = ?1")
913            .where_and("identifier = ?2");
914        let mut stmt = self.conn.prepare_cached(&query.as_string())?;
915        let affected_rows =
916            stmt.execute((row.row_id, secret_id.to_string()))?;
917        Ok(affected_rows > 0)
918    }
919
920    /// Delete all folder secrets.
921    fn delete_all_secrets(
922        &self,
923        folder_id: i64,
924    ) -> StdResult<usize, SqlError> {
925        let query = sql::Delete::new()
926            .delete_from("folder_secrets")
927            .where_clause("folder_id = ?1");
928        let mut stmt = self.conn.prepare_cached(&query.as_string())?;
929        stmt.execute([folder_id])
930    }
931}