2018-08-16 18:35:19 +00:00
|
|
|
use std::str;
|
|
|
|
use std::path::PathBuf;
|
|
|
|
use std::fs::{File, remove_file, create_dir_all, read_link};
|
|
|
|
use std::io::{Write, Read};
|
|
|
|
use std::os::unix::fs::symlink;
|
|
|
|
|
|
|
|
use tempfile;
|
|
|
|
use serde_json;
|
2018-10-24 17:45:11 +00:00
|
|
|
use url;
|
2018-08-16 18:35:19 +00:00
|
|
|
|
2018-10-24 17:45:11 +00:00
|
|
|
use database::{Verify, Delete, Database};
|
2018-08-16 18:35:19 +00:00
|
|
|
use Result;
|
2019-01-04 13:07:14 +00:00
|
|
|
use types::{Email, Fingerprint, KeyID};
|
2018-08-16 18:35:19 +00:00
|
|
|
|
2018-09-19 20:22:59 +00:00
|
|
|
pub struct Filesystem {
|
2018-08-16 18:35:19 +00:00
|
|
|
base: PathBuf,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Filesystem {
|
|
|
|
pub fn new<P: Into<PathBuf>>(base: P) -> Result<Self> {
|
|
|
|
use std::fs;
|
|
|
|
|
|
|
|
let base: PathBuf = base.into();
|
|
|
|
|
|
|
|
if fs::create_dir(&base).is_err() {
|
|
|
|
let meta = fs::metadata(&base);
|
|
|
|
|
|
|
|
match meta {
|
|
|
|
Ok(meta) => {
|
|
|
|
if !meta.file_type().is_dir() {
|
|
|
|
return Err(format!("'{}' exists already and is not a directory",
|
|
|
|
base.display()).into());
|
|
|
|
}
|
|
|
|
|
|
|
|
if meta.permissions().readonly() {
|
|
|
|
return Err(format!("Cannot write '{}'", base.display()).into());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Err(e) => {
|
|
|
|
return Err(format!("Cannot read '{}': {}", base.display(),e).into());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// create directories
|
|
|
|
create_dir_all(base.join("verification_tokens"))?;
|
|
|
|
create_dir_all(base.join("deletion_tokens"))?;
|
|
|
|
create_dir_all(base.join("scratch_pad"))?;
|
2018-11-02 10:48:02 +00:00
|
|
|
create_dir_all(base.join("public").join("by-fpr"))?;
|
|
|
|
create_dir_all(base.join("public").join("by-email"))?;
|
2019-01-04 13:07:14 +00:00
|
|
|
create_dir_all(base.join("public").join("by-kid"))?;
|
2018-08-16 18:35:19 +00:00
|
|
|
|
|
|
|
info!("Opened base dir '{}'", base.display());
|
|
|
|
Ok(Filesystem{
|
|
|
|
base: base,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
fn new_token<'a>(&self, base: &'a str) -> Result<(File, String)> {
|
|
|
|
use rand::{thread_rng, Rng};
|
|
|
|
use rand::distributions::Alphanumeric;
|
|
|
|
|
|
|
|
let mut rng = thread_rng();
|
|
|
|
// samples from [a-zA-Z0-9]
|
|
|
|
// 43 chars ~ 256 bit
|
|
|
|
let name: String = rng.sample_iter(&Alphanumeric).take(43).collect();
|
|
|
|
let dir = self.base.join(base);
|
|
|
|
let fd = File::create(dir.join(name.clone()))?;
|
|
|
|
|
|
|
|
Ok((fd, name))
|
|
|
|
}
|
|
|
|
|
|
|
|
fn pop_token<'a>(&self, base: &'a str, token: &'a str) -> Result<Box<[u8]>> {
|
|
|
|
let path = self.base.join(base).join(token);
|
|
|
|
let buf = {
|
|
|
|
let mut fd = File::open(path.clone())?;
|
|
|
|
let mut buf = Vec::default();
|
|
|
|
|
|
|
|
fd.read_to_end(&mut buf)?;
|
|
|
|
buf.into_boxed_slice()
|
|
|
|
};
|
|
|
|
|
|
|
|
remove_file(path)?;
|
|
|
|
Ok(buf)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Database for Filesystem {
|
2018-09-19 20:22:59 +00:00
|
|
|
fn new_verify_token(&self, payload: Verify) -> Result<String> {
|
2018-08-16 18:35:19 +00:00
|
|
|
let (mut fd, name) = self.new_token("verification_tokens")?;
|
|
|
|
fd.write_all(serde_json::to_string(&payload)?.as_bytes())?;
|
|
|
|
|
|
|
|
Ok(name)
|
|
|
|
}
|
|
|
|
|
2018-09-19 20:22:59 +00:00
|
|
|
fn new_delete_token(&self, payload: Delete) -> Result<String> {
|
2018-08-16 18:35:19 +00:00
|
|
|
let (mut fd, name) = self.new_token("deletion_tokens")?;
|
|
|
|
fd.write_all(serde_json::to_string(&payload)?.as_bytes())?;
|
|
|
|
|
|
|
|
Ok(name)
|
|
|
|
}
|
|
|
|
|
2018-09-19 20:22:59 +00:00
|
|
|
fn compare_and_swap(&self, fpr: &Fingerprint, old: Option<&[u8]>, new: Option<&[u8]>) -> Result<bool> {
|
2018-11-02 10:48:02 +00:00
|
|
|
let target = self.base.join("public").join("by-fpr").join(fpr.to_string());
|
2018-08-16 18:35:19 +00:00
|
|
|
let dir = self.base.join("scratch_pad");
|
|
|
|
|
|
|
|
match new {
|
|
|
|
Some(new) => {
|
|
|
|
let mut tmp = tempfile::Builder::new()
|
|
|
|
.prefix("key")
|
|
|
|
.rand_bytes(16)
|
|
|
|
.tempfile_in(dir)?;
|
|
|
|
tmp.write_all(new)?;
|
|
|
|
|
|
|
|
if target.is_file() {
|
|
|
|
if old.is_some() { remove_file(target.clone())?; }
|
|
|
|
else { return Err(format!("stray file {}", target.display()).into()); }
|
|
|
|
}
|
2019-01-04 13:20:48 +00:00
|
|
|
let _ = tmp.persist(&target)?;
|
|
|
|
|
|
|
|
// fix permissions to 640
|
|
|
|
if cfg!(unix) {
|
|
|
|
use std::os::unix::fs::PermissionsExt;
|
|
|
|
use std::fs::{set_permissions, Permissions};
|
|
|
|
|
|
|
|
let perm = Permissions::from_mode(0o640);
|
|
|
|
set_permissions(target, perm)?;
|
|
|
|
}
|
2018-08-16 18:35:19 +00:00
|
|
|
|
|
|
|
Ok(true)
|
|
|
|
}
|
|
|
|
None => {
|
|
|
|
remove_file(target)?;
|
|
|
|
Ok(true)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-10-24 17:45:11 +00:00
|
|
|
fn link_email(&self, email: &Email, fpr: &Fingerprint) {
|
|
|
|
let email = url::form_urlencoded::byte_serialize(email.to_string().as_bytes()).collect::<String>();
|
2018-11-02 10:48:02 +00:00
|
|
|
let target = self.base.join("public").join("by-fpr").join(fpr.to_string());
|
|
|
|
let link = self.base.join("public").join("by-email").join(email);
|
2018-08-16 18:35:19 +00:00
|
|
|
|
|
|
|
if link.exists() {
|
|
|
|
let _ = remove_file(link.clone());
|
|
|
|
}
|
|
|
|
|
|
|
|
let _ = symlink(target, link);
|
|
|
|
}
|
|
|
|
|
2018-10-24 17:45:11 +00:00
|
|
|
fn unlink_email(&self, email: &Email, fpr: &Fingerprint) {
|
|
|
|
let email = url::form_urlencoded::byte_serialize(email.to_string().as_bytes()).collect::<String>();
|
2018-11-02 10:48:02 +00:00
|
|
|
let link = self.base.join("public").join("by-email").join(email);
|
2018-08-16 18:35:19 +00:00
|
|
|
|
|
|
|
match read_link(link.clone()) {
|
|
|
|
Ok(target) => {
|
2018-11-02 10:48:02 +00:00
|
|
|
let expected = self.base.join("public").join("by-fpr").join(fpr.to_string());
|
2018-08-16 18:35:19 +00:00
|
|
|
|
|
|
|
if target == expected {
|
|
|
|
let _ = remove_file(link);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Err(_) => {}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-01-04 13:07:14 +00:00
|
|
|
fn link_kid(&self, kid: &KeyID, fpr: &Fingerprint) {
|
|
|
|
let target = self.base.join("public").join("by-fpr").join(fpr.to_string());
|
|
|
|
let link = self.base.join("public").join("by-kid").join(kid.to_string());
|
|
|
|
|
|
|
|
if link.exists() {
|
|
|
|
let _ = remove_file(link.clone());
|
|
|
|
}
|
|
|
|
|
|
|
|
let _ = symlink(target, link);
|
|
|
|
}
|
|
|
|
|
|
|
|
fn unlink_kid(&self, kid: &KeyID, fpr: &Fingerprint) {
|
|
|
|
let link = self.base.join("public").join("by-kid").join(kid.to_string());
|
|
|
|
|
|
|
|
match read_link(link.clone()) {
|
|
|
|
Ok(target) => {
|
|
|
|
let expected = self.base.join("public").join("by-fpr").join(fpr.to_string());
|
|
|
|
|
|
|
|
if target == expected {
|
|
|
|
let _ = remove_file(link);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Err(_) => {}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn link_fpr(&self, from: &Fingerprint, fpr: &Fingerprint) {
|
|
|
|
let target = self.base.join("public").join("by-fpr").join(fpr.to_string());
|
|
|
|
let link = self.base.join("public").join("by-fpr").join(from.to_string());
|
|
|
|
|
|
|
|
if link == target { return; }
|
|
|
|
if link.exists() {
|
|
|
|
match link.metadata() {
|
|
|
|
Ok(ref meta) if meta.file_type().is_symlink() => {
|
|
|
|
let _ = remove_file(link.clone());
|
|
|
|
}
|
|
|
|
_ => {}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
let _ = symlink(target, link);
|
|
|
|
}
|
|
|
|
|
|
|
|
fn unlink_fpr(&self, from: &Fingerprint, fpr: &Fingerprint) {
|
|
|
|
let link = self.base.join("public").join("by-fpr").join(from.to_string());
|
|
|
|
|
|
|
|
match read_link(link.clone()) {
|
|
|
|
Ok(target) => {
|
|
|
|
let expected = self.base.join("public").join("by-fpr").join(fpr.to_string());
|
|
|
|
|
|
|
|
if target == expected {
|
|
|
|
let _ = remove_file(link);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Err(_) => {}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-09-19 20:22:59 +00:00
|
|
|
fn pop_verify_token(&self, token: &str) -> Option<Verify> {
|
2018-08-16 18:35:19 +00:00
|
|
|
self.pop_token("verification_tokens", token).ok().and_then(|raw| {
|
|
|
|
str::from_utf8(&raw).ok().map(|s| s.to_string())
|
|
|
|
}).and_then(|s| {
|
|
|
|
let s = serde_json::from_str(&s);
|
|
|
|
s.ok()
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2018-09-19 20:22:59 +00:00
|
|
|
fn pop_delete_token(&self, token: &str) -> Option<Delete> {
|
2018-08-16 18:35:19 +00:00
|
|
|
self.pop_token("deletion_tokens", token).ok().and_then(|raw| {
|
|
|
|
str::from_utf8(&raw).ok().map(|s| s.to_string())
|
|
|
|
}).and_then(|s| {
|
|
|
|
serde_json::from_str(&s).ok()
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
// XXX: slow
|
|
|
|
fn by_fpr(&self, fpr: &Fingerprint) -> Option<Box<[u8]>> {
|
2018-11-02 10:48:02 +00:00
|
|
|
let target = self.base.join("public").join("by-fpr").join(fpr.to_string());
|
2018-08-16 18:35:19 +00:00
|
|
|
|
|
|
|
File::open(target).ok().and_then(|mut fd| {
|
|
|
|
let mut buf = Vec::default();
|
|
|
|
if fd.read_to_end(&mut buf).is_ok() {
|
|
|
|
Some(buf.into_boxed_slice())
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
// XXX: slow
|
2018-10-24 17:45:11 +00:00
|
|
|
fn by_email(&self, email: &Email) -> Option<Box<[u8]>> {
|
2018-09-19 20:23:39 +00:00
|
|
|
use std::fs;
|
2018-08-16 18:35:19 +00:00
|
|
|
|
2018-10-24 17:45:11 +00:00
|
|
|
let email = url::form_urlencoded::byte_serialize(email.to_string().as_bytes()).collect::<String>();
|
2018-11-02 10:48:02 +00:00
|
|
|
let path = self.base.join("public").join("by-email").join(email);
|
2018-09-19 20:23:39 +00:00
|
|
|
|
|
|
|
fs::canonicalize(path).ok()
|
|
|
|
.and_then(|p| {
|
|
|
|
if p.starts_with(&self.base) {
|
|
|
|
Some(p)
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
}
|
|
|
|
}).and_then(|p| {
|
|
|
|
File::open(p).ok()
|
|
|
|
}).and_then(|mut fd| {
|
|
|
|
let mut buf = Vec::default();
|
|
|
|
if fd.read_to_end(&mut buf).is_ok() {
|
|
|
|
Some(buf.into_boxed_slice())
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
}
|
|
|
|
})
|
2018-08-16 18:35:19 +00:00
|
|
|
}
|
2019-01-04 13:07:14 +00:00
|
|
|
|
|
|
|
// XXX: slow
|
|
|
|
fn by_kid(&self, kid: &KeyID) -> Option<Box<[u8]>> {
|
|
|
|
use std::fs;
|
|
|
|
|
|
|
|
let path = self.base.join("public").join("by-kid").join(kid.to_string());
|
|
|
|
|
|
|
|
fs::canonicalize(path).ok()
|
|
|
|
.and_then(|p| {
|
|
|
|
if p.starts_with(&self.base) {
|
|
|
|
Some(p)
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
}
|
|
|
|
}).and_then(|p| {
|
|
|
|
File::open(p).ok()
|
|
|
|
}).and_then(|mut fd| {
|
|
|
|
let mut buf = Vec::default();
|
|
|
|
if fd.read_to_end(&mut buf).is_ok() {
|
|
|
|
Some(buf.into_boxed_slice())
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
2018-08-16 18:35:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
|
|
|
use super::*;
|
|
|
|
use tempfile::TempDir;
|
2018-11-25 14:03:27 +00:00
|
|
|
use sequoia_openpgp::tpk::TPKBuilder;
|
2018-08-16 18:35:19 +00:00
|
|
|
use database::test;
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn init() {
|
|
|
|
let tmpdir = TempDir::new().unwrap();
|
|
|
|
let _ = Filesystem::new(tmpdir.path()).unwrap();
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn new() {
|
|
|
|
let tmpdir = TempDir::new().unwrap();
|
2018-10-25 15:42:02 +00:00
|
|
|
let db = Filesystem::new(tmpdir.path()).unwrap();
|
2018-11-22 15:40:59 +00:00
|
|
|
let k1 = TPKBuilder::default().add_userid("a").generate().unwrap().0;
|
|
|
|
let k2 = TPKBuilder::default().add_userid("b").generate().unwrap().0;
|
|
|
|
let k3 = TPKBuilder::default().add_userid("c").generate().unwrap().0;
|
2018-08-16 18:35:19 +00:00
|
|
|
|
|
|
|
assert!(db.merge_or_publish(k1).unwrap().len() > 0);
|
|
|
|
assert!(db.merge_or_publish(k2.clone()).unwrap().len() > 0);
|
|
|
|
assert!(!db.merge_or_publish(k2).unwrap().len() > 0);
|
|
|
|
assert!(db.merge_or_publish(k3.clone()).unwrap().len() > 0);
|
|
|
|
assert!(!db.merge_or_publish(k3.clone()).unwrap().len() > 0);
|
|
|
|
assert!(!db.merge_or_publish(k3).unwrap().len() > 0);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn uid_verification() {
|
|
|
|
let tmpdir = TempDir::new().unwrap();
|
|
|
|
let mut db = Filesystem::new(tmpdir.path()).unwrap();
|
|
|
|
|
|
|
|
test::test_uid_verification(&mut db);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn uid_deletion() {
|
|
|
|
let tmpdir = TempDir::new().unwrap();
|
|
|
|
let mut db = Filesystem::new(tmpdir.path()).unwrap();
|
|
|
|
|
|
|
|
test::test_uid_deletion(&mut db);
|
|
|
|
}
|
2019-01-04 13:07:14 +00:00
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn subkey_lookup() {
|
|
|
|
let tmpdir = TempDir::new().unwrap();
|
|
|
|
let mut db = Filesystem::new(tmpdir.path()).unwrap();
|
|
|
|
|
|
|
|
test::test_subkey_lookup(&mut db);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn kid_lookup() {
|
|
|
|
let tmpdir = TempDir::new().unwrap();
|
|
|
|
let mut db = Filesystem::new(tmpdir.path()).unwrap();
|
|
|
|
|
|
|
|
test::test_kid_lookup(&mut db);
|
|
|
|
}
|
2019-01-15 15:59:35 +00:00
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn uid_revocation() {
|
|
|
|
let tmpdir = TempDir::new().unwrap();
|
|
|
|
let mut db = Filesystem::new(tmpdir.path()).unwrap();
|
|
|
|
|
|
|
|
test::test_uid_revocation(&mut db);
|
|
|
|
}
|
2018-08-16 18:35:19 +00:00
|
|
|
}
|