Skip to content

Modified ESP8266WebServer to allow for authentication with H(A1). #6020

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
May 23, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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();
}
}
34 changes: 26 additions & 8 deletions libraries/ESP8266WebServer/src/ESP8266WebServer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand All @@ -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();
Expand Down Expand Up @@ -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)
{
Expand Down
3 changes: 3 additions & 0 deletions libraries/ESP8266WebServer/src/ESP8266WebServer.h
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ class ESP8266WebServer
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;
Expand Down Expand Up @@ -127,6 +128,8 @@ class ESP8266WebServer
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>
Expand Down