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
1 change: 1 addition & 0 deletions CHANGELOG-rust.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Changelog

## [Unreleased]
- Restrict persisted Noise config files to private permissions on Unix.

## 0.13.0
- Add `BitBox::from_transport()`
Expand Down
155 changes: 150 additions & 5 deletions src/noise.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// SPDX-License-Identifier: Apache-2.0

use crate::util::Threading;
#[cfg(unix)]
use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
use thiserror::Error;

#[derive(Error, Debug)]
Expand Down Expand Up @@ -55,6 +57,19 @@ pub struct NoiseConfigNoCache;
impl NoiseConfig for NoiseConfigNoCache {}
impl Threading for NoiseConfigNoCache {}

#[cfg(unix)]
fn ensure_private_file_permissions(path: &std::path::Path) -> Result<(), ConfigError> {
let metadata = std::fs::symlink_metadata(path).map_err(|e| ConfigError(e.to_string()))?;

// A symlink can be part of an intentional setup. Do not change its target's permissions.
if metadata.file_type().is_symlink() {
return Ok(());
}

std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))
.map_err(|e| ConfigError(e.to_string()))
}

pub struct PersistedNoiseConfig {
config_dir: String,
}
Expand All @@ -63,7 +78,8 @@ impl Threading for PersistedNoiseConfig {}

impl PersistedNoiseConfig {
/// Creates a new persisting noise config, which stores the pairing information in "bitbox.json"
/// in the provided directory.
/// in the provided directory. The directory must already exist and should be created with
/// `0700` permissions on Unix.
pub fn new(config_dir: &str) -> PersistedNoiseConfig {
PersistedNoiseConfig {
config_dir: config_dir.into(),
Expand All @@ -81,7 +97,10 @@ impl NoiseConfig for PersistedNoiseConfig {
return Ok(NoiseConfigData::default());
}

let mut file = std::fs::File::open(config_path).map_err(|e| ConfigError(e.to_string()))?;
let mut file = std::fs::File::open(&config_path).map_err(|e| ConfigError(e.to_string()))?;

#[cfg(unix)]
ensure_private_file_permissions(&config_path)?;

let mut contents = String::new();
file.read_to_string(&mut contents)
Expand All @@ -94,13 +113,139 @@ impl NoiseConfig for PersistedNoiseConfig {
use std::io::Write;

let config_path = std::path::Path::new(&self.config_dir).join("bitbox.json");
let data = serde_json::to_string(conf).map_err(|e| ConfigError(e.to_string()))?;

let mut file =
std::fs::File::create(config_path).map_err(|e| ConfigError(e.to_string()))?;
let mut options = std::fs::File::options();
options.write(true).create(true);

let data = serde_json::to_string(conf).map_err(|e| ConfigError(e.to_string()))?;
#[cfg(unix)]
options.mode(0o600);

let mut file = options
.open(&config_path)
.map_err(|e| ConfigError(e.to_string()))?;

#[cfg(unix)]
ensure_private_file_permissions(&config_path)?;

file.set_len(0).map_err(|e| ConfigError(e.to_string()))?;

file.write_all(data.as_bytes())
.map_err(|e| ConfigError(e.to_string()))
}
}

#[cfg(all(test, unix))]
mod tests {
use super::*;
use std::os::unix::fs::PermissionsExt;

struct TempDir(std::path::PathBuf);

impl TempDir {
fn new() -> Self {
let unique = format!(
"bitbox-api-noise-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
);
Self(std::env::temp_dir().join(unique))
}
}

impl Drop for TempDir {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}

fn mode(path: &std::path::Path) -> u32 {
std::fs::metadata(path).unwrap().permissions().mode() & 0o777
}

#[test]
fn store_config_uses_private_file_permissions() {
let dir = TempDir::new();
std::fs::create_dir(&dir.0).unwrap();
std::fs::set_permissions(&dir.0, std::fs::Permissions::from_mode(0o755)).unwrap();
let config = PersistedNoiseConfig::new(dir.0.to_str().unwrap());

config.store_config(&NoiseConfigData::default()).unwrap();

assert_eq!(mode(&dir.0), 0o755);
assert_eq!(mode(&dir.0.join("bitbox.json")), 0o600);
}

#[test]
fn store_config_does_not_create_config_directory() {
let dir = TempDir::new();
let config = PersistedNoiseConfig::new(dir.0.to_str().unwrap());

assert!(config.store_config(&NoiseConfigData::default()).is_err());
assert!(!dir.0.exists());
}

#[test]
fn store_config_truncates_existing_file() {
let dir = TempDir::new();
std::fs::create_dir(&dir.0).unwrap();
let config_path = dir.0.join("bitbox.json");
std::fs::write(&config_path, vec![b'x'; 1024]).unwrap();
let config = PersistedNoiseConfig::new(dir.0.to_str().unwrap());
let data = NoiseConfigData::default();

config.store_config(&data).unwrap();

assert_eq!(
std::fs::read_to_string(config_path).unwrap(),
serde_json::to_string(&data).unwrap()
);
}

#[test]
fn read_config_repairs_permissive_permissions() {
let dir = TempDir::new();
std::fs::create_dir(&dir.0).unwrap();
std::fs::write(
dir.0.join("bitbox.json"),
r#"{"app_static_privkey":null,"device_static_pubkeys":[]}"#,
)
.unwrap();
std::fs::set_permissions(&dir.0, std::fs::Permissions::from_mode(0o755)).unwrap();
std::fs::set_permissions(
dir.0.join("bitbox.json"),
std::fs::Permissions::from_mode(0o644),
)
.unwrap();
let config = PersistedNoiseConfig::new(dir.0.to_str().unwrap());

config.read_config().unwrap();

assert_eq!(mode(&dir.0), 0o755);
assert_eq!(mode(&dir.0.join("bitbox.json")), 0o600);
}

#[test]
fn store_config_does_not_change_file_symlink_target_permissions() {
let dir = TempDir::new();
std::fs::create_dir(&dir.0).unwrap();
let target = dir.0.join("target.json");
std::fs::write(&target, "{}").unwrap();
std::fs::set_permissions(&target, std::fs::Permissions::from_mode(0o644)).unwrap();
let config_dir = dir.0.join("config");
std::fs::create_dir(&config_dir).unwrap();
std::os::unix::fs::symlink(&target, config_dir.join("bitbox.json")).unwrap();
let config = PersistedNoiseConfig::new(config_dir.to_str().unwrap());

config.store_config(&NoiseConfigData::default()).unwrap();

assert!(std::fs::symlink_metadata(config_dir.join("bitbox.json"))
.unwrap()
.file_type()
.is_symlink());
assert_eq!(mode(&target), 0o644);
}
}
Loading