Guard the gRPC authority login and register paths
This commit is contained in:
parent
6149a15b34
commit
eea6e30f20
2 changed files with 49 additions and 1 deletions
|
|
@ -58,6 +58,11 @@ impl Engine {
|
||||||
|
|
||||||
pub fn authority_register(&mut self, name: &str, creds: Option<db::Credentials>, email: Option<String>) -> AuthorityStatus {
|
pub fn authority_register(&mut self, name: &str, creds: Option<db::Credentials>, email: Option<String>) -> AuthorityStatus {
|
||||||
let Some(creds) = creds else { return AuthorityStatus::Internal };
|
let Some(creds) = creds else { return AuthorityStatus::Internal };
|
||||||
|
// Re-check the freeze pre_check saw: defcon may have frozen registrations
|
||||||
|
// during the ~1s off-lock derivation between the two calls.
|
||||||
|
if self.db.registrations_frozen() || self.db.readonly() {
|
||||||
|
return AuthorityStatus::Invalid;
|
||||||
|
}
|
||||||
// The IRC REGISTER rejects a FORBIDden email; a website write must too.
|
// The IRC REGISTER rejects a FORBIDden email; a website write must too.
|
||||||
if let Some(addr) = &email {
|
if let Some(addr) = &email {
|
||||||
if self.db.is_forbidden(echo_api::ForbidKind::Email, addr).is_some() {
|
if self.db.is_forbidden(echo_api::ForbidKind::Email, addr).is_some() {
|
||||||
|
|
@ -89,6 +94,28 @@ impl Engine {
|
||||||
self.db.scram_lookup(name, "SCRAM-SHA-256").map(|(a, v)| (a.to_string(), v.to_string()))
|
self.db.scram_lookup(name, "SCRAM-SHA-256").map(|(a, v)| (a.to_string(), v.to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// gRPC login pre-check: apply the same guards the IRC IDENTIFY/SASL paths do
|
||||||
|
/// before the ~1s verify. Refuses a suspended or throttled account — the
|
||||||
|
/// website only sees a plain failure, so it can't tell these from a bad
|
||||||
|
/// password — and otherwise returns the canonical name + verifier to check
|
||||||
|
/// off the engine lock. Feed the result back through [`Self::authority_note_auth`].
|
||||||
|
pub fn authority_auth_begin(&self, name: &str) -> Option<(String, String)> {
|
||||||
|
if let Some(acc) = self.db.resolve_account(name) {
|
||||||
|
if self.db.is_suspended(acc) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if self.db.auth_lockout(name).is_some() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
self.scram_verifier(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Record a gRPC login attempt in the brute-force backoff, exactly like IDENTIFY.
|
||||||
|
pub fn authority_note_auth(&mut self, name: &str, ok: bool) {
|
||||||
|
self.db.note_auth(name, ok);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn authority_set_password(&mut self, account: &str, creds: Option<db::Credentials>) -> AuthorityStatus {
|
pub fn authority_set_password(&mut self, account: &str, creds: Option<db::Credentials>) -> AuthorityStatus {
|
||||||
let Some(creds) = creds else { return AuthorityStatus::Internal };
|
let Some(creds) = creds else { return AuthorityStatus::Internal };
|
||||||
// set_credentials's only Err is Internal, which here always means "no such account".
|
// set_credentials's only Err is Internal, which here always means "no such account".
|
||||||
|
|
|
||||||
23
src/grpc.rs
23
src/grpc.rs
|
|
@ -302,7 +302,7 @@ impl Accounts for AccountsService {
|
||||||
// Fetch the verifier under the lock (cheap), then run the ~1s PBKDF2 on a
|
// Fetch the verifier under the lock (cheap), then run the ~1s PBKDF2 on a
|
||||||
// blocking thread — never hold the engine lock across it, or every login
|
// blocking thread — never hold the engine lock across it, or every login
|
||||||
// freezes the whole daemon (IRC link, gossip, all RPCs).
|
// freezes the whole daemon (IRC link, gossip, all RPCs).
|
||||||
let Some((account, verifier)) = self.engine.lock().await.scram_verifier(&msg.name) else {
|
let Some((account, verifier)) = self.engine.lock().await.authority_auth_begin(&msg.name) else {
|
||||||
return Ok(Response::new(AuthenticateReply { status: PbStatus::Invalid as i32, account: String::new() }));
|
return Ok(Response::new(AuthenticateReply { status: PbStatus::Invalid as i32, account: String::new() }));
|
||||||
};
|
};
|
||||||
let password = msg.password;
|
let password = msg.password;
|
||||||
|
|
@ -311,6 +311,9 @@ impl Accounts for AccountsService {
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
|
// Feed the brute-force backoff just like IDENTIFY, so the website login
|
||||||
|
// can't be used as an unthrottled password oracle against echo.
|
||||||
|
self.engine.lock().await.authority_note_auth(&msg.name, ok);
|
||||||
if ok {
|
if ok {
|
||||||
Ok(Response::new(AuthenticateReply { status: PbStatus::Ok as i32, account }))
|
Ok(Response::new(AuthenticateReply { status: PbStatus::Ok as i32, account }))
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -621,6 +624,24 @@ mod tests {
|
||||||
assert_eq!(bad.status, PbStatus::Invalid as i32);
|
assert_eq!(bad.status, PbStatus::Invalid as i32);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The website login feeds the same brute-force backoff as IRC IDENTIFY, so it
|
||||||
|
// can't be used as an unthrottled password oracle: after enough wrong guesses
|
||||||
|
// even the correct password is refused while the lockout holds.
|
||||||
|
#[tokio::test]
|
||||||
|
async fn authenticate_throttles_repeated_failures() {
|
||||||
|
let (engine, _tx) = engine_with("acct-throttle");
|
||||||
|
let svc = accounts_svc(engine);
|
||||||
|
svc.register(authed(RegisterRequest { name: "carol".into(), password: "hunter2".into(), email: String::new() }, "t")).await.unwrap();
|
||||||
|
|
||||||
|
for _ in 0..10 {
|
||||||
|
let out = svc.authenticate(authed(AuthenticateRequest { name: "carol".into(), password: "nope".into() }, "t")).await.unwrap().into_inner();
|
||||||
|
assert_eq!(out.status, PbStatus::Invalid as i32);
|
||||||
|
}
|
||||||
|
// Correct password, but the account is now locked out.
|
||||||
|
let locked = svc.authenticate(authed(AuthenticateRequest { name: "carol".into(), password: "hunter2".into() }, "t")).await.unwrap().into_inner();
|
||||||
|
assert_eq!(locked.status, PbStatus::Invalid as i32, "throttled even with the right password");
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn provision_creates_account_from_verifiers() {
|
async fn provision_creates_account_from_verifiers() {
|
||||||
let (engine, _tx) = engine_with("acct-provision");
|
let (engine, _tx) = engine_with("acct-provision");
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue