From 071139c5bf9733c2611f1b0e78a25960d4223dcf Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sun, 4 Dec 2011 05:52:09 -0400 Subject: [PATCH] Add document signature verification app --- apps/django_gpg/__init__.py | 19 ++ apps/django_gpg/api.py | 267 ++++++++++++++++++ apps/django_gpg/conf/__init__.py | 0 apps/django_gpg/conf/settings.py | 15 + apps/django_gpg/exceptions.py | 31 ++ .../locale/en/LC_MESSAGES/django.po | 203 +++++++++++++ .../locale/es/LC_MESSAGES/django.mo | Bin 0 -> 4289 bytes .../locale/es/LC_MESSAGES/django.po | 217 ++++++++++++++ apps/django_gpg/models.py | 1 + apps/django_gpg/runtime.py | 4 + apps/django_gpg/static/images/icons/cross.png | Bin 0 -> 1049 bytes .../images/icons/document_signature.png | Bin 0 -> 1598 bytes .../static/images/icons/user_silhouette.png | Bin 0 -> 1087 bytes apps/django_gpg/urls.py | 8 + apps/django_gpg/views.py | 113 ++++++++ apps/documents/views.py | 2 + requirements/development.txt | 1 + requirements/production.txt | 1 + settings.py | 1 + urls.py | 1 + 20 files changed, 884 insertions(+) create mode 100644 apps/django_gpg/__init__.py create mode 100644 apps/django_gpg/api.py create mode 100644 apps/django_gpg/conf/__init__.py create mode 100644 apps/django_gpg/conf/settings.py create mode 100644 apps/django_gpg/exceptions.py create mode 100644 apps/django_gpg/locale/en/LC_MESSAGES/django.po create mode 100644 apps/django_gpg/locale/es/LC_MESSAGES/django.mo create mode 100644 apps/django_gpg/locale/es/LC_MESSAGES/django.po create mode 100644 apps/django_gpg/models.py create mode 100644 apps/django_gpg/runtime.py create mode 100644 apps/django_gpg/static/images/icons/cross.png create mode 100644 apps/django_gpg/static/images/icons/document_signature.png create mode 100644 apps/django_gpg/static/images/icons/user_silhouette.png create mode 100644 apps/django_gpg/urls.py create mode 100644 apps/django_gpg/views.py 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 0000000000000000000000000000000000000000..502545e74a86acadccce3447f66799e3239dca5a GIT binary patch literal 4289 zcmai$O^h5z6~_y32rT5oAp!GU0qoex%y?}QFyna1+FpkhYkTcogE(-g>8_crY)^N) zs(aUCA#vn}P^1WP0O3dogV5p*d;Q0U_PwAO9j=dYed8TUodn-_Cx49JzDubd_!{^<@L%BL;Pd;Gx)=Nf z_#pTjkXPyt;Jd-s!8!0vP}cnwya)VeJO6L+eLUa8q#U&$l>HBb9|RA8i{Mf4)8JK5 z^mqY04gMPZ1b8opajOo2a$X1gF!*_JKX?iJD42kv=Qlxq>O1^B0Dc!d1pXM5^M41v z7rX_20DJ@dIQS2cpSmAoE`g7Lp94qWXTcwU;=fzqC&9mi?*Sj+@2B7aP|o{3$Sd_H z@H)Q!Gq}L>0Oel+Z-9O9t2jsOc^-TO{5~l9-2~a9eg=wtzXCS<8X})e%O!G1XsY1fZqZ& z_#(&>wFBM)Z-M;Oj|j5Je+9(7Q76$`VkmJHE=gP^Mlw2FvR-r+e+s8Q!zJTT0}86D z$G9Hl(p)mcH^Qgsc!(Q`zYK|k_~dbUV2Iykh#w@TGQ?Kl`E-b{BoCh8!iCXSgdh&F zTWpgdToAvD&%}O-MI4JdDaK=)SGw@p=)9<`t_rr9oqXcQ)v89V$IG zRkG1jXBvyu8E@qrthU>tk~+J#sLn=1T8Oht*~pAdrk2LW@qev!-c+v0)j5}stVg@N zQp;!6myy7w6A{aMZM`r2NqsQH>1fW?+SWGJL!3Qb*3sS$y%jf#n@cl_5lA*{vdODZ zXI1FUB=iy2Y4#!~)JbAP7}i-vaOCjlLu7f&QJ5?zVOq0o18)ti&2_*naXGQUne8T< zc65WEtPZfqcn zoRMVgxNRo5>sB9jxgJa^8{+Cn5m}wNP^s0(s}`b=-D$jG;uOY;85c{NU$&W<5cg`k z@SEx~aldR$rgJm4YRx7^p5Cs^?Q?gk!&`4Fwch$@8d->2NO*K&a$T8CtFP7$!i8c? zh@Jcul76M6PDcYmT!{xxiT<5RjJYzUejSKBJ8ho$Nm=Q-%nFk-huYQu-xk~`zxy6i z+HmECcq?eL1>emevpsbs7nDPZdm5cNC0b&#OkD}q>*cdSeT~{6)}o`HkK~h@LMlks zkS?;dqgk-Lc2TF2eFRm40#C<%{q)2dztDYpj3pajY9P;)Q3gEIzxWpI%vcCUd>Qj~3?6FP~r9 zHSt()e(p>|rtbQr#DL0PtB#hLarsG|Y#1MGb?VCcx$cSGb)p~9=`Q6-krL{@J~43B z?DW32VQw|6y~(=g2sZJ_%lH^h9X|>A<;|(XxqkGNK6Ydd%1LSvS+P_Xy3S*FqDvTW zw3*aoD0$uXDC&skfXe7BGg~nB)n6`!YUc)g9o4=d@uX!0YqZVW*yXIw+O<-^Xjp3w zDoXn7St4gfdAi*Bxil^l1qoN99?&1-cv+6b}W-}BRdNz9^(?=N%C8I+s?}(c0e6Z3YRA?bv`z=)>*Ny`KVT-d|ulv z6Pw#oYrLA?%yeU>?Q-2cZ7y*;H*@u+nhwj6ZsaQZ;Bxa>fH%^@s}=FLT9Q^5b%gkF zS5#l##Rj;F!^Iw%5EEn7j?R%{88>wF7UygcTTJ$&X*|oF8zbVSOWFmz!W)<=B@z}$ z?IZ&vn0Tn^l_*8KyzRW*o|#I$rCubsT*UO$T0vM{Rj2LO#~sQ~`=$<#y*A%+goZFp zX(3uj?TghAhnS8IJ2$gJ{ECXA=#&zBiNF@P*@`3V;+E#9mKPAmUULhCC7EIy^SKAhCJUtqg56xD= ztrU|Ix->kRQ9>#W4zDYhx$8oYhN*7o5pFYtNtF>cg?a!;(zk57`_`eO+Puxy3w){b z9n+>&>OxuMj!sPm@Cr2=%OcT85((gxs(i7E6Y5Y)fep0qil8*>r^?g9qxP+Q(T|UV z*vn1ci9sD&+Cl8*lGG;laYO4BZb>7zLE5-8wDGu}79Kmeidt7@Q#1gMDkzwR`X9jL+hza& literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..4ee1253736b73693b282fff8b698b445756c0548 GIT binary patch literal 1049 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..109d113eecfb20351acaf6ba3305b070cb592a74 GIT binary patch literal 1087 zcmV-F1i<@=P)Lblp1$)~lH{|@ui^~OGg+SJKD#nCd7gQ^ zG=kcCQvwhsr>-2?J36|Q%jJjxFu%w`{PvdtkmESCzP5Js&h+i~(2)j!OzDO)QZBz! z6qQ6#B-eG?0%Gv8oD0E#kCz6StTckfG-khg{l+c6xhhE#S(d@K^~m@AHerr+?1K;d zIF}UwO;K`ZG%e3cO!A?;2Voz4;K#W%0Ma~xDv+>GmSr+cGkk(x&+G7hu{c2B$2q1} z764>H{OxEk_f7-*@a4!qo1~wKt~GqC&AiE_=I&(A!2>*YX;*nhgR!&SeF_ zdi3zY#h*VnXkcKF0x0BX=nOy1v5tLKPTH_$q zbD02uM{O=IFFpPCSw|}_`siO6d=hl)mBzMJx9fpa&p%bV2*X{gD>LG6aX@dedy5PQ@XBG zZKE10M(pE@Io5gqB=~kK#eV}JOj@d{9!E*uWM_+&oC9a9wL zGCSJoVyVc*rbyqvev69IpG39qz<$mFEow9xn7^=X`|kYQ?Au<}*sUWH&y7!9lojPB z*LMx(1wm20RwK){yZ9kPRTLT=8VZ3S8v=~yd#39+*Jfv*+~1y~d_=qQ+J;OgiA}RSQ5OHzlP)krWka+ar^~7+j_~TBhcn&fJTEBmt1v+%x=+ zl~Sn`#<#I$giS`QFiqnc%L}cdYNY4&&??PllVw@+{45iwS4lmgE{Q@#Q+2AcnLWp8 zEA158&oE7cEwDv7wnn~?C)c(synKPFnN*H7ys>Rr-xNY#|! z#N7a*W+V6y0B|`;6nD2kub%Ip0YEPQ*6maaf>yJ+h?tF&O_l_?kiR`OcP63|e-1L0 z`z.+)/(?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')), )