[Author Prev][Author Next][Thread Prev][Thread Next][Author Index][Thread Index]
[tor-commits] [orbot/master] UI For Create, Backing up Restoring, etc V3 Client Authorizations
commit 64c4a4627c518862181685a6e5dff8bcbde9b2f5
Author: bim <dsnake@xxxxxxxxxxxxxx>
Date: Thu Jan 28 11:40:24 2021 -0500
UI For Create, Backing up Restoring, etc V3 Client Authorizations
---
app/src/main/AndroidManifest.xml | 11 +-
.../org/torproject/android/OrbotMainActivity.java | 8 +-
.../ui/hiddenservices/ClientCookiesActivity.java | 7 +-
.../ui/hiddenservices/backup/BackupUtils.java | 37 +++++-
.../ui/hiddenservices/dialogs/AddCookieDialog.java | 6 +-
.../OnionServiceActionsDialogFragment.java | 13 +-
...icesActivity.java => OnionServiceActivity.java} | 11 +-
.../OnionServiceContentProvider.java | 21 +--
....java => OnionServiceCreateDialogFragment.java} | 4 +-
.../ui/v3onionservice/OnionServiceDatabase.java | 3 +-
....java => OnionServiceDeleteDialogFragment.java} | 9 +-
.../ui/v3onionservice/OnionV3ListAdapter.java | 1 -
.../ui/v3onionservice/V3ClientAuthActivity.java | 25 ----
.../ClientAuthActionsDialogFragment.java | 42 ++++++
.../clientauth/ClientAuthActivity.java | 142 +++++++++++++++++++++
.../clientauth/ClientAuthBackupDialogFragment.java | 90 +++++++++++++
.../clientauth/ClientAuthContentProvider.java | 104 +++++++++++++++
.../ClientAuthCreateDialogFragment.java} | 27 +++-
.../clientauth/ClientAuthDatabase.java | 30 +++++
.../clientauth/ClientAuthDeleteDialogFragment.java | 36 ++++++
.../clientauth/ClientAuthListAdapter.java | 49 +++++++
app/src/main/res/layout/activity_v3auth.xml | 11 +-
.../main/res/layout/dialog_add_v3_client_auth.xml | 5 +-
app/src/main/res/values/dimens.xml | 1 +
app/src/main/res/values/strings.xml | 17 ++-
.../java/org/torproject/android/core/DiskUtils.kt | 2 +-
.../torproject/android/service/OrbotService.java | 81 +++++++++---
.../android/service/TorServiceConstants.java | 1 +
28 files changed, 679 insertions(+), 115 deletions(-)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 51fc720a..ae1a8be8 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -95,7 +95,7 @@
</activity>
<activity
- android:name=".ui.v3onionservice.OnionServicesActivity"
+ android:name=".ui.v3onionservice.OnionServiceActivity"
android:label="@string/hidden_services"
android:theme="@style/DefaultTheme">
<meta-data
@@ -103,8 +103,8 @@
android:value=".OrbotMainActivity" />
</activity>
- <activity android:name=".ui.v3onionservice.V3ClientAuthActivity"
- android:label="@string/v3_client_auth"
+ <activity android:name=".ui.v3onionservice.clientauth.ClientAuthActivity"
+ android:label="@string/v3_client_auth_activity_title"
android:theme="@style/DefaultTheme">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
@@ -147,6 +147,11 @@
android:authorities="org.torproject.android.ui.v3onionservice"
android:exported="false" />
+ <provider
+ android:authorities="org.torproject.android.ui.v3onionservice.clientauth"
+ android:name=".ui.v3onionservice.clientauth.ClientAuthContentProvider"
+ android:exported="false"/>
+
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="org.torproject.android.ui.hiddenservices.storage"
diff --git a/app/src/main/java/org/torproject/android/OrbotMainActivity.java b/app/src/main/java/org/torproject/android/OrbotMainActivity.java
index d58381d4..bf36020b 100644
--- a/app/src/main/java/org/torproject/android/OrbotMainActivity.java
+++ b/app/src/main/java/org/torproject/android/OrbotMainActivity.java
@@ -74,8 +74,8 @@ import org.torproject.android.ui.hiddenservices.providers.HSContentProvider;
import org.torproject.android.ui.onboarding.BridgeWizardActivity;
import org.torproject.android.ui.onboarding.OnboardingActivity;
import org.torproject.android.ui.v3onionservice.OnionServiceContentProvider;
-import org.torproject.android.ui.v3onionservice.OnionServicesActivity;
-import org.torproject.android.ui.v3onionservice.V3ClientAuthActivity;
+import org.torproject.android.ui.v3onionservice.OnionServiceActivity;
+import org.torproject.android.ui.v3onionservice.clientauth.ClientAuthActivity;
import java.io.File;
import java.io.UnsupportedEncodingException;
@@ -468,9 +468,9 @@ public class OrbotMainActivity extends AppCompatActivity implements OrbotConstan
}
} else if (item.getItemId() == R.id.menu_v3_onion_services) {
- startActivity(new Intent(this, OnionServicesActivity.class));
+ startActivity(new Intent(this, OnionServiceActivity.class));
} else if (item.getItemId() == R.id.menu_v3_onion_client_auth) {
- startActivity(new Intent(this, V3ClientAuthActivity.class));
+ startActivity(new Intent(this, ClientAuthActivity.class));
} else if (item.getItemId() == R.id.menu_hidden_services) {
startActivity(new Intent(this, HiddenServicesActivity.class));
} else if (item.getItemId() == R.id.menu_client_cookies) {
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 7ffe2b43..3e1b60a3 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
@@ -15,7 +15,6 @@ import android.widget.ListView;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
-import androidx.appcompat.widget.Toolbar;
import com.google.zxing.integration.android.IntentIntegrator;
import com.google.zxing.integration.android.IntentResult;
@@ -53,10 +52,8 @@ public class ClientCookiesActivity extends AppCompatActivity {
mResolver = getContentResolver();
- findViewById(R.id.fab).setOnClickListener(view -> {
- AddCookieDialog dialog = new AddCookieDialog();
- dialog.show(getSupportFragmentManager(), "AddCookieDialog");
- });
+ findViewById(R.id.fab).setOnClickListener(view ->
+ new AddCookieDialog().show(getSupportFragmentManager(), AddCookieDialog.class.getSimpleName()));
mAdapter = new ClientCookiesAdapter(this, mResolver.query(CookieContentProvider.CONTENT_URI, CookieContentProvider.PROJECTION, null, null, null), 0);
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 1dbad3cb..af7f1333 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
@@ -12,10 +12,12 @@ import android.widget.Toast;
import org.json.JSONException;
import org.json.JSONObject;
import org.torproject.android.R;
+import org.torproject.android.service.OrbotService;
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 org.torproject.android.ui.v3onionservice.clientauth.ClientAuthContentProvider;
import java.io.File;
import java.io.FileInputStream;
@@ -38,10 +40,6 @@ public class BackupUtils {
mResolver = mContext.getContentResolver();
}
- 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);
ZipUtilities zip = new ZipUtilities(files, zipFile, mResolver);
@@ -49,6 +47,20 @@ public class BackupUtils {
return zipFile.getPath();
}
+ public String createV3AuthBackup(String domain, String keyHash, Uri backupFile) {
+ String fileText = OrbotService.buildV3ClientAuthFile(domain, keyHash);
+ try {
+ ParcelFileDescriptor pfd = mContext.getContentResolver().openFileDescriptor(backupFile, "w");
+ FileOutputStream fos = new FileOutputStream(pfd.getFileDescriptor());
+ fos.write(fileText.getBytes());
+ fos.close();
+ pfd.close();
+ } catch (IOException ioe) {
+ return null;
+ }
+ return backupFile.getPath();
+ }
+
public String createV2ZipBackup(int port, Uri zipFile) {
String[] files = createFilesForZippingV2(port);
ZipUtilities zip = new ZipUtilities(files, zipFile, mResolver);
@@ -59,7 +71,7 @@ public class BackupUtils {
return zipFile.getPath();
}
- // todo also write out authorized clients...
+ // todo this doesn't export data for onions that orbot hosts which have authentication (not supported yet...)
private String[] createFilesForZippingV3(String port) {
final String v3BasePath = getV3BasePath() + "/v3" + port + "/";
final String hostnamePath = v3BasePath + "hostname",
@@ -304,7 +316,6 @@ public class BackupUtils {
Toast.makeText(mContext, R.string.error, Toast.LENGTH_LONG).show();
}
-
public void restoreKeyBackup(int hsPort, Uri hsKeyPath) {
File mHSBasePath = new File(
mContext.getFilesDir().getAbsolutePath(),
@@ -333,6 +344,20 @@ public class BackupUtils {
}
}
+
+ public void restoreClientAuthBackup(String authFileContents) {
+ ContentValues fields = new ContentValues();
+ String[] split = authFileContents.split(":");
+ if (split.length != 4) {
+ Toast.makeText(mContext, R.string.error, Toast.LENGTH_LONG).show();
+ return;
+ }
+ fields.put(ClientAuthContentProvider.V3ClientAuth.DOMAIN, split[0]);
+ fields.put(ClientAuthContentProvider.V3ClientAuth.HASH, split[3]);
+ mResolver.insert(ClientAuthContentProvider.CONTENT_URI, fields);
+ Toast.makeText(mContext, R.string.backup_restored, Toast.LENGTH_LONG).show();
+ }
+
public void restoreCookieBackup(String jString) {
try {
JSONObject savedValues = new JSONObject(jString);
diff --git a/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/AddCookieDialog.java b/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/AddCookieDialog.java
index a0e584c4..1da1d1e4 100644
--- a/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/AddCookieDialog.java
+++ b/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/AddCookieDialog.java
@@ -76,7 +76,11 @@ public class AddCookieDialog extends DialogFragment {
String onion = etOnion.getText().toString();
String cookie = etCookie.getText().toString();
if (TextUtils.isEmpty(onion.trim()) || TextUtils.isEmpty(cookie.trim())) return false;
- return BackupUtils.isV2OnionAddressValid(onion);
+ return isV2OnionAddressValid(onion);
+ }
+
+ private static boolean isV2OnionAddressValid(String onionToTest) {
+ return onionToTest.matches("([a-z0-9]{16}).onion");
}
private void saveData(String domain, String cookie) {
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 4d26270d..17ce59a7 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
@@ -6,6 +6,7 @@ import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
+import android.text.Html;
import android.widget.Toast;
import androidx.annotation.NonNull;
@@ -35,7 +36,7 @@ public class OnionServiceActionsDialogFragment extends DialogFragment {
AlertDialog ad = new AlertDialog.Builder(getActivity())
.setItems(new CharSequence[]{
getString(R.string.copy_address_to_clipboard),
- getString(R.string.backup_service),
+ Html.fromHtml(getString(R.string.backup_service)),
getString(R.string.delete_service)}, null)
.setNegativeButton(android.R.string.cancel, (dialog, which) -> dialog.dismiss())
.setTitle(R.string.hidden_services)
@@ -46,14 +47,14 @@ public class OnionServiceActionsDialogFragment extends DialogFragment {
if (position == 0) doCopy(arguments, getContext());
else if (position == 1) doBackup(arguments, getContext());
else if (position == 2)
- new DeleteOnionServiceDialogFragment(arguments).show(getFragmentManager(), DeleteOnionServiceDialogFragment.class.getSimpleName());
+ new OnionServiceDeleteDialogFragment(arguments).show(getFragmentManager(), OnionServiceDeleteDialogFragment.class.getSimpleName());
if (position != 1) dismiss();
});
return ad;
}
private void doCopy(Bundle arguments, Context context) {
- String onion = arguments.getString(OnionServicesActivity.BUNDLE_KEY_DOMAIN);
+ String onion = arguments.getString(OnionServiceActivity.BUNDLE_KEY_DOMAIN);
if (onion == null)
Toast.makeText(context, R.string.please_restart_Orbot_to_enable_the_changes, Toast.LENGTH_LONG).show();
else
@@ -61,8 +62,8 @@ public class OnionServiceActionsDialogFragment extends DialogFragment {
}
private void doBackup(Bundle arguments, Context context) {
- String filename = "onion_service" + arguments.getString(OnionServicesActivity.BUNDLE_KEY_PORT) + ".zip";
- if (arguments.getString(OnionServicesActivity.BUNDLE_KEY_DOMAIN) == null) {
+ String filename = "onion_service" + arguments.getString(OnionServiceActivity.BUNDLE_KEY_PORT) + ".zip";
+ if (arguments.getString(OnionServiceActivity.BUNDLE_KEY_DOMAIN) == null) {
Toast.makeText(context, R.string.please_restart_Orbot_to_enable_the_changes, Toast.LENGTH_LONG).show();
return;
}
@@ -85,7 +86,7 @@ public class OnionServiceActionsDialogFragment extends DialogFragment {
private void attemptToWriteBackup(Uri outputFile) {
BackupUtils backupUtils = new BackupUtils(getContext());
- String backup = backupUtils.createV3ZipBackup(getArguments().getString(OnionServicesActivity.BUNDLE_KEY_PORT), outputFile);
+ String backup = backupUtils.createV3ZipBackup(getArguments().getString(OnionServiceActivity.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/OnionServiceActivity.java
similarity index 94%
rename from app/src/main/java/org/torproject/android/ui/v3onionservice/OnionServicesActivity.java
rename to app/src/main/java/org/torproject/android/ui/v3onionservice/OnionServiceActivity.java
index 64a014e4..39db0b10 100644
--- a/app/src/main/java/org/torproject/android/ui/v3onionservice/OnionServicesActivity.java
+++ b/app/src/main/java/org/torproject/android/ui/v3onionservice/OnionServiceActivity.java
@@ -30,7 +30,7 @@ import org.torproject.android.ui.hiddenservices.permissions.PermissionManager;
import java.io.File;
-public class OnionServicesActivity extends AppCompatActivity {
+public class OnionServiceActivity extends AppCompatActivity {
static final String BUNDLE_KEY_ID = "id", BUNDLE_KEY_PORT = "port", BUNDLE_KEY_DOMAIN = "domain";
private static final String BASE_WHERE_SELECTION_CLAUSE = OnionServiceContentProvider.OnionService.CREATED_BY_USER + "=";
@@ -51,7 +51,7 @@ public class OnionServicesActivity extends AppCompatActivity {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
fab = findViewById(R.id.fab);
- fab.setOnClickListener(v -> new NewOnionServiceDialogFragment().show(getSupportFragmentManager(), NewOnionServiceDialogFragment.class.getSimpleName()));
+ fab.setOnClickListener(v -> new OnionServiceCreateDialogFragment().show(getSupportFragmentManager(), OnionServiceCreateDialogFragment.class.getSimpleName()));
mContentResolver = getContentResolver();
mAdapter = new OnionV3ListAdapter(this, mContentResolver.query(OnionServiceContentProvider.CONTENT_URI, OnionServiceContentProvider.PROJECTION, BASE_WHERE_SELECTION_CLAUSE + '1', null, null), 0);
@@ -123,8 +123,7 @@ public class OnionServicesActivity extends AppCompatActivity {
private void doRestoreLegacy() { // APIs 16, 17, 18
File backupDir = DiskUtils.getOrCreateLegacyBackupDir(getString(R.string.app_name));
File[] files = backupDir.listFiles(ZipUtilities.FILTER_ZIP_FILES);
- if (files == null) return;
- if (files.length == 0) {
+ if (files == null || files.length == 0) {
Toast.makeText(this, R.string.create_a_backup_first, Toast.LENGTH_LONG).show();
return;
}
@@ -168,9 +167,9 @@ public class OnionServicesActivity extends AppCompatActivity {
OnionServiceContentProvider.OnionService.ENABLED + "=1", null, null);
if (activeServices == null) return;
if (activeServices.getCount() > 0)
- PermissionManager.requestBatteryPermissions(OnionServicesActivity.this, getApplicationContext());
+ PermissionManager.requestBatteryPermissions(OnionServiceActivity.this, getApplicationContext());
else
- PermissionManager.requestDropBatteryPermissions(OnionServicesActivity.this, getApplicationContext());
+ PermissionManager.requestDropBatteryPermissions(OnionServiceActivity.this, getApplicationContext());
activeServices.close();
}
}
diff --git a/app/src/main/java/org/torproject/android/ui/v3onionservice/OnionServiceContentProvider.java b/app/src/main/java/org/torproject/android/ui/v3onionservice/OnionServiceContentProvider.java
index d61f7935..61b8540a 100644
--- a/app/src/main/java/org/torproject/android/ui/v3onionservice/OnionServiceContentProvider.java
+++ b/app/src/main/java/org/torproject/android/ui/v3onionservice/OnionServiceContentProvider.java
@@ -46,13 +46,10 @@ public class OnionServiceContentProvider extends ContentProvider {
@Nullable
@Override
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
- String where = selection;
- if (uriMatcher.match(uri) == ONION_ID) {
- where = "_id=" + uri.getLastPathSegment();
- }
-
+ if (uriMatcher.match(uri) == ONION_ID)
+ selection = "_id=" + uri.getLastPathSegment();
SQLiteDatabase db = mDatabase.getReadableDatabase();
- return db.query(OnionServiceDatabase.ONION_SERVICE_TABLE_NAME, projection, where, selectionArgs, null, null, sortOrder);
+ return db.query(OnionServiceDatabase.ONION_SERVICE_TABLE_NAME, projection, selection, selectionArgs, null, null, sortOrder);
}
@Nullable
@@ -80,9 +77,8 @@ public class OnionServiceContentProvider extends ContentProvider {
@Override
public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
- if (uriMatcher.match(uri) == ONION_ID) {
+ if (uriMatcher.match(uri) == ONION_ID)
selection = "_id=" + uri.getLastPathSegment();
- }
SQLiteDatabase db = mDatabase.getWritableDatabase();
int rows = db.delete(OnionServiceDatabase.ONION_SERVICE_TABLE_NAME, selection, selectionArgs);
getContext().getContentResolver().notifyChange(CONTENT_URI, null);
@@ -92,12 +88,9 @@ public class OnionServiceContentProvider extends ContentProvider {
@Override
public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) {
SQLiteDatabase db = mDatabase.getWritableDatabase();
- String where = selection;
- if (uriMatcher.match(uri) == ONION_ID) {
- where = "_id=" + uri.getLastPathSegment();
- }
-
- int rows = db.update(OnionServiceDatabase.ONION_SERVICE_TABLE_NAME, values, where, null);
+ if (uriMatcher.match(uri) == ONION_ID)
+ selection = "_id=" + uri.getLastPathSegment();
+ int rows = db.update(OnionServiceDatabase.ONION_SERVICE_TABLE_NAME, values, selection, null);
getContext().getContentResolver().notifyChange(CONTENT_URI, null);
return rows;
}
diff --git a/app/src/main/java/org/torproject/android/ui/v3onionservice/NewOnionServiceDialogFragment.java b/app/src/main/java/org/torproject/android/ui/v3onionservice/OnionServiceCreateDialogFragment.java
similarity index 96%
rename from app/src/main/java/org/torproject/android/ui/v3onionservice/NewOnionServiceDialogFragment.java
rename to app/src/main/java/org/torproject/android/ui/v3onionservice/OnionServiceCreateDialogFragment.java
index 1929eb72..1d22ac6c 100644
--- a/app/src/main/java/org/torproject/android/ui/v3onionservice/NewOnionServiceDialogFragment.java
+++ b/app/src/main/java/org/torproject/android/ui/v3onionservice/OnionServiceCreateDialogFragment.java
@@ -19,7 +19,7 @@ import androidx.fragment.app.DialogFragment;
import org.torproject.android.R;
-public class NewOnionServiceDialogFragment extends DialogFragment {
+public class OnionServiceCreateDialogFragment extends DialogFragment {
private EditText etServer, etLocalPort, etOnionPort;
private TextWatcher inputValidator;
@@ -90,7 +90,7 @@ public class NewOnionServiceDialogFragment extends DialogFragment {
fields.put(OnionServiceContentProvider.OnionService.PORT, localPort);
fields.put(OnionServiceContentProvider.OnionService.ONION_PORT, onionPort);
fields.put(OnionServiceContentProvider.OnionService.CREATED_BY_USER, 1);
- ContentResolver cr = getContext().getContentResolver();
+ ContentResolver cr = context.getContentResolver();
cr.insert(OnionServiceContentProvider.CONTENT_URI, fields);
Toast.makeText(context, R.string.please_restart_Orbot_to_enable_the_changes, Toast.LENGTH_LONG).show();
}
diff --git a/app/src/main/java/org/torproject/android/ui/v3onionservice/OnionServiceDatabase.java b/app/src/main/java/org/torproject/android/ui/v3onionservice/OnionServiceDatabase.java
index f2a812e8..558babac 100644
--- a/app/src/main/java/org/torproject/android/ui/v3onionservice/OnionServiceDatabase.java
+++ b/app/src/main/java/org/torproject/android/ui/v3onionservice/OnionServiceDatabase.java
@@ -20,7 +20,7 @@ public class OnionServiceDatabase extends SQLiteOpenHelper {
"enabled INTEGER DEFAULT 1, " +
"port INTEGER);";
- public OnionServiceDatabase(Context context) {
+ OnionServiceDatabase(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
@@ -31,7 +31,6 @@ public class OnionServiceDatabase extends SQLiteOpenHelper {
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
-
}
diff --git a/app/src/main/java/org/torproject/android/ui/v3onionservice/DeleteOnionServiceDialogFragment.java b/app/src/main/java/org/torproject/android/ui/v3onionservice/OnionServiceDeleteDialogFragment.java
similarity index 80%
rename from app/src/main/java/org/torproject/android/ui/v3onionservice/DeleteOnionServiceDialogFragment.java
rename to app/src/main/java/org/torproject/android/ui/v3onionservice/OnionServiceDeleteDialogFragment.java
index 5028e8f3..71111fe1 100644
--- a/app/src/main/java/org/torproject/android/ui/v3onionservice/DeleteOnionServiceDialogFragment.java
+++ b/app/src/main/java/org/torproject/android/ui/v3onionservice/OnionServiceDeleteDialogFragment.java
@@ -12,12 +12,11 @@ import androidx.fragment.app.DialogFragment;
import org.torproject.android.R;
import org.torproject.android.core.DiskUtils;
import org.torproject.android.service.TorServiceConstants;
-import org.torproject.android.ui.hiddenservices.HiddenServicesActivity;
import java.io.File;
-public class DeleteOnionServiceDialogFragment extends DialogFragment {
- DeleteOnionServiceDialogFragment(Bundle arguments) {
+public class OnionServiceDeleteDialogFragment extends DialogFragment {
+ OnionServiceDeleteDialogFragment(Bundle arguments) {
super();
setArguments(arguments);
}
@@ -33,9 +32,9 @@ public class DeleteOnionServiceDialogFragment extends DialogFragment {
}
private void doDelete(Bundle arguments, Context context) {
- context.getContentResolver().delete(OnionServiceContentProvider.CONTENT_URI, OnionServiceContentProvider.OnionService._ID + '=' + arguments.getInt(OnionServicesActivity.BUNDLE_KEY_ID), null);
+ context.getContentResolver().delete(OnionServiceContentProvider.CONTENT_URI, OnionServiceContentProvider.OnionService._ID + '=' + arguments.getInt(OnionServiceActivity.BUNDLE_KEY_ID), null);
String base = context.getFilesDir().getAbsolutePath() + "/" + TorServiceConstants.ONION_SERVICES_DIR;
- DiskUtils.recursivelyDeleteDirectory(new File(base, "v3" + arguments.getString(OnionServicesActivity.BUNDLE_KEY_PORT)));
+ DiskUtils.recursivelyDeleteDirectory(new File(base, "v3" + arguments.getString(OnionServiceActivity.BUNDLE_KEY_PORT)));
}
}
diff --git a/app/src/main/java/org/torproject/android/ui/v3onionservice/OnionV3ListAdapter.java b/app/src/main/java/org/torproject/android/ui/v3onionservice/OnionV3ListAdapter.java
index c0489d3c..6a858a9a 100644
--- a/app/src/main/java/org/torproject/android/ui/v3onionservice/OnionV3ListAdapter.java
+++ b/app/src/main/java/org/torproject/android/ui/v3onionservice/OnionV3ListAdapter.java
@@ -14,7 +14,6 @@ import android.widget.Toast;
import androidx.appcompat.widget.SwitchCompat;
import org.torproject.android.R;
-import org.torproject.android.ui.hiddenservices.providers.HSContentProvider;
public class OnionV3ListAdapter extends CursorAdapter {
diff --git a/app/src/main/java/org/torproject/android/ui/v3onionservice/V3ClientAuthActivity.java b/app/src/main/java/org/torproject/android/ui/v3onionservice/V3ClientAuthActivity.java
deleted file mode 100644
index 53a35f30..00000000
--- a/app/src/main/java/org/torproject/android/ui/v3onionservice/V3ClientAuthActivity.java
+++ /dev/null
@@ -1,25 +0,0 @@
-package org.torproject.android.ui.v3onionservice;
-
-import android.os.Bundle;
-
-import androidx.annotation.Nullable;
-import androidx.appcompat.app.AppCompatActivity;
-
-import org.torproject.android.R;
-
-public class V3ClientAuthActivity extends AppCompatActivity {
- @Override
- protected void onCreate(@Nullable Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_v3auth);
-
- setSupportActionBar(findViewById(R.id.toolbar));
- getSupportActionBar().setDisplayHomeAsUpEnabled(true);
-
- findViewById(R.id.fab).setOnClickListener(v -> {
- new AddV3ClientAuthDialogFragment().show(getSupportFragmentManager(), "AddV3ClientAuthDialogFragment");
- });
-
-
- }
-}
diff --git a/app/src/main/java/org/torproject/android/ui/v3onionservice/clientauth/ClientAuthActionsDialogFragment.java b/app/src/main/java/org/torproject/android/ui/v3onionservice/clientauth/ClientAuthActionsDialogFragment.java
new file mode 100644
index 00000000..af5c6844
--- /dev/null
+++ b/app/src/main/java/org/torproject/android/ui/v3onionservice/clientauth/ClientAuthActionsDialogFragment.java
@@ -0,0 +1,42 @@
+package org.torproject.android.ui.v3onionservice.clientauth;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.os.Bundle;
+import android.text.Html;
+
+import androidx.annotation.NonNull;
+import androidx.fragment.app.DialogFragment;
+
+import org.torproject.android.R;
+
+public class ClientAuthActionsDialogFragment extends DialogFragment {
+
+ public ClientAuthActionsDialogFragment() {}
+
+ public ClientAuthActionsDialogFragment(Bundle args) {
+ super();
+ setArguments(args);
+ }
+
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ AlertDialog ad = new AlertDialog.Builder(getActivity())
+ .setTitle(R.string.v3_client_auth)
+ .setItems(new CharSequence[]{
+ Html.fromHtml(getString(R.string.v3_backup_key)),
+ getString(R.string.v3_delete_client_authorization)
+ }, null)
+ .setNegativeButton(android.R.string.cancel, (dialog, which) -> dialog.dismiss())
+ .create();
+ ad.getListView().setOnItemClickListener((parent, view, position, id) -> {
+ if (position == 0)
+ new ClientAuthBackupDialogFragment(getArguments()).show(getActivity().getSupportFragmentManager(), ClientAuthBackupDialogFragment.class.getSimpleName());
+ else
+ new ClientAuthDeleteDialogFragment(getArguments()).show(getActivity().getSupportFragmentManager(), ClientAuthDeleteDialogFragment.class.getSimpleName());
+ ad.dismiss();
+ });
+ return ad;
+ }
+}
diff --git a/app/src/main/java/org/torproject/android/ui/v3onionservice/clientauth/ClientAuthActivity.java b/app/src/main/java/org/torproject/android/ui/v3onionservice/clientauth/ClientAuthActivity.java
new file mode 100644
index 00000000..b24b81f7
--- /dev/null
+++ b/app/src/main/java/org/torproject/android/ui/v3onionservice/clientauth/ClientAuthActivity.java
@@ -0,0 +1,142 @@
+package org.torproject.android.ui.v3onionservice.clientauth;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.widget.ListView;
+import android.widget.Toast;
+
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AlertDialog;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.fragment.app.Fragment;
+
+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;
+import org.torproject.android.ui.hiddenservices.backup.ZipUtilities;
+
+import java.io.File;
+import java.util.List;
+
+public class ClientAuthActivity extends AppCompatActivity {
+
+ public static final String BUNDLE_KEY_ID = "_id",
+ BUNDLE_KEY_DOMAIN = "domain",
+ BUNDLE_KEY_HASH = "key_hash_value";
+
+ private ContentResolver mResolver;
+ private ClientAuthListAdapter mAdapter;
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_v3auth);
+
+ setSupportActionBar(findViewById(R.id.toolbar));
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+
+ mResolver = getContentResolver();
+ mAdapter = new ClientAuthListAdapter(this, mResolver.query(ClientAuthContentProvider.CONTENT_URI, ClientAuthContentProvider.PROJECTION, null, null, null), 0);
+ mResolver.registerContentObserver(ClientAuthContentProvider.CONTENT_URI, true, new V3ClientAuthContentObserver(new Handler()));
+
+ findViewById(R.id.fab).setOnClickListener(v ->
+ new ClientAuthCreateDialogFragment().show(getSupportFragmentManager(), ClientAuthCreateDialogFragment.class.getSimpleName()));
+
+ ListView auths = findViewById(R.id.auth_hash_list);
+ auths.setAdapter(mAdapter);
+ auths.setOnItemClickListener((parent, view, position, id) -> {
+ Cursor item = (Cursor) parent.getItemAtPosition(position);
+ Bundle args = new Bundle();
+ args.putInt(BUNDLE_KEY_ID, item.getInt(item.getColumnIndex(ClientAuthContentProvider.V3ClientAuth._ID)));
+ args.putString(BUNDLE_KEY_DOMAIN, item.getString(item.getColumnIndex(ClientAuthContentProvider.V3ClientAuth.DOMAIN)));
+ args.putString(BUNDLE_KEY_HASH, item.getString(item.getColumnIndex(ClientAuthContentProvider.V3ClientAuth.HASH)));
+ new ClientAuthActionsDialogFragment(args).show(getSupportFragmentManager(), ClientAuthActionsDialogFragment.class.getSimpleName());
+ });
+ }
+
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ if (requestCode == REQUEST_CODE_READ_ZIP_BACKUP && resultCode == RESULT_OK) {
+ Uri uri = data.getData();
+ if (uri != null) {
+ String authText = DiskUtils.readFileFromInputStream(getContentResolver(), uri);
+ new BackupUtils(this).restoreClientAuthBackup(authText);
+ }
+ } else {
+ super.onActivityResult(requestCode, resultCode, data);
+ List<Fragment> frags = getSupportFragmentManager().getFragments();
+ if (frags != null)
+ for (Fragment f : frags) f.onActivityResult(requestCode, resultCode, data);
+ }
+ }
+
+ @Override
+ protected void attachBaseContext(Context base) {
+ super.attachBaseContext(LocaleHelper.onAttach(base));
+ }
+
+ private class V3ClientAuthContentObserver extends ContentObserver {
+ V3ClientAuthContentObserver(Handler handler) {
+ super(handler);
+ }
+
+ @Override
+ public void onChange(boolean selfChange) {
+ mAdapter.changeCursor(mResolver.query(ClientAuthContentProvider.CONTENT_URI, ClientAuthContentProvider.PROJECTION, null, null, null));
+ }
+
+ }
+
+ private static final int REQUEST_CODE_READ_ZIP_BACKUP = 12;
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (item.getItemId() == R.id.menu_restore_backup) {
+ if (DiskUtils.supportsStorageAccessFramework()) {
+ Intent readFileIntent = DiskUtils.createReadFileIntent("text/*");
+ startActivityForResult(readFileIntent, REQUEST_CODE_READ_ZIP_BACKUP);
+ } else { // APIs 16, 17, 18
+ doRestoreLegacy();
+ }
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ private void doRestoreLegacy() {
+ File backupDir = DiskUtils.getOrCreateLegacyBackupDir(getString(R.string.app_name));
+ File[] files = backupDir.listFiles((dir, name) -> name.toLowerCase().endsWith(".auth_private"));
+ if (files == null || files.length == 0) {
+ Toast.makeText(this, R.string.create_a_backup_first, Toast.LENGTH_LONG).show();
+ return;
+ }
+
+ CharSequence[] fileNames = new CharSequence[files.length];
+ for (int i = 0; i < files.length; i++) fileNames[i] = files[i].getName();
+
+ new AlertDialog.Builder(this)
+ .setTitle(R.string.restore_backup)
+ .setItems(fileNames, (dialog, which) -> {
+ String authFileText = DiskUtils.readFile(getContentResolver(), files[which]);
+ new BackupUtils(this).restoreClientAuthBackup(authFileText);
+ })
+ .show();
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.hs_menu, menu);
+ return true;
+ }
+
+
+}
diff --git a/app/src/main/java/org/torproject/android/ui/v3onionservice/clientauth/ClientAuthBackupDialogFragment.java b/app/src/main/java/org/torproject/android/ui/v3onionservice/clientauth/ClientAuthBackupDialogFragment.java
new file mode 100644
index 00000000..c924fe0e
--- /dev/null
+++ b/app/src/main/java/org/torproject/android/ui/v3onionservice/clientauth/ClientAuthBackupDialogFragment.java
@@ -0,0 +1,90 @@
+package org.torproject.android.ui.v3onionservice.clientauth;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.fragment.app.DialogFragment;
+
+import org.torproject.android.R;
+import org.torproject.android.core.DiskUtils;
+import org.torproject.android.core.ui.NoPersonalizedLearningEditText;
+import org.torproject.android.ui.hiddenservices.backup.BackupUtils;
+
+import java.io.File;
+
+public class ClientAuthBackupDialogFragment extends DialogFragment {
+
+ private NoPersonalizedLearningEditText etFilename;
+
+ public ClientAuthBackupDialogFragment() {
+ }
+
+ public ClientAuthBackupDialogFragment(Bundle args) {
+ super();
+ setArguments(args);
+ }
+
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ AlertDialog ad = new AlertDialog.Builder(getContext())
+ .setTitle(R.string.v3_backup_key)
+ .setMessage(R.string.v3_backup_key_warning)
+ .setPositiveButton(R.string.confirm, null)
+ .setNegativeButton(android.R.string.cancel, (dialog, which) -> dialog.dismiss())
+ .create();
+ ad.setOnShowListener(dialog -> ad.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(v -> doBackup()));
+ FrameLayout container = new FrameLayout(ad.getContext());
+ FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
+ int margin = getResources().getDimensionPixelOffset(R.dimen.alert_dialog_margin);
+ params.leftMargin = margin;
+ params.rightMargin = margin;
+ etFilename = new NoPersonalizedLearningEditText(ad.getContext(), null);
+ etFilename.setSingleLine(true);
+ etFilename.setHint(R.string.v3_backup_name_hint);
+ etFilename.setLayoutParams(params);
+ container.addView(etFilename);
+ ad.setView(container);
+ return ad;
+ }
+
+ private void doBackup() {
+ String filename = etFilename.getText().toString().trim();
+ if (filename.equals("")) filename = "filename";
+ filename += ".auth_private";
+ if (DiskUtils.supportsStorageAccessFramework()) {
+ Intent createFileIntent = DiskUtils.createWriteFileIntent(filename, "text/*");
+ getActivity().startActivityForResult(createFileIntent, REQUEST_CODE_WRITE_FILE);
+ } else { // APIs 16, 17, 18
+ attemptToWriteBackup(Uri.fromFile(new File(DiskUtils.getOrCreateLegacyBackupDir("Orbot"), filename)));
+ }
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ if (requestCode == REQUEST_CODE_WRITE_FILE && resultCode == Activity.RESULT_OK) {
+ if (data != null) {
+ attemptToWriteBackup(data.getData());
+ }
+ }
+ }
+
+ private void attemptToWriteBackup(Uri outputFile) {
+ BackupUtils backupUtils = new BackupUtils(getContext());
+ String domain = getArguments().getString(ClientAuthActivity.BUNDLE_KEY_DOMAIN);
+ String hash = getArguments().getString(ClientAuthActivity.BUNDLE_KEY_HASH);
+ String backup = backupUtils.createV3AuthBackup(domain, hash, outputFile);
+ Toast.makeText(getContext(), backup != null ? R.string.backup_saved_at_external_storage : R.string.error, Toast.LENGTH_LONG).show();
+ dismiss();
+ }
+
+ private static final int REQUEST_CODE_WRITE_FILE = 432;
+}
diff --git a/app/src/main/java/org/torproject/android/ui/v3onionservice/clientauth/ClientAuthContentProvider.java b/app/src/main/java/org/torproject/android/ui/v3onionservice/clientauth/ClientAuthContentProvider.java
new file mode 100644
index 00000000..549e776f
--- /dev/null
+++ b/app/src/main/java/org/torproject/android/ui/v3onionservice/clientauth/ClientAuthContentProvider.java
@@ -0,0 +1,104 @@
+package org.torproject.android.ui.v3onionservice.clientauth;
+
+import android.content.ContentProvider;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.UriMatcher;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.net.Uri;
+import android.provider.BaseColumns;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+public class ClientAuthContentProvider extends ContentProvider {
+ public static final String[] PROJECTION = {
+ V3ClientAuth._ID,
+ V3ClientAuth.DOMAIN,
+ V3ClientAuth.HASH,
+ V3ClientAuth.ENABLED,
+ };
+ private static final String AUTH = "org.torproject.android.ui.v3onionservice.clientauth";
+ public static final Uri CONTENT_URI = Uri.parse("content://" + AUTH + "/v3auth");
+ private static final int V3AUTHS = 1, V3AUTH_ID = 2;
+
+ private static final UriMatcher uriMatcher;
+
+ static {
+ uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
+ uriMatcher.addURI(AUTH, "v3auth", V3AUTHS);
+ uriMatcher.addURI(AUTH, "v3auth/#", V3AUTH_ID);
+ }
+
+ private ClientAuthDatabase mDatabase;
+
+ @Override
+ public boolean onCreate() {
+ mDatabase = new ClientAuthDatabase(getContext());
+ return true;
+ }
+
+ @Nullable
+ @Override
+ public String getType(@NonNull Uri uri) {
+ int match = uriMatcher.match(uri);
+ switch (match) {
+ case V3AUTHS:
+ return "vnd.android.cursor.dir/vnd.torproject.v3auths";
+ case V3AUTH_ID:
+ return "vnd.android.cursor.item/vnd.torproject.v3auth";
+ default:
+ return null;
+ }
+ }
+
+ @Nullable
+ @Override
+ public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
+ if (uriMatcher.match(uri) == V3AUTH_ID)
+ selection = "_id=" + uri.getLastPathSegment();
+ SQLiteDatabase db = mDatabase.getReadableDatabase();
+ return db.query(ClientAuthDatabase.DATABASE_NAME, projection, selection, selectionArgs, null, null, sortOrder);
+ }
+
+ @Nullable
+ @Override
+ public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
+ SQLiteDatabase db = mDatabase.getWritableDatabase();
+ long regId = db.insert(ClientAuthDatabase.DATABASE_NAME, null, values);
+ getContext().getContentResolver().notifyChange(CONTENT_URI, null);
+ return ContentUris.withAppendedId(CONTENT_URI, regId);
+ }
+
+ @Override
+ public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
+ if (uriMatcher.match(uri) == V3AUTH_ID)
+ selection = "_id=" + uri.getLastPathSegment();
+ SQLiteDatabase db = mDatabase.getWritableDatabase();
+ int rows = db.delete(ClientAuthDatabase.DATABASE_NAME, selection, selectionArgs);
+ getContext().getContentResolver().notifyChange(CONTENT_URI, null);
+ return rows;
+ }
+
+ @Override
+ public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) {
+ SQLiteDatabase db = mDatabase.getWritableDatabase();
+ if (uriMatcher.match(uri) == V3AUTH_ID)
+ selection = "id_=" + uri.getLastPathSegment();
+ int rows = db.update(ClientAuthDatabase.DATABASE_NAME, values, selection, null);
+ getContext().getContentResolver().notifyChange(CONTENT_URI, null);
+ return rows;
+ }
+
+ public static final class V3ClientAuth implements BaseColumns {
+ private V3ClientAuth() {
+ } // no-op
+
+ public static final String
+ DOMAIN = "domain",
+ HASH = "hash",
+ ENABLED = "enabled";
+ }
+
+}
diff --git a/app/src/main/java/org/torproject/android/ui/v3onionservice/AddV3ClientAuthDialogFragment.java b/app/src/main/java/org/torproject/android/ui/v3onionservice/clientauth/ClientAuthCreateDialogFragment.java
similarity index 69%
rename from app/src/main/java/org/torproject/android/ui/v3onionservice/AddV3ClientAuthDialogFragment.java
rename to app/src/main/java/org/torproject/android/ui/v3onionservice/clientauth/ClientAuthCreateDialogFragment.java
index a3efe0d6..5ce4f491 100644
--- a/app/src/main/java/org/torproject/android/ui/v3onionservice/AddV3ClientAuthDialogFragment.java
+++ b/app/src/main/java/org/torproject/android/ui/v3onionservice/clientauth/ClientAuthCreateDialogFragment.java
@@ -1,20 +1,23 @@
-package org.torproject.android.ui.v3onionservice;
+package org.torproject.android.ui.v3onionservice.clientauth;
import android.app.AlertDialog;
import android.app.Dialog;
+import android.content.ContentResolver;
+import android.content.ContentValues;
import android.content.Context;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.View;
import android.widget.EditText;
+import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.fragment.app.DialogFragment;
import org.torproject.android.R;
-public class AddV3ClientAuthDialogFragment extends DialogFragment {
+public class ClientAuthCreateDialogFragment extends DialogFragment {
private EditText etOnionUrl, etKeyHash;
private TextWatcher inputValidator;
@@ -58,7 +61,22 @@ public class AddV3ClientAuthDialogFragment extends DialogFragment {
}
private void doSave(Context context) {
+ String onionName = sanitizeOnionDomainTextField();
+ String hash = etKeyHash.getText().toString();
+ ContentValues fields = new ContentValues();
+ fields.put(ClientAuthContentProvider.V3ClientAuth.DOMAIN, onionName);
+ fields.put(ClientAuthContentProvider.V3ClientAuth.HASH, hash);
+ ContentResolver cr = context.getContentResolver();
+ cr.insert(ClientAuthContentProvider.CONTENT_URI, fields);
+ Toast.makeText(context, R.string.please_restart_Orbot_to_enable_the_changes, Toast.LENGTH_LONG).show();
+ }
+ private String sanitizeOnionDomainTextField() {
+ String domain = ".onion";
+ String onion = etOnionUrl.getText().toString();
+ if (onion.endsWith(domain))
+ return onion.substring(0, onion.indexOf(domain));
+ return onion;
}
@Override
@@ -68,10 +86,7 @@ public class AddV3ClientAuthDialogFragment extends DialogFragment {
}
private boolean checkInput() {
- String domain = ".onion";
- String onion = etOnionUrl.getText().toString();
- if (onion.endsWith(domain))
- onion = onion.substring(0, onion.indexOf(domain));
+ String onion = sanitizeOnionDomainTextField();
if (!onion.matches("([a-z0-9]{56})")) return false;
String hash = etKeyHash.getText().toString();
return hash.matches("([A-Z2-7]{52})");
diff --git a/app/src/main/java/org/torproject/android/ui/v3onionservice/clientauth/ClientAuthDatabase.java b/app/src/main/java/org/torproject/android/ui/v3onionservice/clientauth/ClientAuthDatabase.java
new file mode 100644
index 00000000..50b04bc6
--- /dev/null
+++ b/app/src/main/java/org/torproject/android/ui/v3onionservice/clientauth/ClientAuthDatabase.java
@@ -0,0 +1,30 @@
+package org.torproject.android.ui.v3onionservice.clientauth;
+
+import android.content.Context;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+
+public class ClientAuthDatabase extends SQLiteOpenHelper {
+ static final String DATABASE_NAME = "v3_client_auths";
+ private static final int DATABASE_VERSION = 1;
+
+ private static final String V3_AUTHS_CREATE_SQL =
+ "CREATE TABLE " + DATABASE_NAME + " (" +
+ "_id INTEGER PRIMARY KEY AUTOINCREMENT, " +
+ "domain TEXT, " +
+ "hash TEXT, " +
+ "enabled INTEGER DEFAULT 1);";
+
+ ClientAuthDatabase(Context context) {
+ super(context, DATABASE_NAME, null, DATABASE_VERSION);
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ db.execSQL(V3_AUTHS_CREATE_SQL);
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ }
+}
diff --git a/app/src/main/java/org/torproject/android/ui/v3onionservice/clientauth/ClientAuthDeleteDialogFragment.java b/app/src/main/java/org/torproject/android/ui/v3onionservice/clientauth/ClientAuthDeleteDialogFragment.java
new file mode 100644
index 00000000..6a349f8e
--- /dev/null
+++ b/app/src/main/java/org/torproject/android/ui/v3onionservice/clientauth/ClientAuthDeleteDialogFragment.java
@@ -0,0 +1,36 @@
+package org.torproject.android.ui.v3onionservice.clientauth;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.os.Bundle;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.DialogFragment;
+
+import org.torproject.android.R;
+
+public class ClientAuthDeleteDialogFragment extends DialogFragment {
+
+ public ClientAuthDeleteDialogFragment() {}
+ public ClientAuthDeleteDialogFragment(Bundle args) {
+ super();
+ setArguments(args);
+ }
+
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
+ return new AlertDialog.Builder(getActivity())
+ .setTitle(R.string.v3_delete_client_authorization)
+ .setPositiveButton(R.string.v3_delete_client_authorization_confirm, (dialog, which) -> doDelete())
+ .setNegativeButton(android.R.string.cancel, (dialog, which) -> dialog.dismiss())
+ .create();
+ }
+
+ private void doDelete() {
+ int id = getArguments().getInt(ClientAuthActivity.BUNDLE_KEY_ID);
+ getContext().getContentResolver().delete(ClientAuthContentProvider.CONTENT_URI, ClientAuthContentProvider.V3ClientAuth._ID + "=" + id, null);
+ }
+
+}
diff --git a/app/src/main/java/org/torproject/android/ui/v3onionservice/clientauth/ClientAuthListAdapter.java b/app/src/main/java/org/torproject/android/ui/v3onionservice/clientauth/ClientAuthListAdapter.java
new file mode 100644
index 00000000..8f08225d
--- /dev/null
+++ b/app/src/main/java/org/torproject/android/ui/v3onionservice/clientauth/ClientAuthListAdapter.java
@@ -0,0 +1,49 @@
+package org.torproject.android.ui.v3onionservice.clientauth;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.CursorAdapter;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import androidx.appcompat.widget.SwitchCompat;
+
+import org.torproject.android.R;
+
+public class ClientAuthListAdapter extends CursorAdapter {
+ private final LayoutInflater mLayoutInflator;
+
+ ClientAuthListAdapter(Context context, Cursor cursor, int flags) {
+ super(context, cursor, flags);
+ mLayoutInflator = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ }
+
+ @Override
+ public View newView(Context context, Cursor cursor, ViewGroup parent) {
+ return mLayoutInflator.inflate(R.layout.layout_client_cookie_list_item, null);
+ }
+
+ @Override
+ public void bindView(View view, Context context, Cursor cursor) {
+ int id = cursor.getInt(cursor.getColumnIndex(ClientAuthContentProvider.V3ClientAuth._ID));
+ final String where = ClientAuthContentProvider.V3ClientAuth._ID + "=" + id;
+ TextView domain = view.findViewById(R.id.cookie_onion);
+ String url = cursor.getString(cursor.getColumnIndex(ClientAuthContentProvider.V3ClientAuth.DOMAIN)) + ".onion";
+ domain.setText(url);
+ SwitchCompat enabled = view.findViewById(R.id.cookie_switch);
+ enabled.setChecked(cursor.getInt(cursor.getColumnIndex(ClientAuthContentProvider.V3ClientAuth.ENABLED)) == 1);
+ enabled.setOnCheckedChangeListener((buttonView, isChecked) -> {
+ ContentResolver resolver = context.getContentResolver();
+ ContentValues fields = new ContentValues();
+ fields.put(ClientAuthContentProvider.V3ClientAuth.ENABLED, isChecked);
+ resolver.update(ClientAuthContentProvider.CONTENT_URI, fields, where, null);
+ Toast.makeText(context, R.string.please_restart_Orbot_to_enable_the_changes, Toast.LENGTH_LONG).show();
+ });
+ }
+}
+
diff --git a/app/src/main/res/layout/activity_v3auth.xml b/app/src/main/res/layout/activity_v3auth.xml
index a0e66741..647976c8 100644
--- a/app/src/main/res/layout/activity_v3auth.xml
+++ b/app/src/main/res/layout/activity_v3auth.xml
@@ -21,7 +21,16 @@
</com.google.android.material.appbar.AppBarLayout>
- <include layout="@layout/layout_content_client_cookies" />
+ <FrameLayout
+ app:layout_behavior="@string/appbar_scrolling_view_behavior"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <ListView
+ android:id="@+id/auth_hash_list"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" />
+ </FrameLayout>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"
diff --git a/app/src/main/res/layout/dialog_add_v3_client_auth.xml b/app/src/main/res/layout/dialog_add_v3_client_auth.xml
index ec310a0c..9502da01 100644
--- a/app/src/main/res/layout/dialog_add_v3_client_auth.xml
+++ b/app/src/main/res/layout/dialog_add_v3_client_auth.xml
@@ -8,7 +8,8 @@
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:text="@string/onion"
+ android:text="@string/v3_onion"
+ android:hint="@string/onion"
android:textAppearance="@style/TextAppearance.AppCompat.Widget.PopupMenu.Small" />
<org.torproject.android.core.ui.NoPersonalizedLearningEditText
@@ -21,7 +22,7 @@
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:text="@string/auth_cookie"
+ android:text="@string/v3_key_hash"
android:textAppearance="@style/TextAppearance.AppCompat.Widget.PopupMenu.Small" />
<org.torproject.android.core.ui.NoPersonalizedLearningEditText
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
index c1ac42e8..1ad03b32 100644
--- a/app/src/main/res/values/dimens.xml
+++ b/app/src/main/res/values/dimens.xml
@@ -238,4 +238,5 @@
<dimen name="activity_horizontal_margin">16dp</dimen>
<dimen name="activity_vertical_margin">16dp</dimen>
<dimen name="fab_margin">16dp</dimen>
+ <dimen name="alert_dialog_margin">20dp</dimen>
</resources>
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index a312d6da..e812a3c2 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -169,9 +169,18 @@
<string name="vpn_default_world">Global (Auto)</string>
<string name="hidden_services">Onion Services</string>
- <string name="v2_hidden_services">V2 Onion Services (Deprecated)</string>
- <string name="v3_hosted_services">Hosted V3 Onion Services</string>
- <string name="v3_client_auth">V3 Onion Service Client Authorization</string>
+ <string name="v2_hidden_services">v2 Onion Services (Deprecated)</string>
+ <string name="v3_hosted_services">Hosted v3 Onion Services</string>
+ <string name="v3_client_auth">v3 Onion Service Client Authorization</string>
+ <string name="v3_client_auth_activity_title">v3 Client Authorization</string>
+ <string name="v3_key_hash">X25519 Private Key in Base 32</string>
+ <string name="v3_onion">v3 .onion Domain</string>
+ <string name="v3_backup_key">Backup Client Authorization Key</string>
+ <string name="v3_backup_key_warning">Warning: This Could Expose Your Key to Other Apps</string>
+ <string name="v3_delete_client_authorization">Delete Client Authorization Key</string>
+ <string name="v3_delete_client_authorization_confirm">Delete Client Authorization</string>
+ <string name="v3_backup_name_hint">Backup filenameâ?¦</string>
+ <string name="confirm">Confirm</string>
<string name="title_activity_hidden_services">Onion Services</string>
<string name="menu_hidden_services">Onion Services</string>
<string name="save">Save</string>
@@ -181,7 +190,7 @@
<string name="done">Done!</string>
<string name="copy_address_to_clipboard">Copy address to clipboard</string>
<string name="show_auth_cookie">Show auth cookie</string>
- <string name="backup_service">Backup Service</string>
+ <string name="backup_service">Backup Service <i>(Warning: This Could Expose Your Service Configuration to Other Apps)</i></string>
<string name="delete_service">Delete Service</string>
<string name="backup_saved_at_external_storage">Backup saved at external storage</string>
<string name="backup_restored">Backup restored</string>
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 8ac067b7..f473297c 100644
--- a/appcore/src/main/java/org/torproject/android/core/DiskUtils.kt
+++ b/appcore/src/main/java/org/torproject/android/core/DiskUtils.kt
@@ -72,7 +72,7 @@ object DiskUtils {
}
@JvmStatic
- fun recursivelyDeleteDirectory(directory: File) : Boolean {
+ fun recursivelyDeleteDirectory(directory: File): Boolean {
val contents = directory.listFiles()
contents?.forEach { recursivelyDeleteDirectory(it) }
return directory.delete()
diff --git a/orbotservice/src/main/java/org/torproject/android/service/OrbotService.java b/orbotservice/src/main/java/org/torproject/android/service/OrbotService.java
index b9dd34a1..9a401ac6 100644
--- a/orbotservice/src/main/java/org/torproject/android/service/OrbotService.java
+++ b/orbotservice/src/main/java/org/torproject/android/service/OrbotService.java
@@ -58,6 +58,7 @@ import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
@@ -89,6 +90,7 @@ public class OrbotService extends VpnService implements TorServiceConstants, Orb
private static final Uri V2_HS_CONTENT_URI = Uri.parse("content://org.torproject.android.ui.hiddenservices.providers/hs");
private static final Uri V3_ONION_SERVICES_CONTENT_URI = Uri.parse("content://org.torproject.android.ui.v3onionservice/v3");
private static final Uri COOKIE_CONTENT_URI = Uri.parse("content://org.torproject.android.ui.hiddenservices.providers.cookie/cookie");
+ private static final Uri V3_CLIENT_AUTH_URI = Uri.parse("content://org.torproject.android.ui.v3onionservice.clientauth/v3auth");
private final static String NOTIFICATION_CHANNEL_ID = "orbot_channel_1";
private static final String[] LEGACY_V2_ONION_SERVICE_PROJECTION = new String[]{
OnionService._ID,
@@ -112,6 +114,12 @@ public class OrbotService extends VpnService implements TorServiceConstants, Orb
ClientCookie.DOMAIN,
ClientCookie.AUTH_COOKIE_VALUE,
ClientCookie.ENABLED};
+ private static final String[] V3_CLIENT_AUTH_PROJECTION = new String[]{
+ V3ClientAuth._ID,
+ V3ClientAuth.DOMAIN,
+ V3ClientAuth.HASH,
+ V3ClientAuth.ENABLED
+ };
public static int mPortSOCKS = -1;
public static int mPortHTTP = -1;
public static int mPortDns = TOR_DNS_PORT_DEFAULT;
@@ -135,7 +143,7 @@ public class OrbotService extends VpnService implements TorServiceConstants, Orb
private NotificationManager mNotificationManager = null;
private NotificationCompat.Builder mNotifyBuilder;
private boolean mNotificationShowing = false;
- private File mHSBasePath, mV3OnionBasePath;
+ private File mHSBasePath, mV3OnionBasePath, mV3AuthBasePath;
private ArrayList<Bridge> alBridges = null;
/**
@@ -305,17 +313,17 @@ public class OrbotService extends VpnService implements TorServiceConstants, Orb
private void stopTorAsync() {
- new Thread(() ->{
+ new Thread(() -> {
Log.i("OrbotService", "stopTor");
try {
sendCallbackStatus(STATUS_STOPPING);
sendCallbackLogMessage(getString(R.string.status_shutting_down));
- if (useIPtObfsMeekProxy())
- IPtProxy.stopObfs4Proxy();
+ if (useIPtObfsMeekProxy())
+ IPtProxy.stopObfs4Proxy();
- if (useIPtSnowflakeProxy())
- IPtProxy.stopSnowflake();
+ if (useIPtSnowflakeProxy())
+ IPtProxy.stopSnowflake();
stopTorDaemon(true);
@@ -333,21 +341,19 @@ public class OrbotService extends VpnService implements TorServiceConstants, Orb
}).start();
}
- private static boolean useIPtObfsMeekProxy ()
- {
+ private static boolean useIPtObfsMeekProxy() {
String bridgeList = Prefs.getBridgesList();
- return bridgeList.contains("obfs")||bridgeList.contains("meek");
+ return bridgeList.contains("obfs") || bridgeList.contains("meek");
}
- private static boolean useIPtSnowflakeProxy ()
- {
+ private static boolean useIPtSnowflakeProxy() {
String bridgeList = Prefs.getBridgesList();
return bridgeList.contains("snowflake");
}
- private void startSnowflakeProxy () {
+ private void startSnowflakeProxy() {
//this is using the current, default Tor snowflake infrastructure
- IPtProxy.startSnowflake( "stun:stun.l.google.com:19302", "https://snowflake-broker.azureedge.net/",
+ IPtProxy.startSnowflake("stun:stun.l.google.com:19302", "https://snowflake-broker.azureedge.net/",
"ajax.aspnetcdn.com", null, true, false, true, 3);
}
@@ -448,6 +454,10 @@ public class OrbotService extends VpnService implements TorServiceConstants, Orb
if (!mV3OnionBasePath.isDirectory())
mV3OnionBasePath.mkdirs();
+ mV3AuthBasePath = new File(getFilesDir().getAbsolutePath(), TorServiceConstants.V3_CLIENT_AUTH_DIR);
+ if (!mV3AuthBasePath.isDirectory())
+ mV3AuthBasePath.mkdirs();
+
mEventHandler = new TorEventHandler(this);
if (mNotificationManager == null) {
@@ -780,6 +790,7 @@ public class OrbotService extends VpnService implements TorServiceConstants, Orb
}
}
+
private void updateV3OnionNames() throws SecurityException {
ContentResolver contentResolver = getApplicationContext().getContentResolver();
Cursor onionServices = contentResolver.query(V3_ONION_SERVICES_CONTENT_URI, null, null, null, null);
@@ -1309,8 +1320,7 @@ public class OrbotService extends VpnService implements TorServiceConstants, Orb
if (!TextUtils.isEmpty(builtInBridgeType))
getBridges(builtInBridgeType, extraLines);
- else
- {
+ else {
String[] bridgeListLines = parseBridgesFromSettings(bridgeList);
int bridgeIdx = (int) Math.floor(Math.random() * ((double) bridgeListLines.length));
String bridgeLine = bridgeListLines[bridgeIdx];
@@ -1370,6 +1380,7 @@ public class OrbotService extends VpnService implements TorServiceConstants, Orb
ContentResolver contentResolver = getApplicationContext().getContentResolver();
addV3OnionServicesToTorrc(extraLines, contentResolver);
+ addV3ClientAuthToTorrc(extraLines, contentResolver);
addV2HiddenServicesToTorrc(extraLines, contentResolver);
addV2ClientCookiesToTorrc(extraLines, contentResolver);
return extraLines;
@@ -1428,6 +1439,34 @@ public class OrbotService extends VpnService implements TorServiceConstants, Orb
}
}
+ public static String buildV3ClientAuthFile(String domain, String keyHash) {
+ return domain + ":descriptor:x25519:" + keyHash;
+ }
+
+ private void addV3ClientAuthToTorrc(StringBuffer torrc, ContentResolver contentResolver) {
+ Cursor v3auths = contentResolver.query(V3_CLIENT_AUTH_URI, V3_CLIENT_AUTH_PROJECTION, V3ClientAuth.ENABLED + "=1", null, null);
+ if (v3auths != null) {
+ for (File file : mV3AuthBasePath.listFiles()) {
+ if (!file.isDirectory())
+ file.delete(); // todo the adapter should maybe just write these files and not do this in service...
+ }
+ torrc.append("ClientOnionAuthDir " + mV3AuthBasePath.getAbsolutePath()).append('\n');
+ try {
+ while (v3auths.moveToNext()) {
+ String domain = v3auths.getString(v3auths.getColumnIndex(V3ClientAuth.DOMAIN));
+ String hash = v3auths.getString(v3auths.getColumnIndex(V3ClientAuth.HASH));
+ File authFile = new File(mV3AuthBasePath, domain + ".auth_private");
+ authFile.createNewFile();
+ FileOutputStream fos = new FileOutputStream(authFile);
+ fos.write(buildV3ClientAuthFile(domain, hash).getBytes());
+ fos.close();
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "error adding v3 client auth...");
+ }
+ }
+ }
+
private void addV2ClientCookiesToTorrc(StringBuffer torrc, ContentResolver contentResolver) {
try {
Cursor client_cookies = contentResolver.query(COOKIE_CONTENT_URI, LEGACY_COOKIE_PROJECTION, ClientCookie.ENABLED + "=1", null, null);
@@ -1617,18 +1656,18 @@ public class OrbotService extends VpnService implements TorServiceConstants, Orb
public static final String AUTH_COOKIE = "auth_cookie";
public static final String AUTH_COOKIE_VALUE = "auth_cookie_value";
public static final String ENABLED = "enabled";
+ }
- private OnionService() {
- }
+ public static final class V3ClientAuth implements BaseColumns {
+ public static final String DOMAIN = "domain";
+ public static final String HASH = "hash";
+ public static final String ENABLED = "enabled";
}
public static final class ClientCookie implements BaseColumns {
public static final String DOMAIN = "domain";
public static final String AUTH_COOKIE_VALUE = "auth_cookie_value";
public static final String ENABLED = "enabled";
-
- private ClientCookie() {
- }
}
// for bridge loading from the assets default bridges.txt file
@@ -1654,7 +1693,7 @@ public class OrbotService extends VpnService implements TorServiceConstants, Orb
IPtProxy.startObfs4Proxy("DEBUG", false, false);
if (useIPtSnowflakeProxy())
- startSnowflakeProxy();
+ startSnowflakeProxy();
startTor();
diff --git a/orbotservice/src/main/java/org/torproject/android/service/TorServiceConstants.java b/orbotservice/src/main/java/org/torproject/android/service/TorServiceConstants.java
index 915f149d..e69d8ba6 100644
--- a/orbotservice/src/main/java/org/torproject/android/service/TorServiceConstants.java
+++ b/orbotservice/src/main/java/org/torproject/android/service/TorServiceConstants.java
@@ -113,5 +113,6 @@ public interface TorServiceConstants {
String HIDDEN_SERVICES_DIR = "hidden_services";
String ONION_SERVICES_DIR = "v3_onion_services";
+ String V3_CLIENT_AUTH_DIR = "v3_client_auth";
}
_______________________________________________
tor-commits mailing list
tor-commits@xxxxxxxxxxxxxxxxxxxx
https://lists.torproject.org/cgi-bin/mailman/listinfo/tor-commits