#!/usr/bin/env python3 # $Id$ # Copyright (C) 2005,2008 Igor Belyi # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import gtk, gobject, gtk.glade import gettext import time, sys, os from pyme import errors, core from pyme.core import Context, Data, pubkey_algo_name from pyme.constants import validity, status, keylist, sig, sigsum # Enable internationalization using gpa translation pages. gettext.install('gpa', None, 1) gtk.glade.bindtextdomain('gpa') gtk.glade.textdomain('gpa') # Thanks to Bernhard Reiter for pointing out the following: # gpgme_check_version() necessary for initialisation according to # gpgme 1.1.6 and this is not done automatically in pyme-0.7.0 print("gpgme version:", core.check_version()) # Helper functions to convert non-string data into printable strings def sec2str(secs, empty="_(Unknown)"): "Convert seconds since 1970 into mm/dd/yy string" if secs > 0: return time.strftime("%m/%d/%y", time.localtime(secs)) elif secs == 0: return empty else: return "" trusts = { validity.UNDEFINED: "Unknown", validity.NEVER: "Never", validity.MARGINAL: "Marginal", validity.FULL: "Full", validity.ULTIMATE: "Ultimate" } def validity2str(valid): "Convert trust integer into a human understandable string" if valid in trusts: return _("%s" % trusts[valid]) else: return _("Unknown") def keyvalid2str(key): "Create a string representing validity of a key" if key.owner_trust==validity.ULTIMATE: return _("Fully Valid") if key.revoked: return _("Revoked") if key.expired: return _("Expired") if key.disabled: return _("Disabled") if not key.uids or key.uids[0].invalid: return _("Incomplete") return _("Unknown") def subvalid2str(subkey): "Create a string representing validity of a subkey" if subkey.revoked: return _("Revoked") if subkey.expired: return _("Expired") if subkey.disabled: return _("Disabled") if subkey.invalid: return _("Unsigned") return _("Valid") def signstat2str(sig): "Create a string representing validity of a signature" status = _("Unknown") if sig.status == 1: status = _("Valid") elif sig.status == 2: status = _("Bad") if sig.expired: status = _("Expired") elif sig.revoked: status = _("Revoked") return status def sigsum2str(summary): if summary & sigsum.VALID: return _("Valid") if summary & sigsum.RED: return _("Bad") if summary & sigsum.KEY_MISSING: return _("Unknown Key") if summary & sigsum.KEY_REVOKED: return _("Revoked Key") if summary & sigsum.KEY_EXPIRED: return _("Expired Key") return _("Key NOT valid") def class2str(cls): "Convert signature class integer into a human understandable string" if cls==0x10: return _("Generic") if cls==0x11: return _("Persona") if cls==0x12: return _("Casual") if cls==0x13: return _("Positive") return _("Unknown") def algo2str(algo): "Convert algorithm integer into a human understandable string" return pubkey_algo_name(algo) def fpr2str(fpr): "Convert fpr string in a sparsely spaced string for easy reading" result = [] while fpr: result.append(fpr[:4]) fpr = fpr[4:] return " ".join(result) # Helper functions for tasks unrelated to any class def obj2model(objs, columns): "Create a model from the obj (key, subkey, or signature) using columns" model = gtk.ListStore(*[x.ctype for x in columns]) for obj in objs: model.append([x.cfunc(obj) for x in columns]) return model def labels2table(key_labels): "Create a gtk.Table from an array of 2-tuples of strings" table = gtk.Table(len(key_labels), 2) for i, row in enumerate(key_labels): if len(row) != 2: raise ValueError("Unexpected number of rows in labels2table call") label1 = gtk.Label(row[0]) label1.set_alignment(1.0, 0.5) label2 = gtk.Label(row[1]) label2.set_alignment(0.0, 0.5) label2.set_padding(5, 0) table.attach(label1, 0, 1, i, i+1, gtk.FILL, gtk.FILL) table.attach(label2, 1, 2, i, i+1, gtk.FILL, gtk.FILL) return table status2str = {} for name in dir(status): if not name.startswith('__') and name != "util": status2str[getattr(status, name)] = name def editor_func(status, args, val_dict): prompt = "%s %s" % (val_dict["state"], args) if prompt in val_dict: val_dict["state"] = val_dict[prompt][0] return val_dict[prompt][1] elif args and "ignore %s" % status2str[status] not in val_dict: for error in ["error %s" % status2str[status], "error %s" % prompt]: if error in val_dict: raise errors.GPGMEError(val_dict[error]) sys.stderr.write(_("Unexpected status and prompt in editor_func: " + "%s %s\n") % (status2str[status], prompt)) raise EOFError() return "" common_dict = { "state": "start", "quit keyedit.save.okay": ("save", "Y"), "ignore NEED_PASSPHRASE": None, "ignore NEED_PASSPHRASE_SYM": None, "ignore BAD_PASSPHRASE": None, "ignore USERID_HINT": None } def change_key_expire(context, key, date): "Change key's expiration date to date" val_dict = common_dict.copy() val_dict.update({ "start keyedit.prompt": ("expire", "expire"), "expire keygen.valid": ("date", date), "date keyedit.prompt": ("quit", "quit") }) out = Data() context.op_edit(key, editor_func, val_dict, out) def change_key_trust(context, key, new_trust): "Change key's trust to new_trust" val_dict = common_dict.copy() val_dict.update({ "start keyedit.prompt": ("trust", "trust"), "trust edit_ownertrust.value": ("value", "%d" % new_trust), "value edit_ownertrust.set_ultimate.okay": ("value", "Y"), "value keyedit.prompt": ("quit", "quit") }) out = Data() context.op_edit(key, editor_func, val_dict, out) def sign_key(context, key, sign_key, local): "Sign key using sign_key. Signature is exportable if local is False" # Values copied from GPG_ERR_CONFLICT = 70 GPG_ERR_UNUSABLE_PUBKEY = 53 val_dict = common_dict.copy() val_dict.update({ "start keyedit.prompt": ("sign", (local and "lsign") or "sign"), "sign keyedit.sign_all.okay": ("sign", "Y"), "sign sign_uid.expire": ("sign", "Y"), "sign sign_uid.class": ("sign", "0"), "sign sign_uid.okay": ("okay", "Y"), "okay keyedit.prompt": ("quit", "quit"), "error ALREADY_SIGNED": GPG_ERR_CONFLICT, "error sign keyedit.prompt": GPG_ERR_UNUSABLE_PUBKEY }) out = Data() context.signers_clear() context.signers_add(sign_key) context.op_edit(key, editor_func, val_dict, out) def trigger_change_password(context, key): "Trigger sequence of passphrase_cb callbacks to change password of the key" val_dict = common_dict.copy() val_dict.update({ "start keyedit.prompt": ("passwd", "passwd"), "passwd keyedit.prompt": ("quit", "quit") }) out = Data() context.op_edit(key, editor_func, val_dict, out) # Helper classes whose instances are used in the major PyGpa class class KeyInfo: """Helper class to represent key information in different views. If KeyInfo instance is initialized with an integer as a key the views correspond to a state when multiple or no keys are selected""" def __init__(self, key, secret=None): self.key = key self.secret = secret def key_print_labels(self, fpr=False): "Create an array of 2-tuples for 'User Name' and 'Key ID' fields" labels = [] if type(self.key) != int: if self.key.uids: labels.append((_("User Name:"), self.key.uids[0].uid)) for uid in self.key.uids[1:]: labels.append(("", uid.uid)) if fpr: labels += [(_("Fingerprint:"), fpr2str(self.key.subkeys[0].fpr))] else: labels += [(_("Key ID:"), self.key.subkeys[0].keyid[-8:])] return labels def key_expires_label(self): return sec2str(self.key.subkeys[0].expires,_("never expires")) def details(self): "Create a widget for 'Details' notebook tab" if type(self.key) == int: if self.key: details=gtk.Label(_("%d keys selected") % self.key) else: details=gtk.Label(_("No keys selected")) details.set_alignment(0.5, 0) return details if self.secret: header = _("The key has both a private and a public part") else: header = _("The key has only a public part") key_info_labels = [("", header)] if self.key.can_certify: if self.key.can_sign: if self.key.can_encrypt: ability = _("The key can be used for certification, " + "signing and encryption.") else: ability = _("The key can be used for certification and " + "signing, but not for encryption.") else: if self.key.can_encrypt: ability = _("The key can be used for certification and " + "encryption.") else: ability = _("The key can be used only for certification.") else: if self.key.can_sign: if self.key.can_encrypt: ability = _("The key can be used only for signing and " + "encryption, but not for certification.") else: ability = _("The key can be used only for signing.") else: if self.key.can_encrypt: ability = _("The key can be used only for encryption.") else: ability = _("This key is useless.") key_info_labels.append(("", ability)) key_info_labels += self.key_print_labels() + [ (_("Fingerprint:"), fpr2str(self.key.subkeys[0].fpr)), (_("Expires at:"), self.key_expires_label()), (_("Owner Trust:"), validity2str(self.key.owner_trust)), (_("Key Validity:"), keyvalid2str(self.key)), (_("Key Type:"), _("%s %u bits") % \ (algo2str(self.key.subkeys[0].pubkey_algo),self.key.subkeys[0].length)), (_("Created at:"), sec2str(self.key.subkeys[0].timestamp)) ] return labels2table(key_info_labels) def sign_model(self): "Create a model for ComboBox of uids in 'Signatures' notebook tab" model = gtk.ListStore(str, gtk.ListStore) if type(self.key) != int: for uid in self.key.uids: model.append([uid.uid, obj2model(uid.signatures,sign_columns)]) return model def subkey_model(self): "Create a model for TreeView in 'Subkeys' notebook tab" if type(self.key) == int: return gtk.ListStore(*[x.ctype for x in subkey_columns]) else: return obj2model(self.key.subkeys, subkey_columns) class Column: "Helper class to represent a column in a TreeView" def __init__(self, name, ctype, cfunc, detail=False): """Column(name, ctype, cfunc): name - Name to use as a column header ctype - type to use in a model definition for this column cfunc - function retrieving column's infromation from an object detail- indicate if it's a detail visible only in detailed view""" self.name = name self.ctype = ctype self.cfunc = cfunc self.detail = detail # Columns for the list of keys which can be used as default def_keys_columns = [ Column(_("Key ID"), str, lambda x,y: x.subkeys[0].keyid[-8:]), Column(_("User Name"), str, lambda x,y: (x.uids and x.uids[0].uid) or _("[Unknown user ID]")), Column(None, gobject.TYPE_PYOBJECT, lambda x,y: KeyInfo(x,y)) ] # Columns for the list of all keys in the keyring keys_columns = [ Column("", str, lambda x,y: (y and "sec") or "pub"), def_keys_columns[0], Column(_("Expiry Date"), str, lambda x,y: sec2str(x.subkeys[0].expires, _("never expires")), True), Column(_("Owner Trust"),str,lambda x,y:validity2str(x.owner_trust),True), Column(_("Key Validity"), str, lambda x,y: keyvalid2str(x), True) ] + def_keys_columns[1:] # Columns for the list of signatures on a uid sign_columns = [ Column(_("Key ID"), str, lambda x: x.keyid[-8:]), Column(_("Status"), str, lambda x: signstat2str(x)), Column(_("Level"), str, lambda x: class2str(x.sig_class)), Column(_("Local"), type(True), lambda x: x.exportable==0), Column(_("User Name"), str, lambda x: x.uid or _("[Unknown user ID]")) ] # Columns for the list of subkeys subkey_columns = [ Column(_("Subkey ID"), str, lambda x: x.keyid[-8:]), Column(_("Status"), str, lambda x: subvalid2str(x)), Column(_("Algorithm"), str, lambda x: algo2str(x.pubkey_algo)), Column(_("Size"), str, lambda x: _("%u bits") % x.length), Column(_("Expiry Date"), str, lambda x: sec2str(x.expires, _("never expires"))), Column(_("Can sign"), type(True), lambda x: x.can_sign), Column(_("Can certify"), type(True), lambda x: x.can_certify), Column(_("Can encrypt"), type(True), lambda x: x.can_encrypt), Column(_("Can authenticate"), type(True), lambda x: x.can_authenticate) ] file_columns = [ Column(_("File"), str, lambda x: x) ] class PyGpa: "Major class representing PyGpa application" def popup(self, dialog, parent=None, title=None): "Common way to popup a dialog defined in Glade" dialog.set_transient_for(parent or self.main_window) if title: dialog.set_title(title) result = dialog.run() dialog.hide() return result def file_popup(self, dialog, parent=None, title=None): return self.popup(dialog, parent or self.filemanager_window, title) def error_message(self, text, parent=None, title=_("Warning")): "Pop up an error message dialog" if type(text) == int: text = errors.GPGMEError(text).getstring() title = "GPGME error" elif isinstance(text, errors.GPGMEError): text = text.getstring() title = "GPGME error" self.error_label.set_text(text) self.popup(self.error_dialog, parent, title) def file_error_message(self, text, parent=None, title=_("Warning")): self.error_message(text, parent or self.filemanager_window, title) def info_message(self, text, parent=None, title=_("Information")): "Pop up an information dialog" self.info_label.set_text(text) self.popup(self.info_dialog, parent, title) def yesno_message(self, text, parent=None): "Pop up a dialog requiring yes/no answer" self.yesno_label.set_text(text) return self.popup(self.yesno_dialog, parent, _("Warning")) == gtk.RESPONSE_YES def on_uid_list_changed(self, uid_list): "this callback is called when uid selection is changed" index = uid_list.get_active() if index == -1: self.sign_treeview.set_model(KeyInfo(0).sign_model()) else: self.sign_treeview.set_model(uid_list.get_model()[index][1]) def get_selected_keys(self, treeview=None): "Helper function to get all selected rows in a treeview" if not treeview: treeview = self.keys_treeview model, rows = treeview.get_selection().get_selected_rows() return [model[path] for path in rows] def on_keys_changed(self, keys_treeview): "this callback is called when key selection is changed" selection = keys_treeview.get_selection() count = selection.count_selected_rows() if count == 1: key_info = self.get_selected_keys()[0][-1] else: key_info = KeyInfo(count) self.update_menu(key_info) # Update Details tab of the notebook old_child = self.details_view.get_child() if old_child: self.details_view.remove(old_child) self.details_view.add(key_info.details()) self.details_view.show_all() # Update Subkeys tab of the notebook self.subkeys_treeview.set_model(key_info.subkey_model()) # Update Signatures tab of the notebook sign_model = key_info.sign_model() self.uid_list.set_model(sign_model) if len(sign_model) < 2: self.uid_list_box.hide() else: self.uid_list_box.show_all() self.uid_list.set_active(0) self.on_uid_list_changed(self.uid_list) def on_keys_button_press(self, obj, event): "callback on a mouse press in the keys_treeview" if event.button == 3: self.popup_menu.popup(None, None, None, event.button, event.time) return True return False def create_popup_menu(self): "create the popup menu shown on right mouse click" self.items = [ (gtk.ImageMenuItem(gtk.STOCK_COPY), self.on_copy_activate), (gtk.ImageMenuItem(gtk.STOCK_PASTE), self.on_paste_activate), (gtk.ImageMenuItem(gtk.STOCK_DELETE), self.on_delete_activate), (gtk.SeparatorMenuItem(), None), (gtk.MenuItem(_("_Sign Keys...")), self.on_sign_keys_activate), (gtk.MenuItem(_("Set _Owner Trust...")), self.on_set_owner_trust_activate), (gtk.MenuItem(_("_Edit Private Key...")), self.on_edit_private_key_activate), (gtk.SeparatorMenuItem(), None), (gtk.MenuItem(_("E_xport Keys...")), self.on_export_keys_activate) ] self.popup_menu = gtk.Menu() for item, callback in self.items: if callback: item.connect("activate", callback) self.popup_menu.append(item) self.popup_menu.show_all() def update_menu(self, key_info): "update sensitivity of menu items depending on what keys are selected" # copy, delete, sign, trust, edit, export if key_info.secret == None: if key_info.key: # more than one key selected values = ( True, True, True, False, False, True) else: # no keys selected values = (False, False, False, False, False, False) elif key_info.secret: if key_info.key == self.default_key: # default key seleted values = ( True, True, False, True, True, True) else: # secret (not default) key selected values = ( True, True, True, True, True, True) else: # public key selected values = ( True, True, True, True, False, True) for w,v in zip((self.copy, self.delete, self.sign_keys, self.set_owner_trust, self.edit_private_key, self.export_keys), values): w.set_sensitive(v) for w,v in zip((self.items[0][0], self.items[2][0], self.items[4][0], self.items[5][0], self.items[6][0], self.items[8][0]), values): w.set_sensitive(v) def setup_columns(self): "Helper function to setup columns of different treeviews" for treeview, columns in \ [(self.keys_treeview, keys_columns), (self.sign_treeview, sign_columns), (self.subkeys_treeview, subkey_columns), (self.def_keys_treeview, def_keys_columns), (self.sign_with_keys_treeview, def_keys_columns), (self.encrypt_with_keys_treeview, def_keys_columns), (self.files_treeview, file_columns)]: for index, item in enumerate([x for x in columns if x.name!=None]): if item.ctype == str: renderer = gtk.CellRendererText() attrs = {"text": index} else: renderer = gtk.CellRendererToggle() attrs = {"active": index} column = treeview.insert_column_with_attributes( index, item.name, renderer, **attrs) column.set_sort_column_id(index) column.set_visible(not item.detail) for index,item in enumerate([x for x in keys_columns if x.name!=None]): if item.name and not item.detail: renderer = gtk.CellRendererText() column = gtk.TreeViewColumn(item.name, renderer, text=index) column.set_sort_column_id(index) self.encrypt_for_keys_treeview.append_column(column) for treeview in [self.encrypt_with_keys_treeview, self.keys_treeview, self.encrypt_for_keys_treeview, self.files_treeview, self.sign_with_keys_treeview]: treeview.get_selection().set_mode(gtk.SELECTION_MULTIPLE) self.def_keys_treeview.get_selection().set_mode(gtk.SELECTION_SINGLE) cell = gtk.CellRendererText() self.uid_list.pack_start(cell, True) self.uid_list.add_attribute(cell, 'text', 0) model = gtk.ListStore(str, str) for lines in [(_("days"), "d"), (_("weeks"), "w"), (_("months"), "m"), (_("years"), "y")]: model.append(lines) self.new_expire_unit_combo.set_model(model) self.new_expire_unit_combo.child.set_editable(False) self.new_algorithm_combo.child.set_editable(False) self.files_treeview.set_model(gtk.ListStore(str)) def setup_default_views(self): "Setup initial values for different views" self.update_default_keys() self.on_advanced_mode_toggled(self.advanced_mode_rb) self.create_popup_menu() self.on_keys_changed(self.keys_treeview) def load_keys(self): "Download keys from the keyring" context = Context() sec_keys = {} for key in context.op_keylist_all(None, 1): sec_keys[key.subkeys[0].fpr] = 1 model = gtk.ListStore(*[x.ctype for x in keys_columns]) encrypt_model = gtk.ListStore(*[x.ctype for x in keys_columns]) context.set_keylist_mode(keylist.mode.SIGS) for key in context.op_keylist_all(None, 0): secret = key.subkeys[0].fpr in sec_keys data = [x.cfunc(key, secret) for x in keys_columns] if key.can_encrypt: encrypt_model.append(data) model.append(data) self.keys_treeview.set_model(model) self.encrypt_for_keys_treeview.set_model(encrypt_model) def set_default_key(self, key): "Setup default key and update status bar with it" self.default_key = key self.status_uid.set_text((key.uids and key.uids[0].uid) or \ _("[Unknown user ID]")) self.status_keyid.set_text(key.subkeys[0].keyid[-8:]) def on_default_keys_changed(self, treeview): "This callback is called when default key is changed in Preferences" model, rows = treeview.get_selection().get_selected_rows() if model and rows: self.set_default_key(model[rows[0]][-1].key) def add_default_key(self, model, path, iter, def_model): "Helper function to add secret keys to the list of possible defaults" key = model[path][-1] if key.secret: def_model.append([x.cfunc(key.key,True) for x in def_keys_columns]) def add_sig_key(self, model, path, iter, sign_model): "Helper function to add secret keys to the list of possible defaults" key = model[path][-1].key if key.can_sign: sign_model.append([x.cfunc(key,True) for x in def_keys_columns]) def select_default_key(self, model, path, iter): "Helper function to select current default key from the available list" if model[path][-1].key == self.default_key: self.def_keys_treeview.get_selection().select_path(path) def update_default_keys(self): "Update list of default keys" model = gtk.ListStore(*[x.ctype for x in def_keys_columns]) self.keys_treeview.get_model().foreach(self.add_default_key, model) self.def_keys_treeview.set_model(model) model.foreach(self.select_default_key) selection = self.def_keys_treeview.get_selection() if selection.count_selected_rows() != 1: selection.select_path((0,)) self.on_default_keys_changed(self.def_keys_treeview) model = gtk.ListStore(*[x.ctype for x in def_keys_columns]) self.def_keys_treeview.get_model().foreach(self.add_sig_key, model) self.sign_with_keys_treeview.set_model(model) self.encrypt_with_keys_treeview.set_model(model) def on_select_all_activate(self, obj): "This callback is called when SelectAll menu item is selected" self.keys_treeview.get_selection().select_all() def on_file_preferences_activate(self, obj): "Callback called when Preferences menu item is selected in filemanager" self.show_preferences(self.filemanager_window) def on_preferences_activate(self, obj): "Callback called when Preferences menu item is selected in key editor" self.show_preferences(None) def show_preferences(self, parent): "Show preferences positioning its window in the middle of the parent" self.popup(self.preferences_dialog, parent) self.def_keyserver = self.default_keyserver_combox.child.get_text() def on_advanced_mode_toggled(self, radiobutton): "This callback is called when Advanced Mode selection is changed" if radiobutton.get_active(): self.subkeys_notebook_tab.show() self.get_generate_params = self.get_advanced_generate_params else: self.subkeys_notebook_tab.hide() self.get_generate_params = self.get_novice_generate_params def popup_progress_dialog(self, label, parent): self.progress_dialog.set_transient_for(parent) self.progress_label.set_text(label) self.progress_dialog.show_all() gobject.timeout_add(100, self.update_progress) def on_progress_cancel_clicked(self, obj): self.progress_context.cancel() def update_progress(self): "Helper function to show progress while a work on a key is being done" try: status = self.progress_context.wait(0) if status == None or self.progress_func(status): self.new_progressbar.pulse() return True except errors.GPGMEError as exc: self.error_message(exc) self.progress_context = None self.progress_func = None self.progress_dialog.hide() # Let callback to be removed. return False def key_generate_done(self, status): "Helper function called on the completion of a key generation" if status == 0: fpr = self.progress_context.op_genkey_result().fpr self.progress_context.set_keylist_mode(keylist.mode.SIGS) key = self.progress_context.get_key(fpr, 0) data = [x.cfunc(key, True) for x in keys_columns] self.keys_treeview.get_model().append(data) if key.can_encrypt: self.encrypt_for_keys_treeview.get_model().append(data) self.update_default_keys() else: self.error_message(status) return False def on_new_activate(self, obj): "Callback for 'New Key' menu item" params = self.get_generate_params() if params == None: return (key_algo, subkeys, size, userid, email, comment, expire, password, backup) = params gen_string = "\n" + \ "Key-Type: %s\n" % key_algo + \ "Key-Length: %d\n" % size if subkeys: gen_string += "Subkey-Type: %s\n" % subkeys + \ "Subkey-Length: %d\n" % size gen_string += "Name-Real: %s\n" % userid if email: gen_string += "Name-Email: %s\n" % email if comment: gen_string += "Name-Comment: %s\n" % comment if expire: gen_string += "Expire-Date: %s\n" % expire if password: gen_string += "Passphrase: %s\n" % password gen_string += "\n" self.progress_context = Context() self.progress_context.op_genkey_start(gen_string, None, None) self.progress_func = self.key_generate_done self.popup_progress_dialog(_("Generating Key..."), self.main_window) def check_passphrase(self, passphrase, repeat_passphrase, parent): """Helper function to check that enetered password satisfies our requirements""" if not passphrase: self.error_message(_('You did not enter a passphrase.\n' + 'It is needed to protect your private key.'), parent) elif repeat_passphrase != passphrase: self.error_message(_('In "Passphrase" and "Repeat passphrase",\n' + 'you must enter the same passphrase.'), parent) else: return True return False def get_novice_generate_params(self): "Helper function to get generate key parameter in Novice mode" dialogs = [self.generate_userid_dialog, self.generate_email_dialog, self.generate_comment_dialog, self.generate_passphrase_dialog, self.generate_backup_dialog] step = 0 params = None while step>=0: dialog = dialogs[step] dialog.set_transient_for(self.main_window) result = dialog.run() newstep = step if result == 2: if step == 0: userid = self.generate_novice_userid_entry.get_text() if userid: newstep = step + 1 else: self.error_message(_("Please insert your name.")) elif step == 1: email = self.generate_novice_email_entry.get_text() if email: newstep = step + 1 else: self.error_message(_("Please insert your email address")) elif step == 2: comment = self.generate_novice_comment_entry.get_text() newstep = step + 1 elif step == 3: passphrase=self.generate_novice_passphrase_entry.get_text() if self.check_passphrase( passphrase, self.generate_novice_repeat_passphrase_entry.get_text(), dialog): newstep = step + 1 elif step == 4: backup = self.generate_novice_backup_rb.get_active() params = ("DSA", "ELG-E", 1024, userid, email, comment, "", passphrase, backup) newstep = -1 elif result == 1: newstep = step - 1 else: newstep = -1 if newstep != step: dialog.hide() step = newstep self.generate_novice_userid_entry.set_text("") self.generate_novice_email_entry.set_text("") self.generate_novice_comment_entry.set_text("") self.generate_novice_passphrase_entry.set_text("") self.generate_novice_repeat_passphrase_entry.set_text("") return params def on_new_expire_on_rb_toggled(self, expireon_rb): self.new_expire_calendar.set_sensitive(expireon_rb.get_active()) def on_new_expire_after_rb_toggled(self, expireafter_rb): active = expireafter_rb.get_active() self.new_expire_count_entry.set_sensitive(active) self.new_expire_unit_combo.set_sensitive(active) def get_advanced_generate_params(self): "Helper function to get generate key parameter in Advanced mode" params = None self.new_expire_unit_combo.set_active(0) self.new_algorithm_combo.set_active(0) self.new_key_size_combo.set_active(1) self.generate_dialog.set_transient_for(self.main_window) while params == None and self.generate_dialog.run() == gtk.RESPONSE_OK: passphrase = self.new_passphrase_entry.get_text() if not self.check_passphrase( passphrase, self.new_repeat_passphrase_entry.get_text(), self.generate_dialog): continue key_algo, subkeys = { 'DSA and ElGamal (default)': ("DSA", "ELG-E"), 'DSA (sign only)': ("DSA", ""), 'RSA (sign only)': ("RSA", "") }[self.new_algorithm_combo.child.get_text()] try: size = int(self.new_key_size_combo.child.get_text()) except ValueError: self.new_key_size_combo.child.grab_focus() continue userid = self.new_userid_entry.get_text() email = self.new_email_entry.get_text() comment = self.new_comment_entry.get_text() expire = "" if self.new_expire_after_rb.get_active(): model = self.new_expire_unit_combo.get_model() unit = model[(self.new_expire_unit_combo.get_active(),)][1] try: value = int(self.new_expire_count_entry.get_text()) except ValueError: self.new_expire_count_entry.grab_focus() continue expire = "%d%s" % (value, unit) elif self.new_expire_on_rb.get_active(): (year, month, day) = self.new_expire_calendar.get_date() expire = "%04d-%02d-%02d" % (year, month+1, day) params = (key_algo, subkeys, size, userid, email, comment, expire, passphrase, False) self.generate_dialog.hide() self.new_passphrase_entry.set_text("") self.new_repeat_passphrase_entry.set_text("") return params def del_key(self, key, treeview): "Helper function to delete a key from a treeview list" row_list = [] treeview.get_model().foreach(lambda m,p,i,l: l.append(m[p]), row_list) for row in row_list: if row[-1].key.subkeys[0].fpr == key.subkeys[0].fpr: row.model.remove(row.iter) def on_delete_activate(self, obj): "Callback for 'Delete Keys' menu item" message = { True: _("This key has a secret key. Deleting this key cannot be"+ " undone, unless you have a backup copy."), False: _("This key is a public key. Deleting this key cannot be "+ "undone easily, although you may be able to get a new " + "copy from the owner or from a key server.") } keytag = self.delete_key_keyinfo for row in self.get_selected_keys(): self.delete_key_label.set_text(message[row[-1].secret]) table = labels2table(row[-1].key_print_labels()) keytag.add(table) keytag.show_all() if self.popup(self.delete_key_dialog) == gtk.RESPONSE_YES: context = Context() context.op_delete(row[-1].key, 1) if row[-1].key.can_encrypt: self.del_key(row[-1].key, self.encrypt_for_keys_treeview) row.model.remove(row.iter) self.update_default_keys() self.on_keys_changed(self.keys_treeview) keytag.remove(table) def password_cb(self, hint, desc, prev_bad, hook=None): "Callback to setup verification of a passphrase" if prev_bad: header = _("Wrong passphrase, please try again:") else: header = _("Please enter the passphrase for the following key:") self.password_prompt_label.set_text(header) keyid, userid = hint.split(" ", 1) table = labels2table([(_("User Name:"), userid), (_("Key ID:"), keyid[-8:])]) self.password_prompt_keyinfo.add(table) self.password_prompt_keyinfo.show_all() password = None if self.popup(self.password_prompt_dialog) == gtk.RESPONSE_OK: password = self.password_prompt_entry.get_text() self.password_prompt_keyinfo.remove(table) self.password_prompt_entry.set_text("") if not password: GPG_ERR_CANCELED = 99 raise errors.GPGMEError(GPG_ERR_CANCELED) return password def password_change_cb(self, hint, desc, prev_bad, hook): "Callback to setup for passphrase change" if not prev_bad: hook["count"] += 1 if hook["count"] == 1: return self.password_cb(hint, desc, prev_bad) else: password = None self.password_change_dialog.set_transient_for(self.main_window) while password == None and \ self.password_change_dialog.run() == gtk.RESPONSE_OK: password = self.password_change_passphrase.get_text() if not self.check_passphrase( password, self.password_change_repeat_passphrase.get_text(), self.password_change_dialog): password = None self.password_change_dialog.hide() self.password_change_passphrase.set_text("") self.password_change_repeat_passphrase.set_text("") if not password: GPG_ERR_CANCELED = 99 raise errors.GPGMEError(GPG_ERR_CANCELED) return password def on_sign_keys_activate(self, obj): "Callback for 'Sign keys' menu item" context = Context() context.set_passphrase_cb(self.password_cb) context.set_keylist_mode(keylist.mode.SIGS) keytag = self.sign_key_keyinfo for row in self.get_selected_keys(): if row[-1].key == self.default_key: continue if len(row[-1].key.uids) > 1: self.sign_manyuids_label.show() else: self.sign_manyuids_label.hide() table = labels2table(row[-1].key_print_labels(True)) keytag.add(table) keytag.show_all() if self.popup(self.sign_key_dialog) == gtk.RESPONSE_YES: try: sign_key(context, row[-1].key, self.default_key, self.sign_locally_cb.get_active()) row[-1].key=context.get_key(row[-1].key.subkeys[0].fpr,0) self.on_keys_changed(self.keys_treeview) except errors.GPGMEError as exc: self.error_message(exc) keytag.remove(table) def on_change_passphrase_clicked(self, obj, key_info): "Callback for 'Change passphrase' button in editor for a private key" try: context = Context() context.set_passphrase_cb(self.password_change_cb, {"count": 0}) trigger_change_password(context, key_info.key) except errors.GPGMEError as exc: self.error_message(exc) def on_change_expiry_expireon_rb_toggled(self, expire_rb): "Callback for 'never expire' radiobutton in editor for a private key" self.change_expiry_calendar.set_sensitive(expire_rb.get_active()) def on_change_expiration_clicked(self, obj, key_info): "Callback for 'Change expiration' button in editor for a private key" if key_info.key.subkeys[0].expires: year, month, day = time.localtime(key_info.key.subkeys[0].expires)[:3] self.change_expiry_calendar.select_month(month-1, year) self.change_expiry_calendar.select_day(day) self.change_expiry_expireon_rb.set_active(True) else: self.change_expiry_never_rb.set_active(True) if self.popup(self.change_expiry_dialog, self.edit_key_dialog) == gtk.RESPONSE_OK: year, month, day = self.change_expiry_calendar.get_date() expire = "%04d-%02d-%02d" % (year, month+1, day) try: context = Context() context.set_passphrase_cb(self.password_cb) change_key_expire(context, key_info.key, expire) context.set_keylist_mode(keylist.mode.SIGS) key_info.key=context.get_key(key_info.key.subkeys[0].fpr,0) self.on_keys_changed(self.keys_treeview) self.edit_key_date_label.set_text(key_info.key_expires_label()) except errors.GPGMEError as exc: self.error_message(exc) def on_edit_private_key_activate(self, obj): "Callback for 'Edit Private Key' menu item" keys = self.get_selected_keys() if len(keys) != 1 or not keys[0][-1].secret: return key_info = keys[0][-1] table = labels2table(key_info.key_print_labels()) self.edit_key_date_label.set_text(key_info.key_expires_label()) self.edit_key_keyinfo.add(table) self.edit_key_keyinfo.show_all() connect1_id = self.edit_key_change_expiration.connect( "clicked", self.on_change_expiration_clicked, key_info) connect2_id = self.edit_key_change_passphrase.connect( "clicked", self.on_change_passphrase_clicked, key_info) self.popup(self.edit_key_dialog) self.edit_key_change_expiration.disconnect(connect1_id) self.edit_key_change_passphrase.disconnect(connect2_id) self.edit_key_keyinfo.remove(table) def on_set_owner_trust_activate(self, obj): "Callback for 'Set Owner Trust' menu item" keys = self.get_selected_keys() if len(keys) != 1: return key_info = keys[0][-1] table = labels2table(key_info.key_print_labels()) self.ownertrust_key.add(table) self.ownertrust_key.show_all() trust = key_info.key.owner_trust if trust < 0 or trust not in trusts: trust = validity.UNDEFINED getattr(self, "ownertrust_"+trusts[trust]).set_active(True) if self.popup(self.ownertrust_dialog) == gtk.RESPONSE_OK: for trust, name in trusts.items(): if getattr(self, "ownertrust_"+name).get_active(): try: context = Context() change_key_trust(context, key_info.key, trust) key_info.key.owner_trust = trust self.on_keys_changed(self.keys_treeview) except errors.GPGMEError as exc: self.error_message(exc) break self.ownertrust_key.remove(table) def import_keys_from_data(self, data): "Helper function to import keys into application from a Data() object" context = Context() status = context.op_import(data) if status: self.error_message(status) else: result = context.op_import_result() if result.considered == 0: self.error_message(_("No keys were found.")) else: self.load_keys() self.info_message(_("%i public keys read\n" + "%i public keys imported\n" + "%i public keys unchanged\n" + "%i secret keys read\n" + "%i secret keys imported\n" + "%i secret keys unchanged") % \ (result.considered, result.imported, result.unchanged, result.secret_read, result.secret_imported, result.secret_unchanged)) def import_from_clipboard(self, clipboard, text, data): "Callback to setup extraction of data from a clipboard" if text: self.import_keys_from_data(Data(text)) def on_paste_activate(self, obj): "Callback for 'Paste' menu item" gtk.clipboard_get().request_text(self.import_from_clipboard) def on_import_keys_activate(self, obj): "Callback for 'Import Keys' menu item" import_file = None dialog = self.import_file_dialog dialog.set_transient_for(self.main_window) while import_file == None and dialog.run() == gtk.RESPONSE_OK: try: import_file = open(dialog.get_filename(), "rb") except IOError as strerror: self.error_message(strerror, dialog) import_file = None dialog.hide() if import_file != None: self.import_keys_from_data(Data(file=import_file)) import_file.close() def export_selected_keys(self, armor): "Helper function to export selected keys into a Data() object" context = Context() context.set_armor(armor) export_keys = Data() for row in self.get_selected_keys(): context.op_export(row[-1].key.subkeys[0].fpr, 0, export_keys) export_keys.seek(0,0) return export_keys def on_copy_activate(self, obj): "Callback for 'Copy' menu item" if self.keys_treeview.get_selection().count_selected_rows() > 0: export_keys = self.export_selected_keys(True) gtk.clipboard_get().set_text(export_keys.read()) def verify_output(self, filename, parent): "Helper function to verify that user can write into the filename" if os.path.exists(filename): if os.path.isdir(filename): self.error_message(_("%s is a directory")%filename, parent) return False else: return self.yesno_message(_("The file %s already exists.\n" + "Do you want to overwrite it?") % filename, parent) return True def on_export_keys_activate(self, obj): "Callback for 'Export Keys' menu item" if self.keys_treeview.get_selection().count_selected_rows() < 1: return export_file = None dialog = self.export_file_dialog dialog.set_transient_for(self.main_window) while export_file == None and dialog.run() == gtk.RESPONSE_OK: filename = dialog.get_filename() if self.verify_output(filename, dialog): try: export_file = open(filename, "wb") except IOError as strerror: self.error_message(strerror, dialog) export_file = None dialog.hide() if export_file == None: return export_keys = self.export_selected_keys(export_armor_cb.get_active()) export_file.write(export_keys.read()) export_file.close() def on_files_changed(self, obj): "Callback called when selection of files in filemanager is changed" if self.files_treeview.get_selection().count_selected_rows() < 1: value = False else: value = True for item in (self.sign, self.verify, self.encrypt, self.decrypt): item.set_sensitive(value) def open(self, filename, complain=False): "Helper function to add a file into filemanager treeview" model = self.files_treeview.get_model() row_list = [] model.foreach(lambda m,p,i,l: l.append(m[p][0]), row_list) if filename in row_list: if complain: self.file_error_message(_("The file is already open.")) else: item = model.append([filename]) self.files_treeview.get_selection().select_iter(item) self.on_files_changed(None) def on_open_activate(self, obj): "Callback for 'Open' menu item" if self.file_popup(self.open_file_dialog) == gtk.RESPONSE_OK: self.add_file(self.open_file_dialog.get_filename(), True) self.open_file_dialog.unselect_all() def get_selected_files(self): "Helper function to return selected rows in filemanager treeview" return self.get_selected_keys(self.files_treeview) def on_clear_activate(self, obj): "Callback for 'Clear' menu item" for row in self.get_selected_files(): row.model.remove(row.iter) def process_file_start(self, in_name, out_name): "Helper function to start asynchronous processing of one file" if self.verify_output(out_name, self.filemanager_window): try: self.in_data = Data(file=in_name) self.out_data = Data() self.out_name = out_name self.file_func(self.in_data, self.out_data) except errors.GPGMEError as exc: self.file_error_message(exc) def process_file_done(self, status): "The function called when asynchronous processing of the file is done." try: errors.errorcheck(status) self.out_data.seek(0,0) out_file = file(self.out_name, "wb") out_file.write(self.out_data.read()) out_file.close() self.add_file(self.out_name) if self.file_list: self.process_file_start(*(self.file_list.pop(0))) return True except (errors.GPGMEError, IOError) as exc: self.file_error_message(exc) # Let python to free the memory. self.out_data = None self.in_data = None self.out_name = None self.file_list = [] self.file_func = None return False def process_files_async(self, file_list, func, label): "Helper function to initialize async processing of the file list" self.file_list = file_list self.file_func = func self.progress_func = self.process_file_done self.process_file_start(*(self.file_list.pop(0))) self.popup_progress_dialog(label, self.filemanager_window) def on_sign_activate(self, obj): "Callback for 'Sign' menu item" files = self.get_selected_files() if not files: return if self.file_popup(self.sign_dialog) == gtk.RESPONSE_OK: context = Context() context.set_passphrase_cb(self.password_cb) context.set_armor(self.sign_armor_cb.get_active()) for rw in self.get_selected_keys(self.sign_with_keys_treeview): context.signers_add(rw[-1].key) for cb,md,ext in [(self.sign_normal, sig.mode.NORMAL, ".gpg"), (self.sign_clear, sig.mode.CLEAR, ".asc"), (self.sign_separate,sig.mode.DETACH,".sig")]: if cb.get_active(): sigmode = md sigext = ext break self.progress_context = context def sign(x,y):self.progress_context.op_sign_start(x,y,sigmode) self.process_files_async([(f[0], f[0]+sigext) for f in files], sign, _("Signing...")) def verify_file_start(self, in_name, out_name): "Helper function to start file signature verification process" try: self.in_name = in_name self.out_name = out_name self.signed = Data(file=self.in_name) if out_name: self.plain1 = Data(file=self.out_name) self.plain2 = None else: self.plain1 = None self.plain2 = Data() self.progress_context.op_verify_start(self.signed, self.plain1, self.plain2) except errors.GPGMEError as exc: self.file_error_message(exc) def verify_file_done(self, status): "The function called when asynchronous file signature verify is done." try: errors.errorcheck(status) result = self.progress_context.op_verify_result() model = gtk.ListStore(str, str, str) treeview = gtk.TreeView(model) treeview.set_rules_hint(True) for index, title in enumerate([_("Key ID"), _("Status"), _("User Name")]): treeview.append_column(gtk.TreeViewColumn( title, gtk.CellRendererText(), text=index)) for sign in result.signatures: key = self.progress_context.get_key(sign.fpr, 0) if key and key.uids: keyid = key.subkeys[0].keyid[-8:] userid = key.uids[0].uid else: keyid = sign.fpr[-8:] userid = _("[Unknown user ID]") model.append([keyid, sigsum2str(sign.summary), userid]) vbox = gtk.VBox() if self.out_name: vbox.add(gtk.Label(_("Verified data in file: %s") % self.out_name)) label = gtk.Label(_("Signatures:")) label.set_alignment(0, 1) vbox.add(label) vbox.add(treeview) self.verified.append((vbox, gtk.Label(self.in_name))) if self.file_list: self.verify_file_start(*(self.file_list.pop(0))) return True except errors.GPGMEError as exc: self.file_error_message(exc) # Let python to free the memory. self.signed = None self.plain1 = None self.plain2 = None self.in_name = None self.out_name = None self.file_list = [] self.progress_dialog.hide() if self.verified: notebook = gtk.Notebook() for page in self.verified: notebook.append_page(*page) self.verify_result.add(notebook) self.verify_result.show_all() self.file_popup(self.verify_dialog) self.verify_result.remove(notebook) self.verified = [] return False def on_verify_activate(self, obj): "Callback for 'Verify' menu item" files = self.get_selected_files() if not files: return self.file_list = [] for onefile in files: in_name = onefile[0] if in_name[-4:] == ".sig": out_name = in_name[:-4] elif in_name[-5:] == ".sign": out_name = in_name[:-5] else: out_name = None self.file_list.append((in_name, out_name)) self.verified = [] self.progress_context = Context() self.progress_func = self.verify_file_done self.verify_file_start(*(self.file_list.pop(0))) self.popup_progress_dialog(_("Verifying..."), self.filemanager_window) def on_encrypt_sign_toggled(self, cb): "Callback for change of the 'Sign' check box in 'Encrypt files' dialog" self.encrypt_with_keys_treeview.set_sensitive(cb.get_active()) def on_encrypt_activate(self, obj): "Callback for 'Encrypt' menu item" files = self.get_selected_files() if not files: return self.on_encrypt_sign_toggled(self.encrypt_sign_cb) if self.file_popup(self.encrypt_dialog) == gtk.RESPONSE_OK: context = Context() context.set_passphrase_cb(self.password_cb) if self.encrypt_armor_cb.get_active(): context.set_armor(True) ext = ".asc" else: context.set_armor(False) ext = ".gpg" keylist = [row[-1].key for row in self.get_selected_keys( self.encrypt_for_keys_treeview)] if self.encrypt_sign_cb.get_active(): for row in self.get_selected_keys( self.encrypt_with_keys_treeview): context.signers_add(row[-1].key) def encrypt(x,y): self.progress_context.op_encrypt_sign_start( keylist, 1, x, y) else: def encrypt(x,y): self.progress_context.op_encrypt_start( keylist, 1, x, y) self.progress_context = context self.process_files_async([(f[0], f[0]+sigext) for f in files], encrypt, _("Encrypting...")) def on_decrypt_activate(self, obj): "Callback for 'Decrypt' menu item" files = self.get_selected_files() if not files: return file_list = [] for onefile in self.get_selected_files(): in_name = onefile[0] if in_name[-4:] in [".asc", ".gpg", ".pgp"]: out_name = in_name[:-4] else: out_name = in_name + ".txt" file_list.append((in_name, out_name)) self.process_context = Context() self.process_files_async(file_list, self.process_context.op_decrypt_start, _("Decrypting...")) def on_select_all_files_activate(self, obj): "Callback for 'Select All' menu item in filemanager" self.files_treeview.get_selection().select_all() def on_keyring_editor_activate(self, obj): "Callback for 'Keyring Editor' menu item" self.main_window.show() def on_keyring_editor_close_activate(self, obj, event=None): "Callback for 'Close' menu item in Keyring Editor" if self.filemanager_window.get_property("visible"): self.main_window.hide() return True else: self.on_quit_activate(None) def on_filemanager_activate(self, obj): "Callback for 'Filemanager' menu item" self.on_files_changed(None) self.filemanager_window.show() def on_filemanager_close_activate(self, obj, event=None): "Callback for 'Close' menu item in Filemanager" if self.main_window.get_property("visible"): self.filemanager_window.hide() return True else: self.on_quit_activate(None) def on_about_activate(self, obj): "Callback for 'About' menu item" self.popup(self.about_dialog) def __repr__(self): return self.__class__.__name__ def __getattr__(self, name): "Dynamic retrieval of widgets from the glade XML" if name.startswith("on_"): self.__dict__[name] = lambda x: sys.stderr.write( _("Callback %s is not implimented yet\n") % name) elif name.startswith("_"): return None else: self.__dict__[name] = self.wtree.get_widget(name) return self.__dict__[name] def __init__(self, path): "PyGpa(path) - path is where pygpa.glade file can be found" gladefile = os.path.join(path, "pygpa.glade") self.wtree = gtk.glade.XML(gladefile, None, gtk.glade.textdomain()) self.wtree.signal_autoconnect(self) self.default_key = None self.load_keys() self.setup_columns() self.setup_default_views() self.in_progress = {} gtk.main() def on_quit_activate(self, obj): gtk.main_quit() PyGpa(os.path.dirname(sys.argv[0]))