Skip to content

AllanOricil/esp32-mfa-authenticator

Repository files navigation

Build with PlatformIO Contributor Covenant CI

ESP32 MFA Authenticator

This is a personal project that I created to help me to get MFA TOTPs without using my phone.

Other motives:

  • it is not safe to store both the password and the MFA secret in the same place, like in a password manager. It creates a single point of failure.
  • it is not safe to store secrets on a device that you use when going out.
  • it is not safe to store MFA secrets for my personal and professional life in the same place.
  • if my phone is stolen or lost, I would have to re-register MFA for all services.
  • secrets are decoupled from the device, and this allows me to easily switch to another one when necessary. I just need to insert the SD card containing my services onto another device flashed with the same code.
  • not all services that I use support fido keys or passkeys.
  • yubikey is expensive where I live, and I would need a lot.
  • I wanted to learn how MFA works.
  • I wanted to learn how ESP32 works.

🌐 Install

You can flash your ESP32-CYD board with the latest build using this site.

Important

Read the site and the github workflows source codes to verify that the build artifact is, in fact, the one from the latest release published in this repository.

Note

This site was based on https://esphome.github.io/esp-web-tools/

🎬 Demos

demo_2.mp4
demo_3.mp4
demo_v0.5.0.mp4
demo_v0.5.0-2.mp4

βš™οΈ Parts

  • ESP32-2432S028
  • 3D printed black case
  • Acrillic case

Tip

You can buy this board from Aliexpress clicking on any of these affiliate links: USD BRL

Tip

You can buy this acrillic case from Aliexpress clicking on any of these affiliate links: USD BRL

Tip

The 3D model for the black case was taking from this link

πŸ’΅ Total Project Cost

Part Cost
ESP32-2432S028 9.25 USD
3D printed black case 12.7 USD
Acrillic case 2.5 USD

Note

The above list doesn't consider expenses with taxes and shipping.

Note

Prices were taking in February 2024.

πŸ’» Dev Environment Requirements

dependency version
python >= v3.9
node >= v18.18
npm >= v10.2
vscode >= v1.87
platform.io ide vscode extension >= v3.3
docker >= v25.0

Important

Don't forget to install a driver to allow your OS to recognize esp32

Important

Node and npm, its package manager, are required because several development tools are used in this project. Among these tools are those that enforce the "conventional commits" standard. This standard is a lightweight convention on top of commit messages, offering an easy set of rules for creating an explicit commit history.

Tip

If platform.io extension does not recognize your board after clicking on Upload, Upload and Monitor or Monitor buttons, it means the driver was not properly setup. In MacOS, after installing the driver from Sillicon Labs, I had to restart the system before mac could identify the board.

πŸ”Œ Boot and Reset Requirements

  • 2.4Ghz WiFi signal with internet connection, in order to sync the board's clock with the NTP server.
  • SD card with config.yml and services.yml files in the root, as shown below:

βš™οΈ config.yml

# [REQUIRED] configure the credentials used to connect to a wifi network
wifi:
  # [REQUIRED] (text) wifi connection password
  password: test
  # [REQUIRED] (text) wifi id
  ssid: test

# [REQUIRED] configure authentication for the board
authentication:
  # [OPTIONAL] (number) [default 3] board is locked and requires a hard reset, after N wrong unlock attempts
  unlock_attempts: 3
  pin:
    # [OPTIONAL] (text) pin code composed of numbers only and HMAC-SHA256 hashed
    hash: 7dbd45736c57090dd62a7e1c8db1a08c353b4a836f2c6b43fd1dd3f1e747ea59
    # [OPTIONAL] (text) key used to hash pin code
    key: TUwNzIxF5lJncAJVMkmb4EiSP9vm0OyF

# [OPTIONAL] configure display settings
display:
  # [OPTIONAL] (number) [default 10] if provided, the display will turn off after n seconds have passed
  sleep_timeout: 10

# [OPTIONAL] configure touch settings
touch:
  # [OPTIONAL] (bool=false|0) calibrate touch sensor if true or 1
  calibrate: 0

# [OPTIONAL] configure the management app
manager:
  # [OPTIONAL] configure authentication for the management app. The management app is enabled only if username, password, key are set.
  authentication:
    # [REQUIRED] (text) username to start a session
    username: admin
    # [REQUIRED] (text) HMAC-SHA256 hashed password to start a session
    password: 7dbd45736c57090dd62a7e1c8db1a08c353b4a836f2c6b43fd1dd3f1e747ea59
    # [REQUIRED] (text) 32 characters key used to hash the password
    key: TUwNzIxF5lJncAJVMkmb4EiSP9vm0OyF
    # [OPTIONAL] (number) [default 5] amount of minutes for the session duration
    session_length: 5

