ESP8266 Advanced Login Security
More advanced login security for your esp project, including username, password, brute force and inactivity protection.
Things used in this project
Story
Introduction
They often say that security in mcu's are essential, that you have to cover everything from most basic interfacing to advanced communications to the outside world. But there is one problem:
Most of those people work with full blown systems like servers or basic computers.
Those people (generally) don't know the limitations of mcu's specially when cost is a consideration.
Now, our belowed esp8266 has a lot of great features (that I won't talk about), but there isn't a very good login protection example, best one I could find was official espressif's that I used as a base for this code.
Now one thing, I expect in this that you know Arduino IDE well, know how to program using "Arduino C" and are familiar with NodeMCU ecosystem, this example is ment for more experienced people.
What we want to protect
There is never too much security but wee will meet this criteria:
- We have a login page with username/password input
- We do not allow more than 10 false entries in a row to prevent bruteforce attacs
- If there are more than 10 false entries, lock the system from further login for 5 minutes
- If some for example enters wrong username / password 4 times and no further activity is made, reset the "wrong login" counter
- If there is total inactivity for certain amount of time from logged in users, make everyones login invalid
- have a logout button
This may seem like a lot, but don't store your nuclear launch codes here, it's vulnerable to packet sniffing / mitm attack and there is no way to prevent it.
Finnaly, the code
Now as I said earlier, this is not explained in newb level detail, you need to know your arduino and nodemcu ecosystem to understand this It's also deisreable to have basic http/html knowledge. (full code posted below)
Our cookie
This is how we will have our login validation, trough http header validation, our cookie format will be c = sessioncookie;
String sessioncookie, anchars = "abcdefghijklmnopqrstuvwxyz0123456789";
//Alfanumberic character filled string for cookie generation
Void gencookie(){
sessioncookie = "";
for( i = 0; i < 32; i++) sessioncookie += anchars[random(0, anchars.length())];
//Using random chars from anchars string generate 32-bit cookie
}
bool is_authentified(){
//This function checks for Cookie header in request, it compares variable c to sessioncookie and returns true if they are the same
if (server.hasHeader("Cookie")){
String cookie = server.header("Cookie"), authk = "c=" + sessioncookie;
if (cookie.indexOf(authk) != -1) return true;
}
return false;
}
In login():
String header;
if (!is_authentified()){
//This here checks if your cookie is valid in header and it redirects you to /login if not, if ok send loginok html file
String header = "HTTP/1.1 301 OK\r\nLocation: /login\r\nCache-Control: no-cache\r\n\r\n";
server.sendContent(header);
return;
}
In refresh():
if(is_authentified()){
//this is for reseting the inactivity timer, activated on loginok refresh href
tempign = millis();
String header = "HTTP/1.1 301 OK\r\nLocation: /\r\nCache-Control: no-cache\r\n\r\n";
server.sendContent(header);
}
else{
String header = "HTTP/1.1 301 OK\r\nLocation: /login\r\nCache-Control: no-cache\r\n\r\n";
server.sendContent(header);
}
In logoff():
String header = "HTTP/1.1 301 OK\r\nSet-Cookie: c=0\r\nLocation: /login\r\nCache-Control: no-cache\r\n\r\n"; //Set 'c=0', it users header, effectively deleting it's header
server.sendContent(header);
In setup(), the following code tells esp to collect Cookie and User-Agent (not needed) headers from client
const char * headerkeys[] = {"User-Agent","Cookie"} ;
size_t headerkeyssize = sizeof(headerkeys)/sizeof(char*);
server.collectHeaders(headerkeys, headerkeyssize ); //These 3 lines tell esp to collect User-Agent and Cookie in http header when request is made
Login brute force protection
This part is from handleLogin(), it's used to handle login requests, keep track of invalid login attempts and lockout the system if too much invalid entries are made:
if (server.hasHeader("Cookie")){
String cookie = server.header("Cookie");
//Copy the Cookie header to this buffer
}
if (server.hasArg("user") && server.hasArg("pass")){
//if user posted with these arguments
if (server.arg("user") == username && server.arg("pass") == loginPassword && !lock){ //check if login details are good and dont allow it if device is in lockdown
String header = "HTTP/1.1 301 OK\r\nSet-Cookie: c=" + sessioncookie + "\r\nLocation: /\r\nCache-Control: no-cache\r\n\r\n"; //if above values are good, send 'Cookie' header with variable c, with format 'c=sessioncookie'
server.sendContent(header);
trycount = 0; //With good headers in mind, reset the trycount buffer
return;
}
String msg;
//this is our buffer that we will add to the login html page when headers are wrong or device is locked
msg = "<center><br>";
if (trycount != 10 && !lock)trycount++;
//If system is not locked up the trycount buffer
if (trycount < 10 && !lock){
//We go here if systems isn't locked out, we give user 10 times to make a mistake after we lock down the system, thus making brute force attack almost imposible
msg += "Wrong username/password<p></p>";
msg += "You have ";
msg += (10 - trycount);
msg += " tries before system temporarily locks out.";
logincld = millis(); //Reset the logincld timer, since we still have available tries
}
if (trycount == 10){
//If too much bad tries
if(lock){
msg += "Too much invalid login requests, you can use this device in ";
msg += 5 - ((millis() - logincld) / 60000);
//Display lock time remaining in minutes
msg += " minutes.";
}
else{
logincld = millis();
lock = true;
msg += "Too much invalid login requests, you can use this device in 5 minutes."; //This happens when your device first locks down
}
}
}
String content = loginPage;
content += msg + "</center>";
server.send(200, "text/html", content); //merge loginPage and msg and send it
In loop(), we reset the timers if needed:
if(lock && abs(millis() - logincld) > 300000){
lock = false;
trycount = 0;
logincld = millis(); //After 5 minutes is passed unlock the system
}
if(!lock && abs(millis() - logincld) > 60000){
trycount = 0;
logincld = millis();
//After minute is passed without bad entries, reset trycount
}
if(abs(millis() - tempign) > 120000){
gencookie();
tempign = millis();
//if there is no activity from loged on user, change the generate a new cookie. This is more secure than adding expiry to the cookie header
}
Explaining some of the variables in more detail
lock (bool) - this variable is used to set the login lock state, i.e. when too much login attemps are made this goes to true and doesn't allow further logins, and is set to false after timer timeout.
username, loginPassword (String) - our username and password (can't go too much into detail here, can I)
loginPage, loginok (String) - These are our html files for interfacing. I had a version with css but I wanted to make this as simple as posible. HTML code will not be comented.
logincld (unsigned long) - This is our "You tried too much, here is your 5 minute lockout" timer variable that is resets lock variable after 5 minutes
reqmillis (unsigned long) - This is when someone enters wrong details few times, but doesnt trigger lockout mechanism, and no further activity is made on login page, reset the trycount variable
tempign (unsigned long) - This is our logged in user inactivity timeout time variable
trycount (uint8_t) - This stores how many invalid entries are made. It is reseted by a succesfull login, or logincld or reqmillis timer timeout. Maximum allowed false entries are 10 (0 - 9) tries
Few screenshots and final word
Now, while this is suitable for most people, this is far from a reliable security solution for security related products. If you think this will suite your needs, you can use this code however you like, but remember this is just an exapmle, delivered AS IS and WITHOUT ANY GUARANTEES, so use this on your own risk.
沒有留言:
張貼留言