//
// 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
LCLicenceCore.m
This file can be downloaded as part of milnlicence.tbz.