//
// LKKeychainAssistant.m
// LicenceKit - https://indie.miln.eu
//
// Copyright © Graham Miln. All rights reserved. https://miln.eu
//
// This package is subject to the terms of the Artistic License 2.0.
// If a copy of the Artistic-2.0 was not distributed with this file, you can
// obtain one at https://indie.miln.eu/licence
#import "LKKeychainAssistant.h"
// Info.plist keys
NSString* LKLicenceKitInfoPListKeySubjectPrefixes = @"LKSubjectPrefixes";
@interface LKKeychainAssistant () <LCLicenceObserverProtocol>
@property(strong) LCLicenceCore* core;
@property(strong) LCLicenceObserver* observer;
- (NSArray<NSString*>*)derivedSubjectPrefixes;
- (void)addToKeychain:(LCLicence*)aLicence;
- (CFArrayRef)copyLicencesFromKeychain;
@end
@implementation LKKeychainAssistant
- (instancetype)init {
return [self initWithCore:LCLicenceCore.sharedCore];
}
- (instancetype)initWithCore:(LCLicenceCore*)aCore {
NSParameterAssert(aCore != nil);
if ((self = [super init])) {
self.core = aCore;
self.observer = [LCLicenceObserver observerWithCore:self.core delegate:self];
}
return self;
}
- (void)licenceObserver:(LCLicenceObserver *)anObserver didChange:(LCLicence *)aLicence {
// Blunt implementation but enough for now
for(LCLicence* licence in self.core.licences) {
[self addToKeychain:licence];
}
}
- (void)addLicences {
CFArrayRef certificates = [self copyLicencesFromKeychain];
if (certificates != nil) {
[(__bridge NSArray*)certificates enumerateObjectsUsingBlock:^(id inCert, NSUInteger __unused inIndex, BOOL* __unused inShouldStop) {
[self.core addLicenceCertificate:(__bridge SecCertificateRef)inCert];
}];
CFRelease(certificates);
}
}
- (NSError*)removeLicencesFromKeychain {
CFArrayRef certificates = [self copyLicencesFromKeychain];
if (certificates != nil) {
for(id cert in (__bridge NSArray*)certificates) {
NSDictionary<NSString*,id>* deleteQuery = @{(NSString*)kSecValueRef: cert, (NSString*)kSecClass: (id)kSecClassCertificate};
OSStatus validDelete = SecItemDelete((__bridge CFDictionaryRef)deleteQuery);
if (validDelete != errSecSuccess) {
CFRelease(certificates);
return [NSError errorWithDomain:NSOSStatusErrorDomain code:validDelete userInfo:nil];
}
}
CFRelease(certificates);
}
return nil;
}
#pragma mark -
- (NSArray<NSString*>*)derivedSubjectPrefixes {
// Use owner provided list of certificate subject prefixes or...
if (self.subjectPrefixes.count > 0) {
return self.subjectPrefixes;
} else {
// ...use prefixes embeded in main bundle's Info.plist
NSObject* potentialPrefixes = [[NSBundle mainBundle] objectForInfoDictionaryKey:LKLicenceKitInfoPListKeySubjectPrefixes];
if ([potentialPrefixes isKindOfClass:NSArray.class]) {
NSMutableArray<NSString*>* prefixes = [NSMutableArray new];
for (NSString* potentialPrefix in (NSArray*)potentialPrefixes) {
if ([potentialPrefix isKindOfClass:NSString.class]) {
[prefixes addObject:potentialPrefix];
} else {
NSLog(@"[ERROR] Invalid %@ item: %@", LKLicenceKitInfoPListKeySubjectPrefixes, potentialPrefix);
}
}
return prefixes;
} else {
NSLog(@"[ERROR] Missing or invalid %@ Info.plist entry: %@", LKLicenceKitInfoPListKeySubjectPrefixes, potentialPrefixes);
}
}
return nil;
}
- (void)addToKeychain:(LCLicence*)aLicence {
// Trust is required before the leaf certificate is available
if (aLicence.trust != nil) {
SecCertificateRef leafCert = SecTrustGetCertificateAtIndex(aLicence.trust, 0);
if (leafCert != nil) {
// Import certificate into the Keychain
NSDictionary<NSString*,id>* addQuery = @{(NSString*)kSecValueRef: (__bridge id)leafCert, (NSString*)kSecClass: (id)kSecClassCertificate};
OSStatus validAdd = SecItemAdd((__bridge CFDictionaryRef)addQuery, NULL);
if (validAdd != errSecSuccess && validAdd != errSecDuplicateItem) {
NSLog(@"[WARNING] Unable to add certificate to Keychain: %d", (int)validAdd);
}
}
}
}
- (CFArrayRef)copyLicencesFromKeychain {
CFMutableArrayRef certificates = CFArrayCreateMutable(kCFAllocatorDefault, 0, &kCFTypeArrayCallBacks);
if (certificates != nil) {
NSArray<NSString*>* prefixes = [self derivedSubjectPrefixes];
for (NSString* subjectPrefix in prefixes) {
NSDictionary<NSString*,id>* getQuery = @{(NSString*)kSecClass: (id)kSecClassCertificate,
(NSString*)kSecMatchSubjectStartsWith: subjectPrefix,
(NSString*)kSecReturnRef: @YES,
(NSString*)kSecMatchLimit: (id)kSecMatchLimitAll,
};
CFArrayRef results = nil;
OSStatus validCopy = SecItemCopyMatching((CFDictionaryRef)getQuery, (CFTypeRef*)&results);
if (validCopy == errSecSuccess) {
// Paranoid check for array and certificate contents
if (CFGetTypeID(results) == CFArrayGetTypeID()) {
[(__bridge NSArray*)results enumerateObjectsUsingBlock:^(id inPotentialCert, NSUInteger __unused inIndex, BOOL* __unused inShouldStop) {
if (CFGetTypeID((__bridge CFTypeRef)(inPotentialCert)) == SecCertificateGetTypeID()) {
CFArrayAppendValue(certificates, (__bridge SecCertificateRef)inPotentialCert);
}
}];
} else {
NSLog(@"[ERROR] Prefix '%@' returned unexpected certificate type: %@", subjectPrefix, results);
}
} else if (validCopy != errSecItemNotFound) { // Log all but "not found" states
NSLog(@"[ERROR] Keychain query with prefix '%@' returned error: %d", subjectPrefix, validCopy);
}
if (results != NULL) {
CFRelease(results);
}
}
} // else unable to allocate array
return certificates;
}
@end
LKKeychainAssistant.m
This file can be downloaded as part of milnlicence.tbz.