diff --git a/docs/configuration.md b/docs/configuration.md index 728d245a7..453e691d0 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -19,6 +19,11 @@ Here are some recommendations to avoid them and build-time flags you can change. - `ASYNCWEBSERVER_USE_CHUNK_INFLIGHT`: inflight control for chunked responses. If you need to serve chunk requests with a really low buffer (which should be avoided), you can set `-D ASYNCWEBSERVER_USE_CHUNK_INFLIGHT=0` to disable the in-flight control. +- `ASYNCWEBSERVER_DIGEST_NONCE_TTL`: validity window, in seconds, of a Digest authentication nonce (default: 60). + Digest replay protection uses stateless signed nonces: beyond this window a nonce is stale + and clients silently re-authenticate via the `stale=true` challenge directive. Set to 0 to disable expiry + (not recommended: widens the replay window). Can also be tuned at runtime with `server.setDigestNonceTimeout()`. + > [!NOTE] > This relates to ESP32 only, ESP8266 uses different ESPAsyncTCP lib that does not has this build options diff --git a/docs/middleware.md b/docs/middleware.md index b829ffbbd..3ba430591 100644 --- a/docs/middleware.md +++ b/docs/middleware.md @@ -116,6 +116,76 @@ myHandler.addMiddleware(&authMiddleware); // add authentication to a specific ha See the [Auth example here](https://github.com/ESP32Async/ESPAsyncWebServer/blob/master/examples/arduino/Auth/Auth.ino). +### Digest authentication: replay protection + +Digest authentication includes server-side protections against replay attacks, using a **stateless** scheme +(no per-nonce storage, keeping the server's memory footprint constant): + +- **Signed nonce**: each challenge nonce embeds its issuing time and is signed with a per-server-instance + secret (`HMAC-MD5`). A nonce not issued by this server is rejected, and an expired nonce triggers a + new challenge with `stale=true` (RFC 7616 §3.3), so compliant clients (browsers, curl) re-authenticate + silently without prompting the user again. +- **Opaque**: each challenge carries an opaque derived from the instance secret, identifying the server + instance that issued the nonce. + +The nonce validity window defaults to 60 seconds (`ASYNCWEBSERVER_DIGEST_NONCE_TTL` build flag) and can be +tuned at runtime: + +```c++ +server.setDigestNonceTimeout(300); // extend nonce lifetime to 5 minutes +``` + +Changing the instance secret instantly revokes every nonce in circulation (all clients get re-challenged): + +```c++ +uint8_t secret[16] = {/* 16 random bytes */}; +server.setDigestSecret(secret); // kill-switch: invalidates all outstanding nonces +``` + +> [!NOTE] +> As with any stateless scheme, a captured `Authorization: Digest ...` header remains valid for the +> remainder of the nonce TTL. The library does not keep the raw request-target in memory, so the +> header `uri` field is not checked by default — to bind a header to the resource it was issued for +> (RFC 7616 §3.4.6), pass the expected request-target with `authenticate(..., nonce, opaque, uri)` +> (see below). +> For one-shot semantics, keep the TTL short and serve Digest-protected endpoints over HTTPS. + +For custom validation of every digest parameter, `AsyncWebServerRequest::authenticate()` accepts +optional `nonce`, `opaque` and `uri` arguments — these only apply to the Digest auth scheme and are +ignored otherwise. A NULL argument lets the library validate it automatically, while a non-NULL +value is compared verbatim against the caller-supplied value: + +```c++ +// the library validates nonce and opaque (header uri is left unchecked) +request->authenticate("admin", "admin", "MyApp"); + +// the caller takes over the uri check and binds the header to a request-target +// (nonce/opaque are still validated by the library) +request->authenticate("admin", "admin", "MyApp", false, NULL, NULL, "/my/custom/uri"); +``` + +### Re-authentication required + +`AsyncWebServerRequest::reAuthRequired()` returns true when the last `authenticate()` call failed +for a reason that a re-challenge can resolve, i.e. re-sending the same credentials against a fresh +challenge will eventually succeed. + +With Digest auth: the credentials were correct and the nonce was issued by this server, but it +expired. `requestAuthentication()` then automatically issues the next challenge with `stale=true` +(RFC 7616 §3.3), so compliant clients (browsers, curl) re-authenticate silently without prompting +the user again — no special handling is needed from your code: + +```c++ +if (!request->authenticate("admin", "admin", "MyApp")) { + // reAuthRequired() is true when only the nonce expired: the challenge issued + // below will carry stale=true and the client will retry silently + request->requestAuthentication(AsyncAuthType::AUTH_DIGEST, "MyApp"); + return; +} +``` + +Future authentication mechanisms may reuse the same flag for their own equivalent conditions. + ### Migration to Middleware to improve performance and memory usage - `AsyncEventSource.authorizeConnect(...)` => do not use this method anymore: add a common `AsyncAuthorizationMiddleware` to the handler or server, and make sure to add it AFTER the `AsyncAuthenticationMiddleware` if you use authentication. diff --git a/examples/arduino/Auth/Auth.ino b/examples/arduino/Auth/Auth.ino index b2dfb89a1..948fca237 100644 --- a/examples/arduino/Auth/Auth.ino +++ b/examples/arduino/Auth/Auth.ino @@ -157,12 +157,15 @@ void setup() { .addMiddleware(&digestAuth); // digest authentication with hash + // protected with a hash: MD5(user:realm:pass) so no plain password is stored in RAM + // this is the privileged endpoint used by test_digest.sh to prove credentials are + // checked against the pre-computed hash instead of the plain password // curl -v -u admin:admin --digest http://192.168.4.1/auth-digest-hash server .on( "/auth-digest-hash", HTTP_GET, [](AsyncWebServerRequest *request) { - request->send(200, "text/plain", "Hello, world!"); + request->send(200, "text/plain", "DIGEST-HASH-ONLY: sensitive action"); } ) .addMiddleware(&digestAuthHash); @@ -184,6 +187,28 @@ void setup() { ) .addMiddlewares({&complexAuth, &authz}); + // digest authentication with caller-enforced uri binding (RFC 7616 §3.4.6): + // the library does not keep the raw request-target in memory, so binding the header + // "uri" field to the expected request-target is done explicitly by the caller. + // Nonce and opaque are still validated by the library. + static AsyncMiddlewareFunction digestUriAuth([](AsyncWebServerRequest *request, ArMiddlewareNext next) { + if (request->authenticate("admin", "admin", "MyApp", false, NULL, NULL, "/auth-digest-bound")) { + next(); + } else { + request->requestAuthentication(AsyncAuthType::AUTH_DIGEST, "MyApp"); + } + }); + // curl -v --digest -u admin:admin http://192.168.4.1/auth-digest-bound => OK + // a digest header issued for another URI (uri="/auth-digest") => 401 + server + .on( + "/auth-digest-bound", HTTP_GET, + [](AsyncWebServerRequest *request) { + request->send(200, "text/plain", "URI-BOUND: sensitive action"); + } + ) + .addMiddleware(&digestUriAuth); + // Bearer authentication with a shared key // curl -v -H "Authorization: Bearer shared-secret-key" http://192.168.4.1/auth-bearer-shared-key => OK server diff --git a/examples/arduino/Auth/test_digest.sh b/examples/arduino/Auth/test_digest.sh new file mode 100755 index 000000000..d584ed945 --- /dev/null +++ b/examples/arduino/Auth/test_digest.sh @@ -0,0 +1,138 @@ +#!/bin/bash + +# Digest authentication anti-replay test +# Tests the digest endpoints defined in Auth.ino +# +# The library validates the signed nonce and the opaque, but does NOT check the +# header "uri" field by default (the raw request-target is not kept in memory): +# a captured "Authorization: Digest ..." header stays usable within the nonce TTL +# on any endpoint sharing the same credentials and realm. To bind a header to a +# request-target, the caller must pass the expected uri explicitly +# (see /auth-digest-bound, implemented in Auth.ino). +# +# Usage: ./test_digest.sh [SERVER_IP] [SERVER_PORT] + +SERVER_IP="${1:-192.168.4.1}" +SERVER_PORT="${2:-80}" +BASE_URL="http://${SERVER_IP}:${SERVER_PORT}" +AUTH_USER="admin" +AUTH_PASS="admin" +REALM="MyApp" + +echo "Testing Digest auth anti-replay protections at $BASE_URL" +echo "=====================================================" + +NB_PASS=0 +NB_FAIL=0 + +ok() { echo "✅ PASS ($1)"; ((NB_PASS++)); } +ko() { echo "❌ FAIL ($1)"; ((NB_FAIL++)); } + +# check(status, expected, description) +check() { + if [ "$1" = "$2" ]; then ok "$3: $1"; else ko "$3: expected $2, got $1"; fi +} + +# Portable MD5 of stdin, hex-only output (Linux: md5sum; OpenSSL CLI; macOS: BSD md5) +md5s() { + if command -v md5sum >/dev/null 2>&1; then + md5sum | cut -d' ' -f1 + elif command -v openssl >/dev/null 2>&1; then + openssl dgst -md5 -r | cut -d' ' -f1 + else + md5 -q + fi +} + +# get_challenge(path): prints the last received WWW-Authenticate challenge +get_challenge() { + curl -s -i "$BASE_URL$1" | grep -i "^WWW-Authenticate:" | tail -1 | cut -d' ' -f2- | tr -d '\r' +} + +extract() { + echo "$1" | grep -o "$2=\"[^\"]*\"" | cut -d'"' -f2 +} + +# build_auth(nonce, opaque, uri): prints a valid Authorization header for admin/admin +build_auth() { + local nonce="$1" opaque="$2" uri="$3" + local cnonce ha1 ha2 resp + cnonce=$(printf '%s%s' "$nonce" "$opaque" | md5s | cut -c1-16) + ha1=$(printf '%s:%s:%s' "$AUTH_USER" "$REALM" "$AUTH_PASS" | md5s) + ha2=$(printf 'GET:%s' "$uri" | md5s) + resp=$(printf '%s:%s:00000001:%s:auth:%s' "$ha1" "$nonce" "$cnonce" "$ha2" | md5s) + echo "Digest username=\"$AUTH_USER\", realm=\"$REALM\", nonce=\"$nonce\", uri=\"$uri\", response=\"$resp\", cnonce=\"$cnonce\", nc=00000001, qop=auth, opaque=\"$opaque\"" +} + +# 1. legitimate digest auth on both endpoints (curl --digest) +STATUS=$(curl -s -o /dev/null -w "%{http_code}" --digest -u "$AUTH_USER:$AUTH_PASS" "$BASE_URL/auth-digest") +check "$STATUS" "200" "1a. legit digest auth /auth-digest" +STATUS=$(curl -s -o /dev/null -w "%{http_code}" --digest -u "$AUTH_USER:$AUTH_PASS" "$BASE_URL/auth-digest-hash") +check "$STATUS" "200" "1b. legit digest auth /auth-digest-hash" + +# 2. cross-endpoint replay: an Authorization header captured for /auth-digest +# (replayed verbatim: response hash still computed for uri="/auth-digest") +# is accepted by default on /auth-digest-hash, which has the same credentials +# and realm: no raw request-target is kept, so the header uri is not checked. +CH=$(get_challenge /auth-digest) +NONCE=$(extract "$CH" nonce) +OPAQUE=$(extract "$CH" opaque) +AUTH=$(build_auth "$NONCE" "$OPAQUE" "/auth-digest") +STATUS=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: $AUTH" "$BASE_URL/auth-digest-hash") +check "$STATUS" "200" "2. cross-endpoint replay accepted by default (no uri check)" + +# 3. forged nonce must be rejected even with valid credentials and matching uri +CH=$(get_challenge /auth-digest) +NONCE=$(extract "$CH" nonce) +OPAQUE=$(extract "$CH" opaque) +# 40 chars, the exact length of a genuine nonce: the MAC, not the length, must reject it +FORGED="deadbeefdeadbeefdeadbeefdeadbeefdeadbeef" +AUTH=$(build_auth "$FORGED" "$OPAQUE" "/auth-digest") +STATUS=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: $AUTH" "$BASE_URL/auth-digest") +check "$STATUS" "401" "3. forged nonce rejected" + +# 4. foreign opaque must be rejected +AUTH=$(build_auth "$NONCE" "00000000000000000000000000000000" "/auth-digest") +STATUS=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: $AUTH" "$BASE_URL/auth-digest") +check "$STATUS" "401" "4. foreign opaque rejected" + +# 5. query string is NOT part of the default validation (no uri check without binding) +STATUS=$(curl -s -o /dev/null -w "%{http_code}" --digest -u "$AUTH_USER:$AUTH_PASS" "$BASE_URL/auth-digest?a=1") +check "$STATUS" "200" "5a. legit digest auth with query string" +CH=$(get_challenge /auth-digest) +NONCE=$(extract "$CH" nonce) +OPAQUE=$(extract "$CH" opaque) +AUTH=$(build_auth "$NONCE" "$OPAQUE" "/auth-digest") +STATUS=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: $AUTH" "$BASE_URL/auth-digest?a=1") +check "$STATUS" "200" "5b. replay with different query string accepted by default (no uri check)" + +# 6. duplicated nonce must be rejected: the first occurrence (read by the stateless +# signature/TTL validation) must not launder a second, different occurrence (over +# which the response hash is computed). Regression test for the first-vs-last nonce +# parser divergence that allowed replaying an expired captured header. +CH=$(get_challenge /auth-digest) +NONCE=$(extract "$CH" nonce) +OPAQUE=$(extract "$CH" opaque) +LEGIT=$(build_auth "$NONCE" "$OPAQUE" "/auth-digest") # valid header, response computed over NONCE +FRESH_NONCE=$(extract "$(get_challenge /auth-digest)" nonce) +DUP_AUTH=${LEGIT/nonce=\"$NONCE\"/nonce=\"$FRESH_NONCE\", nonce=\"$NONCE\"} +STATUS=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: $DUP_AUTH" "$BASE_URL/auth-digest") +check "$STATUS" "401" "6. duplicated nonce rejected" + +# 7. uri binding enforced by the caller (RFC 7616 §3.4.6): the endpoint +# /auth-digest-bound binds the header uri to its own path, via +# request->authenticate(..., "/auth-digest-bound") +STATUS=$(curl -s -o /dev/null -w "%{http_code}" --digest -u "$AUTH_USER:$AUTH_PASS" "$BASE_URL/auth-digest-bound") +check "$STATUS" "200" "7a. legit digest auth with caller-enforced uri binding" + +# 8. a header issued for another endpoint must be rejected on the uri-bound endpoint +CH=$(get_challenge /auth-digest) +NONCE=$(extract "$CH" nonce) +OPAQUE=$(extract "$CH" opaque) +AUTH=$(build_auth "$NONCE" "$OPAQUE" "/auth-digest") +STATUS=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: $AUTH" "$BASE_URL/auth-digest-bound") +check "$STATUS" "401" "8. cross-endpoint replay rejected with caller-enforced uri binding" + +echo "" +echo "Results: $NB_PASS passed, $NB_FAIL failed" +[ "$NB_FAIL" -eq 0 ] diff --git a/src/AsyncEventSource.cpp b/src/AsyncEventSource.cpp index 11e941b60..26d4e49fb 100644 --- a/src/AsyncEventSource.cpp +++ b/src/AsyncEventSource.cpp @@ -67,7 +67,7 @@ static String generateEventMessage(const char *message, const char *event, uint3 } // a message is a multi-line string - char *nextLine = NULL; + char *nextLine = nullptr; if (nextN != NULL && nextR != NULL) { // windows line-ending \r\n if (nextR + 1 == nextN) { // normal \r\n sequence diff --git a/src/AsyncEventSource.h b/src/AsyncEventSource.h index 0bff368c2..f01c58980 100644 --- a/src/AsyncEventSource.h +++ b/src/AsyncEventSource.h @@ -96,7 +96,7 @@ class AsyncEventSourceClient { * @return true if message was placed in a queue * @return false if queue is full */ - bool send(const char *message, const char *event = NULL, uint32_t id = 0, uint32_t reconnect = 0); + bool send(const char *message, const char *event = nullptr, uint32_t id = 0, uint32_t reconnect = 0); bool send(const String &message, const String &event, uint32_t id = 0, uint32_t reconnect = 0) { return send(message.c_str(), event.c_str(), id, reconnect); } @@ -234,7 +234,7 @@ class AsyncEventSource : public AsyncWebHandler { * @param reconnect client's reconnect timeout * @return SendStatus if message was placed in any/all/part of the client's queues */ - SendStatus send(const char *message, const char *event = NULL, uint32_t id = 0, uint32_t reconnect = 0); + SendStatus send(const char *message, const char *event = nullptr, uint32_t id = 0, uint32_t reconnect = 0); SendStatus send(const String &message, const String &event, uint32_t id = 0, uint32_t reconnect = 0) { return send(message.c_str(), event.c_str(), id, reconnect); } diff --git a/src/AsyncWebSocket.h b/src/AsyncWebSocket.h index 6224b8779..d8b6b098b 100644 --- a/src/AsyncWebSocket.h +++ b/src/AsyncWebSocket.h @@ -200,7 +200,7 @@ class AsyncWebSocketClient { AwsFrameInfo _pinfo; std::unique_ptr _pbuffer; // Payload buffer for torn control frames - bool _queueControl(uint8_t opcode, const uint8_t *data = NULL, size_t len = 0, bool mask = false); + bool _queueControl(uint8_t opcode, const uint8_t *data = nullptr, size_t len = 0, bool mask = false); bool _queueMessage(AsyncWebSocketSharedBuffer buffer, uint8_t opcode = WS_TEXT, bool mask = false); // caller must hold _queue_lock via `lock`; _runQueue may unlock() it (e.g. // before a close() that can synchronously destroy *this) and never touch @@ -286,8 +286,8 @@ class AsyncWebSocketClient { } // control frames - void close(uint16_t code = 0, const char *message = NULL); - bool ping(const uint8_t *data = NULL, size_t len = 0); + void close(uint16_t code = 0, const char *message = nullptr); + bool ping(const uint8_t *data = nullptr, size_t len = 0); // set auto-ping period in seconds. disabled if zero (default) void keepAlivePeriod(uint16_t seconds) { @@ -379,12 +379,12 @@ class AsyncWebSocket : public AsyncWebHandler { return client(id) != nullptr; } - void close(uint32_t id, uint16_t code = 0, const char *message = NULL); - void closeAll(uint16_t code = 0, const char *message = NULL); + void close(uint32_t id, uint16_t code = 0, const char *message = nullptr); + void closeAll(uint16_t code = 0, const char *message = nullptr); void cleanupClients(uint16_t maxClients = DEFAULT_MAX_WS_CLIENTS); - bool ping(uint32_t id, const uint8_t *data = NULL, size_t len = 0); - SendStatus pingAll(const uint8_t *data = NULL, size_t len = 0); // done + bool ping(uint32_t id, const uint8_t *data = nullptr, size_t len = 0); + SendStatus pingAll(const uint8_t *data = nullptr, size_t len = 0); // done bool text(uint32_t id, const uint8_t *message, size_t len); bool text(uint32_t id, const char *message, size_t len); diff --git a/src/ESPAsyncWebServer.h b/src/ESPAsyncWebServer.h index abbae70d6..fe3328c44 100644 --- a/src/ESPAsyncWebServer.h +++ b/src/ESPAsyncWebServer.h @@ -58,8 +58,17 @@ #endif #endif +// Validity window, in seconds, of a Digest authentication nonce. +// Statelessness relies on the nonce embedding its issuing time: beyond this window +// the nonce is stale and clients must re-authenticate (silently, via stale=true). +// Set to 0 to disable nonce expiry (not recommended: widens the replay window). +#if !defined(ASYNCWEBSERVER_DIGEST_NONCE_TTL) +#define ASYNCWEBSERVER_DIGEST_NONCE_TTL 60 +#endif + #include "AsyncWebServerVersion.h" #define ASYNCWEBSERVER_FORK_ESP32Async +#include "WebAuthentication.h" #ifdef ASYNCWEBSERVER_REGEX #include @@ -570,6 +579,11 @@ class AsyncWebServerRequest { String _authorization; RequestedConnectionType _reqconntype; AsyncAuthType _authMethod = AsyncAuthType::AUTH_NONE; + // set by an authentication method when verification failed for a reason that a re-challenge can + // resolve. For Digest, this is the case when the client nonce was valid but expired (RFC 7616 §3.3): + // the next challenge must carry stale=true so compliant clients silently re-authenticate. + // A future authentication mechanism may reuse it for its own equivalent condition. + mutable bool _reAuthRequired = false; bool _isMultipart; bool _isPlainPost; bool _expectingContinue; @@ -624,6 +638,18 @@ class AsyncWebServerRequest { bool _handleUploadByte(uint8_t data, bool last); void _handleUploadEnd(); + /** + * @brief Digest authentication implementation (internal): verifies the "Authorization: Digest" + * header against the credentials and — using the stateless scheme — the server-issued signed + * nonce and the instance opaque. Sets _reAuthRequired when the nonce was valid but expired. + * + * A NULL nonce/opaque/uri argument means "let the library validate it automatically"; + * passing a value means the caller takes over that specific check. + */ + bool _authenticateDigest( + const char *username, const char *password, const char *realm, bool passwordIsHash, const char *nonce, const char *opaque, const char *uri + ) const; + void _send(); void _runMiddlewareChain(); @@ -727,7 +753,56 @@ class AsyncWebServerRequest { // base64(user:pass) for basic or // user:realm:md5(user:realm:pass) for digest bool authenticate(const char *hash) const; - bool authenticate(const char *username, const char *credentials, const char *realm = NULL, bool isHash = false) const; + + /** + * @brief authenticate the request with the given credentials, for the auth + * scheme detected in the "Authorization" header (see authType()). + * + * When the client used Basic or Bearer auth, only username and credentials are checked; + * the realm/hash args follow the Basic semantics (see the other authenticate() signature). + * + * When the client used Digest auth, this call delegates to the Digest verification which, + * beyond the standard digest verification, validates that: + * - the nonce was issued by this server and is not expired (stateless signed nonce); + * - the opaque is the one bound to this server instance. + * + * A NULL nonce/opaque/uri argument means "let the library validate it automatically"; + * passing a value means the caller takes over that specific check. These arguments have + * no effect for non-Digest auth schemes. + * + * Note: the library does not keep the raw request-target in memory, so a NULL uri + * leaves the header "uri" field unchecked (RFC 7616 §3.4.6 recommends binding it to + * the request target). Pass the expected request-target explicitly to enforce that check. + * + * @param username user name + * @param credentials password, or pre-computed hash (see isHash) + * @param realm authentication realm (NULL: not checked) - Digest only + * @param isHash true if credentials is a pre-computed hash (md5(user:realm:password) for Digest) + * @param nonce NULL to validate the server-issued signed nonce, or the exact value to compare against - Digest only + * @param opaque NULL to validate the instance opaque, or the exact value to compare against - Digest only + * @param uri NULL to leave the header "uri" unchecked, or the exact request-target to compare against - Digest only + * @return true if the request is authenticated + */ + bool authenticate( + const char *username, const char *credentials, const char *realm = nullptr, bool isHash = false, const char *nonce = nullptr, const char *opaque = nullptr, + const char *uri = nullptr + ) const; + + /** + * @brief true when the last authenticate() call failed for a reason that a re-challenge can + * resolve, i.e. re-sending the same credentials against a fresh challenge will eventually + * succeed. + * + * With Digest auth: the request credentials were correct and the nonce was issued by this + * server, but it expired. requestAuthentication() then issues the next challenge with + * stale=true (RFC 7616 §3.3), so compliant clients re-authenticate silently without + * prompting the user again. + * + * Future authentication mechanisms may set it for their own equivalent conditions. + */ + bool reAuthRequired() const { + return _reAuthRequired; + } void requestAuthentication(const char *realm = nullptr, bool isDigest = true) { requestAuthentication(isDigest ? AsyncAuthType::AUTH_DIGEST : AsyncAuthType::AUTH_BASIC, realm); } @@ -1815,6 +1890,8 @@ typedef AsyncCallbackJsonWebHandler AsyncCallbackMessagePackWebHandler; class AsyncWebServer : public AsyncMiddlewareChain { protected: + friend class AsyncWebServerRequest; + AsyncServer _server; std::list> _rewrites; // Unified handler list: each entry is an asyncsrv::MaybeOwned. @@ -1825,6 +1902,27 @@ class AsyncWebServer : public AsyncMiddlewareChain { // A catch-all handler is always installed (never nullptr). asyncsrv::MaybeOwned _catchAllHandler; + // Stateless Digest auth anti-replay: the nonce embeds + // its issuing time and is HMAC-signed with this per-instance secret, so no + // per-nonce storage is required. The opaque is derived from the same secret. + // Lazily allocated on first use so that instances not using Digest auth + // only pay for one pointer. First use happens either from setup() (setters) + // or from the async_tcp task processing a Digest request, so no locking is + // required. + struct DigestAuthContext { + uint8_t secret[16]; + uint32_t nonceTtl = ASYNCWEBSERVER_DIGEST_NONCE_TTL * 1000; // milliseconds + String opaque; + }; + DigestAuthContext *_digest = nullptr; + + /** + * @brief Digest auth context, initializing it on first use. + * + * @return the state, or nullptr if the allocation failed. + */ + DigestAuthContext *_digestAuthContext(); + public: /** * @brief Construct a server with a default internal catch-all handler. @@ -2063,11 +2161,35 @@ class AsyncWebServer : public AsyncMiddlewareChain { ArBodyHandlerFunction onBody = nullptr ); + /** + * @brief Set the validity window of Digest nonces issued by this server (stateless scheme). + * + * Nonces embed their issuing time and are signed with a per-instance secret; no + * server-side storage is kept. 0 disables expiry (not recommended). + * @param seconds nonce lifetime in seconds (default: ASYNCWEBSERVER_DIGEST_NONCE_TTL, 60s) + */ + void setDigestNonceTimeout(uint32_t seconds); + + /** + * @brief Set a custom secret used to sign Digest nonces and derive the opaque value. + * + * Changing the secret instantly revokes every nonce in circulation (clients will + * be re-challenged and silently re-authenticate). If never called, a random + * per-instance secret is generated when Digest auth is first used. + * @param secret pointer to 16 raw bytes + */ + void setDigestSecret(const uint8_t secret[16]); + + /** @brief Digest opaque value derived from the instance secret (empty if Digest auth was never used). */ + const String &digestOpaque() const { + return _digest ? _digest->opaque : asyncsrv::emptyString; + } + #if ASYNC_JSON_SUPPORT == 1 AsyncCallbackJsonWebHandler &on(AsyncURIMatcher uri, WebRequestMethodComposite method, ArJsonRequestHandlerFunction onBody); #endif - AsyncStaticWebHandler &serveStatic(const char *uri, fs::FS &fs, const char *path, const char *cache_control = NULL); + AsyncStaticWebHandler &serveStatic(const char *uri, fs::FS &fs, const char *path, const char *cache_control = nullptr); void onNotFound(ArRequestHandlerFunction fn); // called when handler is not assigned void onFileUpload(ArUploadHandlerFunction fn); // handle file uploads diff --git a/src/WebAuthentication.cpp b/src/WebAuthentication.cpp index 58096e037..72d8d1fd8 100644 --- a/src/WebAuthentication.cpp +++ b/src/WebAuthentication.cpp @@ -6,8 +6,16 @@ #include +#include +#include #include +#if defined(ESP32) +#include +#elif defined(ESP8266) +#include +#endif + // platform classification for the MD5 backend used in this file: // - HAS_MD5_BUILDER: the MD5Builder wrapper class exists (ESP32 arduino, RP2040, HOST) // - everything else (ESP8266, LibreTiny): low-level md5_context_t C API from @@ -265,3 +273,291 @@ bool checkDigestAuthentication( // os_printf("AUTH FAIL: password\n"); return false; } + +// Full digest verification with stateless server-side checks. +// The nonce embeds its issuing time and is signed with the per-server secret, so no +// per-nonce storage is required: any nonce not signed by this server, or expired, is rejected. +// nonce, opaque and uri are caller overrides: a non-NULL value means the caller takes that +// specific check over. With a NULL uri (default) the header uri is left unchecked: the library +// does not keep the raw request-target; callers willing to bind the header to a resource +// (RFC 7616 section 3.4.6) must pass the expected effective request URI. +// Fails closed: on allocation failure the request is rejected. +DigestAuthStatus checkDigestAuthentication( + const char *header, const char *method, const char *username, const char *password, const char *realm, bool passwordIsHash, const uint8_t digestSecret[16], + uint32_t digestNonceTtl, const char *nonce, const char *opaque, const char *uri +) { + if (header == NULL || method == NULL || username == NULL || password == NULL || digestSecret == NULL) { + return DigestAuthStatus::DIGEST_AUTH_FAILED; + } + + String myNonce; + String myOpaque; + if (!getDigestParam(header, T_nonce, myNonce) || !getDigestParam(header, T_opaque, myOpaque)) { + return DigestAuthStatus::DIGEST_AUTH_FAILED; // malformed header: missing mandatory fields + } + + // opaque must be the one bound to this server instance (compare on the stack, no allocation) + if (myOpaque.length() != 32) { + return DigestAuthStatus::DIGEST_AUTH_FAILED; + } + if (opaque != NULL) { // caller override: compare verbatim against the expected value + if (myOpaque != opaque) { + async_ws_log_e("Digest auth: invalid opaque"); + return DigestAuthStatus::DIGEST_AUTH_FAILED; + } + } else { + uint8_t opaqueMac[16]; + hmacMD5(digestSecret, 16, (const uint8_t *)T_opaque, strlen(T_opaque), opaqueMac); + uint8_t diff = 0; + const char *got = myOpaque.c_str(); + for (uint8_t i = 0; i < 16; i++) { + diff |= (uint8_t)T_hex_chars[opaqueMac[i] >> 4] ^ (uint8_t)got[i * 2]; + diff |= (uint8_t)T_hex_chars[opaqueMac[i] & 0x0f] ^ (uint8_t)got[i * 2 + 1]; + } + if (diff) { + async_ws_log_e("Digest auth: invalid opaque"); + return DigestAuthStatus::DIGEST_AUTH_FAILED; + } + } + + // nonce must have been issued by this server, and must not be expired + if (nonce != NULL) { // caller override: compare verbatim against the expected value + if (myNonce != nonce) { + async_ws_log_e("Digest auth: invalid nonce"); + return DigestAuthStatus::DIGEST_AUTH_FAILED; + } + } else { + bool stale = false; + if (!validateDigestNonce(myNonce.c_str(), digestSecret, millis(), digestNonceTtl, stale)) { + async_ws_log_e("Digest auth: invalid nonce"); + return DigestAuthStatus::DIGEST_AUTH_FAILED; + } + if (stale) { + return DigestAuthStatus::DIGEST_AUTH_STALE_NONCE; + } + } + + // credentials + response hash. The validated nonce/opaque are passed to the inner check: + // it compares every occurrence of these parameters against them, so a header carrying + // duplicated nonce/opaque fields (e.g. a fresh one first, an expired one last, used to + // bypass the validation with the last occurrence) is rejected. A caller-supplied uri is + // likewise compared against every occurrence. + if (!checkDigestAuthentication(header, method, username, password, realm, passwordIsHash, myNonce.c_str(), myOpaque.c_str(), uri)) { + return DigestAuthStatus::DIGEST_AUTH_FAILED; + } + + return DigestAuthStatus::DIGEST_AUTH_SUCCESS; +} + +void generateSecret(uint8_t secret[16]) { +#if defined(ESP32) + for (uint8_t i = 0; i < 16; i++) { + secret[i] = (uint8_t)esp_random(); + } +#elif defined(ESP8266) + uint32_t r[4] = {RANDOM_REG32, RANDOM_REG32, RANDOM_REG32, RANDOM_REG32}; + memcpy(secret, r, 16); +#elif defined(LIBRETINY) + ESP.random(secret, 16); +#else + // HOST is POSIX: read from /dev/urandom, falling back to rand() + FILE *f = fopen("/dev/urandom", "rb"); + if (f) { + size_t read = fread(secret, 1, 16, f); + fclose(f); + if (read == 16) { + return; + } + } + for (uint8_t i = 0; i < 16; i++) { + secret[i] = (uint8_t)rand(); + } +#endif +} + +#if HAS_MD5_BUILDER +// Adds a buffer to a MD5Builder, working around the pre-3.x core where add() +// takes a non-const uint8_t* although it never modifies the data. +static inline void md5Add(MD5Builder &md5, const uint8_t *data, size_t len) { +#if defined(ESP32) && ESP_ARDUINO_VERSION_MAJOR < 3 + md5.add(const_cast(data), len); +#else + md5.add(data, len); +#endif +} +#endif + +// HMAC-MD5, RFC 2104 +void hmacMD5(const uint8_t *key, size_t keyLen, const uint8_t *data, size_t dataLen, uint8_t out[16]) { + uint8_t k0[64]; + memset(k0, 0, sizeof(k0)); + if (keyLen > 64) { + // keys longer than block size are hashed first +#if HAS_MD5_BUILDER + MD5Builder md5; + md5.begin(); + md5Add(md5, key, keyLen); + md5.calculate(); + md5.getBytes(k0); // writes exactly 16 bytes +#else + md5_context_t ctx; + MD5Init(&ctx); + MD5Update(&ctx, key, keyLen); + MD5Final(k0, &ctx); +#endif + } else { + memcpy(k0, key, keyLen); + } + + uint8_t inner[64], outer[64]; + for (uint8_t i = 0; i < 64; i++) { + inner[i] = k0[i] ^ 0x36; + outer[i] = k0[i] ^ 0x5c; + } + + uint8_t digest[16]; +#if HAS_MD5_BUILDER + MD5Builder md5; + md5.begin(); + md5Add(md5, inner, 64); + md5Add(md5, data, dataLen); + md5.calculate(); + md5.getBytes(digest); // writes exactly 16 bytes + md5 = MD5Builder(); + md5.begin(); + md5Add(md5, outer, 64); + md5Add(md5, digest, 16); + md5.calculate(); + md5.getBytes(out); // writes exactly 16 bytes +#else + md5_context_t ctx; + MD5Init(&ctx); + MD5Update(&ctx, inner, 64); + MD5Update(&ctx, data, dataLen); + MD5Final(digest, &ctx); + MD5Init(&ctx); + MD5Update(&ctx, outer, 64); + MD5Update(&ctx, digest, 16); + MD5Final(out, &ctx); +#endif +} + +// Lowercase hex encoding of a buffer (output length is 2 * len + 1). +static String toHexDigest(const uint8_t *buf, size_t len) { + String out; + if (!out.reserve(len * 2)) { + return asyncsrv::emptyString; + } + for (size_t i = 0; i < len; i++) { + out += T_hex_chars[buf[i] >> 4]; + out += T_hex_chars[buf[i] & 0x0f]; + } + return out; +} + +// nonce = hex(millis) + hex(HMAC-MD5(secret, hex(millis))) : 8 + 32 = 40 chars +String generateDigestNonce(const uint8_t secret[16], uint32_t now) { + char ts[9]; + snprintf(ts, sizeof(ts), "%08" PRIx32, now); + uint8_t mac[16]; + hmacMD5(secret, 16, (const uint8_t *)ts, 8, mac); + String macHex = toHexDigest(mac, 16); + if (!macHex.length()) { + async_ws_log_e("Failed to allocate"); + return asyncsrv::emptyString; + } + String nonce; + if (!nonce.reserve(40)) { + async_ws_log_e("Failed to allocate"); + return asyncsrv::emptyString; + } + nonce.concat(ts, 8); + nonce.concat(macHex); + return nonce; +} + +String generateDigestOpaque(const uint8_t secret[16]) { + uint8_t opaqueMac[16]; + hmacMD5(secret, 16, (const uint8_t *)T_opaque, strlen(T_opaque), opaqueMac); + String opaque = toHexDigest(opaqueMac, 16); + if (!opaque.length()) { + async_ws_log_e("Failed to allocate"); + } + return opaque; +} + +bool validateDigestNonce(const char *nonce, const uint8_t secret[16], uint32_t now, uint32_t ttl, bool &stale) { + stale = false; + if (nonce == NULL || strlen(nonce) != 40) { + return false; + } + char ts[9]; + memcpy(ts, nonce, 8); + ts[8] = '\0'; + uint32_t issued; + if (sscanf(ts, "%08" SCNx32, &issued) != 1) { + return false; + } + // recompute the MAC part and compare in constant time, without any allocation + uint8_t mac[16]; + hmacMD5(secret, 16, (const uint8_t *)ts, 8, mac); + const char *got = nonce + 8; + uint8_t diff = 0; + for (uint8_t i = 0; i < 16; i++) { + diff |= (uint8_t)T_hex_chars[mac[i] >> 4] ^ (uint8_t)got[i * 2]; + diff |= (uint8_t)T_hex_chars[mac[i] & 0x0f] ^ (uint8_t)got[i * 2 + 1]; + } + if (diff) { + return false; + } + // modular arithmetic on the monotonic counters: safe across millis() wrap-around + if (ttl && (uint32_t)(now - issued) > ttl) { + stale = true; // issued by this server (MAC valid) but expired + } + return true; +} + +// Extracts one parameter value from a Digest header (e.g. getDigestParam(header, "nonce", out)). +// Quote-aware: values in quoted form are returned without their quotes. +// Returns false when the parameter is absent; an empty value yields an empty out string. +bool getDigestParam(const char *header, const char *name, String &out) { + out = asyncsrv::emptyString; + if (header == NULL || name == NULL) { + return false; + } + const size_t nameLen = strlen(name); + const char *p = header; + const char *end = header + strlen(header); + while (p < end) { + // skip leading separators + while (p < end && (*p == ',' || *p == ' ')) { + p++; + } + if (p >= end) { + return false; + } + // match the parameter name at this boundary, followed by '=' + if ((size_t)(end - p) > nameLen && memcmp(p, name, nameLen) == 0 && p[nameLen] == '=') { + const char *val = p + nameLen + 1; + if (*val == '"') { + val++; + const char *quote = strchr(val, '"'); + if (!quote) { + return false; // unterminated quoted value + } + return out.concat(val, quote - val); + } + const char *valEnd = val; + while (valEnd < end && *valEnd != ',') { + valEnd++; + } + return out.concat(val, valEnd - val); + } + // not this parameter: skip to the next separator + p = strchr(p, ','); + if (!p) { + return false; + } + } + return false; +} diff --git a/src/WebAuthentication.h b/src/WebAuthentication.h index a4819d60a..5322fb668 100644 --- a/src/WebAuthentication.h +++ b/src/WebAuthentication.h @@ -5,6 +5,48 @@ #include "Arduino.h" +// Outcome of digest verification when server-side checks (nonce/opaque/uri) are applied. +enum class DigestAuthStatus { + DIGEST_AUTH_FAILED, // credentials or header rejected + DIGEST_AUTH_SUCCESS, // fully verified + DIGEST_AUTH_STALE_NONCE, // nonce was issued by this server but has expired: re-challenge with stale=true +}; + +// Full verification: parses the Digest header, checks credentials, and — using the +// stateless scheme — validates the server-issued signed nonce and the instance opaque. +// No per-nonce storage is kept. now is expected to be a monotonic millisecond counter (e.g. millis()). +// nonce, opaque and uri are caller overrides: a non-NULL value means the caller takes that +// specific check over. With a NULL uri (default) the header uri is left unchecked: the library +// does not keep the raw request-target; callers willing to bind the header to a resource +// (RFC 7616 section 3.4.6) must pass the expected effective request URI. +DigestAuthStatus checkDigestAuthentication( + const char *header, const char *method, const char *username, const char *password, const char *realm, bool passwordIsHash, const uint8_t digestSecret[16], + uint32_t digestNonceTtl, const char *nonce = nullptr, const char *opaque = nullptr, const char *uri = nullptr +); + +// Extracts one parameter value from a Digest header (e.g. getDigestParam(header, "nonce", out)). +// Quote-aware: values in quoted form are returned without their quotes. +// Returns false when the parameter is absent; an empty value yields an empty out string. +bool getDigestParam(const char *header, const char *name, String &out); + +// Stateless signed nonce: hex(millis) + hex(HMAC-MD5(secret, hex(millis))) : 40 chars. +// Validity is re-computed from the embedded timestamp, without any server-side storage. +String generateDigestNonce(const uint8_t secret[16], uint32_t now); + +// Opaque value derived from the instance secret, identifying this server instance. +String generateDigestOpaque(const uint8_t secret[16]); + +// Validates a signed nonce. Returns false when the nonce was not issued from this +// secret. When true and the nonce has expired, stale is set: the caller should +// re-challenge with stale=true (RFC 7616 section 3.3) so compliant clients silently re-authenticate. +bool validateDigestNonce(const char *nonce, const uint8_t secret[16], uint32_t now, uint32_t ttl, bool &stale); + +// HMAC-MD5 (RFC 2104) +void hmacMD5(const uint8_t *key, size_t keyLen, const uint8_t *data, size_t dataLen, uint8_t out[16]); + +// Fill a 16-byte secret with best available platform entropy. +void generateSecret(uint8_t secret[16]); + bool checkBasicAuthentication(const char *header, const char *username, const char *password); bool checkDigestAuthentication( diff --git a/src/WebRequest.cpp b/src/WebRequest.cpp index 67e3dbe7a..7d61e3b4d 100644 --- a/src/WebRequest.cpp +++ b/src/WebRequest.cpp @@ -1029,7 +1029,7 @@ const char *AsyncWebServerRequest::_parseMultipartPostByte(uint8_t data, bool la return p.isPost() && p.isFile() && (p.name() == T_name || p.name() == T_filename); }); free(_itemBuffer); - _itemBuffer = NULL; + _itemBuffer = nullptr; // CWE-476 fix: _itemIsFile must be cleared when the buffer is freed. // Otherwise, if the next byte does not confirm a clean boundary close // (e.g. '--' appeared in the file data by coincidence), the @@ -1413,10 +1413,12 @@ void AsyncWebServerRequest::redirect(const char *url, int code) { send(response); } -bool AsyncWebServerRequest::authenticate(const char *username, const char *password, const char *realm, bool passwordIsHash) const { +bool AsyncWebServerRequest::authenticate( + const char *username, const char *password, const char *realm, bool passwordIsHash, const char *nonce, const char *opaque, const char *uri +) const { if (_authorization.length()) { if (_authMethod == AsyncAuthType::AUTH_DIGEST) { - return checkDigestAuthentication(_authorization.c_str(), methodToString(), username, password, realm, passwordIsHash, NULL, NULL, NULL); + return _authenticateDigest(username, password, realm, passwordIsHash, nonce, opaque, uri); } else if (!passwordIsHash) { return checkBasicAuthentication(_authorization.c_str(), username, password); } else { @@ -1426,6 +1428,30 @@ bool AsyncWebServerRequest::authenticate(const char *username, const char *passw return false; } +bool AsyncWebServerRequest::_authenticateDigest( + const char *username, const char *password, const char *realm, bool passwordIsHash, const char *nonce, const char *opaque, const char *uri +) const { + _reAuthRequired = false; + if (!_authorization.length()) { + return false; + } + + // stateless anti-replay validation; nonce/opaque/uri are caller overrides, + // so when any of them is set the caller takes that specific check over + const AsyncWebServer::DigestAuthContext *digest = _server->_digestAuthContext(); + if (digest == nullptr) { + return false; + } + DigestAuthStatus status = checkDigestAuthentication( + _authorization.c_str(), methodToString(), username, password, realm, passwordIsHash, digest->secret, digest->nonceTtl, nonce, opaque, uri + ); + // stale ? + if (status == DigestAuthStatus::DIGEST_AUTH_STALE_NONCE) { + _reAuthRequired = true; + } + return status == DigestAuthStatus::DIGEST_AUTH_SUCCESS; +} + bool AsyncWebServerRequest::authenticate(const char *hash) const { if (!_authorization.length() || hash == NULL) { return false; @@ -1445,7 +1471,7 @@ bool AsyncWebServerRequest::authenticate(const char *hash) const { } String realm = hStr.substring(0, separator); hStr = hStr.substring(separator + 1); - return checkDigestAuthentication(_authorization.c_str(), methodToString(), username.c_str(), hStr.c_str(), realm.c_str(), true, NULL, NULL, NULL); + return _authenticateDigest(username.c_str(), hStr.c_str(), realm.c_str(), true, NULL, NULL, NULL); } // Basic Auth, Bearer Auth, or other @@ -1486,27 +1512,49 @@ void AsyncWebServerRequest::requestAuthentication(AsyncAuthType method, const ch } case AsyncAuthType::AUTH_DIGEST: { - size_t len = sizeof(T_DIGEST_) - 1 + sizeof(T_realm__) - 1 + sizeof(T_auth_nonce) - 1 + 32 + sizeof(T__opaque) - 1 + 32 + 1; - String header; - if (header.reserve(len + strlen(realm))) { - const String nonce = genRandomMD5(); - const String opaque = genRandomMD5(); - if (nonce.length() && opaque.length()) { - header.concat(T_DIGEST_); - header.concat(T_realm__); - header.concat(realm); - header.concat(T_auth_nonce); - header.concat(nonce); - header.concat(T__opaque); - header.concat(opaque); - header.concat((char)0x22); // '"' - r->addHeader(T_WWW_AUTH, header.c_str()); - } else { + // stateless anti-replay scheme: the nonce embeds its + // issuing time, signed with the per-instance secret, so it can be validated + // later without any server-side storage. The opaque is derived from the + // same secret and identifies this server instance. + const AsyncWebServer::DigestAuthContext *digest = _server->_digestAuthContext(); + if (digest == nullptr) { + delete r; + sendRawResponse(asyncsrv::T_HTTP_503_HEAD); + return; + } + const String nonce = generateDigestNonce(digest->secret, millis()); + const String &opaque = digest->opaque; + if (nonce.length() && opaque.length()) { + // Digest realm="", qop="auth", nonce="", opaque=""[, stale=true] + size_t len = sizeof(T_DIGEST_) - 1 + sizeof(T_realm__) - 1 + strlen(realm) + sizeof(T_auth_nonce) - 1 + nonce.length() + sizeof(T__opaque) - 1 + + opaque.length() + 1; + if (_reAuthRequired) { + len += sizeof(T_stale_true) - 1; + } + String header; + if (!header.reserve(len)) { async_ws_log_e("Failed to allocate"); delete r; sendRawResponse(asyncsrv::T_HTTP_503_HEAD); return; } + header.concat(T_DIGEST_); + header.concat(T_realm__); + header.concat(realm); + header.concat(T_auth_nonce); // ends with an opening quote for the nonce + header.concat(nonce); + header.concat(T__opaque); // starts with a closing quote for the nonce + header.concat(opaque); + header.concat((char)0x22); // '"' + if (_reAuthRequired) { + header.concat(T_stale_true); + } + r->addHeader(T_WWW_AUTH, header.c_str()); + } else { + async_ws_log_e("Failed to allocate"); + delete r; + sendRawResponse(asyncsrv::T_HTTP_503_HEAD); + return; } break; } diff --git a/src/WebServer.cpp b/src/WebServer.cpp index 1b97e939e..f755a0099 100644 --- a/src/WebServer.cpp +++ b/src/WebServer.cpp @@ -2,12 +2,16 @@ // Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles #include "ESPAsyncWebServer.h" +#include "AsyncWebServerLogging.h" +#include "WebAuthentication.h" #include "WebHandlerImpl.h" #include #include #include +#include "./literals.h" + #if defined(ESP32) || defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350) || defined(LIBRETINY) || defined(HOST) #include #elif defined(ESP8266) @@ -69,6 +73,59 @@ AsyncWebServer::~AsyncWebServer() { reset(); end(); _catchAllHandler = nullptr; + delete _digest; +} + +AsyncWebServer::DigestAuthContext *AsyncWebServer::_digestAuthContext() { + if (_digest == nullptr) { + DigestAuthContext *state = new (std::nothrow) DigestAuthContext(); + if (state) { + generateSecret(state->secret); + state->opaque = generateDigestOpaque(state->secret); + if (state->opaque.length()) { + _digest = state; // first use + } else { + // Opaque derivation failed (out of memory): retry on next use. + // The random secret is discarded safely — no challenge was ever + // issued with it (issuing requires the opaque). + delete state; + } + } else { + async_ws_log_e("Failed to allocate"); + } + } + return _digest; +} + +void AsyncWebServer::setDigestNonceTimeout(uint32_t seconds) { + if (DigestAuthContext *digest = _digestAuthContext()) { + digest->nonceTtl = seconds * 1000; + } +} + +void AsyncWebServer::setDigestSecret(const uint8_t secret[16]) { + if (!secret) { + return; + } + // Derive before applying: on allocation failure the previous + // secret/opaque pair stays intact instead of being half-updated. + String opaque = generateDigestOpaque(secret); + if (!opaque.length()) { + return; + } + DigestAuthContext *digest = _digest; + if (!digest) { + // First use: allocate directly. _digestAuthContext() would initialize a + // random secret and derive its opaque, both overwritten right below. + digest = new (std::nothrow) DigestAuthContext(); + if (!digest) { + async_ws_log_e("Failed to allocate"); + return; + } + _digest = digest; + } + memcpy(digest->secret, secret, sizeof(digest->secret)); + digest->opaque = std::move(opaque); } AsyncWebRewrite &AsyncWebServer::addRewrite(std::shared_ptr rewrite) { diff --git a/src/literals.h b/src/literals.h index 4be495ea7..d838b67c1 100644 --- a/src/literals.h +++ b/src/literals.h @@ -107,6 +107,7 @@ static constexpr const char T_nn[] = "\n\n"; static constexpr const char T_rn[] = "\r\n"; static constexpr const char T_rnrn[] = "\r\n\r\n"; static constexpr const char T_Server[] = "Server"; +static constexpr const char T_stale_true[] = ", stale=true"; static constexpr const char T_Transfer_Encoding[] = "Transfer-Encoding"; static constexpr const char T_TRUE[] = "true"; static constexpr const char T_UPGRADE[] = "Upgrade";