Skip to content

Commit d9a14ac

Browse files
committed
refactor: modify backup framework to add support for the upcoming backup v5
Also added tests for v4 backups to prevent breaking compatibility in the future. Signed-off-by: Muntashir Al-Islam <muntashirakon@riseup.net>
1 parent 05c53b5 commit d9a14ac

48 files changed

Lines changed: 1594 additions & 368 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
// SPDX-License-Identifier: GPL-3.0-or-later
2+
3+
package io.github.muntashirakon.AppManager.backup;
4+
5+
import androidx.annotation.NonNull;
6+
import androidx.annotation.Nullable;
7+
8+
import io.github.muntashirakon.AppManager.crypto.AESCrypto;
9+
import io.github.muntashirakon.AppManager.crypto.Crypto;
10+
import io.github.muntashirakon.AppManager.crypto.CryptoException;
11+
import io.github.muntashirakon.AppManager.crypto.DummyCrypto;
12+
import io.github.muntashirakon.AppManager.crypto.ECCCrypto;
13+
import io.github.muntashirakon.AppManager.crypto.OpenPGPCrypto;
14+
import io.github.muntashirakon.AppManager.crypto.RSACrypto;
15+
import io.github.muntashirakon.AppManager.crypto.ks.CompatUtil;
16+
import io.github.muntashirakon.AppManager.settings.Prefs;
17+
import io.github.muntashirakon.AppManager.utils.ContextUtils;
18+
19+
public class BackupCryptSetupHelper {
20+
@NonNull
21+
@CryptoUtils.Mode
22+
public final String mode;
23+
public final int version;
24+
@NonNull
25+
public final Crypto crypto;
26+
private String keyIds;
27+
private byte[] aes;
28+
private byte[] iv;
29+
30+
public BackupCryptSetupHelper(@NonNull String mode, int version) throws CryptoException {
31+
this.mode = mode;
32+
this.version = version;
33+
this.crypto = setup();
34+
}
35+
36+
@Nullable
37+
public String getKeyIds() {
38+
return keyIds;
39+
}
40+
41+
@Nullable
42+
public byte[] getAes() {
43+
return aes;
44+
}
45+
46+
@Nullable
47+
public byte[] getIv() {
48+
return iv;
49+
}
50+
51+
@NonNull
52+
private Crypto setup() throws CryptoException {
53+
switch (mode) {
54+
case CryptoUtils.MODE_OPEN_PGP:
55+
keyIds = Prefs.Encryption.getOpenPgpKeyIds();
56+
return new OpenPGPCrypto(ContextUtils.getContext(), keyIds);
57+
case CryptoUtils.MODE_AES: {
58+
iv = generateIv();
59+
AESCrypto aesCrypto = new AESCrypto(iv);
60+
if (version < 4) {
61+
// Old backups use 32 bit MAC
62+
aesCrypto.setMacSizeBits(AESCrypto.MAC_SIZE_BITS_OLD);
63+
}
64+
return aesCrypto;
65+
}
66+
case CryptoUtils.MODE_RSA: {
67+
iv = generateIv();
68+
RSACrypto rsaCrypto = new RSACrypto(iv, null);
69+
if (version < 4) {
70+
// Old backups use 32 bit MAC
71+
rsaCrypto.setMacSizeBits(AESCrypto.MAC_SIZE_BITS_OLD);
72+
}
73+
aes = rsaCrypto.getEncryptedAesKey();
74+
return rsaCrypto;
75+
}
76+
case CryptoUtils.MODE_ECC: {
77+
iv = generateIv();
78+
ECCCrypto eccCrypto = new ECCCrypto(iv, null);
79+
if (version < 4) {
80+
// Old backups use 32 bit MAC
81+
eccCrypto.setMacSizeBits(AESCrypto.MAC_SIZE_BITS_OLD);
82+
}
83+
aes = eccCrypto.getEncryptedAesKey();
84+
return eccCrypto;
85+
}
86+
case CryptoUtils.MODE_NO_ENCRYPTION:
87+
default:
88+
return new DummyCrypto();
89+
}
90+
}
91+
92+
@NonNull
93+
private static byte[] generateIv() {
94+
byte[] iv = new byte[AESCrypto.GCM_IV_SIZE_BYTES];
95+
CompatUtil.getPrng().nextBytes(iv);
96+
return iv;
97+
}
98+
}

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

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import androidx.annotation.IntDef;
99
import androidx.annotation.NonNull;
10+
import androidx.annotation.VisibleForTesting;
1011

