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"
);
}
}