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