feat(core): Add URL encryption for GDPR compliancy

This commit is contained in:
smizrahi
2023-10-17 16:17:25 +02:00
parent ee9899fe3d
commit 31c6f78913
15 changed files with 299 additions and 13 deletions
@@ -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`.
|=======================================================================
+11 -4
View File
@@ -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;
@@ -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];
+7 -1
View File
@@ -26,6 +26,7 @@
#define NSSTRING_CRYPTO_H
#import <Foundation/NSString.h>
#import <Foundation/Foundation.h>
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 */
+136
View File
@@ -29,6 +29,11 @@
#import "NSData+Crypto.h"
#import <NGExtensions/NGBase64Coding.h>
#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
+2
View File
@@ -29,6 +29,8 @@
@interface SOGoDAVAuthenticator : SoHTTPAuthenticator <SOGoAuthenticator>
WOContext *context;
+ (id) sharedSOGoDAVAuthenticator;
@end
+12 -1
View File
@@ -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];
+2 -2
View File
@@ -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",
+3
View File
@@ -131,6 +131,9 @@ NSComparisonResult languageSort(id el1, id el2, void *context);
- (NSArray *) disableSharing;
- (BOOL)isURLEncryptionEnabled;
- (NSString *)urlEncryptionPassphrase;
@end
#endif /* SOGOSYSTEMDEFAULTS_H */
+15
View File
@@ -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
+4
View File
@@ -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__ */
+82
View File
@@ -39,6 +39,7 @@
#import "SOGoUserManager.h"
#import "SOGoUserSettings.h"
#import "WOResourceManager+SOGo.h"
#import "NSString+Crypto.h"
#if defined(MFA_CONFIG)
#include <liboath/oath.h>
@@ -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 */
+14
View File
@@ -24,6 +24,8 @@
#import <NGObjWeb/WOContext+SoObjects.h>
#import <NGObjWeb/WORequest+So.h>
#import <SaxObjC/XMLNamespaces.h>
#import <NGExtensions/NSObject+Logs.h>
#import <NGExtensions/NSString+misc.h>
#import <DOM/DOMNode.h>
@@ -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
+2
View File
@@ -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];
@@ -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) {