Skip to content

Commit

Permalink
feat(core): Add URL encryption for GDPR compliancy
Browse files Browse the repository at this point in the history
  • Loading branch information
WoodySlum committed Oct 17, 2023
1 parent ee9899f commit 31c6f78
Show file tree
Hide file tree
Showing 15 changed files with 299 additions and 13 deletions.
4 changes: 4 additions & 0 deletions Documentation/SOGoInstallationGuide.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
|=======================================================================
Expand Down
15 changes: 11 additions & 4 deletions Main/SOGo.m
Original file line number Diff line number Diff line change
Expand Up @@ -349,8 +349,11 @@ - (id) authenticatorInContext: (WOContext *) context
{
if ([[context request] handledByDefaultHandler])
authenticator = [SOGoWebAuthenticator sharedSOGoWebAuthenticator];
else
else {
authenticator = [SOGoDAVAuthenticator sharedSOGoDAVAuthenticator];
[authenticator setContext: context];
}

}

return authenticator;
Expand All @@ -363,10 +366,14 @@ - (id) lookupUser: (NSString *) _key
{
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;
Expand Down
4 changes: 2 additions & 2 deletions SoObjects/Appointments/SOGoUserFolder+Appointments.m
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ - (NSArray *) davCalendarUserAddressSet
}

tag = [NSArray arrayWithObjects: @"href", XMLNS_WEBDAV, @"D",
[NSString stringWithFormat: @"/SOGo/dav/%@/", nameInContainer],
[NSString stringWithFormat: @"/SOGo/dav/%@/", [self nameInContainer]],
nil];
[addresses addObjectUniquely: tag];

Expand Down Expand Up @@ -128,7 +128,7 @@ - (NSArray *) davEmailAddressSet
}

tag = [NSArray arrayWithObjects: @"href", XMLNS_WEBDAV, @"D",
[NSString stringWithFormat: @"/SOGo/dav/%@/", nameInContainer],
[NSString stringWithFormat: @"/SOGo/dav/%@/", [self nameInContainer]],
nil];
[addresses addObjectUniquely: tag];

Expand Down
8 changes: 7 additions & 1 deletion SoObjects/SOGo/NSString+Crypto.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -35,8 +36,10 @@ typedef enum {
} keyEncoding;

@class NSObject;
@class NSScanner;

@interface NSString (SOGoCryptoExtension)
@interface
NSString(SOGoCryptoExtension)

- (BOOL) isEqualToCrypted: (NSString *) cryptedPassword
withDefaultScheme: (NSString *) theScheme
Expand All @@ -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 changes: 136 additions & 0 deletions SoObjects/SOGo/NSString+Crypto.m
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -349,4 +354,135 @@ - (NSString *) asLMHash
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 changes: 2 additions & 0 deletions SoObjects/SOGo/SOGoDAVAuthenticator.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@

@interface SOGoDAVAuthenticator : SoHTTPAuthenticator <SOGoAuthenticator>

WOContext *context;

+ (id) sharedSOGoDAVAuthenticator;

@end
Expand Down
13 changes: 12 additions & 1 deletion SoObjects/SOGo/SOGoDAVAuthenticator.m
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,19 @@ + (id) sharedSOGoDAVAuthenticator
{
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
{
Expand All @@ -68,6 +75,10 @@ - (BOOL) checkLogin: (NSString *) _login
expire: &expire
grace: &grace]
&& perr == PolicyNoError);
if (context) {
[SOGoUser getEncryptedUsernameIfNeeded: [_login stringByReplacingString: @"%40"
withString: @"@"] withContext: context]; // Create cache entry
}
if (!rc)
{
sd = [SOGoSystemDefaults sharedSystemDefaults];
Expand Down
4 changes: 2 additions & 2 deletions SoObjects/SOGo/SOGoObject.m
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,7 @@ - (SOGoWebDAVValue *) davOwner
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)
Expand Down Expand Up @@ -1222,7 +1222,7 @@ - (SOGoWebDAVValue *) davCurrentUserPrincipal
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",
Expand Down
3 changes: 3 additions & 0 deletions SoObjects/SOGo/SOGoSystemDefaults.h
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,9 @@ NSComparisonResult languageSort(id el1, id el2, void *context);

- (NSArray *) disableSharing;

- (BOOL)isURLEncryptionEnabled;
- (NSString *)urlEncryptionPassphrase;

@end

#endif /* SOGOSYSTEMDEFAULTS_H */
15 changes: 15 additions & 0 deletions SoObjects/SOGo/SOGoSystemDefaults.m
Original file line number Diff line number Diff line change
Expand Up @@ -819,5 +819,20 @@ - (NSArray *) disableSharing
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 changes: 4 additions & 0 deletions SoObjects/SOGo/SOGoUser.h
Original file line number Diff line number Diff line change
Expand Up @@ -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__ */
Loading

0 comments on commit 31c6f78

Please sign in to comment.