]> git.saurik.com Git - apple/security.git/blob - secdxctests/KeychainAPITests.m
Security-59306.11.20.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 SecDbCorruptionExitHandler = SecDbTestCorruptionHandler;
168 sema = dispatch_semaphore_create(0);
169
170 secd_test_setup_temp_keychain([[NSString stringWithFormat:@"%@-bad", [self nameOfTest]] UTF8String], ^{
171 CFStringRef keychain_path_cf = __SecKeychainCopyPath();
172
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);
176
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);
180 });
181
182 CFReleaseNull(keychain_path_cf);
183 });
184
185 NSDictionary* query = @{(id)kSecClass : (id)kSecClassGenericPassword,
186 (id)kSecAttrAccount : @"TestAccount",
187 (id)kSecAttrService : @"TestService",
188 (id)kSecUseDataProtectionKeychain : @(YES),
189 (id)kSecReturnAttributes : @(YES)
190 };
191
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");
196
197 sema = NULL;
198 SecDbResetCorruptionExitHandler();
199 CFReleaseNull(result);
200
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);
204 }
205
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");
212 fclose(file);
213 XCTAssertEqual(stat(filename, &before), 0, "Unable to stat newly created file");
214 });
215
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");
219 fclose(file);
220 });
221
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)
228 } mutableCopy];
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);
233
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);
238
239 XCTAssertEqual(SecItemDelete((__bridge CFDictionaryRef)query), errSecSuccess, @"Should have deleted item from keychain");
240
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");
244 });
245
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");
250 fclose(file);
251 XCTAssertEqual(stat(filename, &after), 0, "Unable to stat newly created file");
252 });
253
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");
256 } else {
257 XCTAssertLessThan(before.st_birthtimespec.tv_sec, after.st_birthtimespec.tv_sec, "db was not deleted and recreated");
258 }
259 }
260
261 @end
262
263 #endif