Add dependencies for logging, testing, pw hashing

- Closes #4
- Closes #6
- Improves test coverage
- Adds password hashing
- Adds totp token generation
pull/11/head
Anthony J. Martinez 3 years ago
parent fb2b39aeb2
commit f3ab948253

77
Cargo.lock generated

@ -18,6 +18,16 @@ dependencies = [
"winapi",
]
[[package]]
name = "argon2"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ee15f9f5e0f846cab0aa13d5dd1edbc49331dcae95a2e43b84ccc0b406dcda4"
dependencies = [
"blake2",
"password-hash",
]
[[package]]
name = "atty"
version = "0.2.14"
@ -41,12 +51,29 @@ version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
[[package]]
name = "base64ct"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0d27fb6b6f1e43147af148af49d49329413ba781aa0d5e10979831c210173b5"
[[package]]
name = "bitflags"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
[[package]]
name = "blake2"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10a5720225ef5daecf08657f23791354e1685a8c91a4c60c7f3d3b2892f978f4"
dependencies = [
"crypto-mac",
"digest 0.9.0",
"opaque-debug 0.3.0",
]
[[package]]
name = "block-buffer"
version = "0.7.3"
@ -118,11 +145,15 @@ checksum = "b700ce4376041dcd0a327fd0097c41095743c4c8af8887265942faf1100bd040"
name = "caty_blog"
version = "0.1.0"
dependencies = [
"argon2",
"clap",
"data-encoding",
"log",
"pulldown-cmark",
"rand 0.8.3",
"serde",
"simplelog",
"tempfile",
"tera",
"tokio",
"toml",
@ -190,6 +221,16 @@ dependencies = [
"lazy_static",
]
[[package]]
name = "crypto-mac"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b584a330336237c1eecd3e94266efb216c56ed91225d634cb2991c5f3fd1aeab"
dependencies = [
"generic-array 0.14.4",
"subtle",
]
[[package]]
name = "data-encoding"
version = "2.3.2"
@ -750,6 +791,16 @@ dependencies = [
"regex",
]
[[package]]
name = "password-hash"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85d8faea6c018131952a192ee55bd9394c51fc6f63294b668d97636e6f842d40"
dependencies = [
"base64ct",
"rand_core 0.6.2",
]
[[package]]
name = "percent-encoding"
version = "2.1.0"
@ -1098,6 +1149,17 @@ dependencies = [
"libc",
]
[[package]]
name = "simplelog"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59d0fe306a0ced1c88a58042dc22fc2ddd000982c26d75f6aa09a394547c41e0"
dependencies = [
"chrono",
"log",
"termcolor",
]
[[package]]
name = "slab"
version = "0.4.2"
@ -1136,6 +1198,12 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
[[package]]
name = "subtle"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e81da0851ada1f3e9d4312c704aa4f8806f0f9d69faaf8df2f3464b4a9437c2"
[[package]]
name = "syn"
version = "1.0.64"
@ -1183,6 +1251,15 @@ dependencies = [
"unic-segment",
]
[[package]]
name = "termcolor"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4"
dependencies = [
"winapi-util",
]
[[package]]
name = "textwrap"
version = "0.11.0"

@ -7,16 +7,22 @@ edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
argon2 = "0.1"
clap = "2.33.3"
data-encoding = "2.3.2"
tera = "1.7.0"
log = "0.4.14"
pulldown-cmark = { version = "0.8", default-features = false, features = ["simd"] }
rand = "0.8"
serde = { version = "1", features = ["derive"] }
simplelog = "0.10.0"
tera = "1.7.0"
tokio = { version = "1", features = ["full"] }
toml = "0.5"
warp = "0.3"
[dev-dependencies]
tempfile = "3"
[profile.release]
panic = "abort"
lto = true

@ -1,4 +1,7 @@
use std::fs::OpenOptions;
use std::io::prelude::*;
use std::path::Path;
/// TODO Document
pub fn generate_secret(len: usize) -> Result<String, Box<dyn std::error::Error>> {
@ -18,10 +21,45 @@ pub fn generate_secret(len: usize) -> Result<String, Box<dyn std::error::Error>>
}
/// TODO Document
pub fn write_secret<T: Write>(secret: &str, dest: &mut T) -> std::io::Result<()> {
dest.write_all(secret.as_bytes())
pub fn generate_argon2_phc(secret: &str) -> Result<String, Box<dyn std::error::Error>> {
use rand::rngs::OsRng;
use argon2::{Argon2, password_hash::{SaltString, PasswordHasher}};
let secret = secret.as_bytes();
let salt = SaltString::generate(&mut OsRng);
let argon2 = Argon2::default();
let argon2_phc: Result<String, Box<dyn std::error::Error>>;
if let Ok(phc) = argon2.hash_password_simple(secret, salt.as_ref()) {
argon2_phc = Ok(phc.to_string());
} else {
argon2_phc = Err(From::from("Failed to hash password"));
}
argon2_phc
}
#[cfg(target_family = "unix")]
pub fn write_secret_file<P: AsRef<Path>>(secret: &str, dest: P) -> std::io::Result<()> {
use std::os::unix::fs::OpenOptionsExt;
let mut options = OpenOptions::new();
options.create(true);
options.write(true);
options.mode(0o600);
let mut secret_file = options.open(dest)?;
secret_file.write_all(secret.as_bytes())
}
#[cfg(target_family = "windows")]
pub fn write_secret_file<P: AsRef<Path>>(secret: &str, dest: P) -> Result<(), Box<dyn std::error::Error>> {
let mut secret_file = File::create(dest)?;
secret_file.write_all(secret.as_bytes())?;
let metadata = secret_file.metadata()?;
let mut perms = metadata.permissions();
perms.set_readonly(true);
Ok(())
}
pub use data_encoding::BASE32_NOPAD;
#[cfg(test)]
@ -43,11 +81,10 @@ mod tests {
}
#[test]
fn check_write_secret() {
let mut writer: Vec<u8> = vec![];
fn check_argon2_hasher() {
const SECLEN: usize = 32;
let secret = generate_secret(SECLEN).unwrap();
write_secret(&secret, &mut writer).unwrap();
assert_eq!(SECLEN, writer.len())
let phc = generate_argon2_phc(&secret);
assert!(phc.is_ok())
}
}

@ -1,4 +1,4 @@
use std::fs::{create_dir_all, File, OpenOptions};
use std::fs::create_dir_all;
use std::{io::BufRead, usize};
use std::path::Path;
@ -36,7 +36,8 @@ pub fn load() -> Result<AppConfig, Box<dyn std::error::Error>> {
} else if matches.is_present("new") {
let reader = std::io::stdin();
let mut reader = reader.lock();
config = AppConfig::generate(&mut reader);
let current_path = std::env::current_dir()?;
config = AppConfig::generate(current_path, &mut reader);
} else {
let msg = format!("Unable to load configuration");
config = Err(From::from(msg));
@ -66,15 +67,15 @@ fn get_input<R: BufRead>(prompt: &str, reader: &mut R) -> Result<String, Box<dyn
Ok(buf)
}
fn topics_to_vec(topics: &str) -> Vec<String> {
let topics: Vec<String> = topics.split(",")
fn csv_to_vec(csv: &str) -> Vec<String> {
let val_vec: Vec<String> = csv.split(",")
.map(|s| s
.trim_start_matches(char::is_whitespace)
.trim_end_matches(char::is_whitespace)
.to_string())
.collect();
topics
val_vec
}
/// TODO Document
@ -85,11 +86,46 @@ pub struct Blog {
topics: Vec<String>,
}
impl Blog {
fn new_from_input<R: BufRead>(reader: &mut R) -> Result<Blog, Box<dyn std::error::Error>> {
let name = get_input("Please enter a name for the blog: ", reader)?;
let author = get_input("Please enter the blog author's name: ", reader)?;
let topics = get_input("Please enter comma-separated blog topics: ", reader)?;
let topics = csv_to_vec(&topics);
let blog = Blog { name, author, topics };
Ok(blog)
}
}
/// TODO Document
#[derive(Debug, Deserialize, PartialEq)]
pub struct Credentials {
user: String,
password: String,
token: String,
}
impl Credentials {
fn new_from_input<P: AsRef<Path>, R: BufRead>(dir: P, reader: &mut R) -> Result<Credentials, Box<dyn std::error::Error>> {
let user = get_input("Please enter an username for the blog admin: ", reader)?;
const PASSWORD_LEN: usize = 32;
let password = auth::generate_secret(PASSWORD_LEN)?;
let password_file = dir.as_ref().join("admin.pass");
auth::write_secret_file(&password, password_file)?;
let password = auth::generate_argon2_phc(&password)?;
const TOKEN_LEN: usize = 34;
let token = auth::generate_secret(TOKEN_LEN)?;
let token = token.as_bytes();
let token = auth::BASE32_NOPAD.encode(token);
let token_file = dir.as_ref().join("admin.totp");
auth::write_secret_file(&token, token_file)?;
let creds = Credentials { user, password, token };
Ok(creds)
}
}
/// TODO Document
@ -100,14 +136,13 @@ pub struct DocPaths {
}
impl DocPaths {
fn create_paths(&self, blog: &Blog) -> Result<(), Box<dyn std::error::Error>> {
create_dir_all(&self.templates)?;
create_dir_all(format!("{}/static/ext", &self.webroot))?;
for topic in &blog.topics {
create_dir_all(format!("{}/{}/ext", &self.webroot, &topic))?;
create_dir_all(format!("{}/{}/posts", &self.webroot, &topic))?;
}
Ok(())
fn new<P: AsRef<Path>>(dir: P) -> Result<DocPaths, Box<dyn std::error::Error>> {
let dir = dir.as_ref().display();
let templates = format!("{}/blog/templates", dir);
let webroot = format!("{}/blog/webroot", dir);
let docpaths = DocPaths { templates, webroot };
Ok(docpaths)
}
}
@ -134,48 +169,10 @@ impl AppConfig {
Ok(app_config)
}
fn generate<R: BufRead>(reader: &mut R) -> Result<AppConfig, Box<dyn std::error::Error>> {
let current_path = std::env::current_dir()?;
let current_path = current_path.display();
let templates = format!("{}/blog/templates", current_path);
let webroot = format!("{}/blog/webroot", current_path);
let docpaths = DocPaths { templates, webroot };
let name = get_input("Please enter a name for the blog: ", reader)?;
let author = get_input("Please enter the blog author's name: ", reader)?;
let topics = get_input("Please enter comma-separated blog topics: ", reader)?;
let topics = topics_to_vec(&topics);
let blog = Blog { name, author, topics };
docpaths.create_paths(&blog)?;
let user = get_input("Please enter an username for the blog admin: ", reader)?;
const PASSWORD_LEN: usize = 32;
let password = auth::generate_secret(PASSWORD_LEN)?;
let password_file = format!("{}/blog/{}.pass", current_path, user);
let password_file = Path::new(&password_file);
if cfg!(unix) {
use std::os::unix::fs::OpenOptionsExt;
let mut options = OpenOptions::new();
options.create(true);
options.write(true);
options.mode(0o600);
let mut password_file = options.open(password_file)?;
auth::write_secret(&password, &mut password_file)?;
} else {
let mut password_file = File::create(&password_file)?;
auth::write_secret(&password, &mut password_file)?;
let metadata = password_file.metadata()?;
let mut perms = metadata.permissions();
perms.set_readonly(true);
}
println!("Password generated and saved at: {}", password_file.to_path_buf().display());
let creds = Credentials { user, password };
fn generate<P: AsRef<Path>, R: BufRead>(dir: P, reader: &mut R) -> Result<AppConfig, Box<dyn std::error::Error>> {
let docpaths = DocPaths::new(&dir)?;
let blog = Blog::new_from_input(reader)?;
let creds = Credentials::new_from_input(&dir, reader)?;
let level = format!("INFO");
let logging = LogConfig { level };
@ -186,13 +183,33 @@ impl AppConfig {
docpaths,
};
config.create_paths()?;
Ok(config)
}
fn create_paths(&self) -> Result<(), Box<dyn std::error::Error>> {
create_dir_all(&self.docpaths.templates)?;
create_dir_all(format!("{}/static/ext", &self.docpaths.webroot))?;
create_dir_all(format!("{}/main/ext", &self.docpaths.webroot))?;
create_dir_all(format!("{}/main/posts", &self.docpaths.webroot))?;
for topic in &self.blog.topics {
let topic = topic
.to_ascii_lowercase()
.replace(char::is_whitespace, "-");
create_dir_all(format!("{}/{}/ext", &self.docpaths.webroot, &topic))?;
create_dir_all(format!("{}/{}/posts", &self.docpaths.webroot, &topic))?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile;
#[test]
fn build_run_config() {
@ -203,28 +220,42 @@ mod tests {
}
#[test]
fn get_user_input() {
fn build_config_from_input() {
let dir = tempfile::tempdir().unwrap();
// Setup all target fields
let name = format!("Blog Name");
let author = format!("Author Name");
let topics = format!("One, Two, Three, And More");
let user = format!("admin");
let password = format!("MagicPassword");
let level = format!("INFO");
let reference_strings = vec![name, author, topics, user, password, level];
let mut src: &[u8] = b"Blog Name\nAuthor Name\nOne, Two, Three, And More\nadmin\nMagicPassword\nINFO\n";
let mut src: &[u8] = b"Blog Name\nAuthor Name\nOne, Two, Three, And More\nadmin\n";
let config = AppConfig::generate(&dir, &mut src);
assert!(config.is_ok());
for field in reference_strings {
assert_eq!(field, get_input(&field, &mut src).unwrap())
let tmp_dir = &dir.path();
let admin = &tmp_dir.join("admin.pass");
let token = &tmp_dir.join("admin.totp");
let blog = &tmp_dir.join("blog");
let templates = &tmp_dir.join("blog/templates");
let webroot = &tmp_dir.join("blog/webroot");
let static_ext = &tmp_dir.join("blog/webroot/static/ext");
let main_ext = &tmp_dir.join("blog/webroot/main/ext");
let main_posts = &tmp_dir.join("blog/webroot/main/posts");
let one_ext = &tmp_dir.join("blog/webroot/one/ext");
let one_posts = &tmp_dir.join("blog/webroot/one/posts");
let two_ext = &tmp_dir.join("blog/webroot/two/ext");
let two_posts = &tmp_dir.join("blog/webroot/two/posts");
let three_ext = &tmp_dir.join("blog/webroot/three/ext");
let three_posts = &tmp_dir.join("blog/webroot/three/posts");
let and_more_ext = &tmp_dir.join("blog/webroot/and-more/ext");
let and_more_posts = &tmp_dir.join("blog/webroot/and-more/posts");
let core = vec![admin, token, blog, templates, webroot, static_ext, main_ext, main_posts,
one_ext, one_posts, two_ext, two_posts, three_ext, three_posts, and_more_ext, and_more_posts];
for p in core {
assert!(Path::new(p).exists())
}
}
#[test]
fn handle_csv_topics() {
let reference_topics: Vec<String> = vec![format!("One"), format!("Two"), format!("Three"), format!("And More")];
let topics = format!("One, Two, Three, And More");
assert_eq!(reference_topics, topics_to_vec(&topics))
assert_eq!(reference_topics, csv_to_vec(&topics))
}
}

@ -6,6 +6,7 @@ topics = ["one", "two", "three"]
[creds]
user = "admin"
password = "123456"
token = "my very secret token"
[logging]
level = "INFO"

Loading…
Cancel
Save