|
| 1 | +/* |
| 2 | + * SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD |
| 3 | + * |
| 4 | + * SPDX-License-Identifier: Apache-2.0 |
| 5 | + */ |
| 6 | + |
| 7 | +/* |
| 8 | + * Specialized and optimized PBKDF2-SHA1 implementation for Wi-Fi PSK |
| 9 | + * |
| 10 | + * Initially authored by Chien Wong([email protected]). |
| 11 | + */ |
| 12 | + |
| 13 | +/* |
| 14 | + * This implementation derives a Pre-Shared Key (PSK) for WPA/WPA2 using a custom PBKDF2-like approach. |
| 15 | + * |
| 16 | + * PBKDF2 (Password-Based Key Derivation Function 2) is a standard algorithm used to derive cryptographic keys |
| 17 | + * from a password and salt. It relies on iteratively applying a pseudorandom function, such as HMAC, to the input. |
| 18 | + * The derived key is designed to be computationally expensive to generate, making brute-force attacks more difficult. |
| 19 | + * |
| 20 | + * In standard PBKDF2, the process is as follows: |
| 21 | + * 1. Combine the password and salt (SSID in WPA). |
| 22 | + * 2. Compute HMAC for this combination, iteratively applying the previous HMAC output as input for the next iteration. |
| 23 | + * 3. XOR all intermediate results to produce the final derived key. |
| 24 | + * |
| 25 | + * This implementation adapts PBKDF2 for WPA/WPA2 by leveraging the SHA1 hashing algorithm and fixed parameters: |
| 26 | + * - The password is up to 63 characters long. |
| 27 | + * - The SSID (salt) is up to 32 bytes. |
| 28 | + * - The iteration count is fixed at 4096, as required by WPA. |
| 29 | + * - The output key length is 32 bytes, suitable for WPA. |
| 30 | + * |
| 31 | + * Key Differences from Standard PBKDF2: |
| 32 | + * - Instead of a general-purpose pseudorandom function, this implementation uses a fixed combination of SHA1 blocks. |
| 33 | + * - The logic for handling HMAC is explicitly implemented to optimize for this specific use case. |
| 34 | + * - Padding and block alignment are carefully managed to fit within hardware constraints (e.g., the ESP32 SHA1 hardware). |
| 35 | + * |
| 36 | + * How This Implementation Works: |
| 37 | + * 1. The `fast_psk_f` function computes one segment of the derived key. It takes as input: |
| 38 | + * - The password. |
| 39 | + * - The SSID. |
| 40 | + * - A counter value (`count`) that varies for each segment. |
| 41 | + * 2. HMAC-SHA1 is implemented explicitly using: |
| 42 | + * - An inner padding block (`ipad`) initialized with 0x36 XORed with the password. |
| 43 | + * - An outer padding block (`opad`) initialized with 0x5C XORed with the password. |
| 44 | + * 3. Intermediate hashes (`U1`, `U2`, ..., `Un`) are computed iteratively. Each `U` value depends on the previous one. |
| 45 | + * - `U1` is derived from the password, SSID, and counter. |
| 46 | + * - Subsequent `U` values are derived using SHA1 on the previous `U` value. |
| 47 | + * 4. All intermediate values are XORed together to produce the final segment of the key. |
| 48 | + * 5. The `esp_fast_psk` function combines two invocations of `fast_psk_f` to produce the complete 32-byte key. |
| 49 | + * - The first invocation computes the first 16 bytes. |
| 50 | + * - The second invocation computes the second 16 bytes. |
| 51 | + * |
| 52 | + * - The code uses the ESP SHA1 hardware accelerator for faster computation. |
| 53 | + */ |
| 54 | + |
| 55 | +#include "fastpsk.h" |
| 56 | + |
| 57 | +#include <string.h> |
| 58 | +#include "soc/soc_caps.h" |
| 59 | + |
| 60 | +#if SOC_SHA_SUPPORT_PARALLEL_ENG |
| 61 | +#include "sha/sha_parallel_engine.h" |
| 62 | +#else |
| 63 | +#include "sha/sha_core.h" |
| 64 | +#endif |
| 65 | +#include "esp_log.h" |
| 66 | + |
| 67 | +#ifndef PUT_UINT32_BE |
| 68 | +#define PUT_UINT32_BE(n, b, i) \ |
| 69 | +{ \ |
| 70 | + (b)[(i)] = (unsigned char)((n) >> 24); \ |
| 71 | + (b)[(i) + 1] = (unsigned char)((n) >> 16); \ |
| 72 | + (b)[(i) + 2] = (unsigned char)((n) >> 8); \ |
| 73 | + (b)[(i) + 3] = (unsigned char)((n)); \ |
| 74 | +} |
| 75 | +#endif |
| 76 | + |
| 77 | +#define FAST_PSK_SHA1_BLOCKS 2 |
| 78 | +#define SHA1_BLOCK_SZ 64 |
| 79 | +#define SHA1_BLOCK_SZ_WORDS 16 |
| 80 | +#define SHA1_OUTPUT_SZ 20 |
| 81 | +#define SHA1_OUTPUT_SZ_WORDS 5 |
| 82 | +#define FAST_PSK_SHA1_BLOCKS_BUF_BYTES (FAST_PSK_SHA1_BLOCKS * SHA1_BLOCK_SZ) |
| 83 | +#define FAST_PSK_SHA1_BLOCKS_BUF_WORDS (FAST_PSK_SHA1_BLOCKS * SHA1_BLOCK_SZ / 4) |
| 84 | + |
| 85 | +/* Union to represent SHA1 HMAC blocks */ |
| 86 | +union hmac_block { |
| 87 | + union { |
| 88 | + uint32_t words[SHA1_BLOCK_SZ / 4]; /* SHA1 block in words */ |
| 89 | + uint8_t bytes[SHA1_BLOCK_SZ]; /* SHA1 block in bytes */ |
| 90 | + } block[FAST_PSK_SHA1_BLOCKS]; |
| 91 | + uint8_t whole_bytes[FAST_PSK_SHA1_BLOCKS_BUF_BYTES]; /* Complete block as bytes */ |
| 92 | + uint32_t whole_words[FAST_PSK_SHA1_BLOCKS_BUF_WORDS]; /* Complete block as words */ |
| 93 | +}; |
| 94 | +_Static_assert(sizeof(union hmac_block) == 128, "Incorrect layout of hmac_block"); |
| 95 | + |
| 96 | +/* Structure to hold HMAC context */ |
| 97 | +struct fast_psk_context { |
| 98 | + union hmac_block inner, outer; /* Inner and outer padding */ |
| 99 | + uint32_t sum[SHA1_OUTPUT_SZ_WORDS]; /* Intermediate hash result */ |
| 100 | +}; |
| 101 | + |
| 102 | +/* Acquire SHA1 hardware for exclusive use */ |
| 103 | +static inline void sha1_setup(void) |
| 104 | +{ |
| 105 | +#if SOC_SHA_SUPPORT_PARALLEL_ENG |
| 106 | + esp_sha_lock_engine(SHA1); |
| 107 | +#else |
| 108 | + esp_sha_acquire_hardware(); |
| 109 | +#endif |
| 110 | +} |
| 111 | + |
| 112 | +/* Release SHA1 hardware */ |
| 113 | +static inline void sha1_teardown(void) |
| 114 | +{ |
| 115 | +#if SOC_SHA_SUPPORT_PARALLEL_ENG |
| 116 | + esp_sha_unlock_engine(SHA1); |
| 117 | +#else |
| 118 | + esp_sha_release_hardware(); |
| 119 | +#endif |
| 120 | +} |
| 121 | + |
| 122 | +/* |
| 123 | + * Pads the given HMAC block context with the appropriate SHA1 padding. |
| 124 | + * Length is the number of bytes of actual data in the block. |
| 125 | + */ |
| 126 | +static void pad_blocks(union hmac_block *ctx, size_t len) |
| 127 | +{ |
| 128 | + size_t bits = len << 3; /* Convert length to bits */ |
| 129 | + uint8_t *bytes = ctx->whole_bytes; |
| 130 | + bytes[len] = 0x80; /* Append 0x80 as per SHA1 padding rules */ |
| 131 | + |
| 132 | + // Set all remaining bytes to 0 |
| 133 | + memset(&bytes[len + 1], 0, FAST_PSK_SHA1_BLOCKS_BUF_BYTES - (len + 1)); |
| 134 | + |
| 135 | + /* |
| 136 | + * Simplified PUT_UINT64_BE(bits, bytes, FAST_PSK_SHA1_BLOCKS_BUF_BYTES - 8). |
| 137 | + * Since len < 128 => bits < 1024, we only need to update the two least significant |
| 138 | + * bytes, actually. |
| 139 | + */ |
| 140 | + PUT_UINT32_BE(bits, bytes, FAST_PSK_SHA1_BLOCKS_BUF_BYTES - 4); |
| 141 | +} |
| 142 | + |
| 143 | +/* |
| 144 | + * Performs SHA1 hash operation on two consecutive blocks. |
| 145 | + * Input: blocks array (two blocks of 64 bytes each), output (20-byte digest). |
| 146 | + */ |
| 147 | +#if CONFIG_IDF_TARGET_ESP32 |
| 148 | +static inline void write32_be(uint32_t n, uint8_t out[4]) |
| 149 | +{ |
| 150 | +#if defined(__GNUC__) && __GNUC__ >= 4 && __BYTE_ORDER == __LITTLE_ENDIAN |
| 151 | + *(uint32_t *)(out) = __builtin_bswap32(n); |
| 152 | +#else |
| 153 | + out[0] = (n >> 24) & 0xff; |
| 154 | + out[1] = (n >> 16) & 0xff; |
| 155 | + out[2] = (n >> 8) & 0xff; |
| 156 | + out[3] = n & 0xff; |
| 157 | +#endif |
| 158 | +} |
| 159 | +#endif /* CONFIG_IDF_TARGET_ESP32 */ |
| 160 | + |
| 161 | +void sha1_op(uint32_t blocks[FAST_PSK_SHA1_BLOCKS_BUF_WORDS], uint32_t output[SHA1_OUTPUT_SZ_WORDS]) |
| 162 | +{ |
| 163 | + /* First block */ |
| 164 | + esp_sha_block(SHA1, blocks, true); |
| 165 | + /* Second block */ |
| 166 | + esp_sha_block(SHA1, &blocks[SHA1_BLOCK_SZ_WORDS], false); |
| 167 | + /* Read the final digest */ |
| 168 | + esp_sha_read_digest_state(SHA1, output); |
| 169 | + |
| 170 | +#if CONFIG_IDF_TARGET_ESP32 |
| 171 | + for (int i = 0; i < SHA1_OUTPUT_SZ_WORDS; i++) { |
| 172 | + write32_be(output[i], ((uint8_t*) output) + i * 4); |
| 173 | + } |
| 174 | +#endif /* CONFIG_IDF_TARGET_ESP32 */ |
| 175 | +} |
| 176 | + |
| 177 | +/* |
| 178 | + * Implements the PBKDF2-HMAC-SHA1 function for WPA key derivation. |
| 179 | + * - password: The passphrase (up to 63 bytes). |
| 180 | + * - password_len: Length of the passphrase. |
| 181 | + * - ssid: The SSID (up to 32 bytes). |
| 182 | + * - ssid_len: Length of the SSID. |
| 183 | + * - count: The iteration counter. |
| 184 | + * - digest: Output buffer for the resulting digest (20 bytes). |
| 185 | + */ |
| 186 | +void fast_psk_f(const char *password, size_t password_len, const uint8_t *ssid, size_t ssid_len, uint32_t count, uint8_t digest[SHA1_OUTPUT_SZ]) |
| 187 | +{ |
| 188 | + struct fast_psk_context ctx_, *ctx = &ctx_; |
| 189 | + size_t i; |
| 190 | + |
| 191 | + /* Clear the context */ |
| 192 | + memset(ctx, 0, sizeof(*ctx)); |
| 193 | + |
| 194 | + /* Initialize inner and outer padding */ |
| 195 | + memset(ctx->outer.block[0].bytes, 0x5c, SHA1_BLOCK_SZ); |
| 196 | + memset(ctx->inner.block[0].bytes, 0x36, SHA1_BLOCK_SZ); |
| 197 | + |
| 198 | + /* XOR the password into the padding */ |
| 199 | + for (i = 0; i < password_len; ++i) { |
| 200 | + ctx->outer.block[0].bytes[i] ^= password[i]; |
| 201 | + ctx->inner.block[0].bytes[i] ^= password[i]; |
| 202 | + } |
| 203 | + |
| 204 | + /* Prepare the first input block for HMAC (S || i) */ |
| 205 | + /* Copy SSID */ |
| 206 | + memcpy(ctx->inner.block[1].bytes, ssid, ssid_len); |
| 207 | + /* Append the counter */ |
| 208 | + PUT_UINT32_BE(count, ctx->inner.block[1].bytes, ssid_len); |
| 209 | + /* Pad the block */ |
| 210 | + pad_blocks(&ctx->inner, SHA1_BLOCK_SZ + ssid_len + 4); |
| 211 | + |
| 212 | + sha1_setup(); |
| 213 | + |
| 214 | + uint32_t *pi, *po; |
| 215 | + pi = ctx->inner.whole_words; |
| 216 | + po = ctx->outer.whole_words; |
| 217 | + |
| 218 | + // T1 = SHA1(K ^ ipad, S || i) |
| 219 | + sha1_op(pi, ctx->outer.block[1].words); |
| 220 | + |
| 221 | + // U1 = SHA1(K ^ opad, T1) |
| 222 | + pad_blocks(&ctx->outer, SHA1_BLOCK_SZ + SHA1_OUTPUT_SZ); |
| 223 | + uint32_t *inner_blk1 = ctx->inner.block[1].words; |
| 224 | + uint32_t *outer_blk1 = ctx->outer.block[1].words; |
| 225 | + uint32_t *sum = ctx->sum; |
| 226 | + |
| 227 | + sha1_op(po, inner_blk1); |
| 228 | + /* Copy result to the sum */ |
| 229 | + memcpy(sum, inner_blk1, SHA1_OUTPUT_SZ); |
| 230 | + pad_blocks(&ctx->inner, SHA1_BLOCK_SZ + SHA1_OUTPUT_SZ); |
| 231 | + |
| 232 | + /* Iterate for remaining 4096 - 1 times */ |
| 233 | + for (i = 1; i < 4096; ++i) { |
| 234 | + /* Compute Tn and Un */ |
| 235 | + // Tn = SHA1(K ^ ipad, Un-1) |
| 236 | + sha1_op(pi, outer_blk1); |
| 237 | + // Un = SHA1(K ^ opad, Tn) |
| 238 | + sha1_op(po, inner_blk1); |
| 239 | + |
| 240 | + /* XOR the results to accumulate into F */ |
| 241 | + // F = U1 ^ U2 ^ ... Un |
| 242 | + for (size_t j = 0; j < SHA1_OUTPUT_SZ_WORDS; ++j) { |
| 243 | + sum[j] ^= inner_blk1[j]; |
| 244 | + } |
| 245 | + } |
| 246 | + |
| 247 | + sha1_teardown(); |
| 248 | + |
| 249 | + /* Copy the final result to the output digest */ |
| 250 | + memcpy(digest, sum, SHA1_OUTPUT_SZ); |
| 251 | + |
| 252 | + /* Clear sensitive data */ |
| 253 | + memset(ctx, 0, sizeof(*ctx)); |
| 254 | +} |
| 255 | + |
| 256 | +int esp_fast_psk(const char *password, size_t password_len, const uint8_t *ssid, size_t ssid_len, size_t iterations, uint8_t *output, size_t output_len) |
| 257 | +{ |
| 258 | + if (!(ssid_len <= 32 && password_len <= 63 && iterations == 4096 && output_len == 32)) { |
| 259 | + return -1; /* Invalid input parameters */ |
| 260 | + } |
| 261 | + |
| 262 | + /* Compute the first 16 bytes of the PSK */ |
| 263 | + fast_psk_f(password, password_len, ssid, ssid_len, 2, output); |
| 264 | + |
| 265 | + /* Replicate the first 16 bytes to form the second half temporarily */ |
| 266 | + memcpy(output + SHA1_OUTPUT_SZ, output, 32 - SHA1_OUTPUT_SZ); |
| 267 | + |
| 268 | + /* Compute the second 16 bytes of the PSK */ |
| 269 | + fast_psk_f(password, password_len, ssid, ssid_len, 1, output); |
| 270 | + |
| 271 | + return 0; /* Success */ |
| 272 | +} |
0 commit comments