1112
import java.lang.annotation.Retention;
1213
import java.lang.annotation.RetentionPolicy;
@@ -54,16 +55,14 @@ public static BackupDataDirectoryInfo getInfo(@NonNull String dataDir, @UserIdIn
5455
@NonNull
5556
private static BackupDataDirectoryInfo getExternalInfo(@NonNull String dataDir, @NonNull String baseDir) {
5657
String relativeDir = dataDir.substring(baseDir.length()); // No starting separator
57-
int subType = TYPE_CUSTOM;
58+
int subType;
5859
if (relativeDir.startsWith("Android/data/")) {
5960
subType = TYPE_ANDROID_DATA;
60-
}
61-
if (relativeDir.startsWith("Android/obb/")) {
61+
} else if (relativeDir.startsWith("Android/obb/")) {
6262
subType = TYPE_ANDROID_OBB;
63-
}
64-
if (relativeDir.startsWith("Android/media/")) {
63+
} else if (relativeDir.startsWith("Android/media/")) {
6564
subType = TYPE_ANDROID_MEDIA;
66-
}
65+
} else subType = TYPE_CUSTOM;
6766
return new BackupDataDirectoryInfo(dataDir, Paths.get(baseDir).isDirectory(), TYPE_EXTERNAL, subType);
6867
}
6968

