Skip to content

Commit f952296

Browse files
committed
backup: use cache directory for storing unencrypted backups
When encryption is enabled, App Manager used to use the backup directory for encryption and decryption, which has several issues: 1. Any adversary watching the directory can detect and obtain a reference to the unencrypted file, thereby, can also access the unencrypted data 2. For folder that are synced to clouds, creation of unencrypted files would trigger a sync which would waste bandwidth. Storing the unencrypted data ensure that an adversary cannot access those transient files without privileged permissions. Signed-off-by: Muntashir Al-Islam <muntashirakon@riseup.net>
1 parent a090f24 commit f952296

13 files changed

Lines changed: 118 additions & 77 deletions

File tree

app/src/main/java/io/github/muntashirakon/AppManager/backup/BackupItems.java

Lines changed: 92 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,21 +12,24 @@
1212
import java.io.IOException;
1313
import java.io.PrintWriter;
1414
import java.util.ArrayList;
15+
import java.util.Arrays;
1516
import java.util.HashMap;
1617
import java.util.List;
18+
import java.util.stream.Collectors;
1719

1820
import io.github.muntashirakon.AppManager.crypto.Crypto;
1921
import io.github.muntashirakon.AppManager.crypto.DummyCrypto;
22+
import io.github.muntashirakon.AppManager.logcat.helper.SaveLogHelper;
2023
import io.github.muntashirakon.AppManager.logs.Log;
2124
import io.github.muntashirakon.AppManager.settings.Prefs;
2225
import io.github.muntashirakon.io.Path;
2326
import io.github.muntashirakon.io.PathReader;
2427
import io.github.muntashirakon.io.PathWriter;
2528

2629
public class BackupItems {
27-
static final String APK_SAVING_DIRECTORY = "apks";
30+
private static final String APK_SAVING_DIRECTORY = "apks";
2831
@Deprecated // No longer used
29-
static final String TEMPORARY_DIRECTORY = ".tmp";
32+
private static final String TEMPORARY_DIRECTORY = ".tmp";
3033

3134
private static final String ICON_FILE = "icon.png";
3235
private static final String RULES_TSV = "rules.am.tsv";
@@ -36,20 +39,51 @@ public class BackupItems {
3639
private static final String NO_MEDIA = ".nomedia";
3740

3841
@NonNull
39-
public static Path getBaseDirectory() {
42+
private static Path getBaseDirectory() {
4043
return Prefs.Storage.getAppManagerDirectory();
4144
}
4245

4346
@NonNull
44-
public static Path findBackupDirectory(@NonNull String backupName, @Nullable String packageName, @Nullable String backupUuid) throws FileNotFoundException {
47+
public static BackupItem findBackupItem(@NonNull String backupName, @Nullable String packageName, @Nullable String backupUuid) throws IOException {
4548
if (packageName == null && backupUuid == null) {
4649
throw new IllegalArgumentException("Neither packageName nor backupUuid is set");
4750
}
51+
Path backupPath;
4852
if (backupUuid != null) {
49-
return getBaseDirectory().findFile(backupUuid);
53+
backupPath = getBaseDirectory().findFile(backupUuid);
5054
} else {
51-
return getBaseDirectory().findFile(packageName).findFile(backupName);
55+
backupPath = getBaseDirectory().findFile(packageName).findFile(backupName);
5256
}
57+
return new BackupItem(backupPath);
58+
}
59+
60+
@NonNull
61+
public static List<BackupItem> findAllBackupItems() {
62+
Path baseDirectory = getBaseDirectory();
63+
Path[] paths = baseDirectory.listFiles(Path::isDirectory);
64+
List<BackupItem> backupItems = new ArrayList<>(paths.length);
65+
for (Path path : paths) {
66+
if (BackupUtils.isUuid(path.getName())) {
67+
// UUID-based backups only store one backup per folder
68+
backupItems.add(new BackupItem(path));
69+
}
70+
if (SaveLogHelper.SAVED_LOGS_DIR.equals(path.getName())) {
71+
continue;
72+
}
73+
if (APK_SAVING_DIRECTORY.equals(path.getName())) {
74+
continue;
75+
}
76+
if (TEMPORARY_DIRECTORY.equals(path.getName())) {
77+
continue;
78+
}
79+
// Other backups can store multiple backups per folder
80+
backupItems.addAll(Arrays.stream(path.listFiles(Path::isDirectory))
81+
.map(BackupItem::new)
82+
.collect(Collectors.toList()));
83+
}
84+
// We don't need to check further at this stage.
85+
// It's the caller's job to check the contents if needed.
86+
return backupItems;
5387
}
5488

5589
@Deprecated
@@ -60,6 +94,17 @@ public static Path getPackagePath(@NonNull String packageName, boolean create) t
6094
} else return getBaseDirectory().findFile(packageName);
6195
}
6296

97+
@NonNull
98+
private static synchronized Path getTemporaryUnencryptedPath(@NonNull String backupName) throws IOException {
99+
Path tmpDir = Prefs.Storage.getTempPath();
100+
String newFilename = backupName;
101+
int i = 0;
102+
while (tmpDir.hasFile(newFilename)) {
103+
newFilename = backupName + "_" + (++i);
104+
}
105+
return tmpDir.findOrCreateDirectory(newFilename);
106+
}
107+
63108
@NonNull
64109
private static synchronized Path getTemporaryBackupPath(@NonNull Path originalBackupPath) throws IOException {
65110
Path tmpDir = originalBackupPath.requireParent();
@@ -101,8 +146,9 @@ public static class BackupItem {
101146
private boolean mBackupMode;
102147
private boolean mBackupSuccess = false;
103148
private final List<Path> mTemporaryFiles = new ArrayList<>();
149+
private Path mTempUnencyptedPath;
104150

105-
public BackupItem(@NonNull Path backupPath, boolean backupMode) throws IOException {
151+
private BackupItem(@NonNull Path backupPath, boolean backupMode) throws IOException {
106152
// For now, backup name is the same as the first path segment
107153
backupName = backupPath.getName();
108154
mBackupPath = backupPath;
@@ -113,6 +159,15 @@ public BackupItem(@NonNull Path backupPath, boolean backupMode) throws IOExcepti
113159
} else mTempBackupPath = mBackupPath;
114160
}
115161

162+
// Read-only instance: the point is not to throw IOException
163+
private BackupItem(@NonNull Path backupPath) {
164+
// For now, backup name is the same as the first path segment
165+
backupName = backupPath.getName();
166+
mBackupPath = backupPath;
167+
mBackupMode = false;
168+
mTempBackupPath = mBackupPath;
169+
}
170+
116171
public void setCrypto(@Nullable Crypto crypto) {
117172
if (crypto == null || crypto instanceof DummyCrypto) {
118173
mCrypto = null;
@@ -133,8 +188,16 @@ public Path getUnencryptedBackupPath() {
133188
// Use real path for unencrypted backups
134189
return getBackupPath();
135190
} else {
136-
// TODO: 8/30/25 Always use temporary path for encrypted backups
137-
return getBackupPath();
191+
if (mTempUnencyptedPath == null) {
192+
// We can only do this once for each BackupItem
193+
try {
194+
mTempUnencyptedPath = getTemporaryUnencryptedPath(getBackupPath().getName());
195+
} catch (IOException e) {
196+
Log.w(TAG, "Could not create temporary unencrypted path, falling back to default path", e);
197+
mTempUnencyptedPath = getBackupPath();
198+
}
199+
}
200+
return mTempUnencyptedPath;
138201
}
139202
}
140203

@@ -243,7 +306,7 @@ public Path getMiscFile() throws IOException {
243306
}
244307

245308
@NonNull
246-
public Path getRulesFile(@CryptoUtils.Mode String mode) throws IOException {
309+
public Path getRulesFile() throws IOException {
247310
if (mBackupMode) {
248311
// Needs to be encrypted in backup mode
249312
return getUnencryptedBackupPath().findOrCreateFile(RULES_TSV, null);
@@ -304,6 +367,10 @@ public void cleanup() {
304367
}
305368
}
306369

370+
public boolean exists() {
371+
return mBackupPath.exists();
372+
}
373+
307374
public boolean delete() {
308375
if (mBackupPath.exists()) {
309376
return mBackupPath.delete();
@@ -353,19 +420,25 @@ public String getPackageName() {
353420
return mPackageName;
354421
}
355422

356-
public BackupItem[] getBackupPaths(boolean backupMode) throws IOException {
423+
public BackupItem[] getExistingItems() throws FileNotFoundException {
424+
BackupItem[] backupFiles = new BackupItem[mBackupNames.length];
425+
for (int i = 0; i < mBackupNames.length; ++i) {
426+
backupFiles[i] = new BackupItem(mPackagePath.findFile(mBackupNames[i]));
427+
}
428+
return backupFiles;
429+
}
430+
431+
public BackupItem[] getOrCreateItems() throws IOException {
357432
BackupItem[] backupFiles = new BackupItem[mBackupNames.length];
358433
for (int i = 0; i < mBackupNames.length; ++i) {
359434
backupFiles[i] = new BackupItem(
360-
backupMode ?
361-
mPackagePath.findOrCreateDirectory(mBackupNames[i]) :
362-
mPackagePath.findFile(mBackupNames[i]),
363-
backupMode);
435+
mPackagePath.findOrCreateDirectory(mBackupNames[i]),
436+
true);
364437
}
365438
return backupFiles;
366439
}
367440

368-
BackupItem[] getFreshBackupPaths() throws IOException {
441+
public BackupItem[] createItemsGracefully() throws IOException {
369442
BackupItem[] backupFiles = new BackupItem[mBackupNames.length];
370443
for (int i = 0; i < mBackupNames.length; ++i) {
371444
backupFiles[i] = new BackupItem(getFreshBackupPath(mBackupNames[i]), true);
@@ -430,7 +503,9 @@ public Path getFile() {
430503

431504
public void add(@NonNull String fileName, @NonNull String checksum) {
432505
synchronized (mChecksums) {
433-
if (!"w".equals(mMode)) throw new IllegalStateException("add is inaccessible in mode " + mMode);
506+
if (!"w".equals(mMode)) {
507+
throw new IllegalStateException("add is inaccessible in mode " + mMode);
508+
}
434509
mWriter.println(String.format("%s\t%s", checksum, fileName));
435510
mChecksums.put(fileName, checksum);
436511
mWriter.flush();

app/src/main/java/io/github/muntashirakon/AppManager/backup/BackupManager.java

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ public class BackupManager {
3030
static final String KEYSTORE_PREFIX = "keystore";
3131
static final int KEYSTORE_PLACEHOLDER = -1000;
3232

33-
public static final String ICON_FILE = "icon.png";
3433
public static final String CERT_PREFIX = "cert_";
3534
static final String MASTER_KEY = ".masterkey";
3635

@@ -93,7 +92,7 @@ public void backup(@Nullable String[] backupNames, @Nullable ProgressHandler pro
9392
// Get backup files based on the number of backupNames
9493
BackupItems backupItems = new BackupItems(mTargetPackage.getPackageName(), mTargetPackage.getUserId(), backupNames);
9594
BackupItems.BackupItem[] backupItemList = mRequestedFlags.backupMultiple() ?
96-
backupItems.getFreshBackupPaths() : backupItems.getBackupPaths(true);
95+
backupItems.createItemsGracefully() : backupItems.getOrCreateItems();
9796
if (progressHandler != null) {
9897
int max = calculateMaxProgress(backupItemList.length);
9998
progressHandler.setProgressTextInterface(ProgressHandler.PROGRESS_PERCENT);
@@ -169,7 +168,7 @@ public void restore(@Nullable String[] backupNames, @Nullable ProgressHandler pr
169168
BackupItems.BackupItem[] backupItemList;
170169
try {
171170
backupItems = new BackupItems(mTargetPackage.getPackageName(), backupUserId, backupNames);
172-
backupItemList = backupItems.getBackupPaths(false);
171+
backupItemList = backupItems.getExistingItems();
173172
} catch (IOException e) {
174173
throw new BackupException("Could not get backup files.", e);
175174
}
@@ -202,7 +201,7 @@ public void deleteBackup(@Nullable String[] backupNames) throws BackupException
202201
try {
203202
backupItems = new BackupItems(mTargetPackage.getPackageName(),
204203
mTargetPackage.getUserId(), null);
205-
backupItemList = backupItems.getBackupPaths(false);
204+
backupItemList = backupItems.getExistingItems();
206205
} catch (IOException e) {
207206
throw new BackupException("Could not get backup files.", e);
208207
}
@@ -224,8 +223,7 @@ public void deleteBackup(@Nullable String[] backupNames) throws BackupException
224223
for (String backupName : backupNames) {
225224
MetadataManager.Metadata metadata;
226225
try {
227-
backupItem = new BackupItems.BackupItem(BackupItems.getPackagePath(mTargetPackage.getPackageName(),
228-
false).findFile(backupName), false);
226+
backupItem = BackupItems.findBackupItem(backupName, mTargetPackage.getPackageName(), null);
229227
metadata = MetadataManager.getMetadata(backupItem);
230228
} catch (IOException e) {
231229
throw new BackupException("Could not get backup files.", e);
@@ -254,7 +252,7 @@ public void verify(@Nullable String backupName) throws BackupException {
254252
try {
255253
backupItems = new BackupItems(mTargetPackage.getPackageName(), backupUserHandle,
256254
backupName == null ? null : new String[]{backupName});
257-
backupItemList = backupItems.getBackupPaths(false);
255+
backupItemList = backupItems.getExistingItems();
258256
} catch (IOException e) {
259257
throw new BackupException("Could not get backup files.", e);
260258
}

app/src/main/java/io/github/muntashirakon/AppManager/backup/BackupOp.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -481,7 +481,7 @@ private void backupExtras() throws BackupException {
481481

482482
private void backupRules() throws BackupException {
483483
try {
484-
Path rulesFile = mBackupItem.getRulesFile(CryptoUtils.MODE_NO_ENCRYPTION);
484+
Path rulesFile = mBackupItem.getRulesFile();
485485
try (OutputStream outputStream = rulesFile.openOutputStream();
486486
ComponentsBlocker cb = ComponentsBlocker.getInstance(mPackageName, mUserId)) {
487487
ComponentUtils.storeRules(outputStream, cb.getAll(), true);

app/src/main/java/io/github/muntashirakon/AppManager/backup/BackupUtils.java

Lines changed: 4 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929

3030
import io.github.muntashirakon.AppManager.db.entity.Backup;
3131
import io.github.muntashirakon.AppManager.db.utils.AppDb;
32-
import io.github.muntashirakon.AppManager.logcat.helper.SaveLogHelper;
3332
import io.github.muntashirakon.AppManager.logs.Log;
3433
import io.github.muntashirakon.AppManager.misc.OsEnvironment;
3534
import io.github.muntashirakon.AppManager.users.Users;
@@ -47,34 +46,6 @@ public static boolean isUuid(@NonNull String name) {
4746
return UUID_PATTERN.matcher(name).matches();
4847
}
4948

50-
@NonNull
51-
private static List<Path> getBackupPaths() {
52-
Path baseDirectory = BackupItems.getBaseDirectory();
53-
List<Path> backupPaths;
54-
Path[] paths = baseDirectory.listFiles(Path::isDirectory);
55-
backupPaths = new ArrayList<>(paths.length);
56-
for (Path path : paths) {
57-
if (isUuid(path.getName())) {
58-
// UUID-based backups only store one backup per folder
59-
backupPaths.add(path);
60-
}
61-
if (SaveLogHelper.SAVED_LOGS_DIR.equals(path.getName())) {
62-
continue;
63-
}
64-
if (BackupItems.APK_SAVING_DIRECTORY.equals(path.getName())) {
65-
continue;
66-
}
67-
if (BackupItems.TEMPORARY_DIRECTORY.equals(path.getName())) {
68-
continue;
69-
}
70-
// Other backups can store multiple backups per folder
71-
backupPaths.addAll(Arrays.asList(path.listFiles(Path::isDirectory)));
72-
}
73-
// We don't need to check further at this stage.
74-
// It's the caller's job to check the contents if needed.
75-
return backupPaths;
76-
}
77-
7849
@NonNull
7950
public static Path[] getSourceFiles(@NonNull Path backupPath, @NonNull String ext) {
8051
Path[] paths = backupPath.listFiles((dir, name) -> name.startsWith(SOURCE_PREFIX) && name.endsWith(ext));
@@ -156,7 +127,7 @@ public static List<Backup> getBackupMetadataFromDbNoLockValidate(@NonNull String
156127
List<Backup> validatedBackups = new ArrayList<>(backups.size());
157128
for (Backup backup : backups) {
158129
try {
159-
if (backup.getBackupPath().exists()) {
130+
if (backup.getItem().exists()) {
160131
validatedBackups.add(backup);
161132
}
162133
} catch (IOException e) {
@@ -186,10 +157,10 @@ public static Backup getLatestBackupMetadataFromDbNoLockValidate(@NonNull String
186157
@NonNull
187158
public static HashMap<String, List<MetadataManager.Metadata>> getAllMetadata() {
188159
HashMap<String, List<MetadataManager.Metadata>> backupMetadata = new HashMap<>();
189-
List<Path> backupPaths = getBackupPaths();
190-
for (Path backupPath : backupPaths) {
160+
List<BackupItems.BackupItem> backupPaths = BackupItems.findAllBackupItems();
161+
for (BackupItems.BackupItem backupItem : backupPaths) {
191162
try {
192-
MetadataManager.Metadata metadata = MetadataManager.getMetadata(backupPath);
163+
MetadataManager.Metadata metadata = MetadataManager.getMetadata(backupItem);
193164
if (!backupMetadata.containsKey(metadata.packageName)) {
194165
backupMetadata.put(metadata.packageName, new ArrayList<>());
195166
}

app/src/main/java/io/github/muntashirakon/AppManager/backup/MetadataManager.java

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -197,14 +197,6 @@ public static MetadataManager getNewInstance() {
197197
return new MetadataManager();
198198
}
199199

200-
@WorkerThread
201-
@NonNull
202-
public static Metadata getMetadata(@NonNull Path backupPath) throws IOException {
203-
MetadataManager metadataManager = MetadataManager.getNewInstance();
204-
metadataManager.readMetadata(new BackupItems.BackupItem(backupPath, false));
205-
return metadataManager.getMetadata();
206-
}
207-
208200
@WorkerThread
209201
@NonNull
210202
public static Metadata getMetadata(@NonNull BackupItems.BackupItem backupFile) throws IOException {

app/src/main/java/io/github/muntashirakon/AppManager/backup/RestoreOp.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -708,7 +708,7 @@ private void restoreRules() throws BackupException {
708708
}
709709
Path rulesFile;
710710
try {
711-
rulesFile = mBackupItem.getRulesFile(mMetadata.crypto);
711+
rulesFile = mBackupItem.getRulesFile();
712712
} catch (IOException e) {
713713
if (mMetadata.hasRules) {
714714
throw new BackupException("Rules file is missing.", e);

app/src/main/java/io/github/muntashirakon/AppManager/backup/VerifyOp.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ private void verifyExtras() throws BackupException {
188188
private void verifyRules() throws BackupException {
189189
Path rulesFile;
190190
try {
191-
rulesFile = mBackupItem.getRulesFile(mMetadata.crypto);
191+
rulesFile = mBackupItem.getRulesFile();
192192
} catch (IOException e) {
193193
if (mMetadata.hasRules) {
194194
throw new BackupException("Rules file is missing.", e);

app/src/main/java/io/github/muntashirakon/AppManager/backup/convert/OABConverter.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ public void convert() throws BackupException {
124124
BackupItems.BackupItem[] backupItemList;
125125
try {
126126
backupItems = new BackupItems(mPackageName, mUserId, new String[]{"OAndBackup"});
127-
backupItemList = backupItems.getBackupPaths(true);
127+
backupItemList = backupItems.getOrCreateItems();
128128
} catch (IOException e) {
129129
throw new BackupException("Could not get backup files.", e);
130130
}

app/src/main/java/io/github/muntashirakon/AppManager/backup/convert/SBConverter.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ public void convert() throws BackupException {
114114
BackupItems.BackupItem[] backupItemList;
115115
try {
116116
backupItems = new BackupItems(mPackageName, mUserId, new String[]{"SB"});
117-
backupItemList = backupItems.getBackupPaths(true);
117+
backupItemList = backupItems.getOrCreateItems();
118118
} catch (IOException e) {
119119
throw new BackupException("Could not get backup files.", e);
120120
}

app/src/main/java/io/github/muntashirakon/AppManager/backup/convert/TBConverter.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ public void convert() throws BackupException {
125125
BackupItems.BackupItem[] backupItemList;
126126
try {
127127
backupItems = new BackupItems(mPackageName, mUserId, new String[]{"TB"});
128-
backupItemList = backupItems.getBackupPaths(true);
128+
backupItemList = backupItems.getOrCreateItems();
129129
} catch (IOException e) {
130130
throw new BackupException("Could not get backup files", e);
131131
}

0 commit comments

Comments
 (0)