diff --git a/brmdoor_authenticator.py b/brmdoor_authenticator.py index 4d0eea3..f11d5b5 100644 --- a/brmdoor_authenticator.py +++ b/brmdoor_authenticator.py @@ -1,4 +1,10 @@ import sqlite3 +import hmac +import hashlib +import logging + +from brmdoor_nfc import NFCError + class UidRecord(object): """Represents UID<->nick pair""" @@ -61,6 +67,78 @@ class UidAuthenticator(object): self.conn.close() +class YubikeyHMACAuthenthicator(object): + """ + Uses Yubikey Neo's built-in HMAC functionality on slot 2 (needs to be + configured using Yubikey tools to be on this slot). + """ + def __init__(self, filename, nfcReader): + """ + Connects to database by given filename and later checks UIDs + against that database. + """ + #again autocommit mode + self.conn = sqlite3.connect(filename, isolation_level=None) + self.nfcReader = nfcReader + + def hmacCheck(self, key, challenge, result): + """ + Returns true iff HMAC-SHA1 with given key and challenge string + transforms into given result. + """ + hashed = hmac.new(key, challenge, hashlib.sha1) + #We should use hmac.compare_digest(), but that's in new Python + #version only. Here timing side channels are not much of concern. + return hashed.digest() == result + + def checkHMACforUID(self, uid_hex): + """ + Checks if UID is in database. If so + @param uid_hex: uid to match in hex + @returns UidRecord instance if found, None otherwise + """ + cursor = self.conn.cursor() + sql = "SELECT nick, key_hex FROM authorized_hmac_keys WHERE UPPER(uid_hex)=?" + sql_data =(uid_hex.upper(),) + + cursor.execute(sql, sql_data) + record = cursor.fetchone() + + if record is None: + return None + + nick = record[0] + secretKey = record[1].decode("hex") + + challenge = 'Sample #2' + + # Select HMAC-SHA1 on slot 2 from Yubikey + apdusHex = [ + "00 A4 04 00 07 A0 00 00 05 27 20 01", + "00 01 38 00 %02x %s" % (len(challenge), challenge.encode("hex")) + ] + + rapdu = None + + for apduHex in apdusHex: + try: + apdu = apduHex.replace(" ", "").decode("hex") + rapdu = self.nfcReader.sendAPDU(apdu) + if not rapdu.valid or rapdu.sw() != 0x9000: + raise NFCError("HMAC - response SW is not 0x9000") + except NFCError, e: + logging.debug("Yubikey HMAC command failed: %s" % e.what()) + return None + + if not self.hmacCheck(secretKey, challenge, rapdu.data()): + return None + + return UidRecord(uid_hex, nick) + + def shutdown(self): + """Closes connection to database""" + self.conn.close() + #test routine if __name__ == "__main__": authenticator = UidAuthenticator("test_uids_db.sqlite") diff --git a/brmdoor_nfc_daemon.py b/brmdoor_nfc_daemon.py index affc8e1..4fdb455 100755 --- a/brmdoor_nfc_daemon.py +++ b/brmdoor_nfc_daemon.py @@ -9,7 +9,7 @@ from binascii import hexlify from brmdoor_nfc import NFCDevice, NFCError -from brmdoor_authenticator import UidAuthenticator +from brmdoor_authenticator import UidAuthenticator, YubikeyHMACAuthenthicator import unlocker class BrmdoorConfigError(ConfigParser.Error): @@ -64,6 +64,7 @@ class NFCScanner(object): """Create worker reading UIDs from PN53x reader. """ self.authenticator = UidAuthenticator(config.authDbFilename) + self.hmacAuthenticator = None self.unknownUidTimeoutSecs = config.unknownUidTimeoutSecs self.lockOpenedSecs = config.lockOpenedSecs @@ -78,6 +79,9 @@ class NFCScanner(object): authorized. """ self.nfc = NFCDevice() + self.hmacAuthenticator = YubikeyHMACAuthenthicator( + config.authDbFilename, self.nfc + ) #self.nfc.pollNr = 0xFF #poll indefinitely while True: try: @@ -106,14 +110,23 @@ class NFCScanner(object): """ record = self.authenticator.fetchUidRecord(uid_hex) - #no match - if record is None: - logging.info("Unknown UID %s", uid_hex) - time.sleep(self.unknownUidTimeoutSecs) + #direct UID match + if record is not None: + logging.info("Unlocking for UID %s", record) + self.unlocker.unlock() return - logging.info("Unlocking for UID %s", record) - self.unlocker.unlock() + #test for Yubikey HMAC auth + record = self.hmacAuthenticator.checkHMACforUID(uid_hex) + + if record is not None: + logging.info("Unlocking after HMAC for UID %s", record) + self.unlocker.unlock() + return + + logging.info("Unknown UID %s", uid_hex) + time.sleep(self.unknownUidTimeoutSecs) + if __name__ == "__main__": diff --git a/create_authenticator_db.py b/create_authenticator_db.py index 726f6c1..c54136b 100755 --- a/create_authenticator_db.py +++ b/create_authenticator_db.py @@ -17,6 +17,16 @@ if __name__ == "__main__": conn = sqlite3.connect(filename) cursor = conn.cursor() - cursor.execute("CREATE TABLE authorized_uids(id INTEGER PRIMARY KEY AUTOINCREMENT, uid_hex TEXT, nick TEXT)") + cursor.execute("""CREATE TABLE authorized_uids( + id INTEGER PRIMARY KEY AUTOINCREMENT, + uid_hex TEXT, + nick TEXT) + """) + cursor.execute("""CREATE TABLE authorized_hmac_keys( + id INTEGER PRIMARY KEY AUTOINCREMENT, + uid_hex TEXT, + key_hex TEXT, + nick TEXT) + """) conn.commit() conn.close() diff --git a/test_uids_db.sqlite b/test_uids_db.sqlite index be6bbe4..b79d94d 100644 Binary files a/test_uids_db.sqlite and b/test_uids_db.sqlite differ