John Bradnam
Published © GPL3+

Combination Password Locker

Protect your passwords with this unique password locker. Once unlocked you can automatically send Website credentials via Bluetooth.

IntermediateFull instructions provided12 hours402

Things used in this project

Hardware components

ESP32 Development Kit V1
×1
Monochrome 0.91”128x32 I2C OLED Display with Chip Pad
DFRobot Monochrome 0.91”128x32 I2C OLED Display with Chip Pad
×1
EC12E24204A2 Rotary Encoder
ALPS EC12E24204A2 Rotary Encoder, Insulated Shaft, Mechanical, Incremental, 24 PPR, 24 Detents, Vertical
×1
Tactile Switch, Top Actuated
Tactile Switch, Top Actuated
17mm Shaft
×1
RGB Diffused Common Anode
RGB Diffused Common Anode
5mm
×1
Passive components
Resistors (0805 SMD): 2 x 10K, 1 x 180R, 1 x 100R, 1 x 47R; Capacitors (0805 SMD): 2 x 0.1uF, 2 x 22nF
×1
Lithium Battery Boost Module 2A 5V Out, 3.7-4.2V
×1

Software apps and online services

Arduino IDE
Arduino IDE

Hand tools and fabrication machines

3D Printer (generic)
3D Printer (generic)
Soldering iron (generic)
Soldering iron (generic)

Story

Read more

Custom parts and enclosures

STL Files

Files required for 3D printing (see description for information on printing)

Schematics

Schematic

PCB

Eagle files

Schematic & PCB in Eagle format

Code

CombinationLockV1.ino

Arduino
/**************************************************************************
 Password Locker with Rotary Combination Lock
 
 Board: "Node32s"
 Partition Scheme: "No OTA (Large APP)"

 Design & Coding by John Bradnam (jbrad2089@gmail.com)
 Encryption code based on software by Daniel J. Murphy (https://github.com/seawarrior181/PasswordPump)

 2022-09-22 - jbrad2089@gmail.com
  - Created initial code
    - 4 digit rotary dial combination lock
    - Master 4 digit code one-way encrypted with sha256 with salt
    - Master password and account/username/password table salts stored in EEPROM
    - Account/username/password table stored in SPIFFS
    - Username & password symmetrically encrypted with AES128 with salts
    - Web server to allow changing of master password and add/modify/delte password table
    - Bluetooth keyboard emnulation to send username and password
    
 Note: ESP32 doesn't really support both WiFi & Bluetooth at the same time.
 Power down after updating the password table over WiFi before sending 
 credentials via bluetooth.
 
 **************************************************************************/
 
//#define BYPASS_MASTER  

#include <OneWire.h>
#include <U8g2lib.h>
#include <SPIFFS.h>
#include <BleKeyboard.h>
#include "Hardware.h"
#include "Display.h"
#include "Encryption.h"
#include "Webserver.h"

BleKeyboard bleKeyboard;
#define KEY_DELAY 10              //mS delay between each keypress when sending via bluetooth

int volatile rotaryPosition = 0;
int lastPosition = -1;
bool volatile lastRotA = false;   // Used to determine direction of rotary encoder
bool volatile lastRotB = false;   // Used to determine direction of rotary encoder
int volatile lastDirection = 0;   // Used to detect change in direction
bool volatile unlocked = false;   // Used to stop combination when unlocked
int volatile menuDirection = 0;   // Used when rotary encoder turns in menu selection mode
int menuSelection = 0;            // Current menu being displayed

//-------------------------------------------------------------------------
// Initialise Hardware
void setup(void)
{
  Serial.begin(115200);
  
  pinMode(ENA_PIN, INPUT);
  pinMode(ENB_PIN, INPUT);
  pinMode(SW_PIN, INPUT_PULLUP);

  attachInterrupt(ENA_PIN, rotaryInterrupt, CHANGE);

  setupDisplay();
  if (!SPIFFS.begin(true))
  {
    drawSmallFont("SPIFFS", "mount error", true);
    return;
  }

  /*
    1. Go to your computers/phones settings
    2. Ensure Bluetooth is turned on
    3. Scan for Bluetooth devices
    4. Connect to the device called "ESP32 Keyboard"
    5. Open site you want to send username and/or password to
  */
  setLedColor(LED_BLUE);
  drawSmallFont("Starting","Bluetooth", true);
  bleKeyboard.setName("Password Locker");
  bleKeyboard.begin();
  setLedColor(LED_RED);

  readEepromData();
  resetLock();

#ifdef BYPASS_MASTER
  //----------------------
  //Temporary for testing
  unlocked = true;
  setLedColor(LED_GREEN);
  strcpy((char*)masterPassword,DEFAULT_PASSWORD);  //Store unencrypted password for AES key
  padPassword(masterPassword);                     //Pad out master password;
  loadSlots();
  menuSelection = MENU_EDIT+1;
  menuDirection = 0;
  displayMenu();
  //---------------------
#endif
    
}

