diff --git a/Software/LICENSE b/Software/LICENSE new file mode 100644 index 0000000..98e8838 --- /dev/null +++ b/Software/LICENSE @@ -0,0 +1,28 @@ +Copyright (c) 2014, Chema García +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of pycham nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + diff --git a/Software/README.md b/Software/README.md new file mode 100644 index 0000000..8f28612 --- /dev/null +++ b/Software/README.md @@ -0,0 +1,11 @@ +pyCham +====== + +A tool to manage the settings and configurations of Chameleon-Mini + +![](https://raw.githubusercontent.com/sch3m4/pycham/master/img/screenshot.png) + +References +---------- +http://hackaday.com/2013/12/28/chameleon-emulates-contactless-smart-cards/ +https://github.com/emsec/ChameleonMini diff --git a/Software/img/screenshot.png b/Software/img/screenshot.png new file mode 100644 index 0000000..4129bc4 Binary files /dev/null and b/Software/img/screenshot.png differ diff --git a/Software/src/lib/__init__.py b/Software/src/lib/__init__.py new file mode 100644 index 0000000..16c3e31 --- /dev/null +++ b/Software/src/lib/__init__.py @@ -0,0 +1,8 @@ +#!/usr/bin/env python +# +# Written by Chema Garcia +# @sch3m4 +# chema@safetybits.net || http://safetybits.net +# + +from pyCham import * diff --git a/Software/src/lib/pyCham.py b/Software/src/lib/pyCham.py new file mode 100644 index 0000000..3235187 --- /dev/null +++ b/Software/src/lib/pyCham.py @@ -0,0 +1,314 @@ +#!/usr/bin/env python +# +# Written by Chema Garcia +# @sch3m4 +# chema@safetybits.net || http://safetybits.net +# + +import serial +import time +from xmodem import XMODEM +from StringIO import StringIO + +class pyCham(): + # reconnection delay + RECON_DELAY = 7 + # Chameleon-Mini ID + DEVICE_ID = 'USB VID:PID=03eb:2044 SNR=435503430313FFFF91FF71000100' + # commands config + COMMAND_GET_VERSION = 'version?' + COMMAND_GET_CONFIGS = 'config' + COMMAND_GET_CURRENT_CONFIG = 'config?' + COMMAND_GET_CURRENT_SETTING = 'setting?' + COMMAND_GET_BUTTON = 'button' + COMMAND_GET_CURRENT_BUTTON = 'button?' + COMMAND_GET_CURRENT_UID = 'uid?' + COMMAND_GET_CURRENT_UID_SIZE = 'uidsize?' + COMMAND_GET_CURRENT_MEMSIZE = 'memsize?' + COMMAND_GET_RO = 'readonly?' + COMMAND_SET_UID = 'uid=' + COMMAND_SET_CONFIG = 'config=' + COMMAND_SET_RO = 'readonly=' + COMMAND_SET_BUTTON = 'button=' + COMMAND_SET_SETTING = 'setting=' + COMMAND_UPLOAD = 'upload' + COMMAND_DOWNLOAD = 'download' + COMMAND_RESET = 'reset' + #COMMAND_UPGRADE = 'upgrade' # not enabled + COMMAND_CLEAR = 'clear' + + INSTANT_COMMANDS = [COMMAND_GET_VERSION,COMMAND_GET_CONFIGS,COMMAND_GET_CURRENT_CONFIG,COMMAND_GET_CURRENT_UID_SIZE, COMMAND_GET_CURRENT_UID , COMMAND_GET_RO,COMMAND_GET_CURRENT_MEMSIZE,COMMAND_GET_BUTTON,COMMAND_GET_CURRENT_BUTTON,COMMAND_GET_CURRENT_SETTING,COMMAND_CLEAR,COMMAND_RESET] + COMMANDS = { COMMAND_RESET: 'Resets the Chameleon', + COMMAND_SET_CONFIG: 'Sets the active configuration', + COMMAND_SET_UID: 'Sets a new UID (hex)', + COMMAND_SET_RO: 'Switches the state of the read-only mode', + COMMAND_UPLOAD: 'Uploads a memory dump upto the memory size', + COMMAND_DOWNLOAD: 'Downloads a memory dump', + COMMAND_SET_BUTTON: 'Sets the current button press action for BUTTON0', + COMMAND_SET_SETTING: 'Sets the active setting', + COMMAND_CLEAR: 'Clears the entire memory of the currently activated setting' } + + # response commands + RESPONSE_OK_TEXT = 101 + RESPONSE_OK = 0 + RESPONSE_WAITING = 1 + RESPONSE_ERROR = 2 + RESPONSES = {RESPONSE_OK : [100,RESPONSE_OK_TEXT] , RESPONSE_WAITING : [110] , RESPONSE_ERROR : [200,201,202] } + + # serial port configuration + SERIAL_TO = 15 + SERIAL_BD = 38400 + + def __init__(self): + self.SERIAL = None + self.SERIAL_PORT = None + + def setSerial(self,port): + self.SERIAL_PORT = port + + def openSerial(self): + if self.SERIAL_PORT is None: + return None + try: + self.SERIAL = serial.Serial(self.SERIAL_PORT,baudrate=self.SERIAL_BD,timeout=self.SERIAL_TO) + except Exception,e: + print "Error: Cannot open serial port: %s" % e + return None + return self.SERIAL + + def getCommands(self): + return self.COMMANDS + + def getInstantCommands(self): + return self.INSTANT_COMMANDS + + def close(self): + try: + self.SERIAL.close() + except: + pass + + def __check_retcode__ ( self , retcode ): + """ + Checks the return code and returns one of the following return codes: RESPONSE_OK | RESPONSE_WAITING | RESPONSE_ERROR + """ + code = retcode.split(':')[0] + ret = None + for r in self.RESPONSES: + if int(code) in self.RESPONSES[r]: + ret = r + break + return ret + + def execute(self,command='version?'): + """ + Executes an action and returns the pair (return_code,return_value) + """ + self.SERIAL.write ( command + '\r' ) + retval = '' + retcode = self.SERIAL.readline() + if int(retcode.split(':')[0]) == self.RESPONSE_OK_TEXT: + retval = self.SERIAL.readline() + return (retcode.strip(),retval.strip()) + + def downloadToFile ( self , tofile='downloaded.bin' , memsize = 1024 ): + def __read_byte ( size , timeout = 1 ): + return self.SERIAL.read(size) + + def __write_byte ( data , timeout = 1 ): + return self.SERIAL.write(data) + + time.sleep(0.1) + buffer = StringIO() + stream = open(tofile, 'wb') + modem = XMODEM(__read_byte,__write_byte).recv(buffer,crc_mode=0,quiet=1) + contents = buffer.getvalue() + stream.write(contents[:memsize]) + stream.close() + buffer.close() + try: + self.execute ( self.COMMAND_RESET ) + except: + pass + return + + def uploadFromFile ( self , fromfile = 'upload.bin' ): + def __read_byte ( size , timeout = 1 ): + return self.SERIAL.read(size) + + def __write_byte ( data , timeout = 1 ): + return self.SERIAL.write(data) + + time.sleep(0.1) + stream = open(fromfile, 'rb') + buffer = StringIO(stream.read()) + stream.close() + modem = XMODEM(__read_byte,__write_byte).send(buffer,quiet=1) + buffer.close() + try: + self.execute ( self.COMMAND_RESET ) + except: + pass + return + + def getVersion(self): + """ + Returns the version of Chameleon + """ + code,val = self.execute ( self.COMMAND_GET_VERSION ) + retcode = self.__check_retcode__ ( code ) + if retcode == self.RESPONSE_WAITING: # volver a leer + print "TODO" + elif retcode == self.RESPONSE_ERROR: # raise an error + print "TODO" + elif retcode == self.RESPONSE_OK: + return val + else: + print "Unknown Error" + return None + + def getConfigurations(self): + """ + Returns all profiles available + """ + code,val = self.execute ( self.COMMAND_GET_CONFIGS ) + retcode = self.__check_retcode__ ( code ) + if retcode == self.RESPONSE_WAITING: # volver a leer + print "TODO" + elif retcode == self.RESPONSE_ERROR: # raise an error + print "TODO" + elif retcode == self.RESPONSE_OK: + return val + else: + print "Unknown Error" + return None + + def getCurSettings(self): + """ + Returns the current settings + """ + code,val = self.execute ( self.COMMAND_GET_CURRENT_SETTING ) + retcode = self.__check_retcode__ ( code ) + if retcode == self.RESPONSE_WAITING: # volver a leer + print "TODO" + elif retcode == self.RESPONSE_ERROR: # raise an error + print "TODO" + elif retcode == self.RESPONSE_OK: + return val + else: + print "Unknown Error" + return None + + def getCurConfig(self): + """ + Returns the current settings + """ + code,val = self.execute ( self.COMMAND_GET_CURRENT_CONFIG ) + retcode = self.__check_retcode__ ( code ) + if retcode == self.RESPONSE_WAITING: # volver a leer + print "TODO" + elif retcode == self.RESPONSE_ERROR: # raise an error + print "TODO" + elif retcode == self.RESPONSE_OK: + return val + else: + print "Unknown Error" + return None + + def getButtonActions(self): + """ + Returns the current settings + """ + code,val = self.execute ( self.COMMAND_GET_BUTTON ) + retcode = self.__check_retcode__ ( code ) + if retcode == self.RESPONSE_WAITING: # volver a leer + print "TODO" + elif retcode == self.RESPONSE_ERROR: # raise an error + print "TODO" + elif retcode == self.RESPONSE_OK: + return val + else: + print "Unknown Error" + return None + + def getCurButtonActions(self): + """ + Returns the current settings + """ + code,val = self.execute ( self.COMMAND_GET_CURRENT_BUTTON ) + retcode = self.__check_retcode__ ( code ) + if retcode == self.RESPONSE_WAITING: # volver a leer + print "TODO" + elif retcode == self.RESPONSE_ERROR: # raise an error + print "TODO" + elif retcode == self.RESPONSE_OK: + return val + else: + print "Unknown Error" + return None + + def getReadOnly(self): + """ + Returns the current settings + """ + code,val = self.execute ( self.COMMAND_GET_RO ) + retcode = self.__check_retcode__ ( code ) + if retcode == self.RESPONSE_WAITING: # volver a leer + print "TODO" + elif retcode == self.RESPONSE_ERROR: # raise an error + print "TODO" + elif retcode == self.RESPONSE_OK: + return val + else: + print "Unknown Error" + return None + + + def getCurrentUID(self): + """ + Returns the current settings + """ + code,val = self.execute ( self.COMMAND_GET_CURRENT_UID ) + retcode = self.__check_retcode__ ( code ) + if retcode == self.RESPONSE_WAITING: # volver a leer + print "TODO" + elif retcode == self.RESPONSE_ERROR: # raise an error + print "TODO" + elif retcode == self.RESPONSE_OK: + return val + else: + print "Unknown Error" + return None + + def getCurrentUIDSize(self): + """ + Returns the current settings + """ + code,val = self.execute ( self.COMMAND_GET_CURRENT_UID_SIZE ) + retcode = self.__check_retcode__ ( code ) + if retcode == self.RESPONSE_WAITING: # volver a leer + print "TODO" + elif retcode == self.RESPONSE_ERROR: # raise an error + print "TODO" + elif retcode == self.RESPONSE_OK: + return val + else: + print "Unknown Error" + return None + + def getCurrentMemSize(self): + """ + Returns the current settings + """ + code,val = self.execute ( self.COMMAND_GET_CURRENT_MEMSIZE ) + retcode = self.__check_retcode__ ( code ) + if retcode == self.RESPONSE_WAITING: # volver a leer + print "TODO" + elif retcode == self.RESPONSE_ERROR: # raise an error + print "TODO" + elif retcode == self.RESPONSE_OK: + return val + else: + print "Unknown Error" + return None + diff --git a/Software/src/pycham.py b/Software/src/pycham.py new file mode 100644 index 0000000..07c1e26 --- /dev/null +++ b/Software/src/pycham.py @@ -0,0 +1,264 @@ +#!/usr/bin/env python +# -*- coding: latin-1 -*- +# +# Written by Chema Garcia +# @sch3m4 +# chema@safetybits.net || http://safetybits.net +# + +import os +import time +import sys +import argparse +import serial.tools.list_ports +from colorama import init as coloramaInit +from colorama import Fore, Back + +from lib import pyCham + +cham = pyCham() + +def showBanner(showFooter = False): + print "██████╗ ██╗ ██╗ ██████╗██╗ ██╗ █████╗ ███╗ ███╗" + print "██╔══██╗╚██╗ ██╔╝██╔════╝██║ ██║██╔══██╗████╗ ████║ by Chema Garcia" + print "██████╔╝ ╚████╔╝ ██║ ███████║███████║██╔████╔██║ @sch3m4" + print "██╔═══╝ ╚██╔╝ ██║ ██╔══██║██╔══██║██║╚██╔╝██║ chema@safetybits.net" + print "██║ ██║ ╚██████╗██║ ██║██║ ██║██║ ╚═╝ ██║ https://github.com/sch3m4/pycham" + print "╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝ v0.2b" + if showFooter is True: + print "a Python interface to Chameleon Mini" + print "" + + +def locateDevice ( devid ): + ''' + Returns the serial port path of the arduino if found, or None if it isn't connected + ''' + retval = None + for port in serial.tools.list_ports.comports(): + if len(port[2]) >= len(devid) and port[2][:len(devid)] == devid: + retval = port[0] + break + + return retval + + +def auxMenu(options): + os.system('clear') + showBanner() + + ret = None + while True: + cont = 1 + for opt in options: + print Fore.RED + "%s" % cont + Fore.RESET + ")\t" + "%s" % opt + cont += 1 + print "0) Exit" + + try: + print "" + opt = int(raw_input ( "Value: " ).strip()) + except: + continue + + if opt > len ( options ): + continue + + if int(opt) == 0: + ret = None + else: + ret = options[opt-1] + break + + return ret + + +def showMenu(): + cmds = cham.getCommands() + instantcmds = cham.getInstantCommands() + map = {} + finish = False + while finish is False: + print "Getting device version..." + version = cham.getVersion() + print "Getting available configurations..." + configurations = cham.getConfigurations() + print "Getting the currently activated setting..." + settings = cham.getCurSettings() + print "Getting current config..." + config = cham.getCurConfig() + print "Getting button actions..." + bactions = cham.getButtonActions() + print "Getting all available button actions..." + bcuractions = cham.getCurButtonActions() + print "Getting readonly status..." + rostatus = cham.getReadOnly() + print "Getting current UID..." + curuid = cham.getCurrentUID() + print "Getting current UID size..." + curuidsize = cham.getCurrentUIDSize() + print "Getting current memory size..." + curmemsize = cham.getCurrentMemSize() + + os.system('clear') + showBanner() + print "-==== Global Configurations ====-" + print Fore.RESET + " = " + Fore.MAGENTA + "+ Version: " + Fore.GREEN + "%s" % version + print Fore.RESET + " = " + Fore.MAGENTA + "+ Configs: " + Fore.GREEN + "%s" % configurations + print Fore.RESET + " = " + Fore.MAGENTA + "+ Button actions: " + Fore.GREEN + "%s" % bactions + print Fore.RESET + " = " + Fore.MAGENTA + "+ ReadOnly status: " + Fore.GREEN + "%s" % rostatus + print Fore.RESET + " = " + Fore.YELLOW + "+ Current button action: " + Fore.GREEN + "%s" % bcuractions + print Fore.RESET + " = " + Fore.YELLOW + "+ Currrent config: " + Fore.GREEN + "%s" % config + print Fore.RESET + " = " + Fore.YELLOW + "+ Current setting: " + Fore.GREEN + "%s" % settings + print Fore.RESET + " = " + Fore.YELLOW + "+ Current UID: " + Fore.GREEN + "%s" % curuid + print Fore.RESET + " = " + Fore.YELLOW + "+ Current UID size: " + Fore.GREEN + "%s" % curuidsize + print Fore.RESET + " = " + Fore.YELLOW + "+ Current memory size: " + Fore.GREEN + "%s" % curmemsize + print Fore.RESET + "-===============================-\n" + + options = cmds.keys() + cont = 1 + for opt in options: + print Fore.RED + "%s" % cont + Fore.RESET + ")\t%s" % cmds[opt] + cont += 1 + if len(map.keys()) != len(options): + map[cont] = opt + + print "0)\tExit" + + correct = False + reload = False + while correct is False and reload is False: + try: + print "" + print Fore.BLUE + "NOTE:" + Fore.RESET + " Press enter to reload the menu" + sel = raw_input("Action: ") + sel = int(sel) + except ValueError: + if len(sel.strip()) == 0: + reload = True + except: + continue + + if reload is True or sel <= len ( options ): + correct = True + + if reload is True: + continue + + if sel == 0: + return + + invalid = False + unknown = False + cmd = map[sel+1] + if cmd in instantcmds: + cham.execute ( cmd ) + elif cmd == pyCham.COMMAND_SET_UID: + uid = '' + while len ( uid ) == 0: + uid = raw_input ( "Enter the new UID value: " ).strip() + if uid is None: + unknown = True + else: + cmd += uid + elif cmd == pyCham.COMMAND_SET_CONFIG: + print "TODO" + elif cmd == pyCham.COMMAND_SET_RO: + if int(rostatus) == 0: + cmd += '1' + elif int(rostatus) == 1: + cmd += '0' + else: + invalid = True + elif cmd == pyCham.COMMAND_SET_BUTTON: + opt = auxMenu ( bactions.split(',') ) + if opt is None: + unknown = True + else: + cmd += opt + elif cmd == pyCham.COMMAND_SET_SETTING: + opt = auxMenu ( ['1','2','3','4','5','6','7','8','9','10','11','12','13','14','15','16'] ) + if opt is None: + unknown = True + else: + cmd += opt + elif cmd == pyCham.COMMAND_DOWNLOAD: + path = '.' + while os.path.isdir(path) is True and os.path.exists(path): + path = raw_input ( "Enter destination file path: " ).strip() + cham.execute ( cmd ) + time.sleep(1) + cham.downloadToFile ( path , int(curmemsize) ) + print "Restarting..." +# finish = True + elif cmd == pyCham.COMMAND_UPLOAD: + path = '' + while os.path.isfile(path) is False: + path = raw_input ( "Enter source file path: " ).strip() + cham.execute ( cmd ) + time.sleep(1) + cham.uploadFromFile ( path ) +# finish = True + print "Restarting..." + else: + unknown = True + print "Unknown option!" + + # this should not happen + if invalid is True: + print "Invalid value!" + elif unknown is False and finish is False: + cham.execute ( cmd ) + + raw_input("Press ENTER to continue") + + return finish + + +def main(): + showBanner(True) + + if len(sys.argv) < 2: + print "[i] Trying to autodetect Chameleon-Mini" + time.sleep(0.5) + args = {'serial': locateDevice ( pyCham.DEVICE_ID ) } + if args['serial'] is not None: + print "[i] Chameleon-Mini found at " + Fore.GREEN + "%s" % args['serial'] + Fore.RESET + time.sleep(1) + else: + parser = argparse.ArgumentParser() + parser.add_argument('serial', metavar='tty', type=str , help='Serial port of Chameleon' ) + args = vars(parser.parse_args()) + + if args['serial'] is None or not os.path.exists ( args['serial' ] ): + print "[e] Error: Chameleon serial port not found. Rerun this script with -h parameter to view help." + sys.exit(-1) + + coloramaInit() + + finish = False + while finish is False: + cham.setSerial ( args['serial'] ) + if cham.openSerial() is None: + print "[e] Error opening serial port" + sys.exit(-2) + + finish = showMenu() + cham.close() + + cham.close() + return finish + + +if __name__ == "__main__": + finish = False + while finish is False: + try: + finish = main() + except serial.SerialException: + print "[e] Connection closed" + for i in range(0,pyCham.RECON_DELAY): + val = pyCham.RECON_DELAY - i + print '[+] Reconnection in %d seconds\r' % val, + sys.stdout.flush() + time.sleep(1)