diff --git a/Documentation/SOGoInstallationGuide.asciidoc b/Documentation/SOGoInstallationGuide.asciidoc index fcb5f5e76..cb0c340db 100644 --- a/Documentation/SOGoInstallationGuide.asciidoc +++ b/Documentation/SOGoInstallationGuide.asciidoc @@ -819,7 +819,11 @@ specified as an array of dictionaries. |D |SOGoCreateIdentitiesDisabled |Disable identity creation for users in preferences. If `YES`, users won't be able to add new identities and will allow to change only full name, signature and default identity. Default value is `NO`. Note : If this settings is set to `YES`, it will not be possible to crete auxiliary mail accounts. +|S |SOGoURLEncryptionEnabled +|Enable URL encryption to make SOGo GDPR compatible. Setting this parameter to `YES` will encrypt username in URL. The encryption data are cached to avoid high cpu usage. If the encryption is enabled, the DAV url will change. Default value is `NO`. +|S |SOGoURLEncryptionPassphrase +|Passphrase for `SOGoURLEncryptionEnabled`. The string must be 128 bits (16 characters). If this settings change, the cache server must be restarted, and the DAV url will change. Default value is `SOGoSuperSecret0`. |======================================================================= diff --git a/Main/SOGo.m b/Main/SOGo.m index beef82a47..f625e71f9 100644 --- a/Main/SOGo.m +++ b/Main/SOGo.m @@ -349,8 +349,11 @@ static BOOL debugLeaks; { if ([[context request] handledByDefaultHandler]) authenticator = [SOGoWebAuthenticator sharedSOGoWebAuthenticator]; - else + else { authenticator = [SOGoDAVAuthenticator sharedSOGoDAVAuthenticator]; + [authenticator setContext: context]; + } + } return authenticator; @@ -363,10 +366,14 @@ static BOOL debugLeaks; { SOGoUser *user; id userFolder; - - user = [SOGoUser userWithLogin: _key roles: nil]; + NSData *decodedLogin; + NSString *login; + + login = [SOGoUser getDecryptedUsernameIfNeeded: _key withContext: _ctx]; + + user = [SOGoUser userWithLogin: login roles: nil]; if (user) - userFolder = [$(@"SOGoUserFolder") objectWithName: _key + userFolder = [$(@"SOGoUserFolder") objectWithName: login inContainer: self]; else userFolder = nil; diff --git a/SoObjects/Appointments/SOGoUserFolder+Appointments.m b/SoObjects/Appointments/SOGoUserFolder+Appointments.m index 2d630d316..83ace9099 100644 --- a/SoObjects/Appointments/SOGoUserFolder+Appointments.m +++ b/SoObjects/Appointments/SOGoUserFolder+Appointments.m @@ -88,7 +88,7 @@ } tag = [NSArray arrayWithObjects: @"href", XMLNS_WEBDAV, @"D", - [NSString stringWithFormat: @"/SOGo/dav/%@/", nameInContainer], + [NSString stringWithFormat: @"/SOGo/dav/%@/", [self nameInContainer]], nil]; [addresses addObjectUniquely: tag]; @@ -128,7 +128,7 @@ } tag = [NSArray arrayWithObjects: @"href", XMLNS_WEBDAV, @"D", - [NSString stringWithFormat: @"/SOGo/dav/%@/", nameInContainer], + [NSString stringWithFormat: @"/SOGo/dav/%@/", [self nameInContainer]], nil]; [addresses addObjectUniquely: tag]; diff --git a/SoObjects/SOGo/NSString+Crypto.h b/SoObjects/SOGo/NSString+Crypto.h index 6368c7157..65cb6221c 100644 --- a/SoObjects/SOGo/NSString+Crypto.h +++ b/SoObjects/SOGo/NSString+Crypto.h @@ -26,6 +26,7 @@ #define NSSTRING_CRYPTO_H #import +#import typedef enum { encDefault, //!< default encoding, let the algorithm decide @@ -35,8 +36,10 @@ typedef enum { } keyEncoding; @class NSObject; +@class NSScanner; -@interface NSString (SOGoCryptoExtension) + @interface + NSString(SOGoCryptoExtension) - (BOOL) isEqualToCrypted: (NSString *) cryptedPassword withDefaultScheme: (NSString *) theScheme @@ -62,6 +65,9 @@ typedef enum { + (NSArray *) getDefaultEncodingForScheme: (NSString *) passwordScheme; +- (NSString *) encodeAES128ECBBase64:(NSString *)passwordScheme encodedURL:(BOOL)encodedURL exception:(NSException **)ex; +- (NSString *) decodeAES128ECBBase64:(NSString *)passwordScheme encodedURL:(BOOL)encodedURL exception:(NSException **)ex; + @end #endif /* NSSTRING_CRYPTO_H */ diff --git a/SoObjects/SOGo/NSString+Crypto.m b/SoObjects/SOGo/NSString+Crypto.m index 2b0a7927e..5acb88b73 100644 --- a/SoObjects/SOGo/NSString+Crypto.m +++ b/SoObjects/SOGo/NSString+Crypto.m @@ -29,6 +29,11 @@ #import "NSData+Crypto.h" #import +#import "aes.h" +#define AES_SIZE 8096 +#define AES_KEY_SIZE 16 + +static const NSString *kAES128ECError = @"kAES128ECError"; @implementation NSString (SOGoCryptoExtension) @@ -349,4 +354,135 @@ return [[NSData encodeDataAsHexString: [d asLM]] uppercaseString]; } +/** + * Encrypts the data using AES 128 ECB mechanism + * + * @param passwordScheme The 128 bits password key + * @param encodedURL YES if the special base64 characters shall be escaped for URL + * @param ex Exception pointer + * @return If successful, encrypted string in base64 + */ +- (NSString *) encodeAES128ECBBase64:(NSString *)passwordScheme encodedURL:(BOOL)encodedURL exception:(NSException **)ex +{ + NSData *inputData, *keyData, *outputData; + + if (AES_KEY_SIZE != [passwordScheme length]) { + *ex = [NSException exceptionWithName:kAES128ECError reason: [NSString stringWithFormat:@"Key must be %d bits", (AES_KEY_SIZE * 8)] userInfo: nil]; + return nil; + } + + NSString *value; + int size, i; + uint8_t output[AES_SIZE], input[AES_SIZE]; + + inputData = [self dataUsingEncoding: NSUnicodeStringEncoding]; + keyData = [passwordScheme dataUsingEncoding: NSUnicodeStringEncoding]; + size = [inputData length] + 16; // Add one unicode char size 16 bits (NUL) + + if (inputData == nil) { + *ex = [NSException exceptionWithName:kAES128ECError reason: @"Invalid input data (encrypt)" userInfo: nil]; + return nil; + } + + if (((AES_SIZE / 2) - 16) < [self length]) { + *ex = [NSException exceptionWithName:kAES128ECError reason: [NSString stringWithFormat:@"Invalid size (encrypt). max size is %d. Current size : %d", ((AES_SIZE / 2) - 16), [self length]] userInfo: nil]; + return nil; + } + + memset(output, 0x00, AES_SIZE); + memset(input, 0x00, AES_SIZE); + + [inputData getBytes: input length: [inputData length]]; + input[[inputData length]] = '\0'; // Add NUL + + + for(i = 0 ; i < (AES_SIZE / 16) ; ++i) + { + AES128_ECB_encrypt(input + (i*16), [keyData bytes], output+(i*16)); + } + + outputData = [NSData dataWithBytes: (char *)output length: size]; + + if (outputData) { + value = [outputData base64EncodedStringWithOptions: 0]; + if (encodedURL) { + value = [value stringByReplacingOccurrencesOfString: @"+" withString: @"."]; + value = [value stringByReplacingOccurrencesOfString: @"/" withString: @"_"]; + value = [value stringByReplacingOccurrencesOfString: @"=" withString: @"-"]; + } + + return value; + } else { + *ex = [NSException exceptionWithName:kAES128ECError reason:@"Empty data" userInfo: nil]; + } + + return nil; +} + +/** + * Decrypts the base64 data using AES 128 ECB mechanism + * + * @param passwordScheme The 128 bits password key + * @param encodedURL YES if the special base64 characters has been escaped for URL + * @param ex Exception pointer + * @return If successful, decrypted string + */ +- (NSString *) decodeAES128ECBBase64:(NSString *)passwordScheme encodedURL:(BOOL)encodedURL exception:(NSException **)ex +{ + NSData *inputData, *keyData, *outputData; + NSString *value, *inputString, *tmpStr; + int i; + uint8_t output[AES_SIZE]; + + if (AES_KEY_SIZE != [passwordScheme length]) { + *ex = [NSException exceptionWithName:kAES128ECError reason: [NSString stringWithFormat:@"Key must be %d bits", (AES_KEY_SIZE * 8)] userInfo: nil]; + return nil; + } + + + value = nil; + keyData = [passwordScheme dataUsingEncoding: NSUnicodeStringEncoding]; + memset(output, 0x00, AES_SIZE); + + inputString = [NSString stringWithString: self]; + if (encodedURL) { + inputString = [inputString stringByReplacingOccurrencesOfString: @"." withString: @"+"]; + inputString = [inputString stringByReplacingOccurrencesOfString: @"_" withString: @"/"]; + inputString = [inputString stringByReplacingOccurrencesOfString: @"-" withString: @"="]; + } + + inputData = [[NSData alloc] initWithBase64EncodedString:inputString options:0]; + + if (inputData == nil) { + *ex = [NSException exceptionWithName:kAES128ECError reason: @"Invalid input data (decrypt)" userInfo: nil]; + return nil; + } + + if ((AES_SIZE) < [inputData length]) { + *ex = [NSException exceptionWithName:kAES128ECError reason: [NSString stringWithFormat:@"Invalid size (decrypt). max size is %d. Current size : %d", AES_SIZE, [inputData length]] userInfo: nil]; + return nil; + } + + for(i = 0; i < (AES_SIZE / 16); ++i) + { + AES128_ECB_decrypt([inputData bytes] + (i*16), [keyData bytes], output + (i*16)); + } + + outputData = [NSData dataWithBytes: output length:([inputData length] - 16)]; + if (outputData) { + tmpStr = [[NSString alloc] initWithData: outputData encoding: NSUnicodeStringEncoding]; + if (tmpStr) { + value = [NSString stringWithUTF8String: [tmpStr UTF8String]]; + } else { + *ex = [NSException exceptionWithName:kAES128ECError reason:@"Empty converted decrypted data" userInfo: nil]; + value = nil; + } + [tmpStr release]; + } else { + *ex = [NSException exceptionWithName:kAES128ECError reason:@"Empty data" userInfo: nil]; + } + + return value; +} + @end diff --git a/SoObjects/SOGo/SOGoDAVAuthenticator.h b/SoObjects/SOGo/SOGoDAVAuthenticator.h index acbdb2349..57dea67f8 100644 --- a/SoObjects/SOGo/SOGoDAVAuthenticator.h +++ b/SoObjects/SOGo/SOGoDAVAuthenticator.h @@ -29,6 +29,8 @@ @interface SOGoDAVAuthenticator : SoHTTPAuthenticator +WOContext *context; + + (id) sharedSOGoDAVAuthenticator; @end diff --git a/SoObjects/SOGo/SOGoDAVAuthenticator.m b/SoObjects/SOGo/SOGoDAVAuthenticator.m index 789a3aeec..82d1b3a08 100644 --- a/SoObjects/SOGo/SOGoDAVAuthenticator.m +++ b/SoObjects/SOGo/SOGoDAVAuthenticator.m @@ -41,12 +41,19 @@ { static SOGoDAVAuthenticator *auth = nil; - if (!auth) + if (!auth) { auth = [self new]; + context = nil; + } return auth; } +- (void) setContext: (WOContext *) _context +{ + context = _context; +} + - (BOOL) checkLogin: (NSString *) _login password: (NSString *) _pwd { @@ -68,6 +75,10 @@ expire: &expire grace: &grace] && perr == PolicyNoError); + if (context) { + [SOGoUser getEncryptedUsernameIfNeeded: [_login stringByReplacingString: @"%40" + withString: @"@"] withContext: context]; // Create cache entry + } if (!rc) { sd = [SOGoSystemDefaults sharedSystemDefaults]; diff --git a/SoObjects/SOGo/SOGoObject.m b/SoObjects/SOGo/SOGoObject.m index 1ea37ee78..227904f2f 100644 --- a/SoObjects/SOGo/SOGoObject.m +++ b/SoObjects/SOGo/SOGoObject.m @@ -360,7 +360,7 @@ NSString *usersUrl; usersUrl = [NSString stringWithFormat: @"%@%@/", - [[WOApplication application] davURLAsString], owner]; + [[WOApplication application] davURLAsString], [SOGoUser getEncryptedUsernameIfNeeded: owner withContext: context]]; ownerHREF = davElementWithContent (@"href", XMLNS_WEBDAV, usersUrl); return [davElementWithContent (@"owner", XMLNS_WEBDAV, ownerHREF) @@ -1222,7 +1222,7 @@ davCurrentUserPrincipal = nil; else { - s = [NSString stringWithFormat: @"/SOGo/dav/%@", login]; + s = [NSString stringWithFormat: @"/SOGo/dav/%@", [SOGoUser getEncryptedUsernameIfNeeded:login withContext:[self context]]]; userHREF = davElementWithContent (@"href", XMLNS_WEBDAV, s); davCurrentUserPrincipal = [davElementWithContent (@"current-user-principal", diff --git a/SoObjects/SOGo/SOGoSystemDefaults.h b/SoObjects/SOGo/SOGoSystemDefaults.h index 359b715a3..0705a1090 100644 --- a/SoObjects/SOGo/SOGoSystemDefaults.h +++ b/SoObjects/SOGo/SOGoSystemDefaults.h @@ -131,6 +131,9 @@ NSComparisonResult languageSort(id el1, id el2, void *context); - (NSArray *) disableSharing; +- (BOOL)isURLEncryptionEnabled; +- (NSString *)urlEncryptionPassphrase; + @end #endif /* SOGOSYSTEMDEFAULTS_H */ diff --git a/SoObjects/SOGo/SOGoSystemDefaults.m b/SoObjects/SOGo/SOGoSystemDefaults.m index f517c7a3e..7462c9b53 100644 --- a/SoObjects/SOGo/SOGoSystemDefaults.m +++ b/SoObjects/SOGo/SOGoSystemDefaults.m @@ -819,5 +819,20 @@ NSComparisonResult languageSort(id el1, id el2, void *context) return disableSharing; } +- (BOOL)isURLEncryptionEnabled { + return [self boolForKey: @"SOGoURLEncryptionEnabled"]; +} + +- (NSString *) urlEncryptionPassphrase +{ + NSString *passphrase; + + passphrase = [self stringForKey: @"SOGoURLEncryptionPassphrase"]; + + if (!passphrase) + passphrase = @"SOGoSuperSecret0"; // Default passphrase + + return passphrase; +} @end diff --git a/SoObjects/SOGo/SOGoUser.h b/SoObjects/SOGo/SOGoUser.h index 7ce25a30b..c82456138 100644 --- a/SoObjects/SOGo/SOGoUser.h +++ b/SoObjects/SOGo/SOGoUser.h @@ -135,6 +135,10 @@ - (SOGoAppointmentFolder *) personalCalendarFolderInContext: (WOContext *) context; - (SOGoContactFolder *) personalContactsFolderInContext: (WOContext *) context; +/* Encryption */ ++ (NSString *)getEncryptedUsernameIfNeeded:(NSString *)username withContext:(WOContext *)context; ++ (NSString *)getDecryptedUsernameIfNeeded:(NSString *)username withContext:(WOContext *)context; + @end #endif /* __SOGoUser_H__ */ diff --git a/SoObjects/SOGo/SOGoUser.m b/SoObjects/SOGo/SOGoUser.m index 85f638c9e..b0241d485 100644 --- a/SoObjects/SOGo/SOGoUser.m +++ b/SoObjects/SOGo/SOGoUser.m @@ -39,6 +39,7 @@ #import "SOGoUserManager.h" #import "SOGoUserSettings.h" #import "WOResourceManager+SOGo.h" +#import "NSString+Crypto.h" #if defined(MFA_CONFIG) #include @@ -46,6 +47,8 @@ #import "SOGoUser.h" +static const NSString *kEncryptedUserNamePrefix = @"uenc-"; + @implementation SoUser (SOGoExtension) - (SOGoDomainDefaults *) userDefaults @@ -1197,4 +1200,83 @@ return [accessValue boolValue]; } +/* Encryption */ ++ (NSString *) getEncryptedUsernameIfNeeded:(NSString *)username withContext:(WOContext *)context +{ + NSException *exception; + NSString *tmp, *cacheKey; + SOGoCache *cache; + WORequest *request; + + if (![[SOGoSystemDefaults sharedSystemDefaults] isURLEncryptionEnabled]) + return username; + + request = [context request]; + cache = [SOGoCache sharedCache]; + cacheKey = [NSString stringWithFormat: @"%@%@%@", kEncryptedUserNamePrefix, username, [request requestHandlerKey]]; + exception = nil; + tmp = nil; + + tmp = [cache valueForKey: cacheKey]; + if (tmp) { + return tmp; + } else { + if (username && [username length] > 0) { + tmp = [username encodeAES128ECBBase64: [[SOGoSystemDefaults sharedSystemDefaults] urlEncryptionPassphrase] encodedURL:YES exception: &exception]; + if (!exception) { + [cache setValue:tmp forKey:cacheKey]; + return tmp; + } else { + [self errorWithFormat: @"URL Encryption error : %@", [exception reason]]; + return username; + } + } else { + [self logWithFormat: @"Empty username for encryption"]; + return username; + } + } +} + ++ (NSString *) getDecryptedUsernameIfNeeded:(NSString *)username withContext:(WOContext *)context +{ + NSException *exception; + NSString *tmp, *cacheKey; + SOGoCache *cache; + WORequest *request; + + if (![[SOGoSystemDefaults sharedSystemDefaults] isURLEncryptionEnabled]) + return username; + + request = [context request]; + cache = [SOGoCache sharedCache]; + cacheKey = [NSString stringWithFormat: @"%@%@%@", kEncryptedUserNamePrefix, username, [request requestHandlerKey]]; + exception = nil; + tmp = nil; + + tmp = [cache valueForKey: cacheKey]; + if (tmp) { + return tmp; + } else { + if (username && [username length] > 0) { + tmp = [username decodeAES128ECBBase64: [[SOGoSystemDefaults sharedSystemDefaults] urlEncryptionPassphrase] encodedURL:YES exception: &exception]; + if (tmp) { + [cache setValue:tmp forKey:cacheKey]; + } else { + [cache setValue:username forKey:cacheKey]; + } + if (!exception) { + return tmp; + } else { + [self errorWithFormat: @"URL Decryption error : %@", [exception reason]]; + return username; + } + } else { + [self errorWithFormat: @"Empty username for decryption"]; + return username; + } + + } +} + + @end /* SOGoUser */ diff --git a/SoObjects/SOGo/SOGoUserFolder.m b/SoObjects/SOGo/SOGoUserFolder.m index ecf16c0a9..31b882beb 100644 --- a/SoObjects/SOGo/SOGoUserFolder.m +++ b/SoObjects/SOGo/SOGoUserFolder.m @@ -24,6 +24,8 @@ #import #import +#import + #import #import #import @@ -36,6 +38,7 @@ #import "NSArray+Utilities.h" #import "NSDictionary+Utilities.h" #import "NSString+Utilities.h" +#import "NSString+Crypto.h" #import "SOGoUserManager.h" #import "SOGoPermissions.h" #import "SOGoSystemDefaults.h" @@ -83,6 +86,17 @@ return children; } +- (NSString *) nameInContainer { + NSString *foo = [SOGoUser getEncryptedUsernameIfNeeded: [super nameInContainer] withContext: context]; + return foo; +} + +- (NSString *) davURLAsString +{ + return [[container davURLAsString] + stringByAppendingFormat: @"%@/", [self nameInContainer]]; +} + /* ownership */ - (NSString *) ownerInContext: (WOContext *) _ctx diff --git a/UI/MainUI/SOGoRootPage.m b/UI/MainUI/SOGoRootPage.m index 9671be805..32f1237e7 100644 --- a/UI/MainUI/SOGoRootPage.m +++ b/UI/MainUI/SOGoRootPage.m @@ -343,6 +343,8 @@ static const NSString *kJwtKey = @"jwt"; forKey: @"expire"]; [json setObject: [NSNumber numberWithInt: grace] forKey: @"grace"]; + [json setObject: [SOGoUser getEncryptedUsernameIfNeeded: username withContext: context] + forKey: @"username"]; response = [self responseWithStatus: 200 andJSONRepresentation: json]; diff --git a/UI/WebServerResources/js/Common/Authentication.service.js b/UI/WebServerResources/js/Common/Authentication.service.js index ce41aa8af..b02d7a3a1 100644 --- a/UI/WebServerResources/js/Common/Authentication.service.js +++ b/UI/WebServerResources/js/Common/Authentication.service.js @@ -102,7 +102,7 @@ else if (typeof data.totpDisabled != 'undefined') { d.resolve({ cn: data.cn, - url: redirectUrl(username, domain), + url: redirectUrl(data.username, domain), totpdisabled: 1 }); } @@ -124,12 +124,12 @@ else { d.resolve({ cn: data.cn, - url: redirectUrl(username, domain) + url: redirectUrl(data.username, domain) }); } } else { - d.resolve({ url: redirectUrl(username, domain) }); + d.resolve({ url: redirectUrl(data.username, domain) }); } } }, function(error) {