use std::collections::{BTreeMap, HashMap, HashSet};
use std::path::{Path, PathBuf};
use crate::consts;
use crate::crypto::CryptoPolicy;
use crate::error::{Error, Result};
use crate::pbkdf::PBKDFCache;
mod parse;
mod transform;
mod write;
mod blob;
pub use blob::{blob_to_tree, tree_to_blob};
pub use parse::parse;
pub use transform::transform;
pub use write::tree_write;
pub trait TransformContext {
fn max_depth(&self) -> usize;
fn transforms(&self) -> &Transforms;
fn transforms_mut(&mut self) -> &mut Transforms;
fn passwords(&self) -> &HashMap<String, String>;
fn crypto(&mut self) -> &mut CryptoConfig;
fn crypto_ref(&self) -> &CryptoConfig;
fn separators(&self) -> &Separators;
fn io(&self) -> &IoConfig;
fn io_mut(&mut self) -> &mut IoConfig;
fn runtime_mut(&mut self) -> &mut RuntimeState;
fn runtime(&self) -> &RuntimeState;
fn anchor(&self) -> &AnchorConfig;
fn anchor_mut(&mut self) -> &mut AnchorConfig;
fn casdir(&self) -> &Path {
&self.io().casdir
}
}
pub struct PBKDFOptions {
pub alg: String,
pub saltlen: usize,
pub salt: Option<Vec<u8>>,
pub msec: Option<u32>,
pub params: Option<BTreeMap<String, usize>>,
}
impl PBKDFOptions {
pub fn new(policy: &dyn CryptoPolicy) -> PBKDFOptions {
PBKDFOptions {
alg: policy.default_pbkdf_alg(),
saltlen: policy.default_pbkdf_salt_length(),
salt: None,
msec: Some(policy.default_pbkdf_millis()),
params: None,
}
}
}
pub struct CipherOptions {
pub alg: String,
pub iv: Option<Vec<u8>>,
}
impl CipherOptions {
pub fn new(policy: &dyn CryptoPolicy) -> CipherOptions {
CipherOptions {
alg: policy.default_cipher_alg(),
iv: None,
}
}
}
pub struct Separators {
pub left: String,
pub right: String,
}
pub struct Transforms {
pub store: HashSet<String>,
pub fetch: HashSet<String>,
pub encrypt: HashSet<String>,
pub decrypt: HashSet<String>,
}
pub struct CryptoConfig {
pub policy: Box<dyn CryptoPolicy>,
pub pbkdfopts: PBKDFOptions,
pub cipheropts: CipherOptions,
pub rng: Option<botan::RandomNumberGenerator>,
pub pbkdf_cache: Option<PBKDFCache>,
pub recipient_pubs: Vec<String>,
pub recipient_privkeys: HashMap<String, String>,
}
pub struct RuntimeState {
pub level: usize,
pub fname: String,
}
#[derive(Clone, Debug, Default)]
pub struct AnchorConfig {
pub enabled: bool,
pub operation: String,
pub words: Vec<String>,
pub signer_priv_pem: Option<String>,
}
impl AnchorConfig {
pub fn disabled() -> Self {
AnchorConfig::default()
}
}
pub struct IoConfig {
pub casdir: PathBuf,
pub verbose: bool,
pub inline_data: bool,
}
pub struct ParseOps {
pub max_depth: usize,
pub separators: Separators,
pub transforms: Transforms,
pub passwords: HashMap<String, String>,
pub crypto: CryptoConfig,
pub runtime: RuntimeState,
pub io: IoConfig,
pub anchor: AnchorConfig,
}
impl Drop for ParseOps {
fn drop(&mut self) {
use zeroize::Zeroize;
for (_, pw) in self.passwords.iter_mut() {
pw.zeroize();
}
for (_, pk) in self.crypto.recipient_privkeys.iter_mut() {
pk.zeroize();
}
if let Some(ref mut pem) = self.anchor.signer_priv_pem {
pem.zeroize();
}
}
}
impl ParseOps {
pub fn new(policy: Box<dyn CryptoPolicy>) -> Result<ParseOps> {
let rng = botan::RandomNumberGenerator::new().map_err(Error::botan)?;
let pbkdfopts = PBKDFOptions::new(&*policy);
let cipheropts = CipherOptions::new(&*policy);
Ok(ParseOps {
max_depth: consts::DEFAULT_MAX_DEPTH,
separators: Separators {
left: consts::DEFAULT_LEFT_SEP.to_string(),
right: consts::DEFAULT_RIGHT_SEP.to_string(),
},
transforms: Transforms {
store: HashSet::new(),
fetch: HashSet::new(),
encrypt: HashSet::new(),
decrypt: HashSet::new(),
},
passwords: HashMap::new(),
crypto: CryptoConfig {
policy,
pbkdfopts,
cipheropts,
rng: Some(rng),
pbkdf_cache: Some(Vec::new()),
recipient_pubs: Vec::new(),
recipient_privkeys: HashMap::new(),
},
runtime: RuntimeState {
level: 0,
fname: String::new(),
},
io: IoConfig {
casdir: Path::new("").to_path_buf(),
verbose: false,
inline_data: false,
},
anchor: AnchorConfig::disabled(),
})
}
}
impl TransformContext for ParseOps {
fn max_depth(&self) -> usize {
self.max_depth
}
fn transforms(&self) -> &Transforms {
&self.transforms
}
fn transforms_mut(&mut self) -> &mut Transforms {
&mut self.transforms
}
fn passwords(&self) -> &HashMap<String, String> {
&self.passwords
}
fn crypto(&mut self) -> &mut CryptoConfig {
&mut self.crypto
}
fn crypto_ref(&self) -> &CryptoConfig {
&self.crypto
}
fn separators(&self) -> &Separators {
&self.separators
}
fn io(&self) -> &IoConfig {
&self.io
}
fn io_mut(&mut self) -> &mut IoConfig {
&mut self.io
}
fn runtime_mut(&mut self) -> &mut RuntimeState {
&mut self.runtime
}
fn runtime(&self) -> &RuntimeState {
&self.runtime
}
fn anchor(&self) -> &AnchorConfig {
&self.anchor
}
fn anchor_mut(&mut self) -> &mut AnchorConfig {
&mut self.anchor
}
}
pub type TextTree = Vec<TextNode>;
#[derive(Clone, PartialEq, Eq, Debug)]
pub enum TextNode {
Plain(String),
Data(Vec<u8>),
Stored {
keyw: String,
cas: String,
},
Encrypted {
keyw: String,
txt: TextTree,
extfields: BTreeMap<String, String>,
},
BeginEnd {
keyw: String,
txt: TextTree,
},
Chain {
extfields: BTreeMap<String, String>,
},
Include {
hash: String,
},
Conflict {
keyw: String,
ours: TextTree,
theirs: TextTree,
},
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub enum Directive {
Begin,
End,
Data,
Stored,
Encrypted,
Chain,
Conflict,
Ours,
Theirs,
Include,
}
impl Directive {
pub fn keyword(self) -> &'static str {
match self {
Directive::Begin => "BEGIN",
Directive::End => "END",
Directive::Data => "DATA",
Directive::Stored => "STORED",
Directive::Encrypted => "ENCRYPTED",
Directive::Chain => "CHAIN",
Directive::Conflict => "CONFLICT",
Directive::Ours => "OURS",
Directive::Theirs => "THEIRS",
Directive::Include => "INCLUDE",
}
}
pub fn from_keyword(kw: &str) -> Option<Self> {
match kw {
"BEGIN" => Some(Directive::Begin),
"END" => Some(Directive::End),
"DATA" => Some(Directive::Data),
"STORED" => Some(Directive::Stored),
"ENCRYPTED" => Some(Directive::Encrypted),
"CHAIN" => Some(Directive::Chain),
"CONFLICT" => Some(Directive::Conflict),
"OURS" => Some(Directive::Ours),
"THEIRS" => Some(Directive::Theirs),
"INCLUDE" => Some(Directive::Include),
_ => None,
}
}
}
pub(crate) type Command = Directive;
pub(crate) fn parse_error(
paops: &ParseOps,
lineno: i32,
line: &str,
msg: impl Into<String>,
) -> Error {
Error::Parse {
file: paops.runtime.fname.clone(),
lineno,
msg: msg.into() + "\n" + line,
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs::File;
use std::io::BufReader;
use tempfile::tempdir;
fn parse_ept(ept_file: &str) -> (TextTree, ParseOps, tempfile::TempDir) {
let casdir = tempdir().unwrap();
let mut paops = ParseOps::new(crate::crypto::default_policy()).unwrap();
paops.runtime.fname = ept_file.to_string();
paops.io.casdir = casdir.path().to_path_buf();
let tree = parse(BufReader::new(File::open(ept_file).unwrap()), &mut paops).unwrap();
(tree, paops, casdir)
}
#[test]
fn transform_test_ept_unchanged() {
let (intree, mut paops, _casdir) = parse_ept("sample/test.ept");
let outtree = transform(&intree, &mut paops).unwrap();
assert_eq!(intree, outtree);
}
#[test]
fn transform_test_ept_store_unchanged() {
let (intree, mut paops, _casdir) = parse_ept("sample/test.ept");
paops.transforms.store.insert("noexist".to_string());
let outtree = transform(&intree, &mut paops).unwrap();
assert_eq!(intree, outtree);
}
#[test]
fn transform_test_ept_store_agent007() {
let (intree, mut paops, _casdir) = parse_ept("sample/test.ept");
paops.transforms.store.insert("Agent_007".to_string());
let outtree = transform(&intree, &mut paops).unwrap();
let blob = tree_to_blob(&outtree, &mut paops).unwrap();
parse(BufReader::new(&blob[..]), &mut paops).unwrap();
}
#[test]
fn transform_test_ept_fetch_agent007() {
let (intree, mut paops, _casdir) = parse_ept("sample/test.ept");
paops.transforms.fetch.insert("Agent_007".to_string());
let _outtree = transform(&intree, &mut paops).unwrap();
}
#[test]
fn transform_test_ept_encrypt_agent007() {
let (intree, mut paops, _casdir) = parse_ept("sample/test.ept");
paops.transforms.encrypt.insert("Agent_007".to_string());
paops
.passwords
.insert("Agent_007".to_string(), "bond".to_string());
let outtree = transform(&intree, &mut paops).unwrap();
let blob = tree_to_blob(&outtree, &mut paops).unwrap();
parse(BufReader::new(&blob[..]), &mut paops).unwrap();
}
#[test]
fn command_enum_recognizes_all_keywords() {
assert_eq!(Command::from_keyword("BEGIN"), Some(Command::Begin));
assert_eq!(Command::from_keyword("END"), Some(Command::End));
assert_eq!(Command::from_keyword("DATA"), Some(Command::Data));
assert_eq!(Command::from_keyword("STORED"), Some(Command::Stored));
assert_eq!(Command::from_keyword("ENCRYPTED"), Some(Command::Encrypted));
assert_eq!(Command::from_keyword("garbage"), None);
}
#[test]
fn directive_round_trips_through_keyword() {
for d in [
Directive::Begin,
Directive::End,
Directive::Data,
Directive::Stored,
Directive::Encrypted,
Directive::Chain,
Directive::Conflict,
Directive::Include,
] {
let kw = d.keyword();
assert_eq!(Directive::from_keyword(kw), Some(d));
}
}
#[test]
fn directive_keywords_are_uppercase() {
for d in [
Directive::Begin,
Directive::End,
Directive::Data,
Directive::Stored,
Directive::Encrypted,
Directive::Chain,
Directive::Conflict,
Directive::Include,
] {
let kw = d.keyword();
assert!(
kw.chars().all(|c| c.is_ascii_uppercase()),
"keyword '{}' must be uppercase ASCII",
kw
);
}
}
#[test]
fn empty_command_line_is_skipped() {
let mut paops = ParseOps::new(crate::crypto::default_policy()).unwrap();
paops.runtime.fname = "<test>".into();
let input = "// <( )>\n";
let tree = parse(BufReader::new(input.as_bytes()), &mut paops).unwrap();
assert!(tree.is_empty());
}
}