Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1909,9 +1909,44 @@ private void bindBackupButtonLogic(MainActivity mainAct, File backupsDir, File i
String tarBin = staticTar.exists() ? staticTar.getAbsolutePath() : "tar";
String gzipBin = staticGzip.exists() ? staticGzip.getAbsolutePath() : "gzip";

// Stamp an identity manifest into the backup so a re-import is
// recognized (kind/arch) AND explicitly declares it carries NO
// integrity checksum (origin=device-backup) — we do NOT turn the
// phone into a builder. It is staged in a temp tree and packed
// FIRST (a second `-C`) so RootfsArchiveValidator reads it from
// the first tar header without decompressing the whole archive.
// See docs/ROOTFS_MANIFEST.md.
String manifestArg = null;
File mfStageRoot = new File(requireContext().getCacheDir(), "mfstage");
try {
if (mfStageRoot.exists()) {
ProcessRunner.run(new String[]{"rm", "-rf", mfStageRoot.getAbsolutePath()});
}
File iiabStage = new File(mfStageRoot, "installed-rootfs/iiab");
if (iiabStage.mkdirs()) {
String appAbi = org.iiab.controller.deploy.data.RootfsManifest.appAbiId();
String debArch = appAbi.contains("64") ? "arm64" : "armhf";
String built = String.format(java.util.Locale.US, "%04d.%03d", year, dayOfYear);
String identityJson = "{\"schema\":1,\"kind\":\"iiab-rootfs\",\"arch\":\""
+ appAbi + "\",\"deb_arch\":\"" + debArch + "\",\"built\":\""
+ built + "\",\"builder\":\"knowledgetogo-app\",\"origin\":\"device-backup\"}";
java.io.FileOutputStream mfo =
new java.io.FileOutputStream(new File(iiabStage, ".iiab-rootfs.json"));
mfo.write(identityJson.getBytes("UTF-8"));
mfo.close();
manifestArg = "-C '" + mfStageRoot.getAbsolutePath()
+ "' 'installed-rootfs/iiab/.iiab-rootfs.json' ";
}
} catch (Exception mfe) {
Log.w(TAG, "Could not stage identity manifest for backup: " + mfe.getMessage());
manifestArg = null;
}

// D11: single-quote the interpolated paths so the backup pipe is robust
// even if a path ever contains spaces/metacharacters (app-internal today).
String cmd = "'" + tarBin + "' -cf - -C '" + iiabRootDir.getAbsolutePath()
String cmd = "'" + tarBin + "' -cf - "
+ (manifestArg != null ? manifestArg : "")
+ "-C '" + iiabRootDir.getAbsolutePath()
+ "' installed-rootfs | '" + gzipBin + "' > '" + backupFile.getAbsolutePath() + "'";
// D12: ProcessRunner drains stderr so a large backup with tar warnings
// cannot deadlock on a full pipe buffer.
Expand Down Expand Up @@ -2284,11 +2319,18 @@ private void importBackupSafely(Uri sourceUri) {
vr == org.iiab.controller.deploy.data.RootfsArchiveValidator.Result.OK;
boolean okNoManifest =
vr == org.iiab.controller.deploy.data.RootfsArchiveValidator.Result.OK_NO_MANIFEST;
if (!okValidated && !okNoManifest) {
boolean okNoChecksum =
vr == org.iiab.controller.deploy.data.RootfsArchiveValidator.Result.OK_NO_CHECKSUM;
if (!okValidated && !okNoManifest && !okNoChecksum) {
if (destFile.exists()) destFile.delete();
final int errMsg = (vr == org.iiab.controller.deploy.data.RootfsArchiveValidator.Result.WRONG_ARCH)
? R.string.install_error_wrong_arch
: R.string.install_error_not_rootfs;
final int errMsg;
if (vr == org.iiab.controller.deploy.data.RootfsArchiveValidator.Result.WRONG_ARCH) {
errMsg = R.string.install_error_wrong_arch;
} else if (vr == org.iiab.controller.deploy.data.RootfsArchiveValidator.Result.CORRUPT) {
errMsg = R.string.install_error_corrupt;
} else {
errMsg = R.string.install_error_not_rootfs;
}
if (getActivity() != null) {
getActivity().runOnUiThread(() -> {
isImporting = false;
Expand All @@ -2306,6 +2348,11 @@ private void importBackupSafely(Uri sourceUri) {
getActivity().runOnUiThread(() ->
Snackbar.make(getView(), R.string.install_warn_manifest_missing, Snackbar.LENGTH_LONG).show());
}
// Transparency: an app-made (device) backup carries no integrity checksum.
if (okNoChecksum && getActivity() != null) {
getActivity().runOnUiThread(() ->
Snackbar.make(getView(), R.string.install_warn_no_checksum, Snackbar.LENGTH_LONG).show());
}

if (getActivity() != null) {
getActivity().runOnUiThread(() -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public final class RootfsArchiveValidator {

private static final String TAG = "IIAB-RootfsValidator";

public enum Result { OK, OK_NO_MANIFEST, NOT_A_ROOTFS, WRONG_ARCH, UNREADABLE }
public enum Result { OK, OK_NO_MANIFEST, OK_NO_CHECKSUM, NOT_A_ROOTFS, WRONG_ARCH, CORRUPT, UNREADABLE }

private RootfsArchiveValidator() {
// Static utility; not instantiable.
Expand All @@ -62,7 +62,7 @@ public static Result validate(Context context, String archivePath) {
if (entries.isEmpty()) {
return Result.UNREADABLE;
}
return validateWithEntries(context, archivePath, isGzip, tarBinary, entries);
return validateWithEntries(context, archivePath, isGzip, tarBinary, entries, true);
} catch (Exception e) {
Log.e(TAG, "Validation error", e);
return Result.UNREADABLE;
Expand All @@ -75,6 +75,19 @@ public static Result validate(Context context, String archivePath) {
*/
public static Result validateWithEntries(Context context, String archivePath,
boolean isGzip, String tarBinary, List<String> entries) {
// Restore re-uses the listing for the D11 guard; integrity was already
// checked at import time, so don't pay a second full pass here.
return validateWithEntries(context, archivePath, isGzip, tarBinary, entries, false);
}

/**
* @param checkIntegrity when true (the import gate) and the rootfs is not an
* app-made backup, recompute the embedded iiab-tree-sha256-v1 treehash
* and fail closed ({@link Result#CORRUPT}) on a mismatch.
*/
public static Result validateWithEntries(Context context, String archivePath,
boolean isGzip, String tarBinary,
List<String> entries, boolean checkIntegrity) {
try {
// Authoritative path: the build/app embeds an identity manifest
// (installed-rootfs/iiab/.iiab-rootfs.json, packed first). See
Expand All @@ -88,7 +101,28 @@ public static Result validateWithEntries(Context context, String archivePath,
&& !id.arch.equals(RootfsManifest.appAbiId())) {
return Result.WRONG_ARCH;
}
return Result.OK; // manifest-validated; no need to probe ELF
// Identity is authoritative for kind+arch. Now decide integrity.
if ("device-backup".equals(id.origin)) {
// App-made backup: no checksum by design (we don't turn the
// phone into a builder). Cheap signal from the first header.
return Result.OK_NO_CHECKSUM;
}
if (!checkIntegrity) {
return Result.OK; // restore: already verified at import
}
RootfsIntegrity.Result ir = RootfsIntegrity.verify(archivePath);
switch (ir.status) {
case MATCH:
case ABSENT: // builder rootfs without integrity yet (soft phase)
return Result.OK;
case DECLARED_NONE:
return Result.OK_NO_CHECKSUM;
case MISMATCH:
case ERROR:
default:
Log.w(TAG, "Integrity check failed (" + ir.status + ") for " + archivePath);
return Result.CORRUPT;
}
}

// Soft fallback (no manifest): legacy ELF/structure heuristic. We
Expand Down
Loading
Loading