//---------------------------------------------------------------------
// Interrupt Handler: Rotary encoder has moved
void rotaryInterrupt()
{
  bool a = (digitalRead(ENA_PIN) == LOW);
  bool b = (digitalRead(ENB_PIN) == LOW);
  if (a != lastRotA) 
  {
    lastRotA = a;
    if (b != lastRotB) 
    {
      lastRotB = b;
      if (b)
      {
        int d = ((a == b) ? 1 : -1);
        if (d != lastDirection)
        {
          if (lastDirection != 0 && !unlocked)
          {
            //Change in direction
            if (passwordIndex == PASSWORD_SIZE)
            {
              passwordIndex = 0;
            }
            password[passwordIndex] = rotaryPosition;
            passwordIndex++;
          }
          lastDirection = d;
        }
        if (unlocked)
        {
          menuDirection = -d;
        }
        rotaryPosition = rotaryPosition + d;
      }
    }
  }
}

//--------------------------------------------------------------------
//Program Loop
void loop(void)
{ 
  checkRotaryEncoder();
  checkButton();
  
  if (unlocked)
  {
    loopWebserver();
  }
 
}

//--------------------------------------------------------------------
// Test rotary encoder and handle any changes
void checkRotaryEncoder()
{
  if (rotaryPosition != lastPosition)
  {
    lastPosition = rotaryPosition;
    if (!unlocked)
    {
      //Limit rotary position from under or over flow
      if (rotaryPosition < -99)
      {
        rotaryPosition = -99;
      }
      else if (rotaryPosition > 99)
      {
        rotaryPosition = 99;
      }
      //Display value on screen
      sprintf(line1,"%d",abs(rotaryPosition));
      drawLargeFont(line1,true);
    }
    else
    {
      //Change menu item
      changeMenuSelection(menuDirection);
      menuDirection = 0;
      if (menuSelection != MENU_EDIT)
      {
        closeWebserver();        
      }
      displayMenu();
    }
  }
}

//--------------------------------------------------------------------
//Check push button and process any presses
void checkButton()
{
  if (digitalRead(SW_PIN) == LOW)
  {
    delay(10);  //Debounce
    if (digitalRead(SW_PIN) == LOW)
    {
      if (unlocked)
      {
        switch (menuSelection)
        {
          case MENU_EDIT: 
            setupWebserver(); 
            displayMenu();
            break;
          
          case MENU_LOGOUT: 
            resetLock(); 
            break;
          
          default:
            //Send username/password
            sendToComputer(menuSelection - 1);
            break;
        }
      }
      else
      {
        unlocked = testCombination();
        if (unlocked)
        {
          // First read in data and decrypt
          if (loadSlots())
          {
            rotaryPosition = 0;
            menuSelection = MENU_EDIT;
            menuDirection = 0;
            displayMenu();
          }
          else
          {
            drawSmallFont("Locker","Open Failed",true);
          }
        }
      }
      //wait until release
      while (digitalRead(SW_PIN) == LOW);
      delay(200);
    }
  }
}

//--------------------------------------------------------------------
//Display the current entry
void displayMenu()
{
  switch(menuSelection)
  {
    case MENU_EDIT: drawSmallFont(edit.account, (char *)edit.username, true); break;
    case MENU_LOGOUT: drawSmallFont(logout.account, "", true); break;
    default: drawSmallFont(slots[menuSelection-1].account, "", true);
  }
}

//--------------------------------------------------------------------
//Change the current menu selection to the previous/next valid entry
// dir - +1 or -1
void changeMenuSelection(int dir)
{      
  SLOT* p;
  do
  {
    menuSelection += dir;
    if (menuSelection < 0)
    {
      menuSelection = MENU_ITEMS - 1;
    }
    else if (menuSelection >= MENU_ITEMS)
    {
      menuSelection = 0;
    }
    switch(menuSelection)
    {
      case MENU_EDIT: p = &edit; break;
      case MENU_LOGOUT: p = &logout; break;
      default: p = &slots[menuSelection-1]; break;
    }
  } while (p->purpose == UNUSED);
}

//--------------------------------------------------------------------
//Reset lock
void resetLock()
{
  unlocked = false;
  passwordIndex = 0;
  rotaryPosition = 0;
  lastPosition = -1;
  lastDirection = 0;
  setLedColor(LED_RED);
}

//--------------------------------------------------------------------
// Returns true if combination is correct
bool testCombination()
{
  bool unlock = false;
  if (passwordIndex == PASSWORD_SIZE && rotaryPosition == 0)
  {
    //Convert to ASCII
    sprintf(line2,"%+d%+d%+d%+d",password[0],password[1],password[2],password[3]);
    unlock = authenticateMaster((uint8_t*)line2);
  }
  if (unlock)
  {
    setLedColor(LED_GREEN);
    drawSmallFont("Locker", "UNLOCKED", true);
    strcpy((char*)masterPassword,line2);       //Store unencrypted password for AES key
    padPassword(masterPassword);        //Pad out master password;
  }
  else
  {
    resetLock();
  }
  return unlock;
}

//--------------------------------------------------------------------
// Send username and/or password via bluetooth
void sendToComputer(int slot)
{
  SLOT* p = &slots[slot];
  setLedColor(LED_BLUE);
  drawSmallFont(p->account, "Connect BT..", true);
  
  if (bleKeyboard.isConnected())
  {
    drawSmallFont(p->account, "Sending Cred.", true);
    sendString(p->username, KEY_DELAY);
    switch(p->usrDelm)
    {
      case NO_DELM: break;
      case TAB_DELM: bleKeyboard.write(KEY_TAB); break;
      case CR_DELM: bleKeyboard.write(KEY_RETURN); break;
    }
    delay(500);
    if (p->password[0] != '\0')
    {
      sendString(p->password, KEY_DELAY);
      switch(p->pwdDelm)
      {
        case NO_DELM: break;
        case TAB_DELM: bleKeyboard.write(KEY_TAB); break;
        case CR_DELM: bleKeyboard.write(KEY_RETURN); break;
      }
      delay(500);
    }
  }
  else
  {
    drawSmallFont(p->account, "No Bluetooth", true);
    delay(1000);
  }
  drawSmallFont(p->account, "", true);
  bleKeyboard.end();
  setLedColor(LED_GREEN);
}

