From 08230b33bb73b0b127723881ce80d0e98c059b9a Mon Sep 17 00:00:00 2001 From: Ludovic Marcotte Date: Thu, 31 May 2012 13:24:32 +0000 Subject: [PATCH] See ChangeLog Monotone-Parent: 747fe8c2134a990c1cdf3b494c2b7776021cb736 Monotone-Revision: 3c4baa92c5634d4da061da739a9b544d04c44586 Monotone-Author: ludovic@Sophos.ca Monotone-Date: 2012-05-31T13:24:32 --- ChangeLog | 2 + SoObjects/SOGo/GNUmakefile | 4 + SoObjects/SOGo/LDAPSource.m | 28 +- SoObjects/SOGo/NSData+Crypto.h | 61 +++ SoObjects/SOGo/NSData+Crypto.m | 620 ++++++++++++++++++++++++++++ SoObjects/SOGo/NSString+Crypto.h | 63 +++ SoObjects/SOGo/NSString+Crypto.m | 304 ++++++++++++++ SoObjects/SOGo/NSString+Utilities.h | 4 - SoObjects/SOGo/NSString+Utilities.m | 52 --- SoObjects/SOGo/SOGoUserManager.m | 1 + SoObjects/SOGo/SQLSource.h | 1 + SoObjects/SOGo/SQLSource.m | 70 ++-- 12 files changed, 1092 insertions(+), 118 deletions(-) create mode 100644 SoObjects/SOGo/NSData+Crypto.h create mode 100644 SoObjects/SOGo/NSData+Crypto.m create mode 100644 SoObjects/SOGo/NSString+Crypto.h create mode 100644 SoObjects/SOGo/NSString+Crypto.m diff --git a/ChangeLog b/ChangeLog index 776e2f1a4..ba7af70b4 100644 --- a/ChangeLog +++ b/ChangeLog @@ -4,6 +4,8 @@ all attribute names * SoObject/SOGo/LDAPSource.m - moved the lowercasing of attributes to SOPE. + * Added patch from bug #1608. This add many more password + schemes for SQL authentication. 2012-05-29 Francis Lachapelle diff --git a/SoObjects/SOGo/GNUmakefile b/SoObjects/SOGo/GNUmakefile index c099e3c49..da52efc75 100644 --- a/SoObjects/SOGo/GNUmakefile +++ b/SoObjects/SOGo/GNUmakefile @@ -46,6 +46,8 @@ SOGo_HEADER_FILES = \ NSObject+Utilities.h \ NSString+DAV.h \ NSString+Utilities.h \ + NSString+Crypto.h \ + NSData+Crypto.h \ NSURL+DAV.h \ \ SOGoAuthenticator.h \ @@ -114,6 +116,8 @@ SOGo_OBJC_FILES = \ NSObject+Utilities.m \ NSString+DAV.m \ NSString+Utilities.m \ + NSString+Crypto.m \ + NSData+Crypto.m \ NSURL+DAV.m \ \ SOGoSession.m \ diff --git a/SoObjects/SOGo/LDAPSource.m b/SoObjects/SOGo/LDAPSource.m index 8a066ee9f..b2a4faff0 100644 --- a/SoObjects/SOGo/LDAPSource.m +++ b/SoObjects/SOGo/LDAPSource.m @@ -40,6 +40,7 @@ #import "LDAPSourceSchema.h" #import "NSArray+Utilities.h" #import "NSString+Utilities.h" +#import "NSString+Crypto.h" #import "SOGoDomainDefaults.h" #import "SOGoSystemDefaults.h" @@ -581,26 +582,13 @@ andMultipleBookingsField: (NSString *) newMultipleBookingsField */ - (NSString *) _encryptPassword: (NSString *) plainPassword { - if ([_userPasswordAlgorithm caseInsensitiveCompare: @"none"] == NSOrderedSame) - { - return plainPassword; - } - else if ([_userPasswordAlgorithm caseInsensitiveCompare: @"crypt"] == NSOrderedSame) - { - return [NSString stringWithFormat: @"{CRYPT}%@", [plainPassword asCryptStringUsingSalt: [plainPassword asMD5String]]]; - } - else if ([_userPasswordAlgorithm caseInsensitiveCompare: @"md5"] == NSOrderedSame) - { - return [NSString stringWithFormat: @"{MD5}%@", [plainPassword asMD5String]]; - } - else if ([_userPasswordAlgorithm caseInsensitiveCompare: @"sha"] == NSOrderedSame) - { - return [NSString stringWithFormat: @"{SHA}%@", [plainPassword asSHA1String]]; - } - - [self errorWithFormat: @"Unsupported user-password algorithm: %@", _userPasswordAlgorithm]; - - return plainPassword; + NSString *pass; + pass = [plainPassword asCryptedPassUsingScheme: _userPasswordAlgorithm]; + + if (pass == nil) + [self errorWithFormat: @"Unsupported user-password algorithm: %@", _userPasswordAlgorithm]; + + return [NSString stringWithFormat: @"{%@}%@", _userPasswordAlgorithm, pass]; } // diff --git a/SoObjects/SOGo/NSData+Crypto.h b/SoObjects/SOGo/NSData+Crypto.h new file mode 100644 index 000000000..547e2d0b6 --- /dev/null +++ b/SoObjects/SOGo/NSData+Crypto.h @@ -0,0 +1,61 @@ +/* NSData+Crypto.h - this file is part of SOGo + * + * Copyright (C) 2012 Nicolas Höft + * Copyright (C) 2012 Inverse inc. + * + * Author: Nicolas Höft + * 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. + */ + +#ifndef NSDATA_CRYPTO_H +#define NSDATA_CRYPTO_H + +#import + +@class NSObject; + +@interface NSData (SOGoCryptoExtension) + +- (NSData *) asCryptedPassUsingScheme: (NSString *) passwordScheme + withSalt: (NSData *) theSalt; + +- (NSData *) asMD5; +- (NSData *) asSMD5UsingSalt: (NSData *) theSalt; +- (NSData *) asSHA1; +- (NSData *) asSSHAUsingSalt: (NSData *) theSalt; +- (NSData *) asSHA256; +- (NSData *) asSSHA256UsingSalt: (NSData *) theSalt; +- (NSData *) asSHA512; +- (NSData *) asSSHA512UsingSalt: (NSData *) theSalt; +- (NSData *) asCramMD5; + +- (NSData *) asCryptUsingSalt: (NSData *) theSalt; +- (NSData *) asMD5CryptUsingSalt: (NSData *) theSalt; + +- (NSData *) extractSalt: (NSString *) theScheme; + ++ (NSData *) generateSaltForLength: (unsigned int) theLength + withBase64: (BOOL) doBase64; ++ (NSData *) generateSaltForLength: (unsigned int) theLength; + ++ (NSString *) encodeDataAsHexString: (NSData* ) theData; ++ (NSData *) decodeDataFromHexString: (NSString* ) theString; + +@end + +#endif /* NSDATA_CRYPTO_H */ diff --git a/SoObjects/SOGo/NSData+Crypto.m b/SoObjects/SOGo/NSData+Crypto.m new file mode 100644 index 000000000..c50ad96a0 --- /dev/null +++ b/SoObjects/SOGo/NSData+Crypto.m @@ -0,0 +1,620 @@ +/* NSData+Crypto.m - this file is part of SOGo + * + * Copyright (C) 2012 Nicolas Höft + * Copyright (C) 2012 Inverse inc. + * + * Author: Nicolas Höft + * 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. + */ + +#ifndef __OpenBSD__ +#include +#endif + +#include +#include +#include +#include + +#define _XOPEN_SOURCE 1 +#include +#include +#include +#include + +#import +#import +#import "NSData+Crypto.h" + +unsigned charTo4Bits(char c); + + +@implementation NSData (SOGoCryptoExtension) + +/** + * Covert binary data to hex encoded data (lower-case). + * + * @param theData The NSData to be converted into a hex-encoded string. + * @return Hex-Encoded data + */ ++ (NSString *) encodeDataAsHexString: (NSData *) theData +{ + unsigned int byteLength = [theData length], byteCounter = 0; + unsigned int stringLength = (byteLength * 2) + 1, stringCounter = 0; + unsigned char dstBuffer[stringLength]; + unsigned char srcBuffer[byteLength]; + unsigned char *srcPtr = srcBuffer; + [theData getBytes: srcBuffer]; + const unsigned char t[16] = "0123456789abcdef"; + + for (; byteCounter < byteLength; byteCounter++) + { + unsigned src = *srcPtr; + dstBuffer[stringCounter++] = t[src >> 4]; + dstBuffer[stringCounter++] = t[src & 15]; + srcPtr++; + } + + dstBuffer[stringCounter] = '\0'; + return [NSString stringWithUTF8String: (char*)dstBuffer]; +} + +/** + * Covert hex-encoded data to binary data. + * + * @param theString The hex-encoded string to be converted into binary data (works both for upper and lowe case characters) + * @return binary data or nil if unsuccessful + */ ++ (NSData *) decodeDataFromHexString: (NSString *) theString +{ + unsigned int stringLength = [theString length]; + unsigned int byteLength = stringLength/2; + unsigned int byteCounter = 0; + unsigned char srcBuffer[stringLength]; + [theString getCString:(char *)srcBuffer]; + unsigned char *srcPtr = srcBuffer; + unsigned char dstBuffer[byteLength]; + unsigned char *dst = dstBuffer; + while (byteCounter < byteLength) + { + unsigned char c = *srcPtr++; + unsigned char d = *srcPtr++; + unsigned hi = 0, lo = 0; + hi = charTo4Bits(c); + lo = charTo4Bits(d); + if (hi == 255 || lo == 255) + { + //errorCase + return nil; + } + dstBuffer[byteCounter++] = ((hi << 4) | lo); + } + return [NSData dataWithBytes: dst length: byteLength]; +} + +/** + * Generate a binary key which can be used for salting hashes. + * + * @param theLength length of the binary data to be generated in bytes + * @return Pseudo-random binary data with length theLength or nil, if an error occured + */ ++ (NSData *) generateSaltForLength: (unsigned int) theLength +{ + return [NSData generateSaltForLength: theLength withBase64: NO]; +} + +/** + * Generate a binary key which can be used for salting hashes. When using + * with doBase64 == YES then the data will be longer than theLength + * + * @param theLength Length of the binary data to be generated in bytes + * @param doBase64 Convert the data into Base-64 before retuning it, be aware that this makes the binary data longer + * @return Pseudo-random binary data with length theLength or nil, if an error occured + */ ++ (NSData *) generateSaltForLength: (unsigned int) theLength + withBase64: (BOOL) doBase64 +{ + char *buf; + int fd; + NSData *data; + + fd = open("/dev/urandom", O_RDONLY); + + if (fd > 0) + { + buf = (char *)malloc(theLength); + read(fd, buf, theLength); + close(fd); + + data = [NSData dataWithBytesNoCopy: buf length: theLength freeWhenDone: YES]; + if(doBase64 == YES) + { + return [data dataByEncodingBase64WithLineLength: 1024]; + } + return data; + } + return nil; +} + +/** + * Encrypt/Hash the data with a given scheme + * + * @param passwordScheme The scheme to use for hashing/encryption. + * @param theSalt The salt to be used. If none is given but needed, it will be generated + * @return Binary data from the encryption by the specified scheme. On error the funciton returns nil. + */ +- (NSData *) asCryptedPassUsingScheme: (NSString *) passwordScheme + withSalt: (NSData *) theSalt +{ + if ([passwordScheme caseInsensitiveCompare: @"none"] == NSOrderedSame || + [passwordScheme caseInsensitiveCompare: @"plain"] == NSOrderedSame || + [passwordScheme caseInsensitiveCompare: @"cleartext"] == NSOrderedSame) + { + return self; + } + else if ([passwordScheme caseInsensitiveCompare: @"crypt"] == NSOrderedSame) + { + return [self asCryptUsingSalt: theSalt]; + } + else if ([passwordScheme caseInsensitiveCompare: @"md5-crypt"] == NSOrderedSame) + { + return [self asMD5CryptUsingSalt: theSalt]; + } + else if ([passwordScheme caseInsensitiveCompare: @"md5"] == NSOrderedSame || + [passwordScheme caseInsensitiveCompare: @"plain-md5"] == NSOrderedSame || + [passwordScheme caseInsensitiveCompare: @"ldap-md5"] == NSOrderedSame) + { + return [self asMD5]; + } + else if ([passwordScheme caseInsensitiveCompare: @"cram-md5"] == NSOrderedSame) + { + return [self asCramMD5]; + } + else if ([passwordScheme caseInsensitiveCompare: @"smd5"] == NSOrderedSame) + { + return [self asSMD5UsingSalt: theSalt]; + } + else if ([passwordScheme caseInsensitiveCompare: @"sha"] == NSOrderedSame) + { + return [self asSHA1]; + } + else if ([passwordScheme caseInsensitiveCompare: @"ssha"] == NSOrderedSame) + { + return [self asSSHAUsingSalt: theSalt]; + } + else if ([passwordScheme caseInsensitiveCompare: @"sha256"] == NSOrderedSame) + { + return [self asSHA256]; + } + else if ([passwordScheme caseInsensitiveCompare: @"ssha256"] == NSOrderedSame) + { + return [self asSSHA256UsingSalt: theSalt]; + } + else if ([passwordScheme caseInsensitiveCompare: @"sha512"] == NSOrderedSame) + { + return [self asSHA512]; + } + else if ([passwordScheme caseInsensitiveCompare: @"ssha512"] == NSOrderedSame) + { + return [self asSSHA512UsingSalt: theSalt]; + } + // in case the scheme was not detected, return nil + return nil; +} + + +/** + * Hash the data with MD5. Uses openssl functions to generate it + * + * @return Binary data from MD5 hashing. On error the funciton returns nil. + */ +- (NSData *) asMD5 +{ + unsigned char md5[MD5_DIGEST_LENGTH]; + memset(md5, 0, MD5_DIGEST_LENGTH); + + MD5([self bytes], [self length], md5); + + return [NSData dataWithBytes: md5 length: MD5_DIGEST_LENGTH]; +} + +/** + * Hash the data with CRAM-MD5. Uses openssl functions to generate it. + * + * Note that the actual CRAM-MD5 algorithm also needs a challenge + * but this is not provided, this function actually calculalates + * only the context data which can be used for the challange-response + * algorithm then. This is just the underlying algorithm to store the passwords. + * + * The code is adopts the dovecot behaviour of storing the passwords + * + * @return Binary data from CRAM-MD5 'hashing'. On error the funciton returns nil. + */ +- (NSData *) asCramMD5 +{ + + MD5_CTX ctx; + unsigned char inner[64]; + unsigned char outer[64]; + unsigned char result[32]; + unsigned char *r; + int i; + int len; + NSData *key; + + if ([self length] > 64) + { + key = [self asMD5]; + } + else + { + key = self; + } + + len = [key length]; + // fill with both inner and outer with key + memcpy(inner, [key bytes], len); + // make sure the rest of the bytes is zero + memset(inner + len, 0, 64 - len); + memcpy(outer, inner, 64); + + for (i = 0; i < 64; i++) + { + inner[i] ^= 0x36; + outer[i] ^= 0x5c; + } +// this transformation is needed for the correct cast to binary data +#define CDPUT(p, c) { \ + *p = (c) & 0xff; p++; \ + *p = (c) >> 8 & 0xff; p++; \ + *p = (c) >> 16 & 0xff; p++; \ + *p = (c) >> 24 & 0xff; p++; \ +} + + // generate first set of context bytes from outer data + MD5_Init(&ctx); + MD5_Transform(&ctx, outer); + r = result; + // convert this to correct binary data according to RFC 1321 + CDPUT(r, ctx.A); + CDPUT(r, ctx.B); + CDPUT(r, ctx.C); + CDPUT(r, ctx.D); + + // second set with inner data is appended to result string + MD5_Init(&ctx); + MD5_Transform(&ctx, inner); + // convert this to correct binary data + CDPUT(r, ctx.A); + CDPUT(r, ctx.B); + CDPUT(r, ctx.C); + CDPUT(r, ctx.D); + + return [NSData dataWithBytes: result length: 32]; +} + +/** + * Hash the data with SHA1. Uses openssl functions to generate it. + * + * @return Binary data from SHA1 hashing. On error the funciton returns nil. + */ +- (NSData *) asSHA1 +{ + unsigned char sha[SHA_DIGEST_LENGTH]; + memset(sha, 0, SHA_DIGEST_LENGTH); + + SHA1([self bytes], [self length], sha); + + return [NSData dataWithBytes: sha length: SHA_DIGEST_LENGTH]; +} + +/** + * Hash the data with SHA256. Uses openssl functions to generate it. + * + * @return Binary data from SHA256 hashing. On error the funciton returns nil. + */ +- (NSData *) asSHA256 +{ + unsigned char sha[SHA256_DIGEST_LENGTH]; + memset(sha, 0, SHA256_DIGEST_LENGTH); + + SHA256([self bytes], [self length], sha); + + return [NSData dataWithBytes: sha length: SHA256_DIGEST_LENGTH]; +} + +/** + * Hash the data with SHA512. Uses openssl functions to generate it. + * + * @return Binary data from SHA512 hashing. On error the funciton returns nil. + */ +- (NSData *) asSHA512 +{ + unsigned char sha[SHA512_DIGEST_LENGTH]; + memset(sha, 0, SHA512_DIGEST_LENGTH); + + SHA512([self bytes], [self length], sha); + + return [NSData dataWithBytes: sha length: SHA512_DIGEST_LENGTH]; +} + +/** + * Hash the data with SSHA. Uses openssl functions to generate it. + * + * SSHA works following: SSHA(pass, salt) = SHA1(pass + salt) + saltData + * + * @param theSalt The salt to be used must not be nil, if empty, one will be generated + * @return Binary data from SHA1 hashing. On error the funciton returns nil. + */ +- (NSData *) asSSHAUsingSalt: (NSData *) theSalt +{ + // + NSMutableData *sshaData; + + // generate salt, if not available + if ([theSalt length] == 0) theSalt = [NSData generateSaltForLength: 8]; + + // put the pass and salt together as one data array + sshaData = [NSMutableData dataWithData: self]; + [sshaData appendData: theSalt]; + // generate SHA1 from pass + salt + sshaData = [NSMutableData dataWithData: [sshaData asSHA1]]; + // append salt again + [sshaData appendData: theSalt]; + + return sshaData; +} + +/** + * Hash the data with SSHA256. Uses openssl functions to generate it. + * + * SSHA256 works following: SSHA256(pass, salt) = SHA256(pass + salt) + saltData + * + * @param theSalt The salt to be used must not be nil, if empty, one will be generated + * @return Binary data from SHA1 hashing. On error the funciton returns nil. + */ + +- (NSData *) asSSHA256UsingSalt: (NSData *) theSalt +{ + NSMutableData *sshaData; + + // generate salt, if not available + if ([theSalt length] == 0) theSalt = [NSData generateSaltForLength: 8]; + + // put the pass and salt together as one data array + sshaData = [NSMutableData dataWithData: self]; + [sshaData appendData: theSalt]; + // generate SHA1 from pass + salt + sshaData = [NSMutableData dataWithData: [sshaData asSHA256]]; + // append salt again + [sshaData appendData: theSalt]; + + return sshaData; +} + +/** + * Hash the data with SSHA512. Uses openssl functions to generate it. + * + * SSHA works following: SSHA512(pass, salt) = SHA512(pass + salt) + saltData + * + * @param theSalt The salt to be used must not be nil, if empty, one will be generated + * @return Binary data from SHA512 hashing. On error the funciton returns nil. + */ + +- (NSData *) asSSHA512UsingSalt: (NSData *) theSalt +{ + NSMutableData *sshaData; + + // generate salt, if not available + if ([theSalt length] == 0) theSalt = [NSData generateSaltForLength: 8]; + + // put the pass and salt together as one data array + sshaData = [NSMutableData dataWithData: self]; + [sshaData appendData: theSalt]; + // generate SHA1 from pass + salt + sshaData = [NSMutableData dataWithData: [sshaData asSHA512]]; + // append salt again + [sshaData appendData: theSalt]; + + return sshaData; +} + +/** + * Hash the data with SMD5. Uses openssl functions to generate it. + * + * SMD5 works following: SMD5(pass, salt) = MD5(pass + salt) + saltData + * + * @param theSalt The salt to be used must not be nil, if empty, one will be generated + * @return Binary data from SMD5 hashing. On error the funciton returns nil. + */ +- (NSData *) asSMD5UsingSalt: (NSData *) theSalt +{ + // SMD5 works following: SMD5(pass, salt) = MD5(pass + salt) + salt + NSMutableData *smdData; + + // generate salt, if not available + if ([theSalt length] == 0) theSalt = [NSData generateSaltForLength: 8]; + + // put the pass and salt together as one data array + smdData = [NSMutableData dataWithData: self]; + [smdData appendData: theSalt]; + // generate SHA1 from pass + salt + smdData = [NSMutableData dataWithData: [smdData asMD5]]; + // append salt again + [smdData appendData: theSalt]; + + return smdData; +} + + +/** + * Hash the data with CRYPT-MD5 as used in /etc/passwd nowadays. Uses crypt() function to generate it. + * + * + * @param theSalt The salt to be used must not be nil, if empty, one will be generated. It must be printable characters only. + * @return Binary data from CRYPT-MD5 hashing. On error the funciton returns nil. + */ +- (NSData *) asMD5CryptUsingSalt: (NSData *) theSalt +{ + char *buf; + NSMutableData *saltData; + NSString *cryptString; + NSString *saltString; + + if ([theSalt length] == 0) + { + // make sure these characters are all printable by using base64 + theSalt = [NSData generateSaltForLength: 8 withBase64: YES]; + } + cryptString = [[NSString alloc] initWithData: self encoding: NSUTF8StringEncoding]; + + NSString * magic = @"$1$"; + saltData = [NSMutableData dataWithData: [magic dataUsingEncoding: NSUTF8StringEncoding]]; + [saltData appendData: theSalt]; + // terminate with "$" + [saltData appendData: [@"$" dataUsingEncoding: NSUTF8StringEncoding]]; + + saltString = [[NSString alloc] initWithData: saltData encoding: NSUTF8StringEncoding]; + + buf = crypt([cryptString UTF8String], [saltString UTF8String]); + [cryptString release]; + [saltString release]; + if (!buf) + return nil; + return [NSData dataWithBytes: buf length: strlen(buf)]; +} + +/** + * Hash the data using crypt() function. + * + * @param theSalt The salt to be used must not be nil, if empty, one will be generated + * @return Binary data from CRYPT-MD5 hashing. On error the funciton returns nil. + */ +- (NSData *) asCryptUsingSalt: (NSData *) theSalt +{ + char *buf; + NSString *saltString; + NSString *cryptString; + + // crypt() works with strings, so convert NSData to strings + cryptString = [[NSString alloc] initWithData: self encoding: NSUTF8StringEncoding]; + + if ([theSalt length] == 0) theSalt = [NSData generateSaltForLength: 8 withBase64: YES]; + + saltString = [[NSString alloc] initWithData: theSalt encoding: NSUTF8StringEncoding]; + + // The salt is weak here, but who cares anyway, crypt should not + // be used anymore + buf = crypt([cryptString UTF8String], [saltString UTF8String]); + [saltString release]; + [cryptString release]; + if (!buf) + return nil; + return [NSData dataWithBytes: buf length: strlen(buf)]; +} + +/** + * Get the salt from a password encrypted with a specied scheme + * + * @param theScheme Needed to get the salt correctly out of the pass + * @return The salt, if one was available in the password/scheme, else empty data + */ +- (NSData *) extractSalt: (NSString *) theScheme +{ + NSRange r; + int len; + len = [self length]; + if (len == 0) + return [NSData data]; + + // for the ssha schemes the salt is appended at the endif + // so the range with the salt are bytes after each digest length + if ([theScheme caseInsensitiveCompare: @"crypt"] == NSOrderedSame) + { + // for crypt schemes simply use the whole string + // the crypt() function is able to extract it by itself + r = NSMakeRange(0, len); + } + else if ([theScheme caseInsensitiveCompare: @"md5-crypt"] == NSOrderedSame) + { + // md5 crypt is generated the following "$1$$" + NSString *cryptString; + NSArray *cryptParts; + cryptString = [NSString stringWithUTF8String: [self bytes] ]; + cryptParts = [cryptString componentsSeparatedByString: @"$"]; + // correct number of elements (first one is an empty string) + if ([cryptParts count] != 4) + { + return [NSData data]; + } + // second is the identifier of md5-crypt + else if( [[cryptParts objectAtIndex: 1] caseInsensitiveCompare: @"1"] != NSOrderedSame ) + { + return [NSData data]; + } + // third is the salt; convert it to NSData + return [[cryptParts objectAtIndex: 2] dataUsingEncoding: NSUTF8StringEncoding]; + } + else if ([theScheme caseInsensitiveCompare: @"ssha"] == NSOrderedSame) + { + r = NSMakeRange(SHA_DIGEST_LENGTH, len - SHA_DIGEST_LENGTH); + } + else if ([theScheme caseInsensitiveCompare: @"ssha256"] == NSOrderedSame) + { + r = NSMakeRange(SHA256_DIGEST_LENGTH, len - SHA256_DIGEST_LENGTH); + } + else if ([theScheme caseInsensitiveCompare: @"ssha512"] == NSOrderedSame) + { + r = NSMakeRange(SHA512_DIGEST_LENGTH, len - SHA512_DIGEST_LENGTH); + } + else if ([theScheme caseInsensitiveCompare: @"smd5"] == NSOrderedSame) + { + r = NSMakeRange(MD5_DIGEST_LENGTH, len - MD5_DIGEST_LENGTH); + } + else + { + // return empty string on unknown scheme + return [NSData data]; + } + + return [self subdataWithRange: r]; +} + +@end + +unsigned charTo4Bits(char c) +{ + unsigned bits = 0; + if (c > '/' && c < ':') + { + bits = c - '0'; + } + else if (c > '@' && c < 'G') + { + bits = (c- 'A') + 10; + } + else if (c > '`' && c < 'g') + { + bits = (c- 'a') + 10; + } + else + { + bits = 255; + } + return bits; +} diff --git a/SoObjects/SOGo/NSString+Crypto.h b/SoObjects/SOGo/NSString+Crypto.h new file mode 100644 index 000000000..cc35a89d5 --- /dev/null +++ b/SoObjects/SOGo/NSString+Crypto.h @@ -0,0 +1,63 @@ +/* NSString+Crypto.h - this file is part of SOGo + * + * Copyright (C) 2012 Nicolas Höft + * Copyright (C) 2012 Inverse inc. + * + * Author: Nicolas Höft + * 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. + */ + +#ifndef NSSTRING_CRYPTO_H +#define NSSTRING_CRYPTO_H + +#import +#import + +typedef enum { + encDefault, //!< default encoding, let the algorithm decide + encPlain, //!< the data is plain text, simply convert to string + encHex, //!< the data is hex encoded + encBase64, //!< base64 encoding +} keyEncoding; + +@class NSObject; + +@interface NSString (SOGoCryptoExtension) + + +- (BOOL) isEqualToCrypted: (NSString *) cryptedPassword + withDefaultScheme: (NSString *) theScheme; + +- (NSString *) asCryptedPassUsingScheme: (NSString *) passwordScheme + withSalt: (NSData *) theSalt + andEncoding: (keyEncoding) encoding; + +// this method uses the default encoding (base64, plain, hex) +// and generates a salt when necessary +- (NSString *) asCryptedPassUsingScheme: (NSString *) passwordScheme; + +- (NSArray *) splitPasswordWithDefaultScheme: (NSString *) defaultScheme; + +- (NSString *) asSHA1String; +- (NSString *) asMD5String; + ++ (keyEncoding) getDefaultEncodingForScheme: (NSString *) passwordScheme; + +@end + +#endif /* NSSTRING_CRYPTO_H */ diff --git a/SoObjects/SOGo/NSString+Crypto.m b/SoObjects/SOGo/NSString+Crypto.m new file mode 100644 index 000000000..566075164 --- /dev/null +++ b/SoObjects/SOGo/NSString+Crypto.m @@ -0,0 +1,304 @@ +/* NSString+Crypto.m - this file is part of SOGo + * + * Copyright (C) 2012 Nicolas Höft + * Copyright (C) 2012 Inverse inc. + * + * Author: Nicolas Höft + * 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 "NSString+Crypto.h" +#import "NSData+Crypto.h" +#import + +@implementation NSString (SOGoCryptoExtension) + +/** + * Extracts the scheme from a string formed "{scheme}pass". + * + * @return The scheme or an empty string if the string did not contained a scheme in the format above + */ +- (NSString *) extractCryptScheme +{ + NSRange r; + int len; + + len = [self length]; + if (len == 0) + return @""; + if ([self characterAtIndex:0] != '{') + return @""; + + r = [self rangeOfString:@"}" options:(NSLiteralSearch)]; + if (r.length == 0) + return @""; + + r.length = (r.location - 1); + r.location = 1; + return [[self substringWithRange:r] lowercaseString]; +} + + +/** + * Split a password of the form {scheme}pass into an array of its components: + * {NSString *scheme, NString *pass, NSInteger encoding}, where encoding is + * the enum keyEncoding converted to an integer value. + * + * @param defaultScheme If no scheme is given in cryptedPassword, fall back to this scheme + * @see asCryptedPassUsingScheme + * @see keyEncoding + * @return NSArray with the three elements described above + */ +- (NSArray *) splitPasswordWithDefaultScheme: (NSString *) defaultScheme +{ + NSString *scheme; + NSString *pass; + NSArray *schemeComps; + keyEncoding encoding; + + NSRange range; + int selflen, len; + + selflen = [self length]; + + scheme = [self extractCryptScheme]; + len = [scheme length]; + if (len > 0) + range = NSMakeRange (len+2, selflen-len-2); + else + range = NSMakeRange (0, selflen); + if (len == 0) + scheme = defaultScheme; + + encoding = [NSString getDefaultEncodingForScheme: scheme]; + + // get the encoding which may be part of the scheme + // e.g. ssha.hex forces a hex encoded ssha scheme + // possible is "b64" or "hex" + schemeComps = [scheme componentsSeparatedByString: @"."]; + if ([schemeComps count] == 2) + { + NSString *stringEncoding; + // scheme without encoding string is the first item + scheme = [schemeComps objectAtIndex: 0]; + // encoding string is second item + stringEncoding = [schemeComps objectAtIndex: 1]; + if ([stringEncoding caseInsensitiveCompare: @"hex"] == NSOrderedSame) + { + encoding = encHex; + } + else if ([stringEncoding caseInsensitiveCompare: @"b64"] == NSOrderedSame || + [stringEncoding caseInsensitiveCompare: @"base64"] == NSOrderedSame) + { + encoding = encBase64; + } + } + + pass = [self substringWithRange: range]; + return [NSArray arrayWithObjects: scheme, pass, [NSNumber numberWithInt: encoding], nil]; +} + +/** + * Compare the hex or base64 encoded password with an encrypted password + * + * @param cryptedPassword The password to compare with, format {scheme}pass , "{scheme}" is optional + * @param theScheme If no scheme is given in cryptedPassword, fall back to this scheme + * @see asCryptedPassUsingScheme + * @return YES if the passwords are identical using this encryption scheme + */ +- (BOOL) isEqualToCrypted: (NSString *) cryptedPassword + withDefaultScheme: (NSString *) theScheme +{ + NSArray *passInfo; + NSString *selfCrypted; + NSString *pass; + NSString *scheme; + NSData *salt; + NSData *decodedData; + NSNumber *encodingNumber; + keyEncoding encoding; + + // split scheme and pass + passInfo = [cryptedPassword splitPasswordWithDefaultScheme: theScheme]; + + scheme = [passInfo objectAtIndex: 0]; + pass = [passInfo objectAtIndex: 1]; + encodingNumber = [passInfo objectAtIndex: 2]; + encoding = [encodingNumber integerValue]; + + if (encoding == encHex) + { + decodedData = [NSData decodeDataFromHexString: pass]; + + if(decodedData == nil) + { + decodedData = [NSData data]; + } + else + { + // decoding was successful, now make sure + // that the pass is in lowercase since decodeDataFromHexString uses + // lowercase charaters, too + pass = [pass lowercaseString]; + } + } + else if(encoding == encBase64) + { + decodedData = [pass dataByDecodingBase64]; + if(decodedData == nil) + { + decodedData = [NSData data]; + } + } + else + { + decodedData = [pass dataUsingEncoding: NSUTF8StringEncoding]; + } + + salt = [decodedData extractSalt: scheme]; + + // encrypt self with the salt an compare the results + selfCrypted = [self asCryptedPassUsingScheme: scheme + withSalt: salt + andEncoding: encoding]; + // return always false when there was a problem + if (selfCrypted == nil) + return NO; + + if ([selfCrypted isEqualToString: pass] == YES) + return YES; + return NO; +} + +/** + * Calls asCryptedPassUsingScheme:withSalt:andEncoding: with an empty salt and uses + * the default encoding. + * + * @param passwordScheme + * @return If successful, the encrypted and encoded NSString of the format {scheme}pass, or nil if the scheme did not exists or an error occured + */ +- (NSString *) asCryptedPassUsingScheme: (NSString *) passwordScheme +{ + return [self asCryptedPassUsingScheme: passwordScheme + withSalt: [NSData data] + andEncoding: encDefault]; +} + +/** + * Uses NSData -asCryptedPassUsingScheme to encrypt the string and converts the + * binary data back to a readable string using userEncoding + * + * @param passwordScheme The scheme to use + * @param theSalt The binary data of the salt + * @param userEncoding The encoding (plain, hex, base64) to be used + * @return If successful, the encrypted and encoded NSString of the format {scheme}pass, or nil if the scheme did not exists or an error occured + */ +- (NSString *) asCryptedPassUsingScheme: (NSString *) passwordScheme + withSalt: (NSData *) theSalt + andEncoding: (keyEncoding) userEncoding +{ + keyEncoding dataEncoding; + NSData* cryptedData; + // convert NSString to NSData and apply encryption scheme + cryptedData = [self dataUsingEncoding: NSUTF8StringEncoding]; + cryptedData = [cryptedData asCryptedPassUsingScheme: passwordScheme withSalt: theSalt]; + // abort on unsupported scheme or error + if (cryptedData == nil) + return nil; + + // use default encoding scheme, when set to default + if (userEncoding == encDefault) + dataEncoding = [NSString getDefaultEncodingForScheme: passwordScheme]; + else + dataEncoding = userEncoding; + + if (dataEncoding == encHex) + { + // hex encoding + return [NSData encodeDataAsHexString: cryptedData]; + } + else if(dataEncoding == encBase64) + { + // base64 encoding + NSString *s = [[NSString alloc] initWithData: [cryptedData dataByEncodingBase64WithLineLength: 1024] + encoding: NSASCIIStringEncoding]; + return [s autorelease]; + } + + // plain string + return [[[NSString alloc] initWithData: cryptedData encoding: NSUTF8StringEncoding] autorelease]; +} + +/** + * Returns the encoding for a specified scheme + * + * @param passwordScheme The scheme for which to get the encoding. + * @see keyEncoding + * @return returns the encoding, if unknown returns encPlain + */ ++ (keyEncoding) getDefaultEncodingForScheme: (NSString *) passwordScheme +{ + // in order to keep backwards-compatibility, hex encoding is used for sha1 here + if ([passwordScheme caseInsensitiveCompare: @"md5"] == NSOrderedSame || + [passwordScheme caseInsensitiveCompare: @"plain-md5"] == NSOrderedSame || + [passwordScheme caseInsensitiveCompare: @"sha"] == NSOrderedSame || + [passwordScheme caseInsensitiveCompare: @"cram-md5"] == NSOrderedSame) + { + return encHex; + } + else if ([passwordScheme caseInsensitiveCompare: @"smd5"] == NSOrderedSame || + [passwordScheme caseInsensitiveCompare: @"ldap-md5"] == NSOrderedSame || + [passwordScheme caseInsensitiveCompare: @"ssha"] == NSOrderedSame || + [passwordScheme caseInsensitiveCompare: @"sha256"] == NSOrderedSame || + [passwordScheme caseInsensitiveCompare: @"ssha256"] == NSOrderedSame || + [passwordScheme caseInsensitiveCompare: @"sha512"] == NSOrderedSame || + [passwordScheme caseInsensitiveCompare: @"ssha512"] == NSOrderedSame) + { + return encBase64; + } + return encPlain; +} + +/** + * Encrypts the data with SHA1 scheme and returns the hex-encoded data + * + * @return If successful, sha1 encrypted and with hex encoded string + */ +- (NSString *) asSHA1String; +{ + NSData *cryptData; + cryptData = [self dataUsingEncoding: NSUTF8StringEncoding]; + return [NSData encodeDataAsHexString: [cryptData asSHA1] ]; +} + +/** + * Encrypts the data with Plain MD5 scheme and returns the hex-encoded data + * + * @return If successful, MD5 encrypted and with hex encoded string + */ +- (NSString *) asMD5String; +{ + NSData *cryptData; + cryptData = [self dataUsingEncoding: NSUTF8StringEncoding]; + return [NSData encodeDataAsHexString: [cryptData asMD5] ]; +} + +@end diff --git a/SoObjects/SOGo/NSString+Utilities.h b/SoObjects/SOGo/NSString+Utilities.h index a37f6e627..a94e48ce6 100644 --- a/SoObjects/SOGo/NSString+Utilities.h +++ b/SoObjects/SOGo/NSString+Utilities.h @@ -66,10 +66,6 @@ - (id) objectFromJSONString; -- (NSString *) asCryptStringUsingSalt: (NSString *) theSalt; -- (NSString *) asMD5String; -- (NSString *) asSHA1String; - - (NSString *) asSafeSQLString; - (NSUInteger) countOccurrencesOfString: (NSString *) substring; diff --git a/SoObjects/SOGo/NSString+Utilities.m b/SoObjects/SOGo/NSString+Utilities.m index 64f87f010..1feb8537d 100644 --- a/SoObjects/SOGo/NSString+Utilities.m +++ b/SoObjects/SOGo/NSString+Utilities.m @@ -21,10 +21,6 @@ * Boston, MA 02111-1307, USA. */ -#ifndef __OpenBSD__ -#include -#endif - #import #import #import @@ -45,12 +41,6 @@ #import "NSString+Utilities.h" -#define _XOPEN_SOURCE 1 -#include -#include -#include -#include - static NSMutableCharacterSet *urlNonEndingChars = nil; static NSMutableCharacterSet *urlAfterEndingChars = nil; static NSMutableCharacterSet *urlStartChars = nil; @@ -535,48 +525,6 @@ static int cssEscapingCount; return object; } -- (NSString *) asCryptStringUsingSalt: (NSString *) theSalt -{ - char *buf; - - // The salt is weak here, but who cares anyway, crypt should not - // be used anymore - buf = crypt([self UTF8String], [theSalt UTF8String]); - return [NSString stringWithUTF8String: buf]; -} - -- (NSString *) asMD5String -{ - unsigned char md[MD5_DIGEST_LENGTH]; - char buf[80]; - int i; - - memset(md, 0, MD5_DIGEST_LENGTH); - memset(buf, 0, 80); - - EVP_Digest((const void *) [self UTF8String], strlen([self UTF8String]), md, NULL, EVP_md5(), NULL); - for (i = 0; i < MD5_DIGEST_LENGTH; i++) - sprintf(&(buf[i*2]), "%02x", md[i]); - - return [NSString stringWithUTF8String: buf]; -} - -- (NSString *) asSHA1String -{ - unsigned char sha[SHA_DIGEST_LENGTH]; - char buf[80]; - int i; - - memset(sha, 0, SHA_DIGEST_LENGTH); - memset(buf, 0, 80); - - SHA1((const void *)[self UTF8String], strlen([self UTF8String]), sha); - for (i = 0; i < SHA_DIGEST_LENGTH; i++) - sprintf(&(buf[i*2]), "%02x", sha[i]); - - return [NSString stringWithUTF8String: buf]; -} - - (NSString *) asSafeSQLString { return [[self stringByReplacingString: @"\\" withString: @"\\\\"] diff --git a/SoObjects/SOGo/SOGoUserManager.m b/SoObjects/SOGo/SOGoUserManager.m index 1f41db01b..e62c21304 100644 --- a/SoObjects/SOGo/SOGoUserManager.m +++ b/SoObjects/SOGo/SOGoUserManager.m @@ -33,6 +33,7 @@ #import "NSArray+Utilities.h" #import "NSString+Utilities.h" +#import "NSString+Crypto.h" #import "NSObject+Utilities.h" #import "SOGoDomainDefaults.h" #import "SOGoSource.h" diff --git a/SoObjects/SOGo/SQLSource.h b/SoObjects/SOGo/SQLSource.h index 8e254c8f1..eeac526cf 100644 --- a/SoObjects/SOGo/SQLSource.h +++ b/SoObjects/SOGo/SQLSource.h @@ -45,6 +45,7 @@ NSString *_imapHostField; NSString *_userPasswordAlgorithm; NSURL *_viewURL; + BOOL _prependPasswordScheme; /* resources handling */ NSString *_kindField; diff --git a/SoObjects/SOGo/SQLSource.m b/SoObjects/SOGo/SQLSource.m index 5ccc833ac..2c5091306 100644 --- a/SoObjects/SOGo/SQLSource.m +++ b/SoObjects/SOGo/SQLSource.m @@ -39,6 +39,7 @@ #import "SOGoConstants.h" #import "NSString+Utilities.h" +#import "NSString+Crypto.h" #import "SQLSource.h" @@ -47,7 +48,10 @@ * * c_uid - will be used for authentication - it's a username or username@domain.tld) * c_name - which can be identical to c_uid - will be used to uniquely identify entries) - * c_password - password of the user, plain-text, md5 or sha encoded for now + * c_password - password of the user, can be encoded in {scheme}pass format, or when stored without + * scheme it uses the scheme set in userPasswordAlgorithm. + * Possible algorithms are: plain, md5, crypt-md5, sha, ssha (including 256/512 variants), + * cram-md5, smd5, crypt, crypt-md5 * c_cn - the user's common name * mail - the user's mail address * @@ -63,8 +67,12 @@ * canAuthenticate = YES; * isAddressBook = YES; * userPasswordAlgorithm = md5; + * prependPasswordScheme = YES; * } * + * If prependPasswordScheme is set to YES, the generated passwords will have the format {scheme}password. + * If it is NO (the default), the password will be written to database without encryption scheme. + * */ @implementation SQLSource @@ -126,6 +134,10 @@ ASSIGN(_kindField, [udSource objectForKey: @"KindFieldName"]); ASSIGN(_multipleBookingsField, [udSource objectForKey: @"MultipleBookingsFieldName"]); ASSIGN(_domainField, [udSource objectForKey: @"DomainFieldName"]); + if ([udSource objectForKey: @"prependPasswordScheme"]) + _prependPasswordScheme = [[udSource objectForKey: @"prependPasswordScheme"] boolValue]; + else + _prependPasswordScheme = NO; if (!_userPasswordAlgorithm) _userPasswordAlgorithm = @"none"; @@ -157,28 +169,8 @@ if (!plainPassword || !encryptedPassword) return NO; - if ([_userPasswordAlgorithm caseInsensitiveCompare: @"none"] == NSOrderedSame) - { - return [plainPassword isEqualToString: encryptedPassword]; - } - else if ([_userPasswordAlgorithm caseInsensitiveCompare: @"crypt"] == NSOrderedSame) - { - return [[plainPassword asCryptStringUsingSalt: encryptedPassword] isEqualToString: encryptedPassword]; - } - else if ([_userPasswordAlgorithm caseInsensitiveCompare: @"md5"] == NSOrderedSame) - { - return [[plainPassword asMD5String] isEqualToString: encryptedPassword]; - } - else if ([_userPasswordAlgorithm caseInsensitiveCompare: @"sha"] == NSOrderedSame) - { - - return [[plainPassword asSHA1String] isEqualToString: encryptedPassword]; - } - - - [self errorWithFormat: @"Unsupported user-password algorithm: %@", _userPasswordAlgorithm]; - - return NO; + return [plainPassword isEqualToCrypted: encryptedPassword + withDefaultScheme: _userPasswordAlgorithm]; } /** @@ -189,26 +181,20 @@ */ - (NSString *) _encryptPassword: (NSString *) plainPassword { - if ([_userPasswordAlgorithm caseInsensitiveCompare: @"none"] == NSOrderedSame) - { - return plainPassword; - } - else if ([_userPasswordAlgorithm caseInsensitiveCompare: @"crypt"] == NSOrderedSame) - { - return [plainPassword asCryptStringUsingSalt: [plainPassword asMD5String]]; - } - else if ([_userPasswordAlgorithm caseInsensitiveCompare: @"md5"] == NSOrderedSame) - { - return [plainPassword asMD5String]; - } - else if ([_userPasswordAlgorithm caseInsensitiveCompare: @"sha"] == NSOrderedSame) - { - return [plainPassword asSHA1String]; - } + NSString *pass; + NSString* result; - [self errorWithFormat: @"Unsupported user-password algorithm: %@", _userPasswordAlgorithm]; - - return plainPassword; + pass = [plainPassword asCryptedPassUsingScheme: _userPasswordAlgorithm]; + + if (pass == nil) + [self errorWithFormat: @"Unsupported user-password algorithm: %@", _userPasswordAlgorithm]; + + if (_prependPasswordScheme) + result = [NSString stringWithFormat: @"{%@}%@", _userPasswordAlgorithm, pass]; + else + result = pass; + + return result; } //