Skip to content

Commit 62bacbc

Browse files
committed
refactor: simplify envelope state handling
1 parent 78a5b80 commit 62bacbc

6 files changed

Lines changed: 405 additions & 151 deletions

File tree

src/command/client.rs

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -57,32 +57,33 @@ pub enum EnvelopeCmd {
5757

5858
impl EnvelopeCmd {
5959
pub async fn run(self) -> Result<()> {
60-
let state = core::state::detect()?;
60+
let state = core::state::detect().await?;
6161

6262
match (self, state) {
6363
// init: only valid when uninitialized
6464
(Self::Init, None) => {
65-
core::init().await?;
65+
UnlockedEnvelope::init().await?;
6666
Ok(())
6767
}
6868
(Self::Init, _) => bail!("envelope is already initialized"),
6969

7070
// unlock: only valid when locked
71-
(Self::Unlock, Some(EnvelopeState::Locked(env))) => {
71+
(Self::Unlock, Some(EnvelopeState::Locked(lenvelope))) => {
7272
let password = utils::prompt_password("Password: ")?;
73-
env.unlock(&password)?;
73+
let path = core::envelope_path()?;
74+
lenvelope.unlock(&password).await?.store(&path).await?;
7475
println!("database unlocked successfully");
7576
Ok(())
7677
}
77-
(Self::Unlock, Some(EnvelopeState::Unlocked)) => {
78+
(Self::Unlock, Some(EnvelopeState::Unlocked(_))) => {
7879
bail!("envelope is already unlocked")
7980
}
8081

8182
// lock: only valid when unlocked
82-
(Self::Lock, Some(EnvelopeState::Unlocked)) => {
83+
(Self::Lock, Some(EnvelopeState::Unlocked(uenvelope))) => {
8384
let password = utils::prompt_password_confirm()?;
84-
let envelope = UnlockedEnvelope::open().await?;
85-
envelope.lock(&password)?;
85+
let path = core::envelope_path()?;
86+
uenvelope.lock(&password).await?.store(&path)?;
8687
println!("database locked successfully");
8788
Ok(())
8889
}
@@ -91,20 +92,18 @@ impl EnvelopeCmd {
9192
}
9293

9394
// all other commands: only valid when unlocked
94-
(cmd, Some(EnvelopeState::Unlocked)) => {
95-
let UnlockedEnvelope { db } = UnlockedEnvelope::open().await?;
96-
cmd.run_with_db(&db).await
97-
}
95+
(cmd, Some(EnvelopeState::Unlocked(envelope))) => cmd.run_with_db(envelope.db()).await,
9896

9997
// all other commands when locked: ask for password, decrypt to
100-
// memory, run, re-encrypt if modified
98+
// memory, run, re-encrypt if modified. The unlocked working copy
99+
// stays in memory for the duration of the command.
101100
(cmd, Some(EnvelopeState::Locked(envelope))) => {
102101
let password = utils::prompt_password("Password: ")?;
103-
let plaintext = envelope.decrypt_bytes(&password)?;
104-
let unlocked = UnlockedEnvelope::open_in_memory(&plaintext).await?;
105-
cmd.run_with_db(&unlocked.db).await?;
106-
if unlocked.db.total_changes().await? > 0 {
107-
unlocked.store_locked(&password).await?;
102+
let unlocked = envelope.unlock(&password).await?;
103+
cmd.run_with_db(unlocked.db()).await?;
104+
if unlocked.db().total_changes().await? > 0 {
105+
let path = core::envelope_path()?;
106+
unlocked.lock(&password).await?.store(&path)?;
108107
}
109108
Ok(())
110109
}

src/core/mod.rs

Lines changed: 10 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
11
pub(crate) mod crypto;
2-
pub mod state;
2+
pub(crate) mod state;
33

44
use std::env;
5-
use std::path::PathBuf;
5+
use std::path::{Path, PathBuf};
66

7-
use anyhow::{Context, Result};
8-
9-
use crate::core::state::UnlockedEnvelope;
10-
use crate::db::EnvelopeDb;
7+
use anyhow::Result;
118

129
const ENVELOPE_FILENAME: &str = ".envelope";
1310
const ENVELOPE_FILENAME_TMP: &str = ".envelope.tmp";
@@ -17,29 +14,13 @@ pub(crate) fn envelope_path() -> Result<PathBuf> {
1714
Ok(env::current_dir()?.join(ENVELOPE_FILENAME))
1815
}
1916

20-
/// Returns the path to the temporary .envelope file
21-
pub(crate) fn envelope_tmp_path() -> Result<PathBuf> {
22-
Ok(env::current_dir()?.join(ENVELOPE_FILENAME_TMP))
23-
}
24-
25-
/// Initializes a new envelope database.
26-
///
27-
/// Creates the .envelope SQLite database file and runs migrations.
28-
/// Consumes self and returns an UnlockedEnvelope on success.
29-
pub async fn init() -> Result<UnlockedEnvelope> {
17+
/// Returns the path to the .envelope file only if it exists
18+
pub(crate) fn envelope_path_exists() -> Result<Option<PathBuf>> {
3019
let path = envelope_path()?;
31-
let db_path = path
32-
.to_str()
33-
.context("current directory path contains invalid characters")?;
34-
35-
let pool = sqlx::sqlite::SqlitePool::connect(&format!("sqlite://{db_path}?mode=rwc"))
36-
.await
37-
.context("failed to create .envelope database")?;
38-
39-
sqlx::migrate!("./migrations")
40-
.run(&pool)
41-
.await
42-
.context("failed to initialize database schema")?;
20+
Ok(path.exists().then_some(path))
21+
}
4322

44-
Ok(UnlockedEnvelope::with_db(EnvelopeDb::with(pool)))
23+
/// Returns the temporary path used while atomically replacing `path`.
24+
pub(crate) fn envelope_tmp_path_for(path: &Path) -> PathBuf {
25+
path.with_file_name(ENVELOPE_FILENAME_TMP)
4526
}

src/core/state/locked.rs

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
11
use std::fs;
2+
use std::path::Path;
23

34
use anyhow::Result;
45
use zeroize::Zeroizing;
56

67
use crate::core::crypto::decrypt;
78
use crate::core::crypto::header::EnvelopeFileHeader;
8-
use crate::core::{envelope_path, envelope_tmp_path};
9+
use crate::core::envelope_tmp_path_for;
10+
use crate::core::state::UnlockedEnvelope;
911

1012
/// Represents a locked (encrypted) envelope.
1113
///
1214
/// Contains the encrypted ciphertext and header information needed for
1315
/// decryption.
1416
#[derive(Debug)]
15-
pub struct LockedEnvelope {
17+
pub(crate) struct LockedEnvelope {
1618
header: EnvelopeFileHeader,
1719
ciphertext: Vec<u8>,
1820
}
@@ -22,30 +24,47 @@ impl LockedEnvelope {
2224
Self { header, ciphertext }
2325
}
2426

25-
/// Decrypts the envelope to raw bytes. Returned in `Zeroizing` to not leak
26-
/// data.
27-
pub fn decrypt_bytes(&self, password: &str) -> Result<Zeroizing<Vec<u8>>> {
28-
decrypt(&self.ciphertext, &self.header, password.as_bytes()).map(Zeroizing::new)
27+
/// Persists the encrypted envelope to `path`.
28+
///
29+
/// The target file is replaced atomically via a temporary file rename.
30+
pub(crate) fn store(self, path: &Path) -> Result<()> {
31+
let tmp_path = envelope_tmp_path_for(path);
32+
let data = [
33+
self.header.to_bytes().as_slice(),
34+
self.ciphertext.as_slice(),
35+
]
36+
.concat();
37+
38+
fs::write(&tmp_path, &data)?;
39+
fs::rename(&tmp_path, path)?;
40+
Ok(())
2941
}
3042

3143
/// Decrypts the envelope database file.
3244
///
3345
/// Reads the encrypted file, decrypts it with the provided password,
34-
/// and writes the decrypted SQLite database back to disk using an
35-
/// atomic write (temp file + rename).
46+
/// and returns an [`UnlockedEnvelope`] loaded in memory.
47+
///
48+
/// Decryption never restores a disk-backed unlocked value directly. The
49+
/// plaintext is materialized into an in-memory SQLite database first, and
50+
/// callers must explicitly persist it if they want a plain SQLite file on
51+
/// disk again.
3652
///
3753
/// Consumes self since the envelope is no longer locked after this
3854
/// operation.
39-
pub fn unlock(self, password: &str) -> Result<()> {
40-
let path = envelope_path()?;
55+
pub(crate) async fn unlock(self, password: &str) -> Result<UnlockedEnvelope> {
4156
let plaintext =
4257
decrypt(&self.ciphertext, &self.header, password.as_bytes()).map(Zeroizing::new)?;
4358

44-
// Atomic write: write to temp file, then rename
45-
let tmp_path = envelope_tmp_path()?;
46-
fs::write(&tmp_path, &plaintext)?;
47-
fs::rename(&tmp_path, &path)?;
59+
UnlockedEnvelope::open_in_memory(plaintext.as_slice()).await
60+
}
4861

49-
Ok(())
62+
#[cfg(test)]
63+
pub(crate) fn to_bytes(&self) -> Vec<u8> {
64+
[
65+
self.header.to_bytes().as_slice(),
66+
self.ciphertext.as_slice(),
67+
]
68+
.concat()
5069
}
5170
}

0 commit comments

Comments
 (0)