LCLicenceCore.m

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

//
//  LCLicenceCore.m
//  LicenceCore - 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 "LCLicenceCore.h"

// Property keys
NSString* LCLicenceCoreKeyLicences = @"licences";
NSString* LCLicenceCoreKeyIsLicensed = @"isLicensed";

// KVO Content
static NSString* LCLicenceCoreKVOLicencesContext = @"LCLicenceCoreKVOLicencesContext";
static NSString* LCLicenceCoreKVOPendingContext = @"LCLicenceCoreKVOPendingContext";

@interface LCLicenceCore () {
	CFArrayRef _anchorCache;
}
@property(strong,readwrite) NSArray<LCLicence*>* licences;
@property(assign,readwrite) BOOL isLicensed;
@property(assign,readwrite) BOOL exceptionalTrustPending;
@property(strong) NSOperationQueue* evaluationQueue;

- (void)evaluate;
@end

@implementation LCLicenceCore

+ (instancetype)sharedCore {
	static LCLicenceCore* shared = nil;
	static dispatch_once_t onceToken;
	dispatch_once(&onceToken, ^{
		shared = [LCLicenceCore new];
	});
	return shared;
}

- (instancetype)init {
	if ((self = [super init])) {
		self.licences = [NSArray new];
		[self addObserver:self forKeyPath:LCLicenceCoreKeyLicences options:(NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld) context:(__bridge void * _Nullable)(LCLicenceCoreKVOLicencesContext)];
		
		self.evaluationQueue = [NSOperationQueue new];
		self.evaluationQueue.name = NSStringFromClass(self.class);
		self.evaluationQueue.maxConcurrentOperationCount = 1; // serial evaluation
		self.evaluationQueue.qualityOfService = NSQualityOfServiceUserInitiated; // affects application functionality
		self.evaluationQueue.suspended = NO;
	}
	return self;
}

- (void)dealloc {
	// Inform licences they are being removed
	[self removeAll];
	[self removeObserver:self forKeyPath:LCLicenceCoreKeyLicences context:(__bridge void * _Nullable)(LCLicenceCoreKVOLicencesContext)];
	
	if (_anchorCache != nil) {
		CFRelease(_anchorCache);
		_anchorCache = nil;
	}
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
	if (context == (__bridge void * _Nullable)(LCLicenceCoreKVOLicencesContext)) {
		id old = change[NSKeyValueChangeOldKey];
		if ([old isKindOfClass:NSArray.class]) {
			NSArray<LCLicence*>* oldLicences = old;
			[oldLicences enumerateObjectsUsingBlock:^(LCLicence* inLicence, NSUInteger __unused inIndex,BOOL* __unused outShouldStop) {
				[inLicence removeObserver:self forKeyPath:LCLicenceKeyIsPending context:(__bridge void * _Nullable)(LCLicenceCoreKVOPendingContext)];
			}];
		}
		
		id new = change[NSKeyValueChangeNewKey];
		if ([new isKindOfClass:NSArray.class]) {
			NSArray<LCLicence*>* newLicences = new;
			[newLicences enumerateObjectsUsingBlock:^(LCLicence* inLicence, NSUInteger __unused inIndex,BOOL* __unused outShouldStop) {
				[inLicence addObserver:self forKeyPath:LCLicenceKeyIsPending options:(NSKeyValueObservingOptionInitial|NSKeyValueObservingOptionNew) context:(__bridge void * _Nullable)(LCLicenceCoreKVOPendingContext)];
			}];
		}
		
		[self evaluate];
	} else if (context == (__bridge void * _Nullable)(LCLicenceCoreKVOPendingContext)) {
		[self evaluate];
	} else {
		[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
	}
}

- (NSError*)setTrustedAnchors:(NSArray<NSData*>*)somePEMs {
	NSError* error = nil;
	CFMutableArrayRef anchors = CFArrayCreateMutable(kCFAllocatorDefault, 0, &kCFTypeArrayCallBacks);
	if (anchors != NULL) {
		// Pass ownership to self
		self->_anchorCache = anchors;
		
		for (NSData* pemData in somePEMs) {
			// Decode PEM; can result in multiple certificates per PEM block
			SecExternalFormat inputFormat = kSecFormatPEMSequence;
			SecExternalItemType itemType = kSecItemTypeCertificate;
			CFArrayRef importedItems = nil;
			OSStatus validImport = SecItemImport((CFDataRef)pemData,nil, &inputFormat, &itemType, 0, nil, nil, &importedItems);
			if (validImport != errSecSuccess) {
				return [[NSError alloc] initWithDomain:NSOSStatusErrorDomain code:validImport userInfo:nil];
			}
			if (importedItems != nil) {
				CFArrayAppendArray(anchors, importedItems, CFRangeMake(0, CFArrayGetCount(importedItems)));
			}
		}
	}
	return error;
}

- (void)setTrustPending:(BOOL)inTrust {
	if (self.exceptionalTrustPending != inTrust) {
		self.exceptionalTrustPending = inTrust;
		[self evaluate];
	}
}

#pragma mark -

- (void)addLicenceCertificate:(SecCertificateRef)aCertificate {
	[self addLicence:[[LCLicence alloc] initWithCertificate:aCertificate anchors:self->_anchorCache queue:self.evaluationQueue]];
}

- (void)addLicenceWithData:(NSData*)someData format:(SecExternalFormat)inFormat {
	[self addLicence:[[LCLicence alloc] initWithData:someData format:inFormat anchors:self->_anchorCache queue:self.evaluationQueue]];
}

- (void)addLicence:(LCLicence*)aLicence {
	// Avoid duplicate certificates
	NSUInteger i = [self.licences indexOfObjectWithOptions:NSEnumerationConcurrent passingTest:^BOOL(LCLicence* inLicence, NSUInteger __unused inIndex, BOOL* __unused outShouldStop) {
		return [inLicence isEqualToLicence:aLicence];
	}];
	if (i == NSNotFound) {
		self.licences = [self.licences arrayByAddingObject:aLicence];
		
		// TODO: use date range to determine if evaluate worthwhile
		[aLicence evaluate];
	}
}

- (void)removeAll {
	[self.licences enumerateObjectsUsingBlock:^(LCLicence* inLicence, NSUInteger __unused inIndex, BOOL* __unused outShouldStop) {
		[inLicence cancelEvaluation];
	}];
	self.licences = [NSArray new];
}

#pragma mark -

- (void)evaluate {
	// Re-evaluate trust
	__block BOOL trusted = NO;
	__block BOOL pending = NO;
	[self.licences enumerateObjectsUsingBlock:^(LCLicence* inLicence, NSUInteger __unused inIndex, BOOL* __unused outShouldStop) {
		trusted = trusted || inLicence.isTrusted;
		pending = pending || inLicence.isPending;
	}];
	
	BOOL result = NO;
	if (trusted) {
		// Simplest case, one licence has been evaluated and is trusted
		result = YES;
	} else if (self.exceptionalTrustPending && pending) {
		// Permit trust when at least one licence is still pending
		result = YES;
	}
	
	// Reset trustPending if nothing is pending or nothing to evaluate
	if (!pending || self.licences.count == 0) {
		self.exceptionalTrustPending = NO;
	}
	
	self.isLicensed = result;
}

@end