use std::path::Path;
pub const PAIRING_DIR_REL: &str = "pairing";
pub fn write(root: &Path, rel: &str, text: &str) -> anyhow::Result<()> {
let kek = kek(root)?;
let sealed = gunnar_core::seal::seal(&kek, rel, text.as_bytes()).map_err(|e| anyhow::anyhow!("sealing {rel}: {e}"))?;
if let Some(parent) = root.join(rel).parent() {
std::fs::create_dir_all(parent)?;
}
crate::durable::write_atomic_unframed(root, rel, &sealed)?;
use std::os::unix::fs::PermissionsExt as _;
std::fs::set_permissions(root.join(rel), std::fs::Permissions::from_mode(0o600))?;
Ok(())
}
fn kek(root: &Path) -> anyhow::Result<gunnar_core::Kek> {
let dir = root.join(PAIRING_DIR_REL);
std::fs::create_dir_all(&dir)?;
gunnar_core::Kek::for_control_dir(&dir).map_err(|e| anyhow::anyhow!("the pairing KEK under {}: {e}", dir.display()))
}
pub fn read(root: &Path, rel: &str) -> Option<String> {
match read_checked(root, rel) {
Ok(t) => t,
Err(e) => {
tracing::error!(file = rel, error = %format!("{e:#}"), "the fed file cannot be read — taken as absent");
None
}
}
}
pub fn read_checked(root: &Path, rel: &str) -> anyhow::Result<Option<String>> {
let path = root.join(rel);
let raw = match std::fs::read(&path) {
Ok(r) => r,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(e) => anyhow::bail!("{}: {e}", path.display()),
};
if !gunnar_core::seal::is_sealed(&raw) {
return String::from_utf8(raw).map(Some).map_err(|e| anyhow::anyhow!("{}: {e}", path.display()));
}
let kek_file = root.join(PAIRING_DIR_REL).join(gunnar_core::seal::KEK_FILE);
let kek = match gunnar_core::Kek::from_env().map_err(|e| anyhow::anyhow!("{e}"))? {
Some(k) => k,
None if kek_file.exists() => gunnar_core::Kek::from_file(&kek_file).map_err(|e| anyhow::anyhow!("{e}"))?,
None => anyhow::bail!("{} is sealed and this box holds no key for it ({} is absent)", path.display(), kek_file.display()),
};
let plain = gunnar_core::seal::unseal(&kek, rel, &raw).map_err(|e| anyhow::anyhow!("{} does not open under this box's key: {e}", path.display()))?;
String::from_utf8(plain).map(Some).map_err(|e| anyhow::anyhow!("{}: {e}", path.display()))
}
#[cfg(test)]
mod tests {
use super::*;
use std::os::unix::fs::PermissionsExt as _;
const REL: &str = crate::pairing::JOINED_REL;
const SECRET: &str = "sk_test_feedAtRestNEVERlive0123456789";
fn text() -> String {
format!("estate = \"e\"\nrole = \"front\"\npeers = []\n\n[me]\nname = \"vm1\"\noverlay = \"10.13.0.1\"\n\n[feed]\n\"secret.stripe_key\" = \"{SECRET}\"\nconsole_admin_token = \"{}\"\n", "a".repeat(64))
}
#[test]
fn the_feed_is_sealed_and_owner_only_on_disk_and_reads_back_whole() {
let d = tempfile::tempdir().unwrap();
std::fs::create_dir_all(d.path().join("pairing")).unwrap();
write(d.path(), REL, &text()).unwrap();
let raw = std::fs::read(d.path().join(REL)).unwrap();
assert!(!raw.windows(SECRET.len()).any(|w| w == SECRET.as_bytes()), "the stripe key is readable in the file on disk");
assert!(!String::from_utf8_lossy(&raw).contains("console_admin_token"), "the feed's keys are readable on disk");
assert!(gunnar_core::seal::is_sealed(&raw), "the file is a sealed value");
let mode = std::fs::metadata(d.path().join(REL)).unwrap().permissions().mode() & 0o777;
assert_eq!(mode, 0o600, "the feed is the owner's alone");
assert_eq!(read(d.path(), REL).as_deref(), Some(text().as_str()), "and it reads back byte for byte");
}
#[test]
fn a_plaintext_feed_from_before_is_still_read_and_sealed_on_the_next_write() {
let d = tempfile::tempdir().unwrap();
std::fs::create_dir_all(d.path().join("pairing")).unwrap();
std::fs::write(d.path().join(REL), text()).unwrap();
assert_eq!(read(d.path(), REL).as_deref(), Some(text().as_str()));
write(d.path(), REL, &text()).unwrap();
assert!(gunnar_core::seal::is_sealed(&std::fs::read(d.path().join(REL)).unwrap()));
}
#[test]
fn a_sealed_feed_is_bound_to_its_file_and_its_key() {
let d = tempfile::tempdir().unwrap();
std::fs::create_dir_all(d.path().join("pairing")).unwrap();
write(d.path(), REL, &text()).unwrap();
std::fs::copy(d.path().join(REL), d.path().join("pairing/other.toml")).unwrap();
assert_eq!(read(d.path(), "pairing/other.toml"), None);
let e = tempfile::tempdir().unwrap();
std::fs::create_dir_all(e.path().join("pairing")).unwrap();
std::fs::copy(d.path().join(REL), e.path().join(REL)).unwrap();
assert_eq!(read(e.path(), REL), None, "a copy of the file without its box's key reads as nothing");
}
}