cat / goq
clone

turns a linux box into a private steam streaming appliance: plug in, tap your key, play from another computer

fix/hero-release-note
goq / crates / sigil-host / src / identity.rs
identity.rs 6.5K
1use std::fs::{self, OpenOptions};2use std::io::{Read, Write};3use std::path::{Path, PathBuf};45use anyhow::{Context, Result, bail, ensure};6use iroh::SecretKey;78#[cfg(unix)]9use std::os::unix::fs::{DirBuilderExt, MetadataExt, OpenOptionsExt, PermissionsExt};1011const SECRET_KEY_LEN: usize = 32;1213pub fn init(path: &Path) -> Result<SecretKey> {14 ensure_parent(path)?;15 reject_symlink(path)?;1617 let secret = SecretKey::generate();18 let mut options = OpenOptions::new();19 options.write(true).create_new(true);20 #[cfg(unix)]21 options.mode(0o600);2223 let mut file = options24 .open(path)25 .with_context(|| format!("refusing to overwrite identity {}", path.display()))?;26 file.write_all(&secret.to_bytes())27 .with_context(|| format!("writing identity {}", path.display()))?;28 file.sync_all()29 .with_context(|| format!("syncing identity {}", path.display()))?;3031 #[cfg(unix)]32 fs::set_permissions(path, fs::Permissions::from_mode(0o600))33 .with_context(|| format!("setting identity permissions on {}", path.display()))?;3435 validate_metadata(path)?;36 Ok(secret)37}3839pub fn load(path: &Path) -> Result<SecretKey> {40 let mut options = OpenOptions::new();41 options.read(true);42 #[cfg(unix)]43 options.custom_flags(libc::O_NOFOLLOW | libc::O_CLOEXEC);44 let mut file = options.open(path).with_context(|| {45 format!(46 "opening identity {} without following symlinks",47 path.display()48 )49 })?;50 let metadata = file51 .metadata()52 .with_context(|| format!("inspecting opened identity {}", path.display()))?;53 validate_open_metadata(path, &metadata)?;54 let mut bytes = [0_u8; SECRET_KEY_LEN];55 file.read_exact(&mut bytes)56 .with_context(|| format!("reading identity {}", path.display()))?;57 let mut extra = [0_u8; 1];58 ensure!(59 file.read(&mut extra)? == 0,60 "identity {} must contain exactly {SECRET_KEY_LEN} bytes",61 path.display()62 );63 Ok(SecretKey::from_bytes(&bytes))64}6566fn ensure_parent(path: &Path) -> Result<()> {67 let parent = path68 .parent()69 .filter(|parent| !parent.as_os_str().is_empty())70 .unwrap_or_else(|| Path::new("."));7172 if !parent.exists() {73 let mut builder = fs::DirBuilder::new();74 builder.recursive(true);75 #[cfg(unix)]76 builder.mode(0o700);77 builder78 .create(parent)79 .with_context(|| format!("creating identity directory {}", parent.display()))?;80 }8182 let metadata = fs::symlink_metadata(parent)83 .with_context(|| format!("inspecting identity directory {}", parent.display()))?;84 ensure!(metadata.is_dir(), "identity parent is not a directory");85 ensure!(86 !metadata.file_type().is_symlink(),87 "identity parent is a symlink"88 );8990 #[cfg(unix)]91 {92 ensure!(93 metadata.mode() & 0o077 == 0,94 "identity directory {} must not be accessible by group or other users",95 parent.display()96 );97 ensure!(98 metadata.uid() == unsafe { libc::geteuid() },99 "identity directory {} is not owned by the current user",100 parent.display()101 );102 }103104 Ok(())105}106107fn reject_symlink(path: &Path) -> Result<()> {108 match fs::symlink_metadata(path) {109 Ok(metadata) if metadata.file_type().is_symlink() => {110 bail!("identity {} must not be a symlink", path.display())111 }112 Ok(_) => Ok(()),113 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),114 Err(error) => Err(error).with_context(|| format!("inspecting identity {}", path.display())),115 }116}117118fn validate_metadata(path: &Path) -> Result<()> {119 let metadata = fs::symlink_metadata(path)120 .with_context(|| format!("inspecting identity {}", path.display()))?;121 validate_open_metadata(path, &metadata)122}123124fn validate_open_metadata(path: &Path, metadata: &fs::Metadata) -> Result<()> {125 ensure!(metadata.is_file(), "identity must be a regular file");126 ensure!(127 metadata.len() == SECRET_KEY_LEN as u64,128 "identity {} must be exactly {SECRET_KEY_LEN} bytes",129 path.display()130 );131132 #[cfg(unix)]133 {134 ensure!(135 metadata.mode() & 0o077 == 0,136 "identity {} must not be readable or writable by group or other users",137 path.display()138 );139 ensure!(140 metadata.uid() == unsafe { libc::geteuid() },141 "identity {} is not owned by the current user",142 path.display()143 );144 }145146 Ok(())147}148149pub fn display_path(path: &Path) -> PathBuf {150 path.canonicalize().unwrap_or_else(|_| path.to_path_buf())151}152153#[cfg(test)]154mod tests {155 use super::*;156157 #[test]158 fn creates_and_round_trips_identity() {159 let temp = tempfile::tempdir().unwrap();160 let directory = temp.path().join("private");161 #[cfg(unix)]162 fs::create_dir(&directory).unwrap();163 #[cfg(unix)]164 fs::set_permissions(&directory, fs::Permissions::from_mode(0o700)).unwrap();165 #[cfg(not(unix))]166 fs::create_dir(&directory).unwrap();167 let path = directory.join("host.key");168169 let created = init(&path).unwrap();170 let loaded = load(&path).unwrap();171 assert_eq!(created.public(), loaded.public());172173 #[cfg(unix)]174 assert_eq!(fs::metadata(&path).unwrap().mode() & 0o777, 0o600);175 }176177 #[test]178 fn refuses_to_overwrite_identity() {179 let temp = tempfile::tempdir().unwrap();180 #[cfg(unix)]181 fs::set_permissions(temp.path(), fs::Permissions::from_mode(0o700)).unwrap();182 let path = temp.path().join("host.key");183 init(&path).unwrap();184 assert!(init(&path).is_err());185 }186187 #[cfg(unix)]188 #[test]189 fn rejects_overly_permissive_identity() {190 let temp = tempfile::tempdir().unwrap();191 fs::set_permissions(temp.path(), fs::Permissions::from_mode(0o700)).unwrap();192 let path = temp.path().join("host.key");193 init(&path).unwrap();194 fs::set_permissions(&path, fs::Permissions::from_mode(0o644)).unwrap();195 assert!(load(&path).is_err());196 }197198 #[cfg(unix)]199 #[test]200 fn rejects_symlink_identity() {201 use std::os::unix::fs::symlink;202203 let temp = tempfile::tempdir().unwrap();204 fs::set_permissions(temp.path(), fs::Permissions::from_mode(0o700)).unwrap();205 let target = temp.path().join("target.key");206 init(&target).unwrap();207 let link = temp.path().join("link.key");208 symlink(&target, &link).unwrap();209 assert!(load(&link).is_err());210 }211}