Files
react-native-code-push/ios/CodePush/CodePushUpdateUtils.m
Ruslan Bikkinin e1cdd90e4e Add double hash checking support for code signing (#1005)
Add double hash checking support for code signing for both android and ios SDK
2017-09-18 13:37:44 +03:00

377 lines
15 KiB
Objective-C

#import "CodePush.h"
#include <CommonCrypto/CommonDigest.h>
#import "JWT.h"
@implementation CodePushUpdateUtils
NSString * const AssetsFolderName = @"assets";
NSString * const BinaryHashKey = @"CodePushBinaryHash";
NSString * const ManifestFolderPrefix = @"CodePush";
NSString * const BundleJWTFile = @".codepushrelease";
/*
Ignore list for hashing
*/
NSString * const IgnoreMacOSX= @"__MACOSX/";
NSString * const IgnoreDSStore = @".DS_Store";
NSString * const IgnoreCodePushMetadata = @".codepushrelease";
+ (BOOL)isHashIgnoredFor:(NSString *) relativePath
{
return [relativePath hasPrefix:IgnoreMacOSX]
|| [relativePath isEqualToString:IgnoreDSStore]
|| [relativePath hasSuffix:[NSString stringWithFormat:@"/%@", IgnoreDSStore]]
|| [relativePath isEqualToString:IgnoreCodePushMetadata]
|| [relativePath hasSuffix:[NSString stringWithFormat:@"/%@", IgnoreCodePushMetadata]];
}
+ (BOOL)addContentsOfFolderToManifest:(NSString *)folderPath
pathPrefix:(NSString *)pathPrefix
manifest:(NSMutableArray *)manifest
error:(NSError **)error
{
NSArray *folderFiles = [[NSFileManager defaultManager]
contentsOfDirectoryAtPath:folderPath
error:error];
if (!folderFiles) {
return NO;
}
for (NSString *fileName in folderFiles) {
NSString *fullFilePath = [folderPath stringByAppendingPathComponent:fileName];
NSString *relativePath = [pathPrefix stringByAppendingPathComponent:fileName];
if([self isHashIgnoredFor:relativePath]){
continue;
}
BOOL isDir = NO;
if ([[NSFileManager defaultManager] fileExistsAtPath:fullFilePath
isDirectory:&isDir] && isDir) {
BOOL result = [self addContentsOfFolderToManifest:fullFilePath
pathPrefix:relativePath
manifest:manifest
error:error];
if (!result) {
return NO;
}
} else {
NSData *fileContents = [NSData dataWithContentsOfFile:fullFilePath];
NSString *fileContentsHash = [self computeHashForData:fileContents];
[manifest addObject:[[relativePath stringByAppendingString:@":"] stringByAppendingString:fileContentsHash]];
}
}
return YES;
}
+ (void)addFileToManifest:(NSURL *)fileURL
manifest:(NSMutableArray *)manifest
{
if ([[NSFileManager defaultManager] fileExistsAtPath:[fileURL path]]) {
NSData *fileContents = [NSData dataWithContentsOfURL:fileURL];
NSString *fileContentsHash = [self computeHashForData:fileContents];
[manifest addObject:[NSString stringWithFormat:@"%@/%@:%@", [self manifestFolderPrefix], [fileURL lastPathComponent], fileContentsHash]];
}
}
+ (NSString *)computeFinalHashFromManifest:(NSMutableArray *)manifest
error:(NSError **)error
{
//sort manifest strings to make sure, that they are completely equal with manifest strings has been generated in cli!
NSArray *sortedManifest = [manifest sortedArrayUsingSelector:@selector(compare:)];
NSData *manifestData = [NSJSONSerialization dataWithJSONObject:sortedManifest
options:kNilOptions
error:error];
if (!manifestData) {
return nil;
}
NSString *manifestString = [[NSString alloc] initWithData:manifestData
encoding:NSUTF8StringEncoding];
// The JSON serialization turns path separators into "\/", e.g. "CodePush\/assets\/image.png"
manifestString = [manifestString stringByReplacingOccurrencesOfString:@"\\/"
withString:@"/"];
return [self computeHashForData:[NSData dataWithBytes:manifestString.UTF8String length:manifestString.length]];
}
+ (NSString *)computeHashForData:(NSData *)inputData
{
uint8_t digest[CC_SHA256_DIGEST_LENGTH];
CC_SHA256(inputData.bytes, (CC_LONG)inputData.length, digest);
NSMutableString* inputHash = [NSMutableString stringWithCapacity:CC_SHA256_DIGEST_LENGTH * 2];
for (int i = 0; i < CC_SHA256_DIGEST_LENGTH; i++) {
[inputHash appendFormat:@"%02x", digest[i]];
}
return inputHash;
}
+ (BOOL)copyEntriesInFolder:(NSString *)sourceFolder
destFolder:(NSString *)destFolder
error:(NSError **)error
{
NSArray *files = [[NSFileManager defaultManager]
contentsOfDirectoryAtPath:sourceFolder
error:error];
if (!files) {
return NO;
}
for (NSString *fileName in files) {
NSString * fullFilePath = [sourceFolder stringByAppendingPathComponent:fileName];
BOOL isDir = NO;
if ([[NSFileManager defaultManager] fileExistsAtPath:fullFilePath
isDirectory:&isDir] && isDir) {
NSString *nestedDestFolder = [destFolder stringByAppendingPathComponent:fileName];
BOOL result = [self copyEntriesInFolder:fullFilePath
destFolder:nestedDestFolder
error:error];
if (!result) {
return NO;
}
} else {
NSString *destFileName = [destFolder stringByAppendingPathComponent:fileName];
if ([[NSFileManager defaultManager] fileExistsAtPath:destFileName]) {
BOOL result = [[NSFileManager defaultManager] removeItemAtPath:destFileName error:error];
if (!result) {
return NO;
}
}
if (![[NSFileManager defaultManager] fileExistsAtPath:destFolder]) {
BOOL result = [[NSFileManager defaultManager] createDirectoryAtPath:destFolder
withIntermediateDirectories:YES
attributes:nil
error:error];
if (!result) {
return NO;
}
}
BOOL result = [[NSFileManager defaultManager] copyItemAtPath:fullFilePath toPath:destFileName error:error];
if (!result) {
return NO;
}
}
}
return YES;
}
+ (NSString *)findMainBundleInFolder:(NSString *)folderPath
expectedFileName:(NSString *)expectedFileName
error:(NSError **)error
{
NSArray* folderFiles = [[NSFileManager defaultManager]
contentsOfDirectoryAtPath:folderPath
error:error];
if (!folderFiles) {
return nil;
}
for (NSString *fileName in folderFiles) {
NSString *fullFilePath = [folderPath stringByAppendingPathComponent:fileName];
BOOL isDir = NO;
if ([[NSFileManager defaultManager] fileExistsAtPath:fullFilePath
isDirectory:&isDir] && isDir) {
NSString *mainBundlePathInFolder = [self findMainBundleInFolder:fullFilePath
expectedFileName:expectedFileName
error:error];
if (mainBundlePathInFolder) {
return [fileName stringByAppendingPathComponent:mainBundlePathInFolder];
}
} else if ([fileName isEqualToString:expectedFileName]) {
return fileName;
}
}
return nil;
}
+ (NSString *)assetsFolderName
{
return AssetsFolderName;
}
+ (NSString *)getHashForBinaryContents:(NSURL *)binaryBundleUrl
error:(NSError **)error
{
// Get the cached hash from user preferences if it exists.
NSString *binaryModifiedDate = [self modifiedDateStringOfFileAtURL:binaryBundleUrl];
NSUserDefaults *preferences = [NSUserDefaults standardUserDefaults];
NSMutableDictionary *binaryHashDictionary = [preferences objectForKey:BinaryHashKey];
NSString *binaryHash = nil;
if (binaryHashDictionary != nil) {
binaryHash = [binaryHashDictionary objectForKey:binaryModifiedDate];
if (binaryHash == nil) {
[preferences removeObjectForKey:BinaryHashKey];
[preferences synchronize];
} else {
return binaryHash;
}
}
binaryHashDictionary = [NSMutableDictionary dictionary];
NSMutableArray *manifest = [NSMutableArray array];
// If the app is using assets, then add
// them to the generated content manifest.
NSString *assetsPath = [CodePush bundleAssetsPath];
if ([[NSFileManager defaultManager] fileExistsAtPath:assetsPath]) {
BOOL result = [self addContentsOfFolderToManifest:assetsPath
pathPrefix:[NSString stringWithFormat:@"%@/%@", [self manifestFolderPrefix], @"assets"]
manifest:manifest
error:error];
if (!result) {
return nil;
}
}
[self addFileToManifest:binaryBundleUrl manifest:manifest];
[self addFileToManifest:[binaryBundleUrl URLByAppendingPathExtension:@"meta"] manifest:manifest];
binaryHash = [self computeFinalHashFromManifest:manifest error:error];
// Cache the hash in user preferences. This assumes that the modified date for the
// JS bundle changes every time a new bundle is generated by the packager.
[binaryHashDictionary setObject:binaryHash forKey:binaryModifiedDate];
[preferences setObject:binaryHashDictionary forKey:BinaryHashKey];
[preferences synchronize];
return binaryHash;
}
+ (NSString *)manifestFolderPrefix
{
return ManifestFolderPrefix;
}
+ (NSString *)modifiedDateStringOfFileAtURL:(NSURL *)fileURL
{
if (fileURL != nil) {
NSDictionary *fileAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:[fileURL path] error:nil];
NSDate *modifiedDate = [fileAttributes objectForKey:NSFileModificationDate];
return [NSString stringWithFormat:@"%f", [modifiedDate timeIntervalSince1970]];
} else {
return nil;
}
}
+ (BOOL)verifyFolderHash:(NSString *)finalUpdateFolder
expectedHash:(NSString *)expectedHash
error:(NSError **)error
{
CPLog(@"Verifying hash for folder path: %@", finalUpdateFolder);
NSMutableArray *updateContentsManifest = [NSMutableArray array];
BOOL result = [self addContentsOfFolderToManifest:finalUpdateFolder
pathPrefix:@""
manifest:updateContentsManifest
error:error];
CPLog(@"Manifest string: %@", updateContentsManifest);
if (!result) {
return NO;
}
NSString *updateContentsManifestHash = [self computeFinalHashFromManifest:updateContentsManifest
error:error];
if (!updateContentsManifestHash) {
return NO;
}
CPLog(@"Expected hash: %@, actual hash: %@", expectedHash, updateContentsManifestHash);
return [updateContentsManifestHash isEqualToString:expectedHash];
}
// remove BEGIN / END tags and line breaks from public key string
+ (NSString *)getKeyValueFromPublicKeyString:(NSString *)publicKeyString
{
publicKeyString = [publicKeyString stringByReplacingOccurrencesOfString:@"-----BEGIN PUBLIC KEY-----\n"
withString:@""];
publicKeyString = [publicKeyString stringByReplacingOccurrencesOfString:@"-----END PUBLIC KEY-----"
withString:@""];
publicKeyString = [publicKeyString stringByReplacingOccurrencesOfString:@"\n"
withString:@""];
return publicKeyString;
}
+ (NSString *)getSignatureFilePath:(NSString *)updateFolderPath
{
return [NSString stringWithFormat:@"%@/%@/%@", updateFolderPath, ManifestFolderPrefix, BundleJWTFile];
}
+ (NSString *)getSignatureFor:(NSString *)folderPath
error:(NSError **)error
{
NSString *signatureFilePath = [self getSignatureFilePath:folderPath];
if ([[NSFileManager defaultManager] fileExistsAtPath:signatureFilePath]) {
return [NSString stringWithContentsOfFile:signatureFilePath encoding:NSUTF8StringEncoding error:error];
} else {
*error = [CodePushErrorUtils errorWithMessage:[NSString stringWithFormat: @"Cannot find signature at %@", signatureFilePath]];
return nil;
}
}
+ (NSDictionary *) verifyAndDecodeJWT:(NSString *)jwt
withPublicKey:(NSString *)publicKey
error:(NSError **)error
{
id <JWTAlgorithmDataHolderProtocol> verifyDataHolder = [JWTAlgorithmRSFamilyDataHolder new].keyExtractorType([JWTCryptoKeyExtractor publicKeyWithPEMBase64].type).algorithmName(@"RS256").secret(publicKey);
JWTCodingBuilder *verifyBuilder = [JWTDecodingBuilder decodeMessage:jwt].addHolder(verifyDataHolder);
JWTCodingResultType *verifyResult = verifyBuilder.result;
if (verifyResult.successResult) {
return verifyResult.successResult.payload;
}
else {
*error = verifyResult.errorResult.error;
return nil;
}
}
+ (BOOL)verifyUpdateSignatureFor:(NSString *)folderPath
expectedHash:(NSString *)newUpdateHash
withPublicKey:(NSString *)publicKeyString
error:(NSError **)error
{
NSLog(@"Verifying signature for folder path: %@", folderPath);
NSString *publicKey = [self getKeyValueFromPublicKeyString: publicKeyString];
NSError *signatureVerificationError;
NSString *signature = [self getSignatureFor: folderPath
error: &signatureVerificationError];
if (signatureVerificationError) {
CPLog(@"The update could not be verified because no signature was found. %@", signatureVerificationError);
*error = signatureVerificationError;
return false;
}
NSError *payloadDecodingError;
NSDictionary *envelopedPayload = [self verifyAndDecodeJWT:signature withPublicKey:publicKey error:&payloadDecodingError];
if(payloadDecodingError){
CPLog(@"The update could not be verified because it was not signed by a trusted party. %@", payloadDecodingError);
*error = payloadDecodingError;
return false;
}
CPLog(@"JWT signature verification succeeded, payload content: %@", envelopedPayload);
if(![envelopedPayload objectForKey:@"contentHash"]){
CPLog(@"The update could not be verified because the signature did not specify a content hash.");
return false;
}
NSString *contentHash = envelopedPayload[@"contentHash"];
return [contentHash isEqualToString:newUpdateHash];
}
@end