├── .gitignore ├── Makefile ├── README.md ├── build.sh ├── files ├── etc │ ├── config │ │ └── slppsk │ └── init.d │ │ └── hostapd_slppsk └── usr │ ├── bin │ └── hostapd_slppsk │ └── lib │ └── hostapd_slppsk │ ├── add_permanent_ppsk.sh │ ├── add_temp_ppsk.sh │ ├── event_handler.sh │ ├── iface_common.sh │ ├── init_iface.sh │ ├── init_psk.sh │ ├── key_common.sh │ └── manage_common.sh └── samples └── var └── run └── hostapd_slppsk └── wlan0 ├── lock ├── merged.psk ├── perm_macs.txt ├── pid └── ppsks ├── 5b503791b99b ├── params.env ├── perm.psk └── temp.psk └── a883dafc480d ├── params.env ├── perm.psk └── temp.psk /.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | include $(TOPDIR)/rules.mk 2 | 3 | PKG_NAME:=slppsk-hostapd 4 | PKG_VERSION:=0.1 5 | PKG_RELEASE:=$(shell git rev-parse --short HEAD) 6 | PKG_BUILD_DIR:=$(BUILD_DIR)/slppsk_hostapd 7 | 8 | include $(INCLUDE_DIR)/package.mk 9 | 10 | define Package/slppsk-hostapd 11 | TITLE:=Stateless Per-Station PSKs for hostapd 12 | SECTION:=net 13 | CATEGORY:=Network 14 | PKGARCH:=all 15 | DEPENDS:=+hostapd-utils +xxd +coreutils-base64 16 | endef 17 | 18 | define Package/slppsk-hostapd/description 19 | Generates a unique Pre Shared Key for each 20 | station based on its MAC address and a master 21 | password. No RADIUS server nor per-device 22 | configuration required. 23 | endef 24 | 25 | define Build/Prepare 26 | endef 27 | 28 | define Build/Configure 29 | endef 30 | 31 | define Build/Compile 32 | endef 33 | 34 | define Package/slppsk-hostapd/install 35 | $(INSTALL_DIR) \ 36 | $(1)/usr/{bin,lib/hostapd_slppsk} \ 37 | $(1)/etc/{config,init.d} 38 | $(INSTALL_BIN) ./files/usr/bin/hostapd_slppsk $(1)/usr/bin 39 | $(INSTALL_BIN) ./files/etc/init.d/hostapd_slppsk $(1)/etc/init.d 40 | $(INSTALL_BIN) \ 41 | ./files/usr/lib/hostapd_slppsk/{add_permanent_ppsk,add_temp_ppsk,event_handler,init_iface,init_psk}.sh \ 42 | $(1)/usr/lib/hostapd_slppsk 43 | $(INSTALL_DATA) \ 44 | ./files/usr/lib/hostapd_slppsk/{key_common,iface_common,manage_common}.sh \ 45 | $(1)/usr/lib/hostapd_slppsk 46 | $(INSTALL_CONF) ./files/etc/config/slppsk $(1)/etc/config 47 | endef 48 | 49 | define Package/ddns-scripts/conffiles 50 | /etc/config/ddns 51 | endef 52 | 53 | $(eval $(call BuildPackage,slppsk-hostapd)) 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Stateless Per-Device PSK for hostapd in OpenWRT 2 | 3 | This set of scripts makes use of the `wpa_psk_file` configuration 4 | in hostapd to assign each station (wifi client device) a pre-shared 5 | key derived from a Master Password and the station MAC address. The 6 | code used to derive the PPSK is equivalent to this bash function: 7 | 8 | ```bash 9 | get_ppsk () { 10 | local master_pwd="$1" sta_addr="$2" ppsk_len_bytes="$3" 11 | printf "%s%s" \ 12 | "$master_pwd" \ 13 | "$(echo "$sta_addr" | tr -dc a-fA-F0-9 | xxd -r -p)" | \ 14 | sha256sum | \ 15 | cut -d" " -f 1 | \ 16 | xxd -r -p | \ 17 | head -c "$ppsk_len_bytes" | \ 18 | base64 19 | } 20 | ``` 21 | 22 | Essentially the Master Password and the station address (without 23 | colons) are concatenated, hashed using the SHA256 algorithm, 24 | trimmed to a set length and then converted to base64 (to avoid 25 | padding use a multiple of 3 length). I believe this key derivation 26 | process is secure since SHA256 is irreversible, although I am not 27 | at all an expert in cryptography, so please let me know if you find 28 | any issues with the implementation. 29 | 30 | The advantage of using keys derived from a Master Password is the 31 | minimal configuration, no databases to maintain, no sync issues, and 32 | the whole thing is pretty lightweight compared to using a RADIUS server 33 | like freeradius. Roaming between multiple APs should also not be an 34 | issue as long as both APs share the same Master Password. 35 | 36 | This project is inspired by 37 | [this answer](https://security.stackexchange.com/a/266499/193181) 38 | in Stack Exchange Information Security. 39 | 40 | ## Usage 41 | 42 | ### Typical config 43 | 44 | ```conf 45 | config password 46 | # 2.5GHz network connected to lan interface 47 | list ifname wlan-lan 48 | # same but 5GHz 49 | list ifname wlan-lan-fghz 50 | 51 | # Master password used to derive the pre shared 52 | # keys for each station 53 | option master_password testing12345 54 | ``` 55 | 56 | Nothing more than that should be needed to have a functional PPSK 57 | setup. For more details read the comments in the 58 | [config file](./files/etc/config/slppsk) 59 | 60 | ### VLANs 61 | 62 | A VLAN ID can be assigned to a Master Password, this can be used 63 | along with the `dynamic_vlan` switch to connect a station to a certain 64 | VLAN depending on if the which PSK the station authenticated with. 65 | Every station is able to connect to every configured VLAN _if_ the PSK 66 | used comes from the correct Master Password. 67 | 68 | To allow hostapd to connect a wireless interface to a particular 69 | VLAN a bridge is used, the wireless interface is added to 70 | the bridge for that VLAN whenever a station connects with 71 | the PSK that has a `vlanid` specified. For this to work 72 | hostapd needs to know how to map a specific ID to a bridge 73 | and wireless interface name. There are two main mechanisms to 74 | achieve this, using a `vlan_file` with static names or 75 | dynamically trough `vlan_naming`. Since the primary objective 76 | for this project is home networking (many switch chips for OpenWRT 77 | compatible routers don't support more than 15 VLANs), I'll 78 | explain the static method. 79 | 80 | For this example, we have three VLANs that we want to connect 81 | to a main WiFi AP with interface name `wifi-main`. 82 | 83 | `/etc/config/slppsk`: 84 | 85 | ```conf 86 | config password 'main_iot' 87 | list ifname 'wlan-main' 88 | option vlanid '10' 89 | 90 | config password 'main_guests' 91 | list ifname 'wlan-main' 92 | option vlanid '4' 93 | 94 | config password 'main_lan' 95 | list ifname 'wlan-main' 96 | option vlanid '2' 97 | ``` 98 | 99 | The wifi interface config should look like this: 100 | 101 | ```conf 102 | config wifi-iface 'wifinet4' 103 | # ... 104 | option ifname 'wlan-main' 105 | # Add the following params 106 | option dynamic_vlan '2' 107 | option vlan_no_bridge '0' 108 | option vlan_file '/etc/slppsk/wlan-main.vlan' 109 | ``` 110 | 111 | And the `vlan_file` associated with this interface follows this 112 | syntax: 113 | 114 | `/etc/slppsk/wlan-main.vlan`: 115 | 116 | ```text 117 | 10 wlan-main.10 br-iot 118 | 2 wlan-main.2 br-lan 119 | 4 wlan-main.4 br-guests 120 | ``` 121 | 122 | The first column corresponds to the `vlanid` parameter in 123 | `/etc/config/slppsk`, the second column is the name for the 124 | wireless interface, and the last column should match the bridge 125 | name the VLAN interface is attached to. 126 | 127 | The following links contain more info about this feature: 128 | 129 | * [Code that parses the `vlan_file`](https://w1.fi/cgit/hostap/tree/hostapd/config_file.c?id=4d663233e64f639998aab31195ab7c819164019c#n36) 130 | * [The third column in `vlan_file` was added in this (relatively recent) commit](https://w1.fi/cgit/hostap/commit/?id=4d663233e64f639998aab31195ab7c819164019c) 131 | * [Patch that added the `vlan_no_bridge` parameter](https://github.com/openwrt/openwrt/blob/openwrt-21.02/package/network/services/hostapd/patches/710-vlan_no_bridge.patch) 132 | * [`vlan_no_bridge` was changed at some point to be default `1`](https://github.com/openwrt/openwrt/issues/9944) 133 | * [Troubleshooting dynamic VLANs](https://openwrt.org/docs/guide-user/network/wifi/wireless.security.8021x#how_it_workstroubleshooting) 134 | 135 | One more thing to keep in mind is that interface names have length 136 | limit, so while it might be fine for interface names alone, 137 | once you specify the VLAN ID using dot notation (`.`) 138 | the interface name might excede that limit. 139 | 140 | ## TODO 141 | 142 | * Add tests 143 | * Automatic releases with github actions 144 | * Perm MAC files can only be modified by the event listener on a single interface, otherwise things get out of sync 145 | * Fix inconsistent naming in code around "master password" 146 | * Fix inconsistent naming of project: "hostapd_slppsk" vs "slppsk-hostapd" 147 | 148 | ## Known issues 149 | 150 | ### `hostapd: CTRL_IFACE monitor[1]: 146 - Connection refused` 151 | 152 | There seems to be an issue with how `hostapd_cli` closes as hostapd 153 | keeps sending events to dead processes, for now it's a matter of 154 | not restarting the service too many times :P 155 | 156 | [hostapd_cli not handling termination with action file](https://www.spinics.net/lists/hostap/msg09087.html). 157 | 158 | However I think that this doesn't happen when running hostapd_cli as a 159 | daemon with the `-B` option. I might investigate adapting the scripts 160 | to run `hostapd_cli` as a daemon and bringing it to the foreground 161 | as a workaround. 162 | 163 | ### hostapd clears the default psk file on service start 164 | 165 | This is not a problem if all services start normally, but if you 166 | restart wpad manually, it will clear the psk file, breaking the slppsk 167 | daemon silently, until an entry gets added to the psk file. To fix 168 | this you can specify the location of the psk file, even if it just 169 | points to the default location, for example: 170 | 171 | `/etc/config/wireless`: 172 | 173 | ```conf 174 | # ... 175 | config wifi-iface 'wifinet3' 176 | # ... 177 | option ifname 'wlan-ifname' 178 | # ... 179 | option wpa_psk_file '/var/run/hostapd-wlan-ifname.psk' 180 | ``` 181 | 182 | ## Building 183 | 184 | This project gets compiled into an OpenWRT package file, the easiest 185 | way to do this is to use the SDK images provided by the OpenWRT team. 186 | These images are tagged based on target architecture and version, but 187 | since this is a script only package, any relatively recent SDK 188 | version and any architecture can be used to build the package. `podman` 189 | or `docker` is required. 190 | 191 | To build the package simply call the `build.sh` script like so: 192 | 193 | ```sh 194 | ./build.sh ./build/ 195 | ``` 196 | 197 | The resulting package will be copied to the `./build/` directory. 198 | 199 | ## Useful links 200 | 201 | * [Example script only package](https://forum.openwrt.org/t/how-to-add-a-shell-script-as-a-package-in-menuconfig/95766) 202 | * [How hostapd parses key parameters](https://github.com/michael-dev/hostapd/blob/f91680c15f80f0b617a0d2c369c8c1bb3dcf078b/src/ap/ap_config.c#L360-L364) 203 | * [OpenWRT development guide: Creating a package from your application](https://openwrt.org/docs/guide-developer/helloworld/chapter3) 204 | * [Example `wpa_psk_file`](https://android.googlesource.com/platform/external/wpa_supplicant_8/+/refs/heads/master/hostapd/hostapd.wpa_psk) 205 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | first_valid_command () { 5 | while (($# > 0)); do 6 | if command -v "$1" &> /dev/null; then 7 | printf "%s" "$1" 8 | return 0 9 | fi 10 | shift 11 | done 12 | return 1 13 | } 14 | 15 | if ! ENGINE="$(first_valid_command podman docker)"; then 16 | echo "Failed to find 'podman' or 'docker' command" 1>&2 17 | exit 1 18 | fi 19 | 20 | BIN_DIR="$1" 21 | # These shouldn't matter much since we're building a script only package 22 | SDK_TAG="${2:-"mips_24kc-23.05.0"}" 23 | SDK_REPO="${3:-"docker.io/openwrt/sdk"}" 24 | 25 | build () { 26 | local temp_dir="$1" 27 | "$ENGINE" run --rm -u root \ 28 | -v .:/builder/package/slppsk-hostapd \ 29 | -v "$temp_dir":/builder/bin/ \ 30 | -ti "$SDK_REPO":"$SDK_TAG" \ 31 | bash -c 'make defconfig 32 | make package/slppsk-hostapd/compile -j$(nproc)' 33 | cp "$temp_dir"/packages/*/base/slppsk-hostapd_*_all.ipk "$BIN_DIR" 34 | } 35 | 36 | with_temp_dir () { 37 | local temp_dir ret=0 cmd="$1" 38 | shift 39 | temp_dir="$(mktemp -d)" 40 | "$cmd" "$temp_dir" "$@" || ret=$? 41 | rm -rf "$temp_dir" 42 | return $ret 43 | } 44 | 45 | with_temp_dir build 46 | -------------------------------------------------------------------------------- /files/etc/config/slppsk: -------------------------------------------------------------------------------- 1 | # Uncommented options are required 2 | 3 | config password 4 | # Name of the wifi interface controlled by hostapd 5 | list ifname wlan0 6 | list ifname wlan1 7 | 8 | # Master password used to derive the pre shared 9 | # keys for each station 10 | option master_password changeme123 11 | 12 | # Max number of temp entries for this password 13 | #option max_temp_entries 20 14 | 15 | # VLAN ID assigned to stations connecting with PSKs from 16 | # this password, zero means no vlan 17 | #option vlanid 0 18 | 19 | # PSKs are trimmed to this length in bytes before being encoded 20 | # to base64, use multiples of 3 to avoid padding (==) 21 | #option ppsk_len_bytes 12 22 | 23 | #option wps 0 24 | 25 | #config iface 26 | # option ifname wlan0 27 | 28 | # Location of the psk file, default is in 29 | # "/var/run/hostapd-$ifname.psk" 30 | #option wpa_psk_file 31 | 32 | # Save the MAC addresses that have been successfully 33 | # authenticated to this interface using any of its 34 | # PSKs derived from passwords to this file. 35 | # Stations saved here will be automatically added every 36 | # time the service restarts, or across reboots if 37 | # this points to a non volatile location of the filesystem 38 | #option macs_file 39 | -------------------------------------------------------------------------------- /files/etc/init.d/hostapd_slppsk: -------------------------------------------------------------------------------- 1 | #!/bin/ash /etc/rc.common 2 | # shellcheck shell=dash 3 | 4 | USE_PROCD=1 5 | START=20 6 | STOP=20 7 | 8 | # Taken from https://github.com/python/cpython/blob/f4fcfdf8c593611f98b9358cc0c5604c15306465/Lib/shlex.py#L321-L332 9 | quote () { 10 | printf "'" 11 | # replaces ' with '"'"' 12 | sed "s/'/'"'"'"'"'"'"'/g" 13 | printf "'" 14 | } 15 | 16 | quote_elem () { 17 | printf "%s" "$1" | quote; printf " " 18 | } 19 | 20 | config_list_quoted () { 21 | config_list_foreach "$1" "$2" quote_elem 22 | } 23 | 24 | first_in () { 25 | local arg ref="$1" 26 | shift 27 | for arg in "$@"; do 28 | [ "$ref" = "$arg" ] && return 0 29 | done 30 | return 1 31 | } 32 | 33 | on_unique_args () { 34 | # Calls func on every unique argument 35 | local arg func="$1"; 36 | shift 37 | for arg in "$@"; do 38 | shift; 39 | first_in "$arg" "$@" || "$func" "$arg" 40 | done 41 | } 42 | 43 | config_iface () { 44 | local iface="$1" 45 | shift 46 | local macs_file wpa_psk_file ifname 47 | config_get ifname "$iface" ifname 48 | if ! first_in "$ifname" "$@"; then 49 | # Interface configured but no password uses it 50 | return; 51 | fi 52 | config_get macs_file "$iface" macs_file 53 | config_get wpa_psk_file "$iface" wpa_psk_file "" 54 | 55 | config_iface_defaults "$ifname" "$wpa_psk_file" "$macs_file" 56 | } 57 | 58 | config_iface_defaults () { 59 | local ifname="$1" wpa_psk_file="$2" macs_file="$3" 60 | if [ -z "$wpa_psk_file" ]; then 61 | wpa_psk_file="/var/run/hostapd-$ifname.psk" 62 | fi 63 | 64 | if ! [ -r "$macs_file" ]; then 65 | hostapd_slppsk "$ifname" init "$wpa_psk_file" 66 | else 67 | hostapd_slppsk "$ifname" init "$wpa_psk_file" "$macs_file" 68 | fi 69 | } 70 | 71 | config_password () { 72 | local password="$1" 73 | shift 74 | local master_pwd temp_entries vlanid ppsk_len wps params 75 | config_get master_pwd "$password" master_password 76 | config_get temp_entries "$password" max_temp_entries 20 77 | config_get vlanid "$password" vlanid 0 78 | config_get ppsk_len "$password" ppsk_len_bytes 12 79 | config_get_bool wps "$password" wps 0 80 | 81 | if [ "$wps" != 0 ]; then 82 | params="wps=1" 83 | fi 84 | if [ "$vlanid" != 0 ]; then 85 | params="$params vlanid=$vlanid" 86 | fi 87 | 88 | # It's tempting to init for one interface and then symlink 89 | # that password directory to the rest of the interfaces, 90 | # but that will mess with the sync as one lock file is held 91 | # for one interface, thus simultaneous writes could occur 92 | # when an event is fired for each interface 93 | 94 | eval "set -- $(config_list_quoted "$password" ifname)" 95 | for ifname in "$@"; do 96 | MASTER_PSK="$master_pwd" hostapd_slppsk \ 97 | "$ifname" init-ppsk \ 98 | "$temp_entries" \ 99 | "$params" \ 100 | "$ppsk_len" 101 | done 102 | } 103 | 104 | # shellcheck disable=SC2120 105 | get_implicit_ifaces () { 106 | eval "set -- $(config_foreach config_list_quoted password ifname)" 107 | on_unique_args quote_elem "$@" 108 | } 109 | 110 | start_instance () { 111 | local ifname="$1" pid_file 112 | pid_file="$(hostapd_slppsk "$ifname" pid-file)" 113 | 114 | procd_open_instance "$ifname" 115 | procd_set_param command hostapd_slppsk "$ifname" listen 116 | procd_set_param respawn 117 | procd_set_param stdout 1 118 | procd_set_param stderr 1 119 | procd_set_param pidfile "$pid_file" 120 | procd_close_instance 121 | } 122 | 123 | start_service () { 124 | local impl_iface 125 | config_load slppsk 126 | eval "set -- $(get_implicit_ifaces)" 127 | 128 | for impl_iface in "$@"; do 129 | config_iface_defaults "$impl_iface" "" "" 130 | done 131 | # Explicitly configured ifaces will overwrite the implicit ones 132 | config_foreach config_iface iface "$@" 133 | config_foreach config_password password 134 | for impl_iface in "$@"; do 135 | start_instance "$impl_iface" 136 | done 137 | } 138 | 139 | service_stopped () { 140 | config_load slppsk 141 | eval "set -- $(get_implicit_ifaces)" 142 | # TODO: Delete interfaces based on service instances 143 | for ifname in "$@"; do 144 | hostapd_slppsk "$ifname" remove 145 | done 146 | } 147 | 148 | service_triggers() { 149 | procd_add_reload_trigger slppsk 150 | } 151 | 152 | reload_service() { 153 | stop 154 | start 155 | } 156 | -------------------------------------------------------------------------------- /files/usr/bin/hostapd_slppsk: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ash 2 | # shellcheck shell=dash 3 | set -euo pipefail 4 | 5 | echoerr() { echo "$@" 1>&2; } 6 | 7 | LIB_DIR="$(dirname "$0")/../lib/hostapd_slppsk" 8 | 9 | if [ $# -lt 2 ]; then 10 | echoerr "Interface and command name must be given" 11 | exit 1 12 | fi 13 | 14 | WIFI_IFACE="$1" 15 | shift 16 | COMMAND="$1" 17 | shift 18 | 19 | # shellcheck source=../lib/hostapd_slppsk/manage_common.sh 20 | . "$LIB_DIR/manage_common.sh" 21 | # shellcheck source=../lib/hostapd_slppsk/iface_common.sh 22 | . "$LIB_DIR/iface_common.sh" 23 | 24 | instance_running () { 25 | local pid 26 | if ! [ -e "$PID_FILE" ]; then 27 | return 1 28 | fi 29 | pid="$(cat "$PID_FILE")" 30 | printf "%s" "$pid" 31 | kill -0 "$pid" 32 | } 33 | 34 | init_iface () { 35 | local pid 36 | if pid="$(instance_running)"; then 37 | echoerr "Instance running with PID $pid" 38 | return 1; 39 | fi 40 | rm -rf "$IFACE_CONFIG" 41 | mkdir -p "$IFACE_CONFIG" 42 | flock "$LOCK_FILE" "$LIB_DIR"/init_iface.sh "$WIFI_IFACE" "$@" 43 | } 44 | 45 | remove_iface () { 46 | local pid 47 | if pid="$(instance_running)"; then 48 | echoerr "Instance running with PID $pid" 49 | return 1; 50 | fi 51 | rm -rf "$IFACE_CONFIG" 52 | } 53 | 54 | case "$COMMAND" in 55 | "init") 56 | init_iface "$@" 57 | ;; 58 | "init-ppsk") 59 | flock "$LOCK_FILE" "$SCRIPT_DIR"/init_psk.sh "$@" 60 | ;; 61 | "listen") 62 | hostapd_cli () { 63 | exec hostapd_cli "$@" 64 | } 65 | hostapd_listen "$@" 66 | ;; 67 | "pid-file") 68 | printf "%s" "$PID_FILE" 69 | ;; 70 | "remove") 71 | remove_iface "$@" 72 | ;; 73 | *) 74 | echoerr "Invalid command $COMMAND" 75 | exit 1 76 | ;; 77 | esac 78 | -------------------------------------------------------------------------------- /files/usr/lib/hostapd_slppsk/add_permanent_ppsk.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ash 2 | # shellcheck shell=dash 3 | set -euo pipefail 4 | 5 | STA_ADDRESS="$3" 6 | 7 | SCRIPT_DIR="$(dirname "$0")" 8 | 9 | # shellcheck source=./iface_common.sh 10 | . "$SCRIPT_DIR/iface_common.sh" 11 | 12 | if grep -q -- "$STA_ADDRESS" "$PERM_MACS_FILE"; then 13 | log debug "Permanent PPSK already in list for $STA_ADDRESS" 14 | exit 15 | fi 16 | 17 | echo "$STA_ADDRESS" >> "$PERM_MACS_FILE" 18 | 19 | add_permanent_ppsk () { 20 | PERMANENT_ENTRY="$(get_entry)" 21 | TEMP_ENTRIES="$(grep -vxF "$PERMANENT_ENTRY" "$TEMP_PPSKS_FILE" || true)" 22 | log info "Adding permanent PPSK entry for $STA_ADDRESS" 23 | # Remove from temp (maybe unnecesary?) 24 | echo "$TEMP_ENTRIES" > "$TEMP_PPSKS_FILE" 25 | echo "$PERMANENT_ENTRY" >> "$PERM_PPSKS_FILE" 26 | } 27 | 28 | for_each_ppsk add_permanent_ppsk 29 | 30 | update_ppsks 31 | -------------------------------------------------------------------------------- /files/usr/lib/hostapd_slppsk/add_temp_ppsk.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ash 2 | # shellcheck shell=dash 3 | set -euo pipefail 4 | 5 | STA_ADDRESS="$3" 6 | 7 | SCRIPT_DIR="$(dirname "$0")" 8 | 9 | # shellcheck source=./iface_common.sh 10 | . "$SCRIPT_DIR/iface_common.sh" 11 | 12 | if grep -q -- "$STA_ADDRESS" "$PERM_MACS_FILE"; then 13 | log debug "Temp PPSK already in permanent list for $STA_ADDRESS" 14 | exit 15 | fi 16 | 17 | add_temp_ppsk () { 18 | local temp_entries 19 | if grep -q -- "$STA_ADDRESS" "$TEMP_PPSKS_FILE"; then 20 | log debug "Temp PPSK already in temp list for $STA_ADDRESS of $key_id" 21 | return 22 | fi 23 | touch "$OUT_OF_SYNC_FILE" 24 | temp_entries="$( 25 | head -n "$(($MAX_TEMP_ENTRIES - 1))" -- "$TEMP_PPSKS_FILE" 26 | )" 27 | log info "Adding temp PPSK for $STA_ADDRESS to $key_id" 28 | get_entry > "$TEMP_PPSKS_FILE" 29 | echo "$temp_entries" >> "$TEMP_PPSKS_FILE" 30 | } 31 | 32 | for_each_ppsk add_temp_ppsk 33 | 34 | if [ -e "$OUT_OF_SYNC_FILE" ]; then 35 | update_ppsks 36 | rm "$OUT_OF_SYNC_FILE" 37 | fi 38 | -------------------------------------------------------------------------------- /files/usr/lib/hostapd_slppsk/event_handler.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ash 2 | # shellcheck shell=dash 3 | set -euo pipefail 4 | 5 | EVENT="$2" 6 | 7 | SCRIPT_DIR="$(dirname "$0")" 8 | 9 | # shellcheck source=./iface_common.sh 10 | . "$SCRIPT_DIR/iface_common.sh" 11 | 12 | case "$EVENT" in 13 | "AP-STA-POSSIBLE-PSK-MISMATCH") 14 | flock "$LOCK_FILE" "$SCRIPT_DIR"/add_temp_ppsk.sh "$@" 15 | ;; 16 | 17 | #"EAPOL-4WAY-HS-COMPLETED") 18 | "AP-STA-CONNECTED") 19 | flock "$LOCK_FILE" "$SCRIPT_DIR"/add_permanent_ppsk.sh "$@" 20 | ;; 21 | esac 22 | -------------------------------------------------------------------------------- /files/usr/lib/hostapd_slppsk/iface_common.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ash 2 | # shellcheck shell=dash 3 | 4 | IFACE_CONFIG="${IFACE_CONFIG:-$(dirname "$SCRIPT_DIR")}" 5 | # shellcheck disable=SC2034 6 | { 7 | PPSKS_DIR="$IFACE_CONFIG/ppsks" 8 | LOCK_FILE="$IFACE_CONFIG/lock" 9 | PPSKS_FILE="$IFACE_CONFIG/merged.psk" 10 | PERM_MACS_FILE="$IFACE_CONFIG/perm_macs.txt" 11 | PID_FILE="$IFACE_CONFIG/pid" 12 | OUT_OF_SYNC_FILE="$IFACE_CONFIG/dirty" 13 | } 14 | 15 | log () { 16 | local level="$1" 17 | shift 18 | #if [ info = "$level" ]; then 19 | echo "$level:" "$@" 20 | #fi 21 | } 22 | 23 | for_each_ppsk () { 24 | local ppsk_dir key_id 25 | for ppsk_dir in "$PPSKS_DIR"/*; do 26 | if ! [ -e "$ppsk_dir" ]; then continue; fi 27 | key_id="$(basename "$ppsk_dir")" 28 | (. "$SCRIPT_DIR/key_common.sh"; "$@" "$key_id") 29 | done 30 | } 31 | 32 | add_ppsks () { 33 | local key_id="$1" 34 | { 35 | echo "# Entries for master key id $key_id" 36 | echo "# Permanent PPSKS 37 | " 38 | cat "$PERM_PPSKS_FILE" 39 | echo "# Temporary PPSKS (waiting for successful authentication) 40 | " 41 | cat "$TEMP_PPSKS_FILE" 42 | } >> "$PPSKS_FILE" 43 | } 44 | 45 | hostapd_cli_i () { 46 | hostapd_cli \ 47 | -i "$(basename "$IFACE_CONFIG")" "$@" 48 | } 49 | 50 | hostapd_reload () { 51 | hostapd_cli_i reload_wpa_psk > /dev/null 52 | } 53 | 54 | hostapd_listen () { 55 | hostapd_cli_i -r -a "$SCRIPT_DIR"/event_handler.sh "$@" 56 | } 57 | 58 | update_ppsks () { 59 | printf "%s" "" > "$PPSKS_FILE" 60 | for_each_ppsk add_ppsks 61 | hostapd_reload 62 | } 63 | 64 | get_entry () { 65 | echo "$KEY_PARAMS $STA_ADDRESS $(get_ppsk)" 66 | } 67 | 68 | get_ppsk () { 69 | printf "%s%s" \ 70 | "$MASTER_PSK" \ 71 | "$(echo "$STA_ADDRESS" | \ 72 | tr -dc a-fA-F0-9 | \ 73 | xxd -r -p)" | \ 74 | sha256sum | \ 75 | cut -d" " -f 1 | \ 76 | xxd -r -p | \ 77 | head -c "$PPSK_BYTE_LEN" | \ 78 | base64 79 | } 80 | -------------------------------------------------------------------------------- /files/usr/lib/hostapd_slppsk/init_iface.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ash 2 | # shellcheck shell=dash 3 | set -euo pipefail 4 | 5 | LIB_DIR="$(dirname "$0")" 6 | 7 | WIFI_IFACE="$1" 8 | PPSKS_SOURCE_FILE="$2" 9 | 10 | # shellcheck source=./manage_common.sh 11 | . "$LIB_DIR/manage_common.sh" 12 | 13 | file_resolve () { 14 | local file_path="$1" dir file 15 | dir="$(dirname "$file_path")" 16 | file="$(basename "$file_path")" 17 | printf "%s" "$(path_resolve "$dir")/$file" 18 | } 19 | 20 | # shellcheck source=./iface_common.sh 21 | . "$LIB_DIR/iface_common.sh" 22 | 23 | # We expect the iface directory to exists 24 | # and the lock to be engaged 25 | ln -s "$(path_resolve "$LIB_DIR")" "$SCRIPT_DIR" 26 | 27 | mkdir "$PPSKS_DIR" 28 | ln -s "$PPSKS_SOURCE_FILE" "$PPSKS_FILE" 29 | printf "%s" "" > "$PPSKS_FILE" 30 | 31 | if [ $# -gt 2 ]; then 32 | PERM_MACS_SOURCE_FILE="$(file_resolve "$3")" 33 | ln -s "$PERM_MACS_SOURCE_FILE" "$PERM_MACS_FILE" 34 | else 35 | touch "$PERM_MACS_FILE" 36 | fi 37 | 38 | -------------------------------------------------------------------------------- /files/usr/lib/hostapd_slppsk/init_psk.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ash 2 | # shellcheck shell=dash 3 | set -euo pipefail 4 | 5 | SCRIPT_DIR="$(dirname "$0")" 6 | 7 | echoerr() { echo "$@" 1>&2; } 8 | 9 | # shellcheck source=./iface_common.sh 10 | . "$SCRIPT_DIR/iface_common.sh" 11 | 12 | # Taken from https://github.com/python/cpython/blob/f4fcfdf8c593611f98b9358cc0c5604c15306465/Lib/shlex.py#L321-L332 13 | quote () { 14 | printf "'" 15 | # replaces ' with '"'"' 16 | sed "s/'/'"'"'"'"'"'"'/g" 17 | printf "'" 18 | } 19 | 20 | quote_arg () { 21 | printf "%s" "$1" | quote 22 | } 23 | 24 | params_file () { 25 | local value 26 | for arg in "$@"; do 27 | value="$(eval 'printf "%s" "$'"$arg"'"')" 28 | printf "%s=%s\n" "$arg" "$(quote_arg "$value")" 29 | done 30 | } 31 | 32 | get_psk_id () { 33 | local psk="$1" 34 | printf "%s" "$psk" | \ 35 | sha256sum | \ 36 | cut -d" " -f 1 | \ 37 | head -c12 38 | } 39 | 40 | check_mac_addr () { 41 | local addr="$1" 42 | # shellcheck disable=SC3010 43 | if ! printf "%s" "$addr" | grep -qE \ 44 | '^\s*([0-9A-Fa-f]{2}:){5}([0-9A-Fa-f]{2})\s*$'; then 45 | echoerr "invalid MAC address: '$addr'" 46 | return 1 47 | fi 48 | printf "%s" "$addr" | tr -dc 'a-fA-F0-9:' 49 | } 50 | 51 | MIN_PSK_LEN=12 52 | MIN_PPSK_BYTE_LEN=8 53 | 54 | if [ -z "${MASTER_PSK+x}" ]; then 55 | echoerr 'The master PSK must be exported to the MASTER_PSK' \ 56 | 'environment variable' 57 | exit 1 58 | fi 59 | if [ "${#MASTER_PSK}" -lt "$MIN_PSK_LEN" ]; then 60 | echoerr "The master PSK must be at least $MIN_PSK_LEN" \ 61 | "characters long" 62 | exit 1 63 | fi 64 | 65 | # shellcheck disable=SC2034 66 | MAX_TEMP_ENTRIES="$1" 67 | KEY_PARAMS="$2" 68 | PPSK_BYTE_LEN="$3" 69 | 70 | if [ "$MIN_PPSK_BYTE_LEN" -gt "$PPSK_BYTE_LEN" ]; then 71 | echoerr "The ppsks must be at least $MIN_PPSK_BYTE_LEN" \ 72 | "bytes long" 73 | exit 1 74 | fi 75 | 76 | key_id="$(get_psk_id "$MASTER_PSK")" 77 | # Don't import params file since it hasn't been created yet 78 | IGNORE_PARAMS=true 79 | . "$SCRIPT_DIR"/key_common.sh 80 | unset IGNORE_PARAMS 81 | 82 | mkdir "$KEY_CONFIG_DIR" 83 | # Create the params file and add the following vars 84 | params_file \ 85 | MASTER_PSK \ 86 | MAX_TEMP_ENTRIES \ 87 | KEY_PARAMS \ 88 | PPSK_BYTE_LEN > "$PARAMS_FILE" 89 | touch "$PERM_PPSKS_FILE" "$TEMP_PPSKS_FILE" 90 | 91 | # "Manually" add the entries to the ppsks file to avoid 92 | # rewriting each time a password is initialized 93 | while read -r STA_ADDRESS; do 94 | if ! STA_ADDRESS="$(check_mac_addr "$STA_ADDRESS")"; then 95 | continue 96 | fi 97 | get_entry >> "$PERM_PPSKS_FILE" 98 | done < "$PERM_MACS_FILE" 99 | 100 | add_ppsks "$key_id" 101 | hostapd_reload 102 | -------------------------------------------------------------------------------- /files/usr/lib/hostapd_slppsk/key_common.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ash 2 | # shellcheck shell=dash 3 | 4 | # shellcheck disable=SC2154 5 | KEY_CONFIG_DIR="$PPSKS_DIR/$key_id" 6 | # shellcheck disable=SC2034 7 | TEMP_PPSKS_FILE="$KEY_CONFIG_DIR/temp.psk" 8 | # shellcheck disable=SC2034 9 | PERM_PPSKS_FILE="$KEY_CONFIG_DIR/perm.psk" 10 | PARAMS_FILE="$KEY_CONFIG_DIR/params.env" 11 | 12 | if [ "${IGNORE_PARAMS:-false}" != "true" ]; then 13 | # shellcheck source=./params.env 14 | . "$PARAMS_FILE" 15 | fi 16 | -------------------------------------------------------------------------------- /files/usr/lib/hostapd_slppsk/manage_common.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ash 2 | # shellcheck shell=dash 3 | 4 | PROGRAM_DIR=/var/run/hostapd_slppsk 5 | #/var/run/hostapd_slppsk 6 | IFACE_CONFIG="$PROGRAM_DIR/$WIFI_IFACE" 7 | # shellcheck disable=SC2034 8 | SCRIPT_DIR="$IFACE_CONFIG/scripts" 9 | 10 | path_resolve () { 11 | local path="$1" 12 | (cd "$path" && pwd) 13 | } 14 | -------------------------------------------------------------------------------- /samples/var/run/hostapd_slppsk/wlan0/lock: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fakuivan/hostapd-slppsk/466ad6451740e14ad5a385b6700079832481f7c4/samples/var/run/hostapd_slppsk/wlan0/lock -------------------------------------------------------------------------------- /samples/var/run/hostapd_slppsk/wlan0/merged.psk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fakuivan/hostapd-slppsk/466ad6451740e14ad5a385b6700079832481f7c4/samples/var/run/hostapd_slppsk/wlan0/merged.psk -------------------------------------------------------------------------------- /samples/var/run/hostapd_slppsk/wlan0/perm_macs.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fakuivan/hostapd-slppsk/466ad6451740e14ad5a385b6700079832481f7c4/samples/var/run/hostapd_slppsk/wlan0/perm_macs.txt -------------------------------------------------------------------------------- /samples/var/run/hostapd_slppsk/wlan0/pid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fakuivan/hostapd-slppsk/466ad6451740e14ad5a385b6700079832481f7c4/samples/var/run/hostapd_slppsk/wlan0/pid -------------------------------------------------------------------------------- /samples/var/run/hostapd_slppsk/wlan0/ppsks/5b503791b99b/params.env: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fakuivan/hostapd-slppsk/466ad6451740e14ad5a385b6700079832481f7c4/samples/var/run/hostapd_slppsk/wlan0/ppsks/5b503791b99b/params.env -------------------------------------------------------------------------------- /samples/var/run/hostapd_slppsk/wlan0/ppsks/5b503791b99b/perm.psk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fakuivan/hostapd-slppsk/466ad6451740e14ad5a385b6700079832481f7c4/samples/var/run/hostapd_slppsk/wlan0/ppsks/5b503791b99b/perm.psk -------------------------------------------------------------------------------- /samples/var/run/hostapd_slppsk/wlan0/ppsks/5b503791b99b/temp.psk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fakuivan/hostapd-slppsk/466ad6451740e14ad5a385b6700079832481f7c4/samples/var/run/hostapd_slppsk/wlan0/ppsks/5b503791b99b/temp.psk -------------------------------------------------------------------------------- /samples/var/run/hostapd_slppsk/wlan0/ppsks/a883dafc480d/params.env: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fakuivan/hostapd-slppsk/466ad6451740e14ad5a385b6700079832481f7c4/samples/var/run/hostapd_slppsk/wlan0/ppsks/a883dafc480d/params.env -------------------------------------------------------------------------------- /samples/var/run/hostapd_slppsk/wlan0/ppsks/a883dafc480d/perm.psk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fakuivan/hostapd-slppsk/466ad6451740e14ad5a385b6700079832481f7c4/samples/var/run/hostapd_slppsk/wlan0/ppsks/a883dafc480d/perm.psk -------------------------------------------------------------------------------- /samples/var/run/hostapd_slppsk/wlan0/ppsks/a883dafc480d/temp.psk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fakuivan/hostapd-slppsk/466ad6451740e14ad5a385b6700079832481f7c4/samples/var/run/hostapd_slppsk/wlan0/ppsks/a883dafc480d/temp.psk --------------------------------------------------------------------------------