From fa2cd0b4841324776b10d968e1250676b0c65e32 Mon Sep 17 00:00:00 2001 From: Max Date: Sun, 13 Sep 2015 05:09:25 +0200 Subject: Replace OpenSSL for SHA1 calculation with Apple's CommonCrypto (The easier half of #2223) Also added some unit tests. --- Source/SPDataAdditions.h | 2 + Source/SPDataAdditions.m | 52 +++++++++++++--- UnitTests/SPDataAdditionsTests.m | 112 +++++++++++++++++++++++++++++++++++ sequel-pro.xcodeproj/project.pbxproj | 10 ++++ 4 files changed, 169 insertions(+), 7 deletions(-) create mode 100644 UnitTests/SPDataAdditionsTests.m diff --git a/Source/SPDataAdditions.h b/Source/SPDataAdditions.h index 0ceca0d3..118f47e2 100644 --- a/Source/SPDataAdditions.h +++ b/Source/SPDataAdditions.h @@ -30,6 +30,8 @@ @interface NSData (SPDataAdditions) +- (NSData *)sha1Hash; + - (NSData *)dataEncryptedWithPassword:(NSString *)password; - (NSData *)dataDecryptedWithPassword:(NSString *)password; - (NSData *)compress; diff --git a/Source/SPDataAdditions.m b/Source/SPDataAdditions.m index e9eaa927..23280c8b 100644 --- a/Source/SPDataAdditions.m +++ b/Source/SPDataAdditions.m @@ -36,11 +36,41 @@ #include #include -#include +#include #include +uint32_t LimitUInt32(NSUInteger i); + +#pragma mark - + @implementation NSData (SPDataAdditions) +- (NSData *)sha1Hash +{ + unsigned char digest[CC_SHA1_DIGEST_LENGTH]; + + //let's do it as a one step operation, if it fits + if([self length] <= UINT32_MAX) { + CC_SHA1([self bytes], (uint32_t)[self length], digest); + } + // or multi-step if length > 32 bit + else { + CC_SHA1_CTX ctx; + CC_SHA1_Init(&ctx); + + NSUInteger offset = 0; + uint32_t len; + while((len = LimitUInt32([self length]-offset)) > 0) { + CC_SHA1_Update(&ctx, ([self bytes]+offset), len); + offset += len; + } + + CC_SHA1_Final(digest, &ctx); + } + + return [NSData dataWithBytes:digest length:CC_SHA1_DIGEST_LENGTH]; +} + - (NSData *)dataEncryptedWithPassword:(NSString *)password { // Create a random 128-bit initialization vector @@ -68,10 +98,9 @@ memcpy(paddedBytes + (paddedLength - 4), &bigIntDataLength, 4); // Create the key from first 128-bits of the 160-bit password hash - unsigned char passwordDigest[20]; - SHA1((const unsigned char *)[password UTF8String], strlen([password UTF8String]), passwordDigest); + NSData *passwordDigest = [[password dataUsingEncoding:NSUTF8StringEncoding] sha1Hash]; AES_KEY aesKey; - AES_set_encrypt_key(passwordDigest, 128, &aesKey); + AES_set_encrypt_key([passwordDigest bytes], 128, &aesKey); // AES-128-cbc encrypt the data, filling in the buffer after the IV AES_cbc_encrypt(paddedBytes, encryptedBytes + 16, paddedLength, &aesKey, iv, AES_ENCRYPT); @@ -83,12 +112,11 @@ - (NSData *)dataDecryptedWithPassword:(NSString *)password { // Create the key from the password hash - unsigned char passwordDigest[20]; - SHA1((const unsigned char *)[password UTF8String], strlen([password UTF8String]), passwordDigest); + NSData *passwordDigest = [[password dataUsingEncoding:NSUTF8StringEncoding] sha1Hash]; // AES-128-cbc decrypt the data AES_KEY aesKey; - AES_set_decrypt_key(passwordDigest, 128, &aesKey); + AES_set_decrypt_key([passwordDigest bytes], 128, &aesKey); // Total length = encrypted length + IV NSInteger totalLength = [self length]; @@ -312,3 +340,13 @@ } @end + +#pragma mark - + +uint32_t LimitUInt32(NSUInteger i) { +#if NSUIntegerMax > UINT32_MAX + return (i > UINT32_MAX)? UINT32_MAX : (uint32_t)i; +#else + return i; +#endif +} diff --git a/UnitTests/SPDataAdditionsTests.m b/UnitTests/SPDataAdditionsTests.m new file mode 100644 index 00000000..dc0df069 --- /dev/null +++ b/UnitTests/SPDataAdditionsTests.m @@ -0,0 +1,112 @@ +// +// SPDataAdditionsTests.m +// sequel-pro +// +// Created by Max Lohrmann on 13.09.15. +// Copyright (c) 2015 Max Lohrmann. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// +// More info at + +#import +#import +#import "SPDataAdditions.h" +#import + +@interface SPDataAdditionsTests : SenTestCase + +- (void)testSha1Hash; + +@end + +@implementation SPDataAdditionsTests + +- (void)testSha1Hash +{ + //simple straight forward case + { + NSString *input = @"Hello World!"; + unsigned char bytes[] = {0x2e,0xf7,0xbd,0xe6,0x08,0xce,0x54,0x04,0xe9,0x7d,0x5f,0x04,0x2f,0x95,0xf8,0x9f,0x1c,0x23,0x28,0x71}; + + STAssertTrue(memcmp([[[input dataUsingEncoding:NSUTF8StringEncoding] sha1Hash] bytes], bytes, 20) == 0, @"SHA1 simple hash from ASCII text"); + } + // 16MB of all 8bit values + { + int bufSz = 16*1024*1024; + unsigned char *buf = malloc(bufSz); + for (int i = 0; i < bufSz; i++) { + buf[i] = (i % 0xff); + } + NSData *input = [NSData dataWithBytesNoCopy:buf length:bufSz]; + NSString *result = @"25E05EB8E9E2B06036DF4026630FE01A19BF0F16"; + + STAssertEqualObjects([[input sha1Hash] dataToHexString], result, @"SHA1 hash from full ASCII range"); + } + // empty hash + { + NSData *input = [NSData data]; + NSString *result = @"DA39A3EE5E6B4B0D3255BFEF95601890AFD80709"; + + STAssertEqualObjects([[input sha1Hash] dataToHexString], result, @"SHA1 hash from empty data"); + } + // test with > 4GB data (other code path) + // HFS+ does not support sparse files, so enable this one only if you have enough disk space. + {/* + // not everyone has 4GB RAM to spare and even then we probably won't be able to get + // them en-block, so we'll just use a file and mmap() to simulate that. + NSString *fileNameTpl = [NSTemporaryDirectory() stringByAppendingPathComponent:@"sha1test.XXXXXX"]; + STAssertNotNil(fileNameTpl, @"No temporary directory available!?"); + const char *cFileNameTpl = [fileNameTpl fileSystemRepresentation]; + char *cFileName = malloc(strlen(cFileNameTpl)+1); + strcpy(cFileName, cFileNameTpl); + if(mkstemp(cFileName) == -1) + STFail(@"could not create temporary filename. errno=%d",errno); + + FILE *fp = fopen(cFileName, "w+"); + fputc(1, fp); + fseek(fp, UINT32_MAX, SEEK_CUR); + fputc(2, fp); + fflush(fp); + fclose(fp); + + NSString *fileName = [[NSFileManager defaultManager] stringWithFileSystemRepresentation:cFileName length:strlen(cFileName)]; + + NSData *input = [NSData dataWithContentsOfFile:fileName]; + NSString *result = @"A31A151AFC12B0D66A4DBE917CB55CEAA0AD639E"; + + STAssertEqualObjects([[input sha1Hash] dataToHexString], result, @"SHA1 hash > 4gb data"); + + unlink(cFileName); + free(cFileName); + */} + //utf8 string input + { + NSData *input = [@"føöbärbãz" dataUsingEncoding:NSUTF8StringEncoding]; + NSString *result = @"8A8B6142281950CBB9B01C9DF0DADB0BDAE2D0E1"; + + STAssertEqualObjects([[input sha1Hash] dataToHexString], result, @"SHA1 hash of UTF-8 string"); + } + +} + +@end diff --git a/sequel-pro.xcodeproj/project.pbxproj b/sequel-pro.xcodeproj/project.pbxproj index fd4f3207..c2c7e035 100644 --- a/sequel-pro.xcodeproj/project.pbxproj +++ b/sequel-pro.xcodeproj/project.pbxproj @@ -179,6 +179,9 @@ 4DECC48F0EC2B436008D359E /* Sparkle.framework in Copy Frameworks */ = {isa = PBXBuildFile; fileRef = 4DECC3320EC2A170008D359E /* Sparkle.framework */; }; 4DECC4910EC2B436008D359E /* Growl.framework in Copy Frameworks */ = {isa = PBXBuildFile; fileRef = 4DECC3340EC2A170008D359E /* Growl.framework */; }; 501B1D181728A3DA0017C92E /* SPCharsetCollationHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 501B1D171728A3DA0017C92E /* SPCharsetCollationHelper.m */; }; + 502D21F61BA50710000D4CE7 /* SPDataAdditionsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 502D21F51BA50710000D4CE7 /* SPDataAdditionsTests.m */; }; + 502D21F81BA50966000D4CE7 /* SPDataAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = BC2C16D30FEBEDF10003993B /* SPDataAdditions.m */; }; + 502D21FA1BA509AF000D4CE7 /* libcrypto.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 502D21F91BA509AF000D4CE7 /* libcrypto.dylib */; }; 503B02CA1AE82C5E0060CAB1 /* SPTableFilterParser.m in Sources */ = {isa = PBXBuildFile; fileRef = 503B02C91AE82C5E0060CAB1 /* SPTableFilterParser.m */; }; 503B02CF1AE95C2C0060CAB1 /* SPTableFilterParserTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 503B02CE1AE95C2C0060CAB1 /* SPTableFilterParserTest.m */; }; 503B02D11AE95DD40060CAB1 /* SPTableFilterParser.m in Sources */ = {isa = PBXBuildFile; fileRef = 503B02C91AE82C5E0060CAB1 /* SPTableFilterParser.m */; }; @@ -891,6 +894,8 @@ 4DECC3340EC2A170008D359E /* Growl.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Growl.framework; path = Frameworks/Growl.framework; sourceTree = ""; }; 501B1D161728A3DA0017C92E /* SPCharsetCollationHelper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPCharsetCollationHelper.h; sourceTree = ""; }; 501B1D171728A3DA0017C92E /* SPCharsetCollationHelper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPCharsetCollationHelper.m; sourceTree = ""; }; + 502D21F51BA50710000D4CE7 /* SPDataAdditionsTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPDataAdditionsTests.m; sourceTree = ""; }; + 502D21F91BA509AF000D4CE7 /* libcrypto.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libcrypto.dylib; path = usr/lib/libcrypto.dylib; sourceTree = SDKROOT; }; 5037F79A1B00148000733564 /* SPNamedNode.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SPNamedNode.h; sourceTree = ""; }; 503B02C81AE82C5E0060CAB1 /* SPTableFilterParser.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPTableFilterParser.h; sourceTree = ""; }; 503B02C91AE82C5E0060CAB1 /* SPTableFilterParser.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPTableFilterParser.m; sourceTree = ""; }; @@ -1272,6 +1277,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 502D21FA1BA509AF000D4CE7 /* libcrypto.dylib in Frameworks */, 1717F9DB1558114D0065C036 /* OCMock.framework in Frameworks */, 1717FA43155831600065C036 /* libicucore.dylib in Frameworks */, 50EA92671AB23EE1008D3C4F /* SPMySQL.framework in Frameworks */, @@ -1936,6 +1942,7 @@ 380F4EF40FC0B68F00B0BFD7 /* SPStringAdditionsTests.m */, 1760599E1336199D0098E162 /* SPMenuAdditionsTests.m */, 1798F1C2155018D4004B0AB8 /* SPMutableArrayAdditionsTests.m */, + 502D21F51BA50710000D4CE7 /* SPDataAdditionsTests.m */, ); name = "Category Additions"; sourceTree = ""; @@ -2349,6 +2356,7 @@ 2A37F4C3FDCFA73011CA2CEA /* Frameworks */ = { isa = PBXGroup; children = ( + 502D21F91BA509AF000D4CE7 /* libcrypto.dylib */, 1058C7A6FEA54F5311CA2CBB /* Linked Frameworks */, ); name = Frameworks; @@ -3052,6 +3060,8 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 502D21F81BA50966000D4CE7 /* SPDataAdditions.m in Sources */, + 502D21F61BA50710000D4CE7 /* SPDataAdditionsTests.m in Sources */, 503B02D21AE95E010060CAB1 /* SPConstants.m in Sources */, 503B02D11AE95DD40060CAB1 /* SPTableFilterParser.m in Sources */, 503B02CF1AE95C2C0060CAB1 /* SPTableFilterParserTest.m in Sources */, -- cgit v1.2.3