mirror of
https://github.com/ankitects/anki.git
synced 2025-09-20 15:02:21 -04:00
sync-server: add option to store hashed passwords (#3083)
This commit is contained in:
parent
e911b4b69a
commit
798d9df4de
7 changed files with 115 additions and 10 deletions
|
@ -166,6 +166,7 @@ Pedro Schreiber <schreiber.mmb@gmail.com>
|
||||||
Foxy_null <https://github.com/Foxy-null>
|
Foxy_null <https://github.com/Foxy-null>
|
||||||
Arbyste <arbyste@outlook.com>
|
Arbyste <arbyste@outlook.com>
|
||||||
Vasll <github.com/vasll>
|
Vasll <github.com/vasll>
|
||||||
|
laalsaas <laalsaas@systemli.org>
|
||||||
********************
|
********************
|
||||||
|
|
||||||
The text of the 3 clause BSD license follows:
|
The text of the 3 clause BSD license follows:
|
||||||
|
|
28
Cargo.lock
generated
28
Cargo.lock
generated
|
@ -132,6 +132,7 @@ dependencies = [
|
||||||
"num_cpus",
|
"num_cpus",
|
||||||
"num_enum",
|
"num_enum",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
|
"pbkdf2 0.12.2",
|
||||||
"percent-encoding-iri",
|
"percent-encoding-iri",
|
||||||
"phf 0.11.2",
|
"phf 0.11.2",
|
||||||
"pin-project",
|
"pin-project",
|
||||||
|
@ -3699,6 +3700,17 @@ dependencies = [
|
||||||
"subtle",
|
"subtle",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "password-hash"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166"
|
||||||
|
dependencies = [
|
||||||
|
"base64ct",
|
||||||
|
"rand_core 0.6.4",
|
||||||
|
"subtle",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "paste"
|
name = "paste"
|
||||||
version = "1.0.14"
|
version = "1.0.14"
|
||||||
|
@ -3719,7 +3731,19 @@ checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"digest",
|
"digest",
|
||||||
"hmac",
|
"hmac",
|
||||||
"password-hash",
|
"password-hash 0.4.2",
|
||||||
|
"sha2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pbkdf2"
|
||||||
|
version = "0.12.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2"
|
||||||
|
dependencies = [
|
||||||
|
"digest",
|
||||||
|
"hmac",
|
||||||
|
"password-hash 0.5.0",
|
||||||
"sha2",
|
"sha2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -6881,7 +6905,7 @@ dependencies = [
|
||||||
"crossbeam-utils",
|
"crossbeam-utils",
|
||||||
"flate2",
|
"flate2",
|
||||||
"hmac",
|
"hmac",
|
||||||
"pbkdf2",
|
"pbkdf2 0.11.0",
|
||||||
"sha1",
|
"sha1",
|
||||||
"time",
|
"time",
|
||||||
"zstd 0.11.2+zstd.1.5.2",
|
"zstd 0.11.2+zstd.1.5.2",
|
||||||
|
|
|
@ -96,6 +96,7 @@ num-format = "0.4.4"
|
||||||
num_cpus = "1.16.0"
|
num_cpus = "1.16.0"
|
||||||
num_enum = "0.7.2"
|
num_enum = "0.7.2"
|
||||||
once_cell = "1.19.0"
|
once_cell = "1.19.0"
|
||||||
|
pbkdf2 = { version = "0.12", features = ["simple"] }
|
||||||
phf = { version = "0.11.2", features = ["macros"] }
|
phf = { version = "0.11.2", features = ["macros"] }
|
||||||
pin-project = "1.1.4"
|
pin-project = "1.1.4"
|
||||||
plist = "1.5.1"
|
plist = "1.5.1"
|
||||||
|
|
|
@ -2510,6 +2510,15 @@
|
||||||
"license_file": null,
|
"license_file": null,
|
||||||
"description": "Traits which describe the functionality of password hashing algorithms, as well as a `no_std`-friendly implementation of the PHC string format (a well-defined subset of the Modular Crypt Format a.k.a. MCF)"
|
"description": "Traits which describe the functionality of password hashing algorithms, as well as a `no_std`-friendly implementation of the PHC string format (a well-defined subset of the Modular Crypt Format a.k.a. MCF)"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "password-hash",
|
||||||
|
"version": "0.5.0",
|
||||||
|
"authors": "RustCrypto Developers",
|
||||||
|
"repository": "https://github.com/RustCrypto/traits/tree/master/password-hash",
|
||||||
|
"license": "Apache-2.0 OR MIT",
|
||||||
|
"license_file": null,
|
||||||
|
"description": "Traits which describe the functionality of password hashing algorithms, as well as a `no_std`-friendly implementation of the PHC string format (a well-defined subset of the Modular Crypt Format a.k.a. MCF)"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "paste",
|
"name": "paste",
|
||||||
"version": "1.0.14",
|
"version": "1.0.14",
|
||||||
|
@ -2528,6 +2537,15 @@
|
||||||
"license_file": null,
|
"license_file": null,
|
||||||
"description": "Generic implementation of PBKDF2"
|
"description": "Generic implementation of PBKDF2"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "pbkdf2",
|
||||||
|
"version": "0.12.2",
|
||||||
|
"authors": "RustCrypto Developers",
|
||||||
|
"repository": "https://github.com/RustCrypto/password-hashes/tree/master/pbkdf2",
|
||||||
|
"license": "Apache-2.0 OR MIT",
|
||||||
|
"license_file": null,
|
||||||
|
"description": "Generic implementation of PBKDF2"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "percent-encoding",
|
"name": "percent-encoding",
|
||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
|
|
|
@ -73,6 +73,7 @@ nom.workspace = true
|
||||||
num_cpus.workspace = true
|
num_cpus.workspace = true
|
||||||
num_enum.workspace = true
|
num_enum.workspace = true
|
||||||
once_cell.workspace = true
|
once_cell.workspace = true
|
||||||
|
pbkdf2.workspace = true
|
||||||
percent-encoding-iri.workspace = true
|
percent-encoding-iri.workspace = true
|
||||||
phf.workspace = true
|
phf.workspace = true
|
||||||
pin-project.workspace = true
|
pin-project.workspace = true
|
||||||
|
|
|
@ -22,6 +22,11 @@ use anki_io::create_dir_all;
|
||||||
use axum::extract::DefaultBodyLimit;
|
use axum::extract::DefaultBodyLimit;
|
||||||
use axum::Router;
|
use axum::Router;
|
||||||
use axum_client_ip::SecureClientIpSource;
|
use axum_client_ip::SecureClientIpSource;
|
||||||
|
use pbkdf2::password_hash::PasswordHash;
|
||||||
|
use pbkdf2::password_hash::PasswordHasher;
|
||||||
|
use pbkdf2::password_hash::PasswordVerifier;
|
||||||
|
use pbkdf2::password_hash::SaltString;
|
||||||
|
use pbkdf2::Pbkdf2;
|
||||||
use snafu::whatever;
|
use snafu::whatever;
|
||||||
use snafu::OptionExt;
|
use snafu::OptionExt;
|
||||||
use snafu::ResultExt;
|
use snafu::ResultExt;
|
||||||
|
@ -91,9 +96,26 @@ impl SimpleServerInner {
|
||||||
match std::env::var(&envvar) {
|
match std::env::var(&envvar) {
|
||||||
Ok(val) => {
|
Ok(val) => {
|
||||||
let hkey = derive_hkey(&val);
|
let hkey = derive_hkey(&val);
|
||||||
let (name, _) = val.split_once(':').with_whatever_context(|| {
|
let (name, pwhash) = {
|
||||||
format!("{envvar} should be in 'username:password' format.")
|
let (name, password) = val.split_once(':').with_whatever_context(|| {
|
||||||
})?;
|
format!("{envvar} should be in 'username:password' format.")
|
||||||
|
})?;
|
||||||
|
if std::env::var("PASSWORDS_HASHED").is_ok() {
|
||||||
|
(name, password.to_string())
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
name,
|
||||||
|
// Plain text passwords provided; hash them with a fixed salt.
|
||||||
|
Pbkdf2
|
||||||
|
.hash_password(
|
||||||
|
password.as_bytes(),
|
||||||
|
&SaltString::from_b64("tonuvYGpksNFQBlEmm3lxg").unwrap(),
|
||||||
|
)
|
||||||
|
.expect("couldn't hash password")
|
||||||
|
.to_string(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
};
|
||||||
let folder = base_folder.join(name);
|
let folder = base_folder.join(name);
|
||||||
create_dir_all(&folder).whatever_context("creating SYNC_BASE")?;
|
create_dir_all(&folder).whatever_context("creating SYNC_BASE")?;
|
||||||
let media =
|
let media =
|
||||||
|
@ -102,6 +124,7 @@ impl SimpleServerInner {
|
||||||
hkey,
|
hkey,
|
||||||
User {
|
User {
|
||||||
name: name.into(),
|
name: name.into(),
|
||||||
|
password_hash: pwhash,
|
||||||
col: None,
|
col: None,
|
||||||
sync_state: None,
|
sync_state: None,
|
||||||
media,
|
media,
|
||||||
|
@ -150,11 +173,47 @@ impl SimpleServer {
|
||||||
request: HostKeyRequest,
|
request: HostKeyRequest,
|
||||||
) -> HttpResult<SyncResponse<HostKeyResponse>> {
|
) -> HttpResult<SyncResponse<HostKeyResponse>> {
|
||||||
let state = self.state.lock().unwrap();
|
let state = self.state.lock().unwrap();
|
||||||
let key = derive_hkey(&format!("{}:{}", request.username, request.password));
|
|
||||||
if state.users.contains_key(&key) {
|
// This control structure might seem a bit crude,
|
||||||
SyncResponse::try_from_obj(HostKeyResponse { key })
|
// its goal is to prevent a timing attack from gaining
|
||||||
} else {
|
// information about whether a specific user exists.
|
||||||
None.or_forbidden("invalid user/pass in get_host_key")
|
let user = {
|
||||||
|
// This inner block returns Ok(hkey,user) if a user with corresponding
|
||||||
|
// name is found and Err(user) with a random user if it isn't found.
|
||||||
|
// The user is needed to verify against a random hash,
|
||||||
|
// before returning an Error.
|
||||||
|
let mut result: Result<(String, &User), &User> =
|
||||||
|
Err(state.users.iter().next().unwrap().1);
|
||||||
|
for (hkey, user) in state.users.iter() {
|
||||||
|
if user.name == request.username {
|
||||||
|
result = Ok((hkey.to_string(), user));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result
|
||||||
|
};
|
||||||
|
|
||||||
|
match user {
|
||||||
|
Ok((key, user)) => {
|
||||||
|
// Verify password
|
||||||
|
let pwhash =
|
||||||
|
&PasswordHash::new(&user.password_hash).expect("couldn't parse password hash");
|
||||||
|
if Pbkdf2
|
||||||
|
.verify_password(request.password.as_bytes(), pwhash)
|
||||||
|
.is_ok()
|
||||||
|
{
|
||||||
|
SyncResponse::try_from_obj(HostKeyResponse { key })
|
||||||
|
} else {
|
||||||
|
None.or_forbidden("invalid user/pass in get_host_key")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(user) => {
|
||||||
|
// Verify random password, in order to ensure constant-timedness,
|
||||||
|
// then return an error
|
||||||
|
let pwhash =
|
||||||
|
&PasswordHash::new(&user.password_hash).expect("couldn't parse password hash");
|
||||||
|
let _ = Pbkdf2.verify_password(request.password.as_bytes(), pwhash);
|
||||||
|
None.or_forbidden("invalid user/pass in get_host_key")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,7 @@ use crate::sync::http_server::media_manager::ServerMediaManager;
|
||||||
|
|
||||||
pub(in crate::sync) struct User {
|
pub(in crate::sync) struct User {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
pub password_hash: String,
|
||||||
pub col: Option<Collection>,
|
pub col: Option<Collection>,
|
||||||
pub sync_state: Option<ServerSyncState>,
|
pub sync_state: Option<ServerSyncState>,
|
||||||
pub media: ServerMediaManager,
|
pub media: ServerMediaManager,
|
||||||
|
|
Loading…
Reference in a new issue