From 7ebdac5525c7444d8d4ce975da5dd31e4ec95e71 Mon Sep 17 00:00:00 2001 From: Francis Lachapelle Date: Tue, 23 Jan 2018 13:30:56 -0500 Subject: [PATCH] Improve display of S/MIME certificates --- SoObjects/Mailer/NSData+SMIME.h | 3 +- SoObjects/Mailer/NSData+SMIME.m | 60 ++++++++++++++++++- UI/Common/English.lproj/Localizable.strings | 1 + UI/Contacts/English.lproj/Localizable.strings | 6 ++ UI/Contacts/UIxContactActions.m | 47 ++++++++++++++- UI/Contacts/UIxContactView.m | 7 ++- UI/Contacts/product.plist | 10 ++++ UI/MailerUI/English.lproj/Localizable.strings | 3 + UI/MailerUI/UIxMailAccountActions.m | 51 +++------------- .../English.lproj/Localizable.strings | 1 - .../ContactsUI/UIxContactEditorTemplate.wox | 45 +++++++++++++- .../ContactsUI/UIxContactViewTemplate.wox | 41 ++++++++++++- UI/Templates/MailerUI/UIxMailViewTemplate.wox | 13 ++-- .../PreferencesUI/UIxAccountEditor.wox | 4 +- .../js/Contacts/Card.service.js | 41 ++++++++++++- .../js/Contacts/CardController.js | 10 ++++ .../js/Mailer/Message.service.js | 8 ++- 17 files changed, 291 insertions(+), 60 deletions(-) diff --git a/SoObjects/Mailer/NSData+SMIME.h b/SoObjects/Mailer/NSData+SMIME.h index dfbf25659..f0d256fc6 100644 --- a/SoObjects/Mailer/NSData+SMIME.h +++ b/SoObjects/Mailer/NSData+SMIME.h @@ -1,6 +1,6 @@ /* NSData+SMIME.h - this file is part of SOGo * - * Copyright (C) 2017 Inverse inc. + * Copyright (C) 2017-2018 Inverse inc. * * This file is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -33,6 +33,7 @@ - (NGMimeMessage *) messageFromEncryptedDataAndCertificate: (NSData *) theCertificate; - (NSData *) convertPKCS12ToPEMUsingPassword: (NSString *) thePassword; - (NSData *) convertPKCS7ToPEM; +- (NSDictionary *) certificateDescription; @end diff --git a/SoObjects/Mailer/NSData+SMIME.m b/SoObjects/Mailer/NSData+SMIME.m index 3ba207e98..7420faa2b 100644 --- a/SoObjects/Mailer/NSData+SMIME.m +++ b/SoObjects/Mailer/NSData+SMIME.m @@ -1,6 +1,6 @@ /* NSData+SMIME.m - this file is part of SOGo * - * Copyright (C) 2017 Inverse inc. + * Copyright (C) 2017-2018 Inverse inc. * * This file is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -19,6 +19,7 @@ */ #import +#import #import #import @@ -36,6 +37,7 @@ #include #endif +#import #import "NSData+SMIME.h" @implementation NSData (SOGoMailSMIME) @@ -457,4 +459,60 @@ return output; } +/** + * Extract usefull information from PEM certificate + */ +- (NSDictionary *) certificateDescription +{ + BIO *pemBio; + NSDictionary *data; + X509 *x; + + data = nil; + OpenSSL_add_all_algorithms(); + pemBio = BIO_new_mem_buf((void *) [self bytes], [self length]); + x = PEM_read_bio_X509(pemBio, NULL, 0, NULL); + + if (x) + { + BIO *buf; + char p[1024]; + NSString *subject, *issuer; + + memset(p, 0, 1024); + buf = BIO_new(BIO_s_mem()); + X509_NAME_print_ex(buf, X509_get_subject_name(x), 0, + ASN1_STRFLGS_ESC_CTRL | XN_FLAG_SEP_MULTILINE | XN_FLAG_FN_LN); + BIO_read(buf, p, 1024); + subject = [NSString stringWithUTF8String: p]; + BIO_free(buf); + + memset(p, 0, 1024); + buf = BIO_new(BIO_s_mem()); + X509_NAME_print_ex(buf, X509_get_issuer_name(x), 0, + ASN1_STRFLGS_ESC_CTRL | XN_FLAG_SEP_MULTILINE | XN_FLAG_FN_LN); + BIO_read(buf, p, 1024); + issuer = [NSString stringWithUTF8String: p]; + BIO_free(buf); + + data = [NSDictionary dictionaryWithObjectsAndKeys: + [subject componentsFromMultilineDN], @"subject", + [issuer componentsFromMultilineDN], @"issuer", + nil]; + } + else + { + int err = ERR_get_error(); + const char* sslError; + NSString *error; + + ERR_load_crypto_strings(); + sslError = ERR_reason_error_string(err); + error = [NSString stringWithUTF8String: sslError]; + NSLog(@"FATAL: failed to read certificate: %@", error); + } + + return data; +} + @end diff --git a/UI/Common/English.lproj/Localizable.strings b/UI/Common/English.lproj/Localizable.strings index 83f3c3777..77d147094 100644 --- a/UI/Common/English.lproj/Localizable.strings +++ b/UI/Common/English.lproj/Localizable.strings @@ -115,6 +115,7 @@ "You cannot (un)subscribe to a folder that you own!" = "You cannot (un)subscribe to a folder that you own!"; /* SMIME Certificate field */ +"S/MIME Certificate" = "S/MIME Certificate"; "Subject Name" = "Subject Name"; "Issuer" = "Issuer"; "countryName" = "Country"; diff --git a/UI/Contacts/English.lproj/Localizable.strings b/UI/Contacts/English.lproj/Localizable.strings index 3ea3d9153..952c09d4f 100644 --- a/UI/Contacts/English.lproj/Localizable.strings +++ b/UI/Contacts/English.lproj/Localizable.strings @@ -239,6 +239,12 @@ "Unable to subscribe to that folder!" = "Unable to subscribe to that folder."; +/* security */ +"Security" = "Security"; +"Uninstall" = "Uninstall"; +"Error reading the card certificate." = "Error reading the card certificate."; +"No certificate associated to card." = "No certificate associated to card."; + /* acls */ "Access rights to" = "Access rights to"; "For user" = "For user"; diff --git a/UI/Contacts/UIxContactActions.m b/UI/Contacts/UIxContactActions.m index 75182fe55..8485dc8b1 100644 --- a/UI/Contacts/UIxContactActions.m +++ b/UI/Contacts/UIxContactActions.m @@ -1,6 +1,6 @@ /* UIxContactActions.m - this file is part of SOGo * - * Copyright (C) 2010-2016 Inverse inc. + * Copyright (C) 2010-2018 Inverse inc. * * This file is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -19,6 +19,7 @@ */ #import +#import #import @@ -30,6 +31,8 @@ #import +#import + #import @interface NGVCard (SOGoActionCategory) @@ -147,4 +150,46 @@ return response; } +- (WOResponse *) certificateAction +{ + NSData *pkcs7; + NSDictionary *data; + WOResponse *response; + + pkcs7 = [[[self clientObject] vCard] certificate]; + + if (pkcs7) + { + data = [[pkcs7 convertPKCS7ToPEM] certificateDescription]; + if (data) + { + response = [self responseWithStatus: 200 andJSONRepresentation: data]; + } + else + { + data = [NSDictionary + dictionaryWithObject: [self labelForKey: @"Error reading the card certificate."] + forKey: @"message"]; + response = [self responseWithStatus: 500 andJSONRepresentation: data]; + } + } + else + { + data = [NSDictionary + dictionaryWithObject: [self labelForKey: @"No certificate associated to card."] + forKey: @"message"]; + response = [self responseWithStatus: 404 andJSONRepresentation: data]; + } + + return response; +} + +- (WOResponse *) removeCertificateAction +{ + [[[self clientObject] vCard] setCertificate: nil]; + + return [self responseWith204]; +} + + @end diff --git a/UI/Contacts/UIxContactView.m b/UI/Contacts/UIxContactView.m index 6eaf282c1..578edb431 100644 --- a/UI/Contacts/UIxContactView.m +++ b/UI/Contacts/UIxContactView.m @@ -252,6 +252,7 @@ * @apiSuccess (Success 200) {String[]} allCategories All available categories * @apiSuccess (Success 200) {Object[]} [categories] Categories assigned to the card * @apiSuccess (Success 200) {String} categories.value Category name + * @apiSuccess (Success 200) {Number} hasCertificate 1 if contact has a mail certificate * @apiSuccess (Success 200) {Object[]} [addresses] Postal addresses * @apiSuccess (Success 200) {String} addresses.type Type (e.g., home or work) * @apiSuccess (Success 200) {String} addresses.postoffice Post office box @@ -330,6 +331,10 @@ [data setObject: [values subarrayWithRange: NSMakeRange(1, [values count] - 1)] forKey: @"orgs"]; } + o = [card certificate]; + if ([o length]) + [data setObject: [NSNumber numberWithBool: YES] forKey: @"hasCertificate"]; + o = [card birthday]; if (o) [data setObject: [o descriptionWithCalendarFormat: @"%Y-%m-%d"] @@ -361,7 +366,7 @@ if ((o = [[card uniqueChildWithTag: @"custom1"] flattenedValuesForKey: @""]) && [o length]) [customFields setObject: o forKey: @"1"]; - if ((o = [[card uniqueChildWithTag: @"custom2"] flattenedValuesForKey: @""]) && [o length]) + if ((o = [[card uniqueChildWithTag: @"custom2"] flattenedValuesForKey: @""]) && [o length]) [customFields setObject: o forKey: @"2"]; if ((o = [[card uniqueChildWithTag: @"custom3"] flattenedValuesForKey: @""]) && [o length]) diff --git a/UI/Contacts/product.plist b/UI/Contacts/product.plist index 4a8112964..37cccfb80 100644 --- a/UI/Contacts/product.plist +++ b/UI/Contacts/product.plist @@ -234,6 +234,16 @@ actionClass = "UIxContactActions"; actionName = "raw"; }; + certificate = { + protectedBy = "View"; + actionClass = "UIxContactActions"; + actionName = "certificate"; + }; + removeCertificate = { + protectedBy = "Change Images And Files"; + actionClass = "UIxContactActions"; + actionName = "removeCertificate"; + }; }; }; diff --git a/UI/MailerUI/English.lproj/Localizable.strings b/UI/MailerUI/English.lproj/Localizable.strings index 9fecef2fc..6f0984769 100644 --- a/UI/MailerUI/English.lproj/Localizable.strings +++ b/UI/MailerUI/English.lproj/Localizable.strings @@ -249,6 +249,9 @@ /* Encrypted message notification */ "This message is encrypted" = "This message is encrypted"; +/* Encrypted message but no certificate */ +"This message can't be decrypted. Please make sure you have uploaded your S/MIME certificate from the mail preferences module." = "This message can't be decrypted. Please make sure you have uploaded your S/MIME certificate from the mail preferences module."; + /* OpenSSL certificate error - unknown issuer */ "certificate verify error" = "Unable to verify message signature"; diff --git a/UI/MailerUI/UIxMailAccountActions.m b/UI/MailerUI/UIxMailAccountActions.m index 7902d9022..fed021f4d 100644 --- a/UI/MailerUI/UIxMailAccountActions.m +++ b/UI/MailerUI/UIxMailAccountActions.m @@ -208,60 +208,25 @@ if (pem) { - BIO *pemBio; - X509 *x; - - OpenSSL_add_all_algorithms(); - ERR_load_crypto_strings(); - pemBio = BIO_new_mem_buf((void *) [pem bytes], [pem length]); - x = PEM_read_bio_X509(pemBio, NULL, 0, NULL); - if (x) + data = [pem certificateDescription]; + if (data) { - BIO *buf; - char p[1024]; - NSString *subject, *issuer; - - memset(p, 0, 1024); - buf = BIO_new(BIO_s_mem()); - X509_NAME_print_ex(buf, X509_get_subject_name(x), 0, - ASN1_STRFLGS_ESC_CTRL | XN_FLAG_SEP_MULTILINE | XN_FLAG_FN_LN); - BIO_read(buf, p, 1024); - subject = [NSString stringWithUTF8String: p]; - BIO_free(buf); - - memset(p, 0, 1024); - buf = BIO_new(BIO_s_mem()); - X509_NAME_print_ex(buf, X509_get_issuer_name(x), 0, - ASN1_STRFLGS_ESC_CTRL | XN_FLAG_SEP_MULTILINE | XN_FLAG_FN_LN); - BIO_read(buf, p, 1024); - issuer = [NSString stringWithUTF8String: p]; - BIO_free(buf); - - data = [NSDictionary dictionaryWithObjectsAndKeys: - [subject componentsFromMultilineDN], @"subject", - [issuer componentsFromMultilineDN], @"issuer", - nil]; - response = [self responseWithStatus: 200 - andJSONRepresentation: data]; + response = [self responseWithStatus: 200 andJSONRepresentation: data]; } else { - NSLog(@"FATAL: failed to read certificate."); data = [NSDictionary dictionaryWithObject: [self labelForKey: @"Error reading the certificate. Please install a new certificate."] forKey: @"message"]; - response = [self responseWithStatus: 500 - andJSONRepresentation: data]; + response = [self responseWithStatus: 500 andJSONRepresentation: data]; } - - BIO_free(pemBio); - X509_free(x); } else { - response = [self responseWithStatus: 404 - andJSONRepresentation: [NSDictionary dictionaryWithObject: [self labelForKey: @"No certificate associated to account."] - forKey: @"message"]]; + data = [NSDictionary + dictionaryWithObject: [self labelForKey: @"No certificate associated to account."] + forKey: @"message"]; + response = [self responseWithStatus: 404 andJSONRepresentation: data]; } return response; diff --git a/UI/PreferencesUI/English.lproj/Localizable.strings b/UI/PreferencesUI/English.lproj/Localizable.strings index c02dbdc8f..a14176c4f 100644 --- a/UI/PreferencesUI/English.lproj/Localizable.strings +++ b/UI/PreferencesUI/English.lproj/Localizable.strings @@ -206,7 +206,6 @@ "Please specify a valid reply-to address." = "Please specify a valid reply-to address."; "Specify a hostname other than the local host" = "Specify a hostname other than the local host"; -"S/MIME Certificate" = "S/MIME Certificate"; "No certificate installed" = "No certificate installed"; "The SSL certificate must use the PKCS#12 (PFX) format." = "The SSL certificate must use the PKCS#12 (PFX) format."; "Uninstall" = "Uninstall"; diff --git a/UI/Templates/ContactsUI/UIxContactEditorTemplate.wox b/UI/Templates/ContactsUI/UIxContactEditorTemplate.wox index 5b90583e2..428327366 100644 --- a/UI/Templates/ContactsUI/UIxContactEditorTemplate.wox +++ b/UI/Templates/ContactsUI/UIxContactEditorTemplate.wox @@ -3,7 +3,8 @@ xmlns="http://www.w3.org/1999/xhtml" xmlns:var="http://www.skyrix.com/od/binding" xmlns:const="http://www.skyrix.com/od/constant" - xmlns:label="OGo:label"> + xmlns:label="OGo:label" + xmlns:rsrc="OGo:url">
@@ -131,6 +132,48 @@ + +
+
+ + + +

