commit 64c4a4627c518862181685a6e5dff8bcbde9b2f5 Author: bim dsnake@protonmail.com 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@lists.torproject.org