Create the archive readable by its owner only
ci / test (pull_request) Skipped
github/ci (branch) GitHub Actions
ci / github (pull_request) Successful in 2m32s
ci / announce (pull_request) Skipped

An archive holds a whole mailbox, its contacts and calendars, and it was
created with SQLite's default mode, readable by every user on the machine.
On Unix a new archive is now created with mode 0600 before SQLite opens it;
SQLite gives the -wal and -shm files the database file's mode, so they
follow, which a test confirms. An existing archive that others can read is
left as it is, with a warning naming it and the chmod that fixes it.
Nothing changes on Windows.
This commit is contained in:
2026-09-30 11:26:18 -07:00
parent 2d94915702
commit 38d32811e4
+95
View File
@@ -1,5 +1,6 @@
/* /*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]> * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
* SPDX-FileCopyrightText: 2026 John Coffey <[email protected]>
* *
* SPDX-License-Identifier: Apache-2.0 OR MIT * SPDX-License-Identifier: Apache-2.0 OR MIT
*/ */
@@ -9,6 +10,7 @@ use rusqlite::Connection;
pub const SCHEMA_SQL: &str = include_str!("schema.sql"); pub const SCHEMA_SQL: &str = include_str!("schema.sql");
pub fn open(path: &std::path::Path) -> Result<Connection, OpenError> { pub fn open(path: &std::path::Path) -> Result<Connection, OpenError> {
private_archive(path)?;
let conn = Connection::open(path)?; let conn = Connection::open(path)?;
apply_pragmas(&conn)?; apply_pragmas(&conn)?;
apply_schema(&conn)?; apply_schema(&conn)?;
@@ -72,6 +74,50 @@ fn ensure_graph_ids_accept_file_nodes(conn: &Connection) -> Result<(), OpenError
Ok(()) Ok(())
} }
/// An archive holds a whole mailbox, so it is created readable by its owner
/// only. SQLite gives its `-wal` and `-shm` files the database file's mode,
/// so they follow. An existing archive others can read is left as it is,
/// with a warning and the command that fixes it.
#[cfg(unix)]
fn private_archive(path: &std::path::Path) -> Result<(), OpenError> {
use std::os::unix::fs::OpenOptionsExt;
match std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.mode(0o600)
.open(path)
{
Ok(_) => Ok(()),
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {
if let Some(warning) = permission_warning(path) {
eprintln!("warning: {warning}");
}
Ok(())
}
Err(e) => Err(OpenError::Create(e)),
}
}
#[cfg(not(unix))]
fn private_archive(_path: &std::path::Path) -> Result<(), OpenError> {
Ok(())
}
/// The warning for an archive that someone other than its owner can read.
#[cfg(unix)]
fn permission_warning(path: &std::path::Path) -> Option<String> {
use std::os::unix::fs::PermissionsExt;
let mode = std::fs::metadata(path).ok()?.permissions().mode();
(mode & 0o077 != 0).then(|| {
format!(
"archive {} can be read by other users (mode {:o}); run: chmod 600 {}",
path.display(),
mode & 0o777,
path.display()
)
})
}
fn apply_pragmas(conn: &Connection) -> Result<(), OpenError> { fn apply_pragmas(conn: &Connection) -> Result<(), OpenError> {
conn.pragma_update(None, "journal_mode", "WAL")?; conn.pragma_update(None, "journal_mode", "WAL")?;
conn.pragma_update(None, "foreign_keys", "ON")?; conn.pragma_update(None, "foreign_keys", "ON")?;
@@ -83,4 +129,53 @@ fn apply_pragmas(conn: &Connection) -> Result<(), OpenError> {
pub enum OpenError { pub enum OpenError {
#[error("sqlite error: {0}")] #[error("sqlite error: {0}")]
Sqlite(#[from] rusqlite::Error), Sqlite(#[from] rusqlite::Error),
#[error("cannot create the archive: {0}")]
Create(std::io::Error),
}
#[cfg(all(test, unix))]
mod permission_tests {
use super::*;
use std::os::unix::fs::PermissionsExt;
fn mode(p: &std::path::Path) -> u32 {
std::fs::metadata(p).unwrap().permissions().mode() & 0o777
}
fn scratch(name: &str) -> std::path::PathBuf {
let dir =
std::env::temp_dir().join(format!("inbuxa-migrate-perm-{}-{name}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
dir.join("a.sqlite")
}
#[test]
fn a_new_archive_and_its_wal_are_private() {
let path = scratch("new");
let conn = open(&path).unwrap();
conn.execute_batch("CREATE TABLE t(x); INSERT INTO t VALUES (1);")
.unwrap();
assert_eq!(mode(&path), 0o600);
let wal = path.with_extension("sqlite-wal");
assert!(wal.exists(), "WAL mode writes a -wal file");
assert_eq!(mode(&wal), 0o600);
drop(conn);
let _ = std::fs::remove_dir_all(path.parent().unwrap());
}
#[test]
fn an_existing_readable_archive_is_warned_about_not_changed() {
let path = scratch("existing");
drop(open(&path).unwrap());
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o644)).unwrap();
let warning = permission_warning(&path).expect("warns");
assert!(warning.contains("chmod 600"), "{warning}");
assert!(warning.contains("644"), "{warning}");
drop(open(&path).unwrap());
assert_eq!(mode(&path), 0o644, "left as it is");
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600)).unwrap();
assert!(permission_warning(&path).is_none());
let _ = std::fs::remove_dir_all(path.parent().unwrap());
}
} }