Compare commits

...

2 commits

Author SHA1 Message Date
fef
64e670480a
add bearcap utility 2022-12-20 14:00:41 +01:00
fef
4e9a09f74b
refactor password hash API 2022-12-20 13:24:08 +01:00
7 changed files with 221 additions and 7 deletions

10
Cargo.lock generated
View file

@ -1134,6 +1134,7 @@ dependencies = [
"pretty_env_logger",
"serde",
"serde_json",
"serde_test",
"sqlx",
"thiserror",
"tokio",
@ -1445,6 +1446,15 @@ dependencies = [
"serde",
]
[[package]]
name = "serde_test"
version = "1.0.151"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8f77be7305dac4f250891d2f7444276315f3c288176d35746b6a4ca786dacb3"
dependencies = [
"serde",
]
[[package]]
name = "serde_urlencoded"
version = "0.7.1"

View file

@ -16,6 +16,7 @@ log = "0.4"
pretty_env_logger = "0.4"
serde = { version = "1.0", features = [ "derive" ] }
serde_json = "1.0"
serde_test = "1.0"
sqlx = { version = "0.6", features = [ "chrono", "runtime-actix-rustls", "postgres" ] }
thiserror = "1.0"
tokio = "1.23"

View file

@ -8,6 +8,7 @@ use std::{fmt, io};
use crate::util::validate;
pub use log::{debug, error, info, trace, warn};
use serde::ser::SerializeMap;
pub use crate::util::validate::{Insane, Sane};
@ -24,6 +25,7 @@ pub enum Error {
MalformedHeader(header::ToStrError),
BadToken(jsonwebtoken::errors::Error),
BadCredentials,
BadBearcap,
}
pub fn utc_now() -> NaiveDateTime {
@ -33,9 +35,7 @@ pub fn utc_now() -> NaiveDateTime {
pub fn unix_now() -> i64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect(
"You've either broken spacetime, or your system clock is a bit off.",
)
.expect("You've either broken spacetime, or your system clock is a bit off.")
.as_secs() as i64
}
@ -47,6 +47,7 @@ impl ResponseError for Error {
Error::MalformedHeader(_) => StatusCode::BAD_REQUEST,
Error::BadToken(_) => StatusCode::UNAUTHORIZED,
Error::BadCredentials => StatusCode::UNAUTHORIZED,
Error::BadBearcap => StatusCode::UNPROCESSABLE_ENTITY,
_ => StatusCode::INTERNAL_SERVER_ERROR,
}
}
@ -66,6 +67,7 @@ impl fmt::Display for Error {
Error::MalformedHeader(to_str_error) => to_str_error.fmt(f),
Error::BadToken(jwt_error) => jwt_error.fmt(f),
Error::BadCredentials => write!(f, "Invalid user name or password"),
Error::BadBearcap => write!(f, "Invalid bearcap URL"),
}
}
}
@ -108,6 +110,8 @@ impl Serialize for Error {
where
S: Serializer,
{
serializer.serialize_str(format!("{}", self).as_str())
let mut fields = serializer.serialize_map(Some(1))?;
fields.serialize_entry("msg", format!("{}", self).as_str())?;
fields.end()
}
}

View file

@ -31,7 +31,7 @@ async fn auth(body: web::Json<AuthRequest>, state: AppState) -> Result<HttpRespo
Error::NotFound => Error::BadCredentials,
e => e,
})?;
password::verify(&body.password, &user.password).map_err(|_| Error::BadCredentials)?;
password::verify(body.password.as_str(), user.password.as_str())?;
let account = state.repo.accounts.by_id(user.account_id).await?;
let token = token::issue(&state, &account)?;
info!(target: "auth", "Successful login for user {}", &account.name);

185
src/util/bear.rs Normal file
View file

