Compare commits
2 commits
271a912879
...
64e670480a
Author | SHA1 | Date | |
---|---|---|---|
64e670480a | |||
4e9a09f74b |
7 changed files with 221 additions and 7 deletions
10
Cargo.lock
generated
10
Cargo.lock
generated
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
185
src/util/bear.rs
Normal 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());
|
||||
}
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
pub mod bear;
|
||||
pub mod password;
|
||||
pub mod token;
|
||||
pub mod validate;
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue