Skip to content

Commit 9165120

Browse files
committed
backup: add tests for AB to AM and AM to AB conversion
In order to ensure consistency among the backups, ADB/Android full backup (AB) to App Manager (AM) formatted backup and vice versa needs to be ensured. The AB backup created by App Manager via ADB backup API is different from a regular ADB backup in the following way: 1. It does not take backup of the APK files or OBB files, because they are handled separately by App Manager. This does not affect the backup, however. 2. Each backup (.ab) file contains backup of just one application package, because App Manager always takes backup in a per-package basis. 3. No backup password should be given in the backup prompt that the user receives. App Manager handles the necessary encryption using its own backup scheme. Besides, Android backup still uses AES/CBC/PKCS5Padding algorithm for backups which is vulnerable to padding oracle attacks (CWE-326). Signed-off-by: Muntashir Al-Islam <muntashirakon@riseup.net>
1 parent 8c4f743 commit 9165120

16 files changed

Lines changed: 812 additions & 61 deletions

File tree

Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
// SPDX-License-Identifier: GPL-3.0-or-later
2+
3+
package io.github.muntashirakon.AppManager.backup.adb;
4+
5+
import static io.github.muntashirakon.AppManager.backup.adb.BackupCategories.CAT_EXT;
6+
import static io.github.muntashirakon.AppManager.backup.adb.BackupCategories.CAT_INT_CE;
7+
import static io.github.muntashirakon.AppManager.backup.adb.BackupCategories.CAT_INT_DE;
8+
import static io.github.muntashirakon.AppManager.backup.adb.BackupCategories.CAT_OBB;
9+
import static io.github.muntashirakon.AppManager.backup.adb.BackupCategories.CAT_SRC;
10+
11+
import android.content.pm.PackageInfo;
12+
import android.content.pm.Signature;
13+
import android.os.Build;
14+
import android.util.StringBuilderPrinter;
15+
16+
import androidx.annotation.NonNull;
17+
import androidx.annotation.Nullable;
18+
import androidx.core.content.pm.PackageInfoCompat;
19+
20+
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
21+
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
22+
import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;
23+
import org.apache.commons.compress.archivers.tar.TarConstants;
24+
25+
import java.io.BufferedInputStream;
26+
import java.io.BufferedOutputStream;
27+
import java.io.File;
28+
import java.io.IOException;
29+
import java.io.InputStream;
30+
import java.io.OutputStream;
31+
import java.security.cert.CertificateEncodingException;
32+
import java.security.cert.X509Certificate;
33+
import java.util.ArrayList;
34+
import java.util.HashMap;
35+
import java.util.List;
36+
import java.util.Map;
37+
38+
import io.github.muntashirakon.AppManager.apk.signing.SignerInfo;
39+
import io.github.muntashirakon.AppManager.backup.BackupUtils;
40+
import io.github.muntashirakon.AppManager.utils.ExUtils;
41+
import io.github.muntashirakon.AppManager.utils.PackageUtils;
42+
import io.github.muntashirakon.AppManager.utils.TarUtils;
43+
import io.github.muntashirakon.io.IoUtils;
44+
import io.github.muntashirakon.io.Path;
45+
import io.github.muntashirakon.io.SplitInputStream;
46+
47+
public class AndroidBackupCreator implements AutoCloseable {
48+
public static final String TAG = AndroidBackupCreator.class.getSimpleName();
49+
50+
public static void fromTar(@NonNull Path tarSource, @NonNull Path abDest, @Nullable char[] password, int api,
51+
boolean compress)
52+
throws IOException {
53+
int backupFileVersion = Constants.getBackupFileVersionFromApi(api);
54+
AndroidBackupHeader header = new AndroidBackupHeader(backupFileVersion, compress, password);
55+
try (InputStream is = tarSource.openInputStream();
56+
OutputStream realOs = header.write(abDest.openOutputStream())) {
57+
IoUtils.copy(is, realOs);
58+
} catch (IOException e) {
59+
throw e;
60+
} catch (Exception e) {
61+
ExUtils.rethrowAsIOException(e);
62+
}
63+
}
64+
65+
@NonNull
66+
private final Path mWorkingDir;
67+
@NonNull
68+
private final String mPackageName;
69+
@NonNull
70+
private final PackageInfo mPackageInfo;
71+
@Nullable
72+
private final String mInstallerPackage;
73+
private final Map<Integer, List<Path>> mCategoryFilesMap;
74+
@NonNull
75+
@TarUtils.TarType
76+
private final String mTarType;
77+
private final List<Path> mFilesToBeDeleted = new ArrayList<>();
78+
79+
public AndroidBackupCreator(@NonNull Map<Integer, List<Path>> categoryFilesMap,
80+
@NonNull Path temporaryDir,
81+
@NonNull PackageInfo packageInfo,
82+
@Nullable String installerPackage,
83+
@NonNull @TarUtils.TarType String tarType) {
84+
mCategoryFilesMap = new HashMap<>(categoryFilesMap);
85+
mWorkingDir = temporaryDir;
86+
mPackageInfo = packageInfo;
87+
mPackageName = packageInfo.packageName;
88+
mInstallerPackage = installerPackage;
89+
mTarType = tarType;
90+
}
91+
92+
@Override
93+
public void close() {
94+
for (Path file : mFilesToBeDeleted) {
95+
file.delete();
96+
}
97+
}
98+
99+
public Path getBackupFile(int dataIndex) throws IOException {
100+
// Create temporary merged TAR file
101+
String backupFilename = BackupUtils.getDataFilePrefix(dataIndex, null);
102+
Path tempTarFile = mWorkingDir.createNewFile(backupFilename + ".tar", null);
103+
mFilesToBeDeleted.add(tempTarFile);
104+
Path backupFile = mWorkingDir.createNewFile(backupFilename + ".ab", null);
105+
106+
// Merge all category files into a single TAR
107+
mergeCategoryFilesIntoTar(tempTarFile);
108+
109+
// Convert to AB file
110+
fromTar(tempTarFile, backupFile, null, Build.VERSION.SDK_INT, true);
111+
return backupFile;
112+
}
113+
114+
private void mergeCategoryFilesIntoTar(@NonNull Path outputTarFile) throws IOException {
115+
try (OutputStream fos = outputTarFile.openOutputStream();
116+
BufferedOutputStream bos = new BufferedOutputStream(fos);
117+
TarArchiveOutputStream taos = new TarArchiveOutputStream(bos)) {
118+
taos.setLongFileMode(TarArchiveOutputStream.LONGFILE_POSIX);
119+
taos.setBigNumberMode(TarArchiveOutputStream.BIGNUMBER_POSIX);
120+
121+
// Add manifest file first
122+
addManifestEntry(taos);
123+
124+
// Process each category
125+
for (Map.Entry<Integer, List<Path>> entry : mCategoryFilesMap.entrySet()) {
126+
int category = entry.getKey();
127+
List<Path> files = entry.getValue();
128+
if (files != null && !files.isEmpty()) {
129+
processCategoryFiles(taos, category, files);
130+
}
131+
}
132+
133+
taos.finish();
134+
}
135+
}
136+
137+
private void addManifestEntry(@NonNull TarArchiveOutputStream taos) throws IOException {
138+
String manifestPath = Constants.APPS_PREFIX + mPackageName + File.separator + Constants.BACKUP_MANIFEST_FILENAME;
139+
byte[] manifestContent = getManifestBytes(mCategoryFilesMap.get(CAT_SRC) != null);
140+
141+
TarArchiveEntry manifestEntry = new TarArchiveEntry(manifestPath);
142+
manifestEntry.setSize(manifestContent.length);
143+
manifestEntry.setMode(0600); // rw-------
144+
manifestEntry.setModTime(0); // See AppMetadataBackupWriter.java
145+
146+
taos.putArchiveEntry(manifestEntry);
147+
taos.write(manifestContent);
148+
taos.closeArchiveEntry();
149+
}
150+
151+
// See AppMetadataBackupWriter#getManifestBytes(PackageInfo, boolean)
152+
@NonNull
153+
private byte[] getManifestBytes(boolean withApk) {
154+
StringBuilder builder = new StringBuilder(4096);
155+
StringBuilderPrinter printer = new StringBuilderPrinter(builder);
156+
printer.println(Integer.toString(Constants.BACKUP_MANIFEST_VERSION));
157+
printer.println(mPackageName);
158+
printer.println(Long.toString(PackageInfoCompat.getLongVersionCode(mPackageInfo)));
159+
printer.println(Integer.toString(Build.VERSION.SDK_INT));
160+
printer.println((mInstallerPackage != null) ? mInstallerPackage : "");
161+
printer.println(withApk ? "1" : "0");
162+
163+
// Write the signature block.
164+
SignerInfo signerInfo = PackageUtils.getSignerInfo(mPackageInfo, true);
165+
if (signerInfo == null || signerInfo.getCurrentSignerCerts() == null) {
166+
printer.println("0");
167+
} else {
168+
// Retrieve the newest signatures to write.
169+
try {
170+
X509Certificate[] signerCerts = signerInfo.getCurrentSignerCerts();
171+
Signature[] signatures = new Signature[signerCerts.length];
172+
for (int i = 0; i < signatures.length; ++i) {
173+
signatures[i] = new Signature(signerCerts[i].getEncoded());
174+
}
175+
printer.println(Integer.toString(signerCerts.length));
176+
for (Signature sig : signatures) {
177+
printer.println(sig.toCharsString());
178+
}
179+
} catch (CertificateEncodingException e) {
180+
// Fall back to 0
181+
printer.println("0");
182+
}
183+
}
184+
return builder.toString().getBytes();
185+
}
186+
187+
private void processCategoryFiles(@NonNull TarArchiveOutputStream taos,
188+
int category,
189+
@NonNull List<Path> files) throws IOException {
190+
try (SplitInputStream sis = new SplitInputStream(files);
191+
BufferedInputStream bis = new BufferedInputStream(sis);
192+
InputStream decompressedStream = TarUtils.createDecompressedStream(bis, mTarType);
193+
TarArchiveInputStream tis = new TarArchiveInputStream(decompressedStream)) {
194+
TarArchiveEntry entry;
195+
while ((entry = tis.getNextTarEntry()) != null) {
196+
String transformedPath = transformEntryPath(entry.getName(), category);
197+
byte linkFlag;
198+
if (entry.isSymbolicLink()) {
199+
linkFlag = TarConstants.LF_SYMLINK;
200+
} else if (entry.isDirectory()) {
201+
linkFlag = TarConstants.LF_DIR;
202+
} else linkFlag = TarConstants.LF_NORMAL;
203+
// Create new entry with transformed path
204+
TarArchiveEntry newEntry = new TarArchiveEntry(transformedPath, linkFlag);
205+
newEntry.setSize(entry.getSize());
206+
newEntry.setMode(entry.getMode());
207+
newEntry.setModTime(entry.getModTime());
208+
newEntry.setUserId(entry.getUserId());
209+
newEntry.setGroupId(entry.getGroupId());
210+
newEntry.setUserName(entry.getUserName());
211+
newEntry.setGroupName(entry.getGroupName());
212+
if (entry.isSymbolicLink()) {
213+
newEntry.setLinkName(entry.getLinkName());
214+
}
215+
taos.putArchiveEntry(newEntry);
216+
if (linkFlag == TarConstants.LF_NORMAL) {
217+
IoUtils.copy(tis, taos);
218+
}
219+
taos.closeArchiveEntry();
220+
}
221+
}
222+
}
223+
224+
@NonNull
225+
private String transformEntryPath(@NonNull String originalPath, int category) {
226+
String basePath = Constants.APPS_PREFIX + mPackageName + File.separator;
227+
if (originalPath.endsWith(File.separator)) {
228+
// AB expects no trailing slashes
229+
originalPath = originalPath.substring(0, originalPath.length() - 1);
230+
}
231+
switch (category) {
232+
case CAT_SRC:
233+
return basePath + Constants.APK_TREE_TOKEN + File.separator + originalPath;
234+
case CAT_INT_CE:
235+
return transformInternalPath(basePath, originalPath, false);
236+
case CAT_INT_DE:
237+
return transformInternalPath(basePath, originalPath, true);
238+
case CAT_EXT:
239+
return basePath + Constants.MANAGED_EXTERNAL_TREE_TOKEN + File.separator + originalPath;
240+
case CAT_OBB:
241+
return basePath + Constants.OBB_TREE_TOKEN + File.separator + originalPath;
242+
default:
243+
throw new IllegalArgumentException("Invalid category: " + category);
244+
}
245+
}
246+
247+
@NonNull
248+
private String transformInternalPath(@NonNull String basePath,
249+
@NonNull String path,
250+
boolean isDE) {
251+
String prefix;
252+
if (path.startsWith("files/")) {
253+
prefix = isDE ? Constants.DEVICE_FILES_TREE_TOKEN : Constants.FILES_TREE_TOKEN;
254+
path = path.substring(6); // Remove "files/"
255+
} else if (path.startsWith("databases/")) {
256+
prefix = isDE ? Constants.DEVICE_DATABASE_TREE_TOKEN : Constants.DATABASE_TREE_TOKEN;
257+
path = path.substring(10); // Remove "databases/"
258+
} else if (path.startsWith("shared_prefs/")) {
259+
prefix = isDE ? Constants.DEVICE_SHAREDPREFS_TREE_TOKEN : Constants.SHAREDPREFS_TREE_TOKEN;
260+
path = path.substring(13); // Remove "shared_prefs/"
261+
} else if (path.startsWith("no_backup/")) {
262+
prefix = isDE ? Constants.DEVICE_NO_BACKUP_TREE_TOKEN : Constants.NO_BACKUP_TREE_TOKEN;
263+
path = path.substring(10); // Remove "no_backup/"
264+
} else if (path.startsWith("caches/")) {
265+
prefix = isDE ? Constants.DEVICE_CACHE_TREE_TOKEN : Constants.CACHE_TREE_TOKEN;
266+
path = path.substring(7); // Remove "caches/"
267+
} else prefix = isDE ? Constants.DEVICE_ROOT_TREE_TOKEN : Constants.ROOT_TREE_TOKEN;
268+
return basePath + prefix + File.separator + path;
269+
}
270+
}

0 commit comments

Comments
 (0)