Skip to content

Commit 6191fbb

Browse files
overtone1000earlephilhower
authored andcommitted
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
1 parent 147b5fb commit 6191fbb

File tree

3 files changed

+287
-8
lines changed

3 files changed

+287
-8
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
/*
2+
HTTP Hashed Credential example
3+
Created April 27, 2019 by Tyler Moore.
4+
This example code is in the public domain.
5+
6+
This is a simple Arduino example to demonstrate a few simple techniques:
7+
1. Creating a secure web server using ESP8266ESP8266WebServerSecure
8+
2. Use of HTTP authentication on this secure server
9+
3. A simple web interface to allow an authenticated user to change Credentials
10+
4. Persisting those credentials through a reboot of the ESP by saving them to SPIFFS without storing them as plain text
11+
*/
12+
13+
#include <FS.h>
14+
#include <ESP8266WiFi.h>
15+
#include <ESP8266WebServerSecure.h>
16+
17+
//Unfortunately it is not possible to have persistent WiFi credentials stored as anything but plain text. Obfuscation would be the only feasible barrier.
18+
#ifndef STASSID
19+
#define STASSID "your-ssid"
20+
#define STAPSK "your-password"
21+
#endif
22+
23+
const char* ssid = STASSID;
24+
const char* wifi_pw = STAPSK;
25+
26+
const String file_credentials = R"(/credentials.txt)"; //SPIFFS file name for the saved credentials
27+
const String change_creds = "changecreds"; //address for a credential change
28+
29+
//The ESP8266WebServerSecure requires an encryption certificate and matching key.
30+
//These can generated with the bash script available in the ESP8266 Arduino repository.
31+
//These values can be used for testing but are available publicly so should not be used in production.
32+
static const char serverCert[] PROGMEM = R"EOF(
33+
-----BEGIN CERTIFICATE-----
34+
MIIDSzCCAjMCCQD2ahcfZAwXxDANBgkqhkiG9w0BAQsFADCBiTELMAkGA1UEBhMC
35+
VVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDU9yYW5nZSBDb3VudHkx
36+
EDAOBgNVBAoMB1ByaXZhZG8xGjAYBgNVBAMMEXNlcnZlci56bGFiZWwuY29tMR8w
37+
HQYJKoZIhvcNAQkBFhBlYXJsZUB6bGFiZWwuY29tMB4XDTE4MDMwNjA1NDg0NFoX
38+
DTE5MDMwNjA1NDg0NFowRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3Rh
39+
dGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDCCASIwDQYJKoZI
40+
hvcNAQEBBQADggEPADCCAQoCggEBAPVKBwbZ+KDSl40YCDkP6y8Sv4iNGvEOZg8Y
41+
X7sGvf/xZH7UiCBWPFIRpNmDSaZ3yjsmFqm6sLiYSGSdrBCFqdt9NTp2r7hga6Sj
42+
oASSZY4B9pf+GblDy5m10KDx90BFKXdPMCLT+o76Nx9PpCvw13A848wHNG3bpBgI
43+
t+w/vJCX3bkRn8yEYAU6GdMbYe7v446hX3kY5UmgeJFr9xz1kq6AzYrMt/UHhNzO
44+
S+QckJaY0OGWvmTNspY3xCbbFtIDkCdBS8CZAw+itnofvnWWKQEXlt6otPh5njwy
45+
+O1t/Q+Z7OMDYQaH02IQx3188/kW3FzOY32knER1uzjmRO+jhA8CAwEAATANBgkq
46+
hkiG9w0BAQsFAAOCAQEAnDrROGRETB0woIcI1+acY1yRq4yAcH2/hdq2MoM+DCyM
47+
E8CJaOznGR9ND0ImWpTZqomHOUkOBpvu7u315blQZcLbL1LfHJGRTCHVhvVrcyEb
48+
fWTnRtAQdlirUm/obwXIitoz64VSbIVzcqqfg9C6ZREB9JbEX98/9Wp2gVY+31oC
49+
JfUvYadSYxh3nblvA4OL+iEZiW8NE3hbW6WPXxvS7Euge0uWMPc4uEcnsE0ZVG3m
50+
+TGimzSdeWDvGBRWZHXczC2zD4aoE5vrl+GD2i++c6yjL/otHfYyUpzUfbI2hMAA
51+
5tAF1D5vAAwA8nfPysumlLsIjohJZo4lgnhB++AlOg==
52+
-----END CERTIFICATE-----
53+
)EOF";
54+
static const char serverKey[] PROGMEM = R"EOF(
55+
-----BEGIN RSA PRIVATE KEY-----
56+
MIIEpQIBAAKCAQEA9UoHBtn4oNKXjRgIOQ/rLxK/iI0a8Q5mDxhfuwa9//FkftSI
57+
IFY8UhGk2YNJpnfKOyYWqbqwuJhIZJ2sEIWp2301OnavuGBrpKOgBJJljgH2l/4Z
58+
uUPLmbXQoPH3QEUpd08wItP6jvo3H0+kK/DXcDzjzAc0bdukGAi37D+8kJfduRGf
59+
zIRgBToZ0xth7u/jjqFfeRjlSaB4kWv3HPWSroDNisy39QeE3M5L5ByQlpjQ4Za+
60+
ZM2yljfEJtsW0gOQJ0FLwJkDD6K2eh++dZYpAReW3qi0+HmePDL47W39D5ns4wNh
61+
BofTYhDHfXzz+RbcXM5jfaScRHW7OOZE76OEDwIDAQABAoIBAQDKov5NFbNFQNR8
62+
djcM1O7Is6dRaqiwLeH4ZH1pZ3d9QnFwKanPdQ5eCj9yhfhJMrr5xEyCqT0nMn7T
63+
yEIGYDXjontfsf8WxWkH2TjvrfWBrHOIOx4LJEvFzyLsYxiMmtZXvy6YByD+Dw2M
64+
q2GH/24rRdI2klkozIOyazluTXU8yOsSGxHr/aOa9/sZISgLmaGOOuKI/3Zqjdhr
65+
eHeSqoQFt3xXa8jw01YubQUDw/4cv9rk2ytTdAoQUimiKtgtjsggpP1LTq4xcuqN
66+
d4jWhTcnorWpbD2cVLxrEbnSR3VuBCJEZv5axg5ZPxLEnlcId8vMtvTRb5nzzszn
67+
geYUWDPhAoGBAPyKVNqqwQl44oIeiuRM2FYenMt4voVaz3ExJX2JysrG0jtCPv+Y
68+
84R6Cv3nfITz3EZDWp5sW3OwoGr77lF7Tv9tD6BptEmgBeuca3SHIdhG2MR+tLyx
69+
/tkIAarxQcTGsZaSqra3gXOJCMz9h2P5dxpdU+0yeMmOEnAqgQ8qtNBfAoGBAPim
70+
RAtnrd0WSlCgqVGYFCvDh1kD5QTNbZc+1PcBHbVV45EmJ2fLXnlDeplIZJdYxmzu
71+
DMOxZBYgfeLY9exje00eZJNSj/csjJQqiRftrbvYY7m5njX1kM5K8x4HlynQTDkg
72+
rtKO0YZJxxmjRTbFGMegh1SLlFLRIMtehNhOgipRAoGBAPnEEpJGCS9GGLfaX0HW
73+
YqwiEK8Il12q57mqgsq7ag7NPwWOymHesxHV5mMh/Dw+NyBi4xAGWRh9mtrUmeqK
74+
iyICik773Gxo0RIqnPgd4jJWN3N3YWeynzulOIkJnSNx5BforOCTc3uCD2s2YB5X
75+
jx1LKoNQxLeLRN8cmpIWicf/AoGBANjRSsZTKwV9WWIDJoHyxav/vPb+8WYFp8lZ
76+
zaRxQbGM6nn4NiZI7OF62N3uhWB/1c7IqTK/bVHqFTuJCrCNcsgld3gLZ2QWYaMV
77+
kCPgaj1BjHw4AmB0+EcajfKilcqtSroJ6MfMJ6IclVOizkjbByeTsE4lxDmPCDSt
78+
/9MKanBxAoGAY9xo741Pn9WUxDyRplww606ccdNf/ksHWNc/Y2B5SPwxxSnIq8nO
79+
j01SmsCUYVFAgZVOTiiycakjYLzxlc6p8BxSVqy6LlJqn95N8OXoQ+bkwUux/ekg
80+
gz5JWYhbD6c38khSzJb0pNXCo3EuYAVa36kDM96k1BtWuhRS10Q1VXk=
81+
-----END RSA PRIVATE KEY-----
82+
)EOF";
83+
84+
ESP8266WebServerSecure server(443);
85+
86+
//These are temporary credentials that will only be used if none are found saved in SPIFFS.
87+
String login = "admin";
88+
const String realm = "global";
89+
String H1 = "";
90+
String authentication_failed = "User authentication has failed.";
91+
92+
void setup() {
93+
Serial.begin(115200);
94+
95+
//Initialize SPIFFS to save credentials
96+
if(!SPIFFS.begin()){
97+
Serial.println("SPIFFS initialization error, programmer flash configured?");
98+
ESP.restart();
99+
}
100+
101+
//Attempt to load credentials. If the file does not yet exist, they will be set to the default values above
102+
loadcredentials();
103+
104+
//Initialize wifi
105+
WiFi.mode(WIFI_STA);
106+
WiFi.begin(ssid, wifi_pw);
107+
if (WiFi.waitForConnectResult() != WL_CONNECTED) {
108+
Serial.println("WiFi Connect Failed! Rebooting...");
109+
delay(1000);
110+
ESP.restart();
111+
}
112+
113+
server.setRSACert(new BearSSL::X509List(serverCert), new BearSSL::PrivateKey(serverKey));
114+
server.on("/",showcredentialpage); //for this simple example, just show a simple page for changing credentials at the root
115+
server.on("/" + change_creds,handlecredentialchange); //handles submission of credentials from the client
116+
server.onNotFound(redirect);
117+
server.begin();
118+
119+
Serial.print("Open https://");
120+
Serial.print(WiFi.localIP());
121+
Serial.println("/ in your browser to see it working");
122+
}
123+
124+
void loop() {
125+
yield();
126+
server.handleClient();
127+
}
128+
129+
//This function redirects home
130+
void redirect(){
131+
String url = "https://" + WiFi.localIP().toString();
132+
Serial.println("Redirect called. Redirecting to " + url);
133+
server.sendHeader("Location", url, true);
134+
Serial.println("Header sent.");
135+
server.send( 302, "text/plain", ""); // Empty content inhibits Content-length header so we have to close the socket ourselves.
136+
Serial.println("Empty page sent.");
137+
server.client().stop(); // Stop is needed because we sent no content length
138+
Serial.println("Client stopped.");
139+
}
140+
141+
//This function checks whether the current session has been authenticated. If not, a request for credentials is sent.
142+
bool session_authenticated() {
143+
Serial.println("Checking authentication.");
144+
if (server.authenticateDigest(login,H1)) {
145+
Serial.println("Authentication confirmed.");
146+
return true;
147+
} else {
148+
Serial.println("Not authenticated. Requesting credentials.");
149+
server.requestAuthentication(DIGEST_AUTH,realm.c_str(),authentication_failed);
150+
redirect();
151+
return false;
152+
}
153+
}
154+
155+
//This function sends a simple webpage for changing login credentials to the client
156+
void showcredentialpage(){
157+
Serial.println("Show credential page called.");
158+
if(!session_authenticated()){
159+
return;
160+
}
161+
162+
Serial.println("Forming credential modification page.");
163+
164+
String page;
165+
page = R"(<html>)";
166+
167+
page+=
168+
R"(
169+
<h2>Login Credentials</h2><br>
170+
171+
<form action=")" + change_creds + R"(" method="post">
172+
Login:<br>
173+
<input type="text" name="login"><br>
174+
Password:<br>
175+
<input type="password" name="password"><br>
176+
Confirm Password:<br>
177+
<input type="password" name="password_duplicate"><br>
178+
<p><button type="submit" name="newcredentials">Change Credentials</button></p>
179+
</form><br>
180+
)"
181+
;
182+
183+
page += R"(</html>)";
184+
185+
Serial.println("Sending credential modification page.");
186+
187+
server.send(200, "text/html", page);
188+
}
189+
190+
//Saves credentials to SPIFFS
191+
void savecredentials(String new_login, String new_password)
192+
{
193+
//Set global variables to new values
194+
login=new_login;
195+
H1=ESP8266WebServer::credentialHash(new_login,realm,new_password);
196+
197+
//Save new values to SPIFFS for loading on next reboot
198+
Serial.println("Saving credentials.");
199+
File f=SPIFFS.open(file_credentials,"w"); //open as a brand new file, discard old contents
200+
if(f){
201+
Serial.println("Modifying credentials in file system.");
202+
f.println(login);
203+
f.println(H1);
204+
Serial.println("Credentials written.");
205+
f.close();
206+
Serial.println("File closed.");
207+
}
208+
Serial.println("Credentials saved.");
209+
}
210+
211+
//loads credentials from SPIFFS
212+
void loadcredentials()
213+
{
214+
Serial.println("Searching for credentials.");
215+
File f;
216+
f=SPIFFS.open(file_credentials,"r");
217+
if(f){
218+
Serial.println("Loading credentials from file system.");
219+
String mod=f.readString(); //read the file to a String
220+
int index_1=mod.indexOf('\n',0); //locate the first line break
221+
int index_2=mod.indexOf('\n',index_1+1); //locate the second line break
222+
login=mod.substring(0,index_1-1); //get the first line (excluding the line break)
223+
H1=mod.substring(index_1+1,index_2-1); //get the second line (excluding the line break)
224+
f.close();
225+
} else {
226+
String default_login = "admin";
227+
String default_password = "changeme";
228+
Serial.println("None found. Setting to default credentials.");
229+
Serial.println("user:" + default_login);
230+
Serial.println("password:" + default_password);
231+
login=default_login;
232+
H1=ESP8266WebServer::credentialHash(default_login,realm,default_password);
233+
}
234+
}
235+
236+
//This function handles a credential change from a client.
237+
void handlecredentialchange() {
238+
Serial.println("Handle credential change called.");
239+
if(!session_authenticated()){
240+
return;
241+
}
242+
243+
Serial.println("Handling credential change request from client.");
244+
245+
String login = server.arg("login");
246+
String pw1 = server.arg("password");
247+
String pw2 = server.arg("password_duplicate");
248+
249+
if(login != "" && pw1 != "" && pw1 == pw2){
250+
251+
savecredentials(login,pw1);
252+
server.send(200, "text/plain", "Credentials updated");
253+
redirect();
254+
} else {
255+
server.send(200, "text/plain", "Malformed credentials");
256+
redirect();
257+
}
258+
}

