Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/commands/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
49 changes: 49 additions & 0 deletions src/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 が走る。
Expand Down Expand Up @@ -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 は走らない。
Expand Down Expand Up @@ -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();
Expand Down
39 changes: 39 additions & 0 deletions src/keychain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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(())
Expand Down
6 changes: 4 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand All @@ -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()
Expand Down
Loading