Important

Upon the initial boot, it is imperative to undergo the calibration process at least once, as outlined in the How to build section below.

Tip

Once the boot process is finished, remove the SD card from the board, and store it somewhere safe. Before rebooting, or if you want to add new secrets, remember to put it back in the board.

Note

A pink screen appears to indicate that both config.yml and services.yml have been parsed and loaded into memory.

πŸ”’ services.yml

# [REQUIRED] (list) stores a list of services
services:
  # [REQUIRED] (text) unique name for a service in a group. It must not exceed 60 characters.
  - name: abc
    # [REQUIRED] (text) Base32 encoded secret for the service.
    secret: abc
    # [OPTIONAL] (number) [default 0] it must be an integer between 0 and 255. It is used to group services and determine their priority. Services with smaller group numbers are rendered first. For instance, services assigned to group 100 will be rendered before those in group 255. In a services file, you can have up to 100 services.
    group: 0

πŸ“– Guides

πŸ“š How to navigate and control the board

  • Change groups: Swipe left or right to navigate between different groups.
  • Lock: Tap twice on the screen to lock the board.
  • Wake up: Tap once to wake up the board.

πŸ“š How to build using PlatformIO CLI

Install PlatformIO's official CLI using this tutorial, and then follow the next steps:

  1. Run platformio device list and annotate the device port of your board.

Tip

You can discover which port belongs to your board by comparing the outputs of this command when your board is connected and when it is not.

  1. Run ./scripts/dev.sh --port ${DEVICE_PORT} --env ${ENV} to build and flash the code into your board

Important

Remember to substitue ${DEVICE_PORT} with the value you got in step 1.

Important

Remember to substitute ${ENV} by prod or dev. The only difference between both environments is the log level. In prod logs are disabled, while in dev all logs are visible.

πŸ“š How to listen to the serial port using PlatformIO CLI

To listen to the serial port using PlatformIO CLI you can use the following commnad:

platformio device monitor

πŸ“š How to register a Service

Services are registered in a file called services.yml that must be located in the root of an SD card. For example:

services:
  - name: aws:root:[email protected]
    secret: encoded-secret
    group: 0
  - name: aws:staging:[email protected]
    secret: encoded-secret
    group: 0
  - name: aws:production:[email protected]
    secret: encoded-secret
    group: 0
  - name: aws:1234565:[email protected]
    secret: encoded-secret
    group: 1
  - name: aws:6785910:[email protected]
    secret: encoded-secret
    group: 1
  - name: aws:7815795:[email protected]
    secret: encoded-secret
    group: 1
  - name: github
    secret: encoded-secret
    group: 2
  - name: docker
    secret: encoded-secret
    group: 2
  - name: npm
    secret: encoded-secret
    group: 2

Important

At present, you can register up to 100 services divided across 10 groups.

Important

The service name must not exceed 60 characters.

Important

The service group must be between 0 and 255. If you don't set a value, it will default to 0.

Important

Secrets must be stored unencrypted and encoded using Base32. All MFA services I tried already provide secrets in Base32 encoding. If you find one that does not, ensure the secret is Base32 encoded before adding it to the file.

Important

The service name acts as a unique key within a group. If two services share the same key within the same group, the last one listed in the file will be the one used because it was the last service to be added in the db.

πŸ“š How to verify if TOTP codes are correct

  1. Go to https://totp.danhersam.com/
  2. Paste/type your encoded base 32 secret in the secret field, and then compare the TOTP code shown with the one you are seeing on the ESP32's screen.

πŸ“š How to recalibrate the touch sensor

  1. Open your config.yml file.
  2. Add the following property at the root level:
touch:
  calibrate: true
  1. Insert the SD card with the updated config.yml into your board.
  2. Press the RST button on the board to reboot it.
  3. Wait for the calibration screen to appear, as shown below:

  1. Follow the on-screen instructions to complete the calibration process.
  2. Once the calibration is finished, update config.yml again:
touch:
  calibrate: false
  1. Save the file, insert the SD card back into the board, and reboot by pressing the RST button.
  2. Confirm that the calibration screen no longer appears.
  3. Your touchscreen is now calibrated and ready to use! πŸŽ‰

Note

