use std::{collections::HashMap, str::FromStr};
use anyhow::{self, Result};
use base64::Engine;
use thiserror::Error;
mod sha256;
#[derive(Error, Debug, PartialEq)]
pub enum RuneError {
#[error("unknown: `{0}`")]
Unknown(String),
#[error("invalid condition: {0}")]
InvalidCondition(String),
#[error("invalid field: {0}")]
InvalidField(String),
#[error("{0}")]
ValueError(String),
#[error("{0} does not contain any operator")]
NoOperator(String),
#[error("unauthorized")]
Unauthorized,
#[error("fewer restrictions than master")]
LessRestrictions,
#[error("does not match master rune")]
DoesNotMatch,
#[error("conversion error {0}")]
Conversion(String),
}
#[derive(Debug, PartialEq, Clone)]
pub enum Condition {
Missing,
Equal,
NotEqual,
BeginsWith,
EndsWith,
Contains,
IntLT,
IntGT,
LexLT,
LexGT,
Comment,
}
impl Condition {
fn symbol(&self) -> &str {
match self {
Condition::Missing => "!",
Condition::Equal => "=",
Condition::NotEqual => "/",
Condition::BeginsWith => "^",
Condition::EndsWith => "$",
Condition::Contains => "~",
Condition::IntLT => "<",
Condition::IntGT => ">",
Condition::LexLT => "{",
Condition::LexGT => "}",
Condition::Comment => "#",
}
}
}
impl TryFrom<char> for Condition {
type Error = RuneError;
fn try_from(value: char) -> Result<Self, Self::Error> {
let cond = match value {
'!' => Condition::Missing,
'=' => Condition::Equal,
'/' => Condition::NotEqual,
'^' => Condition::BeginsWith,
'$' => Condition::EndsWith,
'~' => Condition::Contains,
'<' => Condition::IntLT,
'>' => Condition::IntGT,
'{' => Condition::LexLT,
'}' => Condition::LexGT,
'#' => Condition::Comment,
_ => return Err(RuneError::InvalidCondition(value.to_string())),
};
Ok(cond)
}
}
impl TryFrom<&str> for Condition {
type Error = RuneError;
fn try_from(value: &str) -> std::result::Result<Self, Self::Error> {
if value.len() > 1 {
return Err(RuneError::InvalidCondition(
"condition from multiple chars".to_string(),
));
}
match value.chars().next() {
Some(c) => c.try_into(),
None => Err(RuneError::InvalidCondition(
"condition from empty &str".to_string(),
)),
}
}
}
impl std::fmt::Display for Condition {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let symbol = self.symbol();
write!(f, "{}", symbol)
}
}
fn why(cond: bool, field: &str, xpl: String) -> Result<(), RuneError> {
if !cond {
return Err(RuneError::ValueError(format!("{}: {}", field, xpl)));
}
Ok(())
}
pub trait Check {
fn check_alternative(&self, alt: &Alternative) -> Result<(), RuneError>;
}
pub struct ConditionChecker {
pub value: String,
}
impl Check for ConditionChecker {
fn check_alternative(&self, alt: &Alternative) -> Result<(), RuneError> {
match alt.cond {
Condition::Missing => why(self.value.is_empty(), &alt.field, "is present".to_string()),
Condition::Equal => why(
self.value == alt.value,
&alt.field,
format!("!= {}", alt.value),
),
Condition::NotEqual => why(
self.value != alt.value,
&alt.field,
format!("= {}", alt.value),
),
Condition::BeginsWith => why(
self.value.starts_with(&alt.value),
&alt.field,
format!("does not start with {}", alt.value),
),
Condition::EndsWith => why(
self.value.ends_with(&alt.value),
&alt.field,
format!("does not end with {}", alt.value),
),
Condition::Contains => why(
self.value.contains(&alt.value),
&alt.field,
format!("does not contain {}", alt.value),
),
Condition::IntLT => {
let actual_int = match self.value.parse::<i64>() {
Ok(n) => n,
Err(_) => return why(false, &alt.field, "not an integer field".to_string()),
};
let restriction_int = match alt.value.parse::<i64>() {
Ok(n) => n,
Err(_) => return why(false, &alt.field, "not a valid integer".to_string()),
};
why(
actual_int < restriction_int,
&alt.field,
format!(">= {}", restriction_int),
)
}
Condition::IntGT => {
let actual_int = match self.value.parse::<i64>() {
Ok(n) => n,
Err(_) => return why(false, &alt.field, "not an integer field".to_string()),
};
let restriction_int = match alt.value.parse::<i64>() {
Ok(n) => n,
Err(_) => return why(false, &alt.field, "not a valid integer".to_string()),
};
why(
actual_int > restriction_int,
&alt.field,
format!("<= {}", restriction_int),
)
}
Condition::LexLT => why(
self.value.cmp(&alt.value).is_lt(),
&alt.field,
format!("is the same or ordered after {}", alt.value),
),
Condition::LexGT => why(
self.value.cmp(&alt.value).is_gt(),
&alt.field,
format!("is the same or ordered before {}", alt.value),
),
Condition::Comment => Ok(()),
}
}
}
#[derive(Clone)]
pub struct MapChecker {
pub map: HashMap<String, String>,
}
impl Check for MapChecker {
fn check_alternative(&self, alt: &Alternative) -> Result<(), RuneError> {
if !self.map.contains_key(&alt.field) {
if alt.is_unique_id() {
return why(
!alt.value.contains('-'),
&alt.field,
format!("unknown version {}", alt.value),
);
}
return why(
alt.cond == Condition::Missing,
&alt.field,
"is missing".to_string(),
);
}
let checker = ConditionChecker {
value: self.map.get(&alt.field).unwrap().clone(),
};
checker.check_alternative(alt)
}
}
#[derive(Debug, Clone)]
pub struct Alternative {
field: String,
cond: Condition,
value: String,
}
impl Alternative {
pub fn new<S: Into<String>>(
field: S,
cond: Condition,
value: S,
allow_idfield: bool,
) -> Result<Self, RuneError> {
let field = field.into();
let value = value.into();
if contains_punctuation(&field) {
return Err(RuneError::InvalidField(format!(
"{}: has punctuation",
field
)));
}
if field.is_empty() {
if !allow_idfield {
return Err(RuneError::InvalidField(
"unique_id field not valid".to_string(),
));
}
if cond != Condition::Equal {
return Err(RuneError::InvalidCondition(format!(
"'{}': unique_id condition must be '='",
cond
)));
}
}
Ok(Alternative { field, cond, value })
}
pub fn get_field(&self) -> String {
self.field.clone()
}
pub fn get_condition(&self) -> Condition {
self.cond.clone()
}
pub fn get_value(&self) -> String {
self.value.clone()
}
fn is_unique_id(&self) -> bool {
self.field.is_empty()
}
pub fn test<T: Check>(&self, checker: T) -> Result<(), RuneError> {
if self.cond == Condition::Comment {
return Ok(());
}
checker.check_alternative(self)
}
pub fn encode(&self) -> String {
format!(
"{}{}{}",
self.field,
self.cond,
self.value
.replace('\\', "\\\\")
.replace('|', "\\|")
.replace('&', "\\&")
)
}
pub fn decode(data: &str, allow_idfield: bool) -> Result<(Self, &str), RuneError> {
let mut field = String::new();
let mut cond: Option<Condition> = None;
let mut value = String::new();
let mut chars = data.chars();
for c in chars.by_ref() {
if is_separator(c) {
cond = c.try_into().ok();
break;
}
field.push(c);
}
if cond.is_none() {
return Err(RuneError::NoOperator(field));
}
let mut extra_chars = 0;
while let Some(c) = chars.next() {
match c {
'|' => break,
'&' => {
extra_chars += 1;
break;
}
'\\' => {
if let Some(c) = chars.next() {
value.push(c);
} else {
break;
}
}
_ => value.push(c),
}
}
let alt = Alternative::new(field, cond.unwrap(), value, allow_idfield)?;
let rest = &data[(data.len() - (chars.count() + extra_chars))..];
Ok((alt, rest))
}
}
impl std::fmt::Display for Alternative {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.encode())
}
}
impl std::cmp::PartialEq for Alternative {
fn eq(&self, other: &Self) -> bool {
self.field == other.field && self.cond == other.cond && self.value == other.value
}
}
impl std::cmp::Eq for Alternative {}
#[derive(Debug, Clone)]
pub struct Restriction {
pub alternatives: Vec<Alternative>,
}
impl Restriction {
pub fn new(alternatives: Vec<Alternative>) -> Result<Self, RuneError> {
if alternatives.is_empty() {
return Err(RuneError::Unknown(
"restriction must have some alternatives".to_string(),
));
}
if alternatives.len() > 1 && alternatives[0].is_unique_id() {
return Err(RuneError::Unknown(
"unique_id field cannot have alternatives".to_string(),
));
}
Ok(Restriction { alternatives })
}
pub fn add_alternative(mut self, alt: Alternative) -> Self {
self.alternatives.push(alt);
self
}
pub fn unique_id(unique_id: String, version: Option<String>) -> Result<Self, RuneError> {
if unique_id.contains('-') {
return Err(RuneError::ValueError(
"hyphen not allowed in id".to_string(),
));
}
let mut id_str: String = unique_id;
if let Some(v) = version {
id_str = format!("{}-{}", id_str, v);
}
let alt = Alternative::new("".to_string(), Condition::Equal, id_str, true)?;
Ok(Restriction {
alternatives: vec![alt],
})
}
pub fn test<T: Check + Clone>(&self, checker: T) -> Result<(), RuneError> {
let (oks, errs): (Vec<_>, Vec<_>) = self
.alternatives
.clone()
.into_iter()
.map(|alt| alt.test(checker.clone()))
.partition(Result::is_ok);
if !oks.is_empty() {
return Ok(());
}
let msg = errs
.into_iter()
.map(|e| e.unwrap_err().to_string())
.collect::<Vec<String>>()
.join(" AND ");
Err(RuneError::ValueError(msg))
}
pub fn encode(&self) -> String {
self.alternatives
.iter()
.map(|alt| alt.encode())
.collect::<Vec<String>>()
.join("|")
}
pub fn decode(data: &str, allow_idfield: bool) -> Result<(Self, &str), RuneError> {
let mut local_data = data;
let mut alts = vec![];
while !local_data.is_empty() {
if local_data.as_bytes()[0] as char == '&' {
local_data = &local_data[1..];
break;
}
let (alt, rest) = Alternative::decode(local_data, allow_idfield)?;
alts.push(alt);
local_data = rest;
}
let res = Restriction::new(alts)?;
Ok((res, local_data))
}
}
impl std::cmp::PartialEq for Restriction {
fn eq(&self, other: &Self) -> bool {
if self.alternatives.len() != other.alternatives.len() {
return false;
}
if self
.alternatives
.iter()
.zip(other.alternatives.iter())
.any(|(mr, or)| mr.ne(or))
{
return false;
};
true
}
}
impl std::cmp::Eq for Restriction {}
impl TryFrom<&str> for Restriction {
type Error = RuneError;
fn try_from(value: &str) -> std::prelude::v1::Result<Self, Self::Error> {
let (res, rest) = Restriction::decode(value, false)?;
if !rest.is_empty() {
Err(RuneError::ValueError(format!(
"found more than one restriction in {}",
value
)))
} else {
Ok(res)
}
}
}
#[derive(Clone)]
pub struct Rune {
restrictions: Vec<Restriction>,
compressor: sha256::Compressor,
authcode: [u8; 32],
}
impl Rune {
pub fn new(
authcode: [u8; 32],
restrictions: Vec<Restriction>,
unique_id: Option<String>,
version: Option<String>,
) -> Result<Self, RuneError> {
let compressor = sha256::Compressor::from_bytes(authcode, 64);
let mut rune = Self {
restrictions: vec![],
compressor,
authcode,
};
let mut restrictions = restrictions;
if let Some(id) = unique_id {
let uid = Restriction::unique_id(id, version)?;
restrictions.reverse();
restrictions.push(uid);
restrictions.reverse();
}
for r in restrictions {
rune.add_restriction(r)?;
}
Ok(rune)
}
pub fn new_master_rune(
seedsecret: &[u8],
restrictions: Vec<Restriction>,
unique_id: Option<String>,
version: Option<String>,
) -> Result<Self, RuneError> {
if seedsecret.len() + 1 + 8 > 64 {
return Err(RuneError::ValueError(
"seedsecret is expected to be less than 55 byte".to_string(),
));
}
let mut compressor = sha256::Compressor::default();
let mut base = seedsecret.to_vec();
add_padding(seedsecret.len(), &mut base);
compressor.update(&base);
Self::new(compressor.state().into(), restrictions, unique_id, version)
}
pub fn from_authcode(authcode: [u8; 32], restrictions: Vec<Restriction>) -> Self {
let state = sha256::State::from(authcode);
let mut len = 64;
for r in restrictions.clone() {
len += r.encode().len();
len += pad_len(len);
}
let compressor = sha256::Compressor::from_state(state.inner(), len as u64);
Self {
restrictions,
compressor,
authcode,
}
}
pub fn authcode(&self) -> [u8; 32] {
self.compressor.state().into()
}
pub fn from_base64(s: &str) -> Result<Self, RuneError> {
let engine = base64::engine::general_purpose::GeneralPurpose::new(
&base64::alphabet::URL_SAFE,
base64::engine::general_purpose::PAD,
);
let rune_byte = engine
.decode(s)
.map_err(|e| RuneError::Unknown(format!("{}", e)))?;
if rune_byte.len() < 32 {
return Err(RuneError::ValueError(
"expected decoded len to be contain 32byte authcode".to_string(),
));
}
let auth_str = hex::encode(&rune_byte[..32]);
let rest_str = String::from_utf8(rune_byte[32..].to_vec())
.map_err(|e| RuneError::Unknown(format!("{}", e)))?;
Self::from_str(&format!("{}:{}", auth_str, rest_str))
}
pub fn get_id(self) -> Option<String> {
let id = self
.restrictions
.iter()
.flat_map(|r| r.alternatives.iter())
.find(|&a| a.is_unique_id());
match id {
Some(a) => {
a.get_value().split('-').next().map(str::to_string)
}
None => None,
}
}
pub fn add_restriction<T: TryInto<Restriction> + Clone>(
&mut self,
res: T,
) -> Result<&Self, RuneError> {
let res: Restriction = res
.try_into()
.map_err(|_| RuneError::Conversion(String::new()))?;
self.restrictions.push(res.clone());
let mut data = res.encode().as_bytes().to_vec();
add_padding(self.compressor.size() + data.len(), &mut data);
self.compressor.update(&data);
Ok(self)
}
pub fn are_restrictions_met<T: Check + Clone>(&self, checker: T) -> Result<(), RuneError> {
self.restrictions
.iter()
.try_for_each(|res| res.test(checker.clone()))
}
pub fn is_authorized(&self, other: &Rune) -> bool {
let mut compressor = sha256::Compressor::from_bytes(self.authcode, 64);
for r in other.restrictions.iter() {
let mut data = r.encode().as_bytes().to_vec();
add_padding(compressor.size() + data.len(), &mut data);
compressor.update(&data);
}
compressor == other.compressor
}
pub fn check_with_reason<T: Check + Clone>(
&self,
b64str: &str,
checker: T,
) -> Result<(), RuneError> {
let rune = Rune::from_base64(b64str)?;
if !self.is_authorized(&rune) {
return Err(RuneError::Unauthorized);
}
rune.are_restrictions_met(checker)
}
pub fn to_base64(&self) -> String {
let rest_str = self
.restrictions
.iter()
.map(|r| r.encode())
.collect::<Vec<String>>()
.join("&");
let mut data: Vec<u8> = self.compressor.state().into();
data.append(&mut rest_str.as_bytes().to_vec());
let engine = base64::engine::general_purpose::GeneralPurpose::new(
&base64::alphabet::URL_SAFE,
base64::engine::general_purpose::PAD,
);
engine.encode(data)
}
}
impl ToString for Rune {
fn to_string(&self) -> String {
let rest_str = self
.restrictions
.iter()
.map(|r| r.encode())
.collect::<Vec<String>>()
.join("&");
format!("{}:{}", self.compressor.state().to_string(), rest_str)
}
}
impl FromStr for Rune {
type Err = RuneError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.len() < 64 || s.as_bytes()[64] as char != ':' {
return Err(RuneError::ValueError(
"rune strings must start with 64 hex digits followed by ':'".to_string(),
));
}
let auth_str = hex::decode(&s[..64]).map_err(|e| RuneError::Unknown(format!("{}", e)))?;
let mut res_str = &s[65..];
let mut restrictions = vec![];
while !res_str.is_empty() {
let allow_idfield = restrictions.is_empty();
let (restriction, rest_str) = Restriction::decode(res_str, allow_idfield)?;
restrictions.push(restriction);
res_str = rest_str;
}
let authcode: [u8; 32] = auth_str
.try_into()
.map_err(|e| RuneError::Unknown(format!("can not convert to authcode: {:?}", e)))?;
Ok(Self::from_authcode(authcode, restrictions))
}
}
const PUNCTUATION: [char; 31] = [
'!', '"', '#', '$', '%', '&', '\'', '(', ')', '*', '+', ',', '-', '.', '/', ':', ';', '?', '<',
'=', '>', '@', '[', '\\', ']', '^', '`', '{', '|', '}', '~',
];
fn is_separator(c: char) -> bool {
PUNCTUATION.contains(&c)
}
fn contains_punctuation(s: &str) -> bool {
for c in PUNCTUATION {
if s.contains(c) {
return true;
}
}
false
}
const BLOCK_SIZE: usize = 64;
fn pad_len(x: usize) -> usize {
(BLOCK_SIZE - (x % BLOCK_SIZE)) % BLOCK_SIZE
}
pub fn add_padding(length: usize, buf: &mut Vec<u8>) {
buf.push(0x80);
let pad_len = pad_len(length + 1 + 8);
let mut zeros = vec![0x00; pad_len];
buf.append(&mut zeros);
let len_bits = (length as u64) * 8;
buf.append(&mut len_bits.to_be_bytes().to_vec());
}
#[allow(unused_macros)]
#[macro_export]
macro_rules! restrictions {
() => {
vec![]
};
($s:expr) => {
$s.split('&')
.map(|r| <Restriction as ::std::convert::TryFrom<_>>::try_from(r).unwrap())
.collect::<Vec<Restriction>>()
};
($($arg:tt)+) => {
restrictions!(format!($($arg)+))
};
}
#[allow(unused_imports)]
#[cfg(test)]
mod tests {
use super::*;
use sha2::{Digest, Sha256};
fn check_auth_sha(secret: &[u8], restrictions: Vec<Restriction>) -> [u8; 32] {
let mut stream = secret.to_vec();
for r in restrictions {
add_padding(stream.len(), &mut stream);
stream.append(&mut r.encode().as_bytes().to_vec());
}
let mut hasher = Sha256::new();
hasher.update(&stream);
hasher.finalize().try_into().unwrap()
}
#[test]
fn test_restrictions_macro() {
let _ = restrictions!(
"{}^{}|method^get|method=getinfo&time=0011|time=1234&id=12345",
"method",
"hello"
);
}
#[test]
fn test_rune_auth() {
let secret = vec![0u8; 16];
let mut mr = Rune::new_master_rune(&secret, vec![], None, None).unwrap();
let digest = check_auth_sha(&secret, vec![]);
assert_eq!(mr.authcode(), digest);
assert!(mr.is_authorized(&Rune::from_authcode(mr.authcode(), vec![])));
let restriction =
Restriction::new(vec![
Alternative::new("f1", Condition::Equal, "v1", false).unwrap()
])
.unwrap();
_ = mr.add_restriction(restriction.clone());
assert_eq!(
mr.authcode(),
check_auth_sha(&secret, vec![restriction.clone()])
);
assert!(!mr.is_authorized(&Rune::from_authcode(mr.authcode(), vec![])));
assert!(mr.is_authorized(&Rune::from_authcode(
mr.authcode(),
vec![restriction.clone()]
)));
let field: String = (0..17).map(|_| "f").collect();
let value: String = (0..65).map(|_| "v1").collect();
let alternative = Alternative::new(field, Condition::Equal, value, false).unwrap();
let long_restriction = Restriction::new(vec![alternative]).unwrap();
_ = mr.add_restriction(long_restriction.clone());
assert_eq!(
mr.authcode(),
check_auth_sha(&secret, vec![restriction.clone(), long_restriction.clone()])
);
assert!(!mr.is_authorized(&Rune::from_authcode(
mr.authcode(),
vec![restriction.clone()]
)));
assert!(!mr.is_authorized(&Rune::from_authcode(
mr.authcode(),
vec![long_restriction.clone()]
)));
assert!(!mr.is_authorized(&Rune::from_authcode(
mr.authcode(),
vec![long_restriction.clone(), restriction.clone()]
)));
assert!(mr.is_authorized(&Rune::from_authcode(
mr.authcode(),
vec![restriction, long_restriction]
)));
}
#[test]
fn test_decode_encode_rune() {
let rune_str = "6035731a2cbb022cbeb67645aa0f8a26653d8cc454e0e087d4d19d282b8da4bd:=1";
let rune = Rune::from_str(rune_str).unwrap();
assert_eq!(rune.to_string(), rune_str);
let rune_str = "YDVzGiy7Aiy-tnZFqg-KJmU9jMRU4OCH1NGdKCuNpL09MQ==";
let rune = Rune::from_base64(rune_str).unwrap();
assert_eq!(rune.to_base64(), rune_str);
}
#[test]
fn test_contains_punctuation() {
for c in PUNCTUATION {
assert!(contains_punctuation(&c.to_string()));
}
let a = "Foo=123".to_string();
assert!(contains_punctuation(&a));
let a = "NoPunct".to_string();
assert!(!contains_punctuation(&a));
}
#[test]
fn test_pad_len() {
let a = pad_len(10);
assert_eq!(a, 54);
let a = pad_len(64);
assert_eq!(a, 0);
let a = pad_len(122);
assert_eq!(a, 6);
let a = pad_len(128);
assert_eq!(a, 0);
}
#[test]
fn test_new_restriction() {
let res = Restriction::new(vec![]);
assert!(res.is_err());
assert_eq!(
res.unwrap_err(),
RuneError::Unknown("restriction must have some alternatives".to_string())
);
let a1 = Alternative::new(
"".to_string(),
Condition::Equal,
"1010001".to_string(),
true,
)
.unwrap();
let a2 = Alternative::new(
"ABC".to_string(),
Condition::NotEqual,
"1".to_string(),
false,
)
.unwrap();
let res = Restriction::new(vec![a1, a2]);
assert!(res.is_err());
assert_eq!(
res.unwrap_err(),
RuneError::Unknown("unique_id field cannot have alternatives".to_string())
);
}
#[test]
fn test_decode_restriction() -> Result<(), RuneError> {
let s = "foo=bar|fizz/buzz&bar^foo";
let (res, rest) = Restriction::decode(s, false)?;
assert!(res.alternatives.len() == 2);
assert!(rest == "bar^foo");
Ok(())
}
#[test]
fn test_new_alternative() {
let alt = Alternative::new("".to_string(), Condition::Equal, "1".to_string(), false);
assert!(alt.is_err());
let alt = Alternative::new("".to_string(), Condition::NotEqual, "1".to_string(), true);
assert!(alt.is_err());
let alt = Alternative::new("".to_string(), Condition::Equal, "1".to_string(), true);
assert!(alt.is_ok());
}
#[test]
fn test_encode_alternative() {
let alt =
Alternative::new("f1".to_string(), Condition::IntGT, "1".to_string(), false).unwrap();
assert_eq!(alt.encode(), "f1>1".to_string());
}
#[test]
fn test_decode_alternative() -> Result<(), RuneError> {
let s = "foo=bar|fizz/buzz&bar^foo";
let (alt, rest) = Alternative::decode(s, false)?;
assert!(alt.field == *"foo");
assert!(alt.value == *"bar");
assert!(alt.cond == Condition::Equal);
assert!(rest == "fizz/buzz&bar^foo", "got {}", rest);
let s = "foo=bar&bar^foo";
let (alt, rest) = Alternative::decode(s, false)?;
assert!(alt.field == *"foo");
assert!(alt.value == *"bar");
assert!(alt.cond == Condition::Equal);
assert!(rest == "&bar^foo");
let s = "nocondition";
let result = Alternative::decode(s, false);
assert!(result.is_err());
Ok(())
}
#[test]
fn test_alternatives() -> Result<(), RuneError> {
fn test_alt_condition(
alt: &Alternative,
field: String,
value: String,
) -> Result<(), RuneError> {
let mut t: HashMap<String, String> = HashMap::new();
t.insert(field, value);
alt.test(MapChecker { map: t })
}
let alt = Alternative::new("f1".to_string(), Condition::Missing, "".to_string(), false)?;
assert!(alt
.test(MapChecker {
map: HashMap::new()
})
.is_ok());
assert_eq!(
test_alt_condition(&alt, "f1".to_string(), "1".to_string())
.unwrap_err()
.to_string(),
*"f1: is present"
);
assert!(test_alt_condition(&alt, "f2".to_string(), "1".to_string()).is_ok());
let alt = Alternative::new("f1".to_string(), Condition::Equal, "1".to_string(), false)?;
assert!(
alt.test(MapChecker {
map: HashMap::new()
})
.unwrap_err()
.to_string()
== *"f1: is missing"
);
assert!(test_alt_condition(&alt, "f1".to_string(), "1".to_string()).is_ok());
assert_eq!(
test_alt_condition(&alt, "f1".to_string(), "01".to_string())
.unwrap_err()
.to_string(),
*"f1: != 1"
);
assert_eq!(
test_alt_condition(&alt, "f1".to_string(), "10".to_string())
.unwrap_err()
.to_string(),
*"f1: != 1"
);
assert_eq!(
test_alt_condition(&alt, "f1".to_string(), "010".to_string())
.unwrap_err()
.to_string(),
*"f1: != 1"
);
assert_eq!(
test_alt_condition(&alt, "f1".to_string(), "10101".to_string())
.unwrap_err()
.to_string(),
*"f1: != 1"
);
let alt = Alternative::new(
"f1".to_string(),
Condition::NotEqual,
"1".to_string(),
false,
)?;
assert!(
alt.test(MapChecker {
map: HashMap::new()
})
.unwrap_err()
.to_string()
== *"f1: is missing"
);
assert_eq!(
test_alt_condition(&alt, "f1".to_string(), "1".to_string())
.unwrap_err()
.to_string(),
*"f1: = 1"
);
assert!(test_alt_condition(&alt, "f1".to_string(), "01".to_string()).is_ok());
assert!(test_alt_condition(&alt, "f1".to_string(), "10".to_string()).is_ok());
assert!(test_alt_condition(&alt, "f1".to_string(), "010".to_string()).is_ok());
assert!(test_alt_condition(&alt, "f1".to_string(), "10101".to_string()).is_ok());
let alt = Alternative::new(
"f1".to_string(),
Condition::BeginsWith,
"1".to_string(),
false,
)?;
assert!(
alt.test(MapChecker {
map: HashMap::new()
})
.unwrap_err()
.to_string()
== *"f1: is missing"
);
assert!(test_alt_condition(&alt, "f1".to_string(), "1".to_string()).is_ok());
assert_eq!(
test_alt_condition(&alt, "f1".to_string(), "01".to_string())
.unwrap_err()
.to_string(),
*"f1: does not start with 1"
);
assert!(test_alt_condition(&alt, "f1".to_string(), "10".to_string()).is_ok());
assert_eq!(
test_alt_condition(&alt, "f1".to_string(), "010".to_string())
.unwrap_err()
.to_string(),
*"f1: does not start with 1"
);
assert!(test_alt_condition(&alt, "f1".to_string(), "10101".to_string()).is_ok());
let alt = Alternative::new(
"f1".to_string(),
Condition::EndsWith,
"1".to_string(),
false,
)?;
assert!(
alt.test(MapChecker {
map: HashMap::new()
})
.unwrap_err()
.to_string()
== *"f1: is missing"
);
assert!(test_alt_condition(&alt, "f1".to_string(), "1".to_string()).is_ok());
assert!(test_alt_condition(&alt, "f1".to_string(), "01".to_string()).is_ok());
assert_eq!(
test_alt_condition(&alt, "f1".to_string(), "10".to_string())
.unwrap_err()
.to_string(),
*"f1: does not end with 1"
);
assert_eq!(
test_alt_condition(&alt, "f1".to_string(), "010".to_string())
.unwrap_err()
.to_string(),
*"f1: does not end with 1"
);
assert!(test_alt_condition(&alt, "f1".to_string(), "10101".to_string()).is_ok());
let alt = Alternative::new(
"f1".to_string(),
Condition::Contains,
"1".to_string(),
false,
)?;
assert!(
alt.test(MapChecker {
map: HashMap::new()
})
.unwrap_err()
.to_string()
== *"f1: is missing"
);
assert!(test_alt_condition(&alt, "f1".to_string(), "1".to_string()).is_ok());
assert!(test_alt_condition(&alt, "f1".to_string(), "01".to_string()).is_ok());
assert!(test_alt_condition(&alt, "f1".to_string(), "10".to_string()).is_ok());
assert!(test_alt_condition(&alt, "f1".to_string(), "010".to_string()).is_ok());
assert!(test_alt_condition(&alt, "f1".to_string(), "10101".to_string()).is_ok());
assert_eq!(
test_alt_condition(&alt, "f1".to_string(), "020".to_string())
.unwrap_err()
.to_string(),
*"f1: does not contain 1"
);
let alt = Alternative::new("f1".to_string(), Condition::IntLT, "1".to_string(), false)?;
assert!(
alt.test(MapChecker {
map: HashMap::new()
})
.unwrap_err()
.to_string()
== *"f1: is missing"
);
assert_eq!(
test_alt_condition(&alt, "f1".to_string(), "1".to_string())
.unwrap_err()
.to_string(),
*"f1: >= 1"
);
assert_eq!(
test_alt_condition(&alt, "f1".to_string(), "01".to_string())
.unwrap_err()
.to_string(),
*"f1: >= 1"
);
assert_eq!(
test_alt_condition(&alt, "f1".to_string(), "10".to_string())
.unwrap_err()
.to_string(),
*"f1: >= 1"
);
assert_eq!(
test_alt_condition(&alt, "f1".to_string(), "010".to_string())
.unwrap_err()
.to_string(),
*"f1: >= 1"
);
assert_eq!(
test_alt_condition(&alt, "f1".to_string(), "10101".to_string())
.unwrap_err()
.to_string(),
*"f1: >= 1"
);
assert!(test_alt_condition(&alt, "f1".to_string(), "0".to_string()).is_ok());
assert_eq!(
test_alt_condition(&alt, "f1".to_string(), "x".to_string())
.unwrap_err()
.to_string(),
*"f1: not an integer field"
);
let alt = Alternative::new("f1".to_string(), Condition::IntLT, "x".to_string(), false)?;
assert!(
alt.test(MapChecker {
map: HashMap::new()
})
.unwrap_err()
.to_string()
== *"f1: is missing"
);
assert_eq!(
test_alt_condition(&alt, "f1".to_string(), "1".to_string())
.unwrap_err()
.to_string(),
*"f1: not a valid integer"
);
let alt = Alternative::new("f1".to_string(), Condition::IntGT, "1".to_string(), false)?;
assert!(
alt.test(MapChecker {
map: HashMap::new()
})
.unwrap_err()
.to_string()
== *"f1: is missing"
);
assert_eq!(
test_alt_condition(&alt, "f1".to_string(), "1".to_string())
.unwrap_err()
.to_string(),
*"f1: <= 1"
);
assert_eq!(
test_alt_condition(&alt, "f1".to_string(), "01".to_string())
.unwrap_err()
.to_string(),
*"f1: <= 1"
);
assert!(test_alt_condition(&alt, "f1".to_string(), "10".to_string()).is_ok());
assert!(test_alt_condition(&alt, "f1".to_string(), "010".to_string()).is_ok());
assert!(test_alt_condition(&alt, "f1".to_string(), "10101".to_string()).is_ok());
assert_eq!(
test_alt_condition(&alt, "f1".to_string(), "0".to_string())
.unwrap_err()
.to_string(),
*"f1: <= 1"
);
assert_eq!(
test_alt_condition(&alt, "f1".to_string(), "x".to_string())
.unwrap_err()
.to_string(),
*"f1: not an integer field"
);
let alt = Alternative::new("f1".to_string(), Condition::IntGT, "x".to_string(), false)?;
assert!(
alt.test(MapChecker {
map: HashMap::new()
})
.unwrap_err()
.to_string()
== *"f1: is missing"
);
assert_eq!(
test_alt_condition(&alt, "f1".to_string(), "1".to_string())
.unwrap_err()
.to_string(),
*"f1: not a valid integer"
);
let alt = Alternative::new("f1".to_string(), Condition::LexLT, "1".to_string(), false)?;
assert!(
alt.test(MapChecker {
map: HashMap::new()
})
.unwrap_err()
.to_string()
== *"f1: is missing"
);
assert_eq!(
test_alt_condition(&alt, "f1".to_string(), "1".to_string())
.unwrap_err()
.to_string(),
*"f1: is the same or ordered after 1"
);
assert!(test_alt_condition(&alt, "f1".to_string(), "01".to_string()).is_ok());
assert_eq!(
test_alt_condition(&alt, "f1".to_string(), "10".to_string())
.unwrap_err()
.to_string(),
*"f1: is the same or ordered after 1"
);
assert!(test_alt_condition(&alt, "f1".to_string(), "010".to_string()).is_ok());
assert_eq!(
test_alt_condition(&alt, "f1".to_string(), "10101".to_string())
.unwrap_err()
.to_string(),
*"f1: is the same or ordered after 1"
);
assert!(test_alt_condition(&alt, "f1".to_string(), "0".to_string()).is_ok());
assert!(test_alt_condition(&alt, "f1".to_string(), "020".to_string()).is_ok());
let alt = Alternative::new("f1".to_string(), Condition::LexGT, "1".to_string(), false)?;
assert!(
alt.test(MapChecker {
map: HashMap::new()
})
.unwrap_err()
.to_string()
== *"f1: is missing"
);
assert_eq!(
test_alt_condition(&alt, "f1".to_string(), "1".to_string())
.unwrap_err()
.to_string(),
*"f1: is the same or ordered before 1"
);
assert_eq!(
test_alt_condition(&alt, "f1".to_string(), "01".to_string())
.unwrap_err()
.to_string(),
*"f1: is the same or ordered before 1"
);
assert!(test_alt_condition(&alt, "f1".to_string(), "10".to_string()).is_ok());
assert_eq!(
test_alt_condition(&alt, "f1".to_string(), "010".to_string())
.unwrap_err()
.to_string(),
*"f1: is the same or ordered before 1"
);
assert!(test_alt_condition(&alt, "f1".to_string(), "10101".to_string()).is_ok());
assert_eq!(
test_alt_condition(&alt, "f1".to_string(), "0".to_string())
.unwrap_err()
.to_string(),
*"f1: is the same or ordered before 1"
);
assert_eq!(
test_alt_condition(&alt, "f1".to_string(), "020".to_string())
.unwrap_err()
.to_string(),
*"f1: is the same or ordered before 1"
);
let alt = Alternative::new("f1".to_string(), Condition::Comment, "1".to_string(), false)?;
assert!(
alt.test(MapChecker {
map: HashMap::new()
})
.is_ok(),
"Expected None, got {}",
alt.test(MapChecker {
map: HashMap::new()
})
.unwrap_err(),
);
assert!(test_alt_condition(&alt, "f1".to_string(), "1".to_string()).is_ok());
assert!(test_alt_condition(&alt, "f1".to_string(), "01".to_string()).is_ok());
assert!(test_alt_condition(&alt, "f1".to_string(), "10".to_string()).is_ok());
assert!(test_alt_condition(&alt, "f1".to_string(), "10101".to_string()).is_ok());
assert!(test_alt_condition(&alt, "f1".to_string(), "0".to_string()).is_ok());
Ok(())
}
#[test]
fn test_rune_is_authorized() -> Result<(), RuneError> {
let secret = vec![0; 16];
let (r1, _) = Restriction::decode("f1=v1|f2=v2", false)?;
let (r2, _) = Restriction::decode("f2/v3", false)?;
let mr = Rune::new_master_rune(&secret, vec![r1, r2], None, None)?;
let enc = mr.to_string();
let mut other = Rune::from_str(&enc)?;
let (r3, _) = Restriction::decode("f4=v4", false)?;
_ = other.add_restriction(r3);
assert!(mr.is_authorized(&other));
Ok(())
}
#[test]
fn test_vectors() {
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::str::FromStr;
let secret = vec![0u8; 16];
let mr = Rune::new_master_rune(&secret, vec![], None, None).unwrap();
let mut last_rune1: Option<Rune> = None;
let mut last_rune2: Option<Rune> = None;
let f = File::open("tests/test_vectors.csv")
.map_err(|e| RuneError::Unknown(format!("{}", e)))
.unwrap();
let buf = BufReader::new(f);
for result in buf.lines() {
let line = result.unwrap();
let splits: Vec<&str> = line.split(',').collect();
match splits[0] {
"VALID" => {
println!("{}", splits[1]);
let rune1 = Rune::from_str(splits[2]).unwrap();
let rune2: Rune = Rune::from_base64(splits[3]).unwrap();
last_rune1 = Some(rune1.clone());
last_rune2 = Some(rune2.clone());
assert_eq!(rune1.to_string(), rune2.to_string());
assert_eq!(rune1.to_base64(), rune2.to_base64());
assert!(mr.is_authorized(&rune1));
assert!(mr.is_authorized(&rune2));
if splits.len() == 6 {
assert_eq!(
rune1.restrictions[0].alternatives[0].encode(),
format!("={}-{}", splits[4], splits[5])
)
} else if splits.len() == 5 {
assert_eq!(
rune1.restrictions[0].alternatives[0].encode(),
format!("={}", splits[4])
);
} else {
assert!(splits.len() == 4);
assert!(
rune1.restrictions.is_empty()
|| !rune1.restrictions[0].alternatives[0]
.encode()
.starts_with('=')
)
}
}
"MALFORMED" => {
println!("{}", splits[1]);
let result1 = Rune::from_str(splits[2]);
let result2 = Rune::from_base64(splits[3]);
assert!(result1.is_err());
assert!(result2.is_err());
}
"BAD DERIVATION" => {
println!("{}", splits[1]);
let rune1 = Rune::from_str(splits[2]).unwrap();
let rune2 = Rune::from_base64(splits[3]).unwrap();
assert!(!mr.is_authorized(&rune1));
assert!(!mr.is_authorized(&rune2));
}
"PASS" | "FAIL" => {
assert!(splits[0] == "PASS" || splits[0] == "FAIL");
let rune1 = last_rune1.clone().unwrap();
let rune2 = last_rune2.clone().unwrap();
let mut values: HashMap<String, String> = HashMap::new();
let rest = splits[1..].to_vec();
for var in rest {
let parts: Vec<&str> = var.split('=').collect();
assert_eq!(parts.len(), 2);
values.insert(parts[0].to_string(), parts[1].to_string());
}
if splits[0] == "PASS" {
assert!(rune1
.are_restrictions_met(MapChecker {
map: values.clone()
})
.is_ok());
assert!(rune2
.are_restrictions_met(MapChecker { map: values })
.is_ok());
} else {
assert!(rune1
.are_restrictions_met(MapChecker {
map: values.clone()
})
.is_err());
assert!(rune2
.are_restrictions_met(MapChecker { map: values })
.is_err());
}
}
"DERIVE" => {
println!("{}", splits[1]);
let mut rune1 = Rune::from_base64(splits[2]).unwrap();
assert!(mr.is_authorized(&rune1));
let rune2 = Rune::from_base64(splits[3]).unwrap();
assert!(mr.is_authorized(&rune2));
let mut alts = Vec::new();
let mut b = &splits[4..];
while !b.is_empty() {
alts.push(
Alternative::new(
b[0].to_string(),
b[1].try_into().unwrap(),
b[2].to_string(),
true,
)
.unwrap(),
);
b = &b[3..];
}
_ = rune1.add_restriction(Restriction { alternatives: alts });
assert!(rune1.authcode() == rune2.authcode());
last_rune1 = Some(rune1);
last_rune2 = Some(rune2);
}
p => {
panic!("{} is not handled", p)
}
}
}
}
#[test]
fn test_whole_cycle() {
let secret = vec![0; 16];
let mr = Rune::new_master_rune(&secret, vec![], None, None).unwrap();
let mut res_str = "pubkey=mypubkey";
let mut res = vec![];
while !res_str.is_empty() {
let (restriction, rest) = Restriction::decode(res_str, false).unwrap();
res.push(restriction);
res_str = rest;
}
let rune = Rune::new(mr.authcode(), res, None, None).unwrap();
let mut checks: HashMap<String, String> = HashMap::new();
checks.insert("pubkey".to_string(), "mypubkey".to_string());
assert!(mr
.check_with_reason(
&rune.to_base64(),
MapChecker {
map: checks.clone()
}
)
.is_ok());
checks.insert("pubkey".to_string(), "wrong_pubkey".to_string());
assert!(mr
.check_with_reason(&rune.to_base64(), MapChecker { map: checks })
.is_err());
}
#[test]
fn test_rune_id() {
let secret = vec![0; 16];
let mr = Rune::new_master_rune(&secret, vec![], None, None).unwrap();
assert!(mr.get_id().is_none());
let mr = Rune::new_master_rune(&secret, vec![], Some("myid".to_string()), None).unwrap();
assert!(mr.get_id().unwrap() == *"myid");
let mr = Rune::new_master_rune(&secret, vec![], None, Some("version".to_string())).unwrap();
assert!(mr.get_id().is_none());
let mr = Rune::new_master_rune(
&secret,
vec![],
Some("myid".to_string()),
Some("version".to_string()),
)
.unwrap();
assert!(mr.get_id().unwrap() == *"myid");
}
}