LKKeychainAssistant.m

This file can be downloaded as part of milnlicence.tbz.

//
//  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