From fe94e58a9195968044bc6bad985ff9e1eac2523c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliv=C3=A9r=20Falvai?= Date: Tue, 1 Sep 2026 13:43:32 +0200 Subject: [PATCH] Replace JWT/Base64 pods with CodePushCore Swift verifier --- .npmignore | 5 ++ CodePush.podspec | 10 ++-- ios/CodePush/CodePush.h | 3 -- ios/CodePush/CodePushUpdateUtils.m | 48 +++++++++---------- .../SignatureVerification/RSAPublicKey.swift | 4 +- .../CodePushCoreTests/JWTVerifierTests.swift | 5 +- test/test.mts | 12 +++++ 7 files changed, 49 insertions(+), 38 deletions(-) diff --git a/.npmignore b/.npmignore index eec9287e..831ef99f 100644 --- a/.npmignore +++ b/.npmignore @@ -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/`. diff --git a/CodePush.podspec b/CodePush.podspec index e16fa4c7..12951d06 100644 --- a/CodePush.podspec +++ b/CodePush.podspec @@ -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}', @@ -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 diff --git a/ios/CodePush/CodePush.h b/ios/CodePush/CodePush.h index 018ea553..0efaa3e1 100644 --- a/ios/CodePush/CodePush.h +++ b/ios/CodePush/CodePush.h @@ -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 diff --git a/ios/CodePush/CodePushUpdateUtils.m b/ios/CodePush/CodePushUpdateUtils.m index 5b4aee42..eb7a2aee 100644 --- a/ios/CodePush/CodePushUpdateUtils.m +++ b/ios/CodePush/CodePushUpdateUtils.m @@ -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() +#import +#else +#import "CodePush-Swift.h" +#endif @implementation CodePushUpdateUtils @@ -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]; @@ -337,26 +331,28 @@ + (NSDictionary *) verifyAndDecodeJWT:(NSString *)jwt withPublicKey:(NSString *)publicKey error:(NSError **)error { - id 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]; diff --git a/ios/CodePushCore/Sources/CodePushCore/SignatureVerification/RSAPublicKey.swift b/ios/CodePushCore/Sources/CodePushCore/SignatureVerification/RSAPublicKey.swift index 4ab953da..9f10b35d 100644 --- a/ios/CodePushCore/Sources/CodePushCore/SignatureVerification/RSAPublicKey.swift +++ b/ios/CodePushCore/Sources/CodePushCore/SignatureVerification/RSAPublicKey.swift @@ -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 } diff --git a/ios/CodePushCore/Tests/CodePushCoreTests/JWTVerifierTests.swift b/ios/CodePushCore/Tests/CodePushCoreTests/JWTVerifierTests.swift index f49b84d4..5bb44919 100644 --- a/ios/CodePushCore/Tests/CodePushCoreTests/JWTVerifierTests.swift +++ b/ios/CodePushCore/Tests/CodePushCoreTests/JWTVerifierTests.swift @@ -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: "") @@ -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) diff --git a/test/test.mts b/test/test.mts index b8309e80..e223cf92 100644 --- a/test/test.mts +++ b/test/test.mts @@ -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,