use std::io::Write;
use std::path::Path;
use crate::exec::ExecError;
#[derive(Debug)]
pub struct SecretFile {
file: tempfile::NamedTempFile,
}
impl SecretFile {
pub fn new(secret: &str) -> Result<Self, ExecError> {
let mut builder = tempfile::Builder::new();
builder.prefix("tailscale-mcp-").suffix(".key");
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt as _;
let _ = std::fs::Permissions::from_mode(0o600);
builder.permissions(std::fs::Permissions::from_mode(0o600));
}
let mut file = builder.tempfile().map_err(ExecError::SecretFile)?;
file.write_all(secret.as_bytes())
.and_then(|()| file.flush())
.map_err(ExecError::SecretFile)?;
Ok(Self { file })
}
pub fn path(&self) -> &Path {
self.file.path()
}
pub fn arg(&self) -> String {
format!("file:{}", self.file.path().display())
}
}
#[derive(Debug)]
pub struct PrivateFile {
#[expect(dead_code, reason = "kept alive so that dropping it cleans up")]
dir: tempfile::TempDir,
path: std::path::PathBuf,
}
impl PrivateFile {
pub fn reserved(name: &str) -> Result<Self, ExecError> {
let mut builder = tempfile::Builder::new();
builder.prefix("tailscale-mcp-");
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt as _;
builder.permissions(std::fs::Permissions::from_mode(0o700));
}
let dir = builder.tempdir().map_err(ExecError::SecretFile)?;
let path = dir.path().join(name);
Ok(Self { dir, path })
}
pub fn written(name: &str, contents: &[u8]) -> Result<Self, ExecError> {
let file = Self::reserved(name)?;
std::fs::write(&file.path, contents).map_err(ExecError::SecretFile)?;
Ok(file)
}
pub fn path(&self) -> &Path {
&self.path
}
pub fn arg(&self) -> String {
self.path.display().to_string()
}
pub fn read(&self) -> Result<Vec<u8>, ExecError> {
std::fs::read(&self.path).map_err(ExecError::SecretFile)
}
#[cfg(test)]
fn directory(&self) -> &Path {
self.path.parent().unwrap_or(&self.path)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_reserved_name_is_free_for_the_client_to_create() {
let file = PrivateFile::reserved("serve-config.json").expect("a private directory");
assert!(
!file.path().exists(),
"the client refuses a path that is already taken"
);
std::fs::write(file.path(), b"{}").expect("the client can create it");
assert_eq!(file.read().expect("readable"), b"{}");
}
#[cfg(unix)]
#[test]
fn nobody_else_can_enter_the_directory_the_file_lives_in() {
use std::os::unix::fs::PermissionsExt as _;
let file = PrivateFile::written("serve-config.json", b"{}").expect("a private directory");
let mode = std::fs::metadata(file.directory())
.expect("metadata")
.permissions()
.mode();
assert_eq!(mode & 0o077, 0, "group and other must have no access");
}
#[test]
fn the_file_and_its_directory_go_away_with_the_call() {
let (path, dir) = {
let file = PrivateFile::written("serve-config.json", b"{}").expect("a private file");
(file.path().to_path_buf(), file.directory().to_path_buf())
};
assert!(!path.exists(), "{} outlived its guard", path.display());
assert!(!dir.exists(), "{} outlived its guard", dir.display());
}
#[test]
fn the_secret_reaches_the_cli_by_reference_not_by_value() {
let secret = "tskey-auth-example-notreal";
let file = SecretFile::new(secret).expect("a temporary file");
let arg = file.arg();
assert!(arg.starts_with("file:"));
assert!(
!arg.contains(secret),
"the argument must not carry the secret: {arg}"
);
assert_eq!(
std::fs::read_to_string(file.path()).expect("readable"),
secret
);
}
#[cfg(unix)]
#[test]
fn the_file_is_readable_only_by_this_user() {
use std::os::unix::fs::PermissionsExt as _;
let file = SecretFile::new("tskey-auth-example-notreal").expect("a temporary file");
let mode = std::fs::metadata(file.path())
.expect("metadata")
.permissions()
.mode();
assert_eq!(mode & 0o077, 0, "group and other must have no access");
}
#[test]
fn the_file_goes_away_with_the_call() {
let path = {
let file = SecretFile::new("tskey-auth-example-notreal").expect("a temporary file");
file.path().to_path_buf()
};
assert!(!path.exists(), "{} outlived its guard", path.display());
}
}