diff --git a/apps/django_gpg/__init__.py b/apps/django_gpg/__init__.py new file mode 100644 index 0000000000..de9aeeba41 --- /dev/null +++ b/apps/django_gpg/__init__.py @@ -0,0 +1,19 @@ +from django.utils.translation import ugettext_lazy as _ + +from documents.models import Document +from navigation.api import register_links, register_top_menu, \ + register_model_list_columns, register_multi_item_links, \ + register_sidebar_template +from main.api import register_diagnostic, register_maintenance_links +from permissions.api import register_permission, set_namespace_title +from project_setup.api import register_setup + +PERMISSION_DOCUMENT_VERIFY = {'namespace': 'django_gpg', 'name': 'document_verify', 'label': _(u'Verify document signatures')} + +# Permission setup +set_namespace_title('django_gpg', _(u'Signatures')) +register_permission(PERMISSION_DOCUMENT_VERIFY) + +document_verify = {'text': _(u'Signatures'), 'view': 'document_verify', 'args': 'object.pk', 'famfam': 'text_signature', 'permissions': [PERMISSION_DOCUMENT_VERIFY]} + +register_links(Document, [document_verify], menu_name='form_header') diff --git a/apps/django_gpg/api.py b/apps/django_gpg/api.py new file mode 100644 index 0000000000..504d9ba664 --- /dev/null +++ b/apps/django_gpg/api.py @@ -0,0 +1,267 @@ +import types +from StringIO import StringIO +from pickle import dumps + +import gnupg + +from django.core.files.base import File +from django.utils.translation import ugettext_lazy as _ + +from django_gpg.exceptions import GPGVerificationError, GPGSigningError, \ + GPGDecryptionError, KeyDeleteError, KeyGenerationError, \ + KeyFetchingError, KeyDoesNotExist + + +KEY_TYPES = { + 'pub': _(u'Public'), + 'sec': _(u'Secret'), +} + +KEY_CLASS_RSA = 'RSA' +KEY_CLASS_DSA = 'DSA' +KEY_CLASS_ELG = 'ELG-E' + +KEY_PRIMARY_CLASSES = ( + ((KEY_CLASS_RSA), _(u'RSA')), + ((KEY_CLASS_DSA), _(u'DSA')), +) + +KEY_SECONDARY_CLASSES = ( + ((KEY_CLASS_RSA), _(u'RSA')), + ((KEY_CLASS_ELG), _(u'Elgamal')), +) + +class Key(object): + @staticmethod + def get_key_id(fingerprint): + return fingerprint[-16:] + + @classmethod + def get_all(cls, gpg, secret=False, exclude=None): + result = [] + keys = gpg.gpg.list_keys(secret=secret) + if exclude: + excluded_id = exclude.key_id + else: + excluded_id = u'' + for key in keys: + if not key['keyid'] in excluded_id: + key_instance = Key( + fingerprint=key['fingerprint'], + uids=key['uids'], + type=key['type'], + data=gpg.gpg.export_keys([key['keyid']], secret=secret) + ) + result.append(key_instance) + + return result + + @classmethod + def get(cls, gpg, key_id, secret=False, search_keyservers=False): + if len(key_id) > 16: + # key_id is a fingerprint + key_id = Key.get_key_id(key_id) + + keys = gpg.gpg.list_keys(secret=secret) + key = next((key for key in keys if key['keyid'] == key_id), None) + if not key: + if search_keyservers and secret==False: + try: + gpg.receive_key(key_id) + return Key(gpg, key_id) + except KeyFetchingError: + raise KeyDoesNotExist + else: + raise KeyDoesNotExist + + key_instance = Key( + fingerprint=key['fingerprint'], + uids=key['uids'], + type=key['type'], + data=gpg.gpg.export_keys([key['keyid']], secret=secret) + ) + + return key_instance + + def __init__(self, fingerprint, uids, type, data): + self.fingerprint = fingerprint + self.uids = uids + self.type = type + self.data = data + + @property + def key_id(self): + return Key.get_key_id(self.fingerprint) + + @property + def user_ids(self): + return u', '.join(self.uids) + + def __str__(self): + return '%s "%s" (%s)' % (self.key_id, self.user_ids, KEY_TYPES.get(self.type, _(u'unknown'))) + + def __unicode__(self): + return unicode(self.__str__()) + + def __repr__(self): + return self.__unicode__() + + +class GPG(object): + def __init__(self, binary_path=None, home=None, keyring=None, keyservers=None): + kwargs = {} + if binary_path: + kwargs['gpgbinary'] = binary_path + + if home: + kwargs['gnupghome'] = home + + if keyring: + kwargs['keyring'] = keyring + + self.keyservers = keyservers + + self.gpg = gnupg.GPG(**kwargs) + + def verify_w_retry(self, file_input): + if isinstance(file_input, types.StringTypes): + input_descriptor = open(file_input, 'rb') + elif isinstance(file_input, types.FileType) or isinstance(file_input, File): + input_descriptor = file_input + elif issubclass(file_input.__class__, StringIO): + input_descriptor = file_input + else: + raise ValueError('Invalid file_input argument type') + + try: + verify = self.verify_file(input_descriptor) + if verify.status == 'no public key': + # Try to fetch the public key from the keyservers + try: + self.receive_key(verify.key_id) + return self.verify_w_retry(file_input) + except KeyFetchingError: + return verify + else: + return verify + except IOError: + return False + + def verify_file(self, file_input): + """ + Verify the signature of a file. + """ + if isinstance(file_input, types.StringTypes): + descriptor = open(file_input, 'rb') + elif isinstance(file_input, types.FileType) or isinstance(file_input, File) or isinstance(file_input, StringIO): + descriptor = file_input + else: + raise ValueError('Invalid file_input argument type') + + verify = self.gpg.verify_file(descriptor) + descriptor.close() + + if verify: + return verify + elif getattr(verify, 'status', None) == 'no public key': + # Exception to the rule, to be able to query the keyservers + return verify + else: + raise GPGVerificationError() + + def verify(self, data): + # TODO: try to merge with verify_file + verify = self.gpg.verify(data) + + if verify: + return verify + else: + raise GPGVerificationError(verify.status) + + def sign_file(self, file_input, key=None, destination=None, key_id=None, passphrase=None, clearsign=False): + """ + Signs a filename, storing the signature and the original file + in the destination filename provided (the destination file is + overrided if it already exists), if no destination file name is + provided the signature is returned. + """ + kwargs = {} + kwargs['clearsign'] = clearsign + + if key_id: + kwargs['keyid'] = key_id + + if key: + kwargs['keyid'] = key.key_id + + if passphrase: + kwargs['passphrase'] = passphrase + + if isinstance(file_input, types.StringTypes): + input_descriptor = open(file_input, 'rb') + elif isinstance(file_input, types.FileType) or isinstance(file_input, File): + input_descriptor = file_input + elif issubclass(file_input.__class__, StringIO): + input_descriptor = file_input + else: + raise ValueError('Invalid file_input argument type') + + if destination: + output_descriptor = open(destination, 'wb') + + signed_data = self.gpg.sign_file(input_descriptor, **kwargs) + if not signed_data.fingerprint: + raise GPGSigningError('Unable to sign file') + + if destination: + output_descriptor.write(signed_data.data) + + input_descriptor.close() + + if destination: + output_descriptor.close() + + if not destination: + return signed_data + + def decrypt_file(self, file_input): + if isinstance(file_input, types.StringTypes): + input_descriptor = open(file_input, 'rb') + elif isinstance(file_input, types.FileType) or isinstance(file_input, File) or isinstance(file_input, StringIO): + input_descriptor = file_input + else: + raise ValueError('Invalid file_input argument type') + + result = self.gpg.decrypt_file(input_descriptor) + input_descriptor.close() + if not result.status: + raise GPGDecryptionError('Unable to decrypt file') + + return result + + def create_key(self, *args, **kwargs): + if kwargs.get('passphrase') == u'': + kwargs.pop('passphrase') + + input_data = self.gpg.gen_key_input(**kwargs) + key = self.gpg.gen_key(input_data) + if not key: + raise KeyGenerationError('Unable to generate key') + + return Key.get(self, key.fingerprint) + + def delete_key(self, key): + status = self.gpg.delete_keys(key.fingerprint, key.type == 'sec').status + if status == 'Must delete secret key first': + self.delete_key(Key.get(self, key.fingerprint, secret=True)) + self.delete_key(key) + elif status != 'ok': + raise KeyDeleteError('Unable to delete key') + + def receive_key(self, key_id): + for keyserver in self.keyservers: + import_result = self.gpg.recv_keys(keyserver, key_id) + if import_result: + return Key.get(self, import_result.fingerprints[0], secret=False) + + raise KeyFetchingError diff --git a/apps/django_gpg/conf/__init__.py b/apps/django_gpg/conf/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/django_gpg/conf/settings.py b/apps/django_gpg/conf/settings.py new file mode 100644 index 0000000000..ac9256396a --- /dev/null +++ b/apps/django_gpg/conf/settings.py @@ -0,0 +1,15 @@ +''' +Configuration options for the django_gpg app +''' + +from django.utils.translation import ugettext_lazy as _ + +from smart_settings.api import register_settings + +register_settings( + namespace=u'django_gpg', + module=u'django_gpg.conf.settings', + settings=[ + {'name': u'KEYSERVERS', 'global_name': u'SIGNATURES_KEYSERVERS', 'default': ['keyserver.ubuntu.com'], 'description': _(u'List of keyservers to be queried for unknown keys.')}, + ] +) diff --git a/apps/django_gpg/exceptions.py b/apps/django_gpg/exceptions.py new file mode 100644 index 0000000000..682ad8f795 --- /dev/null +++ b/apps/django_gpg/exceptions.py @@ -0,0 +1,31 @@ +class GPGException(Exception): + pass + + +class GPGVerificationError(GPGException): + pass + + +class GPGSigningError(GPGException): + pass + + +class GPGDecryptionError(GPGException): + pass + + +class KeyDeleteError(GPGException): + pass + + +class KeyGenerationError(GPGException): + pass + + +class KeyFetchingError(GPGException): + pass + + +class KeyDoesNotExist(GPGException): + pass + diff --git a/apps/django_gpg/locale/en/LC_MESSAGES/django.po b/apps/django_gpg/locale/en/LC_MESSAGES/django.po new file mode 100644 index 0000000000..1192c23608 --- /dev/null +++ b/apps/django_gpg/locale/en/LC_MESSAGES/django.po @@ -0,0 +1,203 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2011-11-28 05:18-0400\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: api.py:18 +msgid "Public" +msgstr "" + +#: api.py:19 +msgid "Secret" +msgstr "" + +#: api.py:27 api.py:32 +msgid "RSA" +msgstr "" + +#: api.py:28 +msgid "DSA" +msgstr "" + +#: api.py:33 +msgid "Elgamal" +msgstr "" + +#: api.py:89 +msgid "unknown" +msgstr "" + +#: forms.py:12 +msgid "Real name" +msgstr "" + +#: forms.py:13 +msgid "Your real name." +msgstr "" + +#: forms.py:17 +msgid "Comment" +msgstr "" + +#: forms.py:19 +msgid "A comment or a note to help identify this key." +msgstr "" + +#: forms.py:23 +msgid "Email" +msgstr "" + +#: forms.py:28 +msgid "Primary key class" +msgstr "" + +#: forms.py:29 +msgid "The key that will be used to sign uploaded content." +msgstr "" + +#: forms.py:33 +msgid "Primary key size (in bytes)" +msgstr "" + +#: forms.py:41 +msgid "Secondary key class" +msgstr "" + +#: forms.py:42 +msgid "The key that will be used to encrypt uploaded content." +msgstr "" + +#: forms.py:46 +msgid "Secondary key size (in bytes)" +msgstr "" + +#: forms.py:53 +msgid "Expiration" +msgstr "" + +#: forms.py:54 +msgid "" +"You can use 0 for a non expiring key, an ISO date in the form: -" +"- or a date difference from the current date in the forms: " +"d, m, w or y." +msgstr "" + +#: forms.py:59 +msgid "Passphrase" +msgstr "" + +#: forms.py:65 +msgid "Passphrase (verification)" +msgstr "" + +#: forms.py:72 +msgid "Both passphrase fields entries must match." +msgstr "" + +#: forms.py:80 +msgid "Key" +msgstr "" + +#: forms.py:81 +msgid "Key to be published, only the public part of the key will be sent." +msgstr "" + +#: tasks.py:27 +#, python-format +msgid "Key pair: %s, created successfully." +msgstr "" + +#: tasks.py:34 +#, python-format +msgid "Key creation error; %s" +msgstr "" + +#: views.py:27 +msgid "Private key list" +msgstr "" + +#: views.py:30 +msgid "Public key list" +msgstr "" + +#: views.py:54 +msgid "Key pair queued for creation, refresh this page to check results." +msgstr "" + +#: views.py:64 +msgid "Create a new key" +msgstr "" + +#: views.py:65 +msgid "" +"The key creation process can take quite some time to complete, please be " +"patient." +msgstr "" + +#: views.py:75 +#, python-format +msgid "Key: %s, deleted successfully." +msgstr "" + +#: views.py:82 +msgid "Delete key" +msgstr "" + +#: views.py:83 +#, python-format +msgid "" +"Are you sure you wish to delete key:%s? If you try to delete a public key " +"that is part of a public/private pair the private key will be deleted as " +"well." +msgstr "" + +#: views.py:95 +#, python-format +msgid "Key publish request for key: %s, has been sent" +msgstr "" + +#: views.py:98 +msgid "Unable to send key publish call" +msgstr "" + +#: views.py:105 +msgid "Publish a key to the OpenRelay network" +msgstr "" + +#: templates/key_list.html:10 +msgid "ID" +msgstr "" + +#: templates/key_list.html:11 +msgid "User IDs" +msgstr "" + +#: templates/key_list.html:12 +msgid "Fingerprint" +msgstr "" + +#: templates/key_list.html:13 +msgid "Links" +msgstr "" + +#: templates/key_list.html:22 +msgid "Delete" +msgstr "" + +#: templates/key_list.html:26 +msgid "There are no keys available." +msgstr "" diff --git a/apps/django_gpg/locale/es/LC_MESSAGES/django.mo b/apps/django_gpg/locale/es/LC_MESSAGES/django.mo new file mode 100644 index 0000000000..502545e74a Binary files /dev/null and b/apps/django_gpg/locale/es/LC_MESSAGES/django.mo differ diff --git a/apps/django_gpg/locale/es/LC_MESSAGES/django.po b/apps/django_gpg/locale/es/LC_MESSAGES/django.po new file mode 100644 index 0000000000..dcc0e1eeab --- /dev/null +++ b/apps/django_gpg/locale/es/LC_MESSAGES/django.po @@ -0,0 +1,217 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +# Roberto Rosario , 2011. +msgid "" +msgstr "" +"Project-Id-Version: OpenRelay\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2011-11-28 05:18-0400\n" +"PO-Revision-Date: 2011-11-28 09:25+0000\n" +"Last-Translator: rosarior \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: es\n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" + +#: api.py:18 +msgid "Public" +msgstr "Pública" + +#: api.py:19 +msgid "Secret" +msgstr "Secreta" + +#: api.py:27 api.py:32 +msgid "RSA" +msgstr "RSA" + +#: api.py:28 +msgid "DSA" +msgstr "DSA" + +#: api.py:33 +msgid "Elgamal" +msgstr "Elgamal" + +#: api.py:89 +msgid "unknown" +msgstr "desconocido" + +#: forms.py:12 +msgid "Real name" +msgstr "Nombre real" + +#: forms.py:13 +msgid "Your real name." +msgstr "Su nombre real." + +#: forms.py:17 +msgid "Comment" +msgstr "Comentario" + +#: forms.py:19 +msgid "A comment or a note to help identify this key." +msgstr "Un comentario o una nota para ayudar a identificar esta llave." + +#: forms.py:23 +msgid "Email" +msgstr "E-mail" + +#: forms.py:28 +msgid "Primary key class" +msgstr "Clase de llave primaria" + +#: forms.py:29 +msgid "The key that will be used to sign uploaded content." +msgstr "La llave que se utilizara para firmar el contenido subido." + +#: forms.py:33 +msgid "Primary key size (in bytes)" +msgstr "Tamaño de la llave principal (en bytes)" + +#: forms.py:41 +msgid "Secondary key class" +msgstr "Clase de llave secundaria" + +#: forms.py:42 +msgid "The key that will be used to encrypt uploaded content." +msgstr "La llave que se utilizara para cifrar el contenido subido." + +#: forms.py:46 +msgid "Secondary key size (in bytes)" +msgstr "Tamaño de la llave secundaria (en bytes)" + +#: forms.py:53 +msgid "Expiration" +msgstr "Expiración" + +#: forms.py:54 +msgid "" +"You can use 0 for a non expiring key, an ISO date in the form: " +"-- or a date difference from the current date in the " +"forms: d, m, w or y." +msgstr "" +"Usted puede utilizar 0 para llaves que no expiran, una fecha ISO en la " +"forma: -- o una diferencia de fecha con respecto a la fecha " +"actual en las formas: d, m, w o y." + +#: forms.py:59 +msgid "Passphrase" +msgstr "Frase de contraseña" + +#: forms.py:65 +msgid "Passphrase (verification)" +msgstr "Contraseña (verificación)" + +#: forms.py:72 +msgid "Both passphrase fields entries must match." +msgstr "Las entradas de los campos de contraseña deben coincidir." + +#: forms.py:80 +msgid "Key" +msgstr "Llave" + +#: forms.py:81 +msgid "Key to be published, only the public part of the key will be sent." +msgstr "" +"La llave para ser publicada, sólo la parte pública de la llave será enviada." + +#: tasks.py:27 +#, python-format +msgid "Key pair: %s, created successfully." +msgstr "Par de llaves: %s, creado correctamente." + +#: tasks.py:34 +#, python-format +msgid "Key creation error; %s" +msgstr "Error de creación de llave; %s" + +#: views.py:27 +msgid "Private key list" +msgstr "Lista de llaves privadas" + +#: views.py:30 +msgid "Public key list" +msgstr "Lista de llaves públicas" + +#: views.py:54 +msgid "Key pair queued for creation, refresh this page to check results." +msgstr "" +"Par de llaves en lista de creación, vuelva a cargar esta página " +"periodicamente para comprobar los resultados." + +#: views.py:64 +msgid "Create a new key" +msgstr "Crear una llave nueva" + +#: views.py:65 +msgid "" +"The key creation process can take quite some time to complete, please be " +"patient." +msgstr "" +"El proceso de creación de la llaves puede tomar bastante tiempo en " +"completar, por favor, sea paciente." + +#: views.py:75 +#, python-format +msgid "Key: %s, deleted successfully." +msgstr "Llave: %s, eliminada exitosamente." + +#: views.py:82 +msgid "Delete key" +msgstr "Eliminar la llave" + +#: views.py:83 +#, python-format +msgid "" +"Are you sure you wish to delete key:%s? If you try to delete a public key " +"that is part of a public/private pair the private key will be deleted as " +"well." +msgstr "" +"¿Está seguro que desea eliminar la llave: %s? Si intenta eliminar una llave" +" pública que forma parte de una pareja pública / privada de la llave privada" +" se eliminarán también." + +#: views.py:95 +#, python-format +msgid "Key publish request for key: %s, has been sent" +msgstr "Solicitud publicación de llave: %s, ha sido enviada" + +#: views.py:98 +msgid "Unable to send key publish call" +msgstr "No se puede enviar llave publica" + +#: views.py:105 +msgid "Publish a key to the OpenRelay network" +msgstr "Publicar una llave a la red OpenRelay" + +#: templates/key_list.html:10 +msgid "ID" +msgstr "Identificador" + +#: templates/key_list.html:11 +msgid "User IDs" +msgstr "ID de usuarios" + +#: templates/key_list.html:12 +msgid "Fingerprint" +msgstr "Huella digital" + +#: templates/key_list.html:13 +msgid "Links" +msgstr "Enlaces" + +#: templates/key_list.html:22 +msgid "Delete" +msgstr "Eliminar" + +#: templates/key_list.html:26 +msgid "There are no keys available." +msgstr "No hay llaves disponibles." + + diff --git a/apps/django_gpg/models.py b/apps/django_gpg/models.py new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/apps/django_gpg/models.py @@ -0,0 +1 @@ + diff --git a/apps/django_gpg/runtime.py b/apps/django_gpg/runtime.py new file mode 100644 index 0000000000..b3303059ff --- /dev/null +++ b/apps/django_gpg/runtime.py @@ -0,0 +1,4 @@ +from django_gpg.api import GPG +from django_gpg.conf.settings import KEYSERVERS + +gpg = GPG(keyservers=KEYSERVERS) diff --git a/apps/django_gpg/static/images/icons/cross.png b/apps/django_gpg/static/images/icons/cross.png new file mode 100644 index 0000000000..4ee1253736 Binary files /dev/null and b/apps/django_gpg/static/images/icons/cross.png differ diff --git a/apps/django_gpg/static/images/icons/document_signature.png b/apps/django_gpg/static/images/icons/document_signature.png new file mode 100644 index 0000000000..60155cf1ba Binary files /dev/null and b/apps/django_gpg/static/images/icons/document_signature.png differ diff --git a/apps/django_gpg/static/images/icons/user_silhouette.png b/apps/django_gpg/static/images/icons/user_silhouette.png new file mode 100644 index 0000000000..109d113eec Binary files /dev/null and b/apps/django_gpg/static/images/icons/user_silhouette.png differ diff --git a/apps/django_gpg/urls.py b/apps/django_gpg/urls.py new file mode 100644 index 0000000000..d274237795 --- /dev/null +++ b/apps/django_gpg/urls.py @@ -0,0 +1,8 @@ +from django.conf.urls.defaults import patterns, url + +urlpatterns = patterns('django_gpg.views', + url(r'^delete/(?P.+)/(?P\w+)/$', 'key_delete', (), 'key_delete'), + url(r'^list/private/$', 'key_list', {'secret': True}, 'key_private_list'), + url(r'^list/public/$', 'key_list', {'secret': False}, 'key_public_list'), + url(r'^verify/(?P\d+)/$', 'document_verify', (), 'document_verify'), +) diff --git a/apps/django_gpg/views.py b/apps/django_gpg/views.py new file mode 100644 index 0000000000..9f23d45004 --- /dev/null +++ b/apps/django_gpg/views.py @@ -0,0 +1,113 @@ +from datetime import datetime + +from django.utils.translation import ugettext_lazy as _ +from django.http import HttpResponseRedirect +from django.shortcuts import render_to_response, get_object_or_404 +from django.template import RequestContext +from django.contrib import messages +from django.core.urlresolvers import reverse +from django.utils.safestring import mark_safe +from django.conf import settings +from django.template.defaultfilters import force_escape + +from documents.models import Document, RecentDocument +from permissions.api import check_permissions + +from django_gpg.api import Key +from django_gpg.runtime import gpg +from django_gpg.exceptions import GPGVerificationError +from django_gpg import PERMISSION_DOCUMENT_VERIFY + + +def key_list(request, secret=True): + if secret: + object_list = Key.get_all(gpg, secret=True) + title = _(u'Private key list') + else: + object_list = Key.get_all(gpg) + title = _(u'Public key list') + + return render_to_response('key_list.html', { + 'object_list': object_list, + 'title': title, + }, context_instance=RequestContext(request)) + + +def key_delete(request, fingerprint, key_type): + if request.method == 'POST': + try: + secret = key_type == 'sec' + key = Key.get(gpg, fingerprint, secret=secret) + gpg.delete_key(key) + messages.success(request, _(u'Key: %s, deleted successfully.') % fingerprint) + return HttpResponseRedirect(reverse('home_view')) + except Exception, msg: + messages.error(request, msg) + return HttpResponseRedirect(reverse('home_view')) + + return render_to_response('generic_confirm.html', { + 'title': _(u'Delete key'), + 'message': _(u'Are you sure you wish to delete key:%s? If you try to delete a public key that is part of a public/private pair the private key will be deleted as well.') % Key.get(gpg, fingerprint) + }, context_instance=RequestContext(request)) + + +def document_verify(request, document_pk): + check_permissions(request.user, [PERMISSION_DOCUMENT_VERIFY]) + document = get_object_or_404(Document, pk=document_pk) + + RecentDocument.objects.add_document_for_user(request.user, document) + try: + signature = gpg.verify_w_retry(document.open()) + except GPGVerificationError: + signature = None + + signature_states = { + 'signature bad': { + 'text': _(u'Bad signature.'), + 'icon': 'cross.png' + }, + None: { + 'text': _(u'Document not signed or invalid signature.'), + 'icon': 'cross.png' + }, + 'signature error': { + 'text': _(u'Signature error.'), + 'icon': 'cross.png' + }, + 'no public key': { + 'text': _(u'Document is signed but no public key is available for verification.'), + 'icon': 'user_silhouette.png' + }, + 'signature good': { + 'text': _(u'Document is signed, and signature is good.'), + 'icon': 'document_signature.png' + }, + 'signature valid': { + 'text': _(u'Document is signed with a valid signature.'), + 'icon': 'document_signature.png' + }, + } + + signature_state = signature_states.get(getattr(signature, 'status', None)) + + widget = (u'' % (settings.STATIC_URL, signature_state['icon'])) + paragraphs = [ + _(u'Signature status: %s %s') % (mark_safe(widget), signature_state['text']), + ] + + if signature: + paragraphs.extend( + [ + _(u'Signature ID: %s') % signature.signature_id, + _(u'Key ID: %s') % signature.key_id, + _(u'Timestamp: %s') % datetime.fromtimestamp(int(signature.sig_timestamp)), + _(u'Signee: %s') % force_escape(getattr(signature, 'username', u'')), + ] + ) + + return render_to_response('generic_template.html', { + 'title': _(u'signature properties for: %s') % document, + 'object': document, + 'document': document, + 'paragraphs': paragraphs, + }, context_instance=RequestContext(request)) diff --git a/apps/documents/views.py b/apps/documents/views.py index fdd98922c6..2f44085934 100644 --- a/apps/documents/views.py +++ b/apps/documents/views.py @@ -1155,6 +1155,8 @@ def document_version_list(request, document_pk): check_permissions(request.user, [PERMISSION_DOCUMENT_VIEW]) document = get_object_or_404(Document, pk=document_pk) + RecentDocument.objects.add_document_for_user(request.user, document) + context = { 'object_list': document.versions.order_by('-timestamp'), 'title': _(u'versions for document: %s') % document, diff --git a/requirements/development.txt b/requirements/development.txt index 7dc2ed08bc..ad042365ab 100644 --- a/requirements/development.txt +++ b/requirements/development.txt @@ -20,3 +20,4 @@ django-compressor==1.1 -e git://github.com/rosarior/django-sendfile.git#egg=django-sendfile djangorestframework==0.2.3 South==0.7.3 +python-gnupg==0.2.8 diff --git a/requirements/production.txt b/requirements/production.txt index d0438944d1..9c1b2eae70 100644 --- a/requirements/production.txt +++ b/requirements/production.txt @@ -17,3 +17,4 @@ django-compressor==1.1 -e git://github.com/rosarior/django-sendfile.git#egg=django-sendfile djangorestframework==0.2.3 South==0.7.3 +python-gnupg==0.2.8 diff --git a/settings.py b/settings.py index c31e499fb0..25f316a74c 100644 --- a/settings.py +++ b/settings.py @@ -133,6 +133,7 @@ INSTALLED_APPS = ( 'lock_manager', 'web_theme', 'common', + 'django_gpg', 'pagination', 'dynamic_search', 'filetransfers', diff --git a/urls.py b/urls.py index 38ab8788b2..a4ccd38cef 100644 --- a/urls.py +++ b/urls.py @@ -28,6 +28,7 @@ urlpatterns = patterns('', (r'^project_setup/', include('project_setup.urls')), (r'^project_tools/', include('project_tools.urls')), (r'^api/', include('rest_api.urls')), + (r'^signatures/', include('django_gpg.urls')), )