2 * Copyright (c) 2018 Apple Inc. All Rights Reserved.
4 * @APPLE_LICENSE_HEADER_START@
6 * This file contains Original Code and/or Modifications of Original Code
7 * as defined in and that are subject to the Apple Public Source License
8 * Version 2.0 (the 'License'). You may not use this file except in
9 * compliance with the License. Please obtain a copy of the License at
10 * http://www.opensource.apple.com/apsl/ and read it before using this
13 * The Original Code and all software distributed under the License are
14 * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
15 * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
16 * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
17 * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
18 * Please see the License for the specific language governing rights and
19 * limitations under the License.
21 * @APPLE_LICENSE_HEADER_END@
24 #import "KeychainXCTest.h"
25 #import "SecDbKeychainItem.h"
26 #import "SecdTestKeychainUtilities.h"
28 #import "SecDbKeychainItemV7.h"
29 #import "SecItemPriv.h"
30 #import "SecItemServer.h"
32 #import "SecDbKeychainSerializedItemV7.h"
33 #import "SecDbKeychainSerializedMetadata.h"
34 #import "SecDbKeychainSerializedSecretData.h"
35 #import "SecDbKeychainSerializedAKSWrappedKey.h"
36 #import <utilities/SecCFWrappers.h>
37 #import <SecurityFoundation/SFEncryptionOperation.h>
38 #import <XCTest/XCTest.h>
39 #import <OCMock/OCMock.h>
40 #include <dispatch/dispatch.h>
41 #include <utilities/SecDb.h>
43 #include <utilities/SecFileLocations.h>
45 void* testlist = NULL;
49 @interface KeychainAPITests : KeychainXCTest
52 @implementation KeychainAPITests
61 - (NSString*)nameOfTest
63 return [self.name componentsSeparatedByCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@" ]"]][1];
69 // KeychainXCTest already sets up keychain with custom test-named directory
72 - (void)testReturnValuesInSecItemUpdate
74 NSDictionary* addQuery = @{ (id)kSecClass : (id)kSecClassGenericPassword,
75 (id)kSecValueData : [@"password" dataUsingEncoding:NSUTF8StringEncoding],
76 (id)kSecAttrAccount : @"TestAccount",
77 (id)kSecAttrService : @"TestService",
78 (id)kSecUseDataProtectionKeychain : @(YES),
79 (id)kSecReturnAttributes : @(YES)
82 NSDictionary* updateQueryWithNoReturn = @{ (id)kSecClass : (id)kSecClassGenericPassword,
83 (id)kSecAttrAccount : @"TestAccount",
84 (id)kSecAttrService : @"TestService",
85 (id)kSecUseDataProtectionKeychain : @(YES)
88 CFTypeRef result = NULL;
91 XCTAssertEqual(SecItemAdd((__bridge CFDictionaryRef)addQuery, &result), errSecSuccess, @"Should have succeeded in adding test item to keychain");
92 XCTAssertNotNil((__bridge id)result, @"Should have received a dictionary back from SecItemAdd");
93 CFReleaseNull(result);
95 // And we can update the item
96 XCTAssertEqual(SecItemUpdate((__bridge CFDictionaryRef)updateQueryWithNoReturn, (__bridge CFDictionaryRef)@{(id)kSecValueData: [@"otherpassword" dataUsingEncoding:NSUTF8StringEncoding]}), errSecSuccess, "failed to update item with clean update query");
98 // great, a normal update works
99 // now let's do updates with various queries which include return parameters to ensure they succeed on macOS and throw errors on iOS.
100 // this is a status-quo compromise between changing iOS match macOS (which has lamé no-op characteristics) and changing macOS to match iOS, which risks breaking existing clients
103 NSMutableDictionary* updateQueryWithReturnAttributes = updateQueryWithNoReturn.mutableCopy;
104 updateQueryWithReturnAttributes[(id)kSecReturnAttributes] = @(YES);
105 XCTAssertEqual(SecItemUpdate((__bridge CFDictionaryRef)updateQueryWithReturnAttributes, (__bridge CFDictionaryRef)@{(id)kSecValueData: [@"return-attributes" dataUsingEncoding:NSUTF8StringEncoding]}), errSecSuccess, "failed to update item with return attributes query");
107 NSMutableDictionary* updateQueryWithReturnData = updateQueryWithNoReturn.mutableCopy;
108 updateQueryWithReturnAttributes[(id)kSecReturnData] = @(YES);
109 XCTAssertEqual(SecItemUpdate((__bridge CFDictionaryRef)updateQueryWithReturnData, (__bridge CFDictionaryRef)@{(id)kSecValueData: [@"return-data" dataUsingEncoding:NSUTF8StringEncoding]}), errSecSuccess, "failed to update item with return data query");
111 NSMutableDictionary* updateQueryWithReturnRef = updateQueryWithNoReturn.mutableCopy;
112 updateQueryWithReturnAttributes[(id)kSecReturnRef] = @(YES);
113 XCTAssertEqual(SecItemUpdate((__bridge CFDictionaryRef)updateQueryWithReturnRef, (__bridge CFDictionaryRef)@{(id)kSecValueData: [@"return-ref" dataUsingEncoding:NSUTF8StringEncoding]}), errSecSuccess, "failed to update item with return ref query");
115 NSMutableDictionary* updateQueryWithReturnPersistentRef = updateQueryWithNoReturn.mutableCopy;
116 updateQueryWithReturnAttributes[(id)kSecReturnPersistentRef] = @(YES);
117 XCTAssertEqual(SecItemUpdate((__bridge CFDictionaryRef)updateQueryWithReturnPersistentRef, (__bridge CFDictionaryRef)@{(id)kSecValueData: [@"return-persistent-ref" dataUsingEncoding:NSUTF8StringEncoding]}), errSecSuccess, "failed to update item with return persistent ref query");
119 NSMutableDictionary* updateQueryWithReturnAttributes = updateQueryWithNoReturn.mutableCopy;
120 updateQueryWithReturnAttributes[(id)kSecReturnAttributes] = @(YES);
121 XCTAssertEqual(SecItemUpdate((__bridge CFDictionaryRef)updateQueryWithReturnAttributes, (__bridge CFDictionaryRef)@{(id)kSecValueData: [@"return-attributes" dataUsingEncoding:NSUTF8StringEncoding]}), errSecParam, "failed to generate error updating item with return attributes query");
123 NSMutableDictionary* updateQueryWithReturnData = updateQueryWithNoReturn.mutableCopy;
124 updateQueryWithReturnData[(id)kSecReturnData] = @(YES);
125 XCTAssertEqual(SecItemUpdate((__bridge CFDictionaryRef)updateQueryWithReturnData, (__bridge CFDictionaryRef)@{(id)kSecValueData: [@"return-data" dataUsingEncoding:NSUTF8StringEncoding]}), errSecParam, "failed to generate error updating item with return data query");
127 NSMutableDictionary* updateQueryWithReturnRef = updateQueryWithNoReturn.mutableCopy;
128 updateQueryWithReturnRef[(id)kSecReturnRef] = @(YES);
129 XCTAssertEqual(SecItemUpdate((__bridge CFDictionaryRef)updateQueryWithReturnRef, (__bridge CFDictionaryRef)@{(id)kSecValueData: [@"return-ref" dataUsingEncoding:NSUTF8StringEncoding]}), errSecParam, "failed to generate error updating item with return ref query");
131 NSMutableDictionary* updateQueryWithReturnPersistentRef = updateQueryWithNoReturn.mutableCopy;
132 updateQueryWithReturnPersistentRef[(id)kSecReturnPersistentRef] = @(YES);
133 XCTAssertEqual(SecItemUpdate((__bridge CFDictionaryRef)updateQueryWithReturnPersistentRef, (__bridge CFDictionaryRef)@{(id)kSecValueData: [@"return-persistent-ref" dataUsingEncoding:NSUTF8StringEncoding]}), errSecParam, "failed to generate error updating item with return persistent ref query");
137 #pragma mark - Corruption Tests
139 const uint8_t keychain_data[] = {
140 0x62, 0x70, 0x6c, 0x69, 0x73, 0x74, 0x30, 0x30, 0xd2, 0x01, 0x02, 0x03,
141 0x04, 0x5f, 0x10, 0x1b, 0x4e, 0x53, 0x57, 0x69, 0x6e, 0x64, 0x6f, 0x77,
142 0x20, 0x46, 0x72, 0x61, 0x6d, 0x65, 0x20, 0x50, 0x72, 0x6f, 0x63, 0x65,
143 0x73, 0x73, 0x50, 0x61, 0x6e, 0x65, 0x6c, 0x5f, 0x10, 0x1d, 0x4e, 0x53,
144 0x57, 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x20, 0x46, 0x72, 0x61, 0x6d, 0x65,
145 0x20, 0x41, 0x62, 0x6f, 0x75, 0x74, 0x20, 0x54, 0x68, 0x69, 0x73, 0x20,
146 0x4d, 0x61, 0x63, 0x5f, 0x10, 0x1c, 0x32, 0x38, 0x20, 0x33, 0x37, 0x33,
147 0x20, 0x33, 0x34, 0x36, 0x20, 0x32, 0x39, 0x30, 0x20, 0x30, 0x20, 0x30,
148 0x20, 0x31, 0x34, 0x34, 0x30, 0x20, 0x38, 0x37, 0x38, 0x20, 0x5f, 0x10,
149 0x1d, 0x35, 0x36, 0x38, 0x20, 0x33, 0x39, 0x35, 0x20, 0x33, 0x30, 0x37,
150 0x20, 0x33, 0x37, 0x39, 0x20, 0x30, 0x20, 0x30, 0x20, 0x31, 0x34, 0x34,
151 0x30, 0x20, 0x38, 0x37, 0x38, 0x20, 0x08, 0x0d, 0x2b, 0x4b, 0x6a, 0x00,
152 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
153 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
154 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8a
157 dispatch_semaphore_t sema = NULL;
159 // The real corruption exit handler should xpc_transaction_exit_clean,
160 // let's be certain it does not. Also make sure exit handler gets called at all
161 static void SecDbTestCorruptionHandler(void)
163 dispatch_semaphore_signal(sema);
166 - (void)testCorruptionHandler {
167 SecDbCorruptionExitHandler = SecDbTestCorruptionHandler;
168 sema = dispatch_semaphore_create(0);
170 secd_test_setup_temp_keychain([[NSString stringWithFormat:@"%@-bad", [self nameOfTest]] UTF8String], ^{
171 CFStringRef keychain_path_cf = __SecKeychainCopyPath();
173 CFStringPerformWithCString(keychain_path_cf, ^(const char *keychain_path) {
174 int fd = open(keychain_path, O_RDWR | O_CREAT | O_TRUNC, 0644);
175 XCTAssert(fd > -1, "Could not open fd to write keychain: %{darwin.errno}d", errno);
177 size_t written = write(fd, keychain_data, sizeof(keychain_data));
178 XCTAssertEqual(written, sizeof(keychain_data), "Write garbage to disk, got %lu instead of %lu: %{darwin.errno}d", written, sizeof(keychain_data), errno);
179 XCTAssertEqual(close(fd), 0, "Close keychain file failed: %{darwin.errno}d", errno);
182 CFReleaseNull(keychain_path_cf);
185 NSDictionary* query = @{(id)kSecClass : (id)kSecClassGenericPassword,
186 (id)kSecAttrAccount : @"TestAccount",
187 (id)kSecAttrService : @"TestService",
188 (id)kSecUseDataProtectionKeychain : @(YES),
189 (id)kSecReturnAttributes : @(YES)
192 CFTypeRef result = NULL;
193 // Real keychain should xpc_transaction_exit_clean() after this, but we nerfed it
194 XCTAssertEqual(SecItemCopyMatching((__bridge CFDictionaryRef)query, &result), errSecNotAvailable, "Expected badness from corrupt keychain");
195 XCTAssertEqual(dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC)), 0, "Timed out waiting for corruption exit handler");
198 SecDbResetCorruptionExitHandler();
199 CFReleaseNull(result);
201 NSString* markerpath = [NSString stringWithFormat:@"%@-iscorrupt", CFBridgingRelease(__SecKeychainCopyPath())];
202 struct stat info = {};
203 XCTAssertEqual(stat([markerpath UTF8String], &info), 0, "Unable to stat corruption marker: %{darwin.errno}d", errno);
206 - (void)testRecoverFromCorruption {
207 // Setup does a reset, but that doesn't create the db yet so let's sneak in first
208 __block struct stat before = {};
209 WithPathInKeychainDirectory(CFSTR("keychain-2.db"), ^(const char *filename) {
210 FILE* file = fopen(filename, "w");
211 XCTAssert(file != NULL, "Didn't get a FILE pointer");
213 XCTAssertEqual(stat(filename, &before), 0, "Unable to stat newly created file");
216 WithPathInKeychainDirectory(CFSTR("keychain-2.db-iscorrupt"), ^(const char *filename) {
217 FILE* file = fopen(filename, "w");
218 XCTAssert(file != NULL, "Didn't get a FILE pointer");
222 NSMutableDictionary* query = [@{(id)kSecClass : (id)kSecClassGenericPassword,
223 (id)kSecValueData : [@"password" dataUsingEncoding:NSUTF8StringEncoding],
224 (id)kSecAttrAccount : @"TestAccount",
225 (id)kSecAttrService : @"TestService",
226 (id)kSecUseDataProtectionKeychain : @(YES),
227 (id)kSecReturnAttributes : @(YES)
229 CFTypeRef result = NULL;
230 XCTAssertEqual(SecItemAdd((__bridge CFDictionaryRef)query, &result), errSecSuccess, @"Should have added item to keychain");
231 XCTAssertNotNil((__bridge id)result, @"Should have received a dictionary back from SecItemAdd");
232 CFReleaseNull(result);
234 query[(id)kSecValueData] = nil;
235 XCTAssertEqual(SecItemCopyMatching((__bridge CFDictionaryRef)query, &result), errSecSuccess, @"Should have found item in keychain");
236 XCTAssertNotNil((__bridge id)result, @"Should have received a dictionary back from SecItemCopyMatching");
237 CFReleaseNull(result);
239 XCTAssertEqual(SecItemDelete((__bridge CFDictionaryRef)query), errSecSuccess, @"Should have deleted item from keychain");
241 WithPathInKeychainDirectory(CFSTR("keychain-2.db-iscorrupt"), ^(const char *filename) {
242 struct stat markerinfo = {};
243 XCTAssertNotEqual(stat(filename, &markerinfo), 0, "Expected not to find corruption marker after killing keychain");
246 __block struct stat after = {};
247 WithPathInKeychainDirectory(CFSTR("keychain-2.db"), ^(const char *filename) {
248 FILE* file = fopen(filename, "w");
249 XCTAssert(file != NULL, "Didn't get a FILE pointer");
251 XCTAssertEqual(stat(filename, &after), 0, "Unable to stat newly created file");
254 if (before.st_birthtimespec.tv_sec == after.st_birthtimespec.tv_sec) {
255 XCTAssertLessThan(before.st_birthtimespec.tv_nsec, after.st_birthtimespec.tv_nsec, "db was not deleted and recreated");
257 XCTAssertLessThan(before.st_birthtimespec.tv_sec, after.st_birthtimespec.tv_sec, "db was not deleted and recreated");