#![forbid(unsafe_code)]
extern crate fs2;
extern crate libc;
extern crate pwhash;
use fs2::FileExt;
use pwhash::bcrypt;
use std::{fs, io};
use std::collections::HashMap;
use std::convert::From;
use std::fmt::Debug;
use std::fs::{File, OpenOptions};
use std::io::{BufWriter, Read, Write};
use std::sync::Mutex;
use std::vec::IntoIter;
#[cfg(unix)]
use std::os::unix::fs::OpenOptionsExt;
#[cfg(windows)]
use std::os::windows::fs::OpenOptionsExt;
#[derive(Debug)]
pub enum BackingStoreError {
NoSuchUser,
MissingData,
Locked,
UserExists,
IO(io::Error),
Mutex,
Hash(pwhash::error::Error),
InvalidCredentials,
}
impl PartialEq for BackingStoreError {
fn eq(&self, other: &BackingStoreError) -> bool {
match (self, other) {
(&BackingStoreError::NoSuchUser, &BackingStoreError::NoSuchUser)
| (&BackingStoreError::MissingData, &BackingStoreError::MissingData)
| (&BackingStoreError::Locked, &BackingStoreError::Locked)
| (&BackingStoreError::UserExists, &BackingStoreError::UserExists)
| (&BackingStoreError::IO(_), &BackingStoreError::IO(_))
| (&BackingStoreError::Mutex, &BackingStoreError::Mutex)
| (&BackingStoreError::Hash(_), &BackingStoreError::Hash(_)) => true,
_ => false,
}
}
}
impl From<io::Error> for BackingStoreError {
fn from(err: io::Error) -> BackingStoreError {
BackingStoreError::IO(err)
}
}
impl From<pwhash::error::Error> for BackingStoreError {
fn from(err: pwhash::error::Error) -> BackingStoreError {
BackingStoreError::Hash(err)
}
}
pub trait BackingStore: Debug {
fn encrypt_credentials(&self, plain: &str) -> Result<String, BackingStoreError>;
fn verify(&self, user: &str, plain_cred: &str) -> Result<bool, BackingStoreError>;
fn get_credentials(
&self,
user: &str,
fail_if_locked: bool,
) -> Result<String, BackingStoreError>;
fn update_credentials(&self, user: &str, enc_cred: &str) -> Result<(), BackingStoreError>;
fn update_credentials_plain(
&self,
user: &str,
plain_cred: &str,
) -> Result<(), BackingStoreError> {
let enc_cred = self.encrypt_credentials(plain_cred)?;
self.update_credentials(user, &enc_cred)
}
fn lock(&self, user: &str) -> Result<(), BackingStoreError>;
fn is_locked(&self, user: &str) -> Result<bool, BackingStoreError>;
fn unlock(&self, user: &str) -> Result<(), BackingStoreError>;
fn create_preencrypted(&self, user: &str, enc_cred: &str) -> Result<(), BackingStoreError>;
fn create_plain(&self, user: &str, plain_cred: &str) -> Result<(), BackingStoreError> {
let enc_cred = self.encrypt_credentials(plain_cred)?;
self.create_preencrypted(user, &enc_cred)
}
fn delete(&self, user: &str) -> Result<(), BackingStoreError>;
fn users(&self) -> Result<Vec<String>, BackingStoreError> {
self.users_iter().map(|v| v.collect())
}
fn users_iter(&self) -> Result<IntoIter<String>, BackingStoreError> {
self.users().map(|v| v.into_iter())
}
fn check_user(&self, user: &str) -> Result<bool, BackingStoreError>;
}
#[derive(Debug)]
pub struct FileBackingStore {
filename: Mutex<String>,
cost: u32,
}
#[cfg(unix)]
macro_rules! fbs_options {
($x:expr) => {
$x.mode(0o600).custom_flags(libc::O_NOFOLLOW)
};
}
#[cfg(windows)]
macro_rules! fbs_options {
($x:expr) => {
$x.share_mode(0)
};
}
impl FileBackingStore {
pub fn new(filename: &str) -> FileBackingStore {
let fname = filename.to_string();
FileBackingStore {
filename: Mutex::new(fname),
cost: bcrypt::DEFAULT_COST,
}
}
pub fn new_with_cost(filename: &str, cost: u32) -> FileBackingStore {
let fname = filename.to_string();
FileBackingStore {
filename: Mutex::new(fname),
cost
}
}
fn load_file(&self) -> Result<String, BackingStoreError> {
let fname = self.filename.lock().map_err(|_| BackingStoreError::Mutex)?;
let name = fname.to_string();
let mut buf = String::new();
let mut f = File::open(name)?;
f.lock_shared()?;
f.read_to_string(&mut buf)?;
Ok(buf)
}
fn line_has_user(
line: &str,
user: &str,
fail_if_locked: bool,
) -> Result<Option<String>, BackingStoreError> {
let v: Vec<&str> = line.splitn(2, ':').collect();
let fixed_user = FileBackingStore::fix_username(user);
if v.len() < 2 {
Err(BackingStoreError::MissingData)
} else if v[0] == fixed_user {
if fail_if_locked && FileBackingStore::hash_is_locked(v[1]) {
Err(BackingStoreError::Locked)
} else {
Ok(Some(v[1].to_string()))
}
} else {
Ok(None)
}
}
fn hash_is_locked(hash: &str) -> bool {
hash.starts_with('!')
}
fn fix_username(user: &str) -> String {
user.replace("\n", "\u{FFFD}").replace(":", "\u{FFFFD}")
}
fn create_safe(filename: &str) -> Result<File, BackingStoreError> {
let newf;
let mut opts = OpenOptions::new();
let o = fbs_options!(opts.create_new(true).write(true));
loop {
if match fs::remove_file(&filename) {
Ok(_) => true,
Err(ref e) if e.kind() == io::ErrorKind::NotFound => true,
Err(ref e) if e.kind() == io::ErrorKind::Interrupted => false,
Err(e) => return Err(BackingStoreError::IO(e)),
} {
match o.open(&filename) {
Ok(x) => {
newf = x;
break;
}
Err(ref e) if e.kind() == io::ErrorKind::Interrupted => continue,
Err(ref e) if e.kind() == io::ErrorKind::AlreadyExists => continue,
Err(e) => return Err(BackingStoreError::IO(e)),
}
}
}
newf.set_permissions(fs::metadata(filename)?.permissions())?;
newf.lock_exclusive()?;
Ok(newf)
}
fn update_password_file(
&self,
username: &str,
new_creds: Option<&str>,
fail_if_locked: Option<bool>,
) -> Result<(), BackingStoreError> {
let mut user_recorded = false;
let fixedname = FileBackingStore::fix_username(username);
let (create_new, change_pass, fil) = match (new_creds, fail_if_locked) {
(Some(_), None) => (true, false, false),
(Some(_), Some(fil)) => (false, true, fil),
(None, Some(fil)) => (false, false, fil),
(None, None) => (false, false, false),
};
let all = self.load_file()?;
let basename = self.filename.lock().map_err(|_| BackingStoreError::Mutex)?;
let oldfn = basename.to_string() + ".old";
let newfn = basename.to_string() + ".new";
let mut backupf = FileBackingStore::create_safe(&oldfn)?;
backupf.write_all(all.as_bytes())?;
backupf.flush()?;
drop(backupf);
let mut f = BufWriter::new(FileBackingStore::create_safe(&newfn)?);
for line in all.lines() {
match FileBackingStore::line_has_user(line, username, fil)? {
Some(_) => {
if create_new {
let _ = fs::remove_file(&oldfn);
let _ = fs::remove_file(&newfn);
return Err(BackingStoreError::UserExists);
} else if change_pass {
if user_recorded {
warn!(
"{} already found in {}; removing extra line",
username, basename
);
} else {
f.write_all(fixedname.as_bytes())?;
f.write_all(b":")?;
f.write_all(new_creds.unwrap().as_bytes())?;
f.write_all(b"\n")?;
}
}
user_recorded = true;
}
None => {
f.write_all(line.as_bytes())?;
f.write_all(b"\n")?;
}
}
}
if create_new {
f.write_all(fixedname.as_bytes())?;
f.write_all(b":")?;
f.write_all(new_creds.unwrap().as_bytes())?;
f.write_all(b"\n")?;
} else if !user_recorded {
let _ = fs::remove_file(&oldfn);
let _ = fs::remove_file(&newfn);
return Err(BackingStoreError::NoSuchUser);
}
f.flush()?;
drop(f);
fs::rename(newfn, basename.to_string())?;
Ok(())
}
}
impl BackingStore for FileBackingStore {
fn encrypt_credentials(&self, plain: &str) -> Result<String, BackingStoreError> {
Ok(bcrypt::hash_with(bcrypt::BcryptSetup { cost: Some(self.cost), ..Default::default() }, plain)?)
}
fn get_credentials(
&self,
user: &str,
fail_if_locked: bool,
) -> Result<String, BackingStoreError> {
let pwfile = self.load_file()?;
for line in pwfile.lines() {
if let Some(hash) = FileBackingStore::line_has_user(line, user, fail_if_locked)? {
return Ok(hash);
}
}
Err(BackingStoreError::NoSuchUser)
}
fn verify(&self, user: &str, plain_cred: &str) -> Result<bool, BackingStoreError> {
let hash = self.get_credentials(user, true)?;
Ok(bcrypt::verify(plain_cred, &hash))
}
fn update_credentials(&self, user: &str, enc_cred: &str) -> Result<(), BackingStoreError> {
self.update_password_file(user, Some(enc_cred), Some(true))
}
fn lock(&self, user: &str) -> Result<(), BackingStoreError> {
let mut hash = self.get_credentials(user, false)?;
if !FileBackingStore::hash_is_locked(&hash) {
hash.insert(0, '!');
self.update_password_file(user, Some(&hash), Some(false))
} else {
Ok(())
}
}
fn is_locked(&self, user: &str) -> Result<bool, BackingStoreError> {
let hash = self.get_credentials(user, false)?;
Ok(FileBackingStore::hash_is_locked(&hash))
}
fn unlock(&self, user: &str) -> Result<(), BackingStoreError> {
let mut hash = self.get_credentials(user, false)?;
if FileBackingStore::hash_is_locked(&hash) {
hash.remove(0);
self.update_password_file(user, Some(&hash), Some(false))
} else {
Ok(())
}
}
fn create_preencrypted(&self, user: &str, enc_cred: &str) -> Result<(), BackingStoreError> {
self.update_password_file(user, Some(enc_cred), None)
}
fn delete(&self, user: &str) -> Result<(), BackingStoreError> {
self.update_password_file(user, None, Some(false))
}
fn users(&self) -> Result<Vec<String>, BackingStoreError> {
let mut users = Vec::new();
let pwfile = self.load_file()?;
for line in pwfile.lines() {
let v: Vec<&str> = line.split(':').collect();
if v.is_empty() {
continue;
} else {
users.push(v[0].to_string());
}
}
Ok(users)
}
fn check_user(&self, user: &str) -> Result<bool, BackingStoreError> {
let pwfile = self.load_file()?;
let fixeduser = FileBackingStore::fix_username(user);
for line in pwfile.lines() {
let v: Vec<&str> = line.split(':').collect();
if (v.len() > 1) && (v[0] == fixeduser) {
if FileBackingStore::hash_is_locked(v[1]) {
return Err(BackingStoreError::Locked);
} else {
return Ok(true);
}
}
}
Ok(false)
}
}
#[derive(Debug)]
struct MemoryEntry {
credentials: String,
locked: bool,
}
#[derive(Debug, Default)]
pub struct MemoryBackingStore {
users: Mutex<HashMap<String, MemoryEntry>>,
}
impl MemoryBackingStore {
pub fn new() -> MemoryBackingStore {
MemoryBackingStore {
users: Mutex::new(HashMap::new()),
}
}
}
impl BackingStore for MemoryBackingStore {
fn encrypt_credentials(&self, plain: &str) -> Result<String, BackingStoreError> {
Ok(bcrypt::hash(plain)?)
}
fn get_credentials(
&self,
user: &str,
fail_if_locked: bool,
) -> Result<String, BackingStoreError> {
let hashmap = self.users.lock().map_err(|_| BackingStoreError::Mutex)?;
match hashmap.get(user) {
Some(entry) => {
if !(fail_if_locked && entry.locked) {
Ok(entry.credentials.to_string())
} else {
Err(BackingStoreError::Locked)
}
}
None => Err(BackingStoreError::NoSuchUser),
}
}
fn verify(&self, user: &str, plain_cred: &str) -> Result<bool, BackingStoreError> {
let creds = self.get_credentials(user, true)?;
Ok(bcrypt::verify(plain_cred, &creds))
}
fn update_credentials(&self, user: &str, enc_cred: &str) -> Result<(), BackingStoreError> {
let mut hashmap = self.users.lock().map_err(|_| BackingStoreError::Mutex)?;
match hashmap.get_mut(user) {
Some(entry) => {
if entry.locked {
Err(BackingStoreError::Locked)
} else {
entry.credentials = enc_cred.to_string();
Ok(())
}
},
None => Err(BackingStoreError::NoSuchUser),
}
}
fn lock(&self, user: &str) -> Result<(), BackingStoreError> {
let mut hashmap = self.users.lock().map_err(|_| BackingStoreError::Mutex)?;
match hashmap.get_mut(user) {
Some(entry) => {
entry.locked = true;
Ok(())
}
None => Err(BackingStoreError::NoSuchUser),
}
}
fn is_locked(&self, user: &str) -> Result<bool, BackingStoreError> {
let hashmap = self.users.lock().map_err(|_| BackingStoreError::Mutex)?;
match hashmap.get(user) {
Some(entry) => Ok(entry.locked),
None => Err(BackingStoreError::NoSuchUser),
}
}
fn unlock(&self, user: &str) -> Result<(), BackingStoreError> {
let mut hashmap = self.users.lock().map_err(|_| BackingStoreError::Mutex)?;
match hashmap.get_mut(user) {
Some(entry) => {
entry.locked = false;
Ok(())
}
None => Err(BackingStoreError::NoSuchUser),
}
}
fn create_preencrypted(&self, user: &str, enc_cred: &str) -> Result<(), BackingStoreError> {
let mut hashmap = self.users.lock().map_err(|_| BackingStoreError::Mutex)?;
if hashmap.contains_key(user) {
Err(BackingStoreError::UserExists)
} else {
hashmap.insert(
user.to_string(),
MemoryEntry {
credentials: enc_cred.to_string(),
locked: false,
},
);
Ok(())
}
}
fn delete(&self, user: &str) -> Result<(), BackingStoreError> {
let mut hashmap = self.users.lock().map_err(|_| BackingStoreError::Mutex)?;
match hashmap.remove(user) {
Some(_) => Ok(()),
None => Err(BackingStoreError::NoSuchUser),
}
}
fn users(&self) -> Result<Vec<String>, BackingStoreError> {
let hashmap = self.users.lock().map_err(|_| BackingStoreError::Mutex)?;
Ok(hashmap.keys().cloned().collect::<Vec<String>>())
}
fn check_user(&self, user: &str) -> Result<bool, BackingStoreError> {
let hashmap = self.users.lock().map_err(|_| BackingStoreError::Mutex)?;
if let Some(u) = hashmap.get(user) {
if u.locked {
Err(BackingStoreError::Locked)
} else {
Ok(true)
}
} else {
Ok(false)
}
}
}
#[cfg(test)]
mod test {
extern crate rand;
extern crate tempfile;
use tempfile::TempDir;
use rand::Rng;
use crate::backingstore::*;
use std::collections::HashSet;
use std::fs::File;
fn make_filebackingstore() -> (FileBackingStore, TempDir) {
let fullpath = TempDir::new().unwrap();
let tp = fullpath.path().join("fbs");
let path = tp.to_str().unwrap();
let _f = File::create(path);
(FileBackingStore::new(&path), fullpath)
}
#[test]
fn fbs_colons_in_usernames() {
let (fbs, _temp) = make_filebackingstore();
assert!(fbs.create_plain("now:a:valid:user", "password").is_ok());
}
#[test]
fn mbs_colons_in_usernames() {
let mbs = MemoryBackingStore::new();
assert!(mbs.create_plain("now:a:good:user", "password").is_ok());
}
#[test]
fn fbs_unicrud() {
let (fbs, _temp) = make_filebackingstore();
assert!(fbs.create_plain("Some\u{FFFD}", "Unicode\u{2747}").is_ok());
}
#[test]
fn fbs_create_user_plain() {
let (fbs, _temp) = make_filebackingstore();
assert!(fbs.create_plain("user", "password").is_ok());
}
#[test]
fn mbs_create_user_plain() {
let mbs = MemoryBackingStore::new();
assert!(mbs.create_plain("user", "password").is_ok());
}
#[test]
fn fbs_can_locked_login() {
let (fbs, _temp) = make_filebackingstore();
assert!(fbs.create_plain("user", "password").is_ok());
assert!(fbs.lock("user").is_ok());
assert!(fbs.verify("user", "password").is_err());
}
#[test]
fn mbs_can_locked_login() {
let mbs = MemoryBackingStore::new();
assert!(mbs.create_plain("user", "password").is_ok());
assert!(mbs.verify("user", "password").is_ok());
assert!(mbs.lock("user").is_ok());
assert!(mbs.verify("user", "password").is_err());
}
#[test]
fn fbs_check_user() {
let (fbs, _temp) = make_filebackingstore();
assert!(fbs.create_plain("user", "password").is_ok());
assert!(fbs.check_user("user").is_ok());
assert_eq!(fbs.check_user("user"), Ok(true));
assert_eq!(fbs.check_user("missing"), Ok(false));
assert!(fbs.lock("user").is_ok());
assert_eq!(fbs.check_user("user"), Err(BackingStoreError::Locked));
}
#[test]
fn mbs_check_user() {
let mbs = MemoryBackingStore::new();
assert!(mbs.create_plain("user", "password").is_ok());
assert!(mbs.check_user("user").is_ok());
assert_eq!(mbs.check_user("user"), Ok(true));
assert_eq!(mbs.check_user("missing"), Ok(false));
assert!(mbs.lock("user").is_ok());
assert_eq!(mbs.check_user("user"), Err(BackingStoreError::Locked));
}
#[test]
fn fbs_check_newline() {
let (fbs, _temp) = make_filebackingstore();
assert!(fbs.create_plain("user\nname", "password").is_ok());
assert!(fbs.check_user("user\nname").is_ok());
assert_eq!(fbs.check_user("user\nname"), Ok(true));
assert_eq!(fbs.check_user("user\u{FFFD}name"), Ok(true));
}
#[test]
fn mbs_check_newline1() {
let mbs = MemoryBackingStore::new();
assert!(mbs.create_plain("user\nname", "password").is_ok());
}
#[test]
fn mbs_check_newline2() {
let mbs = MemoryBackingStore::new();
assert!(mbs.create_plain("user\nname", "password").is_ok());
assert!(mbs.check_user("user\nname").is_ok());
}
#[test]
fn mbs_check_newline3() {
let mbs = MemoryBackingStore::new();
assert!(mbs.create_plain("user\nname", "password").is_ok());
assert_eq!(mbs.check_user("user\nname"), Ok(true));
}
#[test]
fn fbs_fuzz_users() {
let (fbs, _temp) = make_filebackingstore();
let names: Vec<String> = (0..10)
.map(|_| (0..10).map(|_| rand::random::<char>()).collect())
.collect();
let passwords: Vec<String> = (0..10)
.map(|_| (0..10).map(|_| rand::random::<char>()).collect())
.collect();
let newpasswords: Vec<String> = (0..10)
.map(|_| (0..10).map(|_| rand::random::<char>()).collect())
.collect();
let mut added: HashSet<&str> = HashSet::new();
let mut locked: HashSet<&str> = HashSet::new();
let mut changed: HashSet<&str> = HashSet::new();
enum Things {
Add,
Lock,
Unlock,
Change,
Delete,
Examine,
Verify,
}
impl Things {
fn random() -> Self {
match rand::thread_rng().gen_range(0..7) {
0 => Things::Add,
1 => Things::Lock,
2 => Things::Unlock,
3 => Things::Change,
4 => Things::Delete,
5 => Things::Examine,
_ => Things::Verify,
}
}
}
for _ in 1..10 {
for (i, x) in names.iter().enumerate() {
match Things::random() {
Things::Add => {
println!("\tAdd {} (already present: {})", x, added.contains(&x.as_str()));
if added.contains(&x.as_str()) {
assert_eq!(fbs.create_plain(&x, &passwords[i]), Err(BackingStoreError::UserExists));
} else {
assert!(fbs.create_plain(&x, &passwords[i]).is_ok());
added.insert(&x.as_str());
}
}
Things::Lock => {
println!("\tLock {} (already present: {})", x, added.contains(&x.as_str()));
if added.contains(&x.as_str()) {
assert!(fbs.lock(&x.as_str()).is_ok());
locked.insert(&x.as_str());
} else {
assert_eq!(fbs.lock(&x.as_str()), Err(BackingStoreError::NoSuchUser));
}
}
Things::Unlock => {
println!("\tUnlock {} (already present: {})", x, added.contains(&x.as_str()));
if added.contains(&x.as_str()) {
assert!(fbs.unlock(&x.as_str()).is_ok());
locked.remove(&x.as_str());
} else {
assert_eq!(fbs.unlock(&x.as_str()), Err(BackingStoreError::NoSuchUser));
}
}
Things::Change => {
println!("\tChange {} (already present: {})", x, added.contains(&x.as_str()));
if added.contains(&x.as_str()) {
println!("\t\tlocked: {}", locked.contains(&x.as_str()));
if locked.contains(&x.as_str()) {
assert_eq!(fbs.update_credentials_plain(&x, &newpasswords[i]), Err(BackingStoreError::Locked));
} else {
assert!(fbs.update_credentials_plain(&x, &newpasswords[i]).is_ok());
changed.insert(&x.as_str());
}
} else {
assert_eq!(fbs.update_credentials_plain(&x, &newpasswords[i]), Err(BackingStoreError::NoSuchUser));
}
}
Things::Delete => {
println!("\tDelete {} (already present: {})", x, added.contains(&x.as_str()));
if added.contains(&x.as_str()) {
assert!(fbs.delete(&x.as_str()).is_ok());
locked.remove(&x.as_str());
changed.remove(&x.as_str());
added.remove(&x.as_str());
} else {
assert_eq!(fbs.delete(&x.as_str()), Err(BackingStoreError::NoSuchUser));
}
}
Things::Examine => {
println!("\tExamine {} (already present: {})", x, added.contains(&x.as_str()));
if added.contains(&x.as_str()) {
println!("\t\tlocked: {}", locked.contains(&x.as_str()));
if locked.contains(&x.as_str()) {
assert_eq!(fbs.check_user(&x.as_str()), Err(BackingStoreError::Locked));
} else {
assert_eq!(fbs.check_user(&x.as_str()), Ok(true));
}
} else {
assert_eq!(fbs.check_user(&x.as_str()), Ok(false));
}
}
Things::Verify => {
println!("\tExamine {} (already present: {})", x, added.contains(&x.as_str()));
if added.contains(&x.as_str()) {
println!("\t\tchanged: {}, locked: {}", changed.contains(&x.as_str()), locked.contains(&x.as_str()));
if locked.contains(&x.as_str()) {
assert_eq!(fbs.verify(&x, &passwords[i]), Err(BackingStoreError::Locked));
assert_eq!(fbs.verify(&x, &newpasswords[i]), Err(BackingStoreError::Locked));
} else if changed.contains(&x.as_str()) {
assert_eq!(fbs.verify(&x, &passwords[i]), Ok(false));
assert_eq!(fbs.verify(&x, &newpasswords[i]), Ok(true));
} else {
assert_eq!(fbs.verify(&x, &passwords[i]), Ok(true));
assert_eq!(fbs.verify(&x, &newpasswords[i]), Ok(false));
}
} else {
assert_eq!(fbs.verify(&x, &passwords[i]), Err(BackingStoreError::NoSuchUser));
assert_eq!(fbs.verify(&x, &newpasswords[i]), Err(BackingStoreError::NoSuchUser));
}
}
}
}
}
}
#[test]
fn check_ciphertext() {
let (fbs, _temp) = make_filebackingstore();
assert!(fbs.create_preencrypted("user", "$2b$08$u5hkiF.YyDvO/rBYf/02eezYvWj/rxRGZISzqBL3KtgL.NECyTUom")
.is_ok());
assert!(fbs.verify("user", "password").is_ok());
}
}