//--------------------------------------------------------------------
// Send string to bluetooth with character delay
void sendString(uint8_t* p, int delta)
{
  while (*p != '\0')
  {
    bleKeyboard.write(*p++);
    delay(delta);
  }
}

index.html.h

C Header File
const char index_html[] PROGMEM = R"=====(
<!DOCTYPE html>
<html>
<head>
  <title>Password Locker</title>
  <meta charset="utf-8" />
  <style type="text/css">
    * {font-family:sans-serif;margin:0;padding:0;}
    th {font-size:10pt;}
    th.title {font-size:12pt;padding:5px;}
    td.master {padding-top:10px;padding-bottom:10px;}
    label {font-size:10pt;margin-left:10px;margin-right:10px;}
    .master label {display:inline-block;width:35px;}
    .master input {width:220px;}
    .msg {text-align:center;font-size:10pt;padding-bottom:10px;color:red;}
    .num {width:20px;}
    .name input {width:200px;}
    .uinp input {width:200px;margin-left:10px;}
    .udlm select {width:60px;}
    .pinp input {width:200px;margin-left:10px;}
    .pdlm select {width:55px;}
    td.btn {padding-top:10px;text-align:center;}
    input#btn {padding:0px 8px 0px 8px;}
  </style>
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
  <script type="text/javascript">
    //<![CDATA[
    window.addEventListener('load', setup);
    
    function setup()
    {
      var xhttp = new XMLHttpRequest();
      xhttp.onreadystatechange = function () {
        if (xhttp.readyState == 4 && xhttp.status == 200)
        {
          document.getElementById('locker').innerHTML = xhttp.responseText;
          elems = document.querySelectorAll('td a');
          [].forEach.call(elems, function (el) {
            el.addEventListener('click', clearRow, false);
          });
        }
      };
      xhttp.open('GET', 'locker', true);
      xhttp.send();
      
      document.getElementById('btn').addEventListener('click', validateForm, false);
    }
    
    function clearRow(event)
    {
      var tr = event.currentTarget.parentElement.parentElement;
      var nodes = tr.getElementsByTagName("INPUT");
      if  ((nodes[0].value == "") || (confirm("Are you sure you wish to clear the entry \"" + nodes[0].value + "\"")))
      {
        for (let n = 0; n < nodes.length; n++) { nodes[n].value = "" };
        var nodes = tr.getElementsByTagName("SELECT");
        for (let n = 0; n < nodes.length; n++) { nodes[n].selectedIndex = 0 };
      }
      event.preventDefault();
      return false;
    }
    
    function validateForm(event)
    {
      var valid = true;
      var names = document.getElementsByClassName("name");
      for (let n = 0; n < names.length; n++) {
        var tr = names[n].parentElement;
        var nodes = tr.getElementsByTagName("INPUT");
        if (nodes[0].value == "" && (nodes[1].value != "" || nodes[2].value != ""))
        {
          alert("Row " + String(n + 1) + " - Username and/or password must have a display name in row");
          names[n].firstChild.focus();
          event.preventDefault();
          valid = false;
          break;
        }
      }
      return valid;
    }
    //]]>
  </script>
</head>
<body>
  <form action="/" method="POST">
    <div id="locker"></div>
  </form>
</body>
</html>
)=====";

Encryption.h

C/C++
#pragma once

/*
 * Encrption and EEPROM routines
 *  - Encryption based on software by Daniel J. Murphy
 *  https://github.com/seawarrior181/PasswordPump
 */

//Uncomment to reset EEPROM data
//#define RESET_EEPROM

#include <EEPROM.h>
#include <SHA256.h>                                           // for hashing the master password
#include <AES.h>                                              // for encrypting credentials

#define PASSWORD_SIZE 4                                       // 4 digit password
#define DEFAULT_PASSWORD "+1-2+3-4"                           // 4 digit password (+ rotate encoder left, - rotate encoder right)
int volatile password[PASSWORD_SIZE];                         // Used by rotary encoder interrupt to build password
int volatile passwordIndex = 0;                               // Used to determine what digit we are entering

#define MAX_SLOTS                 20                          // Number of accounts that can be stored
#define MENU_ITEMS                (MAX_SLOTS + 2)             // Number of items in menu
#define MENU_EDIT                 0                           // Value of menu selection for EDIT menu
#define MENU_LOGOUT               (MAX_SLOTS + 1)             // Value of menu selection for LOGOUT menu
#define CRED_SALT_SIZE            0x02                        // 2 bytes, a uint16_t.  size of key for aes128 == 16 bytes.  2 bytes will be for salt. range= 0 - 65,535
#define MASTER_PASSWORD_SIZE      (0x10 - CRED_SALT_SIZE)     // aes256 keysize = 32 bytes.  aes128 keysize = 16 bytes, aes256 blocksize = 16!, only the first 15 chars are part of the password, the rest are ignored.
#define HASHED_MASTER_PASSWORD_SZ (MASTER_PASSWORD_SIZE * 2)  // the size of the hashed master password
#define NULL_TERM                 0x00                        // The null terminator, NUL, ASCII 0, or '\0'                 
#define SHA_ITERATIONS            1                           // number of times to hash the master password (won't work w/ more than 1 iteration)

