From 988c7921eacee0cf0b519788c143fe428972b0dc Mon Sep 17 00:00:00 2001 From: Stefan Kreutz Date: Sat, 28 May 2022 15:15:39 +0200 Subject: Extract wpa-psk-cli crate --- wpa-psk/CHANGELOG.md | 70 ++++++++++++++++++ wpa-psk/Cargo.toml | 17 +++++ wpa-psk/LICENSE-APACHE-2.0 | 12 +++ wpa-psk/LICENSE-MIT | 19 +++++ wpa-psk/README.md | 22 ++++++ wpa-psk/src/lib.rs | 181 +++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 321 insertions(+) create mode 100644 wpa-psk/CHANGELOG.md create mode 100644 wpa-psk/Cargo.toml create mode 100644 wpa-psk/LICENSE-APACHE-2.0 create mode 100644 wpa-psk/LICENSE-MIT create mode 100644 wpa-psk/README.md create mode 100644 wpa-psk/src/lib.rs (limited to 'wpa-psk') diff --git a/wpa-psk/CHANGELOG.md b/wpa-psk/CHANGELOG.md new file mode 100644 index 0000000..1d80d14 --- /dev/null +++ b/wpa-psk/CHANGELOG.md @@ -0,0 +1,70 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep A Changelog][] and this project adheres to +[Semantic Versioning][]. + +[Keep A Changelog]: https://keepachangelog.com/en/1.0.0/ +[Semantic Versioning]: https://semver.org/spec/v2.0.0.html + +## Unreleased + +### Added + +- Added this changelog. + +### Changed + +- Changed `TryFrom` implementations to return typed validation errors. +- Changed Git repository URL. + +### Removed + +- Extracted the command-line utility `wpa-psk` into a dedicated crate `wpa-psk-cli`. + +## [0.1.5] - 2022-05-23 + +### Added + +- Described command-line interface in [README.md](README.md). + +### Fixed + +- Fixed crate version in [Cargo.toml](Cargo.toml). + +## [0.1.4] - 2022-05-22 + +### Changed + +- Changed Git repository URL. +- Upgraded dependencies to their respective latest versions. + +## [0.1.3] - 2022-05-21 + +### Changed + +- Updated dependencies. +- Changed Git repository URL. + +### Fixed + +- Replaced deprecated function call `clap::IntoApp::into_app`. + +## [0.1.2] - 2022-03-30 + +### Changed + +- Relaxed pbkdf2 version to include 0.11. + +## [0.1.1] - 2022-02-01 + +### Changed + +- Updated dependencies. + +## [0.1.0] - 2022-01-03 + +### Added + +- Add initial implementation. diff --git a/wpa-psk/Cargo.toml b/wpa-psk/Cargo.toml new file mode 100644 index 0000000..ca173f4 --- /dev/null +++ b/wpa-psk/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "wpa-psk" +version = "0.2.0" +authors = ["Stefan Kreutz "] +edition = "2021" +description = "Compute the WPA-PSK of a Wi-FI SSID and passphrase" +readme = "README.md" +repository = "https://www.skreutz.com/scm/git/wpa-psk.git" +license = "MIT OR Apache-2.0" +keywords = ["wifi", "wpa", "password", "hash"] +categories = ["algorithms"] +publish = true + +[dependencies] +pbkdf2 = { version = "0.11.0", default-features = false } +hmac = { version = "0.12.1", default-features = false } +sha-1 = { version = "0.10.0", default-features = false } diff --git a/wpa-psk/LICENSE-APACHE-2.0 b/wpa-psk/LICENSE-APACHE-2.0 new file mode 100644 index 0000000..7eefa9d --- /dev/null +++ b/wpa-psk/LICENSE-APACHE-2.0 @@ -0,0 +1,12 @@ +Copyright 2021 Stefan Kreutz + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use +this file except in compliance with the License. You may obtain a copy of the +License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed +under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. diff --git a/wpa-psk/LICENSE-MIT b/wpa-psk/LICENSE-MIT new file mode 100644 index 0000000..4012e61 --- /dev/null +++ b/wpa-psk/LICENSE-MIT @@ -0,0 +1,19 @@ +Copyright 2021 Stefan Kreutz + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/wpa-psk/README.md b/wpa-psk/README.md new file mode 100644 index 0000000..c2e7136 --- /dev/null +++ b/wpa-psk/README.md @@ -0,0 +1,22 @@ +# wpa-psk + +This is a Rust library to compute the WPA pre-shared key of a Wi-Fi SSID and +passphrase. See also the corresponding command-line interface `wpa-psk-cli`. + +See the [crate documentation][docs] more information. + +[docs]: https://docs.rs/wpa-psk/latest/wpa_psk/ + +## License + +This work is distributed under the terms of both, the [MIT License][MIT] and +the [Apache License, Version 2.0][Apache-2.0]. + +[MIT]: LICENSE-MIT +[Apache-2.0]: LICENSE-APACHE-2.0 + +## Contribution + +Contributions are welcome! Please [contact][] me via email. + +[contact]: https://www.skreutz.com/contact/ diff --git a/wpa-psk/src/lib.rs b/wpa-psk/src/lib.rs new file mode 100644 index 0000000..e99418c --- /dev/null +++ b/wpa-psk/src/lib.rs @@ -0,0 +1,181 @@ +//! Compute the WPA-PSK of a Wi-Fi SSID and passphrase. +//! +//! # Example +//! +//! Compute and print the WPA-PSK of a valid SSID and passphrase: +//! +//! ``` +//! # use wpa_psk::{Ssid, Passphrase, wpa_psk, bytes_to_hex}; +//! # fn main() -> Result<(), Box> { +//! let ssid = Ssid::try_from("home")?; +//! let passphrase = Passphrase::try_from("0123-4567-89")?; +//! let psk = wpa_psk(&ssid, &passphrase); +//! assert_eq!(bytes_to_hex(&psk), "150c047b6fad724512a17fa431687048ee503d14c1ea87681d4f241beb04f5ee"); +//! # Ok(()) +//! # } +//! ``` +//! +//! Compute the WPA-PSK of possibly invalid raw bytes: +//! +//! ``` +//! # use wpa_psk::{wpa_psk_unchecked, bytes_to_hex}; +//! let ssid = "bar".as_bytes(); +//! let passphrase = "2short".as_bytes(); +//! let psk = wpa_psk_unchecked(&ssid, &passphrase); +//! assert_eq!(bytes_to_hex(&psk), "cb5de4e4d23b2ab0bf5b9ba0fe8132c1e2af3bb52298ec801af8ad520cea3437"); +//! ``` + +use std::{error::Error, fmt::Display}; + +use hmac::Hmac; +use pbkdf2::pbkdf2; +use sha1::Sha1; + +/// An SSID consisting of 1 up to 32 arbitrary bytes. +#[derive(Debug)] +pub struct Ssid<'a>(&'a [u8]); + +impl<'a> TryFrom<&'a [u8]> for Ssid<'a> { + type Error = ValidateSsidError; + + fn try_from(value: &'a [u8]) -> Result { + if value.is_empty() { + Err(ValidateSsidError::TooShort) + } else if value.len() > 32 { + Err(ValidateSsidError::TooLong) + } else { + Ok(Ssid(value)) + } + } +} + +impl<'a> TryFrom<&'a str> for Ssid<'a> { + type Error = ValidateSsidError; + + fn try_from(value: &'a str) -> Result { + Self::try_from(value.as_bytes()) + } +} + +#[derive(Debug, PartialEq)] +pub enum ValidateSsidError { + TooShort, + TooLong, +} + +impl Error for ValidateSsidError {} + +impl Display for ValidateSsidError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let msg = match self { + ValidateSsidError::TooShort => "SSID must have at least one byte", + ValidateSsidError::TooLong => "SSID must have at most 32 bytes", + }; + write!(f, "{msg}") + } +} + +/// A passphrase consisting of 8 up to 63 printable ASCII characters. +#[derive(Debug)] +pub struct Passphrase<'a>(&'a [u8]); + +impl<'a> TryFrom<&'a [u8]> for Passphrase<'a> { + type Error = ValidatePassphraseError; + + fn try_from(value: &'a [u8]) -> Result { + if value.len() < 8 { + Err(ValidatePassphraseError::TooShort) + } else if value.len() > 63 { + Err(ValidatePassphraseError::TooLong) + } else if value.iter().any(|i| !matches!(i, 32u8..=126)) { + Err(ValidatePassphraseError::InvalidByte) + } else { + Ok(Passphrase(value)) + } + } +} + +impl<'a> TryFrom<&'a str> for Passphrase<'a> { + type Error = ValidatePassphraseError; + + fn try_from(value: &'a str) -> Result { + Self::try_from(value.as_bytes()) + } +} + +impl Display for Passphrase<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", std::str::from_utf8(self.0).unwrap()) + } +} + +#[derive(Debug, PartialEq)] +pub enum ValidatePassphraseError { + TooShort, + TooLong, + InvalidByte, +} + +impl Error for ValidatePassphraseError {} + +impl Display for ValidatePassphraseError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let msg = match self { + ValidatePassphraseError::TooShort => "passphrase must have at least 8 bytes", + ValidatePassphraseError::TooLong => "passphrase must have at most 63 bytes", + ValidatePassphraseError::InvalidByte => { + "passphrase must consist of printable ASCII characters" + } + }; + write!(f, "{msg}") + } +} + +/// Returns the WPA-PSK of the given SSID and passphrase. +pub fn wpa_psk(ssid: &Ssid, passphrase: &Passphrase) -> [u8; 32] { + wpa_psk_unchecked(ssid.0, passphrase.0) +} + +/// Unchecked WPA-PSK. +/// See [`wpa_psk`]. +pub fn wpa_psk_unchecked(ssid: &[u8], passphrase: &[u8]) -> [u8; 32] { + let mut buf = [0u8; 32]; + pbkdf2::>(passphrase, ssid, 4096, &mut buf); + buf +} + +/// Returns the hexdecimal representation of the given bytes. +pub fn bytes_to_hex(bytes: &[u8]) -> String { + bytes.iter().map(|b| format!("{:02x}", b)).collect() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn special_characters() { + let ssid = Ssid::try_from("123abcABC.,-").unwrap(); + let passphrase = Passphrase::try_from("456defDEF *<:D").unwrap(); + assert_eq!( + bytes_to_hex(&wpa_psk(&ssid, &passphrase)), + "8a366e5bc51cd5d8fbbeffacc5f1af23fac30e3ac93cdcc368fafbbf63a1085c" + ); + } + + #[test] + fn passphrase_too_short() { + assert_eq!( + Passphrase::try_from("foobar").unwrap_err(), + ValidatePassphraseError::TooShort + ); + } + + #[test] + fn display_passphrase() { + assert_eq!( + format!("{}", Passphrase::try_from("foobarbuzz").unwrap()), + "foobarbuzz" + ); + } +} -- cgit v1.2.3