This repository has been archived on 2022-03-12. You can view files and clone it, but cannot push or open issues or pull requests.
2021-04-02 02:24:13 +03:00

218 lines
9.3 KiB
Objective-C

// Copyright 2019-present 650 Industries. All rights reserved.
#import <CommonCrypto/CommonDigest.h>
#import <EXUpdates/EXUpdatesCrypto.h>
#import <EXUpdates/EXUpdatesFileDownloader.h>
NS_ASSUME_NONNULL_BEGIN
static NSString * const EXUpdatesCryptoPublicKeyUrl = @"https://exp.host/--/manifest-public-key";
static NSString * const EXUpdatesCryptoPublicKeyTag = @"exp.host.publickey";
static NSString * const EXUpdatesCryptoPublicKeyFilename = @"manifestPublicKey.pem";
@implementation EXUpdatesCrypto
+ (void)verifySignatureWithData:(NSString *)data
signature:(NSString *)signature
config:(EXUpdatesConfig *)config
cacheDirectory:(NSURL *)cacheDirectory
successBlock:(EXUpdatesVerifySignatureSuccessBlock)successBlock
errorBlock:(EXUpdatesVerifySignatureErrorBlock)errorBlock
{
[self fetchAndVerifySignatureWithData:data
signature:signature
config:config
cacheDirectory:cacheDirectory
useCache:YES
successBlock:successBlock
errorBlock:errorBlock];
}
+ (void)fetchAndVerifySignatureWithData:(NSString *)data
signature:(NSString *)signature
config:(EXUpdatesConfig *)config
cacheDirectory:(NSURL *)cacheDirectory
useCache:(BOOL)useCache
successBlock:(EXUpdatesVerifySignatureSuccessBlock)successBlock
errorBlock:(EXUpdatesVerifySignatureErrorBlock)errorBlock
{
if (!data || !signature) {
errorBlock([NSError errorWithDomain:@"EXUpdatesCrypto" code:1001 userInfo:@{ NSLocalizedDescriptionKey: @"Cannot verify the manifest because it is empty or has no signature." }]);
return;
}
NSURL *cachedPublicKeyUrl = [cacheDirectory URLByAppendingPathComponent:EXUpdatesCryptoPublicKeyFilename];
if (useCache) {
NSData *publicKeyData = [NSData dataWithContentsOfFile:[cachedPublicKeyUrl absoluteString]];
[[self class] verifyWithPublicKey:publicKeyData signature:signature signedString:data callback:^(BOOL isValid) {
if (isValid) {
successBlock(isValid);
} else {
[[self class] fetchAndVerifySignatureWithData:data
signature:signature
config:config
cacheDirectory:cacheDirectory
useCache:NO
successBlock:successBlock
errorBlock:errorBlock];
}
}];
} else {
NSURLSessionConfiguration *configuration = NSURLSessionConfiguration.defaultSessionConfiguration;
configuration.requestCachePolicy = NSURLRequestReloadIgnoringCacheData;
EXUpdatesFileDownloader *fileDownloader = [[EXUpdatesFileDownloader alloc] initWithUpdatesConfig:config URLSessionConfiguration:configuration];
[fileDownloader downloadFileFromURL:[NSURL URLWithString:EXUpdatesCryptoPublicKeyUrl]
toPath:[cachedPublicKeyUrl path]
successBlock:^(NSData *publicKeyData, NSURLResponse *response) {
[[self class] verifyWithPublicKey:publicKeyData signature:signature signedString:data callback:successBlock];
}
errorBlock:^(NSError *error, NSURLResponse *response) {
errorBlock(error);
}
];
}
}
+ (void)verifyWithPublicKey:(NSData *)publicKeyData
signature:(NSString *)signature
signedString:(NSString *)signedString
callback:(EXUpdatesVerifySignatureSuccessBlock)callback
{
if (!publicKeyData) {
callback(NO);
} else {
dispatch_async(dispatch_get_main_queue(), ^{
SecKeyRef publicKey = [self keyRefFromPEMData:publicKeyData];
NSData *signatureData = [[NSData alloc] initWithBase64EncodedString:signature options:0];
NSData *signedData = [signedString dataUsingEncoding:NSUTF8StringEncoding];
BOOL isValid = NO;
if (publicKey) {
isValid = [self verifyRSASHA256SignedData:signedData signatureData:signatureData publicKey:publicKey];
CFRelease(publicKey);
}
callback(isValid);
});
}
}
/**
* Returns a CFRef to a SecKey given the raw pem data.
* The CFRef should be CFReleased when you're finished.
*
* Here is the Apple doc for this black hole:
* https://developer.apple.com/library/prerelease/content/documentation/Security/Conceptual/CertKeyTrustProgGuide/iPhone_Tasks/iPhone_Tasks.html#//apple_ref/doc/uid/TP40001358-CH208-SW13
*/
+ (nullable SecKeyRef)keyRefFromPEMData:(NSData *)pemData
{
NSString *pemString = [[NSString alloc] initWithData:pemData encoding:NSUTF8StringEncoding];
NSString *key = [NSString string];
NSArray<NSString *> *keyLines = [pemString componentsSeparatedByString:@"\n"];
BOOL foundKey = NO;
for (NSString *line in keyLines) {
if ([line isEqualToString:@"-----BEGIN PUBLIC KEY-----"]) {
foundKey = YES;
} else if ([line isEqualToString:@"-----END PUBLIC KEY-----"]) {
foundKey = NO;
} else if (foundKey) {
key = [key stringByAppendingString:line];
}
}
if (key.length == 0) {
return nil;
}
NSData *keyData = [[NSData alloc] initWithBase64EncodedString:key options:0];
if (keyData == nil) {
return nil;
}
NSData *tag = [NSData dataWithBytes:[EXUpdatesCryptoPublicKeyTag UTF8String] length:[EXUpdatesCryptoPublicKeyTag length]];
// Delete any old lingering key with the same tag.
NSDictionary *deleteParams = @{
(__bridge id)kSecClass: (__bridge id)kSecClassKey,
(__bridge id)kSecAttrKeyType: (__bridge id)kSecAttrKeyTypeRSA,
(__bridge id)kSecAttrApplicationTag: tag,
};
OSStatus secStatus = SecItemDelete((CFDictionaryRef)deleteParams);
SecKeyRef savedKeyRef = nil;
// Add key to system keychain.
NSDictionary *saveParams = @{
(__bridge id)kSecClass: (__bridge id) kSecClassKey,
(__bridge id)kSecAttrKeyType: (__bridge id) kSecAttrKeyTypeRSA,
(__bridge id)kSecAttrApplicationTag: tag,
(__bridge id)kSecAttrKeyClass: (__bridge id) kSecAttrKeyClassPublic,
(__bridge id)kSecReturnPersistentRef: (__bridge id)kCFBooleanTrue,
(__bridge id)kSecValueData: keyData,
(__bridge id)kSecAttrKeySizeInBits: [NSNumber numberWithUnsignedInteger:keyData.length],
(__bridge id)kSecAttrEffectiveKeySize: [NSNumber numberWithUnsignedInteger:keyData.length],
(__bridge id)kSecAttrCanDerive: (__bridge id) kCFBooleanFalse,
(__bridge id)kSecAttrCanEncrypt: (__bridge id) kCFBooleanTrue,
(__bridge id)kSecAttrCanDecrypt: (__bridge id) kCFBooleanFalse,
(__bridge id)kSecAttrCanVerify: (__bridge id) kCFBooleanTrue,
(__bridge id)kSecAttrCanSign: (__bridge id) kCFBooleanFalse,
(__bridge id)kSecAttrCanWrap: (__bridge id) kCFBooleanTrue,
(__bridge id)kSecAttrCanUnwrap: (__bridge id) kCFBooleanFalse,
};
secStatus = SecItemAdd((CFDictionaryRef)saveParams, (CFTypeRef *)&savedKeyRef);
if (savedKeyRef != nil) {
CFRelease(savedKeyRef);
}
if (secStatus != noErr && secStatus != errSecDuplicateItem) {
return nil;
}
// Fetch the SecKeyRef version of the key.
// note that kSecAttrKeyClass: kSecAttrKeyClassPublic doesn't seem to be required here.
// also: this doesn't work on iOS < 10.0
SecKeyRef keyRef = nil;
NSDictionary *queryParams = @{
(__bridge id)kSecClass: (__bridge id) kSecClassKey,
(__bridge id)kSecAttrKeyType: (__bridge id) kSecAttrKeyTypeRSA,
(__bridge id)kSecAttrApplicationTag: tag,
(__bridge id)kSecReturnRef: (__bridge id) kCFBooleanTrue,
};
secStatus = SecItemCopyMatching((CFDictionaryRef)queryParams, (CFTypeRef *)&keyRef);
if (secStatus != noErr) {
return nil;
}
return keyRef;
}
+ (BOOL)verifyRSASHA256SignedData:(NSData *)signedData signatureData:(NSData *)signatureData publicKey:(nullable SecKeyRef)publicKey
{
if (!publicKey) {
return NO;
}
uint8_t hashBytes[CC_SHA256_DIGEST_LENGTH];
if (!CC_SHA256([signedData bytes], (CC_LONG)[signedData length], hashBytes)) {
return NO;
}
OSStatus status = SecKeyRawVerify(publicKey,
kSecPaddingPKCS1SHA256,
hashBytes,
CC_SHA256_DIGEST_LENGTH,
[signatureData bytes],
[signatureData length]);
return status == errSecSuccess;
}
@end
NS_ASSUME_NONNULL_END