diff --git a/DashSync/shared/Libraries/SecEnclaveCrypto.h b/DashSync/shared/Libraries/SecEnclaveCrypto.h new file mode 100644 index 000000000..74443ee88 --- /dev/null +++ b/DashSync/shared/Libraries/SecEnclaveCrypto.h @@ -0,0 +1,31 @@ +// +// SecEnclaveCrypto.h +// SecEnclaveCrypto +// +// Created by Andrew Podkovyrin on 06.08.2021. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface SecEnclaveCrypto : NSObject + ++ (BOOL)isAvailable; + +- (BOOL)hasPrivateKeyName:(NSString *)name + error:(NSError *_Nullable __autoreleasing *)error; + +- (nullable NSData *)encrypt:(NSData *)plainTextData + withPublicKeyName:(NSString *)name + error:(NSError *_Nullable __autoreleasing *)error; + +- (nullable NSData *)decrypt:(NSData *)cipherTextData + withPrivateKeyName:(NSString *)name + error:(NSError *_Nullable __autoreleasing *)error; + +- (void)deletePrivateKeyWithName:(NSString *)name; + +@end + +NS_ASSUME_NONNULL_END diff --git a/DashSync/shared/Libraries/SecEnclaveCrypto.m b/DashSync/shared/Libraries/SecEnclaveCrypto.m new file mode 100644 index 000000000..8f6f62bf1 --- /dev/null +++ b/DashSync/shared/Libraries/SecEnclaveCrypto.m @@ -0,0 +1,280 @@ +// +// SecEnclaveCrypto.m +// SecEnclaveCrypto +// +// Created by Andrew Podkovyrin on 06.08.2021. +// + +#import "SecEnclaveCrypto.h" + +#import + +NS_ASSUME_NONNULL_BEGIN + +@implementation SecEnclaveCrypto + +#pragma mark - Public + ++ (BOOL)isAvailable { + NSString *tmpName = [[NSUUID UUID] UUIDString]; + SecEnclaveCrypto *tmpCrypto = [[SecEnclaveCrypto alloc] init]; + + NSError *createError = nil; + SecKeyRef privateKey = [tmpCrypto createPrivateKeyWithName:tmpName error:&createError]; + if (privateKey == nil) { + return NO; + } + + SecKeyRef publicKey = SecKeyCopyPublicKey(privateKey); + if (publicKey == nil) { + CFRelease(privateKey); + [tmpCrypto deletePrivateKeyWithName:tmpName]; + return NO; + } + + BOOL canEncrypt = SecKeyIsAlgorithmSupported(publicKey, kSecKeyOperationTypeEncrypt, [self algorithm]); + BOOL canDecrypt = SecKeyIsAlgorithmSupported(privateKey, kSecKeyOperationTypeDecrypt, [self algorithm]); + + CFRelease(publicKey); + CFRelease(privateKey); + [tmpCrypto deletePrivateKeyWithName:tmpName]; + + return canEncrypt && canDecrypt; +} + +- (BOOL)hasPrivateKeyName:(NSString *)name error:(NSError *_Nullable __autoreleasing *)error { + NSError *checkError = nil; + SecKeyRef privateKey = [self getPrivateKeyWithName:name error:&checkError]; + if (checkError.code == errSecItemNotFound) { + return NO; + } + if (error && checkError) { + *error = checkError; + return NO; + } + BOOL hasKey = privateKey != nil; + CFRelease(privateKey); + return hasKey; +} + +- (nullable NSData *)encrypt:(NSData *)plainTextData + withPublicKeyName:(NSString *)name + error:(NSError *_Nullable __autoreleasing *)error { + NSError *getError = nil; + SecKeyRef privateKey = [self getPrivateKeyWithName:name error:&getError]; + if (privateKey == nil) { + privateKey = [self createPrivateKeyWithName:name error:error]; + if (privateKey == nil) { + return nil; + } + } + + SecKeyRef publicKey = SecKeyCopyPublicKey(privateKey); + CFRelease(privateKey); + if (publicKey == nil) { + if (error) { + *error = [NSError errorWithDomain:NSOSStatusErrorDomain + code:errSecPublicKeyInconsistent + userInfo:nil]; + } + return nil; + } + + NSData *encrypted = [self encrypt:plainTextData withPublicKey:publicKey error:error]; + CFRelease(publicKey); + return encrypted; +} + +- (nullable NSData *)decrypt:(NSData *)cipherTextData + withPrivateKeyName:(NSString *)name + error:(NSError *_Nullable __autoreleasing *)error { + SecKeyRef privateKey = [self getPrivateKeyWithName:name error:error]; + if (privateKey == nil) { + return nil; + } + + NSData *decrypted = [self decrypt:cipherTextData withPrivateKey:privateKey error:error]; + CFRelease(privateKey); + return decrypted; +} + +- (void)deletePrivateKeyWithName:(NSString *)name { + NSDictionary *query = @{ + (__bridge id)kSecClass: (__bridge id)kSecClassKey, + (__bridge id)kSecAttrKeyType: (__bridge id)kSecAttrKeyTypeECSECPrimeRandom, + (__bridge id)kSecAttrApplicationTag: [self tagDataFor:name], + }; + + __unused OSStatus status = SecItemDelete((__bridge CFDictionaryRef)query); +} + +#pragma mark - Private + +- (NSData *)tagDataFor:(NSString *)name { + return [name dataUsingEncoding:NSUTF8StringEncoding]; +} + +- (nullable SecKeyRef)getPrivateKeyWithName:(NSString *)name + error:(NSError *_Nullable __autoreleasing *)error { + NSDictionary *query = @{ + (__bridge id)kSecClass: (__bridge id)kSecClassKey, + (__bridge id)kSecAttrKeyType: (__bridge id)kSecAttrKeyTypeECSECPrimeRandom, + (__bridge id)kSecAttrApplicationTag: [self tagDataFor:name], + (__bridge id)kSecReturnRef: @YES, + }; + + CFDataRef result = nil; + OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, (CFTypeRef *)&result); + + if (status == errSecSuccess) { + return (SecKeyRef)result; + } else { + if (error) { + *error = [NSError errorWithDomain:NSOSStatusErrorDomain + code:status + userInfo:nil]; + } + return nil; + } +} + +- (NSInteger)getPrivateKeyCountWithName:(NSString *)name { + NSDictionary *query = @{ + (__bridge id)kSecClass: (__bridge id)kSecClassKey, + (__bridge id)kSecAttrKeyType: (__bridge id)kSecAttrKeyTypeECSECPrimeRandom, + (__bridge id)kSecAttrApplicationTag: [self tagDataFor:name], + (__bridge id)kSecReturnRef: @YES, + (__bridge id)kSecMatchLimit: (__bridge id)kSecMatchLimitAll, + }; + + CFArrayRef result = nil; + OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, (CFTypeRef *)&result); + if (status == errSecSuccess) { + NSInteger count = CFArrayGetCount(result); + CFRelease(result); + return count; + } else { + return 0; + } +} + +- (nullable NSData *)encrypt:(NSData *)plainTextData + withPublicKey:(SecKeyRef)publicKey + error:(NSError *_Nullable __autoreleasing *)error { + if (SecKeyIsAlgorithmSupported(publicKey, kSecKeyOperationTypeEncrypt, [self.class algorithm]) == NO) { + if (error) { + *error = [NSError errorWithDomain:NSOSStatusErrorDomain + code:errSecInvalidAlgorithm + userInfo:nil]; + } + return nil; + } + + CFErrorRef cfError = nil; + CFDataRef encrypted = SecKeyCreateEncryptedData( + publicKey, + [self.class algorithm], + (CFDataRef)plainTextData, + &cfError); + NSData *result = CFBridgingRelease(encrypted); + if (cfError && error) { + *error = CFBridgingRelease(cfError); + return nil; + } + return result; +} + +- (nullable NSData *)decrypt:(NSData *)cipherTextData + withPrivateKey:(SecKeyRef)privateKey + error:(NSError *_Nullable __autoreleasing *)error { + if (SecKeyIsAlgorithmSupported(privateKey, kSecKeyOperationTypeDecrypt, [self.class algorithm]) == NO) { + if (error) { + *error = [NSError errorWithDomain:NSOSStatusErrorDomain + code:errSecInvalidAlgorithm + userInfo:nil]; + } + return nil; + } + + CFErrorRef cfError = nil; + CFDataRef decrypted = SecKeyCreateDecryptedData( + privateKey, + [self.class algorithm], + (CFDataRef)cipherTextData, + &cfError); + NSData *result = CFBridgingRelease(decrypted); + if (cfError && error) { + *error = CFBridgingRelease(cfError); + return nil; + } + return result; +} + +- (nullable SecKeyRef)createPrivateKeyWithName:(NSString *)name + error:(NSError *_Nullable __autoreleasing *)error { + if ([self getPrivateKeyCountWithName:name] != 0) { + return nil; // key already exists + } + + CFErrorRef cfError = nil; + SecAccessControlRef access = SecAccessControlCreateWithFlags( + kCFAllocatorDefault, + kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly, + kSecAccessControlPrivateKeyUsage, + &cfError); + if (access == nil) { + if (cfError && error) { + *error = CFBridgingRelease(cfError); + } + return nil; + } + + NSMutableDictionary *query = [NSMutableDictionary dictionaryWithDictionary:@{ + (__bridge id)kSecAttrKeyType: (__bridge id)kSecAttrKeyTypeECSECPrimeRandom, + (__bridge id)kSecAttrKeySizeInBits: @([self.class numberOfBitsInKey]), + (__bridge id)kSecPrivateKeyAttrs: @{ + (__bridge id)kSecAttrIsPermanent: @YES, + (__bridge id)kSecAttrApplicationTag: [self tagDataFor:name], + (__bridge id)kSecAttrAccessControl: (__bridge id)access, + }, + }]; + if ([self isSimulator] == NO) { + query[(__bridge id)kSecAttrTokenID] = (__bridge id)kSecAttrTokenIDSecureEnclave; + } + + SecKeyRef key = SecKeyCreateRandomKey((__bridge CFDictionaryRef)query, &cfError); + CFRelease(access); + if (key == nil) { + if (cfError && error) { + *error = [NSError errorWithDomain:NSOSStatusErrorDomain + code:CFErrorGetCode(cfError) + userInfo:nil]; + } + return nil; + } + + return key; +} + +#pragma mark - Private Constants + ++ (SecKeyAlgorithm)algorithm { + // https://developer.apple.com/documentation/security/certificate_key_and_trust_services/keys/storing_keys_in_the_secure_enclave + return kSecKeyAlgorithmECIESEncryptionCofactorVariableIVX963SHA256AESGCM; +} + ++ (NSInteger)numberOfBitsInKey { + return 256; +} + +- (BOOL)isSimulator { +#if TARGET_OS_SIMULATOR + return YES; +#else + return NO; +#endif /* TARGET_OS_SIMULATOR */ +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Example/DashSync.xcodeproj/project.pbxproj b/Example/DashSync.xcodeproj/project.pbxproj index 7d2023971..0fe00c59d 100644 --- a/Example/DashSync.xcodeproj/project.pbxproj +++ b/Example/DashSync.xcodeproj/project.pbxproj @@ -34,6 +34,7 @@ 2A7DCCB220DA10870097049F /* DSBloomFilterTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 2A7DCCB120DA10870097049F /* DSBloomFilterTests.m */; }; 2A7DCCB420DA198F0097049F /* DSPaymentProtocolTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 2A7DCCB320DA198F0097049F /* DSPaymentProtocolTests.m */; }; 2A90EDAA2268C15D00D95B6B /* DSUInt256IndexPathTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 2A90EDA92268C15D00D95B6B /* DSUInt256IndexPathTests.m */; }; + 2A96327126C08E6400D055CC /* DSSecEnclaveCryptoTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 2A96327026C08E6400D055CC /* DSSecEnclaveCryptoTests.m */; }; 2A9FFEC622328D0E00956D5F /* DSContactsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2A9FFEC522328D0E00956D5F /* DSContactsViewController.m */; }; 2A9FFEC92232920900956D5F /* DSContactsTabBarViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2A9FFEC82232920900956D5F /* DSContactsTabBarViewController.m */; }; 2A9FFF012233B90000956D5F /* DSContactsNavigationController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2A9FFF002233B90000956D5F /* DSContactsNavigationController.m */; }; @@ -557,6 +558,7 @@ 2A7DCCB120DA10870097049F /* DSBloomFilterTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = DSBloomFilterTests.m; sourceTree = ""; }; 2A7DCCB320DA198F0097049F /* DSPaymentProtocolTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = DSPaymentProtocolTests.m; sourceTree = ""; }; 2A90EDA92268C15D00D95B6B /* DSUInt256IndexPathTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = DSUInt256IndexPathTests.m; sourceTree = ""; }; + 2A96327026C08E6400D055CC /* DSSecEnclaveCryptoTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = DSSecEnclaveCryptoTests.m; sourceTree = ""; }; 2A9FFEC422328D0E00956D5F /* DSContactsViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DSContactsViewController.h; sourceTree = ""; }; 2A9FFEC522328D0E00956D5F /* DSContactsViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = DSContactsViewController.m; sourceTree = ""; }; 2A9FFEC72232920900956D5F /* DSContactsTabBarViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DSContactsTabBarViewController.h; sourceTree = ""; }; @@ -2249,6 +2251,7 @@ FBAF79782385CA0200F4944E /* DSSparseMerkleTreeTests.m */, FBE07DD22467440200A1079C /* DSChainedSigningTests.m */, 2A90EDA92268C15D00D95B6B /* DSUInt256IndexPathTests.m */, + 2A96327026C08E6400D055CC /* DSSecEnclaveCryptoTests.m */, ); name = LibraryTests; sourceTree = ""; @@ -3050,6 +3053,7 @@ FB1B98D123D6A3BD00D50F69 /* DSTransitionTests.m in Sources */, FB29C31425A3595D001F4F43 /* DSInstantSendLockTests.m in Sources */, FB3ADDE6238E62CD00C31D23 /* DSChainLockTests.m in Sources */, + 2A96327126C08E6400D055CC /* DSSecEnclaveCryptoTests.m in Sources */, FBE4603C256B1AEF0052A6DE /* DSDIP14Tests.m in Sources */, FB87A0CE24B7C28700C22DF7 /* DSMiningTests.m in Sources */, FBDB9B7E20FF7AB900AD61B3 /* DSDeterministicMasternodeListTests.m in Sources */, diff --git a/Example/Tests/DSSecEnclaveCryptoTests.m b/Example/Tests/DSSecEnclaveCryptoTests.m new file mode 100644 index 000000000..d7e7520ed --- /dev/null +++ b/Example/Tests/DSSecEnclaveCryptoTests.m @@ -0,0 +1,96 @@ +// +// Created by Andrew Podkovyrin +// Copyright © 2021 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import + +#import + +@interface DSSecEnclaveCryptoTests : XCTestCase + +@end + +@implementation DSSecEnclaveCryptoTests + +- (void)testAvailability { + XCTAssert([SecEnclaveCrypto isAvailable], @"Should be available"); +} + +- (void)testFlow { + NSString *str1 = @"hello world"; + NSData *plainData = [str1 dataUsingEncoding:NSUTF8StringEncoding]; + + NSString *name = @"org.dash.pk-secencl.test1"; + SecEnclaveCrypto *crypto = [[SecEnclaveCrypto alloc] init]; + NSError *error = nil; + NSData *encrypted = [crypto encrypt:plainData withPublicKeyName:name error:&error]; + XCTAssert(error == nil, @"Encryption failed"); + XCTAssertNotNil(encrypted); + BOOL hasKey = [crypto hasPrivateKeyName:name error:&error]; + XCTAssert(hasKey == YES, @"A key should be created"); + + NSData *decrypted = [crypto decrypt:encrypted withPrivateKeyName:name error:&error]; + XCTAssert(error == nil, @"Decryption failed"); + XCTAssertNotNil(decrypted); + + NSString *str2 = [[NSString alloc] initWithData:decrypted encoding:NSUTF8StringEncoding]; + XCTAssert([str1 isEqualToString:str2], @"Decryption failed"); + + [crypto deletePrivateKeyWithName:name]; + hasKey = [crypto hasPrivateKeyName:name error:&error]; + XCTAssert(hasKey == NO, @"Failed to delete"); + XCTAssert(error == nil, @"Key check error"); +} + +- (void)testDecryptionWithoutKey { + NSString *str1 = @"hello world"; + NSData *plainData = [str1 dataUsingEncoding:NSUTF8StringEncoding]; + + NSString *name = @"org.dash.pk-secencl.test2"; + SecEnclaveCrypto *crypto = [[SecEnclaveCrypto alloc] init]; + + // make sure there's no key + [crypto deletePrivateKeyWithName:name]; + NSError *error = nil; + BOOL hasKey = [crypto hasPrivateKeyName:name error:&error]; + XCTAssert(hasKey == NO, @"Failed to delete"); + XCTAssert(error == nil, @"Key check error"); + + // doesn't matter what we're trying to decrypt + NSData *decrypted = [crypto decrypt:plainData withPrivateKeyName:name error:&error]; + XCTAssert(error != nil, @"Decryption must fail"); + XCTAssertNil(decrypted); +} + +- (void)testInvalidDecryption { + NSString *str1 = @"hello world"; + NSData *plainData = [str1 dataUsingEncoding:NSUTF8StringEncoding]; + + NSString *name = @"org.dash.pk-secencl.test3"; + SecEnclaveCrypto *crypto = [[SecEnclaveCrypto alloc] init]; + NSError *error = nil; + [crypto encrypt:plainData withPublicKeyName:name error:&error]; + XCTAssert(error == nil, @"Encryption failed"); + + // trying to decrypt **plain** data + NSData *decrypted = [crypto decrypt:plainData withPrivateKeyName:name error:&error]; + XCTAssert(error != nil, @"Decryption must fail"); + XCTAssertNil(decrypted); + + [crypto deletePrivateKeyWithName:name]; +} + +@end