sasl/client/mechanisms/
scram.rs

1//! Provides the SASL "SCRAM-*" mechanisms and a way to implement more.
2
3use base64::{engine::general_purpose::STANDARD as Base64, Engine};
4
5use crate::client::{Mechanism, MechanismError};
6use crate::common::scram::{generate_nonce, ScramProvider};
7use crate::common::{parse_frame, xor, ChannelBinding, Credentials, Identity, Password, Secret};
8
9use crate::error::Error;
10
11use alloc::format;
12use alloc::string::String;
13use alloc::vec::Vec;
14use core::marker::PhantomData;
15
16enum ScramState {
17    Init,
18    SentInitialMessage {
19        initial_message: Vec<u8>,
20        gs2_header: Vec<u8>,
21    },
22    GotServerData {
23        server_signature: Vec<u8>,
24    },
25}
26
27/// A struct for the SASL SCRAM-* and SCRAM-*-PLUS mechanisms.
28pub struct Scram<S: ScramProvider> {
29    name: String,
30    name_plus: String,
31    username: String,
32    password: Password,
33    client_first_extensions: String,
34    client_final_extensions: String,
35    client_nonce: String,
36    state: ScramState,
37    channel_binding: ChannelBinding,
38    _marker: PhantomData<S>,
39}
40
41impl<S: ScramProvider> Scram<S> {
42    /// Constructs a new struct for authenticating using the SASL SCRAM-* and SCRAM-*-PLUS
43    /// mechanisms, depending on the passed channel binding.
44    ///
45    /// It is recommended that instead you use a `Credentials` struct and turn it into the
46    /// requested mechanism using `from_credentials`.
47    pub fn new<N: Into<String>, P: Into<Password>>(
48        username: N,
49        password: P,
50        channel_binding: ChannelBinding,
51    ) -> Result<Scram<S>, Error> {
52        Ok(Scram {
53            name: format!("SCRAM-{}", S::name()),
54            name_plus: format!("SCRAM-{}-PLUS", S::name()),
55            username: username.into(),
56            password: password.into(),
57            client_first_extensions: String::new(),
58            client_final_extensions: String::new(),
59            client_nonce: generate_nonce()?,
60            state: ScramState::Init,
61            channel_binding,
62            _marker: PhantomData,
63        })
64    }
65
66    /// Sets extension data to be inserted into the client's first message.
67    /// Extension data must be in the format of a comma separated list of SCRAM extensions to be used e.g. `foo=true,bar=baz`
68    /// If not called, no extensions will be used for the clients first message.
69    pub fn with_first_extensions(mut self, extensions: String) -> Self {
70        self.client_first_extensions = extensions;
71        self
72    }
73
74    /// Sets extension data to be inserted into the client's final message.
75    /// Extension data must be in the format of a comma separated list of SCRAM extensions to be used e.g. `foo=true,bar=baz`
76    /// If not called, no extensions will be used for the clients final message.
77    pub fn with_final_extensions(mut self, extensions: String) -> Self {
78        self.client_final_extensions = extensions;
79        self
80    }
81
82    // Used for testing.
83    #[doc(hidden)]
84    #[cfg(test)]
85    pub fn new_with_nonce<N: Into<String>, P: Into<Password>>(
86        username: N,
87        password: P,
88        nonce: String,
89    ) -> Scram<S> {
90        Scram {
91            name: format!("SCRAM-{}", S::name()),
92            name_plus: format!("SCRAM-{}-PLUS", S::name()),
93            username: username.into(),
94            password: password.into(),
95            client_first_extensions: String::new(),
96            client_final_extensions: String::new(),
97            client_nonce: nonce,
98            state: ScramState::Init,
99            channel_binding: ChannelBinding::None,
100            _marker: PhantomData,
101        }
102    }
103}
104
105impl<S: ScramProvider> Mechanism for Scram<S> {
106    fn name(&self) -> &str {
107        // TODO: this is quite the workaround…
108        match self.channel_binding {
109            ChannelBinding::None | ChannelBinding::Unsupported => &self.name,
110            ChannelBinding::TlsUnique(_) | ChannelBinding::TlsExporter(_) => &self.name_plus,
111        }
112    }
113
114    fn from_credentials(credentials: Credentials) -> Result<Scram<S>, MechanismError> {
115        if let Secret::Password(password) = credentials.secret {
116            if let Identity::Username(username) = credentials.identity {
117                Scram::new(username, password, credentials.channel_binding)
118                    .map_err(|_| MechanismError::CannotGenerateNonce)
119            } else {
120                Err(MechanismError::ScramRequiresUsername)
121            }
122        } else {
123            Err(MechanismError::ScramRequiresPassword)
124        }
125    }
126
127    fn initial(&mut self) -> Vec<u8> {
128        let mut gs2_header = Vec::new();
129        gs2_header.extend(self.channel_binding.header());
130        let mut bare = Vec::new();
131        bare.extend(b"n=");
132        bare.extend(self.username.bytes());
133        bare.extend(b",r=");
134        bare.extend(self.client_nonce.bytes());
135        if !self.client_first_extensions.is_empty() {
136            bare.extend(b",");
137            bare.extend(self.client_first_extensions.bytes());
138        }
139        let mut data = Vec::new();
140        data.extend(&gs2_header);
141        data.extend(&bare);
142        self.state = ScramState::SentInitialMessage {
143            initial_message: bare,
144            gs2_header,
145        };
146        data
147    }
148
149    fn response(&mut self, challenge: &[u8]) -> Result<Vec<u8>, MechanismError> {
150        let next_state;
151        let ret;
152        match self.state {
153            ScramState::SentInitialMessage {
154                ref initial_message,
155                ref gs2_header,
156            } => {
157                let frame =
158                    parse_frame(challenge).map_err(|_| MechanismError::CannotDecodeChallenge)?;
159                let server_nonce = frame.get(&'r');
160                let salt = frame.get(&'s').and_then(|v| Base64.decode(v).ok());
161                let iterations = frame.get(&'i').and_then(|v| v.parse().ok());
162                let server_nonce = server_nonce.ok_or(MechanismError::NoServerNonce)?;
163                let salt = salt.ok_or(MechanismError::NoServerSalt)?;
164                let iterations = iterations.ok_or(MechanismError::NoServerIterations)?;
165                // TODO: SASLprep
166                let mut client_final_message_bare = Vec::new();
167                client_final_message_bare.extend(b"c=");
168                let mut cb_data: Vec<u8> = Vec::new();
169                cb_data.extend(gs2_header);
170                cb_data.extend(self.channel_binding.data());
171                client_final_message_bare.extend(Base64.encode(&cb_data).bytes());
172                client_final_message_bare.extend(b",r=");
173                client_final_message_bare.extend(server_nonce.bytes());
174                if !self.client_final_extensions.is_empty() {
175                    client_final_message_bare.extend(b",");
176                    client_final_message_bare.extend(self.client_final_extensions.bytes());
177                }
178                let salted_password = S::derive(&self.password, &salt, iterations)?;
179                let client_key = S::hmac(b"Client Key", &salted_password)?;
180                let server_key = S::hmac(b"Server Key", &salted_password)?;
181                let mut auth_message = Vec::new();
182                auth_message.extend(initial_message);
183                auth_message.push(b',');
184                auth_message.extend(challenge);
185                auth_message.push(b',');
186                auth_message.extend(&client_final_message_bare);
187                let stored_key = S::hash(&client_key);
188                let client_signature = S::hmac(&auth_message, &stored_key)?;
189                let client_proof = xor(&client_key, &client_signature);
190                let server_signature = S::hmac(&auth_message, &server_key)?;
191                let mut client_final_message = Vec::new();
192                client_final_message.extend(&client_final_message_bare);
193                client_final_message.extend(b",p=");
194                client_final_message.extend(Base64.encode(client_proof).bytes());
195                next_state = ScramState::GotServerData { server_signature };
196                ret = client_final_message;
197            }
198            _ => {
199                return Err(MechanismError::InvalidState);
200            }
201        }
202        self.state = next_state;
203        Ok(ret)
204    }
205
206    fn success(&mut self, data: &[u8]) -> Result<(), MechanismError> {
207        let frame = parse_frame(data).map_err(|_| MechanismError::CannotDecodeSuccessResponse)?;
208        match self.state {
209            ScramState::GotServerData {
210                ref server_signature,
211            } => {
212                if let Some(sig) = frame.get(&'v').and_then(|v| Base64.decode(v).ok()) {
213                    if sig == *server_signature {
214                        Ok(())
215                    } else {
216                        Err(MechanismError::InvalidSignatureInSuccessResponse)
217                    }
218                } else {
219                    Err(MechanismError::NoSignatureInSuccessResponse)
220                }
221            }
222            _ => Err(MechanismError::InvalidState),
223        }
224    }
225}
226
227#[cfg(test)]
228mod tests {
229    use crate::client::mechanisms::Scram;
230    use crate::client::Mechanism;
231    use crate::common::scram::{Sha1, Sha256};
232    use alloc::borrow::ToOwned;
233    use alloc::string::String;
234
235    #[test]
236    fn scram_sha1_works() {
237        // Source: https://wiki.xmpp.org/web/SASLandSCRAM-SHA-1
238        let username = "user";
239        let password = "pencil";
240        let client_nonce = "fyko+d2lbbFgONRv9qkxdawL";
241        let client_init = b"n,,n=user,r=fyko+d2lbbFgONRv9qkxdawL";
242        let server_init = b"r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,s=QSXCR+Q6sek8bf92,i=4096";
243        let client_final =
244            b"c=biws,r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,p=v0X8v3Bz2T0CJGbJQyF0X+HI4Ts=";
245        let server_final = b"v=rmF9pqV8S7suAoZWja4dJRkFsKQ=";
246        let mut mechanism =
247            Scram::<Sha1>::new_with_nonce(username, password, client_nonce.to_owned());
248        let init = mechanism.initial();
249        assert_eq!(
250            String::from_utf8(init.clone()).unwrap(),
251            String::from_utf8(client_init[..].to_owned()).unwrap()
252        ); // depends on ordering…
253        let resp = mechanism.response(&server_init[..]).unwrap();
254        assert_eq!(
255            String::from_utf8(resp.clone()).unwrap(),
256            String::from_utf8(client_final[..].to_owned()).unwrap()
257        ); // again, depends on ordering…
258        mechanism.success(&server_final[..]).unwrap();
259    }
260
261    #[test]
262    fn scram_sha256_works() {
263        // Source: RFC 7677
264        let username = "user";
265        let password = "pencil";
266        let client_nonce = "rOprNGfwEbeRWgbNEkqO";
267        let client_init = b"n,,n=user,r=rOprNGfwEbeRWgbNEkqO";
268        let server_init = b"r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0,s=W22ZaJ0SNY7soEsUEjb6gQ==,i=4096";
269        let client_final = b"c=biws,r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0,p=dHzbZapWIk4jUhN+Ute9ytag9zjfMHgsqmmiz7AndVQ=";
270        let server_final = b"v=6rriTRBi23WpRR/wtup+mMhUZUn/dB5nLTJRsjl95G4=";
271        let mut mechanism =
272            Scram::<Sha256>::new_with_nonce(username, password, client_nonce.to_owned());
273        let init = mechanism.initial();
274        assert_eq!(
275            String::from_utf8(init.clone()).unwrap(),
276            String::from_utf8(client_init[..].to_owned()).unwrap()
277        ); // depends on ordering…
278        let resp = mechanism.response(&server_init[..]).unwrap();
279        assert_eq!(
280            String::from_utf8(resp.clone()).unwrap(),
281            String::from_utf8(client_final[..].to_owned()).unwrap()
282        ); // again, depends on ordering…
283        mechanism.success(&server_final[..]).unwrap();
284    }
285
286    #[test]
287    fn scram_kafka_token_delegation_works() {
288        // credentials and raw messages taken from a real kafka SCRAM token delegation authentication
289        let username = "6Lbb79aSTs-mDWUPc64D9Q";
290        let password = "O574x+7mB0B8R9Yt8DqwWbIzBgEm3lUE+fy7VWdvCwcLvGvwJK9GM4y0Qaz/MxiIxDHEnxDfSuB13uycXiUqyg==";
291        let client_nonce = "o6wj2xqdu0fxe4nmnukkj076m";
292        let client_init = b"n,,n=6Lbb79aSTs-mDWUPc64D9Q,r=o6wj2xqdu0fxe4nmnukkj076m,tokenauth=true";
293        let server_init = b"r=o6wj2xqdu0fxe4nmnukkj076m1eut816hvmsycqw2qzyn14zxvr,s=MWVtNWw1Mzc1MnFianNoYWhqMjhyYzVzZHM=,i=4096";
294        let client_final = b"c=biws,r=o6wj2xqdu0fxe4nmnukkj076m1eut816hvmsycqw2qzyn14zxvr,p=qVfqg28hDgroc6pal4qCF+8hO1/wiB84o7snGRDZKuE=";
295        let server_final = b"v=2ZSkAlHEUj6WehcizLhQRiiVGn+VDVtmAqj1v/IPa28=";
296        let mut mechanism =
297            Scram::<Sha256>::new_with_nonce(username, password, client_nonce.to_owned())
298                .with_first_extensions("tokenauth=true".to_owned());
299        let init = mechanism.initial();
300        assert_eq!(
301            core::str::from_utf8(&init).unwrap(),
302            core::str::from_utf8(client_init).unwrap()
303        ); // depends on ordering…
304        let resp = mechanism.response(server_init).unwrap();
305        assert_eq!(
306            core::str::from_utf8(&resp).unwrap(),
307            core::str::from_utf8(client_final).unwrap()
308        ); // again, depends on ordering…
309        mechanism.success(server_final).unwrap();
310    }
311
312    #[test]
313    fn scram_final_extension_works() {
314        let username = "some_user";
315        let password = "a_password";
316        let client_nonce = "client_nonce";
317        let client_init = b"n,,n=some_user,r=client_nonce";
318        let server_init =
319            b"r=client_nonceserver_nonce,s=MWVtNWw1Mzc1MnFianNoYWhqMjhyYzVzZHM=,i=4096";
320        let client_final = b"c=biws,r=client_nonceserver_nonce,foo=true,p=T9XQLmykBv74DzbaCtX90/ElJYJU2XWM/jHmHJ+BI/w=";
321        let mut mechanism =
322            Scram::<Sha256>::new_with_nonce(username, password, client_nonce.to_owned())
323                .with_final_extensions("foo=true".to_owned());
324        let init = mechanism.initial();
325        assert_eq!(
326            core::str::from_utf8(&init).unwrap(),
327            core::str::from_utf8(client_init).unwrap()
328        ); // depends on ordering…
329        let resp = mechanism.response(server_init).unwrap();
330        assert_eq!(
331            core::str::from_utf8(&resp).unwrap(),
332            core::str::from_utf8(client_final).unwrap()
333        ); // again, depends on ordering…
334    }
335}