@@ -99,7 +98,9 @@ private static BackupDataDirectoryInfo getExternalInfo(@NonNull String dataDir,
9998
public static final int TYPE_EXTERNAL = 2;
10099
public static final int TYPE_UNKNOWN = 3;
101100

102-
public final String rawRath;
101+
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
102+
public final String rawPath;
103+
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
103104
public final Path path;
104105
public final boolean isMounted;
105106
@Type
@@ -108,13 +109,17 @@ private static BackupDataDirectoryInfo getExternalInfo(@NonNull String dataDir,
108109
public final int subtype;
109110

110111
private BackupDataDirectoryInfo(String path, boolean isMounted, @Type int type, @SubType int subtype) {
111-
this.rawRath = path;
112+
this.rawPath = path;
112113
this.path = Paths.get(path);
113114
this.isMounted = isMounted;
114115
this.type = type;
115116
this.subtype = subtype;
116117
}
117118

119+
public Path getDirectory() {
120+
return path;
121+
}
122+
118123
public boolean isExternal() {
119124
return type == TYPE_EXTERNAL;
120125
}

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

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
import java.util.List;
2222
import java.util.stream.Collectors;
2323

24-
import io.github.muntashirakon.AppManager.backup.struct.BackupMetadataV2;
24+
import io.github.muntashirakon.AppManager.backup.struct.BackupMetadataV5;
2525
import io.github.muntashirakon.AppManager.crypto.Crypto;
2626
import io.github.muntashirakon.AppManager.crypto.DummyCrypto;
2727
import io.github.muntashirakon.AppManager.logcat.helper.SaveLogHelper;
@@ -92,7 +92,7 @@ public static List<BackupItem> findAllBackupItems() {
9292

9393
@Deprecated
9494
@NonNull
95-
public static Path getPackagePath(@NonNull String packageName, boolean create) throws IOException {
95+
private static Path getPackagePath(@NonNull String packageName, boolean create) throws IOException {
9696
if (create) {
9797
return getBaseDirectory().findOrCreateDirectory(packageName);
9898
} else return getBaseDirectory().findFile(packageName);
@@ -182,6 +182,10 @@ public void setCrypto(@Nullable Crypto crypto) {
182182
}
183183
}
184184

185+
public boolean isBackupMode() {
186+
return mBackupMode;
187+
}
188+
185189
@NonNull
186190
public Path getBackupPath() {
187191
return mBackupMode ? mTempBackupPath : mBackupPath;
@@ -273,16 +277,46 @@ public Path getIconFile() throws IOException {
273277
} else return getBackupPath().findFile(ICON_FILE);
274278
}
275279

280+
public boolean isV5AndUp() {
281+
return getBackupPath().hasFile(MetadataManager.INFO_V5_FILE);
282+
}
283+
284+
public Path getInfoFile() throws IOException {
285+
// info_v5.am.json is never encrypted
286+
if (mBackupMode) {
287+
return getBackupPath().findOrCreateFile(MetadataManager.INFO_V5_FILE, null);
288+
} else return getBackupPath().findFile(MetadataManager.INFO_V5_FILE);
289+
}
290+
291+
public Path getMetadataV5File() throws IOException {
292+
if (mBackupMode) {
293+
// Needs to be encrypted in backup mode
294+
return getBackupPath().findOrCreateFile(MetadataManager.META_V5_FILE, null);
295+
} else {
296+
// Needs to be decrypted in restore mode
297+
Path file = getBackupPath().findFile(MetadataManager.META_V5_FILE + CryptoUtils.getExtension(mCryptoMode));
298+
return decrypt(new Path[]{file})[0];
299+
}
300+
}
301+
276302
@NonNull
277303
public Path getMetadataV2File() throws IOException {
278-
// meta is never encrypted
304+
// meta_v2.am.json is never encrypted
279305
if (mBackupMode) {
280306
return getBackupPath().findOrCreateFile(MetadataManager.META_V2_FILE, null);
281307
} else return getBackupPath().findFile(MetadataManager.META_V2_FILE);
282308
}
283309

284-
public BackupMetadataV2 getMetadataV2() throws IOException {
285-
return MetadataManager.readMetadataV2(this);
310+
public BackupMetadataV5.Info getInfo() throws IOException {
311+
return MetadataManager.readInfo(this);
312+
}
313+
314+
public BackupMetadataV5 getMetadata() throws IOException {
315+
return MetadataManager.readMetadataV5(this);
316+
}
317+
318+
public BackupMetadataV5 getMetadata(BackupMetadataV5.Info backupInfo) throws IOException {
319+
return MetadataManager.readMetadataV5(this, backupInfo);
286320
}
287321

288322
@NonNull
@@ -478,6 +512,7 @@ public BackupItem[] createItemsGracefully() throws IOException {
478512
return backupFiles;
479513
}
480514

515+
@NonNull
481516
private Path getFreshBackupPath(String backupName) throws IOException {
482517
String newBackupName = backupName;
483518
int i = 0;

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import java.io.IOException;
99
import java.util.Arrays;
1010

11-
import io.github.muntashirakon.AppManager.backup.struct.BackupMetadataV2;
11+
import io.github.muntashirakon.AppManager.backup.struct.BackupMetadataV5;
1212
import io.github.muntashirakon.AppManager.logs.Log;
1313
import io.github.muntashirakon.AppManager.progress.ProgressHandler;
1414
import io.github.muntashirakon.AppManager.types.UserPackagePair;
@@ -204,7 +204,7 @@ public void deleteBackup(@Nullable String[] backupNames) throws BackupException
204204
}
205205
for (BackupItems.BackupItem backupItem : backupItemList) {
206206
try {
207-
BackupMetadataV2 metadata = backupItem.getMetadataV2();
207+
BackupMetadataV5 metadata = backupItem.getMetadata();
208208
if (!backupItem.isFrozen() && !backupItem.delete()) {
209209
throw new BackupException("Could not delete the selected backups");
210210
}
@@ -218,10 +218,10 @@ public void deleteBackup(@Nullable String[] backupNames) throws BackupException
218218
// requested for only single backups
219219
for (String backupName : backupNames) {
220220
BackupItems.BackupItem backupItem;
221-
BackupMetadataV2 metadata;
221+
BackupMetadataV5 metadata;
222222
try {
223223
backupItem = BackupItems.findBackupItem(backupName, mTargetPackage.getPackageName(), null);
224-
metadata = backupItem.getMetadataV2();
224+
metadata = backupItem.getMetadata();
225225
} catch (IOException e) {
226226
throw new BackupException("Could not get backup files.", e);
227227
}

0 commit comments

Comments
 (0)