libraries/ESP8266WebServer/src/ESP8266WebServer.cpp

+26-8
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,20 @@ bool ESP8266WebServer::authenticate(const char * username, const char * password
140140
delete[] toencode;
141141
delete[] encoded;
142142
} else if(authReq.startsWith(F("Digest"))) {
143+
String _realm = _extractParam(authReq, F("realm=\""));
144+
String _H1 = credentialHash((String)username,_realm,(String)password);
145+
return authenticateDigest((String)username,_H1);
146+
}
147+
authReq = "";
148+
}
149+
return false;
150+
}
151+
152+
bool ESP8266WebServer::authenticateDigest(const String& username, const String& H1)
153+
{
154+
if(hasHeader(FPSTR(AUTHORIZATION_HEADER))) {
155+
String authReq = header(FPSTR(AUTHORIZATION_HEADER));
156+
if(authReq.startsWith(F("Digest"))) {
143157
authReq = authReq.substring(7);
144158
#ifdef DEBUG_ESP_HTTP_SERVER
145159
DEBUG_OUTPUT.println(authReq);
@@ -170,14 +184,10 @@ bool ESP8266WebServer::authenticate(const char * username, const char * password
170184
_nc = _extractParam(authReq, F("nc="), ',');
171185
_cnonce = _extractParam(authReq, F("cnonce=\""));
172186
}
173-
MD5Builder md5;
174-
md5.begin();
175-
md5.add(String(username) + ':' + _realm + ':' + String(password)); // md5 of the user:realm:user
176-
md5.calculate();
177-
String _H1 = md5.toString();
178187
#ifdef DEBUG_ESP_HTTP_SERVER
179-
DEBUG_OUTPUT.println("Hash of user:realm:pass=" + _H1);
188+
DEBUG_OUTPUT.println("Hash of user:realm:pass=" + H1);
180189
#endif
190+
MD5Builder md5;
181191
md5.begin();
182192
if(_currentMethod == HTTP_GET){
183193
md5.add(String(F("GET:")) + _uri);
@@ -197,9 +207,9 @@ bool ESP8266WebServer::authenticate(const char * username, const char * password
197207
#endif
198208
md5.begin();
199209
if(authReq.indexOf(FPSTR(qop_auth)) != -1 || authReq.indexOf(FPSTR(qop_auth_quoted)) != -1) {
200-
md5.add(_H1 + ':' + _nonce + ':' + _nc + ':' + _cnonce + F(":auth:") + _H2);
210+
md5.add(H1 + ':' + _nonce + ':' + _nc + ':' + _cnonce + F(":auth:") + _H2);
201211
} else {
202-
md5.add(_H1 + ':' + _nonce + ':' + _H2);
212+
md5.add(H1 + ':' + _nonce + ':' + _H2);
203213
}
204214
md5.calculate();
205215
String _responsecheck = md5.toString();
@@ -478,6 +488,14 @@ void ESP8266WebServer::sendContent_P(PGM_P content, size_t size) {
478488
}
479489
}
480490

491+
String ESP8266WebServer::credentialHash(const String& username, const String& realm, const String& password)
492+
{
493+
MD5Builder md5;
494+
md5.begin();
495+
md5.add(username + ":" + realm + ":" + password); // md5 of the user:realm:password
496+
md5.calculate();
497+
return md5.toString();
498+
}
481499

482500
void ESP8266WebServer::_streamFileCore(const size_t fileSize, const String & fileName, const String & contentType)
483501
{

libraries/ESP8266WebServer/src/ESP8266WebServer.h

+3
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ class ESP8266WebServer
8282
void stop();
8383

8484
bool authenticate(const char * username, const char * password);
85+
bool authenticateDigest(const String& username, const String& H1);
8586
void requestAuthentication(HTTPAuthMethod mode = BASIC_AUTH, const char* realm = NULL, const String& authFailMsg = String("") );
8687

8788
typedef std::function<void(void)> THandlerFunction;
@@ -127,6 +128,8 @@ class ESP8266WebServer
127128
void sendContent_P(PGM_P content);
128129
void sendContent_P(PGM_P content, size_t size);
129130

131+
static String credentialHash(const String& username, const String& realm, const String& password);
132+
130133
static String urlDecode(const String& text);
131134

132135
template<typename T>

0 commit comments

Comments
 (0)