Skip to content

Commit 8a59ee8

Browse files
Merge pull request #37 from appdevforall/feat/rootfs-import-restore-integrity
feat(controller): rootfs integrity verification (iiab-tree-sha256-v1) + device-backup manifest
2 parents abd8635 + a894e49 commit 8a59ee8

15 files changed

Lines changed: 906 additions & 12 deletions

File tree

‎controller/app/src/main/java/org/iiab/controller/DeployFragment.java‎

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1909,9 +1909,44 @@ private void bindBackupButtonLogic(MainActivity mainAct, File backupsDir, File i
19091909
String tarBin = staticTar.exists() ? staticTar.getAbsolutePath() : "tar";
19101910
String gzipBin = staticGzip.exists() ? staticGzip.getAbsolutePath() : "gzip";
19111911

1912+
// Stamp an identity manifest into the backup so a re-import is
1913+
// recognized (kind/arch) AND explicitly declares it carries NO
1914+
// integrity checksum (origin=device-backup) — we do NOT turn the
1915+
// phone into a builder. It is staged in a temp tree and packed
1916+
// FIRST (a second `-C`) so RootfsArchiveValidator reads it from
1917+
// the first tar header without decompressing the whole archive.
1918+
// See docs/ROOTFS_MANIFEST.md.
1919+
String manifestArg = null;
1920+
File mfStageRoot = new File(requireContext().getCacheDir(), "mfstage");
1921+
try {
1922+
if (mfStageRoot.exists()) {
1923+
ProcessRunner.run(new String[]{"rm", "-rf", mfStageRoot.getAbsolutePath()});
1924+
}
1925+
File iiabStage = new File(mfStageRoot, "installed-rootfs/iiab");
1926+
if (iiabStage.mkdirs()) {
1927+
String appAbi = org.iiab.controller.deploy.data.RootfsManifest.appAbiId();
1928+
String debArch = appAbi.contains("64") ? "arm64" : "armhf";
1929+
String built = String.format(java.util.Locale.US, "%04d.%03d", year, dayOfYear);
1930+
String identityJson = "{\"schema\":1,\"kind\":\"iiab-rootfs\",\"arch\":\""
1931+
+ appAbi + "\",\"deb_arch\":\"" + debArch + "\",\"built\":\""
1932+
+ built + "\",\"builder\":\"knowledgetogo-app\",\"origin\":\"device-backup\"}";
1933+
java.io.FileOutputStream mfo =
1934+
new java.io.FileOutputStream(new File(iiabStage, ".iiab-rootfs.json"));
1935+
mfo.write(identityJson.getBytes("UTF-8"));
1936+
mfo.close();
1937+
manifestArg = "-C '" + mfStageRoot.getAbsolutePath()
1938+
+ "' 'installed-rootfs/iiab/.iiab-rootfs.json' ";
1939+
}
1940+
} catch (Exception mfe) {
1941+
Log.w(TAG, "Could not stage identity manifest for backup: " + mfe.getMessage());
1942+
manifestArg = null;
1943+
}
1944+
19121945
// D11: single-quote the interpolated paths so the backup pipe is robust
19131946
// even if a path ever contains spaces/metacharacters (app-internal today).
1914-
String cmd = "'" + tarBin + "' -cf - -C '" + iiabRootDir.getAbsolutePath()
1947+
String cmd = "'" + tarBin + "' -cf - "
1948+
+ (manifestArg != null ? manifestArg : "")
1949+
+ "-C '" + iiabRootDir.getAbsolutePath()
19151950
+ "' installed-rootfs | '" + gzipBin + "' > '" + backupFile.getAbsolutePath() + "'";
19161951
// D12: ProcessRunner drains stderr so a large backup with tar warnings
19171952
// cannot deadlock on a full pipe buffer.
@@ -2284,11 +2319,18 @@ private void importBackupSafely(Uri sourceUri) {
22842319
vr == org.iiab.controller.deploy.data.RootfsArchiveValidator.Result.OK;
22852320
boolean okNoManifest =
22862321
vr == org.iiab.controller.deploy.data.RootfsArchiveValidator.Result.OK_NO_MANIFEST;
2287-
if (!okValidated && !okNoManifest) {
2322+
boolean okNoChecksum =
2323+
vr == org.iiab.controller.deploy.data.RootfsArchiveValidator.Result.OK_NO_CHECKSUM;
2324+
if (!okValidated && !okNoManifest && !okNoChecksum) {
22882325
if (destFile.exists()) destFile.delete();
2289-
final int errMsg = (vr == org.iiab.controller.deploy.data.RootfsArchiveValidator.Result.WRONG_ARCH)
2290-
? R.string.install_error_wrong_arch
2291-
: R.string.install_error_not_rootfs;
2326+
final int errMsg;
2327+
if (vr == org.iiab.controller.deploy.data.RootfsArchiveValidator.Result.WRONG_ARCH) {
2328+
errMsg = R.string.install_error_wrong_arch;
2329+
} else if (vr == org.iiab.controller.deploy.data.RootfsArchiveValidator.Result.CORRUPT) {
2330+
errMsg = R.string.install_error_corrupt;
2331+
} else {
2332+
errMsg = R.string.install_error_not_rootfs;
2333+
}
22922334
if (getActivity() != null) {
22932335
getActivity().runOnUiThread(() -> {
22942336
isImporting = false;
@@ -2306,6 +2348,11 @@ private void importBackupSafely(Uri sourceUri) {
23062348
getActivity().runOnUiThread(() ->
23072349
Snackbar.make(getView(), R.string.install_warn_manifest_missing, Snackbar.LENGTH_LONG).show());
23082350
}
2351+
// Transparency: an app-made (device) backup carries no integrity checksum.
2352+
if (okNoChecksum && getActivity() != null) {
2353+
getActivity().runOnUiThread(() ->
2354+
Snackbar.make(getView(), R.string.install_warn_no_checksum, Snackbar.LENGTH_LONG).show());
2355+
}
23092356

23102357
if (getActivity() != null) {
23112358
getActivity().runOnUiThread(() -> {

‎controller/app/src/main/java/org/iiab/controller/deploy/data/RootfsArchiveValidator.java‎

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ public final class RootfsArchiveValidator {
4747

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

50-
public enum Result { OK, OK_NO_MANIFEST, NOT_A_ROOTFS, WRONG_ARCH, UNREADABLE }
50+
public enum Result { OK, OK_NO_MANIFEST, OK_NO_CHECKSUM, NOT_A_ROOTFS, WRONG_ARCH, CORRUPT, UNREADABLE }
5151

5252
private RootfsArchiveValidator() {
5353
// Static utility; not instantiable.
@@ -62,7 +62,7 @@ public static Result validate(Context context, String archivePath) {
6262
if (entries.isEmpty()) {
6363
return Result.UNREADABLE;
6464
}
65-
return validateWithEntries(context, archivePath, isGzip, tarBinary, entries);
65+
return validateWithEntries(context, archivePath, isGzip, tarBinary, entries, true);
6666
} catch (Exception e) {
6767
Log.e(TAG, "Validation error", e);
6868
return Result.UNREADABLE;
@@ -75,6 +75,19 @@ public static Result validate(Context context, String archivePath) {
7575
*/
7676
public static Result validateWithEntries(Context context, String archivePath,
7777
boolean isGzip, String tarBinary, List<String> entries) {
78+
// Restore re-uses the listing for the D11 guard; integrity was already
79+
// checked at import time, so don't pay a second full pass here.
80+
return validateWithEntries(context, archivePath, isGzip, tarBinary, entries, false);
81+
}
82+
83+
/**
84+
* @param checkIntegrity when true (the import gate) and the rootfs is not an
85+
* app-made backup, recompute the embedded iiab-tree-sha256-v1 treehash
86+
* and fail closed ({@link Result#CORRUPT}) on a mismatch.
87+
*/
88+
public static Result validateWithEntries(Context context, String archivePath,
89+
boolean isGzip, String tarBinary,
90+
List<String> entries, boolean checkIntegrity) {
7891
try {
7992
// Authoritative path: the build/app embeds an identity manifest
8093
// (installed-rootfs/iiab/.iiab-rootfs.json, packed first). See
@@ -88,7 +101,28 @@ public static Result validateWithEntries(Context context, String archivePath,
88101
&& !id.arch.equals(RootfsManifest.appAbiId())) {
89102
return Result.WRONG_ARCH;
90103
}
91-
return Result.OK; // manifest-validated; no need to probe ELF
104+
// Identity is authoritative for kind+arch. Now decide integrity.
105+
if ("device-backup".equals(id.origin)) {
106+
// App-made backup: no checksum by design (we don't turn the
107+
// phone into a builder). Cheap signal from the first header.
108+
return Result.OK_NO_CHECKSUM;
109+
}
110+
if (!checkIntegrity) {
111+
return Result.OK; // restore: already verified at import
112+
}
113+
RootfsIntegrity.Result ir = RootfsIntegrity.verify(archivePath);
114+
switch (ir.status) {
115+
case MATCH:
116+
case ABSENT: // builder rootfs without integrity yet (soft phase)
117+
return Result.OK;
118+
case DECLARED_NONE:
119+
return Result.OK_NO_CHECKSUM;
120+
case MISMATCH:
121+
case ERROR:
122+
default:
123+
Log.w(TAG, "Integrity check failed (" + ir.status + ") for " + archivePath);
124+
return Result.CORRUPT;
125+
}
92126
}
93127

94128
// Soft fallback (no manifest): legacy ELF/structure heuristic. We

0 commit comments

Comments
 (0)