/* NSString+Crypto.m - this file is part of SOGo * * Copyright (C) 2012 Nicolas Höft * Copyright (C) 2012-2019 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 #ifdef HAVE_OPENSSL #import #import #endif #import "aes.h" #define AES_128_KEY_SIZE 16 #define AES_128_BLOCK_SIZE 16 #define AES_256_KEY_SIZE 32 #define AES_256_BLOCK_SIZE 16 #define GMC_IV_LEN 12 #define GMC_TAG_LEN 16 static const NSString *kAES128ECError = @"kAES128ECError"; static const NSString *kAES256GCMError = @"kAES256GCMError"; @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 *encodingAndScheme; 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; encodingAndScheme = [NSString getDefaultEncodingForScheme: scheme]; pass = [self substringWithRange: range]; // Returns an array with [scheme, password, encoding] return [NSArray arrayWithObjects: [encodingAndScheme objectAtIndex: 1], pass, [encodingAndScheme objectAtIndex: 0], 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 keyPath: (NSString *) theKeyPath { NSArray *passInfo; NSString *pass; NSString *scheme; NSData *decodedData; NSData *passwordData; keyEncoding encoding; // split scheme and pass passInfo = [cryptedPassword splitPasswordWithDefaultScheme: theScheme]; scheme = [passInfo objectAtIndex: 0]; pass = [passInfo objectAtIndex: 1]; encoding = [[passInfo objectAtIndex: 2] intValue]; 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]; } passwordData = [self dataUsingEncoding: NSUTF8StringEncoding]; return [decodedData verifyUsingScheme: scheme withPassword: passwordData keyPath: theKeyPath]; } /** * Calls asCryptedPassUsingScheme:withSalt:andEncoding: with an empty salt and uses * the default encoding. * * @param passwordScheme: The password scheme to hash the cleartext password. * @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 keyPath: (NSString *) theKeyPath { return [self asCryptedPassUsingScheme: passwordScheme withSalt: [NSData data] andEncoding: encDefault keyPath: theKeyPath]; } /** * 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. If set to * encDefault, the encoding will be detected from scheme name. * @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 keyPath: (NSString *) theKeyPath { keyEncoding dataEncoding; NSData* cryptedData; // use default encoding scheme, when set to default if (userEncoding == encDefault) { // the encoding needs to be detected before crypting, // to get the plain scheme (without encoding identifier) NSArray* encodingAndScheme; encodingAndScheme = [NSString getDefaultEncodingForScheme: passwordScheme]; dataEncoding = [[encodingAndScheme objectAtIndex: 0] intValue]; passwordScheme = [encodingAndScheme objectAtIndex: 1]; } else { dataEncoding = userEncoding; } // convert NSString to NSData and apply encryption scheme cryptedData = [self dataUsingEncoding: NSUTF8StringEncoding]; cryptedData = [cryptedData asCryptedPassUsingScheme: passwordScheme withSalt: theSalt keyPath: theKeyPath]; // abort on unsupported scheme or error if (cryptedData == nil) return nil; 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. Can be "scheme.encoding" in which case the encoding is returned * @see keyEncoding * @return returns NSArray with elements {NSNumber encoding, NSString* scheme} where scheme is the 'real' scheme without the ".encoding" part. * 'encoding' is stored as NSNumber in the array. If the encoding was not detected, encPlain is used for encoding. */ + (NSArray *) getDefaultEncodingForScheme: (NSString *) passwordScheme { NSArray *schemeComps; NSString *trueScheme; keyEncoding encoding = encPlain; // 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 = [passwordScheme componentsSeparatedByString: @"."]; if ([schemeComps count] == 2) { trueScheme = [schemeComps objectAtIndex: 0]; NSString *stringEncoding; // 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; } } else { trueScheme = passwordScheme; } // in order to keep backwards-compatibility, hex encoding is used for sha1 here if ([passwordScheme caseInsensitiveCompare: @"md4"] == NSOrderedSame || [passwordScheme caseInsensitiveCompare: @"md5"] == NSOrderedSame || [passwordScheme caseInsensitiveCompare: @"plain-md5"] == NSOrderedSame || [passwordScheme caseInsensitiveCompare: @"sha"] == NSOrderedSame || [passwordScheme caseInsensitiveCompare: @"cram-md5"] == NSOrderedSame) { encoding = 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) { encoding = encBase64; } return [NSArray arrayWithObjects: [NSNumber numberWithInt: encoding], trueScheme, nil]; } /** * 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]]; } /** * Encrypts the data using the NT-hash password scheme. * * @return If successful, NT-hash encrypted and with hex encoded string */ - (NSString *) asNTHash { NSData *d; d = [self dataUsingEncoding: NSUTF16LittleEndianStringEncoding]; return [[NSData encodeDataAsHexString: [d asMD4]] uppercaseString]; } /** * Encrypts the data using the LM-hash password scheme. * * @return If successful, LM-hash encrypted and with hex encoded string */ - (NSString *) asLMHash { NSData *d; // See http://en.wikipedia.org/wiki/LM_hash#Algorithm d = [[self uppercaseString] dataUsingEncoding: NSWindowsCP1252StringEncoding]; 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 *data, *keyData, *outputData; NSString *value; int c_len, f_len; unsigned char *ciphertext; #ifdef HAVE_OPENSSL EVP_CIPHER_CTX *ctx; #endif value = nil; if (AES_128_KEY_SIZE != [passwordScheme length]) { *ex = [NSException exceptionWithName: kAES128ECError reason: [NSString stringWithFormat:@"Key must be %d bits, but key has %d bits", (AES_128_KEY_SIZE * 8), ([passwordScheme length] * 8)] userInfo: nil]; return nil; } #ifdef HAVE_OPENSSL data = [self dataUsingEncoding: NSUTF8StringEncoding]; keyData = [passwordScheme dataUsingEncoding: NSUTF8StringEncoding]; // Initialize OpenSSL ctx = EVP_CIPHER_CTX_new(); // Set up cipher parameters EVP_CIPHER_CTX_init(ctx); EVP_EncryptInit_ex(ctx, EVP_aes_128_ecb(), NULL, [keyData bytes], NULL); EVP_CIPHER_CTX_set_padding(ctx, 1); // Perform encryption c_len = [data length] + AES_128_BLOCK_SIZE; ciphertext = malloc(c_len); f_len = 0; EVP_EncryptInit_ex(ctx, NULL, NULL, NULL, NULL); EVP_EncryptUpdate(ctx, ciphertext, &c_len, [data bytes], [data length]); EVP_EncryptFinal_ex(ctx, ciphertext + c_len, &f_len); c_len += f_len; EVP_CIPHER_CTX_free(ctx); outputData = [NSData dataWithBytes: (char *)ciphertext length: c_len]; free(ciphertext); if (outputData) { value = [outputData stringByEncodingBase64]; if (encodedURL) { value = [value stringByReplacingOccurrencesOfString: @"+" withString: @"."]; value = [value stringByReplacingOccurrencesOfString: @"/" withString: @"_"]; value = [value stringByReplacingOccurrencesOfString: @"=" withString: @"-"]; } } else { *ex = [NSException exceptionWithName: kAES128ECError reason:@"Empty data" userInfo: nil]; } return value; #else *ex = [NSException exceptionWithName: kAES128ECError reason:@"Missing OpenSSL framework" userInfo: nil]; return self; #endif } /** * 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 *keyData, *data, *outputData; NSString *inputString, *value; int p_len, f_len; unsigned char *plaintext; value = nil; #ifdef HAVE_OPENSSL if (AES_128_KEY_SIZE != [passwordScheme length]) { *ex = [NSException exceptionWithName: kAES128ECError reason: [NSString stringWithFormat:@"Key must be %d bits, but key has %d bits", (AES_128_KEY_SIZE * 8), ([passwordScheme length] * 8)] userInfo: nil]; return nil; } keyData = [passwordScheme dataUsingEncoding: NSUTF8StringEncoding]; inputString = [NSString stringWithString: self]; if (encodedURL) { inputString = [inputString stringByReplacingOccurrencesOfString: @"." withString: @"+"]; inputString = [inputString stringByReplacingOccurrencesOfString: @"_" withString: @"/"]; inputString = [inputString stringByReplacingOccurrencesOfString: @"-" withString: @"="]; } data = [inputString dataByDecodingBase64]; // Initialize OpenSSL EVP_CIPHER_CTX *ctx; ctx = EVP_CIPHER_CTX_new(); // Set up cipher parameters EVP_CIPHER_CTX_init(ctx); EVP_DecryptInit_ex(ctx, EVP_aes_128_ecb(), NULL, [keyData bytes], NULL); EVP_CIPHER_CTX_set_padding(ctx, 1); // Perform decryption p_len = [data length]; plaintext = malloc(p_len); f_len = 0; EVP_DecryptInit_ex(ctx, NULL, NULL, NULL, NULL); EVP_DecryptUpdate(ctx, plaintext, &p_len, [data bytes], [data length]); EVP_DecryptFinal_ex(ctx, plaintext + p_len, &f_len); p_len += f_len; EVP_CIPHER_CTX_free(ctx); // Trim padding while (plaintext[p_len - 1] == '\0') { p_len--; } if (p_len > 0) { // Convert to NSString outputData = [NSData dataWithBytes: plaintext length: p_len]; if (outputData && [outputData length] > 0) { char lastByte; [outputData getBytes:&lastByte range:NSMakeRange([outputData length]-1, 1)]; if (lastByte == 0x0) { // string is null terminated value = [NSString stringWithUTF8String: [outputData bytes]]; } else { // string is not null terminated value = [[NSString alloc] initWithData:outputData encoding:NSUTF8StringEncoding]; [value autorelease]; } } else { *ex = [NSException exceptionWithName: kAES128ECError reason:@"Empty data" userInfo: nil]; } } else { *ex = [NSException exceptionWithName: kAES128ECError reason:@"Could not decrypt" userInfo: nil]; } // Clean up free(plaintext); return value; #else *ex = [NSException exceptionWithName:kAES128ECError reason:@"Missing OpenSSL framework" userInfo: nil]; return self; #endif } - (NSDictionary *)encryptAES256GCM:(NSString *)passwordScheme exception:(NSException **)ex { NSData *data, *keyData, *ivData, *tagData, *outputData; NSString *value; NSError *error; NSMutableDictionary* gcmDisctionary; int c_len, f_len; unsigned char *ciphertext; unsigned char tag[16]; #ifdef HAVE_OPENSSL EVP_CIPHER_CTX *ctx; #endif value = nil; gcmDisctionary = [NSMutableDictionary dictionaryWithObject: @"" forKey: @"cypher"]; if (AES_256_KEY_SIZE != [passwordScheme length]) { *ex = [NSException exceptionWithName: kAES256GCMError reason: [NSString stringWithFormat:@"Key must be %d bits", (AES_256_KEY_SIZE * 8)] userInfo: nil]; return nil; } #ifdef HAVE_OPENSSL //Generate random IV ivData = [[NSFileHandle fileHandleForReadingAtPath:@"/dev/random"] readDataOfLength:GMC_IV_LEN]; if (GMC_IV_LEN != [ivData length]) { *ex = [NSException exceptionWithName: kAES256GCMError reason: [NSString stringWithFormat:@"IV must be %d bits", (GMC_IV_LEN * 8)] userInfo: nil]; return nil; } data = [self dataUsingEncoding: NSUTF8StringEncoding]; keyData = [passwordScheme dataUsingEncoding: NSUTF8StringEncoding]; //Set cipher encryption ctx = EVP_CIPHER_CTX_new(); EVP_EncryptInit_ex(ctx, EVP_aes_256_gcm(), NULL, NULL, NULL); EVP_EncryptInit_ex(ctx, EVP_aes_256_gcm(), NULL, NULL, NULL); EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, GMC_IV_LEN, NULL); EVP_EncryptInit_ex(ctx, NULL, NULL, [keyData bytes], [ivData bytes]); //Start Encryption c_len = [data length]; ciphertext = malloc(c_len); int status = 0; EVP_EncryptUpdate(ctx, ciphertext, &c_len, [data bytes], (int)[data length]); status = EVP_EncryptFinal_ex(ctx, ciphertext + c_len, &f_len); c_len += f_len; outputData = nil; tagData = nil; if(status) { outputData = [NSData dataWithBytes: (char *)ciphertext length: c_len]; EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, GMC_TAG_LEN, tag); tagData = [NSData dataWithBytes: (char *)tag length: GMC_TAG_LEN]; } else { *ex = [NSException exceptionWithName: kAES256GCMError reason:@"Encryption not successful" userInfo: nil]; } EVP_CIPHER_CTX_free(ctx); free(ciphertext); if(outputData && tagData) { [gcmDisctionary setObject: [outputData stringByEncodingBase64] forKey: @"cypher"]; [gcmDisctionary setObject: [ivData stringByEncodingBase64] forKey: @"iv"]; [gcmDisctionary setObject: [tagData stringByEncodingBase64] forKey: @"tag"]; } else { *ex = [NSException exceptionWithName: kAES256GCMError reason:@"Empty data" userInfo: nil]; } return gcmDisctionary; #else *ex = [NSException exceptionWithName:kAES256GCMError reason:@"Missing OpenSSL framework" userInfo: nil]; return nil; #endif } - (NSString *)decryptAES256GCM:(NSString *)passwordScheme iv:(NSString *)ivString tag:(NSString *)tagString exception:(NSException **)ex { NSData *keyData, *ivData, *tagData, *data, *outputData; NSString *inputString, *value; int p_len, f_len, rv; unsigned char *plaintext; value = nil; #ifdef HAVE_OPENSSL keyData = [passwordScheme dataUsingEncoding: NSUTF8StringEncoding]; ivData = [ivString dataByDecodingBase64]; tagData = [tagString dataByDecodingBase64]; if (AES_256_KEY_SIZE != [keyData length]) { *ex = [NSException exceptionWithName: kAES256GCMError reason: [NSString stringWithFormat:@"Key must be %d bits", (AES_256_KEY_SIZE * 8)] userInfo: nil]; return nil; } if (GMC_IV_LEN!= [ivData length]) { *ex = [NSException exceptionWithName: kAES256GCMError reason: [NSString stringWithFormat:@"Key must be %d bits", (GMC_IV_LEN * 8)] userInfo: nil]; return nil; } if (GMC_TAG_LEN != [tagData length]) { *ex = [NSException exceptionWithName: kAES256GCMError reason: [NSString stringWithFormat:@"Tag must be %d bits", (GMC_TAG_LEN * 8)] userInfo: nil]; return nil; } inputString = [NSString stringWithString: self]; data = [inputString dataByDecodingBase64]; // Initialize OpenSSL EVP_CIPHER_CTX *ctx; ctx = EVP_CIPHER_CTX_new(); // Set up cipher parameters EVP_CIPHER_CTX_init(ctx); EVP_DecryptInit_ex(ctx, EVP_aes_256_gcm(), NULL, NULL, NULL); EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, GMC_IV_LEN, NULL); EVP_DecryptInit_ex(ctx, NULL, NULL, [keyData bytes], [ivData bytes]); // Perform decryption p_len = [data length]; plaintext = malloc(p_len); f_len = 0; int status = 0; EVP_DecryptUpdate(ctx, plaintext, &p_len, [data bytes], [data length]); outputData = [NSData dataWithBytes: (char *)plaintext length: p_len]; EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_TAG, GMC_TAG_LEN, (void *)[tagData bytes]); rv = EVP_DecryptFinal_ex(ctx, plaintext + p_len, &f_len); p_len += f_len; EVP_CIPHER_CTX_free(ctx); if (rv > 0) { if (outputData && [outputData length] > 0) { char lastByte; [outputData getBytes:&lastByte range:NSMakeRange([outputData length]-1, 1)]; if (lastByte == 0x0) { // string is null terminated value = [NSString stringWithUTF8String: [outputData bytes]]; } else { // string is not null terminated value = [[NSString alloc] initWithData:outputData encoding:NSUTF8StringEncoding]; [value autorelease]; } } else { *ex = [NSException exceptionWithName: kAES256GCMError reason:@"Decryption ok but output empty" userInfo: nil]; } } else { *ex = [NSException exceptionWithName: kAES256GCMError reason:@"Decryption not ok" userInfo: nil]; } // Clean up free(plaintext); if(value) return value; else { *ex = [NSException exceptionWithName: kAES128ECError reason:@"Could decrypt but value is null" userInfo: nil]; return nil; } #else *ex = [NSException exceptionWithName:kAES256GCMError reason:@"Missing OpenSSL framework" userInfo: nil]; return self; #endif } @end