Skip to main content

yo_resp/dispatch/
auth.rs

1//! `AUTH`, the one password behind it, and the gate every other command passes.
2//!
3//! # One password and no users
4//!
5//! A real server has an ACL system with users, rules and per command
6//! permissions, and `requirepass` is a thin layer over it: setting it gives the
7//! user called `default` that password, and clearing it puts the `nopass` flag
8//! back. There is no ACL here yet, so this file is the other half of that
9//! sentence written on its own. There is one password, it belongs to a user
10//! called `default`, and any other user name is refused the way a real server
11//! refuses a name it has never heard of.
12//!
13//! That means every message a client can see is the message a real server
14//! sends, and the day the ACL arrives this becomes what it already looks like:
15//! the default user's password.
16//!
17//! # Who starts out let in
18//!
19//! A connection carries a flag saying whether it has authenticated, and the
20//! flag is decided when the connection is accepted rather than when it sends
21//! its first command. A connection accepted while no password is set is let in
22//! at once, because the default user is `nopass` and there is nothing to ask
23//! it for, and it stays let in if a password is set later. A connection
24//! accepted while a password is set has to send `AUTH` first.
25//!
26//! That is a real server's rule and it is worth being clear about, because it
27//! is not the rule anybody would guess. `CONFIG SET requirepass` does not lock
28//! out the clients that are already connected, including the one that just set
29//! it, and it does lock out every client that connects after it.
30//!
31//! `RESET` puts the connection back to how it was accepted, and that includes
32//! this: a connection that authenticated and then sent `RESET` has to
33//! authenticate again on a server with a password, and does not on a server
34//! without one.
35//!
36//! # Why the compare is written out
37//!
38//! Comparing two passwords with `==` gives away how much of the guess was right
39//! by how long the compare took, and a wrong guess that took longer is a wrong
40//! guess that got further. So the compare here looks at every byte of both
41//! whatever it finds, and folds the lengths in rather than returning early on
42//! them, which is what a real server does with the hashes it keeps.
43//!
44//! What is not done here is the hashing. A real server keeps a SHA-256 of the
45//! password and never the password, and the reason that matters is `ACL
46//! GETUSER` and the config file rewrite, neither of which exists yet. It is
47//! written down in D-127 rather than half done.
48
49use std::sync::Mutex;
50use std::sync::atomic::AtomicBool;
51use std::sync::atomic::Ordering::{Acquire, Release};
52
53use yo_common::{Code, Error, Result};
54
55use super::args::{self, Args, is};
56use super::{Server, Session};
57use crate::reply::Out;
58
59/// The one line a command refused for want of a password is answered with.
60///
61/// The whole line and not the part after the code, because it goes two places:
62/// straight into the reply, and spliced into the `EXECABORT` an `EXEC` gets, and
63/// the reference puts the code in both.
64pub(super) const NOAUTH: &str = "NOAUTH Authentication required.";
65
66/// The line `HELLO` gets instead, which says what to do about it.
67///
68/// A client that speaks RESP3 has to send `HELLO` before it can send `AUTH`, or
69/// it would be speaking RESP2 by the time it authenticated, so the reference
70/// spends a sentence here pointing at the option that solves it.
71pub(super) const HELLO_NOAUTH: &str = "NOAUTH HELLO must be called with the client already authenticated, otherwise the HELLO <proto> AUTH <user> <pass> option can be used to authenticate the client and select the RESP protocol version at the same time";
72
73/// The only user there is.
74const DEFAULT_USER: &[u8] = b"default";
75
76/// The password the default user has, if it has one.
77///
78/// The flag is separate from the password rather than read out of it because
79/// every command on the server asks the question once, and on nearly every
80/// server the answer is that there is no password. That is one relaxed load
81/// against a word that is already warm, instead of a lock.
82#[derive(Debug, Default)]
83pub(crate) struct Access {
84    /// Whether a password is set at all, which is the hot half.
85    on: AtomicBool,
86    /// The password itself, which only `AUTH` and `CONFIG` ever look at.
87    secret: Mutex<Vec<u8>>,
88}
89
90impl Server {
91    /// Whether this server asks connections for a password.
92    #[must_use]
93    pub(crate) fn guarded(&self) -> bool {
94        self.access.on.load(Acquire)
95    }
96
97    /// Set or clear the password, where an empty one clears it.
98    ///
99    /// The flag is written after the password on the way in and before it on
100    /// the way out, so a thread that sees the flag on always sees the password
101    /// that goes with it, and a thread that catches the clear half done reads
102    /// the flag as still on and asks for a password that is about to stop being
103    /// needed. Being asked once more than necessary is the safe half of that
104    /// race and the other order does not have a safe half.
105    pub fn set_password(&self, password: &[u8]) {
106        let mut held = self.access.secret.lock().unwrap_or_else(|e| e.into_inner());
107        if password.is_empty() {
108            self.access.on.store(false, Release);
109            held.clear();
110            return;
111        }
112        held.clear();
113        held.extend_from_slice(password);
114        self.access.on.store(true, Release);
115    }
116
117    /// Hand the password to `f`, which is how `CONFIG GET` writes it out.
118    ///
119    /// Borrowed rather than copied, because the one caller writes it straight
120    /// into a reply buffer and a copy would be a second place a password lives.
121    pub(crate) fn with_password<T>(&self, f: impl FnOnce(&[u8]) -> T) -> T {
122        let held = self.access.secret.lock().unwrap_or_else(|e| e.into_inner());
123        f(&held)
124    }
125
126    /// Whether `guess` is the password, compared without giving away how much
127    /// of it was right.
128    fn password_is(&self, guess: &[u8]) -> bool {
129        self.with_password(|real| same(real, guess))
130    }
131}
132
133/// Whether two byte strings are equal, in time that does not depend on where
134/// they stop being equal.
135///
136/// The loop runs over the longer of the two and folds a byte that is not there
137/// in as a difference, so neither the contents nor the length is readable from
138/// how long this took. Returning early on the length would give away the length,
139/// which on a password is a real thing to give away.
140fn same(a: &[u8], b: &[u8]) -> bool {
141    let mut diff = u8::from(a.len() != b.len());
142    for i in 0..a.len().max(b.len()) {
143        diff |= a.get(i).copied().unwrap_or(0) ^ b.get(i).copied().unwrap_or(0xff);
144    }
145    diff == 0
146}
147
148/// Whether this user and password get in, without saying anything to the client.
149///
150/// Shared by `AUTH` and by `HELLO`'s `AUTH` option, which take the same pair and
151/// make the same decision about it. The one thing they do not share is what an
152/// unguarded server says to a one argument `AUTH`, which is `AUTH`'s own problem
153/// because `HELLO` has no one argument form.
154pub(super) fn admits(server: &Server, user: &[u8], password: &[u8]) -> bool {
155    if !is(user, DEFAULT_USER) {
156        // Nobody else exists, on a server with a password and on a server
157        // without one. A real server answers the same thing for a user it has
158        // never heard of and a user whose password was wrong, on purpose: the
159        // difference between the two is a list of user names.
160        return false;
161    }
162    // No password set means the default user is `nopass`, and a `nopass` user
163    // takes any password at all rather than refusing every one of them.
164    !server.guarded() || server.password_is(password)
165}
166
167/// `AUTH password` or `AUTH username password`.
168pub(super) fn execute(
169    server: &Server,
170    session: &mut Session,
171    args: Args<'_>,
172    out: &mut Out,
173) -> Result<()> {
174    // Arity is a minimum of two, so a third argument is where this stops being
175    // a command it knows and the reference calls that a syntax error rather
176    // than a wrong number of arguments.
177    if args.len() > 3 {
178        return Err(args::syntax());
179    }
180    let (user, password) = if args.len() == 3 {
181        (args.get(1), args.get(2))
182    } else {
183        (DEFAULT_USER, args.get(1))
184    };
185    if args.len() == 2 && !server.guarded() && is(user, DEFAULT_USER) {
186        // The one message here that is not about the password being wrong. A
187        // client that sends a one argument `AUTH` to a server with no password
188        // has almost certainly connected to the wrong server, so the reference
189        // says so at length rather than letting it through.
190        return Err(Error::new(
191            Code::Invalid,
192            "AUTH <password> called without any password configured for the default user. Are you sure your configuration is correct?",
193        ));
194    }
195    if !admits(server, user, password) {
196        // Written straight into the buffer, because the code in front of it is
197        // what a client branches on and this is the only place in the engine
198        // that sends it. The same reason `NOPROTO` is written where it is
199        // decided.
200        out.error(b"WRONGPASS invalid username-password pair or user is disabled.");
201        return Ok(());
202    }
203    session.admit(true);
204    out.ok();
205    Ok(())
206}
207
208#[cfg(test)]
209mod tests {
210    use super::same;
211
212    /// The compare says the same thing `==` says, for everything `==` is asked.
213    ///
214    /// Constant time is the point of it and constant time is not a thing a test
215    /// can assert without a clock and a lot of runs, so what is pinned here is
216    /// that being careful about the timing did not change any answer. The empty
217    /// pair is in because the sentinel the loop folds in for a byte that is not
218    /// there has to differ from the one on the other side, and two empty strings
219    /// are the case that catches getting that backwards.
220    #[test]
221    fn the_careful_compare_agrees_with_the_ordinary_one() {
222        let words: [&[u8]; 9] = [
223            b"",
224            b"a",
225            b"b",
226            b"ab",
227            b"ba",
228            b"hunter2",
229            b"hunter3",
230            b"hunter22",
231            b"\x00",
232        ];
233        for a in words {
234            for b in words {
235                assert_eq!(same(a, b), a == b, "{a:?} against {b:?}");
236            }
237        }
238    }
239}