Add MIFARE interface support, including LDAP layer.
authorDavid Adam <[email protected]>
Fri, 22 Feb 2008 15:10:33 +0000 (15:10 +0000)
committerDavid Adam <[email protected]>
Fri, 22 Feb 2008 15:10:33 +0000 (15:10 +0000)
All enquiries to <[email protected]>.

sql-edition/servers/LDAPConnector.py [new file with mode: 0644]
sql-edition/servers/MIFAREClient.py [new file with mode: 0644]
sql-edition/servers/MIFAREDriver.py [new file with mode: 0644]

diff --git a/sql-edition/servers/LDAPConnector.py b/sql-edition/servers/LDAPConnector.py
new file mode 100644 (file)
index 0000000..1f35c20
--- /dev/null
@@ -0,0 +1,74 @@
+#!/usr/bin/env python2.4
+
+import ldap
+import ldap.filter
+
+LDAP_TIMEOUT = 10
+
+def get_ldap_connection():
+        ldap.set_option(ldap.OPT_X_TLS_CACERTFILE, '/etc/ssl/UCC-CA.crt')
+        ldap.set_option(ldap.OPT_X_TLS,1)
+        ldap.set_option(ldap.OPT_X_TLS_ALLOW,1)
+        #ldap.set_option(ldap.OPT_DEBUG_LEVEL,255)
+        conn = ldap.initialize('ldaps://mussel.ucc.gu.uwa.edu.au:636/')
+        
+        binddn = 'cn=admin,dc=ucc,dc=gu,dc=uwa,dc=edu,dc=au'
+        passfile = open('/etc/pam_ldap.secret')
+        password = passfile.readline().strip()
+        passfile.close()
+
+        conn.simple_bind_s(binddn, password)
+        return conn
+
+def get_uid(card_id):
+        ldapconn = get_ldap_connection()
+        
+        basedn = 'ou=People,dc=ucc,dc=gu,dc=uwa,dc=edu,dc=au'
+        filter = ldap.filter.filter_format('(uccDispenseMIFARE=%s)', (card_id, ))
+        attrs = ('uidNumber',)
+        
+        results = ldapconn.search_st(basedn, ldap.SCOPE_SUBTREE, filter, attrs, timeout=LDAP_TIMEOUT)
+        
+        ldapconn.unbind()
+        
+        if len(results) != 1:
+                raise ValueError, "no UID found for card ID"
+        
+        return results[0][1]['uidNumber'][0]
+
+def set_card_id(uidNumber, card_id):
+        ldapconn = get_ldap_connection()
+        
+        basedn = 'ou=People,dc=ucc,dc=gu,dc=uwa,dc=edu,dc=au'
+        filter = ldap.filter.filter_format('(uidNumber=%s)', (uidNumber, ))
+        attrs = ('objectClass', )
+        
+        results = ldapconn.search_st(basedn, ldap.SCOPE_SUBTREE, filter, attrs, timeout=LDAP_TIMEOUT)
+        
+        if len(results) != 1:
+                raise "ValueError", 'error in uidNumber'
+        
+        user_dn = results[0][0]
+        
+        mod_attrs = []
+        
+        # Does it have the correct object class?
+        if 'uccDispenseAccount' not in results[0][1]['objectClass']:
+                # Add uccDispenseAccount objectclass
+                mod_attrs.append((ldap.MOD_ADD, 'objectClass', 'uccDispenseAccount'))
+        
+        # Add MIFARE Card ID
+        mod_attrs.append((ldap.MOD_ADD, 'uccDispenseMIFARE', card_id))
+        
+        # Use a double-try here to work around something that's fixed in Python 2.5
+        try:
+            try:
+                ldapconn.modify_s(user_dn, mod_attrs)
+            except ldap.TYPE_OR_VALUE_EXISTS, e:
+                pass
+        finally:
+            ldapconn.unbind()
+
+if __name__ == '__main__':
+        #print get_uid('\x01\x02\x03\x04\x05\x06')
+        set_card_id('11251', '\x01\x02\x03\x04\x05\x06')
diff --git a/sql-edition/servers/MIFAREClient.py b/sql-edition/servers/MIFAREClient.py
new file mode 100644 (file)
index 0000000..358f550
--- /dev/null
@@ -0,0 +1,37 @@
+from MIFAREDriver import MIFAREReader, MIFAREException\r
+from serial import Serial\r
+from LDAPConnector import get_uid, set_card_id\r
+\r
+class MIFAREClient:\r
+    def __init__(self):\r
+        self.port = Serial('/dev/ttyS2', baudrate = 19200)\r
+        self.reader = MIFAREReader(self.port)\r
+        self.reader.set_led(red = False, green = True)\r
+        self.reader.beep(100)\r
+    \r
+    def get_card(self):\r
+        self.reader.set_led(red = True, green = False)\r
+        try:\r
+            card_id, capacity = self.reader.select_card()\r
+        except MIFAREException:\r
+            self.reader.set_led(red = False, green = True)\r
+            return None\r
+        else:\r
+            self.reader.set_led(red = False, green = True)\r
+            self.reader.beep(100)\r
+            return card_id\r
+    \r
+    def add_card(self, uid):\r
+        self.reader.set_led(red = True, green = False)\r
+        for attempt in range(5):\r
+            self.reader.beep(50)\r
+            try:\r
+                card_id, capacity = self.reader.select_card()\r
+            except MIFAREException:\r
+                pass\r
+            else:\r
+                set_card_id(card_id, uid)\r
+                self.reader.set_led(red = False, green = True)\r
+                return True\r
+        self.reader.set_led(red = False, green = True)\r
+        return False
\ No newline at end of file
diff --git a/sql-edition/servers/MIFAREDriver.py b/sql-edition/servers/MIFAREDriver.py
new file mode 100644 (file)
index 0000000..a3a65b6
--- /dev/null
@@ -0,0 +1,184 @@
+#!/usr/bin/env python2.5
+
+'''mifare - a library for interacting with MIFARE readers.
+Written by David Adam <[email protected]>
+Requires Python 2.5.
+
+Licensed under an MIT-style license: see LICENSE file for details.
+'''
+
+import serial  
+
+xor = lambda x, y: x ^ y
+def checksum(string):
+    return chr(reduce(xor, [ord(i) for i in string]))   
+
+
+class MIFAREException(Exception):
+    pass
+
+
+class MIFARECommunicationException(MIFAREException):
+    pass
+
+
+class MIFAREAuthenticationException(MIFAREException):
+    pass
+
+
+class MIFAREReader:
+    '''An interface to a particular MIFARE reader.'''
+    
+    def __init__(self, io):
+        '''Returns an interface to a MIFARE reader given a file-like object.
+        The file-like object is generally a pyserial Serial object.'''
+        self.io = io
+        if isinstance(self.io, serial.Serial):
+            self.io.setTimeout(2)
+        self.address = '\x00\x00'
+    
+    def get_absolute_block(self, vector):
+        if vector[0] < 32:
+            return vector[0] * 4 + vector[1]
+        else:
+            # Sectors below 32 are 4 blocks
+            # Sectors above are 16 blocks
+            # Thus, sector 32 starts at block 128, 33 at 144, and so on
+            return 128 + (vector[0] - 32) * 16 + vector[1]
+        
+    def send_packet(self, data):
+        '''Constructs a packet for the supplied data string, sends it to the
+        MIFARE reader, then returns the response (if any) to the commmand.'''
+        
+        # Occasionally the reader inserts extra trailing characters into its
+        # responses, so flush the buffers if possible beforehand.
+        if isinstance(self.io, serial.Serial):
+            self.io.flushInput()
+            self.io.flushOutput()
+        
+        # XXX - Needs more error checking.
+        data = '\x00' + self.address + data
+        packet = '\xAA\xBB' + chr(len(data)) + data + checksum(data)
+        self.io.write(packet)
+        response = ''
+        header = self.io.read(2)
+        if header == '\xaa\xbb':
+            length = ord(self.io.read(1))
+            data = self.io.read(length)
+            packet_xsum = self.io.read(1)
+            if checksum(data) == packet_xsum and len(data) == length:
+                # Strip off separator and address header
+                return data[3:]
+            else:
+                raise MIFARECommunicationException, "Invalid response received"
+            
+    def set_antenna(self, state = True):
+        """Turn the card reader's antenna on or off (no return value)"""
+        command = '\x0C\x01' + chr(int(state))
+        response = self.send_packet(command)
+        if response == '\x0c\x01\x00':
+            return None
+        else:
+            raise MIFAREException, 'command failed: set_antenna (%s)' % state
+        
+    def select_card(self, include_halted = False):
+        """Selects a card and returns a tuple of  (serial number, capacity).
+        
+        If include_halted is set, may select a card that halt() has previously
+        been called on."""
+        
+        # Request type of card available
+        command = command = '\x01\x02'
+        if include_halted:
+            command += '\x52'
+        else:
+            command += '\x26'
+        
+        card_type_response = self.send_packet(command)
+        
+        if card_type_response[2] == '\x14':
+            raise MIFAREException, "select_card: no card available"
+        card_type = card_type_response[3:5]
+        
+        if card_type == '\x44\x00': # MIFARE UltraLight
+            raise NotImplementedError, "UltraLight card selected - no functions available"
+        
+        else:
+        # Otherwise, must be a standard MIFARE card.
+            # Anticollision
+            command = '\x02\x02\x04'
+            # No error handling on this command
+            serial = self.send_packet(command)[3:]
+            
+            # Select the card for use
+            capacity = ord(self.send_packet('\x03\x02' + serial)[3])
+            return (serial, capacity)
+    
+    def sector_login(self, blockvect, key, keytype=0):
+        """Log in to a block using the six-byte key.
+        
+        Use a keytype of 1 to use key B."""
+        sector = self.get_absolute_block((blockvect[0], 0))
+        
+        if len(key) != 6:
+            raise ValueError, 'key must be a six-byte string'
+        
+        keytype = 96 + keytype
+        
+        data = chr(keytype) + chr(sector) + key
+        
+        result = self.send_packet('\x07\x02' + data)
+        if ord(result[2]) == 22:
+            raise MIFAREAuthenticationException, "incorrect key provided"
+        
+        return
+    
+    def read_block(self, blockvect):
+        "Read the 16-byte block at vector (sector, block)."
+        block = self.get_absolute_block(blockvect)
+        
+        result = self.send_packet('\x08\x02' + chr(block))
+        return result[3:19]
+    
+    def write_block(self, blockvect, data):
+        """Write the 16 bytes in data to the block at vector (sector, block)."""
+        block = self.get_absolute_block(blockvect)
+        if len(data) != 16:
+            raise ValueError, "invalid data length - must be 16 bytes"
+        
+        result = self.send_packet('\x09\x02' + chr(block) + data)
+        return
+    
+    def write_key(self, key):
+        pass
+    
+    def value_block_increment(self, blocknum, increment):
+        pass
+    
+    def value_block_decrement(self, blocknum, decrement):
+        pass
+    
+    def copy_block(self, source, dest):
+        pass
+    
+    def halt(self):
+        """Halt the current card - no further transactions will be performed with it."""
+        self.send_packet('\x04\x02')
+    
+    def set_led(self, red = False, green = False):
+        led_state = 0
+        if red:
+            led_state += 1
+        if green:
+            led_state += 2
+        self.send_packet('\x07\x01' + chr(led_state))
+        
+    def beep(self, length):
+        '''Beep for a specified length of milliseconds.'''
+        length = int(round(length / 10.))
+        if length > 255:
+            length = 255
+        self.send_packet('\x06\x01' + chr(length))
+        
+    def reset(self):
+        pass

UCC git Repository :: git.ucc.asn.au