* Refactor code-signing JS and android

* Small fixes and refactoring for JS and android

Add ignore list parameter for copying packages files
Rename unzipDir to deployDir for consistency
Fix wrong parrameters passing to verify function
Fix native android parsePublicKey method
Remove redundant line in native code

* Minor fixes for android and ios

Add MACOSX directory to ignore for hashing
Update ios native API

* Add additional signature clearing

* Bump JWT version dependency
This commit is contained in:
Ruslan Bikkinin 2017-11-08 17:16:26 +03:00 коммит произвёл GitHub
Родитель 799c499dcb
Коммит 645c899081
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
13 изменённых файлов: 414 добавлений и 211 удалений

Просмотреть файл

@ -95,7 +95,13 @@ var FileUtil = (function () {
FileUtil.dataDirectoryExists = function (path, callback) {
FileUtil.directoryExists(cordova.file.dataDirectory, path, callback);
};
FileUtil.copyDirectoryEntriesTo = function (sourceDir, destinationDir, callback) {
FileUtil.copyDirectoryEntriesTo = function (sourceDir, destinationDir, ignoreList, callback) {
if (ignoreList.indexOf(".DS_Store") === -1) {
ignoreList.push(".DS_Store");
}
if (ignoreList.indexOf("__MACOSX") === -1) {
ignoreList.push("__MACOSX");
}
var fail = function (error) {
callback(FileUtil.fileErrorToError(error), null);
};
@ -104,7 +110,7 @@ var FileUtil = (function () {
var copyOne = function () {
if (i < entries.length) {
var nextEntry = entries[i++];
if (nextEntry.name === ".DS_Store" || nextEntry.name === "__MACOSX") {
if (ignoreList.indexOf(nextEntry.name) > 0) {
copyOne();
}
else {
@ -113,7 +119,7 @@ var FileUtil = (function () {
callback(new Error("Error during entry replacement. Error code: " + fileError.code), null);
};
if (destinationEntry.isDirectory) {
FileUtil.copyDirectoryEntriesTo(nextEntry, destinationEntry, function (error) {
FileUtil.copyDirectoryEntriesTo(nextEntry, destinationEntry, ignoreList, function (error) {
if (error) {
callback(error, null);
}

Просмотреть файл

@ -1,10 +1,10 @@
/********************************************************************************************
THIS FILE HAS BEEN COMPILED FROM TYPESCRIPT SOURCES.
PLEASE DO NOT MODIFY THIS FILE DIRECTLY AS YOU WILL LOSE YOUR CHANGES WHEN RECOMPILING.
INSTEAD, EDIT THE TYPESCRIPT SOURCES UNDER THE WWW FOLDER, AND THEN RUN GULP.
FOR MORE INFORMATION, PLEASE SEE CONTRIBUTING.md.
*********************************************************************************************/
/********************************************************************************************
THIS FILE HAS BEEN COMPILED FROM TYPESCRIPT SOURCES.
PLEASE DO NOT MODIFY THIS FILE DIRECTLY AS YOU WILL LOSE YOUR CHANGES WHEN RECOMPILING.
INSTEAD, EDIT THE TYPESCRIPT SOURCES UNDER THE WWW FOLDER, AND THEN RUN GULP.
FOR MORE INFORMATION, PLEASE SEE CONTRIBUTING.md.
*********************************************************************************************/
"use strict";
@ -43,13 +43,6 @@ var LocalPackage = (function (_super) {
Sdk.reportStatusDeploy(_this, AcquisitionStatus.DeploymentFailed, _this.deploymentKey);
};
var newPackageLocation = LocalPackage.VersionsDir + "/" + this.packageHash;
var signatureVerified = function (deployDir) {
_this.localPath = deployDir.fullPath;
_this.finishInstall(deployDir, installOptions, installSuccess, installError);
};
var donePackageFileCopy = function (deployDir) {
_this.verifyPackage(deployDir, installError, CodePushUtil.getNodeStyleCallbackFor(signatureVerified, installError));
};
var newPackageUnzipped = function (unzipError) {
if (unzipError) {
installError && installError(new Error("Could not unzip package" + CodePushUtil.getErrorMessage(unzipError)));
@ -58,6 +51,15 @@ var LocalPackage = (function (_super) {
LocalPackage.handleDeployment(newPackageLocation, CodePushUtil.getNodeStyleCallbackFor(donePackageFileCopy, installError));
}
};
var donePackageFileCopy = function (deploymentResult) {
_this.verifyPackage(deploymentResult, installError, function () {
packageVerified(deploymentResult.deployDir);
});
};
var packageVerified = function (deployDir) {
_this.localPath = deployDir.fullPath;
_this.finishInstall(deployDir, installOptions, installSuccess, installError);
};
FileUtil.getDataDirectory(LocalPackage.DownloadUnzipDir, false, function (error, directoryEntry) {
var unzipPackage = function () {
FileUtil.getDataDirectory(LocalPackage.DownloadUnzipDir, true, function (innerError, unzipDir) {
@ -84,44 +86,122 @@ var LocalPackage = (function (_super) {
installError && installError(new Error("An error occured while installing the package. " + CodePushUtil.getErrorMessage(e)));
}
};
LocalPackage.prototype.verifyPackage = function (unzipDir, installError, callback) {
LocalPackage.prototype.verifyPackage = function (deploymentResult, installError, successCallback) {
var _this = this;
var packageHashSuccess = function (localHash) {
CodePushUtil.logMessage("Expected hash: " + _this.packageHash + ", actual hash: " + localHash);
FileUtil.readFile(cordova.file.dataDirectory, unzipDir.fullPath + '/www', '.codepushrelease', function (error, contents) {
var verifySignatureSuccess = function (expectedHash) {
if (localHash !== _this.packageHash) {
installError(new Error("package hash verification failed"));
return;
var deployDir = deploymentResult.deployDir;
var verificationFail = function (error) {
installError && installError(error);
};
var verify = function (isSignatureVerificationEnabled, isSignatureAppearedInBundle, publicKey, signature) {
if (isSignatureVerificationEnabled) {
if (isSignatureAppearedInBundle) {
_this.verifyHash(deployDir, _this.packageHash, verificationFail, function () {
_this.verifySignature(deployDir, _this.packageHash, publicKey, signature, verificationFail, successCallback);
});
}
else {
var errorMessage = "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.";
installError && installError(new Error(errorMessage));
}
}
else {
if (isSignatureAppearedInBundle) {
CodePushUtil.logMessage("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.");
_this.verifyHash(deployDir, _this.packageHash, verificationFail, successCallback);
}
else {
if (deploymentResult.isDiffUpdate) {
_this.verifyHash(deployDir, _this.packageHash, verificationFail, successCallback);
}
if (!expectedHash) {
CodePushUtil.logMessage("The update contents succeeded the data integrity check.");
callback(null, unzipDir);
if (contents != null) {
CodePushUtil.logMessage("Warning! JWT signature exists in codepush update but code integrity check couldn't be performed because there is no public key configured. \n" +
"Please ensure that a public key is properly configured within your application.");
}
return;
}
if (localHash === expectedHash) {
CodePushUtil.logMessage("The update contents succeeded the code signing check.");
callback(null, unzipDir);
return;
}
installError(new Error("The update contents failed the code signing check."));
};
var verifySignatureFail = function (error) {
installError && installError(new Error("The update contents failed the code signing check. " + error));
};
CodePushUtil.logMessage("Verifying signature for folder path: " + unzipDir.fullPath);
cordova.exec(verifySignatureSuccess, verifySignatureFail, "CodePush", "verifySignature", [contents]);
successCallback();
}
}
};
if (deploymentResult.isDiffUpdate) {
CodePushUtil.logMessage("Applying diff update");
}
else {
CodePushUtil.logMessage("Applying full update");
}
var isSignatureVerificationEnabled, isSignatureAppearedInBundle;
var publicKey;
this.getPublicKey(function (error, publicKeyResult) {
if (error) {
installError && installError(new Error("Error reading public key. " + error));
return;
}
publicKey = publicKeyResult;
isSignatureVerificationEnabled = (publicKey !== null);
_this.getSignatureFromUpdate(deploymentResult.deployDir, function (error, signature) {
if (error) {
installError && installError(new Error("Error reading signature from update. " + error));
return;
}
isSignatureAppearedInBundle = (signature !== null);
verify(isSignatureVerificationEnabled, isSignatureAppearedInBundle, publicKey, signature);
});
});
};
LocalPackage.prototype.getPublicKey = function (callback) {
var success = function (publicKey) {
callback(null, publicKey);
};
var fail = function (error) {
callback(error, null);
};
cordova.exec(success, fail, "CodePush", "getPublicKey", []);
};
LocalPackage.prototype.getSignatureFromUpdate = function (deployDir, callback) {
var rootUri = cordova.file.dataDirectory;
var path = deployDir.fullPath + '/www';
var fileName = '.codepushrelease';
FileUtil.fileExists(rootUri, path, fileName, function (error, result) {
if (!result) {
callback(null, null);
return;
}
FileUtil.readFile(rootUri, path, fileName, function (error, signature) {
if (error) {
callback(error, null);
return;
}
callback(null, signature);
});
});
};
LocalPackage.prototype.verifyHash = function (deployDir, newUpdateHash, errorCallback, successCallback) {
var packageHashSuccess = function (computedHash) {
if (computedHash !== newUpdateHash) {
errorCallback(new Error("The update contents failed the data integrity check."));
return;
}
CodePushUtil.logMessage("The update contents succeeded the data integrity check.");
successCallback();
};
var packageHashFail = function (error) {
installError && installError(new Error("unable to compute hash for package: " + error));
errorCallback(new Error("Unable to compute hash for package: " + error));
};
CodePushUtil.logMessage("Verifying hash for folder path: " + unzipDir.fullPath);
cordova.exec(packageHashSuccess, packageHashFail, "CodePush", "getPackageHash", [unzipDir.fullPath]);
CodePushUtil.logMessage("Verifying hash for folder path: " + deployDir.fullPath);
cordova.exec(packageHashSuccess, packageHashFail, "CodePush", "getPackageHash", [deployDir.fullPath]);
};
LocalPackage.prototype.verifySignature = function (deployDir, newUpdateHash, publicKey, signature, errorCallback, successCallback) {
var decodeSignatureSuccess = function (contentHash) {
if (contentHash !== newUpdateHash) {
errorCallback(new Error("The update contents failed the code signing check."));
return;
}
CodePushUtil.logMessage("The update contents succeeded the code signing check.");
successCallback();
};
var decodeSignatureFail = function (error) {
errorCallback(new Error("Unable to verify signature for package: " + error));
};
CodePushUtil.logMessage("Verifying signature for folder path: " + deployDir.fullPath);
cordova.exec(decodeSignatureSuccess, decodeSignatureFail, "CodePush", "decodeSignature", [publicKey, signature]);
};
LocalPackage.prototype.finishInstall = function (deployDir, installOptions, installSuccess, installError) {
var _this = this;
@ -177,7 +257,7 @@ var LocalPackage = (function (_super) {
}
else {
LocalPackage.handleCleanDeployment(newPackageLocation, function (error) {
deployCallback(error, deployDir);
deployCallback(error, { deployDir: deployDir, isDiffUpdate: false });
});
}
});
@ -214,19 +294,19 @@ var LocalPackage = (function (_super) {
cleanDeployCallback(new Error("Could not copy new package."), null);
}
else {
FileUtil.copyDirectoryEntriesTo(unzipDir, deployDir, function (copyError) {
FileUtil.copyDirectoryEntriesTo(unzipDir, deployDir, [], function (copyError) {
if (copyError) {
cleanDeployCallback(copyError, null);
}
else {
cleanDeployCallback(null, deployDir);
cleanDeployCallback(null, { deployDir: deployDir, isDiffUpdate: false });
}
});
}
});
});
};
LocalPackage.copyCurrentPackage = function (newPackageLocation, copyCallback) {
LocalPackage.copyCurrentPackage = function (newPackageLocation, ignoreList, copyCallback) {
var handleError = function (e) {
copyCallback && copyCallback(e, null);
};
@ -249,7 +329,7 @@ var LocalPackage = (function (_super) {
}
else {
var success = function (currentPackageDirectory) {
FileUtil.copyDirectoryEntriesTo(currentPackageDirectory, deployDir, copyCallback);
FileUtil.copyDirectoryEntriesTo(currentPackageDirectory, deployDir, ignoreList, copyCallback);
};
var fail = function (fileSystemError) {
copyCallback && copyCallback(FileUtil.fileErrorToError(fileSystemError), null);
@ -270,7 +350,7 @@ var LocalPackage = (function (_super) {
var handleError = function (e) {
diffCallback(e, null);
};
LocalPackage.copyCurrentPackage(newPackageLocation, function (currentPackageError) {
LocalPackage.copyCurrentPackage(newPackageLocation, [".codepushrelease"], function (currentPackageError) {
LocalPackage.handleCleanDeployment(newPackageLocation, function (cleanDeployError) {
FileUtil.readFileEntry(diffManifest, function (error, content) {
if (error || currentPackageError || cleanDeployError) {
@ -284,7 +364,7 @@ var LocalPackage = (function (_super) {
handleError(new Error("Cannot clean up deleted manifest files."));
}
else {
diffCallback(null, deployDir);
diffCallback(null, { deployDir: deployDir, isDiffUpdate: true });
}
});
});

Просмотреть файл

@ -80,7 +80,7 @@
</platform>
<platform name="ios">
<framework src="JWT" type="podspec" spec="3.0.0-beta.4" />
<framework src="JWT" type="podspec" spec="3.0.0-beta.6" />
<source-file src="src/ios/CDVWKWebViewEngine+CodePush.m" />
<header-file src="src/ios/CodePush.h" />
<source-file src="src/ios/CodePush.m" />

Просмотреть файл

@ -18,6 +18,7 @@ import org.json.JSONException;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Array;
import java.net.MalformedURLException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
@ -37,6 +38,7 @@ public class CodePush extends CordovaPlugin {
private static final String PUBLIC_KEY_PREFERENCE = "codepushpublickey";
private static final String SERVER_URL_PREFERENCE = "codepushserverurl";
private static final String WWW_ASSET_PATH_PREFIX = "file:///android_asset/www/";
private static final String NEW_LINE = System.getProperty("line.separator");
private static boolean ShouldClearHistoryOnLoad = false;
private CordovaWebView mainWebView;
private CodePushPackageManager codePushPackageManager;
@ -90,54 +92,57 @@ public class CodePush extends CordovaPlugin {
return execRestartApplication(args, callbackContext);
} else if ("getPackageHash".equals(action)) {
return execGetPackageHash(args, callbackContext);
} else if ("verifySignature".equals(action)) {
return execVerifySignature(args, callbackContext);
} else if ("decodeSignature".equals(action)) {
return execDecodeSignature(args, callbackContext);
} else if ("getPublicKey".equals(action)) {
return execGetPublicKey(args, callbackContext);
} else {
return false;
}
}
private boolean execVerifySignature(final CordovaArgs args, final CallbackContext callbackContext) {
private boolean execGetPublicKey(final CordovaArgs args, final CallbackContext callbackContext) {
String publicKey = mainWebView.getPreferences().getString(PUBLIC_KEY_PREFERENCE, null);
callbackContext.success(publicKey);
return true;
}
private boolean execDecodeSignature(final CordovaArgs args, final CallbackContext callbackContext) {
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... voids) {
String stringPublicKey = mainWebView.getPreferences().getString(PUBLIC_KEY_PREFERENCE, null);
// bail out early if no public key was configured in config.xml
if (stringPublicKey == null) {
callbackContext.success((String) null);
return null;
}
final PublicKey publicKey;
try {
publicKey = parsePublicKey(stringPublicKey);
} catch (CodePushException e) {
callbackContext.error("Error occurred while creating the a public key" + e.getMessage());
return null;
String stringPublicKey = args.getString(0);
final PublicKey publicKey;
try {
publicKey = parsePublicKey(stringPublicKey);
} catch (CodePushException e) {
callbackContext.error("Error occurred while creating the a public key" + e.getMessage());
return null;
}
final String signature = args.getString(1);
final Map<String, Object> claims;
try {
claims = verifyAndDecodeJWT(signature, publicKey);
} catch (CodePushException e) {
callbackContext.error("The update could not be verified because it was not signed by a trusted party. " + e.getMessage());
return null;
}
final String contentHash = (String) claims.get("contentHash");
if (contentHash == null) {
callbackContext.error("The update could not be verified because the signature did not specify a content hash.");
return null;
}
callbackContext.success(contentHash);
} catch (Exception e) {
callbackContext.error("Unknown error occurred during signature decoding. " + e.getMessage());
}
final String signature = getSignature(args);
if (signature == null) {
callbackContext.error("The update could not be verified because no signature was found.");
return null;
}
final Map<String, Object> claims;
try {
claims = verifyAndDecodeJWT(signature, publicKey);
} catch (CodePushException e) {
callbackContext.error("The update could not be verified because it was not signed by a trusted party. " + e.getMessage());
return null;
}
final String contentHash = (String) claims.get("contentHash");
if (contentHash == null) {
callbackContext.error("The update could not be verified because the signature did not specify a content hash.");
return null;
}
callbackContext.success(contentHash);
return null;
}
}.execute();
@ -146,13 +151,16 @@ public class CodePush extends CordovaPlugin {
private PublicKey parsePublicKey(String stringPublicKey) throws CodePushException {
try {
stringPublicKey = stringPublicKey
.replace("-----BEGIN PUBLIC KEY-----", "")
.replace("-----END PUBLIC KEY-----", "")
.replace("&#xA;", "") //gradle automatically replaces new line to &#xA;
.replace(NEW_LINE, "");
byte[] byteKey = Base64.decode(stringPublicKey.getBytes(), Base64.DEFAULT);
X509EncodedKeySpec X509Key = new X509EncodedKeySpec(byteKey);
KeyFactory kf = KeyFactory.getInstance("RSA");
return kf.generatePublic(X509Key);
} catch (InvalidKeySpecException e) {
throw new CodePushException(e);
} catch (NoSuchAlgorithmException e) {
} catch (Exception e) {
throw new CodePushException(e);
}
}
@ -160,9 +168,11 @@ public class CodePush extends CordovaPlugin {
private Map<String, Object> verifyAndDecodeJWT(String jwt, PublicKey publicKey) throws CodePushException {
try {
SignedJWT signedJWT = SignedJWT.parse(jwt);
JWSVerifier verifier = new RSASSAVerifier((RSAPublicKey)publicKey);
JWSVerifier verifier = new RSASSAVerifier((RSAPublicKey) publicKey);
if (signedJWT.verify(verifier)) {
return signedJWT.getJWTClaimsSet().getClaims();
Map<String, Object> claims = signedJWT.getJWTClaimsSet().getClaims();
Utilities.logMessage("JWT verification succeeded, payload content: " + claims.toString());
return claims;
}
throw new CodePushException("JWT verification failed: wrong signature");
} catch (Exception e) {
@ -170,18 +180,6 @@ public class CodePush extends CordovaPlugin {
}
}
private String getSignature(CordovaArgs args) {
try {
if (args.isNull(0)) {
return null;
} else {
return args.getString(0);
}
} catch (JSONException e) {
return null;
}
}
private boolean execGetBinaryHash(final CallbackContext callbackContext) {
String cachedBinaryHash = codePushPackageManager.getCachedBinaryHash();
if (cachedBinaryHash == null) {
@ -192,11 +190,7 @@ public class CodePush extends CordovaPlugin {
String binaryHash = UpdateHashUtils.getBinaryHash(cordova.getActivity());
codePushPackageManager.saveBinaryHash(binaryHash);
callbackContext.success(binaryHash);
} catch (IOException e) {
callbackContext.error("An error occurred when trying to get the hash of the binary contents. " + e.getMessage());
} catch (NoSuchAlgorithmException e) {
callbackContext.error("An error occurred when trying to get the hash of the binary contents. " + e.getMessage());
} catch (ClassNotFoundException e) {
} catch (Exception e) {
callbackContext.error("An error occurred when trying to get the hash of the binary contents. " + e.getMessage());
}
@ -217,13 +211,7 @@ public class CodePush extends CordovaPlugin {
try {
String binaryHash = UpdateHashUtils.getHashForPath(cordova.getActivity(), args.getString(0) + "/www");
callbackContext.success(binaryHash);
} catch (IOException e) {
callbackContext.error("An error occurred when trying to get the hash of the binary contents. " + e.getMessage());
} catch (NoSuchAlgorithmException e) {
callbackContext.error("An error occurred when trying to get the hash of the binary contents. " + e.getMessage());
} catch (JSONException e) {
callbackContext.error("An error occurred when trying to get the hash of the binary contents. " + e.getMessage());
} catch (ClassNotFoundException e) {
} catch (Exception e) {
callbackContext.error("An error occurred when trying to get the hash of the binary contents. " + e.getMessage());
}

Просмотреть файл

@ -25,7 +25,8 @@ import java.util.Set;
public class UpdateHashUtils {
private static final Set<String> ignoredFiles = new HashSet<String>(Arrays.asList(
".codepushrelease",
".DS_Store"
".DS_Store",
"__MACOSX"
));
public static String getBinaryHash(Activity activity) throws IOException, NoSuchAlgorithmException, ClassNotFoundException {

Просмотреть файл

@ -81,6 +81,10 @@ public class Utilities {
Log.e(CodePush.class.getName(), "An error occured. " + e.getMessage(), e);
}
public static void logMessage(String message) {
Log.e(CodePush.class.getName(), message);
}
/**
* Getting the full path to all the assets in a given asset path.

Просмотреть файл

@ -14,7 +14,9 @@
- (void)restartApplication:(CDVInvokedUrlCommand *)command;
- (void)getBinaryHash:(CDVInvokedUrlCommand *)command;
- (void)getPackageHash:(CDVInvokedUrlCommand *)command;
- (void)verifySignature:(CDVInvokedUrlCommand *)command;
- (void)decodeSignature:(CDVInvokedUrlCommand *)command;
- (void)getPublicKey:(CDVInvokedUrlCommand *)command;
- (void)pluginInitialize;
@end
void CPLog(NSString *formatString, ...);
@end

Просмотреть файл

@ -71,26 +71,26 @@ StatusReport* rollbackStatusReport = nil;
}];
}
- (void)verifySignature:(CDVInvokedUrlCommand *)command {
- (void)getPublicKey:(CDVInvokedUrlCommand *)command {
NSString *publicKey = ((CDVViewController *) self.viewController).settings[PublicKeyPreference];
CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK
messageAsString:publicKey];
[self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
}
- (void)decodeSignature:(CDVInvokedUrlCommand *)command {
[self.commandDelegate runInBackground:^{
NSString *jwt = [command argumentAtIndex:0 withDefault:nil andClass:[NSString class]];
NSString *publicKey = ((CDVViewController *) self.viewController).settings[PublicKeyPreference];
NSString *publicKey = [command argumentAtIndex:0 withDefault:nil andClass:[NSString class]];
// bail out early if no public key was configured in config.xml
if (!publicKey) {
[self.commandDelegate sendPluginResult:[CDVPluginResult resultWithStatus:CDVCommandStatus_OK] callbackId:command.callbackId];
return;
}
// remove BEGIN / END tags and line breaks from public key string
publicKey = [publicKey stringByReplacingOccurrencesOfString:@"-----BEGIN PUBLIC KEY-----\n"
withString:@""];
publicKey = [publicKey stringByReplacingOccurrencesOfString:@"-----END PUBLIC KEY-----"
withString:@""];
publicKey = [publicKey stringByReplacingOccurrencesOfString:@"\n"
withString:@""];
// no .codepushrelease file in the update (or it couldn't be read)
if (!jwt || [jwt isEqualToString:@""]) {
[self.commandDelegate sendPluginResult:[CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR
messageAsString:@"\"Error! Public key was provided but there is no JWT signature within app bundle to verify.\n"
@"Possible reasons, why that might happen: \n"
@"You've released a CodePush bundle update using a version of CodePush CLI that does not support code signing.\n"
@"You've released a CodePush bundle update without providing the --privateKeyPath option."]
callbackId:command.callbackId];
}
NSString *jwt = [command argumentAtIndex:1 withDefault:nil andClass:[NSString class]];
id <JWTAlgorithmDataHolderProtocol> verifyDataHolder = [JWTAlgorithmRSFamilyDataHolder new]
.keyExtractorType([JWTCryptoKeyExtractor publicKeyWithPEMBase64].type)
@ -100,6 +100,7 @@ StatusReport* rollbackStatusReport = nil;
JWTCodingResultType *verifyResult = verifyBuilder.result;
CDVPluginResult *pluginResult;
if (verifyResult.successResult) {
CPLog(@"JWT signature verification succeeded, payload content: %@", verifyResult.successResult.payload);
pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK
messageAsString:verifyResult.successResult.payload[@"contentHash"]];
} else {

Просмотреть файл

@ -6,7 +6,8 @@
+ (NSArray *)ignoredFilenames {
return @[
@".codepushrelease",
@".DS_Store"
@".DS_Store",
@"__MACOSX"
];
}

Просмотреть файл

@ -24,4 +24,12 @@
return fileDate;
}
@end
void CPLog(NSString *formatString, ...) {
va_list args;
va_start(args, formatString);
NSString *prependedFormatString = [NSString stringWithFormat:@"\n[CodePush] %@", formatString];
NSLogv(prependedFormatString, args);
va_end(args);
}
@end

8
typings/codePush.d.ts поставляемый
Просмотреть файл

@ -397,4 +397,12 @@ interface IDiffManifest {
interface DownloadProgress {
totalBytes: number;
receivedBytes: number;
}
/**
* Defines the result of LocalPackage.handleDeployment execution.
*/
interface DeploymentResult {
deployDir: DirectoryEntry,
isDiffUpdate: boolean
}

Просмотреть файл

@ -115,7 +115,16 @@ class FileUtil {
FileUtil.directoryExists(cordova.file.dataDirectory, path, callback);
}
public static copyDirectoryEntriesTo(sourceDir: DirectoryEntry, destinationDir: DirectoryEntry, callback: Callback<void>): void {
public static copyDirectoryEntriesTo(sourceDir: DirectoryEntry, destinationDir: DirectoryEntry, ignoreList: string[], callback: Callback<void>): void {
/*
Native-side exception occurs while trying to copy .DS_Store and __MACOSX entries generated by macOS, so just skip them
*/
if (ignoreList.indexOf(".DS_Store") === -1){
ignoreList.push(".DS_Store");
}
if (ignoreList.indexOf("__MACOSX") === -1){
ignoreList.push("__MACOSX");
}
var fail = (error: FileError) => {
callback(FileUtil.fileErrorToError(error), null);
@ -128,10 +137,7 @@ class FileUtil {
if (i < entries.length) {
var nextEntry = entries[i++];
/* recursively call copyOne on copy success */
if (nextEntry.name === ".DS_Store" || nextEntry.name === "__MACOSX") {
/*
Native-side exception occurs while trying to copy .DS_Store and __MACOSX entries generated by macOS, so just skip them
*/
if (ignoreList.indexOf(nextEntry.name) > 0) {
copyOne();
} else {
var entryAlreadyInDestination = (destinationEntry: Entry) => {
@ -141,7 +147,7 @@ class FileUtil {
if (destinationEntry.isDirectory) {
/* directory */
FileUtil.copyDirectoryEntriesTo(<DirectoryEntry>nextEntry, <DirectoryEntry>destinationEntry, (error: Error) => {
FileUtil.copyDirectoryEntriesTo(<DirectoryEntry>nextEntry, <DirectoryEntry>destinationEntry, ignoreList, (error: Error) => {
if (error) {
callback(error, null);
} else {

Просмотреть файл

@ -66,23 +66,25 @@ class LocalPackage extends Package implements ILocalPackage {
var newPackageLocation = LocalPackage.VersionsDir + "/" + this.packageHash;
var signatureVerified = (deployDir: DirectoryEntry) => {
this.localPath = deployDir.fullPath;
this.finishInstall(deployDir, installOptions, installSuccess, installError);
};
var donePackageFileCopy = (deployDir: DirectoryEntry) => {
this.verifyPackage(deployDir, installError, CodePushUtil.getNodeStyleCallbackFor<DirectoryEntry>(signatureVerified, installError))
};
var newPackageUnzipped = function (unzipError: Error) {
if (unzipError) {
installError && installError(new Error("Could not unzip package" + CodePushUtil.getErrorMessage(unzipError)));
} else {
LocalPackage.handleDeployment(newPackageLocation, CodePushUtil.getNodeStyleCallbackFor<DirectoryEntry>(donePackageFileCopy, installError));
LocalPackage.handleDeployment(newPackageLocation, CodePushUtil.getNodeStyleCallbackFor<DeploymentResult>(donePackageFileCopy, installError));
}
};
var donePackageFileCopy = (deploymentResult: DeploymentResult) => {
this.verifyPackage(deploymentResult, installError, () => {
packageVerified(deploymentResult.deployDir);
});
};
var packageVerified = (deployDir: DirectoryEntry) => {
this.localPath = deployDir.fullPath;
this.finishInstall(deployDir, installOptions, installSuccess, installError);
};
FileUtil.getDataDirectory(LocalPackage.DownloadUnzipDir, false, (error: Error, directoryEntry: DirectoryEntry) => {
var unzipPackage = () => {
FileUtil.getDataDirectory(LocalPackage.DownloadUnzipDir, true, (innerError: Error, unzipDir: DirectoryEntry) => {
@ -112,54 +114,150 @@ class LocalPackage extends Package implements ILocalPackage {
}
}
private verifyPackage(unzipDir: DirectoryEntry, installError: ErrorCallback, callback: Callback<DirectoryEntry>): void {
var packageHashSuccess = (localHash: string) => {
CodePushUtil.logMessage("Expected hash: " + this.packageHash + ", actual hash: " + localHash);
FileUtil.readFile(cordova.file.dataDirectory, unzipDir.fullPath + '/www', '.codepushrelease', (error, contents) => {
var verifySignatureSuccess = (expectedHash?: string) => {
// first, we always compare the hash we just calculated to the packageHash reported from the server
if (localHash !== this.packageHash) {
installError(new Error("package hash verification failed"));
return;
private verifyPackage(deploymentResult: DeploymentResult, installError: ErrorCallback, successCallback: SuccessCallback<void>): void {
var deployDir = deploymentResult.deployDir;
var verificationFail: ErrorCallback = (error: Error) => {
installError && installError(error);
};
var verify = (isSignatureVerificationEnabled: boolean, isSignatureAppearedInBundle: boolean, publicKey: string, signature: string) => {
if (isSignatureVerificationEnabled) {
if (isSignatureAppearedInBundle) {
this.verifyHash(deployDir, this.packageHash, verificationFail, () => {
this.verifySignature(deployDir, this.packageHash, publicKey, signature, verificationFail, successCallback);
});
} else {
var errorMessage =
"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.";
installError && installError(new Error(errorMessage));
}
} else {
if (isSignatureAppearedInBundle) {
CodePushUtil.logMessage(
"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."
);
//verifyHash
this.verifyHash(deployDir, this.packageHash, verificationFail, successCallback);
} else {
if (deploymentResult.isDiffUpdate){
//verifyHash
this.verifyHash(deployDir, this.packageHash, verificationFail, successCallback);
}
successCallback();
}
}
}
// this happens if (and only if) no public key is available in config.xml
// -> no code signing
if (!expectedHash) {
CodePushUtil.logMessage("The update contents succeeded the data integrity check.");
callback(null, unzipDir);
if (deploymentResult.isDiffUpdate){
CodePushUtil.logMessage("Applying diff update");
} else {
CodePushUtil.logMessage("Applying full update");
}
// .codepushrelease was read but there is no public key in config.xml
if (contents != null) {
CodePushUtil.logMessage("Warning! JWT signature exists in codepush update but code integrity check couldn't be performed because there is no public key configured. \n" +
"Please ensure that a public key is properly configured within your application.");
}
return;
}
var isSignatureVerificationEnabled: boolean, isSignatureAppearedInBundle: boolean;
var publicKey: string;
// code signing is active, only proceed if the locally computed hash is the same as the one decoded from the JWT
if (localHash === expectedHash) {
CodePushUtil.logMessage("The update contents succeeded the code signing check.");
callback(null, unzipDir);
return;
}
this.getPublicKey((error, publicKeyResult) => {
if (error) {
installError && installError(new Error("Error reading public key. " + error));
return;
}
installError(new Error("The update contents failed the code signing check."));
};
var verifySignatureFail = (error: string) => {
installError && installError(new Error("The update contents failed the code signing check. " + error));
};
CodePushUtil.logMessage("Verifying signature for folder path: " + unzipDir.fullPath);
cordova.exec(verifySignatureSuccess, verifySignatureFail, "CodePush", "verifySignature", [contents]);
publicKey = publicKeyResult;
isSignatureVerificationEnabled = (publicKey !== null);
this.getSignatureFromUpdate(deploymentResult.deployDir, (error, signature) => {
if (error) {
installError && installError(new Error("Error reading signature from update. " + error));
return;
}
isSignatureAppearedInBundle = (signature !== null);
verify(isSignatureVerificationEnabled, isSignatureAppearedInBundle, publicKey, signature);
});
};
var packageHashFail = (error: string) => {
installError && installError(new Error("unable to compute hash for package: " + error));
};
CodePushUtil.logMessage("Verifying hash for folder path: " + unzipDir.fullPath);
cordova.exec(packageHashSuccess, packageHashFail,"CodePush","getPackageHash",[unzipDir.fullPath]);
});
}
private getPublicKey(callback: Callback<string>) {
var success = (publicKey: string) => {
callback(null, publicKey);
}
var fail = (error: Error) => {
callback(error, null);
}
cordova.exec(success, fail,"CodePush","getPublicKey",[]);
}
private getSignatureFromUpdate(deployDir: DirectoryEntry, callback: Callback<string>){
var rootUri = cordova.file.dataDirectory;
var path = deployDir.fullPath + '/www';
var fileName = '.codepushrelease';
FileUtil.fileExists(rootUri, path, fileName, (error, result) => {
if (!result) {
// signature absents in the bundle
callback(null, null);
return;
}
FileUtil.readFile(rootUri, path, fileName, (error, signature) => {
if (error) {
//error reading signature file from bundle
callback(error, null);
return;
}
callback(null, signature);
});
});
}
private verifyHash(deployDir: DirectoryEntry, newUpdateHash: string, errorCallback: ErrorCallback, successCallback: SuccessCallback<void>){
var packageHashSuccess = (computedHash: string) => {
if (computedHash !== newUpdateHash) {
errorCallback(new Error("The update contents failed the data integrity check."));
return;
}
CodePushUtil.logMessage("The update contents succeeded the data integrity check.");
successCallback();
}
var packageHashFail = (error: Error) => {
errorCallback(new Error("Unable to compute hash for package: " + error));
}
CodePushUtil.logMessage("Verifying hash for folder path: " + deployDir.fullPath);
cordova.exec(packageHashSuccess, packageHashFail, "CodePush", "getPackageHash", [deployDir.fullPath]);
}
private verifySignature(deployDir: DirectoryEntry, newUpdateHash: string, publicKey: string, signature: string, errorCallback: ErrorCallback, successCallback: SuccessCallback<void>){
var decodeSignatureSuccess = (contentHash: string) => {
if (contentHash !== newUpdateHash) {
errorCallback(new Error("The update contents failed the code signing check."));
return;
}
CodePushUtil.logMessage("The update contents succeeded the code signing check.");
successCallback();
}
var decodeSignatureFail = (error: Error) => {
errorCallback(new Error("Unable to verify signature for package: " + error));
}
CodePushUtil.logMessage("Verifying signature for folder path: " + deployDir.fullPath);
cordova.exec(decodeSignatureSuccess, decodeSignatureFail, "CodePush", "decodeSignature", [publicKey, signature]);
}
private finishInstall(deployDir: DirectoryEntry, installOptions: InstallOptions, installSuccess: SuccessCallback<InstallMode>, installError: ErrorCallback): void {
function backupPackageInformationFileIfNeeded(backupIfNeededDone: Callback<void>) {
NativeAppInfo.isPendingUpdate((pendingUpdate: boolean) => {
@ -212,7 +310,7 @@ class LocalPackage extends Package implements ILocalPackage {
}, installError);
}
private static handleDeployment(newPackageLocation: string, deployCallback: Callback<DirectoryEntry>): void {
private static handleDeployment(newPackageLocation: string, deployCallback: Callback<DeploymentResult>): void {
FileUtil.getDataDirectory(newPackageLocation, true, (deployDirError: Error, deployDir: DirectoryEntry) => {
// check for diff manifest
FileUtil.getDataFile(LocalPackage.DownloadUnzipDir, LocalPackage.DiffManifestFile, false, (manifestError: Error, diffManifest: FileEntry) => {
@ -220,7 +318,7 @@ class LocalPackage extends Package implements ILocalPackage {
LocalPackage.handleDiffDeployment(newPackageLocation, diffManifest, deployCallback);
} else {
LocalPackage.handleCleanDeployment(newPackageLocation, (error: Error) => {
deployCallback(error, deployDir);
deployCallback(error, {deployDir, isDiffUpdate: false});
});
}
});
@ -253,18 +351,18 @@ class LocalPackage extends Package implements ILocalPackage {
});
}
private static handleCleanDeployment(newPackageLocation: string, cleanDeployCallback: Callback<DirectoryEntry>): void {
private static handleCleanDeployment(newPackageLocation: string, cleanDeployCallback: Callback<DeploymentResult>): void {
// no diff manifest
FileUtil.getDataDirectory(newPackageLocation, true, (deployDirError: Error, deployDir: DirectoryEntry) => {
FileUtil.getDataDirectory(LocalPackage.DownloadUnzipDir, false, (unzipDirErr: Error, unzipDir: DirectoryEntry) => {
if (unzipDirErr || deployDirError) {
cleanDeployCallback(new Error("Could not copy new package."), null);
} else {
FileUtil.copyDirectoryEntriesTo(unzipDir, deployDir, (copyError: Error) => {
FileUtil.copyDirectoryEntriesTo(unzipDir, deployDir, [/*no need to ignore copy anything*/], (copyError: Error) => {
if (copyError) {
cleanDeployCallback(copyError, null);
} else {
cleanDeployCallback(null, deployDir);
cleanDeployCallback(null, {deployDir, isDiffUpdate: false});
}
});
}
@ -272,7 +370,7 @@ class LocalPackage extends Package implements ILocalPackage {
});
}
private static copyCurrentPackage(newPackageLocation: string, copyCallback: Callback<void>): void {
private static copyCurrentPackage(newPackageLocation: string, ignoreList: string[], copyCallback: Callback<void>): void {
var handleError = (e: Error) => {
copyCallback && copyCallback(e, null);
};
@ -296,7 +394,7 @@ class LocalPackage extends Package implements ILocalPackage {
handleError(new Error("Could not acquire the source/destination folders. "));
} else {
var success = (currentPackageDirectory: DirectoryEntry) => {
FileUtil.copyDirectoryEntriesTo(currentPackageDirectory, deployDir, copyCallback);
FileUtil.copyDirectoryEntriesTo(currentPackageDirectory, deployDir, ignoreList, copyCallback);
};
var fail = (fileSystemError: FileError) => {
@ -319,13 +417,13 @@ class LocalPackage extends Package implements ILocalPackage {
LocalPackage.getPackage(LocalPackage.PackageInfoFile, packageSuccess, packageFailure);
}
private static handleDiffDeployment(newPackageLocation: string, diffManifest: FileEntry, diffCallback: Callback<DirectoryEntry>): void {
private static handleDiffDeployment(newPackageLocation: string, diffManifest: FileEntry, diffCallback: Callback<DeploymentResult>): void {
var handleError = (e: Error) => {
diffCallback(e, null);
};
/* copy old files */
LocalPackage.copyCurrentPackage(newPackageLocation, (currentPackageError: Error) => {
/* copy old files except signature file */
LocalPackage.copyCurrentPackage(newPackageLocation, [".codepushrelease"], (currentPackageError: Error) => {
/* copy new files */
LocalPackage.handleCleanDeployment(newPackageLocation, (cleanDeployError: Error) => {
/* delete files mentioned in the manifest */
@ -339,7 +437,7 @@ class LocalPackage extends Package implements ILocalPackage {
if (deleteError || deployDirError) {
handleError(new Error("Cannot clean up deleted manifest files."));
} else {
diffCallback(null, deployDir);
diffCallback(null, {deployDir, isDiffUpdate: true});
}
});
});