Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .npmignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ Recipes/
bin/
test/

ios/CodePushCore/Tests/
ios/CodePushCore/Package.swift
.build/
.swiftpm/

# Don't publish TypeScript source, only the compiled lib/ output (built via `npm run build:ts`
# as part of `prepare`) ships
# Anchored to repo root (leading slash): an unanchored `src/` would also match `android/app/src/`.
Expand Down
10 changes: 7 additions & 3 deletions CodePush.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ Pod::Spec.new do |s|
s.preserve_paths = '*.js'
s.libraries = 'z', 'bz2'
s.source_files = [
'ios/CodePush/*.{h,m}',
'ios/CodePush/*.{h,m,swift}',
'ios/CodePushCore/Sources/CodePushCore/**/*.swift',
'shared/diffpatch/*.{c,h}',
'shared/third_party/hdiffpatch/libHDiffPatch/HPatch/patch.{c,h}',
'shared/third_party/hdiffpatch/bsdiff_wrapper/bspatch_wrapper.{c,h}',
Expand All @@ -30,12 +31,15 @@ Pod::Spec.new do |s|
"GCC_PREPROCESSOR_DEFINITIONS" => "$(inherited) _IS_NEED_BLOCK_DEV=0 _IS_USED_MULTITHREAD=0 _IS_NEED_DIR_DIFF_PATCH=0",
"HEADER_SEARCH_PATHS" => "$(inherited) $(PODS_TARGET_SRCROOT)/shared $(PODS_TARGET_SRCROOT)/shared/diffpatch $(PODS_TARGET_SRCROOT)/shared/third_party/hdiffpatch $(PODS_TARGET_SRCROOT)/shared/third_party/hdiffpatch/libHDiffPatch/HPatch",
}
s.swift_version = '5.0'

# The pod contains Swift, so CocoaPods refuses to install it as a static library (the React
# Native default) if a dependency does not define a Clang module. Only add a dependency that is
# a Swift pod or whose podspec sets DEFINES_MODULE.
#
# Note: Even though there are copy/pasted versions of some of these dependencies in the repo,
# we explicitly let CocoaPods pull in the versions below so all dependencies are resolved and
# linked properly at a parent workspace level.
s.dependency 'React-Core'
s.dependency 'SSZipArchive', '~> 2.5.5'
s.dependency 'JWT', '~> 3.0.0-beta.12'
s.dependency 'Base64', '~> 1.1'
end
3 changes: 0 additions & 3 deletions ios/CodePush/CodePush.h
Original file line number Diff line number Diff line change
Expand Up @@ -204,9 +204,6 @@ failCallback:(void (^)(NSError *err))failCallback;
expectedHash:(NSString *)expectedHash
error:(NSError **)error;

// remove BEGIN / END tags and line breaks from public key string
+ (NSString *)getKeyValueFromPublicKeyString:(NSString *)publicKeyString;

+ (NSString *)getSignatureFilePath:(NSString *)updateFolderPath;

+ (NSDictionary *) verifyAndDecodeJWT:(NSString *) jwt
Expand Down
48 changes: 22 additions & 26 deletions ios/CodePush/CodePushUpdateUtils.m
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
#import "CodePush.h"
#import "CodePushErrorUtils.h"
#import "CodePushSha256.h"
#import "JWT.h"

// Cocoapods static library vs framework interop: the generated header lives at a different
// path depending on whether the pod is built as a static library or as a framework.
#if __has_include(<CodePush/CodePush-Swift.h>)
#import <CodePush/CodePush-Swift.h>
#else
#import "CodePush-Swift.h"
#endif

@implementation CodePushUpdateUtils

Expand Down Expand Up @@ -303,19 +310,6 @@ + (BOOL)verifyFolderHash:(NSString *)finalUpdateFolder
return YES;
}

// 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];
Expand All @@ -337,26 +331,28 @@ + (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;
NSError *verificationError;
NSDictionary *payload = [CPJWTVerifier verifyAndDecodeJWT:jwt
publicKey:publicKey
error:&verificationError];
if (!payload) {
if (error) {
// Re-wrap in the CodePush error domain so +[CodePushErrorUtils isCodePushError:]
// recognizes a signature failure as a CodePush error, which permanently blacklists
// the update via +[CodePush saveFailedUpdate:] (retrying won't fix a bad signature).
*error = [CodePushErrorUtils errorWithMessage:verificationError.localizedDescription];
}
return nil;
}

return payload;
}

+ (BOOL)verifyUpdateSignatureFor:(NSString *)folderPath
expectedHash:(NSString *)newUpdateHash
withPublicKey:(NSString *)publicKeyString
withPublicKey:(NSString *)publicKey
error:(NSError **)error
{
NSString *publicKey = [self getKeyValueFromPublicKeyString: publicKeyString];

NSError *signatureVerificationError;
NSString *signature = [self getSignatureFor: folderPath
error: &signatureVerificationError];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ enum RSAPublicKey {
}

private static func derBytes(from string: String) -> Data? {
// Not line by line: the ObjC caller removes every newline but only
// the `PUBLIC KEY` markers, so other armor can arrive inline.
// Not line by line: the `CodePushPublicKey` Info.plist value can have
// the armor and the base64 body on one line.
let body = string
.replacingOccurrences(of: "-----[^-]+-----", with: "", options: .regularExpression)
.filter { !$0.isWhitespace }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,7 @@ private enum Fixture {
-----END PUBLIC KEY-----
"""

/// What `+[CodePushUpdateUtils getKeyValueFromPublicKeyString:]` passes
/// to the verifier.
/// The base64 body only, without PEM armor or newlines.
static var strippedPublicKey: String {
publicKeyPEM
.replacingOccurrences(of: "-----BEGIN PUBLIC KEY-----\n", with: "")
Expand Down Expand Up @@ -143,8 +142,6 @@ final class JWTVerifierTests: XCTestCase {
}

func testAcceptsPKCS1PublicKeyOnOneLine() throws {
// getKeyValueFromPublicKeyString: removes every newline but keeps the
// `RSA PUBLIC KEY` markers.
let key = Fixture.pkcs1PublicKeyPEM.replacingOccurrences(of: "\n", with: "")
let payload = try JWTVerifier.verifyAndDecode(Fixture.validToken, publicKey: key)

Expand Down
12 changes: 12 additions & 0 deletions test/test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,18 @@ class RNIOS extends Platform.IOS implements RNPlatform {
`PRODUCT_BUNDLE_IDENTIFIER = "${TestConfig.TestNamespace}"`,
),
)
// Match the app target's deployment target to the Podfile's `platform :ios` (and
// CodePush.podspec's own deployment target). CodePush ships Swift sources, which
// require the app's deployment target to be at least as high as the pod's; the
// generated project's default falls below that.
.then(
TestUtil.replaceString.bind(
undefined,
path.join(iOSProject, `${TestConfig.TestAppName}.xcodeproj`, 'project.pbxproj'),
'IPHONEOS_DEPLOYMENT_TARGET = [^;]*',
'IPHONEOS_DEPLOYMENT_TARGET = 15.5',
),
)
.then(
TestUtil.copyFile.bind(
undefined,
Expand Down
Loading