Skip to content

Commit

Permalink
Validate dynamic patches before attempting to install (flutter#7496)
Browse files Browse the repository at this point in the history
This change adds explicit validation of dynamic patches in all places
where they're used, instead of only validating it in some places which
wasn't as reliable because some of the code paths were missed.

This change also moves utility functions that deal with validating
patches from ResourceExtractor to ResourceUpdater, to make them
available as API for other places in code that need this validation.
  • Loading branch information
sbaranov authored Jan 16, 2019
1 parent 5983e34 commit 5401803
Show file tree
Hide file tree
Showing 3 changed files with 119 additions and 110 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
import io.flutter.view.FlutterNativeView;
import io.flutter.view.FlutterRunArguments;
import io.flutter.view.FlutterView;
import io.flutter.view.ResourceUpdater;
import org.json.JSONObject;

import java.io.File;
import java.util.ArrayList;
Expand Down Expand Up @@ -344,9 +346,13 @@ private void runBundle(String appBundlePath) {
if (!flutterView.getFlutterNativeView().isApplicationRunning()) {
FlutterRunArguments args = new FlutterRunArguments();
ArrayList<String> bundlePaths = new ArrayList<>();
if (FlutterMain.getResourceUpdater() != null) {
File patchFile = FlutterMain.getResourceUpdater().getInstalledPatch();
bundlePaths.add(patchFile.getPath());
ResourceUpdater resourceUpdater = FlutterMain.getResourceUpdater();
if (resourceUpdater != null) {
File patchFile = resourceUpdater.getInstalledPatch();
JSONObject manifest = resourceUpdater.readManifest(patchFile);
if (resourceUpdater.validateManifest(manifest)) {
bundlePaths.add(patchFile.getPath());
}
}
bundlePaths.add(appBundlePath);
args.bundlePaths = bundlePaths.toArray(new String[0]);
Expand Down
136 changes: 30 additions & 106 deletions shell/platform/android/io/flutter/view/ResourceExtractor.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,13 @@
import android.os.Build;
import android.util.Log;
import io.flutter.util.PathUtils;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.*;
import java.util.Collection;
import java.util.HashSet;
import java.util.Scanner;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.zip.CRC32;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

Expand Down Expand Up @@ -63,34 +60,30 @@ protected Void doInBackground(Void... unused) {
File activeFile = resourceUpdater.getInstalledPatch();

if (updateFile.exists()) {
// Graduate patch file as active for asset manager.
if (activeFile.exists() && !activeFile.delete()) {
Log.w(TAG, "Could not delete file " + activeFile);
return null;
}
if (!updateFile.renameTo(activeFile)) {
Log.w(TAG, "Could not create file " + activeFile);
return null;
JSONObject manifest = resourceUpdater.readManifest(updateFile);
if (resourceUpdater.validateManifest(manifest)) {
// Graduate patch file as active for asset manager.
if (activeFile.exists() && !activeFile.delete()) {
Log.w(TAG, "Could not delete file " + activeFile);
return null;
}
if (!updateFile.renameTo(activeFile)) {
Log.w(TAG, "Could not create file " + activeFile);
return null;
}
}
}
}

JSONObject updateManifest = readUpdateManifest();
if (!validateUpdateManifest(updateManifest)) {
updateManifest = null;
}

final String timestamp = checkTimestamp(dataDir, updateManifest);
final String timestamp = checkTimestamp(dataDir);
if (timestamp == null) {
return null;
}

deleteFiles();

if (updateManifest != null) {
if (!extractUpdate(dataDir)) {
return null;
}
if (!extractUpdate(dataDir)) {
return null;
}

if (!extractAPK(dataDir)) {
Expand Down Expand Up @@ -236,6 +229,12 @@ private boolean extractUpdate(File dataDir) {
return true;
}

JSONObject manifest = resourceUpdater.readManifest(updateFile);
if (!resourceUpdater.validateManifest(manifest)) {
// Obsolete patch file, nothing to install.
return true;
}

ZipFile zipFile;
try {
zipFile = new ZipFile(updateFile);
Expand Down Expand Up @@ -290,8 +289,7 @@ private boolean extractUpdate(File dataDir) {

// Returns null if extracted resources are found and match the current APK version
// and update version if any, otherwise returns the current APK and update version.
private String checkTimestamp(File dataDir, JSONObject updateManifest) {

private String checkTimestamp(File dataDir) {
PackageManager packageManager = mContext.getPackageManager();
PackageInfo packageInfo = null;

Expand All @@ -308,24 +306,16 @@ private String checkTimestamp(File dataDir, JSONObject updateManifest) {
String expectedTimestamp =
TIMESTAMP_PREFIX + getVersionCode(packageInfo) + "-" + packageInfo.lastUpdateTime;

if (updateManifest != null) {
String buildNumber = updateManifest.optString("buildNumber", null);
if (buildNumber == null) {
Log.w(TAG, "Invalid update manifest: buildNumber");
} else {
String patchNumber = updateManifest.optString("patchNumber", null);
if (!buildNumber.equals(Long.toString(getVersionCode(packageInfo)))) {
Log.w(TAG, "Outdated update file for " + getVersionCode(packageInfo));
ResourceUpdater resourceUpdater = FlutterMain.getResourceUpdater();
if (resourceUpdater != null) {
File patchFile = resourceUpdater.getInstalledPatch();
JSONObject manifest = resourceUpdater.readManifest(patchFile);
if (resourceUpdater.validateManifest(manifest)) {
String patchNumber = manifest.optString("patchNumber", null);
if (patchNumber != null) {
expectedTimestamp += "-" + patchNumber + "-" + patchFile.lastModified();
} else {
ResourceUpdater resourceUpdater = FlutterMain.getResourceUpdater();
assert resourceUpdater != null;
File patchFile = resourceUpdater.getInstalledPatch();
assert patchFile.exists();
if (patchNumber != null) {
expectedTimestamp += "-" + patchNumber + "-" + patchFile.lastModified();
} else {
expectedTimestamp += "-" + patchFile.lastModified();
}
expectedTimestamp += "-" + patchFile.lastModified();
}
}
}
Expand All @@ -349,70 +339,4 @@ private String checkTimestamp(File dataDir, JSONObject updateManifest) {

return null;
}

/// Returns true if the downloaded update file was indeed built for this APK.
private boolean validateUpdateManifest(JSONObject updateManifest) {
if (updateManifest == null) {
return false;
}

String baselineChecksum = updateManifest.optString("baselineChecksum", null);
if (baselineChecksum == null) {
Log.w(TAG, "Invalid update manifest: baselineChecksum");
return false;
}

final AssetManager manager = mContext.getResources().getAssets();
try (InputStream is = manager.open("flutter_assets/isolate_snapshot_data")) {
CRC32 checksum = new CRC32();

int count = 0;
byte[] buffer = new byte[BUFFER_SIZE];
while ((count = is.read(buffer, 0, BUFFER_SIZE)) != -1) {
checksum.update(buffer, 0, count);
}

if (!baselineChecksum.equals(String.valueOf(checksum.getValue()))) {
Log.w(TAG, "Mismatched update file for APK");
return false;
}

return true;

} catch (IOException e) {
Log.w(TAG, "Could not read APK: " + e);
return false;
}
}

/// Returns null if no update manifest is found.
private JSONObject readUpdateManifest() {
ResourceUpdater resourceUpdater = FlutterMain.getResourceUpdater();
if (resourceUpdater == null) {
return null;
}

File updateFile = resourceUpdater.getInstalledPatch();
if (!updateFile.exists()) {
return null;
}

try {
ZipFile zipFile = new ZipFile(updateFile);
ZipEntry entry = zipFile.getEntry("manifest.json");
if (entry == null) {
Log.w(TAG, "Invalid update file: " + updateFile);
return null;
}

// Read and parse the entire JSON file as single operation.
Scanner scanner = new Scanner(zipFile.getInputStream(entry));
return new JSONObject(scanner.useDelimiter("\\A").next());

} catch (IOException | JSONException e) {
Log.w(TAG, "Invalid update file: " + e);
return null;

}
}
}
81 changes: 80 additions & 1 deletion shell/platform/android/io/flutter/view/ResourceUpdater.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.res.AssetManager;
import android.os.AsyncTask;
import android.os.Bundle;
import android.util.Log;
Expand All @@ -21,14 +22,22 @@
import java.net.URISyntaxException;
import java.net.URL;
import java.util.Date;
import java.util.Scanner;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.zip.CRC32;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import org.json.JSONException;
import org.json.JSONObject;

public final class ResourceUpdater {
private static final String TAG = "ResourceUpdater";

private static final int BUFFER_SIZE = 16 * 1024;

// Controls when to check if a new patch is available for download, and start downloading.
// Note that by default the application will not block to wait for the download to finish.
// Patches are downloaded in the background, but the developer can also use [InstallMode]
Expand Down Expand Up @@ -111,7 +120,7 @@ protected Void doInBackground(String... unused) {
Log.i(TAG, "HTTP response code " + responseCode);

if (responseCode == HttpURLConnection.HTTP_NOT_FOUND) {
Log.i(TAG, "Latest update not found");
Log.i(TAG, "Latest update not found on server");
return null;
}

Expand Down Expand Up @@ -262,6 +271,76 @@ InstallMode getInstallMode() {
}
}

/// Returns manifest JSON from ZIP file, or null if not found.
public JSONObject readManifest(File updateFile) {
if (!updateFile.exists()) {
return null;
}

try {
ZipFile zipFile = new ZipFile(updateFile);
ZipEntry entry = zipFile.getEntry("manifest.json");
if (entry == null) {
Log.w(TAG, "Invalid update file: " + updateFile);
return null;
}

// Read and parse the entire JSON file as single operation.
Scanner scanner = new Scanner(zipFile.getInputStream(entry));
return new JSONObject(scanner.useDelimiter("\\A").next());

} catch (IOException | JSONException e) {
Log.w(TAG, "Invalid update file: " + e);
return null;
}
}

/// Returns true if the patch file was indeed built for this APK.
public boolean validateManifest(JSONObject manifest) {
if (manifest == null) {
return false;
}

String buildNumber = manifest.optString("buildNumber", null);
if (buildNumber == null) {
Log.w(TAG, "Invalid update manifest: missing buildNumber");
return false;
}

if (!buildNumber.equals(getAPKVersion())) {
Log.w(TAG, "Outdated update file for build " + getAPKVersion());
return false;
}

String baselineChecksum = manifest.optString("baselineChecksum", null);
if (baselineChecksum == null) {
Log.w(TAG, "Invalid update manifest: missing baselineChecksum");
return false;
}

final AssetManager manager = context.getResources().getAssets();
try (InputStream is = manager.open("flutter_assets/isolate_snapshot_data")) {
CRC32 checksum = new CRC32();

int count = 0;
byte[] buffer = new byte[BUFFER_SIZE];
while ((count = is.read(buffer, 0, BUFFER_SIZE)) != -1) {
checksum.update(buffer, 0, count);
}

if (!baselineChecksum.equals(String.valueOf(checksum.getValue()))) {
Log.w(TAG, "Mismatched update file for APK");
return false;
}

return true;

} catch (IOException e) {
Log.w(TAG, "Could not read APK: " + e);
return false;
}
}

void startUpdateDownloadOnce() {
if (downloadTask != null ) {
return;
Expand Down

0 comments on commit 5401803

Please sign in to comment.