diff --git a/eID_gui/AUTHORS b/eID_gui/AUTHORS new file mode 100644 index 0000000..4067889 --- /dev/null +++ b/eID_gui/AUTHORS @@ -0,0 +1 @@ +Dominik Oepen diff --git a/eID_gui/COPYING b/eID_gui/COPYING new file mode 120000 index 0000000..6168a39 --- /dev/null +++ b/eID_gui/COPYING @@ -0,0 +1 @@ +/usr/share/automake-1.11/COPYING \ No newline at end of file diff --git a/eID_gui/ChangeLog b/eID_gui/ChangeLog new file mode 100644 index 0000000..e8a5707 --- /dev/null +++ b/eID_gui/ChangeLog @@ -0,0 +1,8 @@ +2010-07-01 11:26 frankmorgner + + * .: added ChangeLog for distribution package + +2010-06-08 07:26 oepen + + * .: Working on autotools scripts + diff --git a/eID_gui/INSTALL b/eID_gui/INSTALL new file mode 120000 index 0000000..cbd1c80 --- /dev/null +++ b/eID_gui/INSTALL @@ -0,0 +1 @@ +/usr/share/automake-1.11/INSTALL \ No newline at end of file diff --git a/eID_gui/Makefile.am b/eID_gui/Makefile.am new file mode 100644 index 0000000..bc3c0b2 --- /dev/null +++ b/eID_gui/Makefile.am @@ -0,0 +1 @@ +SUBDIRS = src images diff --git a/eID_gui/NEWS b/eID_gui/NEWS new file mode 100644 index 0000000..e69de29 diff --git a/eID_gui/README b/eID_gui/README new file mode 100644 index 0000000..e69de29 diff --git a/eID_gui/configure.ac b/eID_gui/configure.ac new file mode 100644 index 0000000..bbe749c --- /dev/null +++ b/eID_gui/configure.ac @@ -0,0 +1,31 @@ +# -*- Autoconf -*- +# Process this file with autoconf to produce a configure script. + +AC_PREREQ([2.64]) +AC_INIT(eid_gui, 0.2, oepen@informatik.hu-berlin.de) +AM_INIT_AUTOMAKE +AC_CONFIG_MACRO_DIR([m4]) + +# Checks for programs. +AC_PROG_INSTALL +AC_PROG_LN_S +AM_PATH_PYTHON + +# Checks for libraries. +PKG_CHECK_MODULES(PYGTK, pygtk-2.0) +AC_SUBST(PYGTK_CFLAGS) +AC_SUBST(PYGTK_LIBS) + +# Checks for header files. + +# Checks for typedefs, structures, and compiler characteristics. + +# Checks for library functions. + +AC_CONFIG_FILES([Makefile + src/Makefile + src/pace-gui.desktop + src/eid/Makefile + src/eid/eid_gui_globals.py + images/Makefile]) +AC_OUTPUT diff --git a/eID_gui/images/Makefile.am b/eID_gui/images/Makefile.am new file mode 100644 index 0000000..05571db --- /dev/null +++ b/eID_gui/images/Makefile.am @@ -0,0 +1,10 @@ +pixmapsdir = $(datadir)/pace-gui/ +pixmaps_DATA = \ + OpenPACElogo.png \ + apply.png \ + error.png \ + wait.gif \ + info.png + +EXTRA_DIST = \ + $(pixmaps_DATA) diff --git a/eID_gui/images/OpenPACElogo.png b/eID_gui/images/OpenPACElogo.png new file mode 100644 index 0000000..f0a48d2 Binary files /dev/null and b/eID_gui/images/OpenPACElogo.png differ diff --git a/eID_gui/images/apply.png b/eID_gui/images/apply.png new file mode 100644 index 0000000..0bb03f2 Binary files /dev/null and b/eID_gui/images/apply.png differ diff --git a/eID_gui/images/error.png b/eID_gui/images/error.png new file mode 100644 index 0000000..fa5f5d4 Binary files /dev/null and b/eID_gui/images/error.png differ diff --git a/eID_gui/images/info.png b/eID_gui/images/info.png new file mode 100644 index 0000000..e836114 Binary files /dev/null and b/eID_gui/images/info.png differ diff --git a/eID_gui/images/wait.gif b/eID_gui/images/wait.gif new file mode 100644 index 0000000..9a4c8d5 Binary files /dev/null and b/eID_gui/images/wait.gif differ diff --git a/eID_gui/src/Makefile.am b/eID_gui/src/Makefile.am new file mode 100644 index 0000000..8841587 --- /dev/null +++ b/eID_gui/src/Makefile.am @@ -0,0 +1,15 @@ +SUBDIRS = eid +bin_SCRIPTS = eid_gui.py + +paceguidir = $(prefix)/bin + +uidir = $(datadir)/eid_gui +ui_DATA = pinpad.glade + +appdir = $(datadir)/applications/ +app_DATA = pace-gui.desktop + +EXTRA_DIST = \ + $(bin_SCRIPTS) \ + $(app_DATA) \ + $(ui_DATA) diff --git a/eID_gui/src/eid/__init__.py b/eID_gui/src/eid/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/eID_gui/src/eid/eid_gui_globals.py.in b/eID_gui/src/eid/eid_gui_globals.py.in new file mode 100644 index 0000000..97bf15d --- /dev/null +++ b/eID_gui/src/eid/eid_gui_globals.py.in @@ -0,0 +1,91 @@ +#coding: utf-8 + +name = "pace-gui" +version = "@VERSION@" +image_dir = "@prefix@/share/" + name +glade_dir = "@prefix@/share/" + name + +GLADE_FILE = glade_dir + "/pinpad.glade" + +STR_NO_CARD = "Ausweis" +STR_CARD_FOUND = "Ausweis" + +FILE_NO_CARD = image_dir + "/error.png" +FILE_CARD_FOUND = image_dir + "/apply.png" + +EPA_ATR = "3b:84:80:01:00:00:90:00:95" + +#The following strings are used as help texts for eID access rights +DOC_AGE_VERIFICATION = u"Die Altersverifikation erlaubt die Überprüfung, ob ein Nutzer alt genug ist um einen Dienst zu nutzen, ohne sein Geburtsdatum zu übertragen" +DOC_COMMUNITY_ID_VERIFICATION = u"Die Wohnort Verifikation erlaubt die Überprüfung, ob ein Nutzer in einem bestimmten Gebiet wohnt, ohne seine tatsächliche Adresse zu übertragen" +DOC_RESTRICTED_ID = u"Die Pseudonymfunktion macht einen Nutzer für den Dienstanbieter wiedererkennbar, ohne weitere Daten von ihm zu erfassen" +DOC_PRIVILEGED_TERMINAL = u"Ein privilegiertes Terminal darf auf den Kartenindividuellen Schlüssel des Ausweis zugreifen" +DOC_CAN_ALLOWED = u"Das Terminal darf die Funktionen des Ausweises mit der CAN nutzen" +DOC_PIN_MANAGMENT = u"Das Terminal darf die eID-Funktion (de-)aktivieren und eine neue eID-PIN setzen" +DOC_INSTALL_CERTIFICATE = u"Der Dienstanbieter darf ein Zertifikat im Ausweis installieren" +DOC_INSTALL_QC = u"Der Dienstanbieter darf ein qualifiziertes Zertifikat im Ausweis installieren" +DOC_DOCUMENT_TYPE = u"Der Dienstanbieter darf den Dokumenttyp (z.B. Personalausweis oder Aufenthaltstitel) auslesen" +DOC_STATE = u"Der Dienstanbieter darf die Staatzugehörigkeit des Ausweisinhabers lesen" +DOC_EXPIRY_DATE = u"Der Dienstanbieter darf das Ablaufdatum des Ausweises auslesen" + +IMAGES = { + "apply": image_dir + "/apply.png", + "error": image_dir + "/error.png", + "info" : image_dir + "/info.png", + "wait" : image_dir + "/wait.gif" +} + +#Authentication Terminal CHAT +AT_CHAT_STRINGS = { + #Age Verification + 0: ("Altersverifikation", DOC_AGE_VERIFICATION), + #Community ID Verification + 1: (u"Gemeindeschlüssel Verifikation", DOC_COMMUNITY_ID_VERIFICATION), + #Restrictied Identification + 2: ("Pseudonymfunktion", DOC_RESTRICTED_ID), + #Privileged Terminal + 3: ("Priviligiertes Terminal", DOC_PRIVILEGED_TERMINAL), + #CAN allowed + 4: ("Verwendung der CAN", DOC_CAN_ALLOWED), + 5: ("PIN Managment", DOC_PIN_MANAGMENT), + #Install Certificate + 6: ("Zertifikat einspielen", DOC_INSTALL_CERTIFICATE), + #Install Qualified Certificate + 7: ("Qualifiziertes Zertifikat einspielen", DOC_INSTALL_QC), + #Read DG 1 + 8: ("Dokumenttyp", DOC_DOCUMENT_TYPE), + #Read DG 2 + 9: ("Ausstellender Staat", DOC_STATE), + 10: ("Ablaufdatum", DOC_EXPIRY_DATE), #Read DG 3 + 11: ("Vorname", None), #Read DG 4 + 12: ("Nachname", None), #Read DG 5 + 13: (u"Künstlername", None), #Read DG 6 + 14: ("Doktorgrad", None), #Read DG 7 + 15: ("Geburtsdatum", None), #Read DG 8 + 16: ("Geburtsort", None), #Read DG 9 + 17: (u"Ungültig", None), #Read DG 10 + 18: (u"Ungültig", None), #Read DG 11 + 19: (u"Ungültig", None),#Read DG 12 + 20: (u"Ungültig", None),#Read DG 13 + 21: (u"Ungültig", None),#Read DG 14 + 22: (u"Ungültig", None),#Read DG 15 + 23: (u"Ungültig", None),#Read DG 16 + 24: ("Adresse", None), #Read DG 17 + 25: (u"Gemeindeschlüssel lesen", None),#Read DG 18 + 26: (u"Ungültig", None),#Read DG 19 + 27: (u"Ungültig", None),#Read DG 20 + 28: (u"Ungültig", None),#Read DG 21 + 29: (u"Ungültig", None),#RFU + 30: (u"Ungültig", None),#RFU + 31: (u"Ungültig", None),#RFU + 32: (u"Ungültig", None),#RFU + 33: (u"Ungültig", None),#Write DG 21 + 34: (u"Ungültig", None),#Write DG 20 + 35: (u"Ungültig", None),#Write DG 19 + 36: (u"Gemeindeschlüssel schreiben", None),#Write DG 18 + 37: (u"Adresse schreiben", None)#Write DG 17" +} + +TEST_CVC = "\x7F\x21\x82\x01\x41\x7F\x4E\x81\xFA\x5F\x29\x01\x00\x42\x0D\x5A\x5A\x44\x56\x43\x41\x41\x54\x41\x30\x30\x30\x33\x7F\x49\x4F\x06\x0A\x04\x00\x7F\x00\x07\x02\x02\x02\x02\x03\x86\x41\x04\x19\xD1\x75\x45\xD3\xFE\x0B\x34\x3E\x7E\xE2\xAE\x4E\x2B\xC9\x2D\x51\x35\x1C\xC1\x17\xA4\x7F\xA9\x51\x9A\xDB\x1E\x40\x5E\xE6\xB8\x12\x12\x80\xBC\xC2\xFF\xF0\x35\x7A\x19\x7D\xE7\x39\xA7\xFD\x2E\xF0\x22\x10\xEF\x34\x3C\xDB\xE7\x9E\xF9\x4B\x8E\x28\x59\x1B\xB9\x5F\x20\x0B\x5A\x5A\x44\x4B\x42\x32\x30\x30\x30\x30\x52\x7F\x4C\x12\x06\x09\x04\x00\x7F\x00\x07\x03\x01\x02\x02\x53\x05\x00\x03\x01\xDF\x04\x5F\x25\x06\x01\x00\x00\x02\x01\x07\x5F\x24\x06\x01\x00\x00\x03\x03\x01\x65\x5E\x73\x2D\x06\x09\x04\x00\x7F\x00\x07\x03\x01\x03\x01\x80\x20\x75\xE0\xC4\xAC\x36\xC2\x5A\x33\xAC\x0E\x9A\x75\xEB\x79\x2A\x72\xF3\x31\xA5\x1E\x28\x63\x4E\xCC\x2E\xD6\x2E\x54\xF3\xC6\x93\xDA\x73\x2D\x06\x09\x04\x00\x7F\x00\x07\x03\x01\x03\x02\x80\x20\x18\x12\x65\x74\x49\xFC\xF1\xD3\xDA\xD8\x3D\x13\x14\x29\x17\x5C\x61\x8B\x21\xBA\xF0\xAF\x44\xAC\xE3\x8C\xB2\xC1\x2C\xEB\x2A\x56\x5F\x37\x40\x54\x0F\x85\x09\x12\xAB\xD3\x51\xF8\xF5\x56\x9B\x53\x4A\x5C\x8F\x64\x54\x5B\x51\xA7\x34\x70\xBE\x5A\xD2\x89\xC1\x9A\x5E\x13\x52\x53\xD3\xBB\x15\x52\x26\x21\x7B\x41\xE7\xF0\x68\xB3\x52\x3F\x3A\x63\x92\x22\xAF\x2B\x62\x8C\x39\x7D\x4F\xD4\x02\x1E\xDE\x00\xDC" + +TEST_DESCRIPTION = "\x30\x82\x01\x90\x06\x0A\x04\x00\x7F\x00\x07\x03\x01\x03\x01\x01\xA1\x16\x0C\x14\x42\x75\x6E\x64\x65\x73\x64\x72\x75\x63\x6B\x65\x72\x65\x69\x20\x47\x6D\x62\x48\xA2\x24\x13\x22\x68\x74\x74\x70\x3A\x2F\x2F\x77\x77\x77\x2E\x62\x75\x6E\x64\x65\x73\x64\x72\x75\x63\x6B\x65\x72\x65\x69\x2E\x64\x65\x2F\x64\x76\x63\x61\xA3\x18\x0C\x16\x44\x65\x75\x74\x73\x63\x68\x65\x20\x4B\x72\x65\x64\x69\x74\x62\x61\x6E\x6B\x20\x41\x47\xA4\x13\x13\x11\x68\x74\x74\x70\x3A\x2F\x2F\x77\x77\x77\x2E\x64\x6B\x62\x2E\x64\x65\xA5\x82\x01\x13\x0C\x82\x01\x0F\x54\x61\x75\x62\x65\x6E\x73\x74\x72\x2E\x20\x37\x2D\x39\x0D\x0A\x31\x30\x31\x31\x37\x20\x42\x65\x72\x6C\x69\x6E\x0D\x0A\x69\x6E\x66\x6F\x40\x64\x6B\x62\x2E\x64\x65\x0D\x0A\x45\x72\xC3\xB6\x66\x66\x6E\x75\x6E\x67\x20\x65\x69\x6E\x65\x73\x20\x4B\x6F\x6E\x74\x6F\x73\x0D\x0A\x42\x65\x72\x6C\x69\x6E\x65\x72\x20\x42\x65\x61\x75\x66\x74\x72\x61\x67\x74\x65\x72\x20\x66\xC3\xBC\x72\x20\x44\x61\x74\x65\x6E\x73\x63\x68\x75\x74\x7A\x20\x75\x6E\x64\x20\x49\x6E\x66\x6F\x72\x6D\x61\x74\x69\x6F\x6E\x73\x66\x72\x65\x69\x68\x65\x69\x74\x2C\x20\x41\x6E\x20\x64\x65\x72\x20\x55\x72\x61\x6E\x69\x61\x20\x34\x2D\x31\x30\x2C\x20\x31\x30\x37\x38\x37\x20\x42\x65\x72\x6C\x69\x6E\x2C\x20\x30\x33\x30\x2F\x31\x33\x20\x38\x38\x39\x2D\x30\x2C\x20\x6D\x61\x69\x6C\x62\x6F\x78\x40\x64\x61\x74\x65\x6E\x73\x63\x68\x75\x74\x7A\x2D\x62\x65\x72\x6C\x69\x6E\x2E\x64\x65\x2C\x20\x68\x74\x74\x70\x3A\x2F\x2F\x77\x77\x77\x2E\x64\x61\x74\x65\x6E\x73\x63\x68\x75\x74\x7A\x2D\x62\x65\x72\x6C\x69\x6E\x2E\x64\x65\x0D\x0A\x45\x72\xC3\xB6\x66\x66\x6E\x75\x6E\x67\x20\x65\x69\x6E\x65\x73\x20\x4B\x6F\x6E\x74\x6F\x73\x0D\x0A" diff --git a/eID_gui/src/eid/logic.py b/eID_gui/src/eid/logic.py new file mode 100644 index 0000000..88c1b0a --- /dev/null +++ b/eID_gui/src/eid/logic.py @@ -0,0 +1,55 @@ +import gobject + +import subprocess +from threading import Thread, Event +from eid_gui_globals import STR_CARD_FOUND, STR_NO_CARD, IMAGES + +class cardChecker(Thread): + """ This class searches for a card with a given ATR and displays + wether or not using a label and an image """ + + def __init__(self, lbl, img, atr, intervall=1): + Thread.__init__(self) + self._finished = Event() + self._paused = Event() + self.lbl = lbl + self.img = img + self.target_atr = atr + self.intervall = intervall + + def _check_card(self): + """ + Actually this method should make use of pyscard, but I couldn't make + it run on the OpenMoko yet. Therefor we call opensc-tool instead, which + leads to higher delays """ + + proc = subprocess.Popen(["opensc-tool", "--atr"], + stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True) + + line = proc.stdout.readline().rstrip() + if line == self.target_atr: + gobject.idle_add(self.lbl.set_label, STR_CARD_FOUND) + gobject.idle_add(self.img.set_from_file, IMAGES["apply"]) + else: + gobject.idle_add(self.lbl.set_label, STR_NO_CARD) + gobject.idle_add(self.img.set_from_file, IMAGES["error"]) + + def run(self): + """Main loop: Poll the card if the thread is not paused or finished""" + while (True): + if self._finished.isSet(): + return + if self._paused.isSet(): + continue + self._check_card() + self._finished.wait(self.intervall) + + def stop(self): + self._finished.set() + + def pause(self): + self._paused.set() + + def resume(self): + self._paused.clear() + diff --git a/eID_gui/src/eid/widgets.py b/eID_gui/src/eid/widgets.py new file mode 100644 index 0000000..ac48fdc --- /dev/null +++ b/eID_gui/src/eid/widgets.py @@ -0,0 +1,65 @@ +import sys +try: + import pygtk + pygtk.require("2.0") + import gtk, pango +except ImportError: + sys.exit(1) + +from eid_gui_globals import AT_CHAT_STRINGS, IMAGES + +class customCheckButton(object): + """This class provides a custom version of gtk.CheckButton. + The main difference isthat the checkbox can be placed at the right + side of the label and that we can store an index with the button""" + + def __init__(self, index, vbox): + self.idx = index + #Fetch name and (possibly empty) helptext + strings = AT_CHAT_STRINGS[index] + self.label = strings[0] + self.helptext = strings[1] + self.parent = vbox.get_parent_window() + + #Setup a label with the name of the access right + self.lbl = gtk.Label(self.label) + self.lbl.set_alignment(0.0, 0.5) + self.lbl.set_padding(20, 0) + self.lbl.modify_font(pango.FontDescription("bold")) + + #If we have a helptext, put an image at the left side of the label + self.info_box = None + if self.helptext is not None: + self.info_box = gtk.EventBox() + img = gtk.Image() + img.set_from_file(IMAGES["info"]) + self.info_box.add(img) + self.info_box.connect("button_release_event", self._show_info) + + #...and a checkbox on the right side of the label + self.chk = gtk.CheckButton("") + self.chk.set_active(True) + self.chk.set_alignment(1.0, 0.5) + + #Insert the label and the checkbox in the vbox and add a seperator + hbox = gtk.HBox() + if self.info_box is not None: + hbox.pack_start(self.info_box, False, False) + else: #pack empty label for spacing + spacer = gtk.Label("") + spacer.set_size_request(32, 32) + hbox.pack_start(spacer, False, False) + hbox.pack_start(self.lbl, True, True) + hbox.pack_start(self.chk, False, False) + vbox.pack_start(gtk.HSeparator()) + vbox.pack_start(hbox) + + def is_active(self): + return self.chk.get_active() + + def _show_info(self, widget=None, data=None): + """Show a messagebox containing info about the access right in + question""" + help = MsgBox(self.parent, self.helptext, "info", + gtk.DIALOG_DESTROY_WITH_PARENT) + diff --git a/eID_gui/src/eid/windows.py b/eID_gui/src/eid/windows.py new file mode 100644 index 0000000..73152e2 --- /dev/null +++ b/eID_gui/src/eid/windows.py @@ -0,0 +1,537 @@ +#coding: utf-8 + +import sys, subprocess, os +try: + import pygtk + pygtk.require("2.0") + import gtk, gobject, glib, pango +except ImportError: + sys.exit(1) + +try: + import pace +except ImportError: + print >> sys.stderr, "Could not import pace module, please install pyPACE" + sys.exit(1) + +from eid_gui_globals import IMAGES, GLADE_FILE, EPA_ATR +from widgets import customCheckButton +from logic import cardChecker + +class MokoWindow(gtk.Window): + """ + Base class for all our dialogues. It's a simple gtk.Window, with a title at + the top and to buttons at the bottom. In the middle there is a vBox where + we can put custom stuff for each dialoge. + All the dialoges are stored as a double-linked list, so the navigation from + one window to another is easy + """ + + def __init__(self, title, predecessor): + super(MokoWindow, self).__init__() + + self.title_str = title + self.predecessor = predecessor + + assert(isinstance(self.title_str, basestring)) + + self.connect("destroy", gtk.main_quit) + #Display resolution of OpenMoko minus height of the SHR toolbar + self.set_size_request(480, 586) + #self.set_resizable(False) + + #Main VBox, which consists of the title, the body, and the buttons + self.__vb = gtk.VBox(False, 5) + self.add(self.__vb) + + #Title label at the top of the window + lbl_title = gtk.Label(self.title_str) + lbl_title.modify_font(pango.FontDescription("sans 14")) + lbl_title.set_alignment(0.5, 0.0) + self.__vb.pack_start(lbl_title, False, False) + + #Body VBox for custom widgets + self.body = gtk.VBox(False) + #Add a scrollbar in case we must display lots of information + #Only use a vertical spacebar, not a horizontal one + scrolled_window = gtk.ScrolledWindow() + scrolled_window.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC) + scrolled_window.set_shadow_type(gtk.SHADOW_ETCHED_OUT) + self.__vb.pack_start(scrolled_window) + scrolled_window.add_with_viewport(self.body) + + #Add two buttons at the bottom of the window + hbox = gtk.HBox(True, 5) + if self.predecessor is None: + btnBack = gtk.Button("Abbrechen") + else: + btnBack = gtk.Button(u"Zurück") + btnBack.set_size_request(150, 75) + btnBack.connect("clicked", self.btnBack_clicked, None) + if isinstance(btnBack.child, gtk.Label): + lbl = btnBack.child + else: + raise ValueError("Button does not have a label") + lbl.modify_font(pango.FontDescription("sans 12")) + hbox.pack_start(btnBack, True, True) + + btnForward = gtk.Button("Weiter") + btnForward.set_size_request(150, 75) + btnForward.connect("clicked", self.btnForward_clicked, None) + if isinstance(btnForward.child, gtk.Label): + lbl = btnForward.child + else: + raise ValueError("Button does not have a label") + hbox.pack_start(btnForward, True, True) + lbl.modify_font(pango.FontDescription("sans 12")) + self.__vb.pack_start(hbox, False, False) + + #Place window in the center of the screen + self.set_position(gtk.WIN_POS_CENTER_ALWAYS) + + def btnBack_clicked(self, widget, data=None): + """Go back to the previous window. If there is no previous window, + do nothing""" + if self.predecessor is None: + gtk.main_quit() + else: + self.predecessor.show_all() + self.hide() + + def btnForward_clicked(self, widget, data=None): + """Go to the next window. If there is no next window, do nothing""" + if self.successor is None: + pass + else: + self.successor.show_all() + self.hide() + + +class MsgBox(gtk.Dialog): + """The gtk.MessageDialog looks like crap on the Moko, so we write our own""" + + def __init__(self, parent, msg, img_type, flags): + #img is a string which is used as a key for the IMAGES dictionary + if img_type is not None: + assert IMAGES.has_key(img_type) + + super(MsgBox, self).__init__(title="", parent=parent, flags=flags) + + #Dialog will be resized to screen width no matter what. Therefore we + #use the whole width from the beginning to avoid being resized + self.set_size_request(480, 160) + + hbox_top = gtk.HBox() + lbl = gtk.Label(msg) + lbl.set_width_chars(20) + lbl.set_line_wrap(True) + btn = gtk.Button("Ok") + btn.connect("clicked", self.__destroy) + img = gtk.Image() + img.set_from_file(IMAGES[img_type]) + hbox_top.pack_start(img, False, False) + hbox_top.pack_start(lbl, True, True) + self.vbox.pack_start(hbox_top) + #self.vbox.pack_start(gtk.HSeparator()) + hbox_bottom = gtk.HBox() + spacer = gtk.Label("") + spacer.set_size_request(380, 25) + hbox_bottom.pack_start(spacer, False, False) + hbox_bottom.pack_start(btn, True, True) + self.vbox.pack_start(hbox_bottom) + #hbox.pack_start(vbox) + #self.add(hbox) + + #self.set_decorated(False) + self.set_modal(True) + + self.show_all() + + def __destroy(self, widget=None, data=None): + self.destroy() + + +class CertificateDescriptionWindow(MokoWindow): + """This window is used to display information about the service provider, + extracted from the terminal certificate and the appendant certificate + description""" + + def __init__(self, binDescription, binCert): + super(CertificateDescriptionWindow, self).__init__("Dienstanbieter", None) + + #binDesciption contains the Certificate Description as a octet string + desc = pace.d2i_CVC_CERTIFICATE_DESCRIPTION(binDescription) + + self.successor = CVCWindow(binCert, self) + + #Display the validity period: + cvc = pace.d2i_CV_CERT(binCert) + effective_date = self.formatDate(pace.get_effective_date(cvc)) + expiration_date = self.formatDate(pace.get_expiration_date(cvc)) + validity_period = effective_date + " - " + expiration_date + #self.addRow(u"Gültig ab:", effective_date) + #self.addRow(u"Gültig bis:", expiration_date) + self.addRow(u"Gültigkeitszeitraum:", validity_period) + + #Display issuer Name and possibly URL and subject name and possibly URL + issuer_name = pace.get_issuer_name(desc) + issuer_url = pace.get_issuer_url(desc) + self.addRow("Zertifikatsaussteller:", issuer_name) + if issuer_url is not None: + self.addRow("", issuer_url, True) + subject_name = pace.get_subject_name(desc) + subject_url = pace.get_subject_url(desc) + self.addRow("Zertifikatsinhaber:", subject_name) + if subject_url is not None: + self.addRow("", subject_url, True) + self.set_focus(None) + + #Extract, format and display the terms of usage + terms = pace.get_termsOfUsage(desc) + formated_terms = self.formatTOU(terms) + self.addRow("Beschreibung:", formated_terms) + + self.show_all() + + def formatDate(self, date): + """Take a date string in the form that is included in CV Certificates + (YYMMDD) and return it in a better readable format (DD.MM.YYYY)""" + + assert(isinstance(date, basestring)) + assert(len(date) == 6) + + year = date[:2] + month = date[2:4] + day = date[4:6] #Cut off trailing \0 + + #Implicit assumption: 2000 <= year < 3000 + return day + "." + month + "." + "20" + year + + def formatTOU(self, terms): + +# for s in self.terms_array: +# s = s.replace(", ", "\n").strip() +# lbl = gtk.Label(s) +# lbl.set_alignment(0.0, 0.0) +# lbl.set_width_chars(60) +# lbl.set_line_wrap(True) #XXX: Doesn't work on the Moko +# lbl.modify_font(pango.FontDescription("bold")) +# self.body.pack_start(gtk.HSeparator()) +# self.body.pack_start(lbl, False, False, 2) + + return terms.replace(", ", "\r\n") + + def addRow(self, title, text, url=False): + if not url: #URL has no title + lbl_title = gtk.Label(title) + lbl_title.set_width_chars(20) + lbl_title.set_alignment(0.0, 0.5) + lbl_title.modify_font(pango.FontDescription("bold")) + self.body.pack_start(lbl_title) + hbox = gtk.HBox() + lbl = gtk.Label("") #Empty label for spacing + lbl.set_width_chars(3) + hbox.pack_start(lbl) + if url: + lblText = gtk.LinkButton(text, text) + if isinstance(lblText.child, gtk.Label): + lbl = lblText.child + else: + raise ValueError("Button does not have a label") + lbl.set_width_chars(40) + + else: + lblText = gtk.Label(text) + lblText.set_width_chars(40) + + lblText.set_alignment(0.0, 0.0) + hbox.pack_start(lblText) + self.body.pack_start(hbox) + +class CVCWindow(MokoWindow): + """This window is used to display and modify the access encoded in the CHAT + of a CV Certificate""" + + def __init__(self, binCert, predecessor): + super(CVCWindow, self).__init__("Zugriffsrechte", predecessor) + + self.successor = PinpadGTK("pin", None) + + #Convert the binary certificate to the internal representation and + #extract the relative authorization from the chat + cvc = pace.d2i_CV_CERT(binCert) + chat = pace.cvc_get_chat(cvc) + self.chat_oid = pace.get_chat_oid(chat) + self.chat = pace.get_binary_chat(chat) + self.rel_auth = [] + for char in self.chat: + self.rel_auth.append(ord(char)) + self.rel_auth_len = len(self.rel_auth) + + #Extract the access rights from the CHAT and display them in the window + self.access_rights = [] + j = 0 + for i in range((self.rel_auth_len - 1) * 8 - 2): + if (i % 8 == 0): + j += 1 + if self.rel_auth[self.rel_auth_len - j] & (1 << (i % 8)): + chk = customCheckButton(i, self.body) + self.access_rights.append(chk) + + def btnForward_clicked(self, widget, data=None): + """ Check wether any access right have been deselected and modify the + CHAT accordingly """ + + for right in self.access_rights: + if not right.is_active(): + idx = right.idx + self.rel_auth[len(self.chat) - 1 - idx / 8] ^= (1 << (idx % 8)) + + #super(CVCWindow, self).btnForward_clicked(widget, data) + new_chat_list = [] + for byte in self.chat_oid: + new_chat_list.append(ord(byte)) + new_chat_list.append(len(self.rel_auth)) + new_chat_list.extend(self.rel_auth) + new_chat = self.__formatHexString(new_chat_list) + self.successor.set_chat(new_chat) + self.successor.show() + self.hide() + + def __formatHexString(self, int_list): + """Take a list of integers and convert it to a hex string, where the + individual bytes are seperated by a colon (e.g. "0f:00:00")""" + hex_str = "" + for i in int_list: + byte = hex(i)[2:] + if len(byte) == 1: byte = "0" + byte + hex_str += byte + ":" + + return hex_str[:-1] + +class PinpadGTK: + """This a simple GTK based GUI to enter a PIN/PUK/CAN""" + + def __init__(self, secret="pin", chat=None, cert=None): + btn_names = ["btnOne", "btnTwo", "btnThree", "btnFour", "btnFive", + "btnSix", "btnSeven", "btnEight", "btnNine", "btnDel", + "btnZero", "btnOk"] + + self.pin = "" + self.secret = secret + if self.secret == "pin" or self.secret == "can": + self.secret_len = 6 + elif self.secret == "transport": + self.secret_len = 5 + elif self.secret == "puk": + self.secret_len = 10 + else: + raise ValueError("Unknwon secret type: %s" % self.secret) + #gtk.main_quit() + self.chat = chat + self.cert = cert + + #Set the Glade file + self.gladefile = GLADE_FILE + self.builder = gtk.Builder() + try: + self.builder.add_from_file(self.gladefile) + except glib.GError, err: + #If we encounter an exception at startup, we display a popup with + #the error message + popup = MsgBox(None, err.message, "error", None) + popup.run() + popup.destroy() + raise err + + #Get the Main Window, and connect the "destroy" event + #Create our dictionay and connect it + dic = { "on_btnOne_clicked" : self.digit_clicked, + "on_btnTwo_clicked" : self.digit_clicked, + "on_btnThree_clicked" : self.digit_clicked, + "on_btnFour_clicked" : self.digit_clicked, + "on_btnFive_clicked" : self.digit_clicked, + "on_btnSix_clicked" : self.digit_clicked, + "on_btnSeven_clicked" : self.digit_clicked, + "on_btnEight_clicked" : self.digit_clicked, + "on_btnNine_clicked" : self.digit_clicked, + "on_btnZero_clicked" : self.digit_clicked, + "on_btnOk_clicked" : self.btnOk_clicked, + "on_btnDel_clicked" : self.btnDel_clicked, + "on_MainWindow_destroy_event" : self.shutdown, + "on_MainWindow_destroy" : self.shutdown } + + self.builder.connect_signals(dic) + + #Change the font for the text field and set it up up properly for the + #type of secret we are using + self.output = self.builder.get_object("txtOutput") + self.output.modify_font(pango.FontDescription("Monospace 16")) + self.output.set_text(self.secret_len * "_") + + #Look for card and set the label accordingly + self.lbl_cardStatus = self.builder.get_object("lbl_cardStatus") + #Fetch the status image + self.img_cardStatus = self.builder.get_object("img_cardStatus") + self.img_cardStatus.set_from_file(IMAGES["error"]) + + #We only start the thread for polling the card when the window + #is shown + self.cardChecker = None + + #Change the font for the buttons + #For this you have to retrieve the label of each button and change + #its font + for btn_name in btn_names: + btn = self.builder.get_object(btn_name) + if btn.get_use_stock(): + lbl = btn.child.get_children()[1] + elif isinstance(btn.child, gtk.Label): + lbl = btn.child + else: + raise ValueError("Button does not have a label") + + if (btn_name == "btnOk" or btn_name == "btnDel"): + lbl.modify_font(pango.FontDescription("sans 14")) + else: + lbl.modify_font(pango.FontDescription("sans 18")) + + #Display Main Window + self.window = self.builder.get_object("MainWindow") + self.window.connect("destroy", self.shutdown) + self.window.hide() + + def show(self): + """Start the polling thread, then show the window""" + if self.cardChecker is None: + self.cardChecker = cardChecker(self.lbl_cardStatus, self.img_cardStatus, + EPA_ATR) + gobject.idle_add(self.cardChecker.start) + self.window.show_all() + + def set_chat(self, chat): + self.chat = chat + + def shutdown(self, widget): + """Stop the cardChecker thread before exiting the application""" + if self.cardChecker is not None: + self.cardChecker.stop() + self.cardChecker.join() + gtk.main_quit() + + def digit_clicked(self, widget): + """We indicate the ammount of digits with underscores + For every digit that is enterd we replace one underscore with a star + We only accept secret_len digits and ignore every additional digit""" + digit = widget.get_label() + assert digit.isdigit() + txt = self.output.get_text() + if len(self.pin) < self.secret_len: +# self.output.set_text(txt.replace("_", digit, 1)) + idx = txt.find('_') + output = txt[:idx] + digit + txt[idx+1:] + self.output.set_text(output) + gobject.timeout_add(750, self.hide_digits, idx) + self.pin += digit + + def hide_digits(self, idx): + """Change character number idx of self.output text to a '*' """ + output = "" + txt = self.output.get_text() + if txt[idx].isdigit(): + output = txt[:idx] + '*' + txt[idx+1:] + self.output.set_text(output) + return False + + def btnOk_clicked(self, widget): + """Pass the entered secret to pace-tool. If pace-tool is run + sucessfully exit, otherwise restart the PIN entry""" + + env_args = os.environ + + #cmd contains the command and all the parameters for our subproccess + cmd = ["pace-tool"] + + #We have to select the type of secret to use via a command line + #parameter and provide the actual secret via an environment variable + if self.secret == "pin" or self.secret == "transport": + cmd.append("--pin") + env_args["PIN"] = self.pin + elif self.secret == "can": + cmd.append("--can") + env_args["CAN"] = self.pin + elif self.secret == "puk": + cmd.append("--puk") + env_args["PUK"] = self.pin + else: + raise ValueError("Unknown secret type: %s" % self.secret) + + #If we have a CHAT, we pass it to pace-tool + if (self.chat): + cmd.append("--chat=" + self.chat) + + cmd.append("-v") + + #Try to call pace-tool. This is a blocking call. An animation is being + #shown while the subprocess is running + try: + #Stop polling the card while PACE is running + self.cardChecker.pause() + proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, env=env_args, + close_fds=True) + except OSError: + popup = MsgBox(self.window, "pace-tool wurde nicht gefunden", + "error", gtk.DIALOG_DESTROY_WITH_PARENT) + popup.run() + popup.destroy() + self.cardChecker.resume() #Restart cardChecker + return + + #Show the animation to indicate that the program is not dead + waiting = gtk.Window(gtk.WINDOW_POPUP) + animation = gtk.gdk.PixbufAnimation(IMAGES["wait"]) + img = gtk.Image() + img.set_from_animation(animation) + waiting.add(img) + waiting.set_position(gtk.WIN_POS_CENTER_ALWAYS) + waiting.set_modal(True) + waiting.set_transient_for(self.window) + waiting.set_decorated(False) + waiting.show_all() + + #Try to keep the GUI responsive by taking care of the event queue + line = proc.stdout.readline() + while line: + while gtk.events_pending(): + gtk.main_iteration() + line = proc.stdout.readline() + + #Get the return value of the pace-tool process + ret = proc.poll() + waiting.destroy() + self.cardChecker.resume() + + if (ret == 0): + popup = MsgBox(self.window, "Pin wurde korrekt eingegeben", "apply", + gtk.DIALOG_DESTROY_WITH_PARENT) + popup.run() + popup.destroy() + #XXX: Actually we should return to the application that started + #the PIN entry + self.shutdown(None) + else: + popup = MsgBox(self.window, "PIN wurde falsch eingegeben", "error", + gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT) + popup.run() + popup.destroy() + self.output.set_text(self.secret_len * "_") + self.pin = "" + + def btnDel_clicked(self, widget): + """Remove one digit from the pin and replace the last * in the output + field with an underscore""" + self.pin = self.pin[:-1] + num_digits = len(self.pin) + output = num_digits * '*' + (self.secret_len - num_digits) * "_" + self.output.set_text(output) + diff --git a/eID_gui/src/eid_gui.py b/eID_gui/src/eid_gui.py new file mode 100644 index 0000000..f8a1ae2 --- /dev/null +++ b/eID_gui/src/eid_gui.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python +# coding: utf-8 + +import sys, os, subprocess +try: + import pygtk + pygtk.require("2.0") + import gtk, gobject +except ImportError: + sys.exit(1) + +from eid.eid_gui_globals import TEST_DESCRIPTION, TEST_CVC +from eid.windows import CertificateDescriptionWindow + +if __name__ == "__main__": + gobject.threads_init() + CertificateDescriptionWindow(TEST_DESCRIPTION, TEST_CVC) + gtk.main() diff --git a/eID_gui/src/pace-gui.desktop.in b/eID_gui/src/pace-gui.desktop.in new file mode 100644 index 0000000..f3fe0ef --- /dev/null +++ b/eID_gui/src/pace-gui.desktop.in @@ -0,0 +1,11 @@ +[Desktop Entry] +Version=@VERSION@ +Encoding=UTF-8 +Name=PACE-GUI +Comment=GUI zur Eingabe einer PIN/CAN/PUK fuer pace-tool +Exec=eid_gui.py +Icon=@prefix@/share/pace-gui/OpenPACElogo.png +Terminal=false +Type=Application +Categories=Utility +StartupNotify=false diff --git a/eID_gui/src/pinpad.glade b/eID_gui/src/pinpad.glade new file mode 100644 index 0000000..aab5264 --- /dev/null +++ b/eID_gui/src/pinpad.glade @@ -0,0 +1,272 @@ + + + + + + True + Pinpad + center + 480 + 586 + OpenPACElogo.png + + + + + True + + + True + <span font_weight="bold" font="11">Bitte PIN eingeben</span> + True + + + False + 0 + + + + + True + + + False + 1 + + + + + True + False + False + 10 + + 0 + True + out + True + False + + + False + 5 + 2 + + + + + True + + + False + 3 + + + + + True + + + True + 0.89999997615814209 + <span fgcolor="red" font="12">Ausweis</span> + True + + + 0 + + + + + True + 0.10000000149011612 + error.png + + + 1 + + + + + 4 + + + + + True + 4 + 3 + True + + + 1 + True + True + True + + + + + + 4 + True + True + True + + + + 1 + 2 + + + + + 7 + True + True + True + + + + 2 + 3 + + + + + Entf + True + True + True + + + + 3 + 4 + + + + + 2 + True + True + True + + + + 1 + 2 + + + + + 5 + True + True + True + + + + 1 + 2 + 1 + 2 + + + + + 8 + True + True + True + + + + 1 + 2 + 2 + 3 + + + + + 0 + True + True + True + + + + 1 + 2 + 3 + 4 + + + + + 3 + True + True + True + + + + 2 + 3 + + + + + 6 + True + True + True + + + + 2 + 3 + 1 + 2 + + + + + 9 + True + True + True + + + + 2 + 3 + 2 + 3 + + + + + Ok + True + True + True + + + + 2 + 3 + 3 + 4 + + + + + 5 + + + + + +