├── .gitattributes ├── README.md ├── door_lock.sql └── lock.py /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Raspberry Pi Smart Door Lock 2 | This award-winning project combines a Raspberry Pi, a MySQL database, an RFID reader, an LCD touchscreen, a relay switch, an electronic door strike and a Twilio SMS account to create the "ultimate" Raspberry Pi smart door lock security system - with "three factor" authentication. 3 | 4 | It's not quite true three-factor authentication, as two of the pieces are "something you have"... but it's still 3 hoops to jump through to gain access. 5 | 6 | In this repo you will find the Python code required as well as the MySQL database table setup. 7 | 8 | Why on earth would you want to require an RFID fob, followed by a PIN entered on the touchscreen, followed by yet another one-time code sent to you via SMS? Well, mainly for the fun of building it! There are also many applications where this might be useful for either an external or an internal door, or even on a cupboard or safe. Just make sure you build in some fail-safe overrides if you use this in the real-world! 9 | 10 | The video below shows a demonstration of the finished system and an in-depth tutorial of how to build and configure it. I hope you enjoy it, and please give the video a thumbs-up and subscribe to the YouTube channel. 11 | 12 | [![WATCH THE VIDEO](https://img.youtube.com/vi/TX_WQMYc0SU/mqdefault.jpg)](https://www.youtube.com/watch?v=TX_WQMYc0SU) 13 | 14 | You can also read an in-depth text tutorial on the Switched On Network website. 15 | -------------------------------------------------------------------------------- /door_lock.sql: -------------------------------------------------------------------------------- 1 | -- phpMyAdmin SQL Dump 2 | -- version 4.2.12deb2+deb8u2 3 | -- http://www.phpmyadmin.net 4 | -- 5 | -- Host: localhost 6 | -- Generation Time: Sep 28, 2017 at 03:22 PM 7 | -- Server version: 5.5.55-0+deb8u1 8 | -- PHP Version: 5.6.30-0+deb8u1 9 | 10 | SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO"; 11 | SET time_zone = "+00:00"; 12 | 13 | 14 | /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; 15 | /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; 16 | /*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; 17 | /*!40101 SET NAMES utf8 */; 18 | 19 | -- 20 | -- Database: `door_lock` 21 | -- 22 | 23 | -- -------------------------------------------------------- 24 | 25 | -- 26 | -- Table structure for table `access_list` 27 | -- 28 | 29 | CREATE TABLE IF NOT EXISTS `access_list` ( 30 | `user_id` int(11) NOT NULL, 31 | `name` varchar(100) NOT NULL, 32 | `image` varchar(50) DEFAULT NULL, 33 | `rfid_code` varchar(20) NOT NULL, 34 | `pin` char(6) NOT NULL, 35 | `sms_number` varchar(15) NOT NULL 36 | ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=latin1; 37 | 38 | -- -------------------------------------------------------- 39 | 40 | -- 41 | -- Table structure for table `access_log` 42 | -- 43 | 44 | CREATE TABLE IF NOT EXISTS `access_log` ( 45 | `access_id` int(11) NOT NULL, 46 | `rfid_presented` varchar(20) DEFAULT NULL, 47 | `rfid_presented_datetime` datetime DEFAULT NULL, 48 | `rfid_granted` tinyint(1) DEFAULT NULL, 49 | `pin_entered` char(6) DEFAULT NULL, 50 | `pin_entered_datetime` datetime DEFAULT NULL, 51 | `pin_granted` tinyint(1) DEFAULT NULL, 52 | `mobile_number` varchar(15) DEFAULT NULL, 53 | `smscode_entered` char(6) DEFAULT NULL, 54 | `smscode_entered_datetime` datetime DEFAULT NULL, 55 | `smscode_granted` tinyint(1) DEFAULT NULL 56 | ) ENGINE=InnoDB AUTO_INCREMENT=116 DEFAULT CHARSET=latin1; 57 | 58 | -- -------------------------------------------------------- 59 | 60 | -- 61 | -- Table structure for table `twilio_api_credentials` 62 | -- 63 | 64 | CREATE TABLE IF NOT EXISTS `twilio_api_credentials` ( 65 | `id` int(11) NOT NULL, 66 | `account_sid` varchar(50) NOT NULL, 67 | `auth_token` varchar(50) NOT NULL, 68 | `twilio_sms_number` varchar(15) NOT NULL 69 | ) ENGINE=InnoDB DEFAULT CHARSET=latin1; 70 | 71 | -- 72 | -- Indexes for dumped tables 73 | -- 74 | 75 | -- 76 | -- Indexes for table `access_list` 77 | -- 78 | ALTER TABLE `access_list` 79 | ADD PRIMARY KEY (`user_id`), ADD UNIQUE KEY `rfid_code` (`rfid_code`), ADD UNIQUE KEY `image` (`image`); 80 | 81 | -- 82 | -- Indexes for table `access_log` 83 | -- 84 | ALTER TABLE `access_log` 85 | ADD PRIMARY KEY (`access_id`); 86 | 87 | -- 88 | -- Indexes for table `twilio_api_credentials` 89 | -- 90 | ALTER TABLE `twilio_api_credentials` 91 | ADD PRIMARY KEY (`id`); 92 | 93 | -- 94 | -- AUTO_INCREMENT for dumped tables 95 | -- 96 | 97 | -- 98 | -- AUTO_INCREMENT for table `access_list` 99 | -- 100 | ALTER TABLE `access_list` 101 | MODIFY `user_id` int(11) NOT NULL AUTO_INCREMENT,AUTO_INCREMENT=3; 102 | -- 103 | -- AUTO_INCREMENT for table `access_log` 104 | -- 105 | ALTER TABLE `access_log` 106 | MODIFY `access_id` int(11) NOT NULL AUTO_INCREMENT,AUTO_INCREMENT=116; 107 | /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; 108 | /*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; 109 | /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; 110 | -------------------------------------------------------------------------------- /lock.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import sys 3 | import MySQLdb 4 | from threading import Thread 5 | import threading 6 | import time 7 | import RPi.GPIO as GPIO 8 | import json 9 | from random import randint 10 | from evdev import InputDevice 11 | from select import select 12 | from twilio.rest import Client 13 | 14 | GPIO.setmode(GPIO.BCM) 15 | GPIO.setwarnings(False) 16 | GPIO.setup(13,GPIO.OUT) 17 | 18 | try: 19 | # python 2 20 | import Tkinter as tk 21 | import ttk 22 | except ImportError: 23 | # python 3 24 | import tkinter as tk 25 | from tkinter import ttk 26 | 27 | class Fullscreen_Window: 28 | 29 | global dbHost 30 | global dbName 31 | global dbUser 32 | global dbPass 33 | 34 | dbHost = 'localhost' 35 | dbName = 'DB_NAME' 36 | dbUser = 'USER' 37 | dbPass = 'PASSWORD' 38 | 39 | def __init__(self): 40 | self.tk = tk.Tk() 41 | self.tk.title("Three-Factor Authentication Security Door Lock") 42 | self.frame = tk.Frame(self.tk) 43 | self.frame.grid() 44 | self.tk.columnconfigure(0, weight=1) 45 | 46 | self.tk.attributes('-zoomed', True) 47 | self.tk.attributes('-fullscreen', True) 48 | self.state = True 49 | self.tk.bind("", self.toggle_fullscreen) 50 | self.tk.bind("", self.end_fullscreen) 51 | self.tk.config(cursor="none") 52 | 53 | self.show_idle() 54 | 55 | t = Thread(target=self.listen_rfid) 56 | t.daemon = True 57 | t.start() 58 | 59 | def show_idle(self): 60 | self.welcomeLabel = ttk.Label(self.tk, text="Please Present\nYour Token") 61 | self.welcomeLabel.config(font='size, 20', justify='center', anchor='center') 62 | self.welcomeLabel.grid(sticky=tk.W+tk.E, pady=210) 63 | 64 | def pin_entry_forget(self): 65 | self.validUser.grid_forget() 66 | self.photoLabel.grid_forget() 67 | self.enterPINlabel.grid_forget() 68 | count = 0 69 | while (count < 12): 70 | self.btn[count].grid_forget() 71 | count += 1 72 | 73 | def returnToIdle_fromPINentry(self): 74 | self.pin_entry_forget() 75 | self.show_idle() 76 | 77 | def returnToIdle_fromPINentered(self): 78 | self.PINresultLabel.grid_forget() 79 | self.show_idle() 80 | 81 | def returnToIdle_fromAccessGranted(self): 82 | GPIO.output(13,GPIO.LOW) 83 | self.SMSresultLabel.grid_forget() 84 | self.show_idle() 85 | 86 | def returnToIdle_fromSMSentry(self): 87 | self.PINresultLabel.grid_forget() 88 | self.smsDigitsLabel.grid_forget() 89 | count = 0 90 | while (count < 12): 91 | self.btn[count].grid_forget() 92 | count += 1 93 | self.show_idle() 94 | 95 | def returnToIdle_fromSMSentered(self): 96 | self.SMSresultLabel.grid_forget() 97 | self.show_idle() 98 | 99 | def toggle_fullscreen(self, event=None): 100 | self.state = not self.state # Just toggling the boolean 101 | self.tk.attributes("-fullscreen", self.state) 102 | return "break" 103 | 104 | def end_fullscreen(self, event=None): 105 | self.state = False 106 | self.tk.attributes("-fullscreen", False) 107 | return "break" 108 | 109 | def listen_rfid(self): 110 | global pin 111 | global accessLogId 112 | 113 | keys = "X^1234567890XXXXqwertzuiopXXXXasdfghjklXXXXXyxcvbnmXXXXXXXXXXXXXXXXXXXXXXX" 114 | dev = InputDevice('/dev/input/event0') 115 | rfid_presented = "" 116 | 117 | while True: 118 | r,w,x = select([dev], [], []) 119 | for event in dev.read(): 120 | if event.type==1 and event.value==1: 121 | if event.code==28: 122 | dbConnection = MySQLdb.connect(host=dbHost, user=dbUser, passwd=dbPass, db=dbName) 123 | cur = dbConnection.cursor(MySQLdb.cursors.DictCursor) 124 | cur.execute("SELECT * FROM access_list WHERE rfid_code = '%s'" % (rfid_presented)) 125 | 126 | if cur.rowcount != 1: 127 | self.welcomeLabel.config(text="ACCESS DENIED") 128 | 129 | # Log access attempt 130 | cur.execute("INSERT INTO access_log SET rfid_presented = '%s', rfid_presented_datetime = NOW(), rfid_granted = 0" % (rfid_presented)) 131 | dbConnection.commit() 132 | 133 | time.sleep(3) 134 | self.welcomeLabel.grid_forget() 135 | self.show_idle() 136 | else: 137 | user_info = cur.fetchone() 138 | userPin = user_info['pin'] 139 | self.welcomeLabel.grid_forget() 140 | self.validUser = ttk.Label(self.tk, text="Welcome\n %s!" % (user_info['name']), font='size, 15', justify='center', anchor='center') 141 | self.validUser.grid(columnspan=3, sticky=tk.W+tk.E) 142 | 143 | self.image = tk.PhotoImage(file=user_info['image'] + ".gif") 144 | self.photoLabel = ttk.Label(self.tk, image=self.image) 145 | self.photoLabel.grid(columnspan=3) 146 | 147 | self.enterPINlabel = ttk.Label(self.tk, text="Please enter your PIN:", font='size, 18', justify='center', anchor='center') 148 | self.enterPINlabel.grid(columnspan=3, sticky=tk.W+tk.E) 149 | pin = '' 150 | 151 | keypad = [ 152 | '1', '2', '3', 153 | '4', '5', '6', 154 | '7', '8', '9', 155 | '*', '0', '#', 156 | ] 157 | 158 | # create and position all buttons with a for-loop 159 | # r, c used for row, column grid values 160 | r = 4 161 | c = 0 162 | n = 0 163 | # list(range()) needed for Python3 164 | self.btn = list(range(len(keypad))) 165 | for label in keypad: 166 | # partial takes care of function and argument 167 | #cmd = partial(click, label) 168 | # create the button 169 | self.btn[n] = tk.Button(self.tk, text=label, font='size, 18', width=4, height=1, command=lambda digitPressed=label:self.codeInput(digitPressed, userPin, user_info['sms_number'])) 170 | # position the button 171 | self.btn[n].grid(row=r, column=c, ipadx=10, ipady=10) 172 | # increment button index 173 | n += 1 174 | # update row/column position 175 | c += 1 176 | if c > 2: 177 | c = 0 178 | r += 1 179 | 180 | 181 | # Log access attempt 182 | cur.execute("INSERT INTO access_log SET rfid_presented = '%s', rfid_presented_datetime = NOW(), rfid_granted = 1" % (rfid_presented)) 183 | dbConnection.commit() 184 | accessLogId = cur.lastrowid 185 | 186 | self.PINentrytimeout = threading.Timer(10, self.returnToIdle_fromPINentry) 187 | self.PINentrytimeout.start() 188 | 189 | self.PINenteredtimeout = threading.Timer(5, self.returnToIdle_fromPINentered) 190 | 191 | rfid_presented = "" 192 | dbConnection.close() 193 | else: 194 | rfid_presented += keys[ event.code ] 195 | 196 | def codeInput(self, value, userPin, mobileNumber): 197 | global accessLogId 198 | global pin 199 | global smsCodeEntered 200 | pin += value 201 | pinLength = len(pin) 202 | 203 | self.enterPINlabel.config(text="Digits Entered: %d" % pinLength) 204 | 205 | if pinLength == 6: 206 | self.PINentrytimeout.cancel() 207 | self.pin_entry_forget() 208 | 209 | if pin == userPin: 210 | pin_granted = 1 211 | else: 212 | pin_granted = 0 213 | 214 | # Log access attempt 215 | dbConnection = MySQLdb.connect(host=dbHost, user=dbUser, passwd=dbPass, db=dbName) 216 | cur = dbConnection.cursor() 217 | cur.execute("UPDATE access_log SET pin_entered = '%s', pin_entered_datetime = NOW(), pin_granted = %s, mobile_number = '%s' WHERE access_id = %s" % (pin, pin_granted, mobileNumber, accessLogId)) 218 | dbConnection.commit() 219 | 220 | if pin == userPin: 221 | self.PINresultLabel = ttk.Label(self.tk, text="Thank You, Now\nPlease Enter Code\nfrom SMS\n") 222 | self.PINresultLabel.config(font='size, 20', justify='center', anchor='center') 223 | self.PINresultLabel.grid(columnspan=3, sticky=tk.W+tk.E, pady=20) 224 | 225 | self.smsDigitsLabel = ttk.Label(self.tk, text="Digits Entered: 0", font='size, 18', justify='center', anchor='center') 226 | self.smsDigitsLabel.grid(columnspan=3, sticky=tk.W+tk.E) 227 | 228 | smsCode = self.sendSMScode(mobileNumber) 229 | smsCodeEntered = '' 230 | 231 | keypad = [ 232 | '1', '2', '3', 233 | '4', '5', '6', 234 | '7', '8', '9', 235 | '', '0', '', 236 | ] 237 | 238 | # create and position all buttons with a for-loop 239 | # r, c used for row, column grid values 240 | r = 4 241 | c = 0 242 | n = 0 243 | # list(range()) needed for Python3 244 | self.btn = list(range(len(keypad))) 245 | for label in keypad: 246 | # partial takes care of function and argument 247 | #cmd = partial(click, label) 248 | # create the button 249 | self.btn[n] = tk.Button(self.tk, text=label, font='size, 18', width=4, height=1, command=lambda digitPressed=label:self.smsCodeEnteredInput(digitPressed, smsCode)) 250 | # position the button 251 | self.btn[n].grid(row=r, column=c, ipadx=10, ipady=10) 252 | # increment button index 253 | n += 1 254 | # update row/column position 255 | c += 1 256 | if c > 2: 257 | c = 0 258 | r += 1 259 | 260 | self.SMSentrytimeout = threading.Timer(60, self.returnToIdle_fromSMSentry) 261 | self.SMSentrytimeout.start() 262 | 263 | else: 264 | self.PINresultLabel = ttk.Label(self.tk, text="Incorrect PIN\nEntered!") 265 | self.PINresultLabel.config(font='size, 20', justify='center', anchor='center') 266 | self.PINresultLabel.grid(sticky=tk.W+tk.E, pady=210) 267 | self.PINenteredtimeout.start() 268 | 269 | def smsCodeEnteredInput(self, value, smsCode): 270 | global smsCodeEntered 271 | global accessLogId 272 | smsCodeEntered += value 273 | smsCodeEnteredLength = len(smsCodeEntered) 274 | 275 | self.smsDigitsLabel.config(text="Digits Entered: %d" % smsCodeEnteredLength) 276 | 277 | if smsCodeEnteredLength == 6: 278 | self.SMSentrytimeout.cancel() 279 | self.pin_entry_forget() 280 | 281 | if smsCodeEntered == smsCode: 282 | smscode_granted = 1 283 | else: 284 | smscode_granted = 0 285 | 286 | # Log access attempt 287 | dbConnection = MySQLdb.connect(host=dbHost, user=dbUser, passwd=dbPass, db=dbName) 288 | cur = dbConnection.cursor() 289 | cur.execute("UPDATE access_log SET smscode_entered = '%s', smscode_entered_datetime = NOW(), smscode_granted = %s WHERE access_id = %s" % (smsCodeEntered, smscode_granted, accessLogId)) 290 | dbConnection.commit() 291 | 292 | if smsCodeEntered == smsCode: 293 | self.SMSresultLabel = ttk.Label(self.tk, text="Thank You,\nAccess Granted") 294 | self.SMSresultLabel.config(font='size, 20', justify='center', anchor='center') 295 | self.SMSresultLabel.grid(columnspan=3, sticky=tk.W+tk.E, pady=210) 296 | 297 | self.PINresultLabel.grid_forget() 298 | self.smsDigitsLabel.grid_forget() 299 | GPIO.output(13,GPIO.HIGH) 300 | 301 | self.doorOpenTimeout = threading.Timer(10, self.returnToIdle_fromAccessGranted) 302 | self.doorOpenTimeout.start() 303 | else: 304 | self.PINresultLabel.grid_forget() 305 | self.smsDigitsLabel.grid_forget() 306 | 307 | self.SMSresultLabel = ttk.Label(self.tk, text="Incorrect SMS\nCode Entered!") 308 | self.SMSresultLabel.config(font='size, 20', justify='center', anchor='center') 309 | self.SMSresultLabel.grid(sticky=tk.W+tk.E, pady=210) 310 | 311 | self.SMSenteredtimeout = threading.Timer(10, self.returnToIdle_fromSMSentered) 312 | self.SMSenteredtimeout.start() 313 | 314 | def sendSMScode(self, mobileNumber): 315 | 316 | # Retreive our Twilio access credentials and "from" number 317 | dbConnection = MySQLdb.connect(host=dbHost, user=dbUser, passwd=dbPass, db=dbName) 318 | cur = dbConnection.cursor(MySQLdb.cursors.DictCursor) 319 | cur.execute("SELECT account_sid, auth_token, twilio_sms_number FROM twilio_api_credentials WHERE id = 1") 320 | credentials = cur.fetchone() 321 | account_sid = credentials['account_sid'] 322 | auth_token = credentials['auth_token'] 323 | twilio_sms_number = credentials['twilio_sms_number'] 324 | dbConnection.close() 325 | 326 | smsCode = str(randint(100000, 999999)) 327 | messageText = "Your access code is %s. Please enter this on the touchscreen to continue." % smsCode 328 | 329 | client = Client(account_sid, auth_token) 330 | message = client.messages.create( 331 | to=mobileNumber, 332 | from_=twilio_sms_number, 333 | body=messageText) 334 | 335 | return smsCode 336 | 337 | if __name__ == '__main__': 338 | w = Fullscreen_Window() 339 | w.tk.mainloop() --------------------------------------------------------------------------------