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 - (void)testBadTypeInParams
139 NSMutableDictionary *attrs = @{
140 (id)kSecClass: (id)kSecClassGenericPassword,
141 (id)kSecUseDataProtectionKeychain: @YES,
142 (id)kSecAttrLabel: @"testentry",
145 SecItemDelete((CFDictionaryRef)attrs);
146 XCTAssertEqual(errSecSuccess, SecItemAdd((CFDictionaryRef)attrs, NULL));
147 XCTAssertEqual(errSecSuccess, SecItemDelete((CFDictionaryRef)attrs));
149 // We try to fool SecItem API with unexpected type of kSecAttrAccessControl attribute in query and it should not crash.
150 attrs[(id)kSecAttrAccessControl] = @"string, no SecAccessControlRef!";
151 XCTAssertEqual(errSecParam, SecItemAdd((CFDictionaryRef)attrs, NULL));
152 XCTAssertEqual(errSecParam, SecItemDelete((CFDictionaryRef)attrs));
155 #pragma mark - Corruption Tests
157 const uint8_t keychain_data[] = {
158 0x62, 0x70, 0x6c, 0x69, 0x73, 0x74, 0x30, 0x30, 0xd2, 0x01, 0x02, 0x03,
159 0x04, 0x5f, 0x10, 0x1b, 0x4e, 0x53, 0x57, 0x69, 0x6e, 0x64, 0x6f, 0x77,
160 0x20, 0x46, 0x72, 0x61, 0x6d, 0x65, 0x20, 0x50, 0x72, 0x6f, 0x63, 0x65,
161 0x73, 0x73, 0x50, 0x61, 0x6e, 0x65, 0x6c, 0x5f, 0x10, 0x1d, 0x4e, 0x53,
162 0x57, 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x20, 0x46, 0x72, 0x61, 0x6d, 0x65,
163 0x20, 0x41, 0x62, 0x6f, 0x75, 0x74, 0x20, 0x54, 0x68, 0x69, 0x73, 0x20,
164 0x4d, 0x61, 0x63, 0x5f, 0x10, 0x1c, 0x32, 0x38, 0x20, 0x33, 0x37, 0x33,
165 0x20, 0x33, 0x34, 0x36, 0x20, 0x32, 0x39, 0x30, 0x20, 0x30, 0x20, 0x30,
166 0x20, 0x31, 0x34, 0x34, 0x30, 0x20, 0x38, 0x37, 0x38, 0x20, 0x5f, 0x10,
167 0x1d, 0x35, 0x36, 0x38, 0x20, 0x33, 0x39, 0x35, 0x20, 0x33, 0x30, 0x37,
168 0x20, 0x33, 0x37, 0x39, 0x20, 0x30, 0x20, 0x30, 0x20, 0x31, 0x34, 0x34,
169 0x30, 0x20, 0x38, 0x37, 0x38, 0x20, 0x08, 0x0d, 0x2b, 0x4b, 0x6a, 0x00,
170 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
171 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
172 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8a
175 dispatch_semaphore_t sema = NULL;
177 // The real corruption exit handler should xpc_transaction_exit_clean,
178 // let's be certain it does not. Also make sure exit handler gets called at all
179 static void SecDbTestCorruptionHandler(void)
181 dispatch_semaphore_signal(sema);
184 - (void)testCorruptionHandler {
185 __security_simulatecrash_enable(false);
187 SecDbCorruptionExitHandler = SecDbTestCorruptionHandler;
188 sema = dispatch_semaphore_create(0);
190 secd_test_setup_temp_keychain([[NSString stringWithFormat:@"%@-bad", [self nameOfTest]] UTF8String], ^{
191 CFStringRef keychain_path_cf = __SecKeychainCopyPath();
193 CFStringPerformWithCString(keychain_path_cf, ^(const char *keychain_path) {
194 int fd = open(keychain_path, O_RDWR | O_CREAT | O_TRUNC, 0644);
195 XCTAssert(fd > -1, "Could not open fd to write keychain: %{darwin.errno}d", errno);
197 size_t written = write(fd, keychain_data, sizeof(keychain_data));
198 XCTAssertEqual(written, sizeof(keychain_data), "Write garbage to disk, got %lu instead of %lu: %{darwin.errno}d", written, sizeof(keychain_data), errno);
199 XCTAssertEqual(close(fd), 0, "Close keychain file failed: %{darwin.errno}d", errno);
202 CFReleaseNull(keychain_path_cf);
205 NSDictionary* query = @{(id)kSecClass : (id)kSecClassGenericPassword,
206 (id)kSecAttrAccount : @"TestAccount",
207 (id)kSecAttrService : @"TestService",
208 (id)kSecUseDataProtectionKeychain : @(YES),
209 (id)kSecReturnAttributes : @(YES)
212 CFTypeRef result = NULL;
213 // Real keychain should xpc_transaction_exit_clean() after this, but we nerfed it
214 XCTAssertEqual(SecItemCopyMatching((__bridge CFDictionaryRef)query, &result), errSecNotAvailable, "Expected badness from corrupt keychain");
215 XCTAssertEqual(dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC)), 0, "Timed out waiting for corruption exit handler");
218 SecDbResetCorruptionExitHandler();
219 CFReleaseNull(result);
221 NSString* markerpath = [NSString stringWithFormat:@"%@-iscorrupt", CFBridgingRelease(__SecKeychainCopyPath())];
222 struct stat info = {};
223 XCTAssertEqual(stat([markerpath UTF8String], &info), 0, "Unable to stat corruption marker: %{darwin.errno}d", errno);
226 - (void)testRecoverFromCorruption {
227 __security_simulatecrash_enable(false);
229 // Setup does a reset, but that doesn't create the db yet so let's sneak in first
230 __block struct stat before = {};
231 WithPathInKeychainDirectory(CFSTR("keychain-2.db"), ^(const char *filename) {
232 FILE* file = fopen(filename, "w");
233 XCTAssert(file != NULL, "Didn't get a FILE pointer");
235 XCTAssertEqual(stat(filename, &before), 0, "Unable to stat newly created file");
238 WithPathInKeychainDirectory(CFSTR("keychain-2.db-iscorrupt"), ^(const char *filename) {
239 FILE* file = fopen(filename, "w");
240 XCTAssert(file != NULL, "Didn't get a FILE pointer");
244 NSMutableDictionary* query = [@{(id)kSecClass : (id)kSecClassGenericPassword,
245 (id)kSecValueData : [@"password" dataUsingEncoding:NSUTF8StringEncoding],
246 (id)kSecAttrAccount : @"TestAccount",
247 (id)kSecAttrService : @"TestService",
248 (id)kSecUseDataProtectionKeychain : @(YES),
249 (id)kSecReturnAttributes : @(YES)
251 CFTypeRef result = NULL;
252 XCTAssertEqual(SecItemAdd((__bridge CFDictionaryRef)query, &result), errSecSuccess, @"Should have added item to keychain");
253 XCTAssertNotNil((__bridge id)result, @"Should have received a dictionary back from SecItemAdd");
254 CFReleaseNull(result);
256 query[(id)kSecValueData] = nil;
257 XCTAssertEqual(SecItemCopyMatching((__bridge CFDictionaryRef)query, &result), errSecSuccess, @"Should have found item in keychain");
258 XCTAssertNotNil((__bridge id)result, @"Should have received a dictionary back from SecItemCopyMatching");
259 CFReleaseNull(result);
261 XCTAssertEqual(SecItemDelete((__bridge CFDictionaryRef)query), errSecSuccess, @"Should have deleted item from keychain");
263 WithPathInKeychainDirectory(CFSTR("keychain-2.db-iscorrupt"), ^(const char *filename) {
264 struct stat markerinfo = {};
265 XCTAssertNotEqual(stat(filename, &markerinfo), 0, "Expected not to find corruption marker after killing keychain");
268 __block struct stat after = {};
269 WithPathInKeychainDirectory(CFSTR("keychain-2.db"), ^(const char *filename) {
270 FILE* file = fopen(filename, "w");
271 XCTAssert(file != NULL, "Didn't get a FILE pointer");
273 XCTAssertEqual(stat(filename, &after), 0, "Unable to stat newly created file");
276 if (before.st_birthtimespec.tv_sec == after.st_birthtimespec.tv_sec) {
277 XCTAssertLessThan(before.st_birthtimespec.tv_nsec, after.st_birthtimespec.tv_nsec, "db was not deleted and recreated");
279 XCTAssertLessThan(before.st_birthtimespec.tv_sec, after.st_birthtimespec.tv_sec, "db was not deleted and recreated");