[tor-commits] [orbot/master] V3 Backup and Restore working with Storage Access Framework

n8fr8 at torproject.org n8fr8 at torproject.org
Wed Dec 22 21:55:06 UTC 2021


commit 30e1442f80a2b36f71b9cbf07140de06bf87e024
Author: bim <dsnake at 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
     }
 





More information about the tor-commits mailing list