commit 30e1442f80a2b36f71b9cbf07140de06bf87e024 Author: bim dsnake@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 }
tor-commits@lists.torproject.org