From 6191fbbd920f472ebdcd24c2a205a2bee9c8d098 Mon Sep 17 00:00:00 2001
From: Tyler Moore <tylerrmoore@gmail.com>
Date: Thu, 23 May 2019 09:57:51 -0700
Subject: [PATCH] Modified ESP8266WebServer (#6020)

-Expose HTTP Digest authentication with H1 hash as the argument
-Preserved HTTP authentication with username/password arguments
-Added a public  static function for generating the H1 hash
-Created an example of how to use this called HttpHashCredAuth.ino
---
 .../HttpHashCredAuth/HttpHashCredAuth.ino     | 258 ++++++++++++++++++
 .../ESP8266WebServer/src/ESP8266WebServer.cpp |  34 ++-
 .../ESP8266WebServer/src/ESP8266WebServer.h   |   3 +
 3 files changed, 287 insertions(+), 8 deletions(-)
 create mode 100644 libraries/ESP8266WebServer/examples/HttpHashCredAuth/HttpHashCredAuth.ino

diff --git a/libraries/ESP8266WebServer/examples/HttpHashCredAuth/HttpHashCredAuth.ino b/libraries/ESP8266WebServer/examples/HttpHashCredAuth/HttpHashCredAuth.ino
new file mode 100644
index 000000000..f77484d7e
--- /dev/null
+++ b/libraries/ESP8266WebServer/examples/HttpHashCredAuth/HttpHashCredAuth.ino
@@ -0,0 +1,258 @@
+/*
+  HTTP Hashed Credential example
+  Created April 27, 2019 by Tyler Moore.
+  This example code is in the public domain.
+
+  This is a simple Arduino example to demonstrate a few simple techniques:
+  1. Creating a secure web server using ESP8266ESP8266WebServerSecure
+  2. Use of HTTP authentication on this secure server
+  3. A simple web interface to allow an authenticated user to change Credentials
+  4. Persisting those credentials through a reboot of the ESP by saving them to SPIFFS without storing them as plain text
+*/
+
+#include <FS.h>
+#include <ESP8266WiFi.h>
+#include <ESP8266WebServerSecure.h>
+
+//Unfortunately it is not possible to have persistent WiFi credentials stored as anything but plain text. Obfuscation would be the only feasible barrier.
+#ifndef STASSID
+#define STASSID "your-ssid"
+#define STAPSK  "your-password"
+#endif
+
+const char* ssid = STASSID;
+const char* wifi_pw = STAPSK;
+
+const String file_credentials = R"(/credentials.txt)"; //SPIFFS file name for the saved credentials
+const String change_creds =  "changecreds"; //address for a credential change
+
+//The ESP8266WebServerSecure requires an encryption certificate and matching key.
+//These can generated with the bash script available in the ESP8266 Arduino repository.
+//These values can be used for testing but are available publicly so should not be used in production.
+static const char serverCert[] PROGMEM = R"EOF(
+-----BEGIN CERTIFICATE-----
+MIIDSzCCAjMCCQD2ahcfZAwXxDANBgkqhkiG9w0BAQsFADCBiTELMAkGA1UEBhMC
+VVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDU9yYW5nZSBDb3VudHkx
+EDAOBgNVBAoMB1ByaXZhZG8xGjAYBgNVBAMMEXNlcnZlci56bGFiZWwuY29tMR8w
+HQYJKoZIhvcNAQkBFhBlYXJsZUB6bGFiZWwuY29tMB4XDTE4MDMwNjA1NDg0NFoX
+DTE5MDMwNjA1NDg0NFowRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3Rh
+dGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDCCASIwDQYJKoZI
+hvcNAQEBBQADggEPADCCAQoCggEBAPVKBwbZ+KDSl40YCDkP6y8Sv4iNGvEOZg8Y
+X7sGvf/xZH7UiCBWPFIRpNmDSaZ3yjsmFqm6sLiYSGSdrBCFqdt9NTp2r7hga6Sj
+oASSZY4B9pf+GblDy5m10KDx90BFKXdPMCLT+o76Nx9PpCvw13A848wHNG3bpBgI
+t+w/vJCX3bkRn8yEYAU6GdMbYe7v446hX3kY5UmgeJFr9xz1kq6AzYrMt/UHhNzO
+S+QckJaY0OGWvmTNspY3xCbbFtIDkCdBS8CZAw+itnofvnWWKQEXlt6otPh5njwy
++O1t/Q+Z7OMDYQaH02IQx3188/kW3FzOY32knER1uzjmRO+jhA8CAwEAATANBgkq
+hkiG9w0BAQsFAAOCAQEAnDrROGRETB0woIcI1+acY1yRq4yAcH2/hdq2MoM+DCyM
+E8CJaOznGR9ND0ImWpTZqomHOUkOBpvu7u315blQZcLbL1LfHJGRTCHVhvVrcyEb
+fWTnRtAQdlirUm/obwXIitoz64VSbIVzcqqfg9C6ZREB9JbEX98/9Wp2gVY+31oC
+JfUvYadSYxh3nblvA4OL+iEZiW8NE3hbW6WPXxvS7Euge0uWMPc4uEcnsE0ZVG3m
++TGimzSdeWDvGBRWZHXczC2zD4aoE5vrl+GD2i++c6yjL/otHfYyUpzUfbI2hMAA
+5tAF1D5vAAwA8nfPysumlLsIjohJZo4lgnhB++AlOg==
+-----END CERTIFICATE-----
+)EOF";
+static const char serverKey[] PROGMEM =  R"EOF(
+-----BEGIN RSA PRIVATE KEY-----
+MIIEpQIBAAKCAQEA9UoHBtn4oNKXjRgIOQ/rLxK/iI0a8Q5mDxhfuwa9//FkftSI
+IFY8UhGk2YNJpnfKOyYWqbqwuJhIZJ2sEIWp2301OnavuGBrpKOgBJJljgH2l/4Z
+uUPLmbXQoPH3QEUpd08wItP6jvo3H0+kK/DXcDzjzAc0bdukGAi37D+8kJfduRGf
+zIRgBToZ0xth7u/jjqFfeRjlSaB4kWv3HPWSroDNisy39QeE3M5L5ByQlpjQ4Za+
+ZM2yljfEJtsW0gOQJ0FLwJkDD6K2eh++dZYpAReW3qi0+HmePDL47W39D5ns4wNh
+BofTYhDHfXzz+RbcXM5jfaScRHW7OOZE76OEDwIDAQABAoIBAQDKov5NFbNFQNR8
+djcM1O7Is6dRaqiwLeH4ZH1pZ3d9QnFwKanPdQ5eCj9yhfhJMrr5xEyCqT0nMn7T
+yEIGYDXjontfsf8WxWkH2TjvrfWBrHOIOx4LJEvFzyLsYxiMmtZXvy6YByD+Dw2M
+q2GH/24rRdI2klkozIOyazluTXU8yOsSGxHr/aOa9/sZISgLmaGOOuKI/3Zqjdhr
+eHeSqoQFt3xXa8jw01YubQUDw/4cv9rk2ytTdAoQUimiKtgtjsggpP1LTq4xcuqN
+d4jWhTcnorWpbD2cVLxrEbnSR3VuBCJEZv5axg5ZPxLEnlcId8vMtvTRb5nzzszn
+geYUWDPhAoGBAPyKVNqqwQl44oIeiuRM2FYenMt4voVaz3ExJX2JysrG0jtCPv+Y
+84R6Cv3nfITz3EZDWp5sW3OwoGr77lF7Tv9tD6BptEmgBeuca3SHIdhG2MR+tLyx
+/tkIAarxQcTGsZaSqra3gXOJCMz9h2P5dxpdU+0yeMmOEnAqgQ8qtNBfAoGBAPim
+RAtnrd0WSlCgqVGYFCvDh1kD5QTNbZc+1PcBHbVV45EmJ2fLXnlDeplIZJdYxmzu
+DMOxZBYgfeLY9exje00eZJNSj/csjJQqiRftrbvYY7m5njX1kM5K8x4HlynQTDkg
+rtKO0YZJxxmjRTbFGMegh1SLlFLRIMtehNhOgipRAoGBAPnEEpJGCS9GGLfaX0HW
+YqwiEK8Il12q57mqgsq7ag7NPwWOymHesxHV5mMh/Dw+NyBi4xAGWRh9mtrUmeqK
+iyICik773Gxo0RIqnPgd4jJWN3N3YWeynzulOIkJnSNx5BforOCTc3uCD2s2YB5X
+jx1LKoNQxLeLRN8cmpIWicf/AoGBANjRSsZTKwV9WWIDJoHyxav/vPb+8WYFp8lZ
+zaRxQbGM6nn4NiZI7OF62N3uhWB/1c7IqTK/bVHqFTuJCrCNcsgld3gLZ2QWYaMV
+kCPgaj1BjHw4AmB0+EcajfKilcqtSroJ6MfMJ6IclVOizkjbByeTsE4lxDmPCDSt
+/9MKanBxAoGAY9xo741Pn9WUxDyRplww606ccdNf/ksHWNc/Y2B5SPwxxSnIq8nO
+j01SmsCUYVFAgZVOTiiycakjYLzxlc6p8BxSVqy6LlJqn95N8OXoQ+bkwUux/ekg
+gz5JWYhbD6c38khSzJb0pNXCo3EuYAVa36kDM96k1BtWuhRS10Q1VXk=
+-----END RSA PRIVATE KEY-----
+)EOF";
+
+ESP8266WebServerSecure server(443);
+
+//These are temporary credentials that will only be used if none are found saved in SPIFFS.
+String login = "admin";
+const String realm = "global";
+String H1 = "";
+String authentication_failed = "User authentication has failed.";
+
+void setup() {
+  Serial.begin(115200);
+
+  //Initialize SPIFFS to save credentials
+  if(!SPIFFS.begin()){
+		Serial.println("SPIFFS initialization error, programmer flash configured?");
+    ESP.restart();
+	}
+
+  //Attempt to load credentials. If the file does not yet exist, they will be set to the default values above
+  loadcredentials();
+
+  //Initialize wifi
+  WiFi.mode(WIFI_STA);
+  WiFi.begin(ssid, wifi_pw);
+  if (WiFi.waitForConnectResult() != WL_CONNECTED) {
+    Serial.println("WiFi Connect Failed! Rebooting...");
+    delay(1000);
+    ESP.restart();
+  }
+
+  server.setRSACert(new BearSSL::X509List(serverCert), new BearSSL::PrivateKey(serverKey));
+  server.on("/",showcredentialpage); //for this simple example, just show a simple page for changing credentials at the root
+  server.on("/" + change_creds,handlecredentialchange); //handles submission of credentials from the client
+  server.onNotFound(redirect);
+  server.begin();
+
+  Serial.print("Open https://");
+  Serial.print(WiFi.localIP());
+  Serial.println("/ in your browser to see it working");
+}
+
+void loop() {
+  yield();
+  server.handleClient();
+}
+
+//This function redirects home
+void redirect(){
+  String url = "https://" + WiFi.localIP().toString();
+  Serial.println("Redirect called. Redirecting to " + url);
+  server.sendHeader("Location", url, true);
+  Serial.println("Header sent.");
+  server.send( 302, "text/plain", ""); // Empty content inhibits Content-length header so we have to close the socket ourselves.
+  Serial.println("Empty page sent.");
+  server.client().stop(); // Stop is needed because we sent no content length
+  Serial.println("Client stopped.");
+}
+
+//This function checks whether the current session has been authenticated. If not, a request for credentials is sent.
+bool session_authenticated() {
+  Serial.println("Checking authentication.");
+  if (server.authenticateDigest(login,H1)) {
+    Serial.println("Authentication confirmed.");
+    return true;
+  } else  {
+    Serial.println("Not authenticated. Requesting credentials.");
+    server.requestAuthentication(DIGEST_AUTH,realm.c_str(),authentication_failed);
+    redirect();
+    return false;
+  }
+}
+
+//This function sends a simple webpage for changing login credentials to the client
+void showcredentialpage(){
+  Serial.println("Show credential page called.");
+  if(!session_authenticated()){
+    return;
+  }
+
+  Serial.println("Forming credential modification page.");
+
+  String page;
+  page = R"(<html>)";
+
+  page+=
+  R"(
+  <h2>Login Credentials</h2><br>
+
+  <form action=")" + change_creds + R"(" method="post">
+  Login:<br>
+  <input type="text" name="login"><br>
+  Password:<br>
+  <input type="password" name="password"><br>
+  Confirm Password:<br>
+  <input type="password" name="password_duplicate"><br>
+  <p><button type="submit" name="newcredentials">Change Credentials</button></p>
+  </form><br>
+  )"
+  ;
+
+  page += R"(</html>)";
+
+  Serial.println("Sending credential modification page.");
+
+  server.send(200, "text/html", page);
+}
+
+//Saves credentials to SPIFFS
+void savecredentials(String new_login, String new_password)
+{
+  //Set global variables to new values
+  login=new_login;
+  H1=ESP8266WebServer::credentialHash(new_login,realm,new_password);
+
+  //Save new values to SPIFFS for loading on next reboot
+  Serial.println("Saving credentials.");
+  File f=SPIFFS.open(file_credentials,"w"); //open as a brand new file, discard old contents
+  if(f){
+    Serial.println("Modifying credentials in file system.");
+    f.println(login);
+    f.println(H1);
+    Serial.println("Credentials written.");
+    f.close();
+    Serial.println("File closed.");
+  }
+  Serial.println("Credentials saved.");
+}
+
+//loads credentials from SPIFFS
+void loadcredentials()
+{
+  Serial.println("Searching for credentials.");
+  File f;
+  f=SPIFFS.open(file_credentials,"r");
+  if(f){
+    Serial.println("Loading credentials from file system.");
+    String mod=f.readString(); //read the file to a String
+    int index_1=mod.indexOf('\n',0); //locate the first line break
+    int index_2=mod.indexOf('\n',index_1+1); //locate the second line break
+    login=mod.substring(0,index_1-1); //get the first line (excluding the line break)
+    H1=mod.substring(index_1+1,index_2-1); //get the second line (excluding the line break)
+    f.close();
+  } else {
+    String default_login = "admin";
+    String default_password = "changeme";
+    Serial.println("None found. Setting to default credentials.");
+    Serial.println("user:" + default_login);
+    Serial.println("password:" + default_password);
+    login=default_login;
+    H1=ESP8266WebServer::credentialHash(default_login,realm,default_password);
+  }
+}
+
+//This function handles a credential change from a client.
+void handlecredentialchange() {
+  Serial.println("Handle credential change called.");
+  if(!session_authenticated()){
+    return;
+  }
+
+  Serial.println("Handling credential change request from client.");
+
+  String login = server.arg("login");
+  String pw1 = server.arg("password");
+  String pw2 = server.arg("password_duplicate");
+
+  if(login != "" && pw1 != "" && pw1 == pw2){
+
+    savecredentials(login,pw1);
+    server.send(200, "text/plain", "Credentials updated");
+    redirect();
+  } else {
+    server.send(200, "text/plain", "Malformed credentials");
+    redirect();
+  }
+}
diff --git a/libraries/ESP8266WebServer/src/ESP8266WebServer.cpp b/libraries/ESP8266WebServer/src/ESP8266WebServer.cpp
index 2aac60808..174e8c047 100644
--- a/libraries/ESP8266WebServer/src/ESP8266WebServer.cpp
+++ b/libraries/ESP8266WebServer/src/ESP8266WebServer.cpp
@@ -140,6 +140,20 @@ bool ESP8266WebServer::authenticate(const char * username, const char * password
       delete[] toencode;
       delete[] encoded;
     } else if(authReq.startsWith(F("Digest"))) {
+      String _realm    = _extractParam(authReq, F("realm=\""));
+      String _H1 = credentialHash((String)username,_realm,(String)password);
+      return authenticateDigest((String)username,_H1);
+    }
+    authReq = "";
+  }
+  return false;
+}
+
+bool ESP8266WebServer::authenticateDigest(const String& username, const String& H1)
+{
+  if(hasHeader(FPSTR(AUTHORIZATION_HEADER))) {
+    String authReq = header(FPSTR(AUTHORIZATION_HEADER));
+    if(authReq.startsWith(F("Digest"))) {
       authReq = authReq.substring(7);
       #ifdef DEBUG_ESP_HTTP_SERVER
       DEBUG_OUTPUT.println(authReq);
@@ -170,14 +184,10 @@ bool ESP8266WebServer::authenticate(const char * username, const char * password
         _nc = _extractParam(authReq, F("nc="), ',');
         _cnonce = _extractParam(authReq, F("cnonce=\""));
       }
-      MD5Builder md5;
-      md5.begin();
-      md5.add(String(username) + ':' + _realm + ':' + String(password));  // md5 of the user:realm:user
-      md5.calculate();
-      String _H1 = md5.toString();
       #ifdef DEBUG_ESP_HTTP_SERVER
-      DEBUG_OUTPUT.println("Hash of user:realm:pass=" + _H1);
+      DEBUG_OUTPUT.println("Hash of user:realm:pass=" + H1);
       #endif
+      MD5Builder md5;
       md5.begin();
       if(_currentMethod == HTTP_GET){
         md5.add(String(F("GET:")) + _uri);
@@ -197,9 +207,9 @@ bool ESP8266WebServer::authenticate(const char * username, const char * password
       #endif
       md5.begin();
       if(authReq.indexOf(FPSTR(qop_auth)) != -1 || authReq.indexOf(FPSTR(qop_auth_quoted)) != -1) {
-        md5.add(_H1 + ':' + _nonce + ':' + _nc + ':' + _cnonce + F(":auth:") + _H2);
+        md5.add(H1 + ':' + _nonce + ':' + _nc + ':' + _cnonce + F(":auth:") + _H2);
       } else {
-        md5.add(_H1 + ':' + _nonce + ':' + _H2);
+        md5.add(H1 + ':' + _nonce + ':' + _H2);
       }
       md5.calculate();
       String _responsecheck = md5.toString();
@@ -478,6 +488,14 @@ void ESP8266WebServer::sendContent_P(PGM_P content, size_t size) {
   }
 }
 
+String ESP8266WebServer::credentialHash(const String& username, const String& realm, const String& password)
+{
+  MD5Builder md5;
+  md5.begin();
+  md5.add(username + ":" + realm + ":" + password);  // md5 of the user:realm:password
+  md5.calculate();
+  return md5.toString();
+}
 
 void ESP8266WebServer::_streamFileCore(const size_t fileSize, const String & fileName, const String & contentType)
 {
diff --git a/libraries/ESP8266WebServer/src/ESP8266WebServer.h b/libraries/ESP8266WebServer/src/ESP8266WebServer.h
index 10e9a5666..835b9a442 100644
--- a/libraries/ESP8266WebServer/src/ESP8266WebServer.h
+++ b/libraries/ESP8266WebServer/src/ESP8266WebServer.h
@@ -82,6 +82,7 @@ public:
   void stop();
 
   bool authenticate(const char * username, const char * password);
+  bool authenticateDigest(const String& username, const String& H1);
   void requestAuthentication(HTTPAuthMethod mode = BASIC_AUTH, const char* realm = NULL, const String& authFailMsg = String("") );
 
   typedef std::function<void(void)> THandlerFunction;
@@ -127,6 +128,8 @@ public:
   void sendContent_P(PGM_P content);
   void sendContent_P(PGM_P content, size_t size);
 
+  static String credentialHash(const String& username, const String& realm, const String& password);
+
   static String urlDecode(const String& text);
 
   template<typename T>