+ + {{::'S/MIME Certificate' | loc}} +

+ + + + expand_more +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/UI/Templates/ContactsUI/UIxContactViewTemplate.wox b/UI/Templates/ContactsUI/UIxContactViewTemplate.wox index 6c49bb35b..0becff098 100644 --- a/UI/Templates/ContactsUI/UIxContactViewTemplate.wox +++ b/UI/Templates/ContactsUI/UIxContactViewTemplate.wox @@ -3,7 +3,8 @@ xmlns="http://www.w3.org/1999/xhtml" xmlns:var="http://www.skyrix.com/od/binding" xmlns:const="http://www.skyrix.com/od/constant" - xmlns:label="OGo:label"> + xmlns:label="OGo:label" + xmlns:rsrc="OGo:url">
@@ -102,6 +103,44 @@
+ +
+
+ + + +

+ + {{::'S/MIME Certificate' | loc}} +

+ expand_more +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/UI/Templates/MailerUI/UIxMailViewTemplate.wox b/UI/Templates/MailerUI/UIxMailViewTemplate.wox index d8ec56651..1cb3d3ec0 100644 --- a/UI/Templates/MailerUI/UIxMailViewTemplate.wox +++ b/UI/Templates/MailerUI/UIxMailViewTemplate.wox @@ -211,10 +211,10 @@
- -
@@ -249,10 +249,13 @@
-
- lock_outline + +
+ lock_outline + lock_outline +

