From 74ac452eebd4d3520c1f610260d55b7e7233eca9 Mon Sep 17 00:00:00 2001 From: Stefan Kreutz Date: Mon, 3 Jan 2022 15:49:53 +0100 Subject: Add initial implementation --- .gitignore | 1 + Cargo.lock | 346 +++++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 20 ++++ LICENSE-APACHE-2.0 | 12 ++ LICENSE-MIT | 19 +++ README.md | 31 +++++ src/lib.rs | 138 +++++++++++++++++++++ src/main.rs | 48 ++++++++ 8 files changed, 615 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 LICENSE-APACHE-2.0 create mode 100644 LICENSE-MIT create mode 100644 README.md create mode 100644 src/lib.rs create mode 100644 src/main.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..4aca404 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,346 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "block-buffer" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1d36a02058e76b040de25a4464ba1c80935655595b661505c8b39b664828b95" +dependencies = [ + "generic-array", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clap" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d17bf219fcd37199b9a29e00ba65dfb8cd5b2688b7297ec14ff829c40ac50ca9" +dependencies = [ + "atty", + "bitflags", + "clap_derive", + "indexmap", + "lazy_static", + "os_str_bytes", + "strsim", + "termcolor", + "textwrap", +] + +[[package]] +name = "clap_derive" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1b9752c030a14235a0bd5ef3ad60a1dcac8468c30921327fc8af36b20c790b9" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "cpufeatures" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95059428f66df56b63431fdb4e1947ed2190586af5c5a8a8b71122bdf5a7f469" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d6b536309245c849479fba3da410962a43ed8e51c26b729208ec0ac2798d0" +dependencies = [ + "generic-array", +] + +[[package]] +name = "digest" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b697d66081d42af4fba142d56918a3cb21dc8eb63372c6b85d14f44fb9c5979b" +dependencies = [ + "block-buffer", + "crypto-common", + "generic-array", + "subtle", +] + +[[package]] +name = "generic-array" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" + +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hmac" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddca131f3e7f2ce2df364b57949a9d47915cfbd35e46cfee355ccebbf794d6a2" +dependencies = [ + "digest", +] + +[[package]] +name = "indexmap" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.112" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b03d17f364a3a042d5e5d46b053bbbf82c92c9430c592dd4c064dc6ee997125" + +[[package]] +name = "memchr" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" + +[[package]] +name = "os_str_bytes" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64" +dependencies = [ + "memchr", +] + +[[package]] +name = "pbkdf2" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4628cc3cf953b82edcd3c1388c5715401420ce5524fedbab426bd5aba017434" +dependencies = [ + "digest", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47aa80447ce4daf1717500037052af176af5d38cc3e571d9ec1c7353fc10c87d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "sha-1" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "028f48d513f9678cda28f6e4064755b3fbb2af6acd672f2c209b62323f7aea0f" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "subtle" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" + +[[package]] +name = "syn" +version = "1.0.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecb2e6da8ee5eb9a61068762a32fa9619cc591ceb055b3687f4cd4051ec2e06b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "termcolor" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "textwrap" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80" + +[[package]] +name = "typenum" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" + +[[package]] +name = "unicode-segmentation" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b" + +[[package]] +name = "unicode-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "wpa-psk" +version = "0.1.0" +dependencies = [ + "clap", + "hmac", + "pbkdf2", + "sha-1", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..2987092 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "wpa-psk" +version = "0.1.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/repos/wpa-psk-rs.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.10.0", default-features = false } +hmac = { version = "0.12.0", default-features = false } +sha-1 = { version = "0.10.0", default-features = false } +clap = { version = "3.0.0-rc.11", features = ["derive"] } diff --git a/LICENSE-APACHE-2.0 b/LICENSE-APACHE-2.0 new file mode 100644 index 0000000..7eefa9d --- /dev/null +++ b/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/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 0000000..4012e61 --- /dev/null +++ b/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/README.md b/README.md new file mode 100644 index 0000000..0fcc42f --- /dev/null +++ b/README.md @@ -0,0 +1,31 @@ +# 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. + +## Example + +I've created the tool to connect to wireless networks with *special* or technically *invalid* SSIDs and passphrases: + + $ wpa-psk "123abcABC.,-" "456defDEF *<:D" + 0x8a366e5bc51cd5d8fbbeffacc5f1af23fac30e3ac93cdcc368fafbbf63a1085c + + $ wpa-psk --force "bar" "2short" + 0xcb5de4e4d23b2ab0bf5b9ba0fe8132c1e2af3bb52298ec801af8ad520cea3437 + +## License + +This work is distributed under the terms of both, the [MIT License](LICENSE-MIT) and the [Apache License, Version 2.0](LICENSE-APACHE-2.0). + +## Contribution + +Contributions are welcome! +Please contact me via email. + +Note: The `Cargo.toml` points to a bare git repository served over dumb HTTPS. +You can clone it as follows: + + $ git clone https://www.skreutz.com/repos/wpa-psk-rs.git + +## See also + +The popular [wpa_supplicant](https://hostap.epitest.fi/wpa_supplicant/) comes with a similar tool, `wpa_passphrase(8)`. diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..f50dea0 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,138 @@ +//! 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::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 = &'static str; + + fn try_from(value: &'a [u8]) -> Result { + if value.is_empty() { + Err("SSID must have at least one byte") + } else if value.len() > 32 { + Err("SSID must have at most 32 bytes") + } else { + Ok(Ssid(value)) + } + } +} + +impl<'a> TryFrom<&'a str> for Ssid<'a> { + type Error = &'static str; + + fn try_from(value: &'a str) -> Result { + Self::try_from(value.as_bytes()) + } +} + +/// 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 = &'static str; + + fn try_from(value: &'a [u8]) -> Result { + if value.len() < 8 { + Err("passphrase must have at least 8 bytes") + } else if value.len() > 63 { + Err("passphrase must have at most 63 bytes") + } else if value.iter().any(|i| !matches!(i, 32u8..=126)) { + Err("passphrase must consist of printable ASCII characters") + } else { + Ok(Passphrase(value)) + } + } +} + +impl<'a> TryFrom<&'a str> for Passphrase<'a> { + type Error = &'static str; + + 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()) + } +} + +/// 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() { + Passphrase::try_from("foobar").unwrap_err(); + } + + #[test] + fn display_passphrase() { + assert_eq!( + format!("{}", Passphrase::try_from("foobarbuzz").unwrap()), + "foobarbuzz" + ); + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..2a8c555 --- /dev/null +++ b/src/main.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::into_app().debug_assert() +} -- cgit v1.2.3