//EEPROM handling
#define EEPROM_ADDRESS 0
#define EEPROM_MAGIC 0x0DAD0BAD
typedef struct {
  uint32_t magic;
  uint8_t master[HASHED_MASTER_PASSWORD_SZ];                  // Master password hash
  uint8_t salt[MASTER_PASSWORD_SIZE];                         // Current salt
  uint8_t salts[MAX_SLOTS][2];                                // Salts for AES encryption
} EEPROM_DATA;

EEPROM_DATA EepromData;                                       // Current EEPROM settings

#define MAX_ACCOUNT 14                                        // Display name for entry
#define MAX_USERNAME 32
#define MAX_PASSWORD 32
enum DELIMITER { NO_DELM, TAB_DELM, CR_DELM };
enum PURPOSE { UNUSED, ACCOUNT, EDIT, LOGOUT };
typedef struct {
  PURPOSE purpose;
  DELIMITER usrDelm;
  DELIMITER pwdDelm;
  char account[MAX_ACCOUNT+1];
  char pad_2[1];
  uint8_t username[MAX_USERNAME];
  uint8_t password[MAX_PASSWORD];
} SLOT;

SLOT edit = {EDIT, NO_DELM, NO_DELM, "Edit locker" };
SLOT slots[MAX_SLOTS];
SLOT logout = {LOGOUT, NO_DELM, NO_DELM, "Close locker" };

SHA256 sha256;
AESSmall128 aes;                                              // 16 byte key, 32 byte block
//AESSmall256 aes;                                            // 32 byte key, 32 byte block; this uses 4% more program memory. Set 
uint8_t masterPassword[MASTER_PASSWORD_SIZE];                 // this is where we store the master password for the device

//-----------------------------------------------------------------------------------
// Forward references

void setUUID(uint8_t *password, uint8_t size, bool appendNullTerm);
void setCredSalt(uint8_t *credSalt, uint8_t size);
bool validatePasswordSyntax(String pwd);
void padPassword(uint8_t *password);
void setMasterPassword(uint8_t *enteredPassword);
boolean authenticateMaster(uint8_t *enteredPassword);
void sha256Hash(uint8_t* password);
void sha256HashOnce(char *password);
void setKey(uint8_t pos);
void memencrypt(uint8_t* dst, uint8_t* src, uint8_t len, int slot);
void memdecrypt(uint8_t* dst, uint8_t* src, uint8_t len, int slot);
bool loadSlots();
bool saveSlots();
void writeEepromData();
void readEepromData();

//---------------------------------------------------------------
//- UUID Generation
void setUUID(uint8_t *password, uint8_t size, bool appendNullTerm) 
{
  for (uint8_t i = 0; i < size; i++) 
  {
    password[i] = random(33,126);                                               // maybe we should use allChars here instead? We're generating PWs w/ chars that we can't input...
                                                                                // 32 = space, 127 = <DEL>, so we want to choose from everything in between.
  }
  if (appendNullTerm) 
  {
    password[size - 1] = NULL_TERM;
  }
}

//---------------------------------------------------------------
// Generate a salt for the AES key for each slot
void setCredSalt(uint8_t *credSalt, uint8_t size) 
{
  for (uint8_t i = 0; i < size; i++) 
  {
    credSalt[i] = random(0,255);                                                
  }
}

//---------------------------------------------------------------
// Check password is in the form -n+n-n+n or -n+n-n+n
bool validatePasswordSyntax(String pwd)
{
  int i = 0;
  int j = 0;
  int n = 0;
  char delm = pwd[0];
  bool valid = true;
  while (valid && i < pwd.length())
  {
    valid = false;
    if (pwd[i] == delm)
    {
      delm = (pwd[i++] == '-') ? '+' : '-';
      n = 0;
      while (i < pwd.length() && pwd[i] >= '0' && pwd[i] <= '9')
      {
        n = n * 10 + (int)(pwd[i++] - '0');
      }
      valid = (n > 0 && n <= 99);
      j++;
    }
  }
  return (valid && j == 4); 
}

//---------------------------------------------------------------
// Pad out password to MASTER_PASSWORD_SIZE bytes
void padPassword(uint8_t *password)
{
  uint8_t pos = 0;
  while (password[pos++] != NULL_TERM);                                  // make sure the unencrypted password is 16 chars long
  while (pos < (MASTER_PASSWORD_SIZE - 1)) password[pos++] = NULL_TERM;  // "           "              " , right padded w/ NULL terminator
  password[MASTER_PASSWORD_SIZE - 1] = NULL_TERM;                        // NULL_TERM in index 13 no matter what (TODO: is this necessary?)
}

