Introduction: Time-Based One-Time Password (TOTP) Smart Safe
In this project I will go over the steps for building a Time-based One-time passwords (TOTP) Smart Safe. If you are not familiar with TOTP here is a good article that describes What is a Time-based One-time Password (TOTP)? | Twilio.
In a nutshell, its a method for calculating a 6 digit passcode given a pre-determined key based on the current date/time. This means that as long as the Safe can keep track of time, I will be able to use my Authenticator app to get a new passcode every 30 seconds, with which I will be able to open the safe.
Update: Take this a step further and add a fingerprint sensor: Fingerprint Safe : 5 Steps - Instructables.
Supplies
- D1 Mini (ESP8266)
- Relay module
- Piezo buzzer
- Power supply (5v)
- Prototype PCB board
Step 1: Background
I found this old safe that died due to a leaking battery. It just needed some cleanup and re-soldering of the connectors to make it work again. There is a button inside that lets you reset the pin, so overall it was operational.
When I was about to place it somewhere for actual use, I started to think what would be a good pin code to use, one that I will remember for years to come and that isn't one of the "regular" pins we use at the house, so that my kids wouldn't be able to figure it out. That made me think about using an Authenticator-based time-based one-time passwords (TOTP).
Step 2: How the Safe Works
The way this safe (and I assume many other) work is using a large lock metal bar, that can slide in and out. Once out (locked), the spring on the edge of the solenoid jumps out and prevents the lock from going back in, hence the safe cannot be unlocked.
To unlock it again, you can either use the key (in the middle, not very visible in these photos), or if the solenoid will pull itself back. This of course happens when there is current running through it. The control board manages that part. It will fire the solenoid when the correct pin is entered.
The pin is entered using the keypad on the other side of the door. The green-ish ribbon is the connection between the keypad and the control board.
Step 3: The Plan
The plan is to replace the control board with a D1 Mini that will control a relay. The relay will close a circuit between the power supply and the solenoid for a few seconds, thus opening the safe, when the correct pin is entered. We'll need to also connect the original keypad to the D1 Mini for input, and also to add a buzzer for feedback.
The D1 Mini will need to be connected to the WiFi so that it'll be able to sync the clock. This is required for the TOTP to work. We'll need to write some simple code that accepts input from the keypad and after 6 digits will compare it to the current TOTP. If it matches, it will close the relay for a few seconds to allow the safe to open.
In the pictures you can see the final setup.
- I used a relay module, but a bare relay can be used as well, with the needed adjustments to the board.
- I also used some connectors that I had, for power, for the solenoid and for the keyboard (harvested that one from the original board).
- I used a 5v 2A power supply, but that's probably an overkill for this solenoid. You can probably get away with something smaller that can be placed outside the box. Just make sure not to expect the D1 Mini to drive the solenoid directly.
Step 4: Decoding the Keypad
The most difficult part was to figure out how the keypad works.
In general, 3x4 keypads connect using 7 wires. 1 for each row and 1 for each column. The rows are pulled high and the columns are low, and when a key is pressed it closes the circuit and that's how the controller can figure out which key was pressed. Here is a good article that explains this: How to Set Up a Keypad on an Arduino - Circuit Basics.
So I had to figure out the order of the wires on the ribbon, and associate each with the right row / column. This required some trial and error. I used a voltmeter in diode mode to check each pair while pressing the various keys until I saw the voltmeter indicating the circuit is closed.
While I was expecting the wires to be ordered (i.e. wire 1-4 for rows and wire 5-7 for columns), it turned out that the correct order for my keypad is:
Wire 1 - Column 1
Wire 2 - Row 1
Wire 3 - Row 2
Wire 4 - Column 2
Wire 5 - Row 3
Wire 6 - Column 3
Wire 7 - Row 4
Each wire from the keypad connects to a different pin in the D1 Mini. I used the following mapping, but you can obviously connect it differently. You will need to refer to this later in the code.
const byte ROWS = 4; const byte COLS = 3; char hexaKeys[ROWS][COLS] = { {'1', '2', '3'}, {'4', '5', '6'}, {'7', '8', '9'}, {'A', '0', 'B'} }; byte rowPins[ROWS] = {TX, RX, D2, D4}; byte colPins[COLS] = {D5, D1, D3};
Note that I used TX and RX as regular GPIO because I originally wanted to also connect LEDs (and the D1 Mini doesn't have enough pins). This obviously prevents you from using the Serial for logging and debugging.
To make this happen, you will need to add the following lines to Setup()
void setup() { pinMode(TX, FUNCTION_3); pinMode(RX, FUNCTION_3); }
Once done, the following code controls the Keypad, accepts input from the user, compares to the secret (to be implemented later) and opens the safe if equals, otherwise (and also on 5 seconds of idle) resets the input buffer.
#include <Keypad.h> const byte ROWS = 4; const byte COLS = 3; char hexaKeys[ROWS][COLS] = { {'1', '2', '3'}, {'4', '5', '6'}, {'7', '8', '9'}, {'A', '0', 'B'} }; byte rowPins[ROWS] = {TX, RX, D2, D4}; byte colPins[COLS] = {D5, D1, D3}; unsigned long lastClickMillis = 0; char code[6]; int codeIndex = 0; Keypad customKeypad = Keypad(makeKeymap(hexaKeys), rowPins, colPins, ROWS, COLS); void setup() { pinMode(TX, FUNCTION_3); pinMode(RX, FUNCTION_3); } void loop() { char customKey = customKeypad.getKey(); if (customKey) { code[codeIndex] = customKey; codeIndex = codeIndex + 1; if (codeIndex == 6) { if (strcmp(code, $secret$) == 0) { // openSafe(); } else { // incorrectPin(); } codeIndex = 0; memset(code, 0, 8); } } if (codeIndex != 0 && (millis() - lastClickMillis > 5000)) { // incorrectPin(); codeIndex = 0; memset(code, 0, 8); } }
Step 5: Connecting the Hardware
I connected everything on a prototype board, and drilled some holes to match the ones on the original board. This allowed me to use the same mounting screws that the original board had. I messed up the measurement a little, and it ended up a little croocked.
In the center, I placed headers for the D1 Mini. This gives plenty of space to connect the rest of the wires to the various pins.
On the top right I used a regular screw connector (POW) for power. This connects directly to the +/- from the power supply.
Below that I used the same connector that is used on the original board for connecting the solenoid (SOL). This wire gets the negative connection from the above power connector (POW), and the positive goes from the upper connector into the Relay's NO connection and back to the solenoid connector's positive connector.
The Relay module connects to the 5V and GND, and it's signal pin is connected to the D0 pin in the D1 Mini.
The buzzer is connected to GND and to the D7 pin in the D1 Mini.
Finally, the 7 pins of the Keypad are connected to D5, TX, RX and D1-D4.
Step 6: The Pin Code
In Step #3 we have the code to get input from the Keypad and construct that into a 6 digit pin. Now we need to compare it to the current pin code as calculated based on the current time and initial secret key. For this I used two libraries. ezTime (ropg/ezTime: ezTime (github.com)) is the simplest one I know for getting a micro controller's clock synced. As I mentioned before, this is required for the calculation of the one-time password. The second is a simple implementation of TOTP (lucadentella/TOTP-Arduino (github.com)).
The following code was cleaned up for brevity.
#include <TOTP.h> #include <ezTime.h> uint8_t hmacKey[] = {0x4D, 0x79, 0x20, 0x73, 0x61, 0x66, 0x65}; TOTP totp = TOTP(hmacKey, 7); void setup() { waitForSync(); } void loop() { char customKey = customKeypad.getKey(); if (customKey) { code[codeIndex] = customKey; codeIndex = codeIndex + 1; if (codeIndex == 6) { char* tot = totp.getCode(UTC.now()); if (strcmp(code, tot) == 0) { // openSafe(); } else { // incorrectPin(); } codeIndex = 0; memset(code, 0, 8); } } }
The code above defines a key (seed) for the TOTP. The key is the string "My safe" in hex. You can use any online Text to Hex converter, such as: Text to Hex Converter - Online Toolz (online-toolz.com).
uint8_t hmacKey[] = {0x4D, 0x79, 0x20, 0x73, 0x61, 0x66, 0x65};
With this key in place, our TOTP implementation will be able to calculate a new 6 digit key every 30 seconds.
Next we need to create an entry in our favorite Authenticator app. I use Microsoft's Authenticator, but this works just as well in Google's. Both apps will require the BASE32 representation of the key ("My safe" in my case). You can use any online Base32 Encoder, for example: Base32 Encode Online (emn178.github.io).
The base32 encoded string (e.g. "My safe" -> "JV4SA43BMZSQ") is the Key to be used for the Authenticator entry. You can enter this manually in your Authenticator app, or you can use a QR. A simple online tool for creating QR codes that Authenticator apps understand is Generate QR Codes for Google Authenticator (hersam.com).
Alternatively, just for testing, you can use TOTP Generator (danhersam.com). You can manually enter the key (again, in base32 encoded string), and it will prompt you for the 6 digit one-time password, which the safe will accept in the 30 second time window.
The complete code is available in the attached file below, and also available at GitHub adi-miller/totp-safe.
Attachments
Step 7: Be Careful
Needless to say, this Safe isn't very safe. There are many potential failure points here, besides the actual safe. If the D1 Mini dies, you will not be able to operate this at all. In addition, if it looses internet connectivity it will not be able to sync it's clock, and then the passcode will be incorrect. Bottom line - be careful.
Thanks for reading. Happy to hear what you think and if you are trying to build this or something similar. Please leave comments below, or follow me on Twitter @adi_miller.