Skip to content

Commit

Permalink
Implement code signing for client android SDK (microsoft#966)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
ruslan-bikkinin authored and sergey-akhalkov committed Sep 13, 2017
1 parent 4ab0e5e commit 5e332bb
Show file tree
Hide file tree
Showing 8 changed files with 260 additions and 16 deletions.
3 changes: 3 additions & 0 deletions android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,7 @@ android {

dependencies {
compile "com.facebook.react:react-native:+"
//todo as required minimal sdk version will be more then 23, upgrade this to latest version
//see https://github.com/auth0/java-jwt/issues/131
compile 'com.auth0:java-jwt:2.2.2'
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.support.annotation.NonNull;

import com.facebook.react.ReactInstanceManager;
import com.facebook.react.ReactPackage;
Expand All @@ -15,6 +17,7 @@
import org.json.JSONObject;

import java.io.File;
import java.io.NotActiveException;
import java.util.ArrayList;
import java.util.List;

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

// Config properties.
private String mDeploymentKey;
private String mServerUrl = "https://codepush.azurewebsites.net/";
private static String mServerUrl = "https://codepush.azurewebsites.net/";

private Context mContext;
private final boolean mIsDebugMode;

private static String mPublicKey;

private static ReactInstanceHolder mReactInstanceHolder;
private static CodePush mCurrentInstance;

public CodePush(String deploymentKey, Context context) {
this(deploymentKey, context, false);
}

public static String getServiceUrl() {
return mServerUrl;
}

public CodePush(String deploymentKey, Context context, boolean isDebugMode) {
mContext = context.getApplicationContext();

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

public CodePush(String deploymentKey, Context context, boolean isDebugMode, String serverUrl) {
public CodePush(String deploymentKey, Context context, boolean isDebugMode, @NonNull String serverUrl) {
this(deploymentKey, context, isDebugMode);
mServerUrl = serverUrl;
}

public CodePush(String deploymentKey, Context context, boolean isDebugMode, int publicKeyResourceDescriptor) {
this(deploymentKey, context, isDebugMode);

mPublicKey = getPublicKeyByResourceDescriptor(publicKeyResourceDescriptor);
}

public CodePush(String deploymentKey, Context context, boolean isDebugMode, @NonNull String serverUrl, Integer publicKeyResourceDescriptor) {
this(deploymentKey, context, isDebugMode);

if (publicKeyResourceDescriptor != null) {
mPublicKey = getPublicKeyByResourceDescriptor(publicKeyResourceDescriptor);
}

mServerUrl = serverUrl;
}

private String getPublicKeyByResourceDescriptor(int publicKeyResourceDescriptor){
String publicKey;
try {
publicKey = mContext.getString(publicKeyResourceDescriptor);
} catch (Resources.NotFoundException e) {
throw new CodePushInvalidPublicKeyException(
"Unable to get public key, related resource descriptor " +
publicKeyResourceDescriptor +
" can not be found", e
);
}

if (publicKey.isEmpty()) {
throw new CodePushInvalidPublicKeyException("Specified public key is empty");
}
return publicKey;
}

public void clearDebugCacheIfNeeded() {
if (mIsDebugMode && mSettingsManager.isPendingUpdate(null)) {
// 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
Expand All @@ -99,6 +142,10 @@ public String getAssetsBundleFileName() {
return mAssetsBundleFileName;
}

public String getPublicKey() {
return mPublicKey;
}

long getBinaryResourcesModifiedTime() {
try {
String packageName = this.mContext.getPackageName();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.microsoft.codepush.react;

import android.content.Context;

public class CodePushBuilder {
private String mDeploymentKey;
private Context mContext;

private boolean mIsDebugMode;
private String mServerUrl;
private Integer mPublicKeyResourceDescriptor;

public CodePushBuilder(String deploymentKey, Context context) {
this.mDeploymentKey = deploymentKey;
this.mContext = context;
this.mServerUrl = CodePush.getServiceUrl();
}

public CodePushBuilder setIsDebugMode(boolean isDebugMode) {
this.mIsDebugMode = isDebugMode;
return this;
}

public CodePushBuilder setServerUrl(String serverUrl) {
this.mServerUrl = serverUrl;
return this;
}

public CodePushBuilder setPublicKeyResourceDescriptor(int publicKeyResourceDescriptor) {
this.mPublicKeyResourceDescriptor = publicKeyResourceDescriptor;
return this;
}

public CodePush build() {
return new CodePush(this.mDeploymentKey, this.mContext, this.mIsDebugMode, this.mServerUrl, this.mPublicKeyResourceDescriptor);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,5 @@ public class CodePushConstants {
public static final String STATUS_FILE = "codepush.json";
public static final String UNZIPPED_FOLDER_NAME = "unzipped";
public static final String CODE_PUSH_APK_BUILD_TIME_KEY = "CODE_PUSH_APK_BUILD_TIME";
public static final String BUNDLE_JWT_FILE = ".codepushrelease";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.microsoft.codepush.react;

class CodePushInvalidPublicKeyException extends RuntimeException {

public CodePushInvalidPublicKeyException(String message, Throwable cause) {
super(message, cause);
}

public CodePushInvalidPublicKeyException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ public void dispatchDownloadProgressEvent() {
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
.emit(CodePushConstants.DOWNLOAD_PROGRESS_EVENT_NAME, latestDownloadProgress.createWritableMap());
}
});
}, mCodePush.getPublicKey());

JSONObject newPackage = mUpdateManager.getPackage(CodePushUtils.tryGetString(updatePackage, CodePushConstants.PACKAGE_HASH_KEY));
promise.resolve(CodePushUtils.convertJsonObjectToWritable(newPackage));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public JSONObject getCurrentPackageInfo() {
return CodePushUtils.getJsonObjectFromFile(statusFilePath);
} catch (IOException e) {
// Should not happen.
throw new CodePushUnknownException("Error getting current package info" , e);
throw new CodePushUnknownException("Error getting current package info", e);
}
}

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

Expand Down Expand Up @@ -116,16 +116,16 @@ public JSONObject getCurrentPackage() {
if (packageHash == null) {
return null;
}

return getPackage(packageHash);
}

public JSONObject getPreviousPackage() {
String packageHash = getPreviousPackageHash();
if (packageHash == null) {
return null;
}

return getPackage(packageHash);
}

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

public void downloadPackage(JSONObject updatePackage, String expectedBundleFileName,
DownloadProgressCallback progressCallback) throws IOException {
DownloadProgressCallback progressCallback,
String stringPublicKey) throws IOException {
String newUpdateHash = updatePackage.optString(CodePushConstants.PACKAGE_HASH_KEY, null);
String newUpdateFolderPath = getPackageFolderPath(newUpdateHash);
String newUpdateMetadataPath = CodePushUtils.appendPathComponent(newUpdateFolderPath, CodePushConstants.PACKAGE_FILE_NAME);
Expand Down Expand Up @@ -179,7 +180,7 @@ public void downloadPackage(JSONObject updatePackage, String expectedBundleFileN
while ((numBytesRead = bin.read(data, 0, CodePushConstants.DOWNLOAD_BUFFER_SIZE)) >= 0) {
if (receivedBytes < 4) {
for (int i = 0; i < numBytesRead; i++) {
int headerOffset = (int)(receivedBytes) + i;
int headerOffset = (int) (receivedBytes) + i;
if (headerOffset >= 4) {
break;
}
Expand Down Expand Up @@ -244,7 +245,39 @@ public void downloadPackage(JSONObject updatePackage, String expectedBundleFileN
}

if (isDiffUpdate) {
CodePushUpdateUtils.verifyHashForDiffUpdate(newUpdateFolderPath, newUpdateHash);
CodePushUtils.log("Applying diff update.");
} else {
CodePushUtils.log("Applying full update.");
}

boolean isSignatureVerificationEnabled = (stringPublicKey != null);

String signaturePath = CodePushUpdateUtils.getSignatureFilePath(newUpdateFolderPath);
boolean isSignatureAppearedInBundle = FileUtils.fileAtPathExists(signaturePath);

if (isSignatureVerificationEnabled) {
if (isSignatureAppearedInBundle) {
CodePushUpdateUtils.verifySignature(newUpdateFolderPath, stringPublicKey);
} else {
throw new CodePushInvalidUpdateException(
"Error! Public key was provided but there is no JWT signature within app bundle to verify. " +
"Possible reasons, why that might happen: \n" +
"1. You've been released CodePush bundle update using version of CodePush CLI that is not support code signing.\n" +
"2. You've been released CodePush bundle update without providing --privateKeyPath option."
);
}
} else {
if (isSignatureAppearedInBundle) {
CodePushUtils.log(
"Warning! JWT signature exists in codepush update but code integrity check couldn't be performed because there is no public key configured. " +
"Please ensure that public key is properly configured within your application."
);
CodePushUpdateUtils.verifyFolderHash(newUpdateFolderPath, newUpdateHash);
} else {
if (isDiffUpdate) {
CodePushUpdateUtils.verifyFolderHash(newUpdateFolderPath, newUpdateHash);
}
}
}

CodePushUtils.setJSONValueForKey(updatePackage, CodePushConstants.RELATIVE_BUNDLE_PATH_KEY, relativeBundlePath);
Expand Down
Loading

0 comments on commit 5e332bb

Please sign in to comment.