//---------------------------------------------------------------
// Write the master password to the EEPROM
void setMasterPassword(uint8_t *enteredPassword)
{
  padPassword(enteredPassword);
  setUUID(EepromData.salt, MASTER_PASSWORD_SIZE, false);                                 // generate a random salt
  memcpy(EepromData.master, EepromData.salt, MASTER_PASSWORD_SIZE);                      // copy salt into the hashed master password variable
  memcpy(EepromData.master + MASTER_PASSWORD_SIZE,enteredPassword,MASTER_PASSWORD_SIZE); // concatinate the salt and the master password
  sha256Hash(EepromData.master);                                                         // hash the master password in place; pass in 32, get back 16
  writeEepromData();                                                                     // write it to EEPROM
}

//---------------------------------------------------------------
// Verify if the master password is correct here
boolean authenticateMaster(uint8_t *enteredPassword) 
{
  uint8_t enteredMasterHash[HASHED_MASTER_PASSWORD_SZ];                         // holds the unhashed master password after some processing

  padPassword(enteredPassword);
  memcpy(enteredMasterHash,EepromData.salt,MASTER_PASSWORD_SIZE);                      // copy salt into the hashed master password variable
  memcpy(enteredMasterHash+MASTER_PASSWORD_SIZE,enteredPassword,MASTER_PASSWORD_SIZE); // entered password
  sha256Hash(enteredMasterHash);                                                       // hash the master salt||entered password
  
  return (memcmp(enteredMasterHash,EepromData.master,HASHED_MASTER_PASSWORD_SZ) == 0);
}

//---------------------------------------------------------------
// Hash using SHA encrpytion SHA_ITERATIONS times
void sha256Hash(uint8_t* password) 
{
  for (int i = 0; i < SHA_ITERATIONS; i++) 
  {
    sha256HashOnce((char*)password);
  }
}

//---------------------------------------------------------------
// Hash using SHA encrpytion once
void sha256HashOnce(char *password) 
{
  size_t passwordSize = strlen(password);                                       // strlen(password) == 28
  size_t posn, len;
  uint8_t value[HASHED_MASTER_PASSWORD_SZ];                                     // HASHED_MASTER_PASSWORD_SZ == 32

  sha256.reset();
  for (posn = 0; posn < passwordSize; posn += 1)                                // posn=0|1|2|3|...
  {
    len = passwordSize - posn;                                                // 28|27|26|25|...
    if (len > passwordSize)
    {
      len = passwordSize;
    }
    sha256.update(password + posn, len);                                      // password[0], 28|password[1],27|password[2],26
  }
  sha256.finalize(value, sizeof(value));
  sha256.clear();
  memcpy(password, value, HASHED_MASTER_PASSWORD_SZ);
}

//---------------------------------------------------------------
// Create a encryption key based on the master password and salt
void setKey(uint8_t pos) 
{
  uint8_t key[MASTER_PASSWORD_SIZE + CRED_SALT_SIZE];                           // key size is 16
  memcpy(key,&EepromData.salts[pos][0],CRED_SALT_SIZE);                         // Get the salt assigned this slot                   
  memcpy(key + CRED_SALT_SIZE, masterPassword, MASTER_PASSWORD_SIZE);           // append the master password to the key to set the encryption key
  aes.setKey(key, MASTER_PASSWORD_SIZE + CRED_SALT_SIZE);                       // set the key for aes to equal the salt||un-hashed entered master password
}

//---------------------------------------------------------------
// Encrypt a source buffer and place in destination buffer
void memencrypt(uint8_t* dst, uint8_t* src, uint8_t len, int slot)
{
  uint8_t pos = 0;
  while (src[pos++] != '\0');                   // make sure the src is len chars long, pad with nulls
  while (pos < len) src[pos++] = '\0';          // "           "              "
  setKey(slot);
  //Encrypt a total of 32 bytes by 16 bytes at a time
  aes.encryptBlock(dst, src);
  aes.encryptBlock(dst+16, src+16);
}  

//---------------------------------------------------------------
// Decrypt a source buffer and place in destination buffer
void memdecrypt(uint8_t* dst, uint8_t* src, uint8_t len, int slot)
{
  setKey(slot);
  //Decrypt a total of 32 bytes by 16 bytes at a time
  aes.decryptBlock(dst, src);
  aes.decryptBlock(dst+16, src+16);
}  

//---------------------------------------------------------------
// Read and decrypt slots from flash
//
bool loadSlots()
{
  SLOT t;
  int c = sizeof(SLOT) * MAX_SLOTS;
  char* p;
  File file;

#ifndef RESET_EEPROM
  //Open file for reading
  file = SPIFFS.open("/slots","r");
  if (!file)
#endif  
  {
    //If no file, create a blank one
    file = SPIFFS.open("/slots","w");
    if (!file)
    {
      drawSmallFont("SPIFFS", "create fail", true);
      return false;
    }
    //Write out new slot list
    p = (char*)&slots;
    memset(p, '\0', c);
    for (int i = 0; i < c; i++)
    {
      file.print(*p++);
    }
    file.seek(0, SeekSet);
  }

  for (int s = 0; s < MAX_SLOTS; s++)
  {
    //Read to temporary slot
    p = (char *)&t;
    for (int i = 0; i < sizeof(SLOT); i++)
    {
      if (file.available())
      {
        *p = file.read();
      }
      else
      {
        *p = '\0';
      }
      p++;
    }

    //Copy to slot and decrypt username and password
    memcpy(&slots[s],&t,sizeof(SLOT));
    if (slots[s].purpose == ACCOUNT)
    {
      memdecrypt(slots[s].username,t.username,MAX_USERNAME,s);
      memdecrypt(slots[s].password,t.password,MAX_PASSWORD,s);
    }
  }

  //Close file
  file.close();
  return true;
}

