Last active
April 25, 2017 11:42
-
-
Save rbaulin/26a31dc30a6ebe78560e3299d8f41d29 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// KeychainBehaviourTests.m | |
// | |
// Created by Roman Baulin on 25/04/2017. | |
// Copyright © 2017. All rights reserved. | |
// | |
// | |
// see SecBase.h and osstatus.com and to investigate keychain error codes | |
// | |
// errSecItemNotFound = -25300, /* The specified item could not be found in the keychain. */ | |
// errSecDuplicateItem = -25299, /* The specified item already exists in the keychain. */ | |
// errSecItemNotFound = -25300, /* The specified item could not be found in the keychain. */ | |
// errSecInteractionNotAllowed = -25308, /* User interaction is not allowed. */ | |
// | |
#import <XCTest/XCTest.h> | |
@interface KeychainBehaviourTests : XCTestCase | |
@end | |
@implementation KeychainBehaviourTests | |
- (void)setUp { | |
[super setUp]; | |
// cleanup keychain | |
NSArray *secItemClasses = @[(__bridge id)kSecClassGenericPassword, | |
(__bridge id)kSecClassInternetPassword, | |
(__bridge id)kSecClassCertificate, | |
(__bridge id)kSecClassKey, | |
(__bridge id)kSecClassIdentity]; | |
for (id secItemClass in secItemClasses) { | |
NSDictionary *spec = @{(__bridge id)kSecClass: secItemClass}; | |
SecItemDelete((__bridge CFDictionaryRef)spec); | |
} | |
} | |
- (void)tearDown { | |
// cleanup keychain | |
NSArray *secItemClasses = @[(__bridge id)kSecClassGenericPassword, | |
(__bridge id)kSecClassInternetPassword, | |
(__bridge id)kSecClassCertificate, | |
(__bridge id)kSecClassKey, | |
(__bridge id)kSecClassIdentity]; | |
for (id secItemClass in secItemClasses) { | |
NSDictionary *spec = @{(__bridge id)kSecClass: secItemClass}; | |
SecItemDelete((__bridge CFDictionaryRef)spec); | |
} | |
[super tearDown]; | |
} | |
- (void)testKeychainAddAccessibleNilSearchAllCases { | |
// this test shows that kSecAttrAccessible defaults to kSecAttrAccessibleWhenUnlocked | |
NSString *service = @"service"; | |
NSString *account = @"account"; | |
NSData *data = [@"whenunlocked" dataUsingEncoding:NSUTF8StringEncoding]; | |
OSStatus status; | |
NSDictionary *query; | |
CFMutableDictionaryRef result = nil; | |
// add item with kSecAttrAccessible nil | |
query = @{ (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword, | |
(__bridge id)kSecAttrService: service, | |
(__bridge id)kSecAttrAccount: account, | |
(__bridge id)kSecValueData: data, | |
(__bridge id)kSecAttrAccessible: (__bridge id)kSecAttrAccessibleWhenUnlocked }; | |
status = SecItemAdd((__bridge CFDictionaryRef)query, (CFTypeRef *)&result); | |
XCTAssert(status == errSecSuccess); | |
// fetch, double check | |
query = @{ (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword, | |
(__bridge id)kSecAttrService: service, | |
(__bridge id)kSecAttrAccount: account }; | |
status = SecItemCopyMatching((__bridge CFDictionaryRef)query, (CFTypeRef *)&result); | |
status = SecItemCopyMatching((__bridge CFDictionaryRef)query, (CFTypeRef *)&result); | |
XCTAssert(status == errSecSuccess, @"fetch kSecAttrAccessible nil"); | |
query = @{ (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword, | |
(__bridge id)kSecAttrService: service, | |
(__bridge id)kSecAttrAccount: account, | |
(__bridge id)kSecAttrAccessible: (__bridge id)kSecAttrAccessibleWhenUnlocked }; | |
status = SecItemCopyMatching((__bridge CFDictionaryRef)query, (CFTypeRef *)&result); | |
status = SecItemCopyMatching((__bridge CFDictionaryRef)query, (CFTypeRef *)&result); | |
XCTAssert(status == errSecSuccess, @"fetch kSecAttrAccessible kSecAttrAccessibleWhenUnlocked"); | |
query = @{ (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword, | |
(__bridge id)kSecAttrService: service, | |
(__bridge id)kSecAttrAccount: account, | |
(__bridge id)kSecAttrAccessible: (__bridge id)kSecAttrAccessibleAfterFirstUnlock }; | |
status = SecItemCopyMatching((__bridge CFDictionaryRef)query, (CFTypeRef *)&result); | |
status = SecItemCopyMatching((__bridge CFDictionaryRef)query, (CFTypeRef *)&result); | |
XCTAssert(status == errSecItemNotFound, @"fetch kSecAttrAccessible kSecAttrAccessibleAfterFirstUnlock"); | |
query = @{ (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword, | |
(__bridge id)kSecAttrService: service, | |
(__bridge id)kSecAttrAccount: account, | |
(__bridge id)kSecAttrAccessible: (__bridge id)kSecAttrAccessibleAlways }; | |
status = SecItemCopyMatching((__bridge CFDictionaryRef)query, (CFTypeRef *)&result); | |
status = SecItemCopyMatching((__bridge CFDictionaryRef)query, (CFTypeRef *)&result); | |
XCTAssert(status == errSecItemNotFound, @"fetch kSecAttrAccessible kSecAttrAccessibleAlways"); | |
query = @{ (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword, | |
(__bridge id)kSecAttrService: service, | |
(__bridge id)kSecAttrAccount: account, | |
(__bridge id)kSecAttrAccessible: (__bridge id)kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly }; | |
status = SecItemCopyMatching((__bridge CFDictionaryRef)query, (CFTypeRef *)&result); | |
status = SecItemCopyMatching((__bridge CFDictionaryRef)query, (CFTypeRef *)&result); | |
XCTAssert(status == errSecItemNotFound, @"fetch kSecAttrAccessible kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly"); | |
query = @{ (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword, | |
(__bridge id)kSecAttrService: service, | |
(__bridge id)kSecAttrAccount: account, | |
(__bridge id)kSecAttrAccessible: (__bridge id)kSecAttrAccessibleWhenUnlockedThisDeviceOnly }; | |
status = SecItemCopyMatching((__bridge CFDictionaryRef)query, (CFTypeRef *)&result); | |
status = SecItemCopyMatching((__bridge CFDictionaryRef)query, (CFTypeRef *)&result); | |
XCTAssert(status == errSecItemNotFound, @"fetch kSecAttrAccessible kSecAttrAccessibleWhenUnlockedThisDeviceOnly"); | |
query = @{ (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword, | |
(__bridge id)kSecAttrService: service, | |
(__bridge id)kSecAttrAccount: account, | |
(__bridge id)kSecAttrAccessible: (__bridge id)kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly }; | |
status = SecItemCopyMatching((__bridge CFDictionaryRef)query, (CFTypeRef *)&result); | |
status = SecItemCopyMatching((__bridge CFDictionaryRef)query, (CFTypeRef *)&result); | |
XCTAssert(status == errSecItemNotFound, @"fetch kSecAttrAccessible kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly"); | |
query = @{ (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword, | |
(__bridge id)kSecAttrService: service, | |
(__bridge id)kSecAttrAccount: account, | |
(__bridge id)kSecAttrAccessible: (__bridge id)kSecAttrAccessibleAlwaysThisDeviceOnly }; | |
status = SecItemCopyMatching((__bridge CFDictionaryRef)query, (CFTypeRef *)&result); | |
status = SecItemCopyMatching((__bridge CFDictionaryRef)query, (CFTypeRef *)&result); | |
XCTAssert(status == errSecItemNotFound, @"fetch kSecAttrAccessible kSecAttrAccessibleAlwaysThisDeviceOnly"); | |
} | |
- (void)testKeychainAddAccessibleAlwaysSearchAllCases { | |
NSString *service = @"service"; | |
NSString *account = @"account"; | |
NSData *data = [@"always" dataUsingEncoding:NSUTF8StringEncoding]; | |
OSStatus status; | |
NSDictionary *query; | |
CFMutableDictionaryRef result = nil; | |
// add item with kSecAttrAccessible nil | |
query = @{ (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword, | |
(__bridge id)kSecAttrService: service, | |
(__bridge id)kSecAttrAccount: account, | |
(__bridge id)kSecValueData: data, | |
(__bridge id)kSecAttrAccessible: (__bridge id)kSecAttrAccessibleAlways }; | |
status = SecItemAdd((__bridge CFDictionaryRef)query, (CFTypeRef *)&result); | |
XCTAssert(status == errSecSuccess); | |
// fetch, double check | |
query = @{ (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword, | |
(__bridge id)kSecAttrService: service, | |
(__bridge id)kSecAttrAccount: account }; | |
status = SecItemCopyMatching((__bridge CFDictionaryRef)query, (CFTypeRef *)&result); | |
status = SecItemCopyMatching((__bridge CFDictionaryRef)query, (CFTypeRef *)&result); | |
XCTAssert(status == errSecSuccess, @"fetch kSecAttrAccessible nil"); | |
query = @{ (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword, | |
(__bridge id)kSecAttrService: service, | |
(__bridge id)kSecAttrAccount: account, | |
(__bridge id)kSecAttrAccessible: (__bridge id)kSecAttrAccessibleAlways }; | |
status = SecItemCopyMatching((__bridge CFDictionaryRef)query, (CFTypeRef *)&result); | |
status = SecItemCopyMatching((__bridge CFDictionaryRef)query, (CFTypeRef *)&result); | |
XCTAssert(status == errSecSuccess, @"fetch kSecAttrAccessible kSecAttrAccessibleAlways"); | |
query = @{ (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword, | |
(__bridge id)kSecAttrService: service, | |
(__bridge id)kSecAttrAccount: account, | |
(__bridge id)kSecAttrAccessible: (__bridge id)kSecAttrAccessibleWhenUnlocked }; | |
status = SecItemCopyMatching((__bridge CFDictionaryRef)query, (CFTypeRef *)&result); | |
status = SecItemCopyMatching((__bridge CFDictionaryRef)query, (CFTypeRef *)&result); | |
XCTAssert(status == errSecItemNotFound, @"fetch kSecAttrAccessible kSecAttrAccessibleWhenUnlocked"); | |
query = @{ (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword, | |
(__bridge id)kSecAttrService: service, | |
(__bridge id)kSecAttrAccount: account, | |
(__bridge id)kSecAttrAccessible: (__bridge id)kSecAttrAccessibleAfterFirstUnlock }; | |
status = SecItemCopyMatching((__bridge CFDictionaryRef)query, (CFTypeRef *)&result); | |
status = SecItemCopyMatching((__bridge CFDictionaryRef)query, (CFTypeRef *)&result); | |
XCTAssert(status == errSecItemNotFound, @"fetch kSecAttrAccessible kSecAttrAccessibleAfterFirstUnlock"); | |
query = @{ (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword, | |
(__bridge id)kSecAttrService: service, | |
(__bridge id)kSecAttrAccount: account, | |
(__bridge id)kSecAttrAccessible: (__bridge id)kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly }; | |
status = SecItemCopyMatching((__bridge CFDictionaryRef)query, (CFTypeRef *)&result); | |
status = SecItemCopyMatching((__bridge CFDictionaryRef)query, (CFTypeRef *)&result); | |
XCTAssert(status == errSecItemNotFound, @"fetch kSecAttrAccessible kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly"); | |
query = @{ (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword, | |
(__bridge id)kSecAttrService: service, | |
(__bridge id)kSecAttrAccount: account, | |
(__bridge id)kSecAttrAccessible: (__bridge id)kSecAttrAccessibleWhenUnlockedThisDeviceOnly }; | |
status = SecItemCopyMatching((__bridge CFDictionaryRef)query, (CFTypeRef *)&result); | |
status = SecItemCopyMatching((__bridge CFDictionaryRef)query, (CFTypeRef *)&result); | |
XCTAssert(status == errSecItemNotFound, @"fetch kSecAttrAccessible kSecAttrAccessibleWhenUnlockedThisDeviceOnly"); | |
query = @{ (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword, | |
(__bridge id)kSecAttrService: service, | |
(__bridge id)kSecAttrAccount: account, | |
(__bridge id)kSecAttrAccessible: (__bridge id)kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly }; | |
status = SecItemCopyMatching((__bridge CFDictionaryRef)query, (CFTypeRef *)&result); | |
status = SecItemCopyMatching((__bridge CFDictionaryRef)query, (CFTypeRef *)&result); | |
XCTAssert(status == errSecItemNotFound, @"fetch kSecAttrAccessible kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly"); | |
query = @{ (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword, | |
(__bridge id)kSecAttrService: service, | |
(__bridge id)kSecAttrAccount: account, | |
(__bridge id)kSecAttrAccessible: (__bridge id)kSecAttrAccessibleAlwaysThisDeviceOnly }; | |
status = SecItemCopyMatching((__bridge CFDictionaryRef)query, (CFTypeRef *)&result); | |
status = SecItemCopyMatching((__bridge CFDictionaryRef)query, (CFTypeRef *)&result); | |
XCTAssert(status == errSecItemNotFound, @"fetch kSecAttrAccessible kSecAttrAccessibleAlwaysThisDeviceOnly"); | |
} | |
- (void)testKeychainAdd { | |
// result: add results in errSecDuplicateItem when trying to add with different kSecAttrAccessible attribute | |
// NOTE: in case of background use kSecAttrAccessibleAfterFirstUnlock or kSecAttrAccessibleAlways in SecItemAdd | |
// otherwise default kSecAttrAccessible is kSecAttrAccessibleWhenUnlocked, which leads to -25308 errSecInteractionNotAllowed error | |
NSString *service = @"service"; | |
NSString *account = @"account"; | |
NSData *data = [@"data" dataUsingEncoding:NSUTF8StringEncoding]; | |
OSStatus status; | |
NSDictionary *query; | |
CFMutableDictionaryRef result = nil; | |
// add item | |
query = @{ (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword, | |
(__bridge id)kSecAttrService: service, | |
(__bridge id)kSecAttrAccount: account, | |
(__bridge id)kSecValueData: data, | |
(__bridge id)kSecAttrAccessible: (__bridge id)kSecAttrAccessibleWhenUnlocked }; | |
status = SecItemAdd((__bridge CFDictionaryRef)query, (CFTypeRef *)&result); | |
XCTAssert(status == errSecSuccess); | |
status = SecItemAdd((__bridge CFDictionaryRef)query, (CFTypeRef *)&result); | |
XCTAssert(status == errSecDuplicateItem); | |
// add item with different kSecAttrAccessible attribute | |
query = @{ (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword, | |
(__bridge id)kSecAttrService: service, | |
(__bridge id)kSecAttrAccount: account, | |
(__bridge id)kSecValueData: data, | |
(__bridge id)kSecAttrAccessible: (__bridge id)kSecAttrAccessibleAfterFirstUnlock }; | |
status = SecItemAdd((__bridge CFDictionaryRef)query, (CFTypeRef *)&result); | |
XCTAssert(status == errSecDuplicateItem); | |
} | |
- (void)testKeychainUpdate { | |
// result: can update item added with different kSecAttrAccessible attribute | |
// do not set kSecAttrAccessible attribute for SecItemCopyMatching | |
NSString *service = @"service"; | |
NSString *account = @"account"; | |
OSStatus status; | |
NSDictionary *query; | |
CFMutableDictionaryRef result = nil; | |
// add | |
query = @{ (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword, | |
(__bridge id)kSecAttrService: service, | |
(__bridge id)kSecAttrAccount: account, | |
(__bridge id)kSecValueData: [@"data" dataUsingEncoding:NSUTF8StringEncoding], | |
(__bridge id)kSecAttrAccessible: (__bridge id)kSecAttrAccessibleAlways }; | |
status = SecItemAdd((__bridge CFDictionaryRef)query, (CFTypeRef *)&result); | |
XCTAssert(status == errSecSuccess); | |
// find item with * kSecAttrAccessible | |
NSDictionary *searchQuery = @{ (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword, | |
(__bridge id)kSecAttrService: service, | |
(__bridge id)kSecAttrAccount: account }; | |
status = SecItemCopyMatching((__bridge CFDictionaryRef)searchQuery, (CFTypeRef *)&result); | |
XCTAssert(status == errSecSuccess); | |
// update found item with new data and kSecAttrAccessible attribute | |
NSDictionary *updateQuery = @{ (__bridge id)kSecValueData: [@"update" dataUsingEncoding:NSUTF8StringEncoding], | |
(__bridge id)kSecAttrAccessible:(__bridge id)kSecAttrAccessibleWhenUnlockedThisDeviceOnly }; | |
status = SecItemUpdate((__bridge CFDictionaryRef)(searchQuery), (__bridge CFDictionaryRef)(updateQuery)); | |
XCTAssert(status == errSecSuccess); | |
status = SecItemCopyMatching((__bridge CFDictionaryRef)searchQuery, (CFTypeRef *)&result); | |
XCTAssert(status == errSecSuccess); | |
// update found item with new kSecAttrAccessible attribute | |
updateQuery = @{ (__bridge id)kSecValueData: [@"update" dataUsingEncoding:NSUTF8StringEncoding], | |
(__bridge id)kSecAttrAccessible:(__bridge id)kSecAttrAccessibleAlways }; | |
status = SecItemUpdate((__bridge CFDictionaryRef)(searchQuery), (__bridge CFDictionaryRef)(updateQuery)); | |
XCTAssert(status == errSecSuccess); | |
status = SecItemCopyMatching((__bridge CFDictionaryRef)searchQuery, (CFTypeRef *)&result); | |
XCTAssert(status == errSecSuccess); | |
} | |
- (void)testKeychainCopy { | |
// result: can not fetch items added with different kSecAttrAccessible attribute | |
// same as in search, do not set kSecAttrAccessible for SecItemCopyMatching | |
NSString *service = @"service"; | |
NSString *account = @"account"; | |
NSString *inString = @"data"; | |
OSStatus status; | |
NSDictionary *query; | |
CFTypeRef result; | |
// add | |
query = @{ (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword, | |
(__bridge id)kSecAttrService: service, | |
(__bridge id)kSecAttrAccount: account, | |
(__bridge id)kSecValueData: [inString dataUsingEncoding:NSUTF8StringEncoding], | |
(__bridge id)kSecAttrAccessible: (__bridge id)kSecAttrAccessibleAlways }; | |
status = SecItemAdd((__bridge CFDictionaryRef)query, NULL); | |
XCTAssert(status == errSecSuccess); | |
// search with different kSecAttrAccessible | |
query = @{ (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword, | |
(__bridge id)kSecAttrService: service, | |
(__bridge id)kSecAttrAccount: account, | |
(__bridge id)kSecReturnData: @YES, | |
(__bridge id)kSecMatchLimit: (__bridge id)kSecMatchLimitOne, | |
(__bridge id)kSecAttrAccessible: (__bridge id)kSecAttrAccessibleAfterFirstUnlock }; | |
status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &result); | |
XCTAssert(status == errSecItemNotFound); | |
// search with * kSecAttrAccessible | |
query = @{ (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword, | |
(__bridge id)kSecAttrService: service, | |
(__bridge id)kSecAttrAccount: account, | |
(__bridge id)kSecReturnData: @YES, | |
(__bridge id)kSecMatchLimit: (__bridge id)kSecMatchLimitOne }; | |
status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &result); | |
XCTAssert(status == errSecSuccess); | |
NSData *passwordData = (__bridge_transfer NSData *)result; | |
NSString *outString = [[NSString alloc] initWithData:passwordData encoding:NSUTF8StringEncoding]; | |
XCTAssertEqualObjects(outString, inString); | |
} | |
- (void)testKeychainDelete { | |
// result: can not delete items added with different kSecAttrAccessible attribute | |
// dont use kSecAttrAccessible when deleting | |
NSString *service = @"service"; | |
NSString *account = @"account"; | |
OSStatus status; | |
NSDictionary *query; | |
// add | |
query = @{ (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword, | |
(__bridge id)kSecAttrService: service, | |
(__bridge id)kSecAttrAccount: account, | |
(__bridge id)kSecValueData: [@"data" dataUsingEncoding:NSUTF8StringEncoding], }; | |
//(__bridge id)kSecAttrAccessible: (__bridge id)kSecAttrAccessibleAfterFirstUnlock }; | |
status = SecItemAdd((__bridge CFDictionaryRef)query, NULL); | |
XCTAssert(status == errSecSuccess); | |
// delete with specific kSecAttrAccessible | |
query = @{ (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword, | |
(__bridge id)kSecAttrService: service, | |
(__bridge id)kSecAttrAccount: account, | |
(__bridge id)kSecAttrAccessible: (__bridge id)kSecAttrAccessibleAlways }; | |
status = SecItemDelete((__bridge CFDictionaryRef)query); | |
XCTAssert(status == errSecItemNotFound); | |
// delete with * kSecAttrAccessible | |
query = @{ (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword, | |
(__bridge id)kSecAttrService: service, | |
(__bridge id)kSecAttrAccount: account }; | |
status = SecItemDelete((__bridge CFDictionaryRef)query); | |
XCTAssert(status == errSecSuccess); | |
} | |
@end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment