diff --git a/src/commands/auth.rs b/src/commands/auth.rs index bf9a766..597061b 100644 --- a/src/commands/auth.rs +++ b/src/commands/auth.rs @@ -134,11 +134,13 @@ pub async fn run_login( db.upsert_account(&account)?; // Store to keychain and verify before clearing DB (keyutils may lose keys across processes) + // 再起動非永続な store (Linux keyutils) では DB フォールバックを消さない (notedeck#785) if crate::keychain::store_token(&account_id, &auth.token).is_ok() && crate::keychain::get_token(&account_id) .ok() .flatten() .is_some() + && crate::keychain::is_persistent() { let _ = db.clear_token(&account_id); } diff --git a/src/db.rs b/src/db.rs index dbbecb9..f595a8f 100644 --- a/src/db.rs +++ b/src/db.rs @@ -125,6 +125,11 @@ impl Database { let mut writer = Connection::open(path)?; writer.execute_batch(PRAGMAS_WRITER)?; + // DB は API トークンのフォールバック等の機微情報を含むため owner-only にする。 + // WAL/SHM は SQLite が本体と同じパーミッションで作るが、既存ファイルは + // 直さないので明示的に締める (notedeck#785) + Self::restrict_permissions(path); + // auto_vacuum=INCREMENTAL を保証してから migration 走らせる。 // auto_vacuum モード変更は VACUUM 後に有効化される SQLite の仕様なので、 // 既存 DB の場合はここで一度だけ VACUUM が走る。 @@ -163,6 +168,32 @@ impl Database { Ok(db) } + /// DB 本体と WAL/SHM を owner-only (0600) に締める。失敗しても DB は開ける + /// (パーミッションより可用性を優先し、エラーは握りつぶす)。 + #[cfg(unix)] + fn restrict_permissions(path: &Path) { + use std::os::unix::fs::PermissionsExt; + for suffix in ["", "-wal", "-shm"] { + let target = if suffix.is_empty() { + path.to_path_buf() + } else { + let mut os = path.as_os_str().to_owned(); + os.push(suffix); + std::path::PathBuf::from(os) + }; + if let Ok(meta) = std::fs::metadata(&target) { + let mut perms = meta.permissions(); + if perms.mode() & 0o077 != 0 { + perms.set_mode(0o600); + let _ = std::fs::set_permissions(&target, perms); + } + } + } + } + + #[cfg(not(unix))] + fn restrict_permissions(_path: &Path) {} + /// `auto_vacuum=INCREMENTAL` を保証する。SQLite では `auto_vacuum` モード変更は /// VACUUM 後にしか有効化されない仕様のため、必要なら 1 度だけ VACUUM を走らせる。 /// 新規 DB なら最初の `PRAGMA auto_vacuum` 実行で適用済みとなり VACUUM は走らない。 @@ -1552,6 +1583,24 @@ mod tests { assert_eq!(results[0].id, "n1"); } + #[test] + #[cfg(unix)] + fn db_file_is_owner_only() { + use std::os::unix::fs::PermissionsExt; + let dir = tempfile::tempdir().unwrap(); + let db_path = dir.path().join("test.db"); + + // 既存 DB が緩いパーミッションでも open 時に締められること + std::fs::write(&db_path, b"").unwrap(); + let mut perms = std::fs::metadata(&db_path).unwrap().permissions(); + perms.set_mode(0o644); + std::fs::set_permissions(&db_path, perms).unwrap(); + + let _db = Database::open(&db_path).unwrap(); + let mode = std::fs::metadata(&db_path).unwrap().permissions().mode(); + assert_eq!(mode & 0o777, 0o600, "DB は owner-only であること"); + } + #[test] fn fts_search_reflects_note_edit() { let (_dir, db) = temp_db(); diff --git a/src/keychain.rs b/src/keychain.rs index 3c31a84..31bd1a6 100644 --- a/src/keychain.rs +++ b/src/keychain.rs @@ -40,6 +40,22 @@ pub fn init_store() -> Result<(), NoteDeckError> { Ok(()) } +/// 現在の credential store が再起動をまたいで永続するかどうか。 +/// +/// Linux の keyutils store はカーネルメモリ常駐(`UntilReboot`)のため false。 +/// false の場合、呼び出し側は DB 等の永続フォールバックを消してはならず、 +/// keychain は高速キャッシュとして扱う(notedeck#785)。 +#[cfg(feature = "keyring")] +pub fn is_persistent() -> bool { + match keyring_core::get_default_store() { + Some(store) => matches!( + store.persistence(), + keyring_core::CredentialPersistence::UntilDelete + ), + None => false, + } +} + #[cfg(feature = "keyring")] pub fn store_token(account_id: &str, token: &str) -> Result<(), NoteDeckError> { let entry = keyring_core::Entry::new(SERVICE, account_id) @@ -77,6 +93,29 @@ pub fn init_store() -> Result<(), NoteDeckError> { Ok(()) } +#[cfg(all(test, feature = "keyring", target_os = "linux"))] +mod tests { + use super::*; + + /// Linux の keyutils store は UntilReboot なので、init 前後どちらでも + /// is_persistent() は false を返す(DB フォールバックを消してはならない) + #[test] + fn is_persistent_is_false_on_linux() { + assert!( + !is_persistent(), + "store 未設定時は安全側 (false) に倒すこと" + ); + if init_store().is_ok() { + assert!(!is_persistent(), "keyutils store は再起動非永続"); + } + } +} + +#[cfg(not(feature = "keyring"))] +pub fn is_persistent() -> bool { + false +} + #[cfg(not(feature = "keyring"))] pub fn store_token(_account_id: &str, _token: &str) -> Result<(), NoteDeckError> { Ok(()) diff --git a/src/lib.rs b/src/lib.rs index bf70e4c..b2406aa 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,7 +25,8 @@ pub fn get_credentials(db: &Database, account_id: &str) -> Result<(String, Strin // Try keychain first (ignore errors — keychain may be unavailable) if let Some(token) = keychain::get_token(account_id).ok().flatten() { - if !account.token.is_empty() { + // 再起動非永続な store (Linux keyutils) では DB フォールバックを消さない (notedeck#785) + if !account.token.is_empty() && keychain::is_persistent() { let _ = db.clear_token(account_id); } return Ok((host, token)); @@ -35,7 +36,8 @@ pub fn get_credentials(db: &Database, account_id: &str) -> Result<(String, Strin let mut db_token = account.token.clone(); if !db_token.is_empty() { // Try lazy migration to keychain; verify before clearing DB - if keychain::store_token(account_id, &db_token).is_ok() + if keychain::is_persistent() + && keychain::store_token(account_id, &db_token).is_ok() && keychain::get_token(account_id) .ok() .flatten()