fix(lite-backend): keep wallet files and logs private to the owner
On Linux and macOS the Lite data dir (~/.silentdragonxlite) was created
with create_dir_all (0755 under the default umask) on every
get_zcash_data_path() call, even by the startup wallet_exists() probe.
do_save wrote the wallet through File::create (0644) and renamed it into
place, and fs::copy gave the .bak the same mode, so any local account
could read the wallet file, and the plaintext seed with it when the
wallet was not encrypted. The debug log was 0644 and its roller pattern
was relative, so rolled logs landed in the process's working directory.
A crash-leftover .dat.tmp was truncated and reused: an fd another
account had opened on it read the next wallet. And after encrypt(), the
first save copied the previous, plaintext wallet to .bak, where the seed
stayed until the next save, or for good if the app quit right away.
- New helpers, each an item-level #[cfg(unix)] / #[cfg(not(unix))]
pair: create_private_dir_all (0700 components), tighten (strips only
the group/other bits of an existing dir or file, keeps the owner bits,
never follows a symlink), create_private_file_new (unlinks a stale
file, then create_new with 0600), and a once-per-process warning when
a chmod fails (drvfs, FAT, NFS).
- get_zcash_data_path creates the default data dir 0700 and tightens an
existing one (for test/regtest also its parent). A caller-supplied
data_dir is created as before and never chmodded, and the params dir
is left alone.
- do_save writes a fresh 0600 temp file and tightens the .bak after the
copy. At the plaintext -> encrypted transition (the wallet is
encrypted and the on-disk header says plaintext) the .bak is copied
from the new, encrypted file. wallet_header_encrypted reads the flag
byte for versions 4 and 6-9 only (v5 is gzip) and returns None when it
cannot tell, which keeps the old behaviour.
- read_from_disk tightens a wallet restored from .bak, and the wallet
and .bak after every successful read.
- get_log_config pre-creates the log 0600 on unix and anchors the
roller in the data dir; a data path containing "{}" keeps the old
relative pattern. log4rs re-creates the log with the umask mode after
a 5 MB roll, so the 0700 data dir is the lasting control.
Flagged behaviour changes:
- on unix, the data dir is 0700 and existing files lose their
group/other bits (owner bits kept, symlinks and a caller-supplied
data_dir untouched); a shared group or a backup user under another uid
loses read access;
- a fresh 0600 temp file is used;
- the first encrypted save backs up the encrypted file;
- the log rolls inside the data dir (old rolled logs in former working
directories are not cleaned up).
On Windows the file modes are unchanged; the encrypted first .bak and
the log-roll location apply there too. Wallet format v9 is unchanged;
no ABI or symbol change.
Tests: the wallet and .bak are 0600 after every save and after an open
from 0644, and a wallet restored from a loose .bak is 0600, while the
caller's 0755 data dir is unchanged; a stale 0644 .dat.tmp is replaced
by a new inode and an fd held on it still reads the stale bytes; tighten
keeps owner bits (0555 -> 0500), ignores the other kind and symlinks,
create_private_dir_all makes every component 0700, and
create_private_file_new never reuses or follows a stale file or link;
after encrypt the first save's .bak is encrypted, byte-identical to the
wallet and unlocks to the seed; the header probe answers only for v4 and
v6-v9; the log is 0600, the roller pattern and a real roll stay in the
data dir, and a "{}" path falls back. An opt-in test
(DRAGONX_LITE_SCRATCH_HOME=1, temp HOME) checks the default data dir,
its testnet parent, a symlinked data dir and the params dir.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+471
-9
@@ -47,6 +47,83 @@ pub const DEFAULT_SERVER: &str = "https://lite.dragonx.is";
|
||||
pub const WALLET_NAME: &str = "silentdragonxlite-wallet.dat";
|
||||
pub const LOGFILE_NAME: &str = "silentdragonxlite-wallet.debug.log";
|
||||
|
||||
// C254: the data dir, the wallet files and the log are kept private to the owner on unix. On Windows these helpers
|
||||
// are the plain std calls (the per-user AppData ACL confines the files). Each one is an item-level
|
||||
// #[cfg(unix)] / #[cfg(not(unix))] pair, so the mingw build has nothing unused.
|
||||
|
||||
/// A chmod that fails (drvfs /mnt/c, FAT, NFS root-squash, a file of another uid) is warned about once per process:
|
||||
/// get_zcash_data_path() runs on every save. An AtomicBool, not a Once (which poisons if its closure panics).
|
||||
#[cfg(unix)]
|
||||
static PERM_WARNED: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
#[cfg(unix)]
|
||||
fn perm_warn(p: &Path, e: &io::Error) {
|
||||
if !PERM_WARNED.swap(true, Ordering::Relaxed) {
|
||||
warn!("Couldn't restrict {:?}: {}", p, e);
|
||||
}
|
||||
}
|
||||
|
||||
/// Create `d` and any missing parents; on unix every created component is 0700.
|
||||
#[cfg(unix)]
|
||||
fn create_private_dir_all(d: &Path) -> io::Result<()> {
|
||||
use std::os::unix::fs::DirBuilderExt;
|
||||
std::fs::DirBuilder::new().recursive(true).mode(0o700).create(d)
|
||||
}
|
||||
#[cfg(not(unix))]
|
||||
fn create_private_dir_all(d: &Path) -> io::Result<()> {
|
||||
std::fs::create_dir_all(d)
|
||||
}
|
||||
|
||||
/// Strip the group/other bits from an existing dir (`want_dir`) or regular file. The owner bits are kept (none is
|
||||
/// ever added), a symlink is never followed, and a missing path or a path of the other kind is left alone.
|
||||
#[cfg(unix)]
|
||||
fn tighten(p: &Path, want_dir: bool) {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
if let Ok(md) = std::fs::symlink_metadata(p) {
|
||||
let m = md.permissions().mode();
|
||||
let kind_ok = if want_dir { md.is_dir() } else { md.is_file() };
|
||||
if kind_ok && m & 0o077 != 0 {
|
||||
if let Err(e) = std::fs::set_permissions(p, std::fs::Permissions::from_mode(m & 0o7700)) {
|
||||
perm_warn(p, &e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(not(unix))]
|
||||
fn tighten(_p: &Path, _want_dir: bool) {}
|
||||
|
||||
/// A fresh inode, owner-only from creation (s1-D9). A stale file at `p` (a crash-leftover .dat.tmp, or a symlink) is
|
||||
/// unlinked, never reused or followed, so an fd opened on it earlier never sees the new content. A concurrent creator
|
||||
/// in between makes this fail, which is the safe outcome.
|
||||
#[cfg(unix)]
|
||||
fn create_private_file_new(p: &Path) -> io::Result<File> {
|
||||
use std::os::unix::fs::OpenOptionsExt;
|
||||
match std::fs::remove_file(p) {
|
||||
Ok(()) => {},
|
||||
Err(ref e) if e.kind() == ErrorKind::NotFound => {},
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
std::fs::OpenOptions::new().write(true).create_new(true).mode(0o600).open(p)
|
||||
}
|
||||
#[cfg(not(unix))]
|
||||
fn create_private_file_new(p: &Path) -> io::Result<File> {
|
||||
File::create(p)
|
||||
}
|
||||
|
||||
/// X3: the encryption flag in a wallet file's header (u64 LE version, then the flag byte that LightWallet::read reads
|
||||
/// for every version >= 4). None when the header cannot tell: a short read or an I/O error, v5 (gzip: its byte 8 is
|
||||
/// not the flag), v<=3 (no flag) or v>9 (a format this build does not know).
|
||||
fn wallet_header_encrypted(p: &Path) -> Option<bool> {
|
||||
let mut h = [0u8; 9];
|
||||
File::open(p).and_then(|mut f| f.read_exact(&mut h)).ok()?;
|
||||
let mut v = [0u8; 8];
|
||||
v.copy_from_slice(&h[..8]);
|
||||
match u64::from_le_bytes(v) {
|
||||
4 | 6 | 7 | 8 | 9 => Some(h[8] != 0),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct WalletStatus {
|
||||
pub is_syncing: bool,
|
||||
@@ -117,11 +194,35 @@ impl LightClientConfig {
|
||||
}
|
||||
|
||||
|
||||
/// The log roller's file pattern, anchored in the data dir (s1-D7): rolled logs stay inside the owner-only data
|
||||
/// dir instead of the process's working directory (which may be read-only, on Windows too). FixedWindowRoller
|
||||
/// replaces every "{}" in the pattern, so a data path that contains "{}" itself (or is not UTF-8) keeps the old
|
||||
/// relative pattern.
|
||||
fn log_roll_pattern(&self) -> String {
|
||||
const PATTERN: &str = "zecwallet-light-wallet-log{}";
|
||||
let data_dir = self.get_zcash_data_path();
|
||||
match (data_dir.to_str(), data_dir.join(PATTERN).to_str()) {
|
||||
(Some(d), Some(p)) if !d.contains("{}") => p.to_string(),
|
||||
_ => PATTERN.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Build the Logging config
|
||||
pub fn get_log_config(&self) -> io::Result<Config> {
|
||||
// C254: on unix the log is created owner-only (0600) before log4rs opens it for appending (log4rs 0.8.3 has
|
||||
// no mode option), and an existing one loses its group/other bits. That mode lasts only until the first 5 MB
|
||||
// roll, after which log4rs re-creates the file with the umask mode: the 0700 data dir is the lasting control
|
||||
// (s1-D11).
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::OpenOptionsExt;
|
||||
let log_path = self.get_log_path();
|
||||
let _ = std::fs::OpenOptions::new().append(true).create(true).mode(0o600).open(&log_path);
|
||||
tighten(&log_path, false);
|
||||
}
|
||||
let window_size = 3; // log0, log1, log2
|
||||
let fixed_window_roller =
|
||||
FixedWindowRoller::builder().build("zecwallet-light-wallet-log{}",window_size).unwrap();
|
||||
FixedWindowRoller::builder().build(&self.log_roll_pattern(),window_size).unwrap();
|
||||
let size_limit = 5 * 1024 * 1024; // 5MB as max log file size to roll
|
||||
let size_trigger = SizeTrigger::new(size_limit);
|
||||
let compound_policy = CompoundPolicy::new(Box::new(size_trigger),Box::new(fixed_window_roller));
|
||||
@@ -168,9 +269,27 @@ impl LightClientConfig {
|
||||
};
|
||||
}
|
||||
|
||||
// Create directory if it doesn't exist
|
||||
match std::fs::create_dir_all(zcash_data_location.clone()) {
|
||||
Ok(_) => zcash_data_location.into_boxed_path(),
|
||||
// Create directory if it doesn't exist. C254: a caller-supplied data_dir (tests, embedders) is created as
|
||||
// before and never chmodded. The default one is created owner-only (0700 on unix), and an existing one (for
|
||||
// test/regtest also its parent) loses its group/other bits; the owner bits are kept and a symlink is never
|
||||
// followed. The params dir (get_zcash_params_path) is left alone (s1-D5).
|
||||
let created = if self.data_dir.is_some() {
|
||||
std::fs::create_dir_all(zcash_data_location.clone())
|
||||
} else {
|
||||
create_private_dir_all(&zcash_data_location)
|
||||
};
|
||||
match created {
|
||||
Ok(_) => {
|
||||
if self.data_dir.is_none() {
|
||||
tighten(&zcash_data_location, true);
|
||||
if self.chain_name != "main" {
|
||||
if let Some(parent) = zcash_data_location.parent() {
|
||||
tighten(parent, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
zcash_data_location.into_boxed_path()
|
||||
},
|
||||
Err(e) => {
|
||||
eprintln!("Couldn't create zcash directory!\n{}", e);
|
||||
panic!("Couldn't create zcash directory!");
|
||||
@@ -474,6 +593,7 @@ impl LightClient {
|
||||
if bak_path.exists() {
|
||||
warn!("Wallet file missing but backup found, attempting recovery from {:?}", bak_path);
|
||||
std::fs::copy(&bak_path, config.get_wallet_path())?;
|
||||
tighten(&config.get_wallet_path(), false);
|
||||
info!("Wallet recovered from backup");
|
||||
} else {
|
||||
return Err(Error::new(ErrorKind::AlreadyExists,
|
||||
@@ -493,6 +613,7 @@ impl LightClient {
|
||||
let bak_path = config.get_wallet_path().with_extension("dat.bak");
|
||||
if bak_path.exists() {
|
||||
std::fs::copy(&bak_path, config.get_wallet_path())?;
|
||||
tighten(&config.get_wallet_path(), false);
|
||||
let mut file_buffer = BufReader::new(File::open(config.get_wallet_path())?);
|
||||
LightWallet::read(&mut file_buffer, config)?
|
||||
} else {
|
||||
@@ -501,6 +622,11 @@ impl LightClient {
|
||||
}
|
||||
};
|
||||
|
||||
// C254: a wallet or .bak written before owner-only saves (0644 under the default umask) is healed on every
|
||||
// open, not only at the next save.
|
||||
tighten(&config.get_wallet_path(), false);
|
||||
tighten(&config.get_wallet_path().with_extension("dat.bak"), false);
|
||||
|
||||
let mut lc = LightClient {
|
||||
wallet : Arc::new(RwLock::new(wallet)),
|
||||
config : config.clone(),
|
||||
@@ -718,11 +844,12 @@ impl LightClient {
|
||||
let tmp_path = wallet_path.with_extension("dat.tmp");
|
||||
let bak_path = wallet_path.with_extension("dat.bak");
|
||||
|
||||
// Write to a temporary file first (atomic save)
|
||||
// Write to a temporary file first (atomic save). C254/s1-D9: a fresh owner-only file (0600 on unix); a stale
|
||||
// .dat.tmp is unlinked first, never reused. The rename carries the mode to the wallet.
|
||||
let wallet = self.wallet.write().unwrap();
|
||||
let mut file_buffer = BufWriter::with_capacity(
|
||||
1_000_000, // 1 MB write buffer
|
||||
File::create(&tmp_path).map_err(|e| format!("Failed to create temp file: {}", e))?);
|
||||
create_private_file_new(&tmp_path).map_err(|e| format!("Failed to create temp file: {}", e))?);
|
||||
|
||||
r = match wallet.write(&mut file_buffer) {
|
||||
Ok(_) => Ok(()),
|
||||
@@ -744,9 +871,17 @@ impl LightClient {
|
||||
if r.is_ok() {
|
||||
// Create backup of existing wallet (if it exists)
|
||||
if wallet_path.exists() {
|
||||
if let Err(e) = std::fs::copy(&wallet_path, &bak_path) {
|
||||
warn!("Failed to create wallet backup: {}", e);
|
||||
// Non-fatal: continue with the save
|
||||
// s1-D6: at the plaintext -> encrypted transition, back up the NEW (encrypted) file, so no
|
||||
// plaintext seed survives in .bak
|
||||
let bak_src: &Path = if wallet.is_encrypted() && wallet_header_encrypted(&wallet_path) == Some(false)
|
||||
{ tmp_path.as_path() } else { &*wallet_path };
|
||||
match std::fs::copy(bak_src, &bak_path) {
|
||||
// fs::copy gives the .bak its source's mode: strip the group/other bits (C254)
|
||||
Ok(_) => tighten(&bak_path, false),
|
||||
Err(e) => {
|
||||
warn!("Failed to create wallet backup: {}", e);
|
||||
// Non-fatal: continue with the save
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2147,4 +2282,331 @@ pub fn test_bad_import() {
|
||||
assert_eq!(one_panic, Err("t-address scan job panicked".to_string()));
|
||||
}
|
||||
|
||||
// C254 tests (s1 tests 9-14). Every mode is set explicitly before it is checked, never left to the umask.
|
||||
#[cfg(unix)]
|
||||
fn mode(p: &std::path::Path) -> u32 {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
std::fs::symlink_metadata(p).unwrap().permissions().mode() & 0o7777
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn chmod(p: &std::path::Path, m: u32) {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
std::fs::set_permissions(p, std::fs::Permissions::from_mode(m)).unwrap();
|
||||
}
|
||||
|
||||
// s1 test 9: the wallet and its .bak are owner-only (0600) after every save and after an open, whatever their
|
||||
// earlier mode, and a wallet restored from a loose .bak is owner-only too. The caller's data dir is never chmodded.
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
pub fn test_wallet_files_private() {
|
||||
let tmp = TempDir::new("lctest").unwrap();
|
||||
chmod(tmp.path(), 0o755);
|
||||
let config = main_config(&tmp);
|
||||
let wallet = config.get_wallet_path().into_path_buf();
|
||||
let bak = wallet.with_extension("dat.bak");
|
||||
let lc = LightClient::new_from_phrase(TEST_SEED.to_string(), &config, 0, 0, false).unwrap();
|
||||
lc.do_save().unwrap();
|
||||
assert!(bak.exists());
|
||||
for save in 1..=2 {
|
||||
chmod(&wallet, 0o644);
|
||||
chmod(&bak, 0o644);
|
||||
lc.do_save().unwrap();
|
||||
assert_eq!(mode(&wallet), 0o600, "the wallet after save {}", save);
|
||||
assert_eq!(mode(&bak), 0o600, "the .bak after save {}", save);
|
||||
}
|
||||
drop(lc);
|
||||
|
||||
// An open heals both files before any save.
|
||||
chmod(&wallet, 0o644);
|
||||
chmod(&bak, 0o644);
|
||||
let lc = LightClient::read_from_disk(&config).unwrap();
|
||||
assert_eq!(mode(&wallet), 0o600, "the wallet after an open");
|
||||
assert_eq!(mode(&bak), 0o600, "the .bak after an open");
|
||||
drop(lc);
|
||||
|
||||
// A missing wallet restored from a loose .bak is owner-only.
|
||||
std::fs::remove_file(&wallet).unwrap();
|
||||
chmod(&bak, 0o644);
|
||||
let lc = LightClient::read_from_disk(&config).unwrap();
|
||||
assert_eq!(lc.do_seed_phrase().unwrap()["seed"], TEST_SEED.to_string());
|
||||
assert_eq!(mode(&wallet), 0o600, "the wallet restored from the .bak");
|
||||
|
||||
assert!(!wallet.with_extension("dat.tmp").exists());
|
||||
assert_eq!(mode(tmp.path()), 0o755, "a caller-supplied data dir is never chmodded");
|
||||
}
|
||||
|
||||
// s1 test 10 (D9): a crash-leftover 0644 .dat.tmp is unlinked, not reused. The save writes a fresh 0600 inode, so
|
||||
// an fd opened on the stale tmp earlier (another account's) never sees the new wallet.
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
pub fn test_stale_tmp_replaced() {
|
||||
use std::io::Read;
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
let tmp = TempDir::new("lctest").unwrap();
|
||||
let config = main_config(&tmp);
|
||||
let wallet = config.get_wallet_path().into_path_buf();
|
||||
let tmp_path = wallet.with_extension("dat.tmp");
|
||||
let lc = LightClient::new_from_phrase(TEST_SEED.to_string(), &config, 0, 0, false).unwrap();
|
||||
|
||||
std::fs::write(&tmp_path, b"stale").unwrap();
|
||||
chmod(&tmp_path, 0o644);
|
||||
let mut held = std::fs::File::open(&tmp_path).unwrap();
|
||||
let stale_ino = held.metadata().unwrap().ino();
|
||||
lc.do_save().unwrap();
|
||||
|
||||
assert!(!tmp_path.exists(), "a .dat.tmp is left behind");
|
||||
assert_eq!(mode(&wallet), 0o600);
|
||||
assert_ne!(std::fs::metadata(&wallet).unwrap().ino(), stale_ino, "the stale tmp's inode was reused");
|
||||
let mut seen = Vec::new();
|
||||
held.read_to_end(&mut seen).unwrap();
|
||||
assert_eq!(seen, b"stale".to_vec(), "the stale tmp's fd sees the new wallet");
|
||||
drop(lc);
|
||||
let lc = LightClient::read_from_disk(&config).unwrap();
|
||||
assert_eq!(lc.do_seed_phrase().unwrap()["seed"], TEST_SEED.to_string());
|
||||
}
|
||||
|
||||
// s1 test 11: the helpers' semantics.
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
pub fn test_tighten_semantics() {
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
use super::{tighten, create_private_dir_all, create_private_file_new};
|
||||
let tmp = TempDir::new("lctest").unwrap();
|
||||
let t = tmp.path();
|
||||
|
||||
// Only the group/other bits go; the owner bits stay as they were (none is added).
|
||||
for &(from, to) in &[(0o755, 0o700), (0o777, 0o700), (0o750, 0o700), (0o700, 0o700), (0o555, 0o500), (0o500, 0o500)] {
|
||||
let d = t.join(format!("dir{:o}", from));
|
||||
std::fs::create_dir(&d).unwrap();
|
||||
chmod(&d, from);
|
||||
tighten(&d, false);
|
||||
assert_eq!(mode(&d), from, "tighten(dir {:o}, want_dir=false) changed it", from);
|
||||
tighten(&d, true);
|
||||
assert_eq!(mode(&d), to, "dir {:o}", from);
|
||||
}
|
||||
for &(from, to) in &[(0o644, 0o600), (0o666, 0o600), (0o640, 0o600), (0o600, 0o600), (0o444, 0o400), (0o400, 0o400)] {
|
||||
let f = t.join(format!("file{:o}", from));
|
||||
std::fs::write(&f, b"x").unwrap();
|
||||
chmod(&f, from);
|
||||
tighten(&f, true);
|
||||
assert_eq!(mode(&f), from, "tighten(file {:o}, want_dir=true) changed it", from);
|
||||
tighten(&f, false);
|
||||
assert_eq!(mode(&f), to, "file {:o}", from);
|
||||
}
|
||||
tighten(&t.join("missing"), true);
|
||||
tighten(&t.join("missing"), false);
|
||||
|
||||
// A symlink is never followed.
|
||||
let target_dir = t.join("target-dir");
|
||||
std::fs::create_dir(&target_dir).unwrap();
|
||||
chmod(&target_dir, 0o755);
|
||||
let target_file = t.join("target-file");
|
||||
std::fs::write(&target_file, b"x").unwrap();
|
||||
chmod(&target_file, 0o644);
|
||||
std::os::unix::fs::symlink(&target_dir, t.join("link-dir")).unwrap();
|
||||
std::os::unix::fs::symlink(&target_file, t.join("link-file")).unwrap();
|
||||
tighten(&t.join("link-dir"), true);
|
||||
tighten(&t.join("link-file"), false);
|
||||
assert_eq!(mode(&target_dir), 0o755, "tighten followed a dir symlink");
|
||||
assert_eq!(mode(&target_file), 0o644, "tighten followed a file symlink");
|
||||
|
||||
// create_private_dir_all: every created component is 0700; an existing dir is accepted as it is.
|
||||
create_private_dir_all(&t.join("a").join("b").join("c")).unwrap();
|
||||
for p in &[t.join("a"), t.join("a").join("b"), t.join("a").join("b").join("c")] {
|
||||
assert_eq!(mode(p), 0o700, "{:?}", p);
|
||||
}
|
||||
create_private_dir_all(&target_dir).unwrap();
|
||||
assert_eq!(mode(&target_dir), 0o755);
|
||||
|
||||
// create_private_file_new: a fresh 0600 inode; a stale file or a symlink at the path is unlinked, never
|
||||
// reused or followed; a directory at the path is an error and stays.
|
||||
let fresh = t.join("fresh.tmp");
|
||||
create_private_file_new(&fresh).unwrap();
|
||||
assert_eq!(mode(&fresh), 0o600);
|
||||
let stale = t.join("stale.tmp");
|
||||
std::fs::write(&stale, b"stale").unwrap();
|
||||
chmod(&stale, 0o644);
|
||||
let stale_ino = std::fs::metadata(&stale).unwrap().ino();
|
||||
let _held = std::fs::File::open(&stale).unwrap();
|
||||
create_private_file_new(&stale).unwrap();
|
||||
assert_eq!(mode(&stale), 0o600);
|
||||
assert_eq!(std::fs::read(&stale).unwrap().len(), 0);
|
||||
assert_ne!(std::fs::metadata(&stale).unwrap().ino(), stale_ino);
|
||||
let via_link = t.join("via-link.tmp");
|
||||
std::os::unix::fs::symlink(&target_file, &via_link).unwrap();
|
||||
create_private_file_new(&via_link).unwrap();
|
||||
assert!(std::fs::symlink_metadata(&via_link).unwrap().is_file(), "the symlink was followed");
|
||||
assert_eq!(mode(&via_link), 0o600);
|
||||
assert_eq!(std::fs::read(&target_file).unwrap(), b"x".to_vec());
|
||||
assert_eq!(mode(&target_file), 0o644);
|
||||
assert!(create_private_file_new(&target_dir).is_err());
|
||||
assert!(target_dir.is_dir());
|
||||
|
||||
// A caller-supplied data_dir is never chmodded.
|
||||
chmod(t, 0o755);
|
||||
let config = main_config(&tmp);
|
||||
assert_eq!(&*config.get_zcash_data_path(), t);
|
||||
assert_eq!(mode(t), 0o755, "a caller-supplied data dir was chmodded");
|
||||
}
|
||||
|
||||
// s1 test 12 (D6): the first save after encrypt() backs up the new, encrypted file, so no plaintext seed survives
|
||||
// in .bak; that .bak is a full copy that unlocks to the seed. Later saves back up the encrypted wallet too.
|
||||
#[test]
|
||||
pub fn test_bak_not_plaintext_after_encrypt() {
|
||||
use crate::lightwallet::LightWallet;
|
||||
use super::wallet_header_encrypted;
|
||||
let tmp = TempDir::new("lctest").unwrap();
|
||||
let config = main_config(&tmp);
|
||||
let wallet = config.get_wallet_path().into_path_buf();
|
||||
let bak = wallet.with_extension("dat.bak");
|
||||
let header = |p: &std::path::Path| {
|
||||
let b = std::fs::read(p).unwrap();
|
||||
(b[..8].to_vec(), b[8])
|
||||
};
|
||||
let v9 = 9u64.to_le_bytes().to_vec();
|
||||
let lc = LightClient::new_from_phrase(TEST_SEED.to_string(), &config, 0, 0, false).unwrap();
|
||||
lc.do_save().unwrap();
|
||||
assert_eq!(header(&bak), (v9.clone(), 0), "precondition: a plaintext v9 .bak");
|
||||
assert_eq!(wallet_header_encrypted(&wallet), Some(false));
|
||||
|
||||
lc.wallet.write().unwrap().encrypt("password".to_string()).unwrap();
|
||||
lc.do_save().unwrap();
|
||||
assert_eq!(header(&wallet), (v9.clone(), 1), "the wallet is saved encrypted, as v9");
|
||||
assert_eq!(header(&bak), (v9.clone(), 1), "the .bak still holds the plaintext wallet after the encrypt save");
|
||||
assert_eq!(wallet_header_encrypted(&bak), Some(true));
|
||||
assert_eq!(std::fs::read(&bak).unwrap(), std::fs::read(&wallet).unwrap());
|
||||
let mut from_bak = LightWallet::read(std::io::BufReader::new(std::fs::File::open(&bak).unwrap()), &config).unwrap();
|
||||
assert!(from_bak.is_encrypted());
|
||||
from_bak.unlock("password".to_string()).unwrap();
|
||||
assert_eq!(from_bak.get_seed_phrase(), TEST_SEED.to_string());
|
||||
|
||||
lc.do_save().unwrap();
|
||||
assert_eq!(header(&bak).1, 1, "a later save backs up the encrypted wallet");
|
||||
// Removing encryption is not the transition: its save backs up the encrypted wallet as before.
|
||||
lc.wallet.write().unwrap().remove_encryption("password".to_string()).unwrap();
|
||||
lc.do_save().unwrap();
|
||||
assert_eq!(header(&wallet).1, 0, "the wallet is saved decrypted");
|
||||
assert_eq!(header(&bak).1, 1);
|
||||
}
|
||||
|
||||
// X3: wallet_header_encrypted reads the flag only where LightWallet::read has one at byte 8 (v4 and v6-v9).
|
||||
#[test]
|
||||
pub fn test_wallet_header_encrypted_probe() {
|
||||
use super::wallet_header_encrypted;
|
||||
let tmp = TempDir::new("lctest").unwrap();
|
||||
let p = tmp.path().join("w.dat");
|
||||
let probe = |v: u64, rest: &[u8]| {
|
||||
let mut b = v.to_le_bytes().to_vec();
|
||||
b.extend_from_slice(rest);
|
||||
std::fs::write(&p, &b).unwrap();
|
||||
wallet_header_encrypted(&p)
|
||||
};
|
||||
for &v in &[4u64, 6, 7, 8, 9] {
|
||||
assert_eq!(probe(v, &[0, 7, 7]), Some(false), "v{}", v);
|
||||
assert_eq!(probe(v, &[1]), Some(true), "v{}", v);
|
||||
assert_eq!(probe(v, &[2]), Some(true), "v{}: any non-zero flag, as LightWallet::read", v);
|
||||
assert_eq!(probe(v, &[]), None, "v{} without a flag byte", v);
|
||||
}
|
||||
for &v in &[0u64, 1, 2, 3, 5, 10, u64::MAX] {
|
||||
assert_eq!(probe(v, &[0]), None, "v{}", v);
|
||||
assert_eq!(probe(v, &[1]), None, "v{}", v);
|
||||
}
|
||||
std::fs::write(&p, b"").unwrap();
|
||||
assert_eq!(wallet_header_encrypted(&p), None, "an empty file");
|
||||
assert_eq!(wallet_header_encrypted(&tmp.path().join("missing.dat")), None, "a missing file");
|
||||
assert_eq!(wallet_header_encrypted(tmp.path()), None, "a directory");
|
||||
}
|
||||
|
||||
// s1 test 13 (D7, D11): get_log_config() creates the log owner-only and tightens an existing one; the roller's
|
||||
// pattern lies in the data dir, so a roll renames the log inside it; a data path containing "{}" keeps the old
|
||||
// relative pattern (FixedWindowRoller replaces every "{}").
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
pub fn test_log_paths() {
|
||||
use log4rs::append::rolling_file::policy::compound::roll::{Roll, fixed_window::FixedWindowRoller};
|
||||
let tmp = TempDir::new("lctest").unwrap();
|
||||
let config = main_config(&tmp);
|
||||
let log = config.get_log_path().into_path_buf();
|
||||
assert!(!log.exists());
|
||||
config.get_log_config().unwrap();
|
||||
assert_eq!(mode(&log), 0o600, "a new log");
|
||||
chmod(&log, 0o644);
|
||||
config.get_log_config().unwrap();
|
||||
assert_eq!(mode(&log), 0o600, "an existing loose log");
|
||||
|
||||
let pattern = config.log_roll_pattern();
|
||||
assert_eq!(pattern, tmp.path().join("zecwallet-light-wallet-log{}").to_str().unwrap());
|
||||
FixedWindowRoller::builder().build(&pattern, 3).unwrap().roll(&log).unwrap();
|
||||
assert!(!log.exists());
|
||||
assert_eq!(mode(&tmp.path().join("zecwallet-light-wallet-log0")), 0o600, "the rolled log is not in the data dir");
|
||||
|
||||
let odd = tmp.path().join("we{}ird");
|
||||
let odd_config = LightClientConfig::create_unconnected("main".to_string(), odd.to_str().map(|s| s.to_string()));
|
||||
assert_eq!(odd_config.log_roll_pattern(), "zecwallet-light-wallet-log{}");
|
||||
odd_config.get_log_config().unwrap();
|
||||
assert_eq!(mode(&odd_config.get_log_path()), 0o600);
|
||||
}
|
||||
|
||||
// s1 test 14, opt-in: the default data dir (no data_dir) is created 0700, a pre-made 0755 one becomes 0700 (for
|
||||
// test/regtest also its parent), and HOME, the params dir and a symlinked data dir are left alone. HOME points at
|
||||
// a fresh temp dir for the test's own duration, so no real ~/.silentdragonxlite is ever seen; still, run it alone
|
||||
// and only through rtest.sh (scratch HOME):
|
||||
// DRAGONX_LITE_SCRATCH_HOME=1 rtest.sh --lib --offline test_default_data_dir_private -- --ignored
|
||||
#[cfg(all(unix, not(target_os = "macos")))]
|
||||
#[test]
|
||||
#[ignore]
|
||||
pub fn test_default_data_dir_private() {
|
||||
if std::env::var("DRAGONX_LITE_SCRATCH_HOME").ok().as_deref() != Some("1") {
|
||||
eprintln!("test_default_data_dir_private: needs DRAGONX_LITE_SCRATCH_HOME=1 (through rtest.sh); skipped");
|
||||
return;
|
||||
}
|
||||
let home = TempDir::new("lchome").unwrap();
|
||||
chmod(home.path(), 0o755);
|
||||
let saved_home = std::env::var_os("HOME");
|
||||
std::env::set_var("HOME", home.path());
|
||||
let r = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
let h = home.path();
|
||||
let data = h.join(".silentdragonxlite");
|
||||
let main = LightClientConfig::create_unconnected("main".to_string(), None);
|
||||
assert!(!data.exists());
|
||||
assert!(!main.wallet_exists());
|
||||
assert_eq!(&*main.get_zcash_data_path(), data.as_path());
|
||||
assert_eq!(mode(&data), 0o700, "a new default data dir");
|
||||
chmod(&data, 0o755);
|
||||
assert!(!main.wallet_exists());
|
||||
assert_eq!(mode(&data), 0o700, "a pre-made 0755 data dir");
|
||||
|
||||
chmod(&data, 0o755);
|
||||
let test = LightClientConfig::create_unconnected("test".to_string(), None);
|
||||
assert!(!test.wallet_exists());
|
||||
assert_eq!(mode(&data.join("testnet3")), 0o700, "a new testnet3 dir");
|
||||
assert_eq!(mode(&data), 0o700, "the parent of testnet3");
|
||||
|
||||
let params = h.join(".zcash-params");
|
||||
std::fs::create_dir(¶ms).unwrap();
|
||||
chmod(¶ms, 0o755);
|
||||
main.get_zcash_params_path().unwrap();
|
||||
assert_eq!(mode(¶ms), 0o755, "the params dir was chmodded");
|
||||
assert_eq!(mode(h), 0o755, "HOME was chmodded");
|
||||
|
||||
let h2 = h.join("h2");
|
||||
let real = h.join("real");
|
||||
std::fs::create_dir(&h2).unwrap();
|
||||
std::fs::create_dir(&real).unwrap();
|
||||
chmod(&real, 0o755);
|
||||
std::os::unix::fs::symlink(&real, h2.join(".silentdragonxlite")).unwrap();
|
||||
std::env::set_var("HOME", &h2);
|
||||
assert!(!main.wallet_exists());
|
||||
assert_eq!(mode(&real), 0o755, "a symlinked data dir was followed");
|
||||
}));
|
||||
match saved_home {
|
||||
Some(v) => std::env::set_var("HOME", v),
|
||||
None => std::env::remove_var("HOME"),
|
||||
}
|
||||
if let Err(e) = r {
|
||||
std::panic::resume_unwind(e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user