wlan_rsn/auth/
psk.rs

1// Copyright 2018 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use crate::Error;
6use anyhow::ensure;
7use core::num::NonZeroU32;
8use ieee80211::Ssid;
9use std::str;
10use wlan_common::security::wpa::credential::Psk as CommonPsk;
11use wlan_common::security::wpa::{self};
12
13// PBKDF2-HMAC-SHA1 is considered insecure but required for PSK computation.
14#[allow(deprecated)]
15use mundane::insecure::insecure_pbkdf2_hmac_sha1;
16
17/// Conversion of WPA credentials to a PSK.
18pub trait ToPsk {
19    fn to_psk(&self, ssid: &Ssid) -> CommonPsk;
20}
21
22/// Conversion of WPA1 credentials to a PSK.
23///
24/// RSN specifies that PSKs are used as-is and passphrases are transformed into a PSK. This
25/// implementation performs this transformation when necessary for WPA1 credentials.
26impl ToPsk for wpa::Wpa1Credentials {
27    fn to_psk(&self, ssid: &Ssid) -> CommonPsk {
28        match self {
29            wpa::Wpa1Credentials::Psk(ref psk) => psk.clone(),
30            wpa::Wpa1Credentials::Passphrase(ref passphrase) => {
31                // TODO(https://fxbug.dev/42177995): Unify the representation of PSKs. There can only be
32                //                        one...!
33                CommonPsk(
34                    compute(passphrase.as_ref(), ssid)
35                        .expect("invalid WPA passphrase data")
36                        .as_ref()
37                        .try_into()
38                        .expect("invalid derived PSK data"),
39                )
40            }
41        }
42    }
43}
44
45/// Conversion of WPA2 Personal credentials to a PSK.
46///
47/// RSN specifies that PSKs are used as-is and passphrases are transformed into a PSK. This
48/// implementation performs this transformation when necessary for WPA2 Personal credentials.
49impl ToPsk for wpa::Wpa2PersonalCredentials {
50    fn to_psk(&self, ssid: &Ssid) -> CommonPsk {
51        match self {
52            wpa::Wpa2PersonalCredentials::Psk(ref psk) => psk.clone(),
53            wpa::Wpa2PersonalCredentials::Passphrase(ref passphrase) => {
54                // TODO(https://fxbug.dev/42177995): Unify the representation of PSKs. There can only be
55                //                        one...!
56                CommonPsk(
57                    compute(passphrase.as_ref(), ssid)
58                        .expect("invalid WPA passphrase data")
59                        .as_ref()
60                        .try_into()
61                        .expect("invalid derived PSK data"),
62                )
63            }
64        }
65    }
66}
67
68/// Keys derived from a passphrase provide comparably low levels of security.
69/// Passphrases should have a minimum length of 20 characters since shorter passphrases
70/// are unlikely to prevent attacks.
71pub type Psk = Box<[u8]>;
72
73pub fn compute(passphrase: &[u8], ssid: &Ssid) -> Result<Psk, anyhow::Error> {
74    // IEEE Std 802.11-2016, J.4.1 provides a reference implementation that describes the
75    // passphrase as:
76    //
77    //   ... sequence of between 8 and 63 ASCII-encoded characters ... Each character in the
78    //   pass-phrase has an encoding in the range 32 to 126 (decimal).
79    //
80    // However, the standard does not seem to specify this encoding or otherwise state that it is a
81    // requirement. In practice, devices accept UTF-8 encoded passphrases, which is far less
82    // restrictive than a subset of ASCII. This code attempts to parse the passphrase as UTF-8 and
83    // emits an error if this is not possible. Note that this also accepts the ASCII encoding
84    // suggested by J.4.1.
85    let _utf8 = str::from_utf8(passphrase)
86        .map_err(|error| Error::InvalidPassphraseEncoding(error.valid_up_to()))?;
87
88    // IEEE Std 802.11-2016, J.4.1 suggests a passphrase length of [8, 64). However, J.4.1 also
89    // suggests ASCII encoding and this code expects UTF-8 encoded passphrases. This implicitly
90    // supports the ASCII encodings described in J.4.1. However, the length of the byte sequence no
91    // longer represents the number of encoded characters, so non-ASCII passphrases may appear to
92    // have arbitrary character limits. Note that the character count can be obtained via
93    // `_utf8.chars().count()`.
94    ensure!(
95        passphrase.len() >= 8 && passphrase.len() <= 63,
96        Error::InvalidPassphraseLen(passphrase.len())
97    );
98
99    // Compute PSK: IEEE Std 802.11-2016, J.4.1
100    let size = 256 / 8;
101    let mut psk = vec![0_u8; size];
102    const ITERS: NonZeroU32 = NonZeroU32::new(4096).unwrap();
103    // PBKDF2-HMAC-SHA1 is considered insecure but required for PSK computation.
104    #[allow(deprecated)]
105    insecure_pbkdf2_hmac_sha1(&passphrase[..], &ssid[..], ITERS, &mut psk[..]);
106    Ok(psk.into_boxed_slice())
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112    use hex::FromHex;
113    use wlan_common::security::wpa::credential::Passphrase;
114
115    fn assert_psk(password: &str, ssid: &str, expected: &str) {
116        let psk = compute(password.as_bytes(), &Ssid::try_from(ssid).unwrap())
117            .expect("computing PSK failed");
118        let expected = Vec::from_hex(expected).unwrap();
119        assert_eq!(&psk[..], &expected[..]);
120    }
121
122    // IEEE Std 802.11-2016, J.4.2, Test case 1
123    #[test]
124    fn test_psk_test_case_1() {
125        assert_psk(
126            "password",
127            "IEEE",
128            "f42c6fc52df0ebef9ebb4b90b38a5f902e83fe1b135a70e23aed762e9710a12e",
129        );
130    }
131
132    // IEEE Std 802.11-2016, J.4.2, Test case 2
133    #[test]
134    fn test_psk_test_case_2() {
135        assert_psk(
136            "ThisIsAPassword",
137            "ThisIsASSID",
138            "0dc0d6eb90555ed6419756b9a15ec3e3209b63df707dd508d14581f8982721af",
139        );
140    }
141
142    // IEEE Std 802.11-2016, J.4.2, Test case 3
143    #[test]
144    fn test_psk_test_case_3() {
145        assert_psk(
146            "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
147            "ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ",
148            "becb93866bb8c3832cb777c2f559807c8c59afcb6eae734885001300a981cc62",
149        );
150    }
151
152    #[test]
153    fn test_psk_too_short_password() {
154        let result = compute("short".as_bytes(), &Ssid::try_from("Some SSID").unwrap());
155        assert!(result.is_err());
156    }
157
158    #[test]
159    fn test_psk_too_long_password() {
160        let result = compute(
161            "1234567890123456789012345678901234567890123456789012345678901234".as_bytes(),
162            &Ssid::try_from("Some SSID").unwrap(),
163        );
164        assert!(result.is_err());
165    }
166
167    #[test]
168    fn test_psk_ascii_bounds_password() {
169        let result =
170            compute("\x20ASCII Bound Test \x7E".as_bytes(), &Ssid::try_from("Some SSID").unwrap());
171        assert!(result.is_ok());
172    }
173
174    #[test]
175    fn test_psk_non_ascii_password() {
176        assert!(compute("パスワード".as_bytes(), &Ssid::try_from("Some SSID").unwrap()).is_ok());
177    }
178
179    #[test]
180    fn test_psk_invalid_encoding_password() {
181        assert!(compute(&[0xFFu8; 32], &Ssid::try_from("Some SSID").unwrap()).is_err());
182    }
183
184    #[test]
185    fn wpa1_credentials_to_psk() {
186        let ssid = Ssid::try_from("IEEE").unwrap();
187
188        let credentials = wpa::Wpa1Credentials::Psk(CommonPsk::from([0u8; 32]));
189        assert_eq!(CommonPsk::from([0u8; 32]), credentials.to_psk(&ssid));
190
191        let credentials =
192            wpa::Wpa1Credentials::Passphrase(Passphrase::try_from("password").unwrap());
193        assert_eq!(
194            CommonPsk::parse("f42c6fc52df0ebef9ebb4b90b38a5f902e83fe1b135a70e23aed762e9710a12e")
195                .unwrap(),
196            credentials.to_psk(&ssid),
197        );
198    }
199
200    #[test]
201    fn wpa2_personal_credentials_to_psk() {
202        let ssid = Ssid::try_from("IEEE").unwrap();
203
204        let credentials = wpa::Wpa2PersonalCredentials::Psk(CommonPsk::from([0u8; 32]));
205        assert_eq!(CommonPsk::from([0u8; 32]), credentials.to_psk(&ssid));
206
207        let credentials =
208            wpa::Wpa2PersonalCredentials::Passphrase(Passphrase::try_from("password").unwrap());
209        assert_eq!(
210            CommonPsk::parse("f42c6fc52df0ebef9ebb4b90b38a5f902e83fe1b135a70e23aed762e9710a12e")
211                .unwrap(),
212            credentials.to_psk(&ssid),
213        );
214    }
215}