//---------------------------------------------------------------
// Encrypt and save slots to flash
//
bool saveSlots()
{
  SLOT t;

  //Open file
  File file = SPIFFS.open("/slots","w");
  if (!file)
  {
    drawSmallFont("SPIFFS", "open w fail", true);
    return false;
  }

  //Write file
  for (int s = 0; s < MAX_SLOTS; s++)
  {
    //Copy unencrypted slot to temporary slot and encrypt
    memcpy(&t,&slots[s],sizeof(SLOT));
    if (slots[s].purpose == ACCOUNT)
    {
      memencrypt(t.username,slots[s].username,MAX_USERNAME,s);
      memencrypt(t.password,slots[s].password,MAX_PASSWORD,s);
    }
    //Write encrypted temporary slot to file
    char* p = (char *)&t;
    for (int i = 0; i < sizeof(SLOT); i++)
    {
      file.print(*p++);
    }
  }

  //Close file
  file.close();
  return true;
}

//---------------------------------------------------------------
//Write the EepromData structure to EEPROM
void writeEepromData()
{
  EEPROM.put(EEPROM_ADDRESS,EepromData);
  EEPROM.commit();
}

//---------------------------------------------------------------
//Read the EepromData structure from EEPROM, initialise if necessary
void readEepromData()
{
  //Eprom
  EEPROM.begin(sizeof(EEPROM_DATA));
  EEPROM.get(EEPROM_ADDRESS,EepromData);
  #ifndef RESET_EEPROM
  if (EepromData.magic != EEPROM_MAGIC)
  #endif
  {
    EepromData.magic = EEPROM_MAGIC;

    //Add a default master password
    uint8_t defaultPassword[MASTER_PASSWORD_SIZE];
    memset(defaultPassword,'\0',MASTER_PASSWORD_SIZE);
    strcpy((char*)defaultPassword,DEFAULT_PASSWORD);
    setMasterPassword(defaultPassword);

    //Add salt for each slot
    for (int i = 0; i < MAX_SLOTS; i++)
    {
      setCredSalt(&EepromData.salts[i][0], CRED_SALT_SIZE);  // calculate the salt and put it in key
    }
    writeEepromData();
  }
}

Display.h

C/C++
#pragma once

/*
 * Display routines
*/

#include "Hardware.h"

// Instantiate the display
U8G2_SSD1306_128X32_UNIVISION_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE);

enum LED_COLOR {LED_OFF,LED_RED,LED_GREEN,LED_BLUE,LED_YELLOW};

//used to create display lines
char line1[32]; 
char line2[32]; 

//-----------------------------------------------------------------------------------
// Forward references

void setupDisplay();
void setLedColor(LED_COLOR c);
void drawLargeFont(const char* p, bool center);
void drawSmallFont(const char* p1, const char* p2, bool center);

//-------------------------------------------------------------------------
// Initialise Hardware
void setupDisplay()
{
  pinMode(BLU_PIN, OUTPUT);
  pinMode(GRN_PIN, OUTPUT);
  pinMode(RED_PIN, OUTPUT);
  
  setLedColor(LED_OFF);
  
  u8g2.begin();
  u8g2.clearBuffer();         // clear the internal memory
  u8g2.sendBuffer();          // transfer internal memory to the display
}

//--------------------------------------------------------------------
// Draw a single line using the large font

void setLedColor(LED_COLOR c)
{
  digitalWrite(BLU_PIN, (c == LED_BLUE) ? LOW : HIGH);
  digitalWrite(GRN_PIN, (c == LED_GREEN || c == LED_YELLOW) ? LOW : HIGH);
  digitalWrite(RED_PIN, (c == LED_RED || c == LED_YELLOW) ? LOW : HIGH); 
}

//--------------------------------------------------------------------
// Draw a single line using the large font
void drawLargeFont(const char* p, bool center)
{    
  u8g2.clearBuffer();
  u8g2.setFont(u8g2_font_logisoso28_tr);
  int width = (center) ? u8g2.getStrWidth(p) : 128;
  u8g2.drawStr((128-width)/2,26,p);
  u8g2.sendBuffer();                // transfer internal memory to the display
}

//--------------------------------------------------------------------
//Draw top and/or bottom lines using the small font
void drawSmallFont(const char* p1, const char* p2, bool center)
{
  int width = 0;
  if (p1 != NULL && p2 != NULL)
  {    
    u8g2.clearBuffer();
  }
  u8g2.setFont(u8g2_font_helvB12_tr);
  if (p1 != NULL)
  {
    width = (center) ? u8g2.getStrWidth(p1) : 128;
    u8g2.drawStr((128-width)/2,14,p1);
  }
  if (p2 != NULL)
  {
    width = (center) ? u8g2.getStrWidth(p2) : 128;
    u8g2.drawStr((128-width)/2,29,p2);
  }
  u8g2.sendBuffer();                // transfer internal memory to the display
}

Webserver.h

C/C++
#pragma once

/*
 * Webserver routines
*/
//Uncomment to reset WIFI credentials
//#define RESET_WIFI

#include <WiFi.h>
#include <WiFiClient.h>
#include <WebServer.h>
#include "Display.h"
#include "Encryption.h"

