commit 30e1442f80a2b36f71b9cbf07140de06bf87e024
Author: bim <dsnake(a)protonmail.com>
Date: Fri Nov 27 13:10:25 2020 -0500
V3 Backup and Restore working with Storage Access Framework
---
.../ui/hiddenservices/ClientCookiesActivity.java | 2 +-
.../ui/hiddenservices/HiddenServicesActivity.java | 8 +-
.../ui/hiddenservices/backup/BackupUtils.java | 128 +++++++++++++++++++--
.../dialogs/CookieActionsDialog.java | 2 +-
.../ui/hiddenservices/dialogs/HSActionsDialog.java | 4 +-
.../OnionServiceActionsDialogFragment.java | 6 +-
.../ui/v3onionservice/OnionServicesActivity.java | 14 ++-
.../java/org/torproject/android/core/DiskUtils.kt | 4 +-
8 files changed, 144 insertions(+), 24 deletions(-)
diff --git a/app/src/main/java/org/torproject/android/ui/hiddenservices/ClientCookiesActivity.java b/app/src/main/java/org/torproject/android/ui/hiddenservices/ClientCookiesActivity.java
index 4c43d897..93443678 100644
--- a/app/src/main/java/org/torproject/android/ui/hiddenservices/ClientCookiesActivity.java
+++ b/app/src/main/java/org/torproject/android/ui/hiddenservices/ClientCookiesActivity.java
@@ -113,7 +113,7 @@ public class ClientCookiesActivity extends AppCompatActivity {
}
private void restoreBackupLegacy() {
- File backupDir = DiskUtils.getOrCreateLegacyBackupDir();
+ File backupDir = DiskUtils.getOrCreateLegacyBackupDir(getString(R.string.app_name));
try {
File[] files = backupDir.listFiles((dir, name) -> name.toLowerCase(Locale.ENGLISH).endsWith(".json"));
diff --git a/app/src/main/java/org/torproject/android/ui/hiddenservices/HiddenServicesActivity.java b/app/src/main/java/org/torproject/android/ui/hiddenservices/HiddenServicesActivity.java
index d7429181..7a943093 100644
--- a/app/src/main/java/org/torproject/android/ui/hiddenservices/HiddenServicesActivity.java
+++ b/app/src/main/java/org/torproject/android/ui/hiddenservices/HiddenServicesActivity.java
@@ -107,8 +107,8 @@ public class HiddenServicesActivity extends AppCompatActivity {
}
private void doRestoreLegacy() { // API 16, 17, 18
- File backupDir = DiskUtils.getOrCreateLegacyBackupDir();
- File[] files = backupDir.listFiles((dir, name) -> name.toLowerCase(Locale.ENGLISH).endsWith(".zip"));
+ File backupDir = DiskUtils.getOrCreateLegacyBackupDir(getString(R.string.app_name));
+ File[] files = backupDir.listFiles((dir, name) -> name.toLowerCase().endsWith(".zip"));
if (files != null) {
if (files.length == 0) {
Toast.makeText(this, R.string.create_a_backup_first, Toast.LENGTH_LONG).show();
@@ -120,7 +120,7 @@ public class HiddenServicesActivity extends AppCompatActivity {
new AlertDialog.Builder(this)
.setTitle(R.string.restore_backup)
- .setItems(fileNames, (dialog, which) -> new BackupUtils(HiddenServicesActivity.this).restoreZipBackupLegacy(files[which]))
+ .setItems(fileNames, (dialog, which) -> new BackupUtils(HiddenServicesActivity.this).restoreZipBackupV2Legacy(files[which]))
.show();
}
@@ -132,7 +132,7 @@ public class HiddenServicesActivity extends AppCompatActivity {
if (request == REQUEST_CODE_READ_ZIP_BACKUP) {
if (response != RESULT_OK) return;
BackupUtils backupUtils = new BackupUtils(this);
- backupUtils.restoreZipBackup(data.getData());
+ backupUtils.restoreZipBackupV2(data.getData());
}
}
diff --git a/app/src/main/java/org/torproject/android/ui/hiddenservices/backup/BackupUtils.java b/app/src/main/java/org/torproject/android/ui/hiddenservices/backup/BackupUtils.java
index a10dd7ff..96ffdf5c 100644
--- a/app/src/main/java/org/torproject/android/ui/hiddenservices/backup/BackupUtils.java
+++ b/app/src/main/java/org/torproject/android/ui/hiddenservices/backup/BackupUtils.java
@@ -15,6 +15,7 @@ import org.torproject.android.R;
import org.torproject.android.service.TorServiceConstants;
import org.torproject.android.ui.hiddenservices.providers.CookieContentProvider;
import org.torproject.android.ui.hiddenservices.providers.HSContentProvider;
+import org.torproject.android.ui.v3onionservice.OnionServiceContentProvider;
import java.io.File;
import java.io.FileInputStream;
@@ -37,8 +38,19 @@ public class BackupUtils {
mResolver = mContext.getContentResolver();
}
- public String createZipBackup(int port, Uri zipFile) {
- String[] files = createFilesForZipping(port);
+ public static boolean isV2OnionAddressValid(String onionToTest) {
+ return onionToTest.matches("([a-z0-9]{16}).onion");
+ }
+
+ public String createV3ZipBackup(String port, Uri zipFile) {
+ String[] files = createFilesForZippingV3(port);
+ ZipIt zip = new ZipIt(files, zipFile, mResolver);
+ if (!zip.zip()) return null;
+ return zipFile.getPath();
+ }
+
+ public String createV2ZipBackup(int port, Uri zipFile) {
+ String[] files = createFilesForZippingV2(port);
ZipIt zip = new ZipIt(files, zipFile, mResolver);
if (!zip.zip())
@@ -47,11 +59,50 @@ public class BackupUtils {
return zipFile.getPath();
}
- public static boolean isV2OnionAddressValid(String onionToTest) {
- return onionToTest.matches("([a-z0-9]{16}).onion");
+ // todo also write out authorized clients...
+ private String[] createFilesForZippingV3(String port) {
+ final String v3BasePath = getV3BasePath() + "/v3" + port + "/";
+ final String hostnamePath = v3BasePath + "hostname",
+ configFilePath = v3BasePath + configFileName,
+ privKeyPath = v3BasePath + "hs_ed25519_secret_key",
+ pubKeyPath = v3BasePath + "hs_ed25519_public_key";
+
+ Cursor portData = mResolver.query(OnionServiceContentProvider.CONTENT_URI, OnionServiceContentProvider.PROJECTION,
+ OnionServiceContentProvider.OnionService.PORT + "=" + port, null, null);
+
+ JSONObject config = new JSONObject();
+ try {
+ if (portData == null || portData.getCount() != 1)
+ return null;
+ portData.moveToNext();
+
+
+ config.put(OnionServiceContentProvider.OnionService.NAME, portData.getString(portData.getColumnIndex(OnionServiceContentProvider.OnionService.NAME)));
+ config.put(OnionServiceContentProvider.OnionService.PORT, portData.getString(portData.getColumnIndex(OnionServiceContentProvider.OnionService.PORT)));
+ config.put(OnionServiceContentProvider.OnionService.ONION_PORT, portData.getString(portData.getColumnIndex(OnionServiceContentProvider.OnionService.ONION_PORT)));
+ config.put(OnionServiceContentProvider.OnionService.DOMAIN, portData.getString(portData.getColumnIndex(OnionServiceContentProvider.OnionService.DOMAIN)));
+ config.put(OnionServiceContentProvider.OnionService.CREATED_BY_USER, portData.getString(portData.getColumnIndex(OnionServiceContentProvider.OnionService.CREATED_BY_USER)));
+ config.put(OnionServiceContentProvider.OnionService.ENABLED, portData.getString(portData.getColumnIndex(OnionServiceContentProvider.OnionService.ENABLED)));
+
+ } catch (JSONException jsone) {
+ jsone.printStackTrace();
+ return null;
+ }
+ portData.close();
+
+ try {
+ FileWriter fileWriter = new FileWriter(configFilePath);
+ fileWriter.write(config.toString());
+ fileWriter.close();
+ } catch (IOException ioe) {
+ ioe.printStackTrace();
+ return null;
+ }
+
+ return new String[]{hostnamePath, configFilePath, privKeyPath, pubKeyPath};
}
- private String[] createFilesForZipping(int port) {
+ private String[] createFilesForZippingV2(int port) {
File hsBasePath = getHSBasePath();
String configFilePath = hsBasePath + "/hs" + port + "/" + configFileName;
String hostnameFilePath = hsBasePath + "/hs" + port + "/hostname";
@@ -101,7 +152,45 @@ public class BackupUtils {
return new String[]{hostnameFilePath, keyFilePath, configFilePath};
}
- private void extractConfigFromUnzippedBackup(String backupName) {
+ private void extractConfigFromUnzippedBackupV3(String backupName) {
+ File v3BasePath = getV3BasePath();
+ String v3Dir = backupName.substring(0, backupName.lastIndexOf('.'));
+ String configFilePath = v3BasePath + "/" + v3Dir + "/" + configFileName;
+ File v3Path = new File(v3BasePath.getAbsolutePath(), v3Dir);
+ if (!v3Path.isDirectory()) v3Path.mkdirs();
+
+ File configFile = new File(configFilePath);
+ try {
+ FileInputStream fis = new FileInputStream(configFile);
+ FileChannel fc = fis.getChannel();
+ MappedByteBuffer bb = fc.map(FileChannel.MapMode.READ_ONLY, 0, fc.size());
+ String jsonString = Charset.defaultCharset().decode(bb).toString();
+ JSONObject savedValues = new JSONObject(jsonString);
+ ContentValues fields = new ContentValues();
+
+ int port = savedValues.getInt(OnionServiceContentProvider.OnionService.PORT);
+ fields.put(OnionServiceContentProvider.OnionService.PORT, port);
+ fields.put(OnionServiceContentProvider.OnionService.NAME, savedValues.getString(OnionServiceContentProvider.OnionService.NAME));
+ fields.put(OnionServiceContentProvider.OnionService.ONION_PORT, savedValues.getInt(OnionServiceContentProvider.OnionService.ONION_PORT));
+ fields.put(OnionServiceContentProvider.OnionService.DOMAIN, savedValues.getString(OnionServiceContentProvider.OnionService.DOMAIN));
+ fields.put(OnionServiceContentProvider.OnionService.CREATED_BY_USER, savedValues.getInt(OnionServiceContentProvider.OnionService.CREATED_BY_USER));
+ fields.put(OnionServiceContentProvider.OnionService.ENABLED, savedValues.getInt(OnionServiceContentProvider.OnionService.ENABLED));
+
+ Cursor dbService = mResolver.query(OnionServiceContentProvider.CONTENT_URI, OnionServiceContentProvider.PROJECTION,
+ OnionServiceContentProvider.OnionService.PORT + "=" + port, null, null);
+ if (dbService == null || dbService.getCount() == 0)
+ mResolver.insert(OnionServiceContentProvider.CONTENT_URI, fields);
+ else
+ mResolver.update(OnionServiceContentProvider.CONTENT_URI, fields, OnionServiceContentProvider.OnionService.PORT + "=" + port, null);
+ dbService.close();
+ Toast.makeText(mContext, R.string.backup_restored, Toast.LENGTH_LONG).show();
+ } catch (IOException | JSONException e) {
+ e.printStackTrace();
+ Toast.makeText(mContext, R.string.error, Toast.LENGTH_LONG).show();
+ }
+ }
+
+ private void extractConfigFromUnzippedBackupV2(String backupName) {
File mHSBasePath = getHSBasePath();
int port;
String hsDir = backupName.substring(0, backupName.lastIndexOf('.'));
@@ -166,18 +255,22 @@ public class BackupUtils {
return new File(mContext.getFilesDir().getAbsolutePath(), TorServiceConstants.HIDDEN_SERVICES_DIR);
}
- public void restoreZipBackupLegacy(File zipFile) {
+ private File getV3BasePath() {
+ return new File(mContext.getFilesDir().getAbsolutePath(), TorServiceConstants.ONION_SERVICES_DIR);
+ }
+
+ public void restoreZipBackupV2Legacy(File zipFile) {
String backupName = zipFile.getName();
ZipIt zip = new ZipIt(null, null, mResolver);
String hsDir = backupName.substring(0, backupName.lastIndexOf('.'));
File hsPath = new File(getHSBasePath().getAbsolutePath(), hsDir);
if (zip.unzipLegacy(hsPath.getAbsolutePath(), zipFile))
- extractConfigFromUnzippedBackup(backupName);
+ extractConfigFromUnzippedBackupV2(backupName);
else
Toast.makeText(mContext, R.string.error, Toast.LENGTH_LONG).show();
}
- public void restoreZipBackup(Uri zipUri) {
+ public void restoreZipBackupV2(Uri zipUri) {
Cursor returnCursor = mResolver.query(zipUri, null, null, null, null);
int nameIndex = returnCursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
returnCursor.moveToFirst();
@@ -187,12 +280,27 @@ public class BackupUtils {
String hsDir = backupName.substring(0, backupName.lastIndexOf('.'));
File hsPath = new File(getHSBasePath().getAbsolutePath(), hsDir);
if (new ZipIt(null, zipUri, mResolver).unzip(hsPath.getAbsolutePath()))
- extractConfigFromUnzippedBackup(backupName);
+ extractConfigFromUnzippedBackupV2(backupName);
else
Toast.makeText(mContext, R.string.error, Toast.LENGTH_LONG).show();
+ }
+ public void restoreZipBackupV3(Uri zipUri) {
+ Cursor returnCursor = mResolver.query(zipUri, null, null, null, null);
+ int nameIndex = returnCursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
+ returnCursor.moveToFirst();
+ String backupName = returnCursor.getString(nameIndex);
+ returnCursor.close();
+
+ String v3Dir = backupName.substring(0, backupName.lastIndexOf('.'));
+ File v3Paath = new File(getV3BasePath().getAbsolutePath(), v3Dir);
+ if (new ZipIt(null, zipUri, mResolver).unzip(v3Paath.getAbsolutePath()))
+ extractConfigFromUnzippedBackupV3(backupName);
+ else
+ Toast.makeText(mContext, R.string.error, Toast.LENGTH_LONG).show();
}
+
public void restoreKeyBackup(int hsPort, Uri hsKeyPath) {
File mHSBasePath = new File(
mContext.getFilesDir().getAbsolutePath(),
diff --git a/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/CookieActionsDialog.java b/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/CookieActionsDialog.java
index 8c5ef304..d3286ab1 100644
--- a/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/CookieActionsDialog.java
+++ b/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/CookieActionsDialog.java
@@ -73,7 +73,7 @@ public class CookieActionsDialog extends DialogFragment {
} else { // API 16, 17, and 18
int msg = R.string.backup_saved_at_external_storage;
try {
- File externalStorage = DiskUtils.getOrCreateLegacyBackupDir();
+ File externalStorage = DiskUtils.getOrCreateLegacyBackupDir(getString(R.string.app_name));
String backupFile = externalStorage.getAbsolutePath() + "/" + filename;
String data = createBackupData();
FileWriter writer = new FileWriter(backupFile);
diff --git a/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/HSActionsDialog.java b/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/HSActionsDialog.java
index 76932c88..1d2e7844 100644
--- a/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/HSActionsDialog.java
+++ b/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/HSActionsDialog.java
@@ -92,7 +92,7 @@ public class HSActionsDialog extends DialogFragment {
Intent createFile = DiskUtils.createWriteFileIntent(filename, "application/zip");
startActivityForResult(createFile, REQUEST_CODE_WRITE_FILE);
} else { // API 16, 17, 18
- attemptToWriteBackup(Uri.fromFile(new File(DiskUtils.getOrCreateLegacyBackupDir(), filename)));
+ attemptToWriteBackup(Uri.fromFile(new File(DiskUtils.getOrCreateLegacyBackupDir(getString(R.string.app_name)), filename)));
}
}
@@ -107,7 +107,7 @@ public class HSActionsDialog extends DialogFragment {
private void attemptToWriteBackup(Uri outputFile) {
BackupUtils backupUtils = new BackupUtils(getContext());
- String backup = backupUtils.createZipBackup(port, outputFile);
+ String backup = backupUtils.createV2ZipBackup(port, outputFile);
Toast.makeText(getContext(), backup != null ? R.string.backup_saved_at_external_storage : R.string.error, Toast.LENGTH_LONG).show();
dismiss();
}
diff --git a/app/src/main/java/org/torproject/android/ui/v3onionservice/OnionServiceActionsDialogFragment.java b/app/src/main/java/org/torproject/android/ui/v3onionservice/OnionServiceActionsDialogFragment.java
index 822a45f2..4d26270d 100644
--- a/app/src/main/java/org/torproject/android/ui/v3onionservice/OnionServiceActionsDialogFragment.java
+++ b/app/src/main/java/org/torproject/android/ui/v3onionservice/OnionServiceActionsDialogFragment.java
@@ -61,7 +61,7 @@ public class OnionServiceActionsDialogFragment extends DialogFragment {
}
private void doBackup(Bundle arguments, Context context) {
- String filename = "onion_service" + arguments.getInt(OnionServicesActivity.BUNDLE_KEY_PORT) + ".zip";
+ String filename = "onion_service" + arguments.getString(OnionServicesActivity.BUNDLE_KEY_PORT) + ".zip";
if (arguments.getString(OnionServicesActivity.BUNDLE_KEY_DOMAIN) == null) {
Toast.makeText(context, R.string.please_restart_Orbot_to_enable_the_changes, Toast.LENGTH_LONG).show();
return;
@@ -70,7 +70,7 @@ public class OnionServiceActionsDialogFragment extends DialogFragment {
Intent createFileIntent = DiskUtils.createWriteFileIntent(filename, "application/zip");
startActivityForResult(createFileIntent, REQUEST_CODE_WRITE_FILE);
} else { // APIs 16, 17, 18
- attemptToWriteBackup(Uri.fromFile(new File(DiskUtils.getOrCreateLegacyBackupDir(), filename)));
+ attemptToWriteBackup(Uri.fromFile(new File(DiskUtils.getOrCreateLegacyBackupDir(getString(R.string.app_name)), filename)));
}
}
@@ -85,7 +85,7 @@ public class OnionServiceActionsDialogFragment extends DialogFragment {
private void attemptToWriteBackup(Uri outputFile) {
BackupUtils backupUtils = new BackupUtils(getContext());
-// String backup = backupUtils.createZipBackup(port, outputFile); TODO need to break apart backup logic for v2 and v3 onions
+ String backup = backupUtils.createV3ZipBackup(getArguments().getString(OnionServicesActivity.BUNDLE_KEY_PORT), outputFile);
Toast.makeText(getContext(), backup != null ? R.string.backup_saved_at_external_storage : R.string.error, Toast.LENGTH_LONG).show();
dismiss();
}
diff --git a/app/src/main/java/org/torproject/android/ui/v3onionservice/OnionServicesActivity.java b/app/src/main/java/org/torproject/android/ui/v3onionservice/OnionServicesActivity.java
index 3be04731..eed43a71 100644
--- a/app/src/main/java/org/torproject/android/ui/v3onionservice/OnionServicesActivity.java
+++ b/app/src/main/java/org/torproject/android/ui/v3onionservice/OnionServicesActivity.java
@@ -2,6 +2,7 @@ package org.torproject.android.ui.v3onionservice;
import android.content.ContentResolver;
import android.content.Context;
+import android.content.Intent;
import android.database.ContentObserver;
import android.database.Cursor;
import android.os.Bundle;
@@ -19,11 +20,13 @@ import com.google.android.material.floatingactionbutton.FloatingActionButton;
import org.torproject.android.R;
import org.torproject.android.core.DiskUtils;
import org.torproject.android.core.LocaleHelper;
+import org.torproject.android.ui.hiddenservices.backup.BackupUtils;
public class OnionServicesActivity extends AppCompatActivity {
static final String BUNDLE_KEY_ID = "id", BUNDLE_KEY_PORT = "port", BUNDLE_KEY_DOMAIN = "domain";
private static final String WHERE_SELECTION_CLAUSE = OnionServiceContentProvider.OnionService.CREATED_BY_USER + "=1";
+ private static final int REQUEST_CODE_READ_ZIP_BACKUP = 347;
private RadioButton radioShowUserServices, radioShowAppServices;
private FloatingActionButton fab;
private ContentResolver mContentResolver;
@@ -73,7 +76,8 @@ public class OnionServicesActivity extends AppCompatActivity {
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == R.id.menu_restore_backup) {
if (DiskUtils.supportsStorageAccessFramework()) {
-
+ Intent readFileIntent = DiskUtils.createReadFileIntent("application/zip");
+ startActivityForResult(readFileIntent, REQUEST_CODE_READ_ZIP_BACKUP);
} else { // 16, 17, 18
}
@@ -81,6 +85,14 @@ public class OnionServicesActivity extends AppCompatActivity {
return super.onOptionsItemSelected(item);
}
+ @Override
+ protected void onActivityResult(int requestCode, int result, Intent data) {
+ super.onActivityResult(requestCode, result, data);
+ if (requestCode == REQUEST_CODE_READ_ZIP_BACKUP && result == RESULT_OK) {
+ new BackupUtils(this).restoreZipBackupV3(data.getData());
+ }
+ }
+
private class OnionServiceObserver extends ContentObserver {
OnionServiceObserver(Handler handler) {
diff --git a/appcore/src/main/java/org/torproject/android/core/DiskUtils.kt b/appcore/src/main/java/org/torproject/android/core/DiskUtils.kt
index be74aa4e..1a9551b5 100644
--- a/appcore/src/main/java/org/torproject/android/core/DiskUtils.kt
+++ b/appcore/src/main/java/org/torproject/android/core/DiskUtils.kt
@@ -66,9 +66,9 @@ object DiskUtils {
fun readFile(contentResolver: ContentResolver, file: File): String = readFileFromInputStream(contentResolver, Uri.fromFile(file))
@JvmStatic
- fun getOrCreateLegacyBackupDir(): File? {
+ fun getOrCreateLegacyBackupDir(directoryName: String): File? {
if (Environment.MEDIA_MOUNTED != Environment.getExternalStorageState()) return null
- val dir = File(Environment.getExternalStorageDirectory(), )
+ val dir = File(Environment.getExternalStorageDirectory(), directoryName)
return if (!dir.isDirectory && !dir.mkdirs()) null else dir
}