ebd8734ad7
* The entirety of the Python 3 port of PyME up to commit 2145348ec54c6027f2ea20f695de0277e2871405 * The old commit log has been saved as lang/py3-pyme/docs/old-commits.log * Can be viewed as a normal (separate) git repository at https://github.com/adversary-org/pyme3 * Utilising the submodule feature of git was deliberately skipped on humanitarian grounds (in order to prevent pain and suffering on the part of anyone having to manage this repository).
1458 lines
60 KiB
Python
Executable File
1458 lines
60 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
# $Id$
|
|
# Copyright (C) 2005,2008 Igor Belyi <belyi@users.sourceforge.net>
|
|
#
|
|
# 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-error.h>
|
|
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 = "<GnupgKeyParms format=\"internal\">\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 += "</GnupgKeyParms>\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]))
|