The calibration state will initiate upon the initial boot of the board, regardless of the content stored in config.yml, if no calibration is found in SPIFFS.

Important

The pin screen won't work if you did not calibrate the touch sensor.

πŸ“š How to update config.yml from a browser

When the board is connected to your local network, a settings page, similarly to the one found in routers, can be used to update the config.yml in the SD card without the need of inserting it on a different computer. You can access this settings page at http://${LOCAL_NETWORK_DEVICE_IP}/esp32/settings.

image

Important

At the moment, all secrets in this form must be set before submiting it. If you fail to do it, all secrets in your config.yml will be overwritten by *****.

Important

For security purposes, none of the secrets are exposed by the board's webserver. If you inspect the page using your browser dev tools, you will noticed that all secrets are fetched as *****. In the future, after implementing HTTPS, you will be to manage those secrets from the browser, but only after providing a PIN number of using your fingerprint.

πŸ“š How to setup PIN number

Option 1

  1. Use this website to create a strong secret that is exactly 32 characters long. Copy the value from Encryption key 256, which has exactly 32 characters
image
  1. Open this website to hash your pin number. Add you pin number as plain text, in the first input. You must use numbers only because the UI doesn't support letters or symbols, and it must contain at least 6 digits and no more than 20 digits.

  2. Copy the generated hash. It must be 64 characters long.

image
  1. In your config.yml
  • set hash with the generated hash you got in step 3
  • set key with the secret you got in step 1
authentication:
  pin:
    hash: 7dbd45736c57090dd62a7e1c8db1a08c353b4a836f2c6b43fd1dd3f1e747ea59
    key: TUwNzIxF5lJncAJVMkmb4EiSP9vm0OyF
  unlock_attempts: 3

Option 2

  1. Use this website to create a strong secret that is exactly 32 characters long. Copy the value from Encryption key 256, which has exactly 32 characters
image
  1. Open a terminal and run the following comand to hash your pin number. Don't forget to substitute "YOUR_PIN_NUMBER" and "YOUR_32_CHARACTERS_LONG_SECRET". The PIN must consist only of digits and must be between 6 and 20 digits in length.
echo -n "YOUR_PIN_NUMBER" | openssl dgst -sha256 -hmac "YOUR_32_CHARACTERS_LONG_SECRET" | awk '{print $2}'
image
  1. Copy the generated hash. It must be 64 characters long.

  2. In your config.yml

  • set hash with the generated hash you got in step 3
  • set key with the secret you got in step 1
authentication:
  pin:
    hash: 7dbd45736c57090dd62a7e1c8db1a08c353b4a836f2c6b43fd1dd3f1e747ea59
    key: TUwNzIxF5lJncAJVMkmb4EiSP9vm0OyF
  unlock_attempts: 3

πŸ“š How to setup the Password for the Management App

The steps to generate the password for the management app are the same as those used to set up the PIN. Once the password is created, add the following properties to your config.yml:

manager:
  authentication:
    username: admin
    password: 7dbd45736c57090dd62a7e1c8db1a08c353b4a836f2c6b43fd1dd3f1e747ea59
    key: TUwNzIxF5lJncAJVMkmb4EiSP9vm0OyF

🎯 Roadmap

βœ… Display multiple TOTP codes

People often use multiple services that require MFA TOTP codes with high frequency because of their short living sessions.

βœ… Unlock with PIN Code

It is not secure to have unencrypted secrets stored without protection

βœ… Manage board settings using a browser

Users can manage their board settings using a browser. Once the board is connected to the local network, open a browser and type {IP_ADDRESS}/esp32/settings.

Important

At the moment, secrets must be re-entered before submiting the form. If you don't re-type the secrets, they will be stored as ******* in the SD card, and this will break the boot.

βœ… Display turns off automatically after N seconds without user interaction

After booting, the display turns off automatically if it doesn't receive touch events after N seconds. N is a configurable in the config.yml.

βœ… Organize Services into Groups

Services can be organized into groups.

βœ… Lock board after N wrong unlock attempts

Improve the validation function to block access to the board after few wrong attempts happened. With this enhancement, brute forcing all possible combinations won't be possible.

πŸ”œ Unlock with fingerprint

Instead of typing a pin code, it will be possible to unlock the board using a fingerprint. The goal is to ease the access to the TOTP codes, while maintaining them secure. It will also work globally or by group.

πŸ’– Become a Sponsor

If this device has made your life easier, consider supporting its development by clicking the button below.

Buy Me A Coffee