From e6bfab7ee17ca789d6e07be06577c742dfc9bb3f Mon Sep 17 00:00:00 2001 From: Hivert Quentin Date: Thu, 1 Feb 2024 16:55:29 +0100 Subject: [PATCH] feat(accounts): Add parameter to encrypt/decrypt auxiliary account's password --- Documentation/SOGoInstallationGuide.asciidoc | 19 + Main/sogod.m | 11 +- SoObjects/SOGo/NSString+Crypto.m | 182 +++++++- SoObjects/SOGo/SOGoSystemDefaults.h | 2 + SoObjects/SOGo/SOGoSystemDefaults.m | 56 +++ SoObjects/SOGo/SOGoUser.m | 44 ++ SoObjects/SOGo/SOGoUserDefaults.m | 3 + Tools/GNUmakefile | 3 +- Tools/SOGoToolUpdateSecret.m | 421 +++++++++++++++++++ UI/PreferencesUI/UIxJSONPreferences.m | 38 +- UI/PreferencesUI/UIxPreferences.m | 78 +++- 11 files changed, 841 insertions(+), 16 deletions(-) create mode 100644 Tools/SOGoToolUpdateSecret.m diff --git a/Documentation/SOGoInstallationGuide.asciidoc b/Documentation/SOGoInstallationGuide.asciidoc index f13c966501..e8aad6b1d7 100644 --- a/Documentation/SOGoInstallationGuide.asciidoc +++ b/Documentation/SOGoInstallationGuide.asciidoc @@ -445,6 +445,25 @@ else, leave that value empty. Defaults to `NO` when unset. +|S |SOGoSecretType +|To be used with _SOGoSecretValue_. Parameter used to define what +type is the secret: 'plain' to directly put the secret in _SOGoSecretValue_, 'env' +to put the name of a environment variable in _SOGoSecretValue_ +'none' to not use any secret. +For now, it is only used to encrypt/decrypt auxiliary account's password. the secret must be +128 bits long i.e. 32 utf8 chars string. + +Defaults to 'none' when unset + +|S |SOGoSecretValue +|Parameter used whenever SOGo need a secret to encrypt/decrypt. For now, +only for password of auxiliary accounts. If _SOGoSecretType_ is 'plain', +directly put the secret here. if _SOGoSecretType_ is 'env', put the name +of the environment variable here. Must be set with _SOGoSecretType_. +If _SOGoSecretType_ is not 'none', sogo won't start is the value is unfetchable or incorrect + +There is no default value + |S |SOGoEncryptionKey |Parameter used to define a key to encrypt the passwords of remote Web calendars when _SOGoTrustProxyAuthentication_ is enabled. diff --git a/Main/sogod.m b/Main/sogod.m index 31a84f9f4c..37eb0f50f9 100644 --- a/Main/sogod.m +++ b/Main/sogod.m @@ -48,7 +48,16 @@ rc = 0; sd = [SOGoSystemDefaults sharedSystemDefaults]; [NSTimeZone setDefaultTimeZone: [sd timeZone]]; - WOWatchDogApplicationMain (@"SOGo", argc, (void *) argv); + //Check if sogo secret is set and correct + if([sd isSogoSecretSet] && ![sd sogoSecretValue]) + { + rc =-1; + NSLog (@"Sogo secret is not correctly set"); + } + else + { + WOWatchDogApplicationMain (@"SOGo", argc, (void *) argv); + } } else { diff --git a/SoObjects/SOGo/NSString+Crypto.m b/SoObjects/SOGo/NSString+Crypto.m index 72b056e744..37e2ea952b 100644 --- a/SoObjects/SOGo/NSString+Crypto.m +++ b/SoObjects/SOGo/NSString+Crypto.m @@ -35,10 +35,15 @@ #endif #import "aes.h" -#define AES_KEY_SIZE 16 -#define AES_BLOCK_SIZE 16 +#define AES_128_KEY_SIZE 16 +#define AES_128_BLOCK_SIZE 16 +#define AES_256_KEY_SIZE 32 +#define AES_256_BLOCK_SIZE 16 +#define GMC_IV_LEN 12 +#define GMC_TAG_LEN 16 static const NSString *kAES128ECError = @"kAES128ECError"; +static const NSString *kAES256GCMError = @"kAES256GCMError"; @implementation NSString (SOGoCryptoExtension) @@ -379,8 +384,8 @@ - (NSString *) encodeAES128ECBBase64:(NSString *)passwordScheme encodedURL:(BOOL value = nil; - if (AES_KEY_SIZE != [passwordScheme length]) { - *ex = [NSException exceptionWithName: kAES128ECError reason: [NSString stringWithFormat:@"Key must be %d bits", (AES_KEY_SIZE * 8)] userInfo: nil]; + if (AES_128_KEY_SIZE != [passwordScheme length]) { + *ex = [NSException exceptionWithName: kAES128ECError reason: [NSString stringWithFormat:@"Key must be %d bits", (AES_128_KEY_SIZE * 8)] userInfo: nil]; return nil; } @@ -398,7 +403,7 @@ - (NSString *) encodeAES128ECBBase64:(NSString *)passwordScheme encodedURL:(BOOL EVP_CIPHER_CTX_set_padding(ctx, 1); // Perform encryption - c_len = [data length] + AES_BLOCK_SIZE; + c_len = [data length] + AES_128_BLOCK_SIZE; ciphertext = malloc(c_len); f_len = 0; @@ -451,8 +456,8 @@ - (NSString *) decodeAES128ECBBase64:(NSString *)passwordScheme encodedURL:(BOOL #ifdef HAVE_OPENSSL - if (AES_KEY_SIZE != [passwordScheme length]) { - *ex = [NSException exceptionWithName: kAES128ECError reason: [NSString stringWithFormat:@"Key must be %d bits", (AES_KEY_SIZE * 8)] userInfo: nil]; + if (AES_128_KEY_SIZE != [passwordScheme length]) { + *ex = [NSException exceptionWithName: kAES128ECError reason: [NSString stringWithFormat:@"Key must be %d bits", (AES_128_KEY_SIZE * 8)] userInfo: nil]; return nil; } keyData = [passwordScheme dataUsingEncoding: NSUTF8StringEncoding]; @@ -516,4 +521,167 @@ - (NSString *) decodeAES128ECBBase64:(NSString *)passwordScheme encodedURL:(BOOL #endif } +- (NSDictionary *)encryptAES256GCM:(NSString *)passwordScheme exception:(NSException **)ex +{ + + NSData *data, *keyData, *ivData, *tagData, *outputData; + NSString *value; + NSError *error; + NSMutableDictionary* gcmDisctionary; + int c_len, f_len; + unsigned char *ciphertext; + unsigned char tag[16]; + + #ifdef HAVE_OPENSSL + EVP_CIPHER_CTX *ctx; + #endif + + value = nil; + gcmDisctionary = [NSMutableDictionary dictionaryWithObject: @"" forKey: @"cypher"]; + + + if (AES_256_KEY_SIZE != [passwordScheme length]) { + *ex = [NSException exceptionWithName: kAES256GCMError reason: [NSString stringWithFormat:@"Key must be %d bits", (AES_256_KEY_SIZE * 8)] userInfo: nil]; + return nil; + } + + #ifdef HAVE_OPENSSL + + //Generate random IV + ivData = [[NSFileHandle fileHandleForReadingAtPath:@"/dev/random"] readDataOfLength:GMC_IV_LEN]; + if (GMC_IV_LEN != [ivData length]) { + *ex = [NSException exceptionWithName: kAES256GCMError reason: [NSString stringWithFormat:@"IV must be %d bits", (GMC_IV_LEN * 8)] userInfo: nil]; + return nil; + } + + data = [self dataUsingEncoding: NSUTF8StringEncoding]; + keyData = [passwordScheme dataUsingEncoding: NSUTF8StringEncoding]; + + //Set cipher encryption + ctx = EVP_CIPHER_CTX_new(); + EVP_EncryptInit_ex(ctx, EVP_aes_256_gcm(), NULL, NULL, NULL); + EVP_EncryptInit_ex(ctx, EVP_aes_256_gcm(), NULL, NULL, NULL); + EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_SET_IVLEN, GMC_IV_LEN, NULL); + EVP_EncryptInit_ex(ctx, NULL, NULL, [keyData bytes], [ivData bytes]); + + //Start Encryption + c_len = [data length]; + ciphertext = malloc(c_len); + int status = 0; + EVP_EncryptUpdate(ctx, ciphertext, &c_len, [data bytes], (int)[data length]); + status = EVP_EncryptFinal_ex(ctx, ciphertext + c_len, &f_len); + c_len += f_len; + + outputData = nil; + tagData = nil; + if(status) + { + outputData = [NSData dataWithBytes: (char *)ciphertext length: c_len]; + EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, GMC_TAG_LEN, tag); + tagData = [NSData dataWithBytes: (char *)tag length: GMC_TAG_LEN]; + } + else { + *ex = [NSException exceptionWithName: kAES256GCMError reason:@"Encryption not successful" userInfo: nil]; + } + + EVP_CIPHER_CTX_free(ctx); + + free(ciphertext); + if(outputData && tagData) + { + [gcmDisctionary setObject: [outputData stringByEncodingBase64] forKey: @"cypher"]; + [gcmDisctionary setObject: [ivData stringByEncodingBase64] forKey: @"iv"]; + [gcmDisctionary setObject: [tagData stringByEncodingBase64] forKey: @"tag"]; + } + else { + *ex = [NSException exceptionWithName: kAES256GCMError reason:@"Empty data" userInfo: nil]; + } + + return gcmDisctionary; + + #else + *ex = [NSException exceptionWithName:kAES256GCMError reason:@"Missing OpenSSL framework" userInfo: nil]; + return nil; + #endif +} + +- (NSString *)decryptAES256GCM:(NSString *)passwordScheme iv:(NSString *)ivString tag:(NSString *)tagString exception:(NSException **)ex +{ + + NSData *keyData, *ivData, *tagData, *data, *outputData; + NSString *inputString, *value; + int p_len, f_len, rv; + unsigned char *plaintext; + + value = nil; + + #ifdef HAVE_OPENSSL + + keyData = [passwordScheme dataUsingEncoding: NSUTF8StringEncoding]; + ivData = [[NSData alloc] initWithBase64EncodedString: ivString options:0]; + tagData = [[NSData alloc] initWithBase64EncodedString: tagString options:0]; + + if (AES_256_KEY_SIZE != [keyData length]) { + *ex = [NSException exceptionWithName: kAES256GCMError reason: [NSString stringWithFormat:@"Key must be %d bits", (AES_256_KEY_SIZE * 8)] userInfo: nil]; + return nil; + } + if (GMC_IV_LEN!= [ivData length]) { + *ex = [NSException exceptionWithName: kAES256GCMError reason: [NSString stringWithFormat:@"Key must be %d bits", (GMC_IV_LEN * 8)] userInfo: nil]; + return nil; + } + if (GMC_TAG_LEN != [tagData length]) { + *ex = [NSException exceptionWithName: kAES256GCMError reason: [NSString stringWithFormat:@"Tag must be %d bits", (GMC_TAG_LEN * 8)] userInfo: nil]; + return nil; + } + + inputString = [NSString stringWithString: self]; + data = [[NSData alloc] initWithBase64EncodedString: inputString options:0]; + + // Initialize OpenSSL + EVP_CIPHER_CTX *ctx; + ctx = EVP_CIPHER_CTX_new(); + + // Set up cipher parameters + EVP_CIPHER_CTX_init(ctx); + EVP_DecryptInit_ex(ctx, EVP_aes_256_gcm(), NULL, NULL, NULL); + EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_SET_IVLEN, GMC_IV_LEN, NULL); + EVP_DecryptInit_ex(ctx, NULL, NULL, [keyData bytes], [ivData bytes]); + + // Perform decryption + p_len = [data length]; + plaintext = malloc(p_len); + f_len = 0; + + int status = 0; + EVP_DecryptUpdate(ctx, plaintext, &p_len, [data bytes], [data length]); + outputData = [NSData dataWithBytes: plaintext length: p_len]; + EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_SET_TAG, GMC_TAG_LEN, (void *)[tagData bytes]); + rv = EVP_DecryptFinal_ex(ctx, plaintext + p_len, &f_len); + p_len += f_len; + EVP_CIPHER_CTX_free(ctx); + + if (rv > 0) { + if (outputData) { + value = [NSString stringWithUTF8String: [outputData bytes]]; + } else { + *ex = [NSException exceptionWithName: kAES256GCMError reason:@"Decryption ok but output empty" userInfo: nil]; + } + } else { + *ex = [NSException exceptionWithName: kAES256GCMError reason:@"Decryption not ok" userInfo: nil]; + } + + // Clean up + free(plaintext); + [data release]; + [ivData release]; + [tagData release]; + + return value; + + #else + *ex = [NSException exceptionWithName:kAES256GCMError reason:@"Missing OpenSSL framework" userInfo: nil]; + return self; + #endif +} + @end diff --git a/SoObjects/SOGo/SOGoSystemDefaults.h b/SoObjects/SOGo/SOGoSystemDefaults.h index 3f198d950c..5f38ead9a4 100644 --- a/SoObjects/SOGo/SOGoSystemDefaults.h +++ b/SoObjects/SOGo/SOGoSystemDefaults.h @@ -46,6 +46,8 @@ static const NSString *kDisableSharingCalendar = @"Calendar"; - (int) vmemLimit; - (BOOL) trustProxyAuthentication; - (NSString *) encryptionKey; +- (BOOL) isSogoSecretSet; +- (NSString *) sogoSecretValue; - (BOOL) useRelativeURLs; - (NSString *) sieveFolderEncoding; diff --git a/SoObjects/SOGo/SOGoSystemDefaults.m b/SoObjects/SOGo/SOGoSystemDefaults.m index 03137f4af8..160a14dda4 100644 --- a/SoObjects/SOGo/SOGoSystemDefaults.m +++ b/SoObjects/SOGo/SOGoSystemDefaults.m @@ -25,10 +25,12 @@ #import #import #import +#import #import #import "NSArray+Utilities.h" +#import "NSString+Crypto.h" #import "NSDictionary+Utilities.h" #import "SOGoStartupLogger.h" @@ -353,6 +355,60 @@ - (NSString *) encryptionKey; return [self stringForKey: @"SOGoEncryptionKey"]; } + +- (BOOL) isSogoSecretSet +{ + NSString *type; + type = [self stringForKey: @"SOGoSecretType"]; + if(!type || [type isEqualToString:@"none"]) + return NO; + else + return YES; +} + +- (NSString *) sogoSecretValue +{ + NSString *value, *type; + NSDictionary *env; + + type = [self stringForKey: @"SOGoSecretType"]; + if(!type) + type = @"none"; + + if ([type isEqualToString:@"plain"]) + { + value = [self stringForKey: @"SOGoSecretValue"]; + } + else if ([type isEqualToString:@"env"]) + { + value = [self stringForKey: @"SOGoSecretValue"]; + [self errorWithFormat: @"SOGo env fetching %@", value]; + if(!value || [value length] < 1) + { + [self errorWithFormat: @"SOGoSecretValue is not set!"]; + return nil; + } + env = [[NSProcessInfo processInfo] environment]; + value = [env objectForKey:value]; + } + else if ([type isEqualToString:@"none"]) + { + return nil; + } + else { + [self errorWithFormat: @"SOGo can't understand the type of secret SOGoSecretType"]; + return nil; + } + + if(!value || [value length] != 32){ + [self errorWithFormat: @"SOGo doesn't have a correct secret value of 32 chars SOGoSecretValue"]; + return nil; + } + + return value; +} + + - (BOOL) useRelativeURLs { return [self boolForKey: @"WOUseRelativeURLs"]; diff --git a/SoObjects/SOGo/SOGoUser.m b/SoObjects/SOGo/SOGoUser.m index 5d22e3e5ea..664b53b1b5 100644 --- a/SoObjects/SOGo/SOGoUser.m +++ b/SoObjects/SOGo/SOGoUser.m @@ -1041,7 +1041,51 @@ - (NSArray *) mailAccountsWithDelegatedIdentities: (BOOL) appendDeletegatedIdent { auxAccounts = [[self userDefaults] auxiliaryMailAccounts]; if (auxAccounts) + { + //Check if we need to decrypt password + NSString* sogoSecret; + sogoSecret = [[SOGoSystemDefaults sharedSystemDefaults] sogoSecretValue]; + if(sogoSecret) + { + int i; + NSString *encryptedPassword, *password, *iv, *tag; + NSDictionary *account, *accountPassword; + NSException* exception = nil; + for (i = 0; i < [auxAccounts count]; i++) + { + account = [auxAccounts objectAtIndex: i]; + if (![[account objectForKey: @"password"] isKindOfClass: [NSDictionary class]]) + { + [self errorWithFormat:@"Can't decrypt the password for auxiliary account %@, is not a dictionnary", + [account objectForKey: @"name"]]; + continue; + } + + accountPassword = [account objectForKey: @"password"]; + encryptedPassword = [accountPassword objectForKey: @"cypher"]; + iv = [accountPassword objectForKey: @"iv"]; + tag = [accountPassword objectForKey: @"tag"]; + NS_DURING + { + password = [encryptedPassword decryptAES256GCM: sogoSecret iv: iv tag: tag exception:&exception]; + } + NS_HANDLER + { + [self errorWithFormat:@"Can't decrypt the password for auxiliary account %@", + [account objectForKey: @"name"]]; + password = [account objectForKey: @"password"]; + } + NS_ENDHANDLER + if(exception) + [self errorWithFormat:@"Can't decrypt the password for auxiliary account %@: %@", + [account objectForKey: @"name"], [exception reason]]; + else + [account setObject: password forKey: @"password"]; + } + } + [mailAccounts addObjectsFromArray: auxAccounts]; + } } } diff --git a/SoObjects/SOGo/SOGoUserDefaults.m b/SoObjects/SOGo/SOGoUserDefaults.m index 3f8dc24867..d6ed907737 100644 --- a/SoObjects/SOGo/SOGoUserDefaults.m +++ b/SoObjects/SOGo/SOGoUserDefaults.m @@ -29,6 +29,9 @@ #import #import +#import +#import + #import "NSString+Utilities.h" #import "SOGoSystemDefaults.h" #import "SOGoUserProfile.h" diff --git a/Tools/GNUmakefile b/Tools/GNUmakefile index fa8bc58569..bae7a01567 100644 --- a/Tools/GNUmakefile +++ b/Tools/GNUmakefile @@ -27,7 +27,8 @@ $(SOGO_TOOL)_OBJC_FILES += \ SOGoToolUserPreferences.m \ SOGoToolManageACL.m \ SOGoToolManageEAS.m \ - SOGoToolTruncateCalendar.m + SOGoToolTruncateCalendar.m \ + SOGoToolUpdateSecret.m TOOL_NAME += $(SOGO_TOOL) ### diff --git a/Tools/SOGoToolUpdateSecret.m b/Tools/SOGoToolUpdateSecret.m new file mode 100644 index 0000000000..93fbc6535f --- /dev/null +++ b/Tools/SOGoToolUpdateSecret.m @@ -0,0 +1,421 @@ +/* SOGoToolUserPreferences.m - this file is part of SOGo + * + * Copyright (C) 2011-2019 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 + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This file 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; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#import +#import +#import + +#import + +#import + +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import + +#import "SOGoTool.h" + +@interface SOGoToolUpdateSecret : SOGoTool +{ + NSArray *usersWithAuxiliaryAccounts; +} + +@end + +@implementation SOGoToolUpdateSecret + ++ (NSString *) command +{ + return @"update-secret"; +} + ++ (NSString *) description +{ + return @"Update all database data that needs to be encrypted with a new secret value"; +} + +- (id) init +{ + if ((self = [super init])) + { + usersWithAuxiliaryAccounts = nil; + } + + return self; +} + +- (void) dealloc +{ + [usersWithAuxiliaryAccounts release]; + [super dealloc]; +} + +- (void) usage +{ + fprintf (stderr, "update-secret -n new_secret -o old_secret\n\n" + " -n new_secret the new secret value to encrypt with, if given alone it will assume the data are not currently encrypted\n" + " -o old_secret the current value of the secret if any, if given alone, it will decrypts all current data\n" + " The secret must be a 32 chars long utf-8 strings (128 bits)\n\n" + " Example set a secret for the first time:\n" + " sogo-tool update-secret -n exemple_NewSuperSecretOfLenght32\n" + " Examples change for a new value of secret:\n" + " sogo-tool update-secret -n exemple_NewSuperSecretOfLenght32:32 -o exemple_OldSuperSecretOfLenght32\n" + " Examples unset the secret and come back to unencrypted data:\n" + " sogo-tool update-secret -o exemple_OldSuperSecretOfLenght32\n"); +} + + +- (BOOL) fetchAllUsersForAuxiliaryAccountPassword +{ + NSAutoreleasePool *pool; + SOGoUserManager *lm; + NSDictionary *infos; + NSString *user; + SOGoSystemDefaults* sd; + id allUsers; + int count, max; + + lm = [SOGoUserManager sharedUserManager]; + + GCSFolderManager *fm; + GCSChannelManager *cm; + NSURL *userProfileUrl; + EOAdaptorChannel *fc; + NSArray *users, *attrs; + NSMutableArray *allSqlUsers; + NSString *sql, *profileURL; + + sd = [SOGoSystemDefaults sharedSystemDefaults]; + profileURL = [sd profileURL]; + if (profileURL) + userProfileUrl = [[NSURL alloc] initWithString: profileURL]; + else + { + NSLog(@"Can't find the value for SOGoProfileURL!"); + return NO; + } + + fm = [GCSFolderManager defaultFolderManager]; + cm = [fm channelManager]; + fc = [cm acquireOpenChannelForURL: userProfileUrl]; + if (fc) + { + allSqlUsers = [NSMutableArray new]; + sql = [NSString stringWithFormat: @"SELECT c_uid FROM %@ WHERE c_defaults LIKE '%%AuxiliaryMailAccounts\":[{%%'", + [userProfileUrl gcsTableName]]; + [fc evaluateExpressionX: sql]; + attrs = [fc describeResults: NO]; + while ((infos = [fc fetchAttributes: attrs withZone: NULL])) + { + user = [infos objectForKey: @"c_uid"]; + if (user) + [allSqlUsers addObject: user]; + } + [cm releaseChannel: fc immediately: YES]; + + users = allSqlUsers; + max = [users count]; + [allSqlUsers autorelease]; + } + else + { + NSLog(@"Can't create channel to %@", userProfileUrl); + return NO; + } + + ASSIGN (usersWithAuxiliaryAccounts, users); + + return ([usersWithAuxiliaryAccounts count] > 0); +} + +- (BOOL) updateSecretFromPlainData: (NSString*) secret +{ + BOOL rc; + rc = [self fetchAllUsersForAuxiliaryAccountPassword]; + if(rc){ + int i; + for(i=0; i < [usersWithAuxiliaryAccounts count]; i++){ + + SOGoUser* user; + SOGoDefaultsSource *source; + int count; + NSDictionary* account; + NSArray *aux; + NSString *password; + + user = [SOGoUser userWithLogin: [usersWithAuxiliaryAccounts objectAtIndex: i]]; + source = [user userDefaults]; + aux = [source objectForKey: @"AuxiliaryMailAccounts"]; + if(!aux) + continue; + + for (count = 0; count < [aux count]; count++) + { + account = [aux objectAtIndex: count]; + if(![[account objectForKey: @"password"] isKindOfClass: [NSString class]]) + { + NSLog(@"Can't encrypt the password for auxiliary account %@, password is not a string, probabbly already encrypted", + [account objectForKey: @"name"]); + continue; + } + password = [account objectForKey: @"password"]; + if([password length] > 0) + { + NSString* newPassword; + NSException* exception = nil; + newPassword = [password encryptAES256GCM: secret exception:&exception]; + if(exception) + NSLog(@"Can't encrypt the password: %@", [exception reason]); + else + [account setObject: newPassword forKey: @"password"]; + } + else + NSLog(@"Password not found! For user: %@ and account %@", user, account); + } + [source setObject: aux forKey: @"AuxiliaryMailAccounts"]; + [source synchronize]; + } + } + + return rc; +} + +- (void) updateSecretFromEncryptedData: (NSString*) newSecret oldSecret: (NSString*) oldSecret +{ + BOOL rc; + rc = [self fetchAllUsersForAuxiliaryAccountPassword]; + if(rc){ + int i; + for(i=0; i < [usersWithAuxiliaryAccounts count]; i++){ + + SOGoUser* user; + SOGoDefaultsSource *source; + int count; + NSDictionary* account, *accountPassword; + NSArray *aux; + NSString *password, *iv, *tag; + + user = [SOGoUser userWithLogin: [usersWithAuxiliaryAccounts objectAtIndex: i]]; + source = [user userDefaults]; + aux = [source objectForKey: @"AuxiliaryMailAccounts"]; + if(!aux) + continue; + + for (count = 0; count < [aux count]; count++) + { + account = [aux objectAtIndex: count]; + if(![[account objectForKey: @"password"] isKindOfClass: [NSDictionary class]]) + { + NSLog(@"Can't decrypt the password for auxiliary account %@, is not a dictionnary", + [account objectForKey: @"name"]); + continue; + } + accountPassword = [account objectForKey: @"password"]; + password = [accountPassword objectForKey: @"cypher"]; + iv = [accountPassword objectForKey: @"iv"]; + tag = [accountPassword objectForKey: @"tag"]; + if([password length] > 0) + { + NSString* decryptedPassword; + NSDictionary* encryptedPassword; + NSException* exception = nil; + NS_DURING + decryptedPassword = [password decryptAES256GCM: oldSecret iv: iv tag: tag exception:&exception]; + encryptedPassword = [decryptedPassword encryptAES256GCM: newSecret exception:&exception]; + NS_HANDLER + encryptedPassword = accountPassword; + NSLog(@"Can't decrypt the password, unexpected exception"); + NS_ENDHANDLER + + if(exception) + NSLog(@"Can't decrypt the password: %@", [exception reason]); + else + [account setObject: encryptedPassword forKey: @"password"]; + } + else + NSLog(@"Password not found! For user: %@ and account %@", user, account); + } + [source setObject: aux forKey: @"AuxiliaryMailAccounts"]; + [source synchronize]; + } + } + + return rc; +} + +- (BOOL) updateToPlainData: (NSString*) oldSecret +{ + BOOL rc; + rc = [self fetchAllUsersForAuxiliaryAccountPassword]; + if(rc){ + int i; + for(i=0; i < [usersWithAuxiliaryAccounts count]; i++){ + + SOGoUser* user; + SOGoDefaultsSource *source; + int count; + NSDictionary* account, *accountPassword; + NSArray *aux; + NSString *password, *iv, *tag; + + user = [SOGoUser userWithLogin: [usersWithAuxiliaryAccounts objectAtIndex: i]]; + source = [user userDefaults]; + aux = [source objectForKey: @"AuxiliaryMailAccounts"]; + if(!aux) + continue; + + for (count = 0; count < [aux count]; count++) + { + account = [aux objectAtIndex: count]; + if(![[account objectForKey: @"password"] isKindOfClass: [NSDictionary class]]) + { + NSLog(@"Can't decrypt the password for auxiliary account %@, is not a dictionnary", + [account objectForKey: @"name"]); + continue; + } + accountPassword = [account objectForKey: @"password"]; + password = [accountPassword objectForKey: @"cypher"]; + iv = [accountPassword objectForKey: @"iv"]; + tag = [accountPassword objectForKey: @"tag"]; + if([password length] > 0) + { + NSString* newPassword; + NSException* exception = nil; + NS_DURING + newPassword = [password decryptAES256GCM: oldSecret iv: iv tag: tag exception:&exception]; + if(exception) + NSLog(@"Can't decrypt the password: %@", [exception reason]); + else + [account setObject: newPassword forKey: @"password"]; + NS_HANDLER + NSLog(@"Can't decrypt the password, unexpected exception"); + NS_ENDHANDLER + } + else + NSLog(@"Password not found! For user: %@ and account %@", user, account); + } + [source setObject: aux forKey: @"AuxiliaryMailAccounts"]; + [source synchronize]; + } + } + return rc; +} + + +- (BOOL) checkArguments: (NSArray*)args +{ + int size, i; + NSString *type1, *type2; + size = [args count]; + if (size != 2 && size != 4) + { + NSLog(@"Wrong number of arguments, should be 2 or 4 and was %d", size); + return NO; + } + for(i=0; i #import - +#import #import #import #import +#import #import #import #import @@ -493,8 +494,10 @@ - (NSString *) jsonDefaults if ([accounts count]) { int i; - NSDictionary *security; + NSDictionary *security, *accountPassword; NSMutableDictionary *auxAccount, *limitedSecurity; + NSString *password, *encryptedPassword, *sogoSecret, *iv, *tag; + NSException* exception = nil; for (i = 0; i < [accounts count]; i++) { @@ -510,6 +513,37 @@ - (NSString *) jsonDefaults } [auxAccount setObject: limitedSecurity forKey: @"security"]; } + + //Decrypt password if needed + sogoSecret = [[SOGoSystemDefaults sharedSystemDefaults] sogoSecretValue]; + if (sogoSecret) + { + if(![[auxAccount objectForKey: @"password"] isKindOfClass: [NSDictionary class]]) + { + [self errorWithFormat:@"Can't decrypt the password for auxiliary account %@, is not a dictionnary", + [auxAccount objectForKey: @"name"]]; + continue; + } + accountPassword = [auxAccount objectForKey: @"password"]; + encryptedPassword = [accountPassword objectForKey: @"cypher"]; + iv = [accountPassword objectForKey: @"iv"]; + tag = [accountPassword objectForKey: @"tag"]; + if([encryptedPassword length] > 0) + { + NS_DURING + password = [encryptedPassword decryptAES256GCM: sogoSecret iv: iv tag: tag exception:&exception]; + NS_HANDLER + [self errorWithFormat:@"Can't decrypt the password for auxiliary account %@, probably not encrypted.", + [auxAccount objectForKey: @"name"]]; + password = [auxAccount objectForKey: @"password"]; + NS_ENDHANDLER + if(exception) + [self errorWithFormat:@"Can't decrypt the password for auxiliary account %@: %@", + [auxAccount objectForKey: @"name"], [exception reason]]; + else + [auxAccount setObject: password forKey: @"password"]; + } + } } } // We inject our default mail account diff --git a/UI/PreferencesUI/UIxPreferences.m b/UI/PreferencesUI/UIxPreferences.m index 2169f0c7e6..03bbdedbe4 100644 --- a/UI/PreferencesUI/UIxPreferences.m +++ b/UI/PreferencesUI/UIxPreferences.m @@ -35,6 +35,7 @@ #import #import #import +#import #import #import #import @@ -1532,6 +1533,7 @@ - (NSArray *) _extractAuxiliaryAccounts: (NSArray *) accounts int count, max; NSMutableArray *auxAccounts; NSMutableDictionary *account; + NSString *sogoSecret, *password; max = [accounts count]; auxAccounts = [NSMutableArray arrayWithCapacity: max]; @@ -1542,6 +1544,24 @@ - (NSArray *) _extractAuxiliaryAccounts: (NSArray *) accounts if ([self _validateAccount: account]) { [self _updateAuxiliaryAccount: account]; + + //Encrypt password if needed + sogoSecret = [[SOGoSystemDefaults sharedSystemDefaults] sogoSecretValue]; + //Note that password here will always be a string and not a dictionnary of gcm encryption as it comes from the saveAction Request + password = [account objectForKey: @"password"]; + if(sogoSecret && [password length] > 0) + { + NSDictionary* newPassword; + NSException* exception = nil; + newPassword = [password encryptAES256GCM: sogoSecret exception:&exception]; + if(exception) + [self errorWithFormat:@"Can't encrypt the password: %@", [exception reason]]; + else + [account setObject: newPassword forKey: @"password"]; + } + else + [self warnWithFormat:@"Password for auxiliary accounts are not stored encrypted see SOGoSecretType"]; + [auxAccounts addObject: account]; } } @@ -1556,7 +1576,7 @@ - (void) _updateAuxiliaryAccount: (NSMutableDictionary *) newAccount NSDictionary *oldAccount, *oldSecurity; NSEnumerator *comparisonAttributesList; NSMutableDictionary *newSecurity; - NSString *comparisonAttribute, *password, *certificate; + NSString *comparisonAttribute, *password, *certificate, *sogoSecret, *decryptedPassword; comparisonAttributes = [NSArray arrayWithObjects: @"serverName", @"userName", nil]; oldAccounts = [user mailAccounts]; @@ -1580,11 +1600,59 @@ - (void) _updateAuxiliaryAccount: (NSMutableDictionary *) newAccount // Use previous password if none is provided password = [newAccount objectForKey: @"password"]; if (!password) - password = [oldAccount objectForKey: @"password"]; - if (!password) - password = @""; - [newAccount setObject: password forKey: @"password"]; + { + if([oldAccount objectForKey: @"password"]) + { + if([[oldAccount objectForKey: @"password"] isKindOfClass: [NSDictionary class]]) + { + //Old password is encrypted, decrypt it first as it will be encrypt again after + NSDictionary *accountPassword; + NSString *encryptedPassword, *iv, *tag; + accountPassword = [oldAccount objectForKey: @"password"]; + encryptedPassword = [accountPassword objectForKey: @"cypher"]; + iv = [accountPassword objectForKey: @"iv"]; + tag = [accountPassword objectForKey: @"tag"]; + sogoSecret = [[SOGoSystemDefaults sharedSystemDefaults] sogoSecretValue]; + if(sogoSecret) + { + NSException* exception = nil; + NS_DURING + decryptedPassword = [encryptedPassword decryptAES256GCM: sogoSecret iv: iv tag: tag exception:&exception]; + if(exception) + { + [self errorWithFormat:@"Can't decrypt the password for auxiliary account %@: %@", + [oldAccount objectForKey: @"name"], [exception reason]]; + decryptedPassword = @""; + } + else + password = decryptedPassword; + NS_HANDLER + [self errorWithFormat:@"Can't decrypt the password for auxiliary account %@, probably wrong key", + [oldAccount objectForKey: @"name"]]; + decryptedPassword = @""; + NS_ENDHANDLER + [newAccount setObject: decryptedPassword forKey: @"password"]; + } + else + { + [self errorWithFormat:@"Password currently stored for account %@ is encrypted but there is a no secret SOGoSecretValue to decrypt with", + [newAccount objectForKey: @"name"]]; + [newAccount setObject: @"" forKey: @"password"]; + } + } + else + { + //Old password is not encrypted, nothing to do + [newAccount setObject: [oldAccount objectForKey: @"password"] forKey: @"password"]; + } + } + else { + //No password for this account, weird choice but possible + [newAccount setObject: @"" forKey: @"password"]; + } + } + // Keep previous certificate oldSecurity = [oldAccount objectForKey: @"security"]; if (oldSecurity)