Skip to content
This repository was archived by the owner on May 20, 2025. It is now read-only.

Commit 5e332bb

Browse files
ruslan-bikkininsergey-akhalkov
authored andcommitted
Implement code signing for client android SDK (#966)
* Add new optional way to create CodePush instance based on builder pattern Add constructor with additional option PublicKeyFilePath * Adapt changes from old code-signing branch Add `com.auth0:java-jwt:3.2.0` to deps Adapt changes from code-signing branch Fix errors appeared due to jwt library update. * Non-breaking change of CodePush constructor, downgrade jwt library Replace publicKey by publicKeyResourceDescriptor in CodePush constructor Downgrade jwt library to 2.2.2 due to issue with base64 decoding * Make code signing optional * Add small improvements Replace CodePushUnknownException catch with CodePushInvalidPublicKeyException in certain places Make mPublicKey static Add additional log for applying updates * Rename method verifyJWT with verifyAndDecodeJWT * Add minor fixes Add additional checking for potential problems with code-signing integration Fix Public Key parsing from strings.xml * Fix constructors * Fix constructors bug * Fix log messages
1 parent 4ab0e5e commit 5e332bb

File tree

8 files changed

+260
-16
lines changed

8 files changed

+260
-16
lines changed

android/app/build.gradle

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,7 @@ android {
2222

2323
dependencies {
2424
compile "com.facebook.react:react-native:+"
25+
//todo as required minimal sdk version will be more then 23, upgrade this to latest version
26+
//see https://github.com/auth0/java-jwt/issues/131
27+
compile 'com.auth0:java-jwt:2.2.2'
2528
}

android/app/src/main/java/com/microsoft/codepush/react/CodePush.java

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import android.content.Context;
44
import android.content.pm.PackageInfo;
55
import android.content.pm.PackageManager;
6+
import android.content.res.Resources;
7+
import android.support.annotation.NonNull;
68

79
import com.facebook.react.ReactInstanceManager;
810
import com.facebook.react.ReactPackage;
@@ -15,6 +17,7 @@
1517
import org.json.JSONObject;
1618

1719
import java.io.File;
20+
import java.io.NotActiveException;
1821
import java.util.ArrayList;
1922
import java.util.List;
2023

@@ -36,18 +39,24 @@ public class CodePush implements ReactPackage {
3639

3740
// Config properties.
3841
private String mDeploymentKey;
39-
private String mServerUrl = "https://codepush.azurewebsites.net/";
42+
private static String mServerUrl = "https://codepush.azurewebsites.net/";
4043

4144
private Context mContext;
4245
private final boolean mIsDebugMode;
4346

47+
private static String mPublicKey;
48+
4449
private static ReactInstanceHolder mReactInstanceHolder;
4550
private static CodePush mCurrentInstance;
4651

4752
public CodePush(String deploymentKey, Context context) {
4853
this(deploymentKey, context, false);
4954
}
5055

56+
public static String getServiceUrl() {
57+
return mServerUrl;
58+
}
59+
5160
public CodePush(String deploymentKey, Context context, boolean isDebugMode) {
5261
mContext = context.getApplicationContext();
5362

@@ -72,11 +81,45 @@ public CodePush(String deploymentKey, Context context, boolean isDebugMode) {
7281
initializeUpdateAfterRestart();
7382
}
7483

75-
public CodePush(String deploymentKey, Context context, boolean isDebugMode, String serverUrl) {
84+
public CodePush(String deploymentKey, Context context, boolean isDebugMode, @NonNull String serverUrl) {
7685
this(deploymentKey, context, isDebugMode);
7786
mServerUrl = serverUrl;
7887
}
7988

89+
public CodePush(String deploymentKey, Context context, boolean isDebugMode, int publicKeyResourceDescriptor) {
90+
this(deploymentKey, context, isDebugMode);
91+
92+
mPublicKey = getPublicKeyByResourceDescriptor(publicKeyResourceDescriptor);
93+
}
94+
95+
public CodePush(String deploymentKey, Context context, boolean isDebugMode, @NonNull String serverUrl, Integer publicKeyResourceDescriptor) {
96+
this(deploymentKey, context, isDebugMode);
97+
98+
if (publicKeyResourceDescriptor != null) {
99+
mPublicKey = getPublicKeyByResourceDescriptor(publicKeyResourceDescriptor);
100+
}
101+
102+
mServerUrl = serverUrl;
103+
}
104+
105+
private String getPublicKeyByResourceDescriptor(int publicKeyResourceDescriptor){
106+
String publicKey;
107+
try {
108+
publicKey = mContext.getString(publicKeyResourceDescriptor);
109+
} catch (Resources.NotFoundException e) {
110+
throw new CodePushInvalidPublicKeyException(
111+
"Unable to get public key, related resource descriptor " +
112+
publicKeyResourceDescriptor +
113+
" can not be found", e
114+
);
115+
}
116+
117+
if (publicKey.isEmpty()) {
118+
throw new CodePushInvalidPublicKeyException("Specified public key is empty");
119+
}
120+
return publicKey;
121+
}
122+
80123
public void clearDebugCacheIfNeeded() {
81124
if (mIsDebugMode && mSettingsManager.isPendingUpdate(null)) {
82125
// This needs to be kept in sync with https://github.com/facebook/react-native/blob/master/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManager.java#L78
@@ -99,6 +142,10 @@ public String getAssetsBundleFileName() {
99142
return mAssetsBundleFileName;
100143
}
101144

145+
public String getPublicKey() {
146+
return mPublicKey;
147+
}
148+
102149
long getBinaryResourcesModifiedTime() {
103150
try {
104151
String packageName = this.mContext.getPackageName();
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package com.microsoft.codepush.react;
2+
3+
import android.content.Context;
4+
5+
public class CodePushBuilder {
6+
private String mDeploymentKey;
7+
private Context mContext;
8+
9+
private boolean mIsDebugMode;
10+
private String mServerUrl;
11+
private Integer mPublicKeyResourceDescriptor;
12+
13+
public CodePushBuilder(String deploymentKey, Context context) {
14+
this.mDeploymentKey = deploymentKey;
15+
this.mContext = context;
16+
this.mServerUrl = CodePush.getServiceUrl();
17+
}
18+
19+
public CodePushBuilder setIsDebugMode(boolean isDebugMode) {
20+
this.mIsDebugMode = isDebugMode;
21+
return this;
22+
}
23+
24+
public CodePushBuilder setServerUrl(String serverUrl) {
25+
this.mServerUrl = serverUrl;
26+
return this;
27+
}
28+
29+
public CodePushBuilder setPublicKeyResourceDescriptor(int publicKeyResourceDescriptor) {
30+
this.mPublicKeyResourceDescriptor = publicKeyResourceDescriptor;
31+
return this;
32+
}
33+
34+
public CodePush build() {
35+
return new CodePush(this.mDeploymentKey, this.mContext, this.mIsDebugMode, this.mServerUrl, this.mPublicKeyResourceDescriptor);
36+
}
37+
}

android/app/src/main/java/com/microsoft/codepush/react/CodePushConstants.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,5 @@ public class CodePushConstants {
2727
public static final String STATUS_FILE = "codepush.json";
2828
public static final String UNZIPPED_FOLDER_NAME = "unzipped";
2929
public static final String CODE_PUSH_APK_BUILD_TIME_KEY = "CODE_PUSH_APK_BUILD_TIME";
30+
public static final String BUNDLE_JWT_FILE = ".codepushrelease";
3031
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.microsoft.codepush.react;
2+
3+
class CodePushInvalidPublicKeyException extends RuntimeException {
4+
5+
public CodePushInvalidPublicKeyException(String message, Throwable cause) {
6+
super(message, cause);
7+
}
8+
9+
public CodePushInvalidPublicKeyException(String message) {
10+
super(message);
11+
}
12+
}

android/app/src/main/java/com/microsoft/codepush/react/CodePushNativeModule.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,7 @@ public void dispatchDownloadProgressEvent() {
251251
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
252252
.emit(CodePushConstants.DOWNLOAD_PROGRESS_EVENT_NAME, latestDownloadProgress.createWritableMap());
253253
}
254-
});
254+
}, mCodePush.getPublicKey());
255255

256256
JSONObject newPackage = mUpdateManager.getPackage(CodePushUtils.tryGetString(updatePackage, CodePushConstants.PACKAGE_HASH_KEY));
257257
promise.resolve(CodePushUtils.convertJsonObjectToWritable(newPackage));

android/app/src/main/java/com/microsoft/codepush/react/CodePushUpdateManager.java

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ public JSONObject getCurrentPackageInfo() {
5555
return CodePushUtils.getJsonObjectFromFile(statusFilePath);
5656
} catch (IOException e) {
5757
// Should not happen.
58-
throw new CodePushUnknownException("Error getting current package info" , e);
58+
throw new CodePushUnknownException("Error getting current package info", e);
5959
}
6060
}
6161

@@ -64,7 +64,7 @@ public void updateCurrentPackageInfo(JSONObject packageInfo) {
6464
CodePushUtils.writeJsonToFile(packageInfo, getStatusFilePath());
6565
} catch (IOException e) {
6666
// Should not happen.
67-
throw new CodePushUnknownException("Error updating current package info" , e);
67+
throw new CodePushUnknownException("Error updating current package info", e);
6868
}
6969
}
7070

@@ -116,16 +116,16 @@ public JSONObject getCurrentPackage() {
116116
if (packageHash == null) {
117117
return null;
118118
}
119-
119+
120120
return getPackage(packageHash);
121121
}
122-
122+
123123
public JSONObject getPreviousPackage() {
124124
String packageHash = getPreviousPackageHash();
125125
if (packageHash == null) {
126126
return null;
127127
}
128-
128+
129129
return getPackage(packageHash);
130130
}
131131

@@ -140,7 +140,8 @@ public JSONObject getPackage(String packageHash) {
140140
}
141141

142142
public void downloadPackage(JSONObject updatePackage, String expectedBundleFileName,
143-
DownloadProgressCallback progressCallback) throws IOException {
143+
DownloadProgressCallback progressCallback,
144+
String stringPublicKey) throws IOException {
144145
String newUpdateHash = updatePackage.optString(CodePushConstants.PACKAGE_HASH_KEY, null);
145146
String newUpdateFolderPath = getPackageFolderPath(newUpdateHash);
146147
String newUpdateMetadataPath = CodePushUtils.appendPathComponent(newUpdateFolderPath, CodePushConstants.PACKAGE_FILE_NAME);
@@ -179,7 +180,7 @@ public void downloadPackage(JSONObject updatePackage, String expectedBundleFileN
179180
while ((numBytesRead = bin.read(data, 0, CodePushConstants.DOWNLOAD_BUFFER_SIZE)) >= 0) {
180181
if (receivedBytes < 4) {
181182
for (int i = 0; i < numBytesRead; i++) {
182-
int headerOffset = (int)(receivedBytes) + i;
183+
int headerOffset = (int) (receivedBytes) + i;
183184
if (headerOffset >= 4) {
184185
break;
185186
}
@@ -244,7 +245,39 @@ public void downloadPackage(JSONObject updatePackage, String expectedBundleFileN
244245
}
245246

246247
if (isDiffUpdate) {
247-
CodePushUpdateUtils.verifyHashForDiffUpdate(newUpdateFolderPath, newUpdateHash);
248+
CodePushUtils.log("Applying diff update.");
249+
} else {
250+
CodePushUtils.log("Applying full update.");
251+
}
252+
253+
boolean isSignatureVerificationEnabled = (stringPublicKey != null);
254+
255+
String signaturePath = CodePushUpdateUtils.getSignatureFilePath(newUpdateFolderPath);
256+
boolean isSignatureAppearedInBundle = FileUtils.fileAtPathExists(signaturePath);
257+
258+
if (isSignatureVerificationEnabled) {
259+
if (isSignatureAppearedInBundle) {
260+
CodePushUpdateUtils.verifySignature(newUpdateFolderPath, stringPublicKey);
261+
} else {
262+
throw new CodePushInvalidUpdateException(
263+
"Error! Public key was provided but there is no JWT signature within app bundle to verify. " +
264+
"Possible reasons, why that might happen: \n" +
265+
"1. You've been released CodePush bundle update using version of CodePush CLI that is not support code signing.\n" +
266+
"2. You've been released CodePush bundle update without providing --privateKeyPath option."
267+
);
268+
}
269+
} else {
270+
if (isSignatureAppearedInBundle) {
271+
CodePushUtils.log(
272+
"Warning! JWT signature exists in codepush update but code integrity check couldn't be performed because there is no public key configured. " +
273+
"Please ensure that public key is properly configured within your application."
274+
);
275+
CodePushUpdateUtils.verifyFolderHash(newUpdateFolderPath, newUpdateHash);
276+
} else {
277+
if (isDiffUpdate) {
278+
CodePushUpdateUtils.verifyFolderHash(newUpdateFolderPath, newUpdateHash);
279+
}
280+
}
248281
}
249282

250283
CodePushUtils.setJSONValueForKey(updatePackage, CodePushConstants.RELATIVE_BUNDLE_PATH_KEY, relativeBundlePath);

0 commit comments

Comments
 (0)