#include "index.html.h"

#define AP_NAME "PasswordLockerAP"      // Name of AP when configuring WiFi credentials
#define WIFI_SSID "YOUR_SSID"
#define WIFI_PASSWORD "YOU_PASSWORD"

#define WIFI_TIMEOUT 30000              // checks WiFi every ...ms. Reset after this time, if WiFi cannot reconnect.
#define HTTP_PORT    80

//Password change
bool changePassword = false;
String oldPassword = "";
String newPassword = "";
String masterMessage = "";

// Instantiate a web server
WebServer server(HTTP_PORT);
bool serverStarted = false;

//-----------------------------------------------------------------------------------
// Forward references

bool setupWebserver();
void closeWebserver();
void loopWebserver();
String ip2str(const IPAddress& ipAddress);
void handleNotFound(); 
void handleIndexHtml(); 
void handlePasswordTable(); 
String getTextInput(int index, String className, String value, int maxLength);
String getSelectInput(int index, String className, int value);
char* rtrim(char* s);
String htmlEncode(String src);

//-------------------------------------------------------------------------
// Initialise Hardware
bool setupWebserver()
{
  if (!serverStarted)
  {
    // Local intialization. Once th WiFiManager's business is done, there is no need to keep it around
    WiFi.mode(WIFI_STA);
    WiFi.begin(WIFI_SSID, WIFI_PASSWORD);  
  
    String s = "to WIFI .";
    while (WiFi.status() != WL_CONNECTED) 
    {
      drawSmallFont("Connecting", s.c_str(), true);
      delay(500);
      s += ".";
    }
  
    strcpy((char *)edit.username,ip2str(WiFi.localIP()).c_str()); //Store IP address for display
    setLedColor(LED_YELLOW);
  
    server.on("/", handleIndexHtml);
    server.on("/locker", HTTP_GET, handlePasswordTable);
    server.onNotFound(handleNotFound);
    
    server.begin();
    serverStarted = true;
  }
  return true;
}

//-------------------------------------------------------------------------
// Shut down the webserver
void closeWebserver()
{
  if (serverStarted)
  {
    while (WiFi.status() == WL_CONNECTED) 
    {
      WiFi.disconnect();
      delay(100);
    }
    serverStarted = false;
  }
  strcpy((char *)edit.username,""); //Store IP address for display
  setLedColor(LED_GREEN);
}

//-------------------------------------------------------------------------
// Main Web server loop
void loopWebserver()
{
  server.handleClient();
}

//-------------------------------------------------------------------------
// Convert IPaddress to printable form
String ip2str(const IPAddress& ipAddress)
{
  return String(ipAddress[0]) + String(".") + String(ipAddress[1]) + String(".") + String(ipAddress[2]) + String(".") + String(ipAddress[3]); 
}

//-------------------------------------------------------------------------
// Send 404 not found response
void handleNotFound() 
{
  String s = "File Not Found\n\n";
  s += "URI: ";
  s += server.uri();
  s += "\nMethod: ";
  s += (server.method() == HTTP_GET) ? "GET" : "POST";
  s += "\nArguments: ";
  s += server.args();
  s += "\n";
  for (uint8_t i = 0; i < server.args(); i++) {
    s += " " + server.argName(i) + ": " + server.arg(i) + "\n";
  }
  server.send(404, "text/plain", s);
}

//-------------------------------------------------------------------------
// Send primary HTML page
void handleIndexHtml() 
{
  if (server.method() == HTTP_POST)
  {
    //Submit
    //  "name" : ""
    //  "uinp" : ""
    //  "udlm" : "0"
    //  "pinp" : ""
    //  "pdlm" : "0"
    masterMessage = "";
    int n = 0;
    int i = 0;

    //Check for master password change
    changePassword = (server.argName(i) == "change");
    if (changePassword)
    {
      i++;
    }
    oldPassword = server.arg(i++);
    newPassword = server.arg(i++);
    if (changePassword)
    {
      //Change master passord if valid
      //#define DEFAULT_PASSWORD "+1-2+3-4"                           // 4 digit password (+ rotate encoder left, - rotate encoder right)
      if  (strcmp(oldPassword.c_str(), (char*)masterPassword) == 0)
      {
        if (validatePasswordSyntax(newPassword))
        {
          strcpy((char*)masterPassword,newPassword.c_str());          //Store unencrypted password for AES key
          padPassword(masterPassword);                                //Pad out master password;
          setMasterPassword(masterPassword);
          masterMessage = "Master password changed";
          changePassword = false;                                     //Clear out fields of password is changed
        }
        else
        {
          masterMessage = "New password format is -n+n-n+n or +n-n+n-n]";
        }
      }
      else
      {
        masterMessage = "Old password not valid";
      }
    }

    //Process slots        
    while (i < server.args() && n < MAX_SLOTS)
    {
      //Clear out slot
      memset((char*)&slots[n], '\0', sizeof(SLOT));
      memcpy(slots[n].account, server.arg(i++).c_str(), MAX_ACCOUNT);
      rtrim(slots[n].account);
      if (strlen(slots[n].account) > 0)
      {
        slots[n].purpose = ACCOUNT;
        memcpy(slots[n].username, server.arg(i++).c_str(), MAX_USERNAME);
        slots[n].usrDelm = (DELIMITER)server.arg(i++).toInt();
        memcpy(slots[n].password, server.arg(i++).c_str(), MAX_PASSWORD);
        slots[n].pwdDelm = (DELIMITER)server.arg(i++).toInt();
      }
      else
      {
        i = i + 4;
      }
      n++;
    }
    saveSlots();    
  }

  //Resend page
  server.send_P(200, "text/html", index_html);
}

