]> git.saurik.com Git - apple/security.git/blob - secdxctests/KeychainAPITests.m
Security-59306.41.2.tar.gz
[apple/security.git] / secdxctests / KeychainAPITests.m
1 /*
2 * Copyright (c) 2018 Apple Inc. All Rights Reserved.
3 *
4 * @APPLE_LICENSE_HEADER_START@
5 *
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
11 * file.
12 *
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.
20 *
21 * @APPLE_LICENSE_HEADER_END@
22 */
23
24 #import "KeychainXCTest.h"
25 #import "SecDbKeychainItem.h"
26 #import "SecdTestKeychainUtilities.h"
27 #import "CKKS.h"
28 #import "SecDbKeychainItemV7.h"
29 #import "SecItemPriv.h"
30 #import "SecItemServer.h"
31 #import "spi.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>
42 #include <sys/stat.h>
43 #include <utilities/SecFileLocations.h>
44
45 void* testlist = NULL;
46
47 #if USE_KEYSTORE
48
49 @interface KeychainAPITests : KeychainXCTest
50 @end
51
52 @implementation KeychainAPITests
53
54 + (void)setUp
55 {
56 [super setUp];
57 SecCKKSDisable();
58 securityd_init(NULL);
59 }
60
61 - (NSString*)nameOfTest
62 {
63 return [self.name componentsSeparatedByCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@" ]"]][1];
64 }
65
66 - (void)setUp
67 {
68 [super setUp];
69 // KeychainXCTest already sets up keychain with custom test-named directory
70 }
71
72 - (void)testReturnValuesInSecItemUpdate
73 {
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)
80 };
81
82 NSDictionary* updateQueryWithNoReturn = @{ (id)kSecClass : (id)kSecClassGenericPassword,
83 (id)kSecAttrAccount : @"TestAccount",
84 (id)kSecAttrService : @"TestService",
85 (id)kSecUseDataProtectionKeychain : @(YES)
86 };
87
88 CFTypeRef result = NULL;
89
90 // Add the item
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);
94
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");
97
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
101
102 #if TARGET_OS_OSX
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");
106
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");
110
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");
114
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");
118 #else
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");
122
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");
126
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");
130
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");
134 #endif
135 }
136
137 #pragma mark - Corruption Tests
138
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
155 };
156
157 dispatch_semaphore_t sema = NULL;
158
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)
162 {
163 dispatch_semaphore_signal(sema);
164 }
165
166 - (void)testCorruptionHandler {
167 __security_simulatecrash_enable(false);
168
169 SecDbCorruptionExitHandler = SecDbTestCorruptionHandler;
170 sema = dispatch_semaphore_create(0);
171
172 secd_test_setup_temp_keychain([[NSString stringWithFormat:@"%@-bad", [self nameOfTest]] UTF8String], ^{
173 CFStringRef keychain_path_cf = __SecKeychainCopyPath();
174
175 CFStringPerformWithCString(keychain_path_cf, ^(const char *keychain_path) {
176 int fd = open(keychain_path, O_RDWR | O_CREAT | O_TRUNC, 0644);
177 XCTAssert(fd > -1, "Could not open fd to write keychain: %{darwin.errno}d", errno);
178
179 size_t written = write(fd, keychain_data, sizeof(keychain_data));
180 XCTAssertEqual(written, sizeof(keychain_data), "Write garbage to disk, got %lu instead of %lu: %{darwin.errno}d", written, sizeof(keychain_data), errno);
181 XCTAssertEqual(close(fd), 0, "Close keychain file failed: %{darwin.errno}d", errno);
182 });
183
184 CFReleaseNull(keychain_path_cf);
185 });
186
187 NSDictionary* query = @{(id)kSecClass : (id)kSecClassGenericPassword,
188 (id)kSecAttrAccount : @"TestAccount",
189 (id)kSecAttrService : @"TestService",
190 (id)kSecUseDataProtectionKeychain : @(YES),
191 (id)kSecReturnAttributes : @(YES)
192 };
193
194 CFTypeRef result = NULL;
195 // Real keychain should xpc_transaction_exit_clean() after this, but we nerfed it
196 XCTAssertEqual(SecItemCopyMatching((__bridge CFDictionaryRef)query, &result), errSecNotAvailable, "Expected badness from corrupt keychain");
197 XCTAssertEqual(dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC)), 0, "Timed out waiting for corruption exit handler");
198
199 sema = NULL;
200 SecDbResetCorruptionExitHandler();
201 CFReleaseNull(result);
202
203 NSString* markerpath = [NSString stringWithFormat:@"%@-iscorrupt", CFBridgingRelease(__SecKeychainCopyPath())];
204 struct stat info = {};
205 XCTAssertEqual(stat([markerpath UTF8String], &info), 0, "Unable to stat corruption marker: %{darwin.errno}d", errno);
206 }
207
208 - (void)testRecoverFromCorruption {
209 __security_simulatecrash_enable(false);
210
211 // Setup does a reset, but that doesn't create the db yet so let's sneak in first
212 __block struct stat before = {};
213 WithPathInKeychainDirectory(CFSTR("keychain-2.db"), ^(const char *filename) {
214 FILE* file = fopen(filename, "w");
215 XCTAssert(file != NULL, "Didn't get a FILE pointer");
216 fclose(file);
217 XCTAssertEqual(stat(filename, &before), 0, "Unable to stat newly created file");
218 });
219
220 WithPathInKeychainDirectory(CFSTR("keychain-2.db-iscorrupt"), ^(const char *filename) {
221 FILE* file = fopen(filename, "w");
222 XCTAssert(file != NULL, "Didn't get a FILE pointer");
223 fclose(file);
224 });
225
226 NSMutableDictionary* query = [@{(id)kSecClass : (id)kSecClassGenericPassword,
227 (id)kSecValueData : [@"password" dataUsingEncoding:NSUTF8StringEncoding],
228 (id)kSecAttrAccount : @"TestAccount",
229 (id)kSecAttrService : @"TestService",
230 (id)kSecUseDataProtectionKeychain : @(YES),
231 (id)kSecReturnAttributes : @(YES)
232 } mutableCopy];
233 CFTypeRef result = NULL;
234 XCTAssertEqual(SecItemAdd((__bridge CFDictionaryRef)query, &result), errSecSuccess, @"Should have added item to keychain");
235 XCTAssertNotNil((__bridge id)result, @"Should have received a dictionary back from SecItemAdd");
236 CFReleaseNull(result);
237
238 query[(id)kSecValueData] = nil;
239 XCTAssertEqual(SecItemCopyMatching((__bridge CFDictionaryRef)query, &result), errSecSuccess, @"Should have found item in keychain");
240 XCTAssertNotNil((__bridge id)result, @"Should have received a dictionary back from SecItemCopyMatching");
241 CFReleaseNull(result);
242
243 XCTAssertEqual(SecItemDelete((__bridge CFDictionaryRef)query), errSecSuccess, @"Should have deleted item from keychain");
244
245 WithPathInKeychainDirectory(CFSTR("keychain-2.db-iscorrupt"), ^(const char *filename) {
246 struct stat markerinfo = {};
247 XCTAssertNotEqual(stat(filename, &markerinfo), 0, "Expected not to find corruption marker after killing keychain");
248 });
249
250 __block struct stat after = {};
251 WithPathInKeychainDirectory(CFSTR("keychain-2.db"), ^(const char *filename) {
252 FILE* file = fopen(filename, "w");
253 XCTAssert(file != NULL, "Didn't get a FILE pointer");
254 fclose(file);
255 XCTAssertEqual(stat(filename, &after), 0, "Unable to stat newly created file");
256 });
257
258 if (before.st_birthtimespec.tv_sec == after.st_birthtimespec.tv_sec) {
259 XCTAssertLessThan(before.st_birthtimespec.tv_nsec, after.st_birthtimespec.tv_nsec, "db was not deleted and recreated");
260 } else {
261 XCTAssertLessThan(before.st_birthtimespec.tv_sec, after.st_birthtimespec.tv_sec, "db was not deleted and recreated");
262 }
263 }
264
265 @end
266
267 #endif