mirror of
https://github.com/inverse-inc/sogo.git
synced 2026-07-18 22:54:55 +00:00
feat(core): Add URL encryption for GDPR compliancy
This commit is contained in:
@@ -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
@@ -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];
|
||||
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -29,6 +29,8 @@
|
||||
|
||||
@interface SOGoDAVAuthenticator : SoHTTPAuthenticator <SOGoAuthenticator>
|
||||
|
||||
WOContext *context;
|
||||
|
||||
+ (id) sharedSOGoDAVAuthenticator;
|
||||
|
||||
@end
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -131,6 +131,9 @@ NSComparisonResult languageSort(id el1, id el2, void *context);
|
||||
|
||||
- (NSArray *) disableSharing;
|
||||
|
||||
- (BOOL)isURLEncryptionEnabled;
|
||||
- (NSString *)urlEncryptionPassphrase;
|
||||
|
||||
@end
|
||||
|
||||
#endif /* SOGOSYSTEMDEFAULTS_H */
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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__ */
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user