//-------------------------------------------------------------------------
// Send current password table
//  Ajax call from index.html
void handlePasswordTable() 
{
  String locker = String("<table><tr><th class='title' colspan='6'>PASSWORD LOCKER CONTENTS</th></tr>");
  locker += String("<tr><td>&nbsp;</td><td class='check'><input type='checkbox' name='change' id='change'");
  if (changePassword)
  {
    locker += String(" checked='checked'");
  }
  locker += String("/><label for='change'>Change master password</label></td>");
  locker += String("<td class='master' colspan='2'><label>Old:</label><input type='text' name='old'");
  if (changePassword)
  {
    locker += String(" value='") + htmlEncode(oldPassword) + String("'");
  }
  locker += String("/></td><td class='master' colspan='2'><label>New:</label><input type='text' name='new'");
  if (changePassword)
  {
    locker += String(" value='") + htmlEncode(newPassword) + String("'");
  }
  locker += String("/></td></tr>");
  locker += String("<tr><td>&nbsp;</td><td class='msg' colspan='4'><span>" + masterMessage + "</span></td></tr>");
  locker += String("<tr><th>&nbsp;</th><th>DISPLAY NAME</th><th colspan='2'>USERNAME</th><th colspan='2'>PASSWORD</th></tr>");
  for (uint8_t i = 0; i < MAX_SLOTS; i++) 
  {
    locker += String("<tr userdata='row_" + String(i) + "'>");
    locker += String("<td class='num'><a href='#' class='fa fa-trash' title='Clear row'></a></td>");
    locker += getTextInput(i, "name", slots[i].account, MAX_ACCOUNT);
    locker += getTextInput(i, "uinp", (const char*)slots[i].username, MAX_USERNAME);
    locker += getSelectInput(i, "udlm", (int)slots[i].usrDelm);
    locker += getTextInput(i, "pinp", (const char*)slots[i].password, MAX_PASSWORD);
    locker += getSelectInput(i, "pdlm", (int)slots[i].pwdDelm);
    locker += String("</tr>");
  }
  locker += String("<tr><td>&nbsp;</td><td colspan='4' class='btn'><input type='submit' id='btn' value='Save Changes' /></td></tr></table>");
  server.send(200, "text/plain", locker);
}

//Format a <input type="text" control
String getTextInput(int index, String className, String value, int maxLength)
{
  String n = className + String(index);
  String s = "<td class='" + className + "'><input type='text' name='" + className + "'";
  if (value != "")
  {
    s += " value='" + htmlEncode(value) + "'";
  }
  if (maxLength > 0)
  {
    s += " maxlength='" + String(maxLength) + "'";
  }
  s += " /></td>";
  return s;
}

//Format a <select control
String getSelectInput(int index, String className, int value)
{
  String n = className + String(index);
  String s = "<td class='" + className + "'><select name='" + className + "'>";
  s += "<option value='0'";
  if (value == 0) { s += " selected='selected'"; }
  s += ">none</option>";
  s += "<option value='1'";
  if (value == 1) { s += " selected='selected'"; }
  s += ">tab</option>";
  s += "<option value='2'";
  if (value == 2) { s += " selected='selected'"; }
  s += ">cr</option>";
  s += " /></td>";
  return s;
}

//-------------------------------------------------------------------------
// Trim spaces t the right and fills them with nulls
char* rtrim(char* s)
{
  char* back = s + strlen(s);
  while (isspace(*--back))
  {
    *(back+1) = '\0';
  }
  return s;
}

//-------------------------------------------------------------------------
// Encode a string to HTML
String htmlEncode(String src)
{
  String dst = "";
  for (int i = 0; i < src.length(); i++)
  {
    switch (src[i])
    {
      case '&': dst += "&amp;"; break;
      case '<': dst += "&lt;"; break;
      case '>': dst += "&gt;"; break;
      case '\'': dst += "&apos;"; break; 
      case '\"': dst += "&quot;"; break; 
      default: dst += src[i];
    }
  }
  return dst;
}

Hardware.h

C/C++
#pragma once

/*
 * Hardware definitions
*/

#define V3	//V3 Prototype, V4 or higher Final version

#ifdef V3
	#define SCL_PIN 22
	#define SDA_PIN 21
	#define BLU_PIN 26
	#define GRN_PIN 13
	#define RED_PIN 32
	#define ENA_PIN 27
	#define ENB_PIN 33
	#define ENS_PIN 16
	#define SW_PIN 25
	#define IRQ_PIN 19
	#define DATA_PIN 18
#endif

#ifdef V4
	#define SCL_PIN 22
	#define SDA_PIN 21
	#define BLU_PIN 32
	#define GRN_PIN 33
	#define RED_PIN 25
	#define ENA_PIN 27
	#define ENB_PIN 26
	#define ENS_PIN 16
	#define SW_PIN 13
	#define IRQ_PIN 19
	#define DATA_PIN 18
#endif

Credits

John Bradnam

John Bradnam

145 projects • 179 followers
Thanks to Daniel J. Murphy.

Comments