import os import sqlite3 import hmac import hashlib import logging import json import axolotl_curve25519 as curve from nfc_smartcard import NFCError class UidRecord(object): """Represents UID<->nick pair""" def __init__(self, uid_hex, nick): """ Create instance binding UID to nick. UIDs should be either 4, 7 or 10 bytes long, but that's ISO14443 thing - this object has no such limitation. UID will be stored in uppercase hex, converted if necessary. @param uid_hex: uid in hex @param nick: nickname this UID belongs to """ self.uid_hex = uid_hex.upper() self.nick = nick def __str__(self): return "(uid: %s, nick: %s)" % (self.uid_hex, self.nick) def __repr__(self): return "" % \ (repr(self.uid_hex), repr(self.nick)) class UidAuthenticator(object): """Checks UIDs of ISO14443 RFID cards against database.""" def __init__(self, filename): """ Connects to database by given filename and later checks UIDs against that database. """ #open in autocommit mode - we are not changing anything self.conn = sqlite3.connect(filename, isolation_level=None) def fetchUidRecord(self, uid_hex): """ Returns first record that matches given UID or None if nothing is found. @param uid_hex: uid to match in hex @returns UidRecord instance if found, None otherwise """ cursor = self.conn.cursor() sql = "SELECT nick FROM authorized_uids 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] return UidRecord(uid_hex, nick) def shutdown(self): """Closes connection to database""" 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 = os.urandom(32) # 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.info("Yubikey HMAC command failed: %s" % e.what()) return None if not self.hmacCheck(secretKey, challenge, rapdu.data()): logging.info("HMAC check failed for UID %s", uid_hex) return None return UidRecord(uid_hex, nick) def shutdown(self): """Closes connection to database""" self.conn.close() class DesfireEd25519Authenthicator(object): """ Reads NDEF message from Desfire and it must be signed binary value of UID """ def __init__(self, filename, nfcReader, pubKey): """ Connects to database by given filename and later checks UIDs using the pubkey (given as binary string). """ #again autocommit mode self.conn = sqlite3.connect(filename, isolation_level=None) self.nfcReader = nfcReader self.pubKey = pubKey def signatureCheck(self, uid, signature): """ Returns true iff uid (as binary) is the message signed by signature (binary string) """ verified = curve.verifySignature(self.pubKey, uid, signature) == 0 return verified def checkUIDSignature(self, uid_hex): """ Checks if UID is in database. If so, it retrieves NDEF which should be signature of the UID @param uid_hex: uid to match in hex @returns UidRecord instance if found, None otherwise """ cursor = self.conn.cursor() sql = "SELECT nick FROM authorized_desfires 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] try: ndefJson = json.loads(self.nfcReader.readDesfireNDEF()) ndefSignature = ndefJson["brmdoorSignature"].decode("hex") if len(ndefSignature) != 64: logging.error("NDEF signature has wrong length") return None if self.signatureCheck(uid_hex.decode("hex"), ndefSignature): return UidRecord(uid_hex, nick) else: logging.info("Signature check failed for Desfire NDEF for UID %s", uid_hex) return None except NFCError, e: logging.error("Desfire read NDEF failed: %s", e.what()) return None except TypeError, e: logging.error("Could not decode signature from JSON: %s", e) return None except ValueError, e: logging.error("Could not decode JSON from NDEF: %s", e) return None except KeyError, e: logging.error("Missing signature in JSON: %s", e) return None def shutdown(self): """Closes connection to database""" self.conn.close() #test routine if __name__ == "__main__": authenticator = UidAuthenticator("test_uids_db.sqlite") record = authenticator.fetchUidRecord("043a1482cc2280") print "For UID 043a1482cc2280 we found:", repr(record) record = authenticator.fetchUidRecord("34795fad") print "For UID 34795fad we found:", repr(record) record = authenticator.fetchUidRecord("01020304") print "For UID 01020304 we found:", repr(record) authenticator.shutdown()