@ -0,0 +1,185 @@
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
use std::fmt;
use std::iter::Peekable;
use std::str::Chars;
use crate::core::*;
const MAX_PARAM_LEN: usize = 16384;
/// Bearer Capabilities URI, see <https://docs.joinmastodon.org/spec/bearcaps/>
#[derive(PartialEq, Debug)]
pub struct Bearcap {
pub url: String,
pub token: String,
}
struct MaybeBearcap {
url: Option<String>,
token: Option<String>,
}
impl TryFrom<&str> for Bearcap {
type Error = Error;
fn try_from(v: &str) -> Result<Self> {
const BEAR_PREFIX: &'static str = "bear:";
if !v.starts_with(BEAR_PREFIX) {
return Err(Error::BadBearcap);
}
let v = &v[BEAR_PREFIX.len()..];
let mut params = MaybeBearcap {
url: None,
token: None,
};
let mut chars = v.chars().peekable();
for _ in 0..2 {
match chars.next() {
Some('?' | '&') => parse_param(&mut params, &mut chars)?,
None => break,
_ => return Err(Error::BadBearcap),
}
}
params.try_into()
}
}
fn parse_param(params: &mut MaybeBearcap, chars: &mut Peekable<Chars>) -> Result<()> {
match chars.next() {
Some('t') => params.token = Some(parse_param_val(chars)?),
Some('u') => params.url = Some(parse_param_val(chars)?),
_ => return Err(Error::BadBearcap),
}
Ok(())
}
fn parse_param_val(chars: &mut Peekable<Chars>) -> Result<String> {
if chars.next() != Some('=') {
return Err(Error::BadBearcap);
}
let mut val = String::new();
for _ in 0..MAX_PARAM_LEN {
let c = chars.peek().map(|c| *c);
match c {
Some('&') | None => return Ok(val),
Some(c) => {
chars.next();
val.push(c);
}
}
}
warn!(
"Rejecting bearcap with param longer than {} chars",
MAX_PARAM_LEN
);
Err(Error::BadBearcap)
}
impl TryFrom<MaybeBearcap> for Bearcap {
type Error = Error;
fn try_from(v: MaybeBearcap) -> Result<Self> {
if let (Some(token), Some(url)) = (v.token, v.url) {
if token.len() > 0 && url.len() > 0 {
return Ok(Bearcap { token, url });
}
}
Err(Error::BadBearcap)
}
}
impl Serialize for Bearcap {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
let s = format!("bear:?t={}&u={}", self.token, self.url);
serializer.serialize_str(s.as_str())
}
}
impl<'de> Deserialize<'de> for Bearcap {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_str(BearcapVisitor)
}
}
struct BearcapVisitor;
impl<'de> de::Visitor<'de> for BearcapVisitor {
type Value = Bearcap;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("a valid bearcap URI")
}
fn visit_str<E>(self, value: &str) -> std::result::Result<Self::Value, E>
where
E: de::Error,
{
match Bearcap::try_from(value) {
Ok(bc) => Ok(bc),
Err(_) => Err(E::custom("Expected a valid bearcap URI")),
}
}
}
#[cfg(test)]
mod tests {
use crate::util::bear::Bearcap;
use serde_test::{assert_tokens, Token};
#[test]
fn parse_vali() {
let bc = Bearcap::try_from("bear:?t=asdf&u=https://example.com/test")
.expect("Doesn't parse valid bearcap");
assert_eq!(bc.url, String::from("https://example.com/test"));
assert_eq!(bc.token, String::from("asdf"));
}
#[test]
fn serialize() {
let bc = Bearcap {
token: String::from("asdf"),
url: String::from("https://example.com/test"),
};
assert_tokens(
&bc,
&[Token::Str("bear:?t=asdf&u=https://example.com/test")],
);
}
#[test]
fn reject_without_url() {
let result = Bearcap::try_from("bear:?t=asdf");
assert!(result.is_err());
}
#[test]
fn reject_without_token() {
let result = Bearcap::try_from("bear:?u=https://example.com/test");
assert!(result.is_err());
}
#[test]
fn reject_with_empty_token() {
let result = Bearcap::try_from("bear:?t=&u=https://example.com/test");
assert!(result.is_err());
}
#[test]
fn reject_with_empty_url() {
let result = Bearcap::try_from("bear:?t=asdf&u=");
assert!(result.is_err());
}
}

View file

@ -1,3 +1,4 @@
pub mod bear;
pub mod password;
pub mod token;
pub mod validate;

View file

@ -4,7 +4,7 @@ use argon2::{Argon2, PasswordHash, PasswordHasher, PasswordVerifier};
use crate::core::*;
pub fn hash(clear: &String) -> String {
pub fn hash(clear: &str) -> String {
let salt = SaltString::generate(&mut OsRng);
let argon2 = Argon2::default();
argon2
@ -13,10 +13,23 @@ pub fn hash(clear: &String) -> String {
.to_string()
}
pub fn verify(clear: &String, hash: &String) -> Result<()> {
pub fn verify(clear: &str, hash: &str) -> Result<()> {
let parsed_hash = PasswordHash::new(&hash).unwrap();
match Argon2::default().verify_password(clear.as_bytes(), &parsed_hash) {
Ok(_) => Ok(()),
Err(_) => Err(Error::BadCredentials),
}
}
#[cfg(test)]
mod tests {
use crate::util::password::{hash, verify};
#[test]
fn validate_hashes() {
let h = hash("asdf");
assert!(verify("asdf", h.as_str()).is_ok());
assert!(verify("fdsa", h.as_str()).is_err());
assert!(verify("asdf\0", h.as_str()).is_err());
}
}