22
33import assert = require( "assert" ) ;
44import childProcess = require( "child_process" ) ;
5- import crypto = require( "crypto" ) ;
65import fs = require( "fs" ) ;
76import mkdirp = require( "mkdirp" ) ;
87import os = require( "os" ) ;
98import path = require( "path" ) ;
109import slash = require( "slash" ) ;
10+ import { promisify } from "util" ;
1111
1212import { Platform , PluginTestingFramework , ProjectManager , setupTestRunScenario , setupUpdateScenario , ServerUtil , TestBuilder , TestConfig , TestUtil } from "code-push-plugin-testing-framework" ;
1313
1414import Q = require( "q" ) ;
1515
1616import del = require( "del" ) ;
1717
18+ import { codeSigningPublicKey , signAndRecordUpdateArchive , setupTamperedSignatureUpdateScenario } from "./codesign" ;
19+
1820function ensureAndroidCleartextTraffic ( androidManifestPath : string ) : void {
1921 const androidManifestContents = fs . readFileSync ( androidManifestPath , "utf8" ) ;
2022
@@ -37,6 +39,19 @@ function ensureAndroidCleartextTraffic(androidManifestPath: string): void {
3739 }
3840}
3941
42+ /**
43+ * Sets a string value at the top level of a plist file via `plutil` (adds the key if it doesn't
44+ * already exist), instead of via text/regex find-replace. Info.plist edits used to be done with
45+ * TestUtil.replaceString, whose second argument is a regular expression: an unescaped/unanchored
46+ * pattern like "1.0" can match unrelated text elsewhere in the file - including inside another
47+ * value injected earlier in the same chain, such as "1O0" inside CodePushPublicKey's base64,
48+ * silently corrupting the RSA key and breaking signature verification. plutil edits the plist
49+ * structurally, so it can't reach into or corrupt unrelated values.
50+ */
51+ async function setPlistStringValue ( plistPath : string , key : string , value : string ) : Promise < void > {
52+ await promisify ( childProcess . execFile ) ( "plutil" , [ "-replace" , key , "-string" , value , plistPath ] ) ;
53+ }
54+
4055/**
4156 * Returns a " --platform <ios|android>" flag for `expo prebuild` when exactly one platform is
4257 * under test in this mocha run, so prebuild only regenerates that platform's native project
@@ -69,47 +84,6 @@ function installExpoBundleTooling(projectPath: string): Q.Promise<void> {
6984 ) . then ( ( ) => { return null ; } ) ;
7085}
7186
72- const CODEPUSH_METADATA_FILE_NAME = ".codepushrelease" ;
73-
74- function isHashIgnored ( relativePath : string ) : boolean {
75- return relativePath . startsWith ( "__MACOSX/" )
76- || relativePath === ".DS_Store"
77- || relativePath . endsWith ( "/.DS_Store" )
78- || relativePath === CODEPUSH_METADATA_FILE_NAME
79- || relativePath . endsWith ( `/${ CODEPUSH_METADATA_FILE_NAME } ` ) ;
80- }
81-
82- /**
83- * Computes the same content hash that the native SDKs compute over an installed update folder, so the mock server
84- * can hand back a package_hash that will actually match what the client expects.
85- */
86- function computeUpdateContentsHash ( folderPath : string ) : string {
87- const manifest : string [ ] = [ ] ;
88-
89- const walk = ( currentPath : string , relativePrefix : string ) => {
90- for ( const entryName of fs . readdirSync ( currentPath ) ) {
91- const entryPath = path . join ( currentPath , entryName ) ;
92- const relativePath = relativePrefix ? `${ relativePrefix } /${ entryName } ` : entryName ;
93-
94- if ( isHashIgnored ( relativePath ) ) {
95- continue ;
96- }
97-
98- if ( fs . statSync ( entryPath ) . isDirectory ( ) ) {
99- walk ( entryPath , relativePath ) ;
100- } else {
101- const fileHash = crypto . createHash ( "sha256" ) . update ( fs . readFileSync ( entryPath ) ) . digest ( "hex" ) ;
102- manifest . push ( `${ relativePath } :${ fileHash } ` ) ;
103- }
104- }
105- } ;
106-
107- walk ( folderPath , "" ) ;
108- manifest . sort ( ) ;
109-
110- return crypto . createHash ( "sha256" ) . update ( JSON . stringify ( manifest ) ) . digest ( "hex" ) ;
111- }
112-
11387//////////////////////////////////////////////////////////////////////////////////////////
11488// Create the platforms to run the tests on.
11589
@@ -209,6 +183,7 @@ class RNAndroid extends Platform.Android implements RNPlatform {
209183 const string = path . join ( innerprojectDirectory , "android" , "app" , "src" , "main" , "res" , "values" , "strings.xml" ) ;
210184 TestUtil . replaceString ( string , TestUtil . SERVER_URL_PLACEHOLDER , this . getServerUrl ( ) ) ;
211185 TestUtil . replaceString ( string , TestUtil . ANDROID_KEY_PLACEHOLDER , this . getDefaultDeploymentKey ( ) ) ;
186+ TestUtil . replaceString ( string , "</resources>" , `<string moduleConfig="true" name="CodePushPublicKey">${ codeSigningPublicKey } </string>\n</resources>` ) ;
212187 TestUtil . replaceString ( AndroidManifest , "\\${usesCleartextTraffic}" , "true" ) ;
213188
214189
@@ -280,14 +255,12 @@ class RNIOS extends Platform.IOS implements RNPlatform {
280255 // Install the Podfile
281256 return TestUtil . copyFile ( path . join ( TestConfig . templatePath , "ios" , "Podfile" ) , podfilePath , true )
282257 . then ( ( ) => TestUtil . getProcessOutput ( `pod install` , { cwd : iOSProject , noLogStdOut : true } ) )
283- // Put the IOS deployment key in the Info.plist
284- . then ( TestUtil . replaceString . bind ( undefined , infoPlistPath ,
285- "</dict>\n</plist>" ,
286- "<key>CodePushDeploymentKey</key>\n\t<string>" + this . getDefaultDeploymentKey ( ) + "</string>\n\t<key>CodePushServerURL</key>\n\t<string>" + this . getServerUrl ( ) + "</string>\n\t</dict>\n</plist>" ) )
287- // Set the app version to 1.0.0 instead of 1.0 in the Info.plist
288- . then ( TestUtil . replaceString . bind ( undefined , infoPlistPath , "1.0" , "1.0.0" ) )
289- // Remove dependence of CFBundleShortVersionString from project.pbxproj
290- . then ( TestUtil . replaceString . bind ( undefined , infoPlistPath , "\\$\\(MARKETING_VERSION\\)" , "1.0.0" ) )
258+ // Set the app version to 1.0.0 instead of 1.0 (or "$(MARKETING_VERSION)") in the Info.plist,
259+ // and put the IOS deployment key, server URL and code signing public key in there too.
260+ . then ( ( ) => setPlistStringValue ( infoPlistPath , "CFBundleShortVersionString" , "1.0.0" ) )
261+ . then ( ( ) => setPlistStringValue ( infoPlistPath , "CodePushDeploymentKey" , this . getDefaultDeploymentKey ( ) ) )
262+ . then ( ( ) => setPlistStringValue ( infoPlistPath , "CodePushServerURL" , this . getServerUrl ( ) ) )
263+ . then ( ( ) => setPlistStringValue ( infoPlistPath , "CodePushPublicKey" , codeSigningPublicKey ) )
291264 // Fix the linker flag list in project.pbxproj (pod install adds an extra comma)
292265 . then ( TestUtil . replaceString . bind ( undefined , path . join ( iOSProject , TestConfig . TestAppName + ".xcodeproj" , "project.pbxproj" ) ,
293266 "\"[$][(]inherited[)]\",\\s*[)];" , "\"$(inherited)\"\n\t\t\t\t);" ) )
@@ -539,28 +512,19 @@ class RNProjectManager extends ProjectManager {
539512 . then ( TestUtil . getProcessOutput . bind ( undefined , "npx expo prebuild --platform " + targetPlatform . getName ( ) , { cwd : path . join ( projectDirectory , TestConfig . TestAppName ) , noLogStdOut : true } ) )
540513 . then ( TestUtil . getProcessOutput . bind ( undefined , "npx react-native bundle --entry-file index.js --platform " + targetPlatform . getName ( ) + " --bundle-output " + bundlePath + " --assets-dest " + bundleFolder + " --dev false" ,
541514 { cwd : path . join ( projectDirectory , TestConfig . TestAppName ) , noLogStdOut : true } ) )
515+ . then ( ( ) => signAndRecordUpdateArchive ( bundleFolder , isDiff ) )
542516 . then < string > ( TestUtil . archiveFolder . bind ( undefined , bundleFolder , "" , path . join ( projectDirectory , TestConfig . TestAppName , "update.zip" ) , isDiff ) )
543- . then < string > ( this . updateMockPackageHash . bind ( this , bundleFolder , isDiff ) )
544517 . then ( ( result ) => { console . log ( `[TIMING] createUpdateArchive(${ projectDirectory } , ${ targetPlatform . getName ( ) } ) took ${ Date . now ( ) - t0 } ms` ) ; return result ; } ) ;
545518 } else {
546519 return deferred . promise
547520 . then ( TestUtil . getProcessOutput . bind ( undefined , "npx react-native bundle --entry-file index.js --platform " + targetPlatform . getName ( ) + " --bundle-output " + bundlePath + " --assets-dest " + bundleFolder + " --dev false" ,
548521 { cwd : path . join ( projectDirectory , TestConfig . TestAppName ) , noLogStdOut : true } ) )
522+ . then ( ( ) => signAndRecordUpdateArchive ( bundleFolder , isDiff ) )
549523 . then < string > ( TestUtil . archiveFolder . bind ( undefined , bundleFolder , "" , path . join ( projectDirectory , TestConfig . TestAppName , "update.zip" ) , isDiff ) )
550- . then < string > ( this . updateMockPackageHash . bind ( this , bundleFolder , isDiff ) )
551524 . then ( ( result ) => { console . log ( `[TIMING] createUpdateArchive(${ projectDirectory } , ${ targetPlatform . getName ( ) } ) took ${ Date . now ( ) - t0 } ms` ) ; return result ; } ) ;
552525 }
553526 }
554527
555- // Records the real hash of bundleFolder of an archive, so the mock server can hand back a
556- // package_hash that matches what the client's verifyFolderHash integrity check will compute.
557- private updateMockPackageHash ( bundleFolder : string , isDiff : boolean , archivePath : string ) : string {
558- // TODO(RA-4875): Diff updates clear it instead, since they are poorly implemented in the entire test harness.
559- // It's going to be a bigger refactor, so for now we just clear the known package hash to avoid using a stale value in diff tests.
560- ServerUtil . setKnownPackageHash ( isDiff ? undefined : computeUpdateContentsHash ( bundleFolder ) ) ;
561- return archivePath ;
562- }
563-
564528 /** JSON file containing the platforms the plugin is currently installed for.
565529 * Keys must match targetPlatform.getName()!
566530 *
@@ -1050,6 +1014,27 @@ PluginTestingFramework.initializeTests(new RNProjectManager(), supportedTargetPl
10501014 } ) ;
10511015 } , ScenarioInstall ) ;
10521016
1017+ TestBuilder . describe ( "#localPackage.install.codeSigning" ,
1018+ ( ) => {
1019+ TestBuilder . it ( "localPackage.install.codeSigning.tamperedSignature" , false ,
1020+ async ( done : Mocha . Done ) => {
1021+ try {
1022+ ServerUtil . updateResponse = { update_info : ServerUtil . createUpdateResponse ( false , targetPlatform ) } ;
1023+
1024+ /* create a normal update, then tamper with its signature after it's been signed */
1025+ const updatePath = await setupTamperedSignatureUpdateScenario ( projectManager , targetPlatform , UpdateNotifyApplicationReady , "Tampered Update" ) ;
1026+ ServerUtil . updatePackagePath = updatePath ;
1027+ projectManager . runApplication ( TestConfig . testRunDirectory , targetPlatform ) ;
1028+ await ServerUtil . expectTestMessages ( [
1029+ ServerUtil . TestMessage . CHECK_UPDATE_AVAILABLE ,
1030+ ServerUtil . TestMessage . DOWNLOAD_ERROR ] ) ;
1031+ done ( ) ;
1032+ } catch ( e ) {
1033+ done ( e ) ;
1034+ }
1035+ } ) ;
1036+ } , ScenarioInstall ) ;
1037+
10531038 TestBuilder . describe ( "#localPackage.install.revert" ,
10541039 ( ) => {
10551040 TestBuilder . it ( "localPackage.install.revert.dorevert" , false ,
0 commit comments