-
+
diff --git a/UI/Templates/PreferencesUI/UIxAccountEditor.wox b/UI/Templates/PreferencesUI/UIxAccountEditor.wox index 917fc7dbd..2502221da 100644 --- a/UI/Templates/PreferencesUI/UIxAccountEditor.wox +++ b/UI/Templates/PreferencesUI/UIxAccountEditor.wox @@ -187,9 +187,9 @@ -

- + + {{::'S/MIME Certificate' | loc}}

diff --git a/UI/WebServerResources/js/Contacts/Card.service.js b/UI/WebServerResources/js/Contacts/Card.service.js index 00fbac211..6735215ad 100644 --- a/UI/WebServerResources/js/Contacts/Card.service.js +++ b/UI/WebServerResources/js/Contacts/Card.service.js @@ -38,10 +38,11 @@ * @desc The factory we'll use to register with Angular. * @returns the Card constructor */ - Card.$factory = ['$timeout', 'sgSettings', 'sgCard_STATUS', 'Resource', 'Preferences', function($timeout, Settings, Card_STATUS, Resource, Preferences) { + Card.$factory = ['$q', '$timeout', 'sgSettings', 'sgCard_STATUS', 'Resource', 'Preferences', function($q, $timeout, Settings, Card_STATUS, Resource, Preferences) { angular.extend(Card, { STATUS: Card_STATUS, $$resource: new Resource(Settings.activeUser('folderURL') + 'Contacts', Settings.activeUser()), + $q: $q, $timeout: $timeout, $Preferences: Preferences }); @@ -485,6 +486,44 @@ return this.refs.length - 1; }; + /** + * @function $certificate + * @memberof Account.prototype + * @desc View the S/MIME certificate details associated to the account. + * @returns a promise of the HTTP operation + */ + Card.prototype.$certificate = function() { + var _this = this; + + if (this.hasCertificate) { + if (this.$$certificate) + return Card.$q.when(this.$$certificate); + else { + return Card.$$resource.fetch([this.pid, this.id].join('/'), 'certificate').then(function(data) { + _this.$$certificate = data; + return data; + }); + } + } + else { + return Card.$q.reject(); + } + }; + + /** + * @function $removeCertificate + * @memberof Account.prototype + * @desc Remove any S/MIME certificate associated with the account. + * @returns a promise of the HTTP operation + */ + Card.prototype.$removeCertificate = function() { + var _this = this; + + return Card.$$resource.fetch([this.pid, this.id].join('/'), 'removeCertificate').then(function() { + _this.hasCertificate = false; + }); + }; + /** * @function explode * @memberof Card.prototype diff --git a/UI/WebServerResources/js/Contacts/CardController.js b/UI/WebServerResources/js/Contacts/CardController.js index 2cb86268b..a1f62d9df 100644 --- a/UI/WebServerResources/js/Contacts/CardController.js +++ b/UI/WebServerResources/js/Contacts/CardController.js @@ -43,6 +43,7 @@ _registerHotkeys(hotkeys); + _loadCertificate(); $scope.$on('$destroy', function() { // Deregister hotkeys @@ -71,6 +72,15 @@ }); } + function _loadCertificate() { + if (vm.card.hasCertificate) + vm.card.$certificate().then(function(crt) { + vm.certificate = crt; + }, function() { + delete vm.card.hasCertificate; + }); + } + function transformCategory(input) { if (angular.isString(input)) return { value: input }; diff --git a/UI/WebServerResources/js/Mailer/Message.service.js b/UI/WebServerResources/js/Mailer/Message.service.js index 69826dab1..ebab2697e 100644 --- a/UI/WebServerResources/js/Mailer/Message.service.js +++ b/UI/WebServerResources/js/Mailer/Message.service.js @@ -298,7 +298,7 @@ var formattedMessage = "

" + part.error.replace(/\n/, "

"); formattedMessage = formattedMessage.replace(/\n/g, "

") + "

"; _this.$smime = { - validSignature: part.valid, + valid: part.valid, certificate: part.certificates[part.certificates.length - 1], message: formattedMessage }; @@ -306,8 +306,12 @@ else if (part.type == 'UIxMailPartEncryptedViewer') { _this.$smime = { isEncrypted: true, - message: l("This message is encrypted") + valid: part.valid }; + if (part.valid) + _this.$smime.message = l("This message is encrypted"); + else + _this.$smime.message = l("This message can't be decrypted. Please make sure you have uploaded your S/MIME certificate from the mail preferences module."); } _.forEach(part.content, function(mixedPart) { _visit(mixedPart);