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 --- Cargo.lock | 21 +++-- Cargo.toml | 28 ++----- LICENSE-APACHE-2.0 | 12 --- LICENSE-MIT | 19 ----- README.md | 47 ----------- src/lib.rs | 181 ----------------------------------------- src/main.rs | 48 ----------- tests/cli.rs | 34 -------- wpa-psk-cli/CHANGELOG.md | 16 ++++ wpa-psk-cli/Cargo.toml | 19 +++++ wpa-psk-cli/LICENSE-APACHE-2.0 | 12 +++ wpa-psk-cli/LICENSE-MIT | 19 +++++ wpa-psk-cli/README.md | 47 +++++++++++ wpa-psk-cli/src/bin/wpa-psk.rs | 48 +++++++++++ wpa-psk-cli/tests/cli.rs | 34 ++++++++ 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 +++++++++++++++++++++++++++++++++++++++++ 21 files changed, 535 insertions(+), 371 deletions(-) delete mode 100644 LICENSE-APACHE-2.0 delete mode 100644 LICENSE-MIT delete mode 100644 README.md delete mode 100644 src/lib.rs delete mode 100644 src/main.rs delete mode 100644 tests/cli.rs create mode 100644 wpa-psk-cli/CHANGELOG.md create mode 100644 wpa-psk-cli/Cargo.toml create mode 100644 wpa-psk-cli/LICENSE-APACHE-2.0 create mode 100644 wpa-psk-cli/LICENSE-MIT create mode 100644 wpa-psk-cli/README.md create mode 100644 wpa-psk-cli/src/bin/wpa-psk.rs create mode 100644 wpa-psk-cli/tests/cli.rs 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 diff --git a/Cargo.lock b/Cargo.lock index 1415e60..80cd202 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -194,9 +194,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "1.8.1" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f647032dfaa1f8b6dc29bd3edb7bbef4861b8b8007ebb118d6db284fd59f6ee" +checksum = "e6012d540c5baa3589337a98ce73408de9b5a25ec9fc2c6fd6be8f0d39e0ca5a" dependencies = [ "autocfg", "hashbrown", @@ -231,9 +231,9 @@ checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" [[package]] name = "os_str_bytes" -version = "6.0.1" +version = "6.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "029d8d0b2f198229de29dca79676f2738ff952edf3fde542eb8bf94d8c21b435" +checksum = "21326818e99cfe6ce1e524c2a805c189a99b5ae555a35d19f9a284b427d86afa" [[package]] name = "pbkdf2" @@ -434,11 +434,18 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "wpa-psk" -version = "0.1.5" +version = "0.2.0" dependencies = [ - "assert_cmd", - "clap", "hmac", "pbkdf2", "sha-1", ] + +[[package]] +name = "wpa-psk-cli" +version = "0.1.0" +dependencies = [ + "assert_cmd", + "clap", + "wpa-psk", +] diff --git a/Cargo.toml b/Cargo.toml index 6480261..47db587 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,23 +1,5 @@ -[package] -name = "wpa-psk" -version = "0.1.5" -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 = ["command-line-utilities", "algorithms"] -publish = true - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[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 } -clap = { version = "3.1.18", features = ["derive"] } - -[dev-dependencies] -assert_cmd = "2.0.4" +[workspace] +members = [ + "wpa-psk", + "wpa-psk-cli", +] diff --git a/LICENSE-APACHE-2.0 b/LICENSE-APACHE-2.0 deleted file mode 100644 index 7eefa9d..0000000 --- a/LICENSE-APACHE-2.0 +++ /dev/null @@ -1,12 +0,0 @@ -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/LICENSE-MIT b/LICENSE-MIT deleted file mode 100644 index 4012e61..0000000 --- a/LICENSE-MIT +++ /dev/null @@ -1,19 +0,0 @@ -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/README.md b/README.md deleted file mode 100644 index 0052d9c..0000000 --- a/README.md +++ /dev/null @@ -1,47 +0,0 @@ -# WPA-PSK - -This is a command-line utility and Rust library to compute the WPA pre-shared -key of a Wi-Fi SSID and passphrase. - -## Usage - -Hash a typical SSID and passphrase: - - $ wpa-psk home password123 - 0xde811641af2c516ffd35cc6f851b3abf03c7c84fc703cfb580f1c0456943cdc0 - -Hash *special* albeit valid credentials: - - $ wpa-psk "123abcABC.,-" "456defDEF *<:D" - 0x8a366e5bc51cd5d8fbbeffacc5f1af23fac30e3ac93cdcc368fafbbf63a1085c - -Hash *invalid* credentials: - - $ wpa-psk --force bar 2short - 0xcb5de4e4d23b2ab0bf5b9ba0fe8132c1e2af3bb52298ec801af8ad520cea3437 - -## Installation - -You can install `wpa-psk` using Cargo: - - $ cargo install 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/ - -## See also - -The popular [wpa_supplicant][] comes with a similar tool, `wpa_passphrase(8)`. - -[wpa_supplicant]: https://hostap.epitest.fi/wpa_supplicant/ diff --git a/src/lib.rs b/src/lib.rs deleted file mode 100644 index e99418c..0000000 --- a/src/lib.rs +++ /dev/null @@ -1,181 +0,0 @@ -//! 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" - ); - } -} diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index 181585c..0000000 --- a/src/main.rs +++ /dev/null @@ -1,48 +0,0 @@ -use std::process::exit; - -use clap::Parser; -use wpa_psk::{bytes_to_hex, wpa_psk, wpa_psk_unchecked, Passphrase, Ssid}; - -/// Compute the WPA-PSK of a Wi-Fi SSID and passphrase. -#[derive(Debug, Parser)] -#[clap(about, version, author)] -struct Args { - /// An SSID consisting of 1 up to 32 arbitrary bytes. - ssid: String, - - /// A passphrase consisting of 8 up to 63 printable ASCII characters. - passphrase: String, - - /// Don't check the given SSID and passphrase. - #[clap(short, long)] - force: bool, -} - -fn main() { - exit(match run() { - Ok(_) => 0, - Err(err) => { - eprintln!("{}", err); - 1 - } - }) -} - -fn run() -> Result<(), Box> { - let args = Args::try_parse()?; - let psk = if args.force { - wpa_psk_unchecked(args.ssid.as_bytes(), args.passphrase.as_bytes()) - } else { - let ssid = Ssid::try_from(args.ssid.as_bytes())?; - let passphrase = Passphrase::try_from(args.passphrase.as_bytes())?; - wpa_psk(&ssid, &passphrase) - }; - println!("0x{}", bytes_to_hex(&psk)); - Ok(()) -} - -#[test] -fn verify_clap_app() { - use clap::IntoApp; - Args::command().debug_assert() -} diff --git a/tests/cli.rs b/tests/cli.rs deleted file mode 100644 index 47fab8d..0000000 --- a/tests/cli.rs +++ /dev/null @@ -1,34 +0,0 @@ -use std::process::Command; - -use assert_cmd::prelude::*; - -#[test] -fn valid() { - let mut cmd = Command::cargo_bin("wpa-psk").unwrap(); - cmd.arg("home") - .arg("0123-4567-89") - .assert() - .success() - .stdout("0x150c047b6fad724512a17fa431687048ee503d14c1ea87681d4f241beb04f5ee\n"); -} - -#[test] -fn passphrase_too_short() { - let mut cmd = Command::cargo_bin("wpa-psk").unwrap(); - cmd.arg("bar") - .arg("2short") - .assert() - .failure() - .stderr("passphrase must have at least 8 bytes\n"); -} - -#[test] -fn force() { - let mut cmd = Command::cargo_bin("wpa-psk").unwrap(); - cmd.arg("--force") - .arg("bar") - .arg("2short") - .assert() - .success() - .stdout("0xcb5de4e4d23b2ab0bf5b9ba0fe8132c1e2af3bb52298ec801af8ad520cea3437\n"); -} diff --git a/wpa-psk-cli/CHANGELOG.md b/wpa-psk-cli/CHANGELOG.md new file mode 100644 index 0000000..3ba3406 --- /dev/null +++ b/wpa-psk-cli/CHANGELOG.md @@ -0,0 +1,16 @@ +# 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 the existing command-line interface from crate `wpa-psk` version 0.1.5. +- Added this changelog. diff --git a/wpa-psk-cli/Cargo.toml b/wpa-psk-cli/Cargo.toml new file mode 100644 index 0000000..6561dde --- /dev/null +++ b/wpa-psk-cli/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "wpa-psk-cli" +version = "0.1.0" +authors = ["Stefan Kreutz "] +edition = "2021" +description = "A command-line utility to 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 = ["command-line-utilities"] +publish = true + +[dependencies] +clap = { version = "3.1.18", features = ["derive"] } +wpa-psk = { path = "../wpa-psk", version = "0.2.0" } + +[dev-dependencies] +assert_cmd = "2.0.4" diff --git a/wpa-psk-cli/LICENSE-APACHE-2.0 b/wpa-psk-cli/LICENSE-APACHE-2.0 new file mode 100644 index 0000000..7eefa9d --- /dev/null +++ b/wpa-psk-cli/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-cli/LICENSE-MIT b/wpa-psk-cli/LICENSE-MIT new file mode 100644 index 0000000..4012e61 --- /dev/null +++ b/wpa-psk-cli/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-cli/README.md b/wpa-psk-cli/README.md new file mode 100644 index 0000000..6c00a4c --- /dev/null +++ b/wpa-psk-cli/README.md @@ -0,0 +1,47 @@ +# wpa-psk-cli + +This is a command-line utility to compute the WPA pre-shared key of a Wi-Fi +SSID and passphrase. See also the corresponding Rust library `wpa-psk`. + +## Usage + +Hash a typical SSID and passphrase: + + $ wpa-psk home password123 + 0xde811641af2c516ffd35cc6f851b3abf03c7c84fc703cfb580f1c0456943cdc0 + +Hash *special* albeit valid credentials: + + $ wpa-psk "123abcABC.,-" "456defDEF *<:D" + 0x8a366e5bc51cd5d8fbbeffacc5f1af23fac30e3ac93cdcc368fafbbf63a1085c + +Hash *invalid* credentials: + + $ wpa-psk --force bar 2short + 0xcb5de4e4d23b2ab0bf5b9ba0fe8132c1e2af3bb52298ec801af8ad520cea3437 + +## Installation + +You can install the `wpa-psk` utility using Cargo: + + $ cargo install wpa-psk-cli + +## 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/ + +## See also + +The popular [wpa_supplicant][] comes with a similar tool, `wpa_passphrase(8)`. + +[wpa_supplicant]: https://hostap.epitest.fi/wpa_supplicant/ diff --git a/wpa-psk-cli/src/bin/wpa-psk.rs b/wpa-psk-cli/src/bin/wpa-psk.rs new file mode 100644 index 0000000..181585c --- /dev/null +++ b/wpa-psk-cli/src/bin/wpa-psk.rs @@ -0,0 +1,48 @@ +use std::process::exit; + +use clap::Parser; +use wpa_psk::{bytes_to_hex, wpa_psk, wpa_psk_unchecked, Passphrase, Ssid}; + +/// Compute the WPA-PSK of a Wi-Fi SSID and passphrase. +#[derive(Debug, Parser)] +#[clap(about, version, author)] +struct Args { + /// An SSID consisting of 1 up to 32 arbitrary bytes. + ssid: String, + + /// A passphrase consisting of 8 up to 63 printable ASCII characters. + passphrase: String, + + /// Don't check the given SSID and passphrase. + #[clap(short, long)] + force: bool, +} + +fn main() { + exit(match run() { + Ok(_) => 0, + Err(err) => { + eprintln!("{}", err); + 1 + } + }) +} + +fn run() -> Result<(), Box> { + let args = Args::try_parse()?; + let psk = if args.force { + wpa_psk_unchecked(args.ssid.as_bytes(), args.passphrase.as_bytes()) + } else { + let ssid = Ssid::try_from(args.ssid.as_bytes())?; + let passphrase = Passphrase::try_from(args.passphrase.as_bytes())?; + wpa_psk(&ssid, &passphrase) + }; + println!("0x{}", bytes_to_hex(&psk)); + Ok(()) +} + +#[test] +fn verify_clap_app() { + use clap::IntoApp; + Args::command().debug_assert() +} diff --git a/wpa-psk-cli/tests/cli.rs b/wpa-psk-cli/tests/cli.rs new file mode 100644 index 0000000..47fab8d --- /dev/null +++ b/wpa-psk-cli/tests/cli.rs @@ -0,0 +1,34 @@ +use std::process::Command; + +use assert_cmd::prelude::*; + +#[test] +fn valid() { + let mut cmd = Command::cargo_bin("wpa-psk").unwrap(); + cmd.arg("home") + .arg("0123-4567-89") + .assert() + .success() + .stdout("0x150c047b6fad724512a17fa431687048ee503d14c1ea87681d4f241beb04f5ee\n"); +} + +#[test] +fn passphrase_too_short() { + let mut cmd = Command::cargo_bin("wpa-psk").unwrap(); + cmd.arg("bar") + .arg("2short") + .assert() + .failure() + .stderr("passphrase must have at least 8 bytes\n"); +} + +#[test] +fn force() { + let mut cmd = Command::cargo_bin("wpa-psk").unwrap(); + cmd.arg("--force") + .arg("bar") + .arg("2short") + .assert() + .success() + .stdout("0xcb5de4e4d23b2ab0bf5b9ba0fe8132c1e2af3bb52298ec801af8ad520cea3437\n"); +} 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