1458 lines
60 KiB
Python
1458 lines
60 KiB
Python
|
#!/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]))
|