[tor-commits] [orbot/master] New feature added: HidServAuth manager and QR share

n8fr8 at torproject.org n8fr8 at torproject.org
Thu Mar 2 04:10:21 UTC 2017


commit 2aa2b4c3703f707d26c44b0b05663cb76cb084a3
Author: arrase <arrase at gmail.com>
Date:   Tue Dec 6 23:10:36 2016 +0100

    New feature added: HidServAuth manager and QR share
---
 app/src/main/AndroidManifest.xml                   | 191 +++++++++++---------
 .../org/torproject/android/OrbotMainActivity.java  |   3 +
 .../ui/hiddenservices/ClientCookiesActivity.java   | 199 +++++++++++++++++++++
 .../ui/hiddenservices/HiddenServicesActivity.java  |  11 +-
 .../adapters/ClienCookiesAdapter.java              |  64 +++++++
 .../ui/hiddenservices/backup/BackupUtils.java      |  93 +++++++++-
 .../ui/hiddenservices/database/HSDatabase.java     |   9 +
 .../ui/hiddenservices/dialogs/AddCookieDialog.java |  84 +++++++++
 .../dialogs/CookieActionsDialog.java               |  95 ++++++++++
 .../hiddenservices/dialogs/CookieDeleteDialog.java |  50 ++++++
 .../ui/hiddenservices/dialogs/HSCookieDialog.java  |  25 +++
 .../hiddenservices/dialogs/SelectBackupDialog.java |  84 ---------
 .../dialogs/SelectCookieBackupDialog.java          |  84 +++++++++
 .../dialogs/SelectHSBackupDialog.java              |  84 +++++++++
 .../providers/CookieContentProvider.java           | 134 ++++++++++++++
 .../ui/hiddenservices/storage/ExternalStorage.java |   4 +-
 .../res/layout/layout_activity_client_cookies.xml  |  35 ++++
 .../res/layout/layout_add_client_cookie_dialog.xml |  64 +++++++
 .../res/layout/layout_client_cookie_list_item.xml  |  24 +++
 .../res/layout/layout_content_client_cookies.xml   |  21 +++
 app/src/main/res/layout/layout_cookie_actions.xml  |  24 +++
 app/src/main/res/layout/layout_hs_cookie.xml       |   6 +
 app/src/main/res/menu/cookie_menu.xml              |  11 ++
 app/src/main/res/menu/orbot_main.xml               |  16 +-
 app/src/main/res/values-v21/styles.xml             |   1 +
 app/src/main/res/values/strings.xml                | 105 ++++++-----
 .../org/torproject/android/service/TorService.java |  45 ++++-
 27 files changed, 1320 insertions(+), 246 deletions(-)

diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 826ce67..7dd9c1c 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -1,28 +1,31 @@
 <?xml version="1.0" encoding="utf-8"?>
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-      package="org.torproject.android"
-      android:versionName="15.2.0-RC-8-multi"
-      android:versionCode="15208000"
-        android:installLocation="auto"
-      >
-    <uses-sdk android:minSdkVersion="16" android:targetSdkVersion="23"/>
-    <!-- 
+    package="org.torproject.android"
+    android:installLocation="auto"
+    android:versionCode="15208000"
+    android:versionName="15.2.0-RC-8-multi">
+
+    <uses-sdk
+        android:minSdkVersion="16"
+        android:targetSdkVersion="23" />
+    <!--
  <permission android:name="org.torproject.android.MANAGE_TOR" 
      android:label="@string/permission_manage_tor_label" 
      android:description="@string/permission_manage_tor_description" 
      android:protectionLevel="signature"/>
  
 	<uses-permission android:name="org.torproject.android.MANAGE_TOR"/>
-	-->
-
-	<uses-permission android:name="android.permission.INTERNET"/>
-	<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
-	<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
-	<uses-permission android:name="android.permission.ACCESS_SUPERUSER" />
+    -->
 
+    <uses-permission android:name="android.permission.INTERNET" />
+    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
+    <uses-permission android:name="android.permission.ACCESS_SUPERUSER" />
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
     <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
 
+    <android:uses-permission android:name="android.permission.READ_PHONE_STATE" />
+
     <application
         android:name=".OrbotApp"
         android:allowBackup="false"
@@ -38,118 +41,114 @@
             android:name=".OrbotMainActivity"
             android:configChanges="orientation|screenSize"
             android:excludeFromRecents="true"
-            android:launchMode="singleTop"
-            >
+            android:launchMode="singleTop">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
-          </intent-filter>
 
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
             <intent-filter>
                 <action android:name="android.intent.action.VIEW" />
+
                 <category android:name="android.intent.category.DEFAULT" />
                 <category android:name="android.intent.category.BROWSABLE" />
+
                 <data android:scheme="bridge" />
             </intent-filter>
-
-         <intent-filter>
+            <intent-filter>
                 <category android:name="android.intent.category.DEFAULT" />
+
                 <action android:name="org.torproject.android.REQUEST_HS_PORT" />
             </intent-filter>
-             <intent-filter>
+            <intent-filter>
                 <category android:name="android.intent.category.DEFAULT" />
+
                 <action android:name="org.torproject.android.START_TOR" />
             </intent-filter>
-
         </activity>
 
-        <!-- 
-        This is for ensuring the background service still runs when/if the app is swiped away
-         -->
-      	<activity
-			android:name=".service.util.DummyActivity"
-			android:theme="@android:style/Theme.Translucent"
-			android:enabled="true"
-			android:allowTaskReparenting="true"
-			android:noHistory="true"
-			android:excludeFromRecents="true"
-			android:alwaysRetainTaskState="false"
-			android:stateNotNeeded="true"
-			android:clearTaskOnLaunch="true"
-			android:finishOnTaskLaunch="true"
-
-			/>
-
-      	<activity
-			android:name=".vpn.VPNEnableActivity" android:label="@string/app_name" android:exported="false"
-			/>
-
-
-      	<activity android:name="org.torproject.android.ui.PromoAppsActivity" android:exported="false"/>
-
-
-      	<activity android:name=".settings.SettingsPreferences"  android:label="@string/app_name"/>
-        <activity android:name=".ui.AppManager"  android:label="@string/app_name"
-			android:theme="@style/Theme.AppCompat"
-			/>
+		<!--         This is for ensuring the background service still runs when/if the app is swiped away -->
+        <activity
+            android:name=".service.util.DummyActivity"
+            android:allowTaskReparenting="true"
+            android:alwaysRetainTaskState="false"
+            android:clearTaskOnLaunch="true"
+            android:enabled="true"
+            android:excludeFromRecents="true"
+            android:finishOnTaskLaunch="true"
+            android:noHistory="true"
+            android:stateNotNeeded="true"
+            android:theme="@android:style/Theme.Translucent" />
+        <activity
+            android:name=".vpn.VPNEnableActivity"
+            android:exported="false"
+            android:label="@string/app_name" />
+        <activity
+            android:name=".ui.PromoAppsActivity"
+            android:exported="false" />
+        <activity
+            android:name=".settings.SettingsPreferences"
+            android:label="@string/app_name" />
+        <activity
+            android:name=".ui.AppManager"
+            android:label="@string/app_name"
+            android:theme="@style/Theme.AppCompat" />
 
         <service
             android:name=".service.TorService"
             android:enabled="true"
             android:permission="android.permission.BIND_VPN_SERVICE"
-            android:stopWithTask="false" >
+            android:stopWithTask="false"></service>
+        <service
+            android:name=".service.vpn.TorVpnService"
+            android:enabled="true"
+            android:permission="android.permission.BIND_VPN_SERVICE">
+            <intent-filter>
+                <action android:name="android.net.VpnService" />
+            </intent-filter>
         </service>
 
-
-		<service
-			android:name=".service.vpn.TorVpnService"
-			android:enabled="true"
-			android:permission="android.permission.BIND_VPN_SERVICE" >
-			<intent-filter>
-				<action android:name="android.net.VpnService"/>
-			</intent-filter>
-		</service>
-
         <receiver
             android:name=".service.StartTorReceiver"
             android:exported="true">
-             <intent-filter>
-                 <action android:name="org.torproject.android.intent.action.START" />
-             </intent-filter>
+            <intent-filter>
+                <action android:name="org.torproject.android.intent.action.START" />
+            </intent-filter>
         </receiver>
+        <receiver
+            android:name=".OnBootReceiver"
+            android:enabled="true"
+            android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.BOOT_COMPLETED" />
+
+                <category android:name="android.intent.category.HOME" />
+            </intent-filter>
+            <intent-filter>
+                <action android:name="android.intent.action.QUICKBOOT_POWERON" />
 
-   	<receiver android:name=".OnBootReceiver"
-   	    android:enabled="true" android:exported="true"
-
-   	    >
-			<intent-filter>
-				<action	android:name="android.intent.action.BOOT_COMPLETED" />
-				  <category android:name="android.intent.category.HOME" />
-			</intent-filter>
-			<intent-filter>
-				<action android:name="android.intent.action.QUICKBOOT_POWERON" />
-				  <category android:name="android.intent.category.HOME" />
-			</intent-filter>
-			<intent-filter>
-				<action android:name="android.intent.action.MEDIA_MOUNTED"/>
-				  <category android:name="android.intent.category.HOME" />
-			</intent-filter>
-		</receiver>
+                <category android:name="android.intent.category.HOME" />
+            </intent-filter>
+            <intent-filter>
+                <action android:name="android.intent.action.MEDIA_MOUNTED" />
+
+                <category android:name="android.intent.category.HOME" />
+            </intent-filter>
+        </receiver>
 
         <activity
             android:name=".ui.hiddenservices.HiddenServicesActivity"
             android:label="@string/title_activity_hidden_services"
-            android:theme="@style/DefaultTheme" >
+            android:theme="@style/DefaultTheme">
             <meta-data
                 android:name="android.support.PARENT_ACTIVITY"
-                android:value="org.torproject.android.OrbotMainActivity" />
+                android:value=".OrbotMainActivity" />
         </activity>
 
         <provider
             android:name=".ui.hiddenservices.providers.HSContentProvider"
-            android:exported="false"
-            android:authorities="org.torproject.android.ui.hiddenservices.providers" />
-
+            android:authorities="org.torproject.android.ui.hiddenservices.providers"
+            android:exported="false" />
         <provider
             android:name="android.support.v4.content.FileProvider"
             android:authorities="org.torproject.android.ui.hiddenservices.storage"
@@ -157,8 +156,22 @@
             android:grantUriPermissions="true">
             <meta-data
                 android:name="android.support.FILE_PROVIDER_PATHS"
-                android:resource="@xml/hidden_services_paths"/>
+                android:resource="@xml/hidden_services_paths" />
         </provider>
 
-</application>
-</manifest> 
+        <activity
+            android:name=".ui.hiddenservices.ClientCookiesActivity"
+            android:label="@string/client_cookies"
+            android:theme="@style/DefaultTheme">
+            <meta-data
+                android:name="android.support.PARENT_ACTIVITY"
+                android:value=".OrbotMainActivity" />
+        </activity>
+
+        <provider
+            android:name=".ui.hiddenservices.providers.CookieContentProvider"
+            android:authorities="org.torproject.android.ui.hiddenservices.providers.cookie"
+            android:exported="false" />
+    </application>
+
+</manifest>
\ No newline at end of file
diff --git a/app/src/main/java/org/torproject/android/OrbotMainActivity.java b/app/src/main/java/org/torproject/android/OrbotMainActivity.java
index caed0c3..3abcdb8 100644
--- a/app/src/main/java/org/torproject/android/OrbotMainActivity.java
+++ b/app/src/main/java/org/torproject/android/OrbotMainActivity.java
@@ -27,6 +27,7 @@ import org.torproject.android.ui.AppManager;
 import org.torproject.android.ui.ImageProgressView;
 import org.torproject.android.ui.PromoAppsActivity;
 import org.torproject.android.ui.Rotate3dAnimation;
+import org.torproject.android.ui.hiddenservices.ClientCookiesActivity;
 import org.torproject.android.ui.hiddenservices.HiddenServicesActivity;
 import org.torproject.android.ui.hiddenservices.backup.BackupUtils;
 import org.torproject.android.ui.hiddenservices.providers.HSContentProvider;
@@ -513,6 +514,8 @@ public class OrbotMainActivity extends AppCompatActivity
 
          } else if (item.getItemId() == R.id.menu_hidden_services) {
              startActivity(new Intent(this, HiddenServicesActivity.class));
+         } else if (item.getItemId() == R.id.menu_client_cookies) {
+             startActivity(new Intent(this, ClientCookiesActivity.class));
          }
      
 		return super.onOptionsItemSelected(item);
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
new file mode 100644
index 0000000..0b94812
--- /dev/null
+++ b/app/src/main/java/org/torproject/android/ui/hiddenservices/ClientCookiesActivity.java
@@ -0,0 +1,199 @@
+package org.torproject.android.ui.hiddenservices;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.os.Handler;
+import android.support.design.widget.FloatingActionButton;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.widget.Toolbar;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.ListView;
+import android.widget.Toast;
+
+import com.google.zxing.integration.android.IntentIntegrator;
+import com.google.zxing.integration.android.IntentResult;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.torproject.android.R;
+import org.torproject.android.ui.hiddenservices.adapters.ClienCookiesAdapter;
+import org.torproject.android.ui.hiddenservices.dialogs.AddCookieDialog;
+import org.torproject.android.ui.hiddenservices.dialogs.CookieActionsDialog;
+import org.torproject.android.ui.hiddenservices.dialogs.SelectCookieBackupDialog;
+import org.torproject.android.ui.hiddenservices.providers.CookieContentProvider;
+import org.torproject.android.ui.hiddenservices.storage.PermissionManager;
+
+public class ClientCookiesActivity extends AppCompatActivity {
+    public final int WRITE_EXTERNAL_STORAGE_FROM_COOKIE_ACTIONBAR = 3;
+
+    private ContentResolver mResolver;
+    private ClienCookiesAdapter mAdapter;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.layout_activity_client_cookies);
+
+        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
+        setSupportActionBar(toolbar);
+        getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+
+        mResolver = getContentResolver();
+
+        FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
+        fab.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View view) {
+                AddCookieDialog dialog = new AddCookieDialog();
+                dialog.show(getSupportFragmentManager(), "AddCookieDialog");
+            }
+        });
+
+        mAdapter = new ClienCookiesAdapter(
+                this,
+                mResolver.query(CookieContentProvider.CONTENT_URI, CookieContentProvider.PROJECTION, null, null, null)
+                , 0);
+
+        mResolver.registerContentObserver(
+                CookieContentProvider.CONTENT_URI, true, new HSObserver(new Handler())
+        );
+
+        ListView cookies = (ListView) findViewById(R.id.clien_cookies_list);
+        cookies.setAdapter(mAdapter);
+
+        cookies.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+
+            @Override
+            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+                Cursor item = (Cursor) parent.getItemAtPosition(position);
+
+                Bundle arguments = new Bundle();
+                arguments.putInt(
+                        "_id", item.getInt(item.getColumnIndex(CookieContentProvider.ClientCookie._ID))
+                );
+
+                arguments.putString(
+                        "domain", item.getString(item.getColumnIndex(CookieContentProvider.ClientCookie.DOMAIN))
+                );
+
+                arguments.putString(
+                        "auth_cookie_value", item.getString(item.getColumnIndex(CookieContentProvider.ClientCookie.AUTH_COOKIE_VALUE))
+                );
+
+                arguments.putInt(
+                        "enabled", item.getInt(item.getColumnIndex(CookieContentProvider.ClientCookie.ENABLED))
+                );
+
+                CookieActionsDialog dialog = new CookieActionsDialog();
+                dialog.setArguments(arguments);
+                dialog.show(getSupportFragmentManager(), "CookieActionsDialog");
+            }
+        });
+
+    }
+
+    @Override
+    public boolean onCreateOptionsMenu(Menu menu) {
+        getMenuInflater().inflate(R.menu.cookie_menu, menu);
+        return true;
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        int id = item.getItemId();
+
+        if (id == R.id.cookie_restore_backup) {
+            if (PermissionManager.usesRuntimePermissions()
+                    && !PermissionManager.hasExternalWritePermission(this)) {
+                PermissionManager.requestPermissions(this, WRITE_EXTERNAL_STORAGE_FROM_COOKIE_ACTIONBAR);
+                return true;
+            }
+
+            SelectCookieBackupDialog dialog = new SelectCookieBackupDialog();
+            dialog.show(getSupportFragmentManager(), "SelectCookieBackupDialog");
+
+        } else if (id == R.id.cookie_from_qr) {
+            IntentIntegrator integrator = new IntentIntegrator(ClientCookiesActivity.this);
+            integrator.initiateScan();
+        }
+
+        return super.onOptionsItemSelected(item);
+    }
+
+    @Override
+    public void onRequestPermissionsResult(int requestCode,
+                                           String permissions[], int[] grantResults) {
+        if (grantResults.length < 1
+                || grantResults[0] != PackageManager.PERMISSION_GRANTED) {
+            return;
+        }
+
+        switch (requestCode) {
+            case WRITE_EXTERNAL_STORAGE_FROM_COOKIE_ACTIONBAR: {
+                SelectCookieBackupDialog dialog = new SelectCookieBackupDialog();
+                dialog.show(getSupportFragmentManager(), "SelectCookieBackupDialog");
+                break;
+            }
+            case CookieActionsDialog.WRITE_EXTERNAL_STORAGE_FROM_COOKIE_ACTION_DIALOG: {
+                Toast.makeText(this, R.string.click_again_for_backup, Toast.LENGTH_LONG).show();
+                break;
+            }
+        }
+    }
+
+    @Override
+    protected void onActivityResult(int request, int response, Intent data) {
+        super.onActivityResult(request, response, data);
+
+        IntentResult scanResult = IntentIntegrator.parseActivityResult(request, response, data);
+
+        if (scanResult == null) return;
+
+        String results = scanResult.getContents();
+
+        if (results == null || results.length() < 1) return;
+
+        try {
+            JSONObject savedValues = new JSONObject(results);
+            ContentValues fields = new ContentValues();
+
+            fields.put(
+                    CookieContentProvider.ClientCookie.DOMAIN,
+                    savedValues.getString(CookieContentProvider.ClientCookie.DOMAIN)
+            );
+
+            fields.put(
+                    CookieContentProvider.ClientCookie.AUTH_COOKIE_VALUE,
+                    savedValues.getString(CookieContentProvider.ClientCookie.AUTH_COOKIE_VALUE)
+            );
+
+            mResolver.insert(CookieContentProvider.CONTENT_URI, fields);
+
+        } catch (JSONException e) {
+            e.printStackTrace();
+            Toast.makeText(this, R.string.error, Toast.LENGTH_LONG).show();
+        }
+    }
+
+    class HSObserver extends ContentObserver {
+        HSObserver(Handler handler) {
+            super(handler);
+        }
+
+        @Override
+        public void onChange(boolean selfChange) {
+            mAdapter.changeCursor(mResolver.query(
+                    CookieContentProvider.CONTENT_URI, CookieContentProvider.PROJECTION, null, null, null
+            ));
+        }
+    }
+
+}
diff --git a/app/src/main/java/org/torproject/android/ui/hiddenservices/HiddenServicesActivity.java b/app/src/main/java/org/torproject/android/ui/hiddenservices/HiddenServicesActivity.java
index c4f55a1..ae2cd55 100644
--- a/app/src/main/java/org/torproject/android/ui/hiddenservices/HiddenServicesActivity.java
+++ b/app/src/main/java/org/torproject/android/ui/hiddenservices/HiddenServicesActivity.java
@@ -24,7 +24,7 @@ import org.torproject.android.R;
 import org.torproject.android.ui.hiddenservices.adapters.OnionListAdapter;
 import org.torproject.android.ui.hiddenservices.dialogs.HSActionsDialog;
 import org.torproject.android.ui.hiddenservices.dialogs.HSDataDialog;
-import org.torproject.android.ui.hiddenservices.dialogs.SelectBackupDialog;
+import org.torproject.android.ui.hiddenservices.dialogs.SelectHSBackupDialog;
 import org.torproject.android.ui.hiddenservices.providers.HSContentProvider;
 import org.torproject.android.ui.hiddenservices.storage.PermissionManager;
 
@@ -156,9 +156,8 @@ public class HiddenServicesActivity extends AppCompatActivity {
                 return true;
             }
 
-            SelectBackupDialog dialog = new SelectBackupDialog();
-            dialog.show(getSupportFragmentManager(), "SelectBackupDialog");
-            return true;
+            SelectHSBackupDialog dialog = new SelectHSBackupDialog();
+            dialog.show(getSupportFragmentManager(), "SelectHSBackupDialog");
         }
 
         return super.onOptionsItemSelected(item);
@@ -174,8 +173,8 @@ public class HiddenServicesActivity extends AppCompatActivity {
 
         switch (requestCode) {
             case WRITE_EXTERNAL_STORAGE_FROM_ACTIONBAR: {
-                SelectBackupDialog dialog = new SelectBackupDialog();
-                dialog.show(getSupportFragmentManager(), "SelectBackupDialog");
+                SelectHSBackupDialog dialog = new SelectHSBackupDialog();
+                dialog.show(getSupportFragmentManager(), "SelectHSBackupDialog");
                 break;
             }
             case HSActionsDialog.WRITE_EXTERNAL_STORAGE_FROM_ACTION_DIALOG: {
diff --git a/app/src/main/java/org/torproject/android/ui/hiddenservices/adapters/ClienCookiesAdapter.java b/app/src/main/java/org/torproject/android/ui/hiddenservices/adapters/ClienCookiesAdapter.java
new file mode 100644
index 0000000..a9d782b
--- /dev/null
+++ b/app/src/main/java/org/torproject/android/ui/hiddenservices/adapters/ClienCookiesAdapter.java
@@ -0,0 +1,64 @@
+package org.torproject.android.ui.hiddenservices.adapters;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.support.v4.widget.CursorAdapter;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.CompoundButton;
+import android.widget.Switch;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import org.torproject.android.R;
+import org.torproject.android.ui.hiddenservices.providers.CookieContentProvider;
+
+public class ClienCookiesAdapter extends CursorAdapter {
+    private LayoutInflater cursorInflater;
+
+    public ClienCookiesAdapter(Context context, Cursor c, int flags) {
+        super(context, c, flags);
+
+        cursorInflater = (LayoutInflater) context.getSystemService(
+                Context.LAYOUT_INFLATER_SERVICE);
+    }
+
+    @Override
+    public void bindView(View view, Context context, Cursor cursor) {
+        final Context mContext = context;
+        int id = cursor.getInt(cursor.getColumnIndex(CookieContentProvider.ClientCookie._ID));
+        final String where = CookieContentProvider.ClientCookie._ID + "=" + id;
+
+        TextView domain = (TextView) view.findViewById(R.id.cookie_onion);
+        domain.setText(cursor.getString(cursor.getColumnIndex(CookieContentProvider.ClientCookie.DOMAIN)));
+
+        Switch enabled = (Switch) view.findViewById(R.id.cookie_switch);
+        enabled.setChecked(
+                cursor.getInt(cursor.getColumnIndex(CookieContentProvider.ClientCookie.ENABLED)) == 1
+        );
+
+        enabled.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
+            @Override
+            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+                ContentResolver resolver = mContext.getContentResolver();
+                ContentValues fields = new ContentValues();
+                fields.put(CookieContentProvider.ClientCookie.ENABLED, isChecked);
+                resolver.update(
+                        CookieContentProvider.CONTENT_URI, fields, where, null
+                );
+
+                Toast.makeText(
+                        mContext, R.string.please_restart_Orbot_to_enable_the_changes, Toast.LENGTH_LONG
+                ).show();
+            }
+        });
+    }
+
+    @Override
+    public View newView(Context context, Cursor cursor, ViewGroup parent) {
+        return cursorInflater.inflate(R.layout.layout_client_cookie_list_item, parent, false);
+    }
+}
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 58911d9..a4790dd 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,6 +12,7 @@ import org.json.JSONException;
 import org.json.JSONObject;
 import org.torproject.android.R;
 import org.torproject.android.service.TorServiceConstants;
+import org.torproject.android.ui.hiddenservices.providers.CookieContentProvider;
 import org.torproject.android.ui.hiddenservices.providers.HSContentProvider;
 import org.torproject.android.ui.hiddenservices.storage.ExternalStorage;
 
@@ -28,21 +29,19 @@ import java.nio.charset.Charset;
 
 public class BackupUtils {
     private final String configFileName = "config.json";
-    private File mHSBasePath;
     private Context mContext;
     private ContentResolver mResolver;
 
     public BackupUtils(Context context) {
         mContext = context;
-        mHSBasePath = new File(
-                mContext.getFilesDir().getAbsolutePath(),
-                TorServiceConstants.HIDDEN_SERVICES_DIR
-        );
-
         mResolver = mContext.getContentResolver();
     }
 
     public String createZipBackup(Integer port) {
+        File mHSBasePath = new File(
+                mContext.getFilesDir().getAbsolutePath(),
+                TorServiceConstants.HIDDEN_SERVICES_DIR
+        );
 
         String configFilePath = mHSBasePath + "/hs" + port + "/" + configFileName;
         String hostnameFilePath = mHSBasePath + "/hs" + port + "/hostname";
@@ -115,6 +114,8 @@ public class BackupUtils {
             return null;
         }
 
+        portData.close();
+
         try {
             FileWriter file = new FileWriter(configFilePath);
             file.write(config.toString());
@@ -124,8 +125,6 @@ public class BackupUtils {
             return null;
         }
 
-        portData.close();
-
         String zip_path = storage_path.getAbsolutePath() + "/hs" + port + ".zip";
         String files[] = {hostnameFilePath, keyFilePath, configFilePath};
 
@@ -139,6 +138,11 @@ public class BackupUtils {
 
     public void restoreZipBackup(File backup) {
 
+        File mHSBasePath = new File(
+                mContext.getFilesDir().getAbsolutePath(),
+                TorServiceConstants.HIDDEN_SERVICES_DIR
+        );
+
         int port;
         Cursor service;
         String backupName = backup.getName();
@@ -154,7 +158,7 @@ public class BackupUtils {
         zip.unzip(hsPath.getAbsolutePath());
 
         File config = new File(configFilePath);
-        FileInputStream stream = null;
+        FileInputStream stream;
 
         try {
             stream = new FileInputStream(config);
@@ -236,6 +240,10 @@ public class BackupUtils {
     }
 
     public void restoreKeyBackup(int hsPort, Uri hsKeyPath) {
+        File mHSBasePath = new File(
+                mContext.getFilesDir().getAbsolutePath(),
+                TorServiceConstants.HIDDEN_SERVICES_DIR
+        );
 
         File serviceDir = new File(mHSBasePath, "hs" + hsPort);
 
@@ -258,4 +266,71 @@ public class BackupUtils {
             e.printStackTrace();
         }
     }
+
+    public void restoreCookieBackup(File p) {
+        File config = new File(p.getAbsolutePath());
+        FileInputStream stream;
+        String jString = null;
+
+        try {
+            stream = new FileInputStream(config);
+            FileChannel fc = stream.getChannel();
+            MappedByteBuffer bb = fc.map(FileChannel.MapMode.READ_ONLY, 0, fc.size());
+            jString = Charset.defaultCharset().decode(bb).toString();
+            stream.close();
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+
+        if (jString == null)
+            Toast.makeText(mContext, R.string.error, Toast.LENGTH_LONG).show();
+
+        try {
+            JSONObject savedValues = new JSONObject(jString);
+            ContentValues fields = new ContentValues();
+
+            fields.put(
+                    CookieContentProvider.ClientCookie.DOMAIN,
+                    savedValues.getString(CookieContentProvider.ClientCookie.DOMAIN)
+            );
+
+            fields.put(
+                    CookieContentProvider.ClientCookie.AUTH_COOKIE_VALUE,
+                    savedValues.getString(CookieContentProvider.ClientCookie.AUTH_COOKIE_VALUE)
+            );
+
+            fields.put(
+                    CookieContentProvider.ClientCookie.ENABLED,
+                    savedValues.getInt(CookieContentProvider.ClientCookie.ENABLED)
+            );
+
+            mResolver.insert(CookieContentProvider.CONTENT_URI, fields);
+
+        } catch (JSONException e) {
+            e.printStackTrace();
+            Toast.makeText(mContext, R.string.error, Toast.LENGTH_LONG).show();
+        }
+
+        Toast.makeText(mContext, R.string.backup_restored, Toast.LENGTH_LONG).show();
+    }
+
+    public String createCookieBackup(String domain, String cookie, Integer enabled) {
+        File storage_path = ExternalStorage.getOrCreateBackupDir();
+        String backupFile = storage_path.getAbsolutePath() + '/' + domain.replace(".onion", ".json");
+
+        JSONObject backup = new JSONObject();
+        try {
+            backup.put(CookieContentProvider.ClientCookie.DOMAIN, domain);
+            backup.put(CookieContentProvider.ClientCookie.AUTH_COOKIE_VALUE, cookie);
+            backup.put(CookieContentProvider.ClientCookie.ENABLED, enabled);
+            FileWriter file = new FileWriter(backupFile);
+            file.write(backup.toString());
+            file.close();
+        } catch (JSONException | IOException e) {
+            e.printStackTrace();
+            return null;
+        }
+
+        return backupFile;
+    }
 }
diff --git a/app/src/main/java/org/torproject/android/ui/hiddenservices/database/HSDatabase.java b/app/src/main/java/org/torproject/android/ui/hiddenservices/database/HSDatabase.java
index 779c775..18bc7aa 100644
--- a/app/src/main/java/org/torproject/android/ui/hiddenservices/database/HSDatabase.java
+++ b/app/src/main/java/org/torproject/android/ui/hiddenservices/database/HSDatabase.java
@@ -8,6 +8,7 @@ import android.database.sqlite.SQLiteOpenHelper;
 public class HSDatabase extends SQLiteOpenHelper {
 
     public static final String HS_DATA_TABLE_NAME = "hs_data";
+    public static final String HS_CLIENT_COOKIE_TABLE_NAME = "hs_client_cookie";
     private static final int DATABASE_VERSION = 2;
     private static final String DATABASE_NAME = "hidden_services";
     private static final String HS_DATA_TABLE_CREATE =
@@ -22,6 +23,13 @@ public class HSDatabase extends SQLiteOpenHelper {
                     "enabled INTEGER DEFAULT 1, " +
                     "port INTEGER);";
 
+    private static final String HS_CLIENT_COOKIE_TABLE_CREATE =
+            "CREATE TABLE " + HS_CLIENT_COOKIE_TABLE_NAME + " (" +
+                    "_id INTEGER PRIMARY KEY AUTOINCREMENT, " +
+                    "domain TEXT, " +
+                    "auth_cookie_value TEXT, " +
+                    "enabled INTEGER DEFAULT 1);";
+
     public HSDatabase(Context context) {
         super(context, DATABASE_NAME, null, DATABASE_VERSION);
     }
@@ -29,6 +37,7 @@ public class HSDatabase extends SQLiteOpenHelper {
     @Override
     public void onCreate(SQLiteDatabase db) {
         db.execSQL(HS_DATA_TABLE_CREATE);
+        db.execSQL(HS_CLIENT_COOKIE_TABLE_CREATE);
     }
 
     @Override
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
new file mode 100644
index 0000000..7566364
--- /dev/null
+++ b/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/AddCookieDialog.java
@@ -0,0 +1,84 @@
+package org.torproject.android.ui.hiddenservices.dialogs;
+
+
+import android.app.Dialog;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.v4.app.DialogFragment;
+import android.support.v7.app.AlertDialog;
+import android.view.View;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.Toast;
+
+import org.torproject.android.R;
+import org.torproject.android.ui.hiddenservices.providers.CookieContentProvider;
+
+public class AddCookieDialog extends DialogFragment {
+
+    @NonNull
+    @Override
+    public Dialog onCreateDialog(Bundle savedInstanceState) {
+
+        final View dialog_view = getActivity().getLayoutInflater().inflate(R.layout.layout_add_client_cookie_dialog, null);
+
+        final AlertDialog addCookieDialog = new AlertDialog.Builder(getActivity())
+                .setView(dialog_view)
+                .setTitle(R.string.client_cookies)
+                .create();
+
+        Button save = (Button) dialog_view.findViewById(R.id.cookie_dialog_save);
+        save.setOnClickListener(new View.OnClickListener() {
+            public void onClick(View v) {
+                String onion = ((EditText) dialog_view.findViewById(R.id.cookie_onion)).getText().toString();
+                String cookie = ((EditText) dialog_view.findViewById(R.id.cookie_value)).getText().toString();
+
+                if (checkInput(onion, cookie)) {
+                    saveData(onion, cookie);
+                    Toast.makeText(
+                            v.getContext(), R.string.please_restart_Orbot_to_enable_the_changes, Toast.LENGTH_LONG
+                    ).show();
+                    addCookieDialog.dismiss();
+                }
+            }
+        });
+
+        Button cancel = (Button) dialog_view.findViewById(R.id.cookie_dialog_cancel);
+        cancel.setOnClickListener(new View.OnClickListener() {
+            public void onClick(View v) {
+                addCookieDialog.cancel();
+            }
+        });
+
+        return addCookieDialog;
+    }
+
+    private boolean checkInput(String onion, String cookie) {
+
+        boolean is_set = ((onion != null && onion.length() > 0) && (cookie != null && cookie.length() > 0));
+        if (!is_set) {
+            Toast.makeText(getContext(), R.string.fields_can_t_be_empty, Toast.LENGTH_SHORT).show();
+            return false;
+        }
+
+        if (!onion.matches("([a-z0-9]{16})\\.onion")) {
+            Toast.makeText(getContext(), R.string.invalid_onion_address, Toast.LENGTH_SHORT).show();
+            return false;
+        }
+
+        return true;
+    }
+
+    private void saveData(String domain, String cookie) {
+
+        ContentValues fields = new ContentValues();
+        fields.put(CookieContentProvider.ClientCookie.DOMAIN, domain);
+        fields.put(CookieContentProvider.ClientCookie.AUTH_COOKIE_VALUE, cookie);
+
+        ContentResolver cr = getContext().getContentResolver();
+
+        cr.insert(CookieContentProvider.CONTENT_URI, fields);
+    }
+}
diff --git a/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/CookieActionsDialog.java b/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/CookieActionsDialog.java
new file mode 100644
index 0000000..e5683da
--- /dev/null
+++ b/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/CookieActionsDialog.java
@@ -0,0 +1,95 @@
+package org.torproject.android.ui.hiddenservices.dialogs;
+
+
+import android.app.Dialog;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.v4.app.DialogFragment;
+import android.support.v7.app.AlertDialog;
+import android.view.View;
+import android.widget.Button;
+import android.widget.Toast;
+
+import org.torproject.android.R;
+import org.torproject.android.ui.hiddenservices.backup.BackupUtils;
+import org.torproject.android.ui.hiddenservices.storage.PermissionManager;
+
+public class CookieActionsDialog extends DialogFragment {
+    public static final int WRITE_EXTERNAL_STORAGE_FROM_COOKIE_ACTION_DIALOG = 4;
+
+    @NonNull
+    @Override
+    public Dialog onCreateDialog(Bundle savedInstanceState) {
+        final Bundle arguments = getArguments();
+
+        final View dialog_view = getActivity().getLayoutInflater().inflate(R.layout.layout_cookie_actions, null);
+        final AlertDialog actionDialog = new AlertDialog.Builder(getActivity())
+                .setView(dialog_view)
+                .setTitle(R.string.client_cookies)
+                .create();
+
+        Button backup = (Button) dialog_view.findViewById(R.id.btn_cookie_backup);
+        backup.setOnClickListener(new View.OnClickListener() {
+            public void onClick(View v) {
+                Context mContext = v.getContext();
+
+                if (PermissionManager.usesRuntimePermissions()
+                        && !PermissionManager.hasExternalWritePermission(mContext)) {
+
+                    PermissionManager.requestPermissions(
+                            getActivity(), WRITE_EXTERNAL_STORAGE_FROM_COOKIE_ACTION_DIALOG);
+
+                    return;
+                }
+
+                BackupUtils backup_utils = new BackupUtils(mContext);
+                String backupPath = backup_utils.createCookieBackup(
+                        arguments.getString("domain"),
+                        arguments.getString("auth_cookie_value"),
+                        arguments.getInt("enabled")
+                );
+
+                if (backupPath == null || backupPath.length() < 1) {
+                    Toast.makeText(mContext, R.string.error, Toast.LENGTH_LONG).show();
+                    actionDialog.dismiss();
+                    return;
+                }
+
+                Toast.makeText(mContext, R.string.backup_saved_at_external_storage, Toast.LENGTH_LONG).show();
+
+                Uri selectedUri = Uri.parse(backupPath.substring(0, backupPath.lastIndexOf("/")));
+                Intent intent = new Intent(Intent.ACTION_VIEW);
+                intent.setDataAndType(selectedUri, "resource/folder");
+
+                if (intent.resolveActivityInfo(mContext.getPackageManager(), 0) != null) {
+                    startActivity(intent);
+                } else {
+                    Toast.makeText(mContext, R.string.filemanager_not_available, Toast.LENGTH_LONG).show();
+                }
+                actionDialog.dismiss();
+            }
+        });
+
+        Button delete = (Button) dialog_view.findViewById(R.id.btn_cookie_delete);
+        delete.setOnClickListener(new View.OnClickListener() {
+            public void onClick(View v) {
+                CookieDeleteDialog dialog = new CookieDeleteDialog();
+                dialog.setArguments(arguments);
+                dialog.show(getFragmentManager(), "CookieDeleteDialog");
+                actionDialog.dismiss();
+            }
+        });
+
+        Button cancel = (Button) dialog_view.findViewById(R.id.btn_cookie_cancel);
+        cancel.setOnClickListener(new View.OnClickListener() {
+            public void onClick(View v) {
+                actionDialog.dismiss();
+            }
+        });
+
+        return actionDialog;
+    }
+}
diff --git a/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/CookieDeleteDialog.java b/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/CookieDeleteDialog.java
new file mode 100644
index 0000000..60c4d8e
--- /dev/null
+++ b/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/CookieDeleteDialog.java
@@ -0,0 +1,50 @@
+package org.torproject.android.ui.hiddenservices.dialogs;
+
+
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.v4.app.DialogFragment;
+import android.support.v7.app.AlertDialog;
+
+import org.torproject.android.R;
+import org.torproject.android.ui.hiddenservices.providers.CookieContentProvider;
+
+public class CookieDeleteDialog extends DialogFragment {
+
+    @NonNull
+    @Override
+    public Dialog onCreateDialog(Bundle savedInstanceState) {
+        final Bundle arguments = getArguments();
+        final Context context = getContext();
+
+        DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() {
+            @Override
+            public void onClick(DialogInterface dialog, int which) {
+                switch (which) {
+                    case DialogInterface.BUTTON_POSITIVE:
+                        // Delete from db
+                        context.getContentResolver().delete(
+                                CookieContentProvider.CONTENT_URI,
+                                CookieContentProvider.ClientCookie._ID + "=" + arguments.getInt("_id"),
+                                null
+                        );
+
+                        break;
+
+                    case DialogInterface.BUTTON_NEGATIVE:
+                        // Do nothing
+                        break;
+                }
+            }
+        };
+
+        return new AlertDialog.Builder(context)
+                .setMessage(R.string.confirm_cookie_deletion)
+                .setPositiveButton(R.string.btn_okay, dialogClickListener)
+                .setNegativeButton(R.string.btn_cancel, dialogClickListener)
+                .create();
+    }
+}
diff --git a/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/HSCookieDialog.java b/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/HSCookieDialog.java
index ff53cec..39c1b51 100644
--- a/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/HSCookieDialog.java
+++ b/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/HSCookieDialog.java
@@ -13,7 +13,12 @@ import android.widget.Button;
 import android.widget.TextView;
 import android.widget.Toast;
 
+import com.google.zxing.integration.android.IntentIntegrator;
+
+import org.json.JSONException;
+import org.json.JSONObject;
 import org.torproject.android.R;
+import org.torproject.android.ui.hiddenservices.providers.CookieContentProvider;
 
 public class HSCookieDialog extends DialogFragment {
 
@@ -44,6 +49,26 @@ public class HSCookieDialog extends DialogFragment {
             }
         });
 
+        Button shareQR = (Button) dialog_view.findViewById(R.id.hs_cookie_to_qr);
+        shareQR.setOnClickListener(new View.OnClickListener() {
+            public void onClick(View v) {
+                try {
+                    JSONObject backup = new JSONObject();
+                    backup.put(CookieContentProvider.ClientCookie.DOMAIN, arguments.getString("onion"));
+                    backup.put(CookieContentProvider.ClientCookie.AUTH_COOKIE_VALUE, arguments.getString("auth_cookie_value"));
+
+                    IntentIntegrator integrator = new IntentIntegrator(getActivity());
+                    integrator.shareText(backup.toString());
+
+                } catch (JSONException e) {
+                    // TODO Auto-generated catch block
+                    e.printStackTrace();
+                }
+
+                cookieDialog.dismiss();
+            }
+        });
+
         Button cancel = (Button) dialog_view.findViewById(R.id.hs_cookie_cancel);
         cancel.setOnClickListener(new View.OnClickListener() {
             public void onClick(View v) {
diff --git a/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/SelectBackupDialog.java b/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/SelectBackupDialog.java
deleted file mode 100644
index 0c0d943..0000000
--- a/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/SelectBackupDialog.java
+++ /dev/null
@@ -1,84 +0,0 @@
-package org.torproject.android.ui.hiddenservices.dialogs;
-
-import android.app.Dialog;
-import android.content.DialogInterface;
-import android.os.Bundle;
-import android.support.annotation.NonNull;
-import android.support.v4.app.DialogFragment;
-import android.support.v7.app.AlertDialog;
-import android.view.View;
-import android.widget.AdapterView;
-import android.widget.ListView;
-
-import org.torproject.android.R;
-import org.torproject.android.ui.hiddenservices.adapters.BackupAdapter;
-import org.torproject.android.ui.hiddenservices.backup.BackupUtils;
-import org.torproject.android.ui.hiddenservices.storage.ExternalStorage;
-
-import java.io.File;
-import java.io.FilenameFilter;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-public class SelectBackupDialog extends DialogFragment {
-
-    @NonNull
-    @Override
-    public Dialog onCreateDialog(Bundle savedInstanceState) {
-        AlertDialog.Builder backupsDialog = new AlertDialog.Builder(getActivity());
-
-        backupsDialog.setTitle(R.string.restore_backup);
-
-        File backupDir = ExternalStorage.getOrCreateBackupDir();
-        File[] files = null;
-
-        try {
-            files = backupDir.listFiles(new FilenameFilter() {
-                @Override
-                public boolean accept(File dir, String name) {
-                    return name.toLowerCase().endsWith(".zip");
-                }
-            });
-        } catch (NullPointerException e) {
-            // Silent block
-        }
-
-        if (files == null || files.length < 1) {
-            backupsDialog.setMessage(R.string.create_a_backup_first);
-            backupsDialog.setNegativeButton(R.string.btn_cancel, new DialogInterface.OnClickListener() {
-                public void onClick(DialogInterface dialog, int id) {
-                    dialog.dismiss();
-                }
-            });
-
-            return backupsDialog.create();
-        }
-
-        final View dialog_view = getActivity().getLayoutInflater().inflate(R.layout.layout_hs_backups_list, null);
-
-        backupsDialog.setView(dialog_view);
-        backupsDialog.setPositiveButton(R.string.btn_okay, new DialogInterface.OnClickListener() {
-            public void onClick(DialogInterface dialog, int id) {
-                dialog.dismiss();
-            }
-        });
-
-        ListView backups = (ListView) dialog_view.findViewById(R.id.listview_hs_backups);
-
-        List<File> zips = new ArrayList<>();
-        Collections.addAll(zips, files);
-
-        backups.setAdapter(new BackupAdapter(getContext(), R.layout.layout_hs_backups_list_item, zips));
-        backups.setOnItemClickListener(new AdapterView.OnItemClickListener() {
-            @Override
-            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
-                BackupUtils backupUtils = new BackupUtils(view.getContext().getApplicationContext());
-                File p = (File) parent.getItemAtPosition(position);
-                backupUtils.restoreZipBackup(p);
-            }
-        });
-
-        return backupsDialog.create();
-    }
-}
diff --git a/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/SelectCookieBackupDialog.java b/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/SelectCookieBackupDialog.java
new file mode 100644
index 0000000..de5b784
--- /dev/null
+++ b/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/SelectCookieBackupDialog.java
@@ -0,0 +1,84 @@
+package org.torproject.android.ui.hiddenservices.dialogs;
+
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.v4.app.DialogFragment;
+import android.support.v7.app.AlertDialog;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.ListView;
+
+import org.torproject.android.R;
+import org.torproject.android.ui.hiddenservices.adapters.BackupAdapter;
+import org.torproject.android.ui.hiddenservices.backup.BackupUtils;
+import org.torproject.android.ui.hiddenservices.storage.ExternalStorage;
+
+import java.io.File;
+import java.io.FilenameFilter;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public class SelectCookieBackupDialog extends DialogFragment {
+
+    @NonNull
+    @Override
+    public Dialog onCreateDialog(Bundle savedInstanceState) {
+        AlertDialog.Builder cookieBackupDialog = new AlertDialog.Builder(getActivity());
+
+        cookieBackupDialog.setTitle(R.string.restore_backup);
+
+        File backupDir = ExternalStorage.getOrCreateBackupDir();
+        File[] files = null;
+
+        try {
+            files = backupDir.listFiles(new FilenameFilter() {
+                @Override
+                public boolean accept(File dir, String name) {
+                    return name.toLowerCase().endsWith(".json");
+                }
+            });
+        } catch (NullPointerException e) {
+            // Silent block
+        }
+
+        if (files == null || files.length < 1) {
+            cookieBackupDialog.setMessage(R.string.create_a_backup_first);
+            cookieBackupDialog.setNegativeButton(R.string.btn_cancel, new DialogInterface.OnClickListener() {
+                public void onClick(DialogInterface dialog, int id) {
+                    dialog.dismiss();
+                }
+            });
+
+            return cookieBackupDialog.create();
+        }
+
+        final View dialog_view = getActivity().getLayoutInflater().inflate(R.layout.layout_hs_backups_list, null);
+
+        cookieBackupDialog.setView(dialog_view);
+        cookieBackupDialog.setPositiveButton(R.string.btn_okay, new DialogInterface.OnClickListener() {
+            public void onClick(DialogInterface dialog, int id) {
+                dialog.dismiss();
+            }
+        });
+
+        ListView backups = (ListView) dialog_view.findViewById(R.id.listview_hs_backups);
+
+        List<File> json_backups = new ArrayList<>();
+        Collections.addAll(json_backups, files);
+
+        backups.setAdapter(new BackupAdapter(getContext(), R.layout.layout_hs_backups_list_item, json_backups));
+        backups.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+            @Override
+            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+                BackupUtils backupUtils = new BackupUtils(view.getContext().getApplicationContext());
+                File p = (File) parent.getItemAtPosition(position);
+                backupUtils.restoreCookieBackup(p);
+            }
+        });
+
+        return cookieBackupDialog.create();
+    }
+}
diff --git a/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/SelectHSBackupDialog.java b/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/SelectHSBackupDialog.java
new file mode 100644
index 0000000..a6e3bac
--- /dev/null
+++ b/app/src/main/java/org/torproject/android/ui/hiddenservices/dialogs/SelectHSBackupDialog.java
@@ -0,0 +1,84 @@
+package org.torproject.android.ui.hiddenservices.dialogs;
+
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.v4.app.DialogFragment;
+import android.support.v7.app.AlertDialog;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.ListView;
+
+import org.torproject.android.R;
+import org.torproject.android.ui.hiddenservices.adapters.BackupAdapter;
+import org.torproject.android.ui.hiddenservices.backup.BackupUtils;
+import org.torproject.android.ui.hiddenservices.storage.ExternalStorage;
+
+import java.io.File;
+import java.io.FilenameFilter;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public class SelectHSBackupDialog extends DialogFragment {
+
+    @NonNull
+    @Override
+    public Dialog onCreateDialog(Bundle savedInstanceState) {
+        AlertDialog.Builder backupsDialog = new AlertDialog.Builder(getActivity());
+
+        backupsDialog.setTitle(R.string.restore_backup);
+
+        File backupDir = ExternalStorage.getOrCreateBackupDir();
+        File[] files = null;
+
+        try {
+            files = backupDir.listFiles(new FilenameFilter() {
+                @Override
+                public boolean accept(File dir, String name) {
+                    return name.toLowerCase().endsWith(".zip");
+                }
+            });
+        } catch (NullPointerException e) {
+            // Silent block
+        }
+
+        if (files == null || files.length < 1) {
+            backupsDialog.setMessage(R.string.create_a_backup_first);
+            backupsDialog.setNegativeButton(R.string.btn_cancel, new DialogInterface.OnClickListener() {
+                public void onClick(DialogInterface dialog, int id) {
+                    dialog.dismiss();
+                }
+            });
+
+            return backupsDialog.create();
+        }
+
+        final View dialog_view = getActivity().getLayoutInflater().inflate(R.layout.layout_hs_backups_list, null);
+
+        backupsDialog.setView(dialog_view);
+        backupsDialog.setPositiveButton(R.string.btn_okay, new DialogInterface.OnClickListener() {
+            public void onClick(DialogInterface dialog, int id) {
+                dialog.dismiss();
+            }
+        });
+
+        ListView backups = (ListView) dialog_view.findViewById(R.id.listview_hs_backups);
+
+        List<File> zips = new ArrayList<>();
+        Collections.addAll(zips, files);
+
+        backups.setAdapter(new BackupAdapter(getContext(), R.layout.layout_hs_backups_list_item, zips));
+        backups.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+            @Override
+            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+                BackupUtils backupUtils = new BackupUtils(view.getContext().getApplicationContext());
+                File p = (File) parent.getItemAtPosition(position);
+                backupUtils.restoreZipBackup(p);
+            }
+        });
+
+        return backupsDialog.create();
+    }
+}
diff --git a/app/src/main/java/org/torproject/android/ui/hiddenservices/providers/CookieContentProvider.java b/app/src/main/java/org/torproject/android/ui/hiddenservices/providers/CookieContentProvider.java
new file mode 100644
index 0000000..f420a6d
--- /dev/null
+++ b/app/src/main/java/org/torproject/android/ui/hiddenservices/providers/CookieContentProvider.java
@@ -0,0 +1,134 @@
+package org.torproject.android.ui.hiddenservices.providers;
+
+import android.content.ContentProvider;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.UriMatcher;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.net.Uri;
+import android.provider.BaseColumns;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+
+import org.torproject.android.ui.hiddenservices.database.HSDatabase;
+
+
+public class CookieContentProvider extends ContentProvider {
+    public static final String[] PROJECTION = new String[]{
+            ClientCookie._ID,
+            ClientCookie.DOMAIN,
+            ClientCookie.AUTH_COOKIE_VALUE,
+            ClientCookie.ENABLED
+    };
+    private static final String AUTH = "org.torproject.android.ui.hiddenservices.providers.cookie";
+    public static final Uri CONTENT_URI =
+            Uri.parse("content://" + AUTH + "/cookie");
+    //UriMatcher
+    private static final int COOKIES = 1;
+    private static final int COOKIE_ID = 2;
+
+    private static final UriMatcher uriMatcher;
+
+    static {
+        uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
+        uriMatcher.addURI(AUTH, "hs", COOKIES);
+        uriMatcher.addURI(AUTH, "hs/#", COOKIE_ID);
+    }
+
+    private HSDatabase mServervices;
+    private Context mContext;
+
+    @Override
+    public boolean onCreate() {
+        mContext = getContext();
+        mServervices = new HSDatabase(mContext);
+        return true;
+    }
+
+    @Nullable
+    @Override
+    public Cursor query(@NonNull Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
+        String where = selection;
+        if (uriMatcher.match(uri) == COOKIE_ID) {
+            where = "_id=" + uri.getLastPathSegment();
+        }
+
+        SQLiteDatabase db = mServervices.getReadableDatabase();
+
+        return db.query(HSDatabase.HS_CLIENT_COOKIE_TABLE_NAME, projection, where,
+                selectionArgs, null, null, sortOrder);
+    }
+
+    @Nullable
+    @Override
+    public String getType(@NonNull Uri uri) {
+        int match = uriMatcher.match(uri);
+
+        switch (match) {
+            case COOKIES:
+                return "vnd.android.cursor.dir/vnd.torproject.cookies";
+            case COOKIE_ID:
+                return "vnd.android.cursor.item/vnd.torproject.cookie";
+            default:
+                return null;
+        }
+    }
+
+    @Nullable
+    @Override
+    public Uri insert(@NonNull Uri uri, ContentValues values) {
+        long regId;
+
+        SQLiteDatabase db = mServervices.getWritableDatabase();
+
+        regId = db.insert(HSDatabase.HS_CLIENT_COOKIE_TABLE_NAME, null, values);
+
+        mContext.getContentResolver().notifyChange(CONTENT_URI, null);
+
+        return ContentUris.withAppendedId(CONTENT_URI, regId);
+    }
+
+    @Override
+    public int delete(@NonNull Uri uri, String selection, String[] selectionArgs) {
+
+        String where = selection;
+        if (uriMatcher.match(uri) == COOKIE_ID) {
+            where = "_id=" + uri.getLastPathSegment();
+        }
+
+        SQLiteDatabase db = mServervices.getWritableDatabase();
+
+        Integer rows = db.delete(HSDatabase.HS_CLIENT_COOKIE_TABLE_NAME, where, selectionArgs);
+
+        mContext.getContentResolver().notifyChange(CONTENT_URI, null);
+
+        return rows;
+
+    }
+
+    @Override
+    public int update(@NonNull Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+        SQLiteDatabase db = mServervices.getWritableDatabase();
+
+        String where = selection;
+        if (uriMatcher.match(uri) == COOKIE_ID) {
+            where = "_id=" + uri.getLastPathSegment();
+        }
+
+        Integer rows = db.update(HSDatabase.HS_CLIENT_COOKIE_TABLE_NAME, values, where, null);
+        mContext.getContentResolver().notifyChange(CONTENT_URI, null);
+
+        return rows;
+    }
+
+    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() {
+        }
+    }
+}
diff --git a/app/src/main/java/org/torproject/android/ui/hiddenservices/storage/ExternalStorage.java b/app/src/main/java/org/torproject/android/ui/hiddenservices/storage/ExternalStorage.java
index cda3f8c..c50108b 100644
--- a/app/src/main/java/org/torproject/android/ui/hiddenservices/storage/ExternalStorage.java
+++ b/app/src/main/java/org/torproject/android/ui/hiddenservices/storage/ExternalStorage.java
@@ -5,13 +5,13 @@ import android.os.Environment;
 import java.io.File;
 
 public class ExternalStorage {
-    private static final String BACKUPS_DIR = "Orbot-HiddenServices";
+    private static final String ORBOT_BACKUPS_DIR = "Orbot";
 
     public static File getOrCreateBackupDir() {
         if (!isExternalStorageWritable())
             return null;
 
-        File dir = new File(Environment.getExternalStorageDirectory(), BACKUPS_DIR);
+        File dir = new File(Environment.getExternalStorageDirectory(), ORBOT_BACKUPS_DIR);
 
         if (!dir.isDirectory() && !dir.mkdirs())
             return null;
diff --git a/app/src/main/res/layout/layout_activity_client_cookies.xml b/app/src/main/res/layout/layout_activity_client_cookies.xml
new file mode 100644
index 0000000..b8f0626
--- /dev/null
+++ b/app/src/main/res/layout/layout_activity_client_cookies.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:fitsSystemWindows="true"
+    tools:context="org.torproject.android.ui.hiddenservices.ClientCookiesActivity">
+
+    <android.support.design.widget.AppBarLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:theme="@style/DefaultTheme.AppBarOverlay">
+
+        <android.support.v7.widget.Toolbar
+            android:id="@+id/toolbar"
+            android:layout_width="match_parent"
+            android:layout_height="?attr/actionBarSize"
+            android:background="?attr/colorPrimary"
+            app:popupTheme="@style/DefaultTheme.PopupOverlay" />
+
+    </android.support.design.widget.AppBarLayout>
+
+    <include layout="@layout/layout_content_client_cookies" />
+
+    <android.support.design.widget.FloatingActionButton
+        android:id="@+id/fab"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="bottom|end"
+        android:layout_margin="@dimen/fab_margin"
+        app:srcCompat="@android:drawable/stat_notify_more"
+        app:backgroundTint="@android:color/darker_gray" />
+
+</android.support.design.widget.CoordinatorLayout>
diff --git a/app/src/main/res/layout/layout_add_client_cookie_dialog.xml b/app/src/main/res/layout/layout_add_client_cookie_dialog.xml
new file mode 100644
index 0000000..4669a92
--- /dev/null
+++ b/app/src/main/res/layout/layout_add_client_cookie_dialog.xml
@@ -0,0 +1,64 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:orientation="vertical"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:paddingLeft="5dp"
+    android:paddingRight="5dp"
+    android:paddingTop="5dp"
+    android:paddingBottom="5dp">
+
+    <TextView
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:id="@+id/label_onion_name"
+        android:text="@string/onion"
+        android:textAppearance="@style/TextAppearance.AppCompat.Widget.PopupMenu.Small"
+        android:paddingLeft="5dp" />
+
+    <EditText
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:inputType="text"
+        android:ems="10"
+        android:id="@+id/cookie_onion" />
+
+    <TextView
+        android:text="@string/auth_cookie"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:id="@+id/label_cookie_value"
+        android:textAppearance="@style/TextAppearance.AppCompat.Widget.PopupMenu.Small"
+        android:paddingLeft="5dp" />
+
+    <EditText
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:ems="10"
+        android:id="@+id/cookie_value"
+        android:inputType="text" />
+
+    <LinearLayout
+        android:orientation="horizontal"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent">
+
+        <Button
+            android:text="@string/btn_cancel"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:id="@+id/cookie_dialog_cancel"
+            android:layout_weight="1"
+            style="@style/Widget.AppCompat.Button.Borderless.Colored" />
+
+        <Button
+            android:text="@string/save"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:id="@+id/cookie_dialog_save"
+            android:layout_weight="1"
+            style="@style/Widget.AppCompat.Button.Borderless.Colored" />
+
+    </LinearLayout>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/app/src/main/res/layout/layout_client_cookie_list_item.xml b/app/src/main/res/layout/layout_client_cookie_list_item.xml
new file mode 100644
index 0000000..80eefb9
--- /dev/null
+++ b/app/src/main/res/layout/layout_client_cookie_list_item.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="horizontal"
+    android:padding="15dp">
+
+    <TextView
+        android:id="@+id/cookie_onion"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:textSize="18sp"
+        android:layout_weight="1" />
+
+    <Switch
+        android:id="@+id/cookie_switch"
+        android:layout_width="wrap_content"
+        android:layout_height="match_parent"
+        android:focusable="false"
+        android:focusableInTouchMode="false"
+        android:switchPadding="30dp" />
+
+</LinearLayout>
\ No newline at end of file
diff --git a/app/src/main/res/layout/layout_content_client_cookies.xml b/app/src/main/res/layout/layout_content_client_cookies.xml
new file mode 100644
index 0000000..3ee9a89
--- /dev/null
+++ b/app/src/main/res/layout/layout_content_client_cookies.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/content_client_cookies"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:paddingBottom="@dimen/activity_vertical_margin"
+    android:paddingLeft="@dimen/activity_horizontal_margin"
+    android:paddingRight="@dimen/activity_horizontal_margin"
+    android:paddingTop="@dimen/activity_vertical_margin"
+    app:layout_behavior="@string/appbar_scrolling_view_behavior"
+    tools:context="org.torproject.android.ui.hiddenservices.ClientCookiesActivity"
+    tools:showIn="@layout/layout_activity_client_cookies">
+
+    <ListView
+        android:id="@+id/clien_cookies_list"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent" />
+
+</RelativeLayout>
diff --git a/app/src/main/res/layout/layout_cookie_actions.xml b/app/src/main/res/layout/layout_cookie_actions.xml
new file mode 100644
index 0000000..64d8c1c
--- /dev/null
+++ b/app/src/main/res/layout/layout_cookie_actions.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:orientation="vertical" android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <Button
+        android:text="@string/backup_cookie"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:id="@+id/btn_cookie_backup" />
+
+    <Button
+        android:text="@string/delete_cookie"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:id="@+id/btn_cookie_delete" />
+
+    <Button
+        android:text="@string/btn_cancel"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:id="@+id/btn_cookie_cancel" />
+
+</LinearLayout>
\ No newline at end of file
diff --git a/app/src/main/res/layout/layout_hs_cookie.xml b/app/src/main/res/layout/layout_hs_cookie.xml
index 398178f..6d1a893 100644
--- a/app/src/main/res/layout/layout_hs_cookie.xml
+++ b/app/src/main/res/layout/layout_hs_cookie.xml
@@ -18,6 +18,12 @@
         android:id="@+id/hs_cookie_to_clipboard" />
 
     <Button
+        android:text="@string/share_as_qr"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:id="@+id/hs_cookie_to_qr" />
+
+    <Button
         android:text="@string/btn_cancel"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
diff --git a/app/src/main/res/menu/cookie_menu.xml b/app/src/main/res/menu/cookie_menu.xml
new file mode 100644
index 0000000..7811025
--- /dev/null
+++ b/app/src/main/res/menu/cookie_menu.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto">
+
+    <item
+        android:id="@+id/cookie_restore_backup"
+        android:title="@string/restore_backup" />
+    <item
+        android:id="@+id/cookie_from_qr"
+        android:title="@string/cookie_from_QR" />
+</menu>
\ No newline at end of file
diff --git a/app/src/main/res/menu/orbot_main.xml b/app/src/main/res/menu/orbot_main.xml
index 179675a..e556b76 100644
--- a/app/src/main/res/menu/orbot_main.xml
+++ b/app/src/main/res/menu/orbot_main.xml
@@ -44,10 +44,22 @@
   </menu>
  </item>
 
-    <item android:id="@+id/menu_hidden_services"
+    <item
         android:title="@string/menu_hidden_services"
         yourapp:showAsAction="never"
-        />
+        >
+      <menu>
+        <item android:id="@+id/menu_hidden_services"
+              android:title="@string/hosted_services"
+            yourapp:showAsAction="never"
+               />
+
+        <item android:id="@+id/menu_client_cookies"
+              android:title="@string/client_cookies"
+            yourapp:showAsAction="never"
+               />
+      </menu>
+    </item>
         
  <!--
   <item android:id="@+id/menu_promo_apps"
diff --git a/app/src/main/res/values-v21/styles.xml b/app/src/main/res/values-v21/styles.xml
new file mode 100644
index 0000000..7abc06d
--- /dev/null
+++ b/app/src/main/res/values-v21/styles.xml
@@ -0,0 +1 @@
+<resources></resources>
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 4402608..6a07c44 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -128,21 +128,21 @@
   <string name="pref_entrance_node_dialog">Enter Entrance Nodes</string>
   <string name="pref_allow_background_starts_title">Allow Background Starts</string>
   <string name="pref_allow_background_starts_summary">Let any app tell Orbot to start Tor and related services</string>
-  
+
   <string name="button_proxy_all">Proxy All</string>
   <string name="button_proxy_none">Proxy None</string>
   <string name="button_invert_selection">Invert Selection</string>
 
   <string name="pref_proxy_title">Outbound Network Proxy (Optional)</string>
-  
+
   <string name="pref_proxy_type_title">Outbound Proxy Type</string>
   <string name="pref_proxy_type_summary">Protocol to use for proxy server: HTTP, HTTPS, Socks4, Socks5</string>
   <string name="pref_proxy_type_dialog">Enter Proxy Type</string>
-  
+
   <string name="pref_proxy_host_title">Outbound Proxy Host</string>
   <string name="pref_proxy_host_summary">Proxy Server hostname</string>
   <string name="pref_proxy_host_dialog">Enter Proxy Host</string>
-  
+
   <string name="pref_proxy_port_title">Outbound Proxy Port</string>
   <string name="pref_proxy_port_summary">Proxy Server port</string>
   <string name="pref_proxy_port_dialog">Enter Proxy port</string>
@@ -150,13 +150,12 @@
   <string name="pref_proxy_username_title">Outbound Proxy Username</string>
   <string name="pref_proxy_username_summary">Proxy Username (Optional)</string>
   <string name="pref_proxy_username_dialog">Enter Proxy Username</string>
-  
+
   <string name="pref_proxy_password_title">Outbound Proxy Password</string>
   <string name="pref_proxy_password_summary">Proxy Password (Optional)</string>
   <string name="pref_proxy_password_dialog">Enter Proxy Password</string>
-  
-  
-  
+
+
   <string name="status">Status</string>
   <string name="setting_up_full_transparent_proxying_">Setting up full transparent proxying…</string>
   <string name="setting_up_app_based_transparent_proxying_">Setting up app-based transparent proxying…</string>
@@ -227,17 +226,17 @@
   <string name="unable_to_reset_tor">Reboot your device, unable to reset Tor!</string>
   <string name="pref_use_sys_iptables_title">Use Default Iptables</string>
   <string name="pref_use_sys_iptables_summary">use the built-in iptables binary instead of the one bundled with Orbot</string>
-  
+
   <string name="error_installing_binares">The Tor binaries were not able to be installed or upgraded.</string>
-  
+
   <string name="pref_use_persistent_notifications">Always keep the icon in toolbar when Orbot is connected</string>
   <string name="pref_use_persistent_notifications_title">Always-On Notifications</string>
-  
+
   <string name="pref_use_expanded_notifications">Show expanded notification with Tor exit country and IP</string>
   <string name="pref_use_expanded_notifications_title">Expanded Notifications</string>
-  
+
   <string name="notification_using_bridges">Bridges enabled!</string>
-  <string name="default_bridges"/>
+  <string name="default_bridges" />
   <string name="set_locale_title">Language</string>
   <string name="set_locale_summary">Choose the locale and language for Orbot</string>
   <string name="wizard_locale_title">Choose Language</string>
@@ -251,8 +250,8 @@
   <string name="pref_disable_network_title">No Network Auto-Sleep</string>
   <string name="pref_disable_network_summary">Put Tor to sleep when there is no internet available</string>
   <string name="newnym">You\'ve switched to a new Tor identity!</string>
-  
-    <string name="menu_verify_browser">Browser</string>
+
+  <string name="menu_verify_browser">Browser</string>
     <string name="menu_use_chatsecure">Use ChatSecure</string>
 
     <string name="permission_manage_tor_label">Manage Tor</string>
@@ -262,77 +261,77 @@
     <string name="no_network_connectivity_putting_tor_to_sleep_">No network connectivity. Putting Tor to sleep…</string>
     <string name="network_connectivity_is_good_waking_tor_up_">Network connectivity is good. Waking Tor up…</string>
     <string name="updating_settings_in_tor_service">updating settings in Tor service</string>
-  
-    <string name="pref_socks_title">Tor SOCKS</string>
+
+  <string name="pref_socks_title">Tor SOCKS</string>
     <string name="pref_socks_summary">Port that Tor offers its SOCKS proxy on (default: 9050 or 0 to disable)</string>
     <string name="pref_socks_dialog">SOCKS Port Config</string>
-    
-    <string name="pref_transport_title">Tor TransProxy Port</string>
+
+  <string name="pref_transport_title">Tor TransProxy Port</string>
     <string name="pref_transport_summary">Port that Tor offers its Transparent Proxy on (default: 9040 or 0 to disable)</string>
     <string name="pref_transport_dialog">TransProxy Port Config</string>
-    
-    
-    <string name="pref_dnsport_title">Tor DNS Port</string>
+
+
+  <string name="pref_dnsport_title">Tor DNS Port</string>
     <string name="pref_dnsport_summary">Port that Tor offers its DNS on (default: 5400 or 0 to disable)</string>
     <string name="pref_dnsport_dialog">DNS Port Config</string>
-    
-    
-    <string name="pref_torrc_title">Torrc Custom Config</string>
+
+
+  <string name="pref_torrc_title">Torrc Custom Config</string>
     <string name="pref_torrc_summary">EXPERTS ONLY: enter direct torrc config lines</string>
     <string name="pref_torrc_dialog">Custom Torrc</string>
-    
-    <string name="wizard_tips_martus">Mobile Martus - Benetech Human Rights Documentation App</string>
+
+  <string name="wizard_tips_martus">Mobile Martus - Benetech Human Rights Documentation App</string>
   <string name="your_tor_public_ips_">Your Tor Public IPs:</string>
   <string name="please_disable_this_app_in_android_settings_apps_if_you_are_having_problems_with_orbot_">"Please disable this app in Android->Settings->Apps if you are having problems with Orbot: "</string>
   <string name="app_conflict">App Conflict</string>
-  
+
 
   <string name="pref_transproxy_refresh_title">Transproxy Auto-Refresh</string>
   <string name="pref_transproxy_refresh_summary">Re-apply Transproxy rules when the network state changes</string>
-  
+
   <string name="pref_transproxy_flush_title">Transproxy FORCE REMOVE</string>
   <string name="pref_transproxy_flush_summary">Tap here to flush all transproxy network rules NOW</string>
   <string name="transparent_proxy_rules_flushed_">Transparent proxy rules flushed!</string>
   <string name="you_do_not_have_root_access_enabled">You do not have ROOT access enabled</string>
   <string name="you_may_need_to_stop_and_start_orbot_for_settings_change_to_be_enabled_">You may need to stop and start Orbot for settings change to be enabled.</string>
-  
+
   <string name="menu_vpn">Apps</string>
-  
+
   <string name="kbps">kbps</string>
-  
+
   <string name="mbps">mbps</string>
-  
+
   <string name="kb">KB</string>
-  
+
   <string name="mb">MB</string>
-  
+
   <string name="bridges_updated">Bridges Updated</string>
-  
+
   <string name="restart_orbot_to_use_this_bridge_">Please restart Orbot to enable the changes</string>
-  
+
   <string name="menu_qr">QR Codes</string>
-  
+
   <string name="if_your_mobile_network_actively_blocks_tor_you_can_use_a_tor_bridge_to_access_the_network_another_way_to_get_bridges_is_to_send_an_email_to_bridges_torproject_org_please_note_that_you_must_send_the_email_using_an_address_from_one_of_the_following_email_providers_riseup_gmail_or_yahoo_">If your mobile network actively blocks Tor, you can use a Bridge to access the network. SELECT one of the bridge types above to enable bridges.</string>
-  
+
   <string name="bridge_mode">Bridge Mode</string>
-  
+
   <string name="get_bridges_email">Email</string>
   <string name="get_bridges_web">Web</string>
-  
+
   <string name="activate">Activate</string>
-  
+
   <string name="apps_mode">Apps VPN Mode</string>
-  
+
   <string name="you_can_enable_all_apps_on_your_device_to_run_through_the_tor_network_using_the_vpn_feature_of_android_">You can enable all apps on your device to run through the Tor network using the VPN feature of Android.\n\n*WARNING* This is a new, experimental feature and in some cases may not start automatically, or may stop. It should NOT be used for anonymity, and ONLY used for getting through firewalls and filters.</string>
-  
+
   <string name="send_email">Send Email</string>
-  
+
   <string name="you_must_get_a_bridge_address_by_email_web_or_from_a_friend_once_you_have_this_address_please_paste_it_into_the_bridges_preference_in_orbot_s_setting_and_restart_">You can get a bridge address through email, the web or by scanning a bridge QR code. Select \'Email\' or \'Web\' below to request a bridge address.\n\nOnce you have an address, copy & paste it into the \"Bridges\" preference in Orbot\'s setting and restart.</string>
-  
+
   <string name="install_orweb">Install Orfox</string>
-  
+
   <string name="standard_browser">Standard Browser</string>
-  
+
   <string name="note_only_standard_tor_bridges_work_on_intel_x86_atom_devices">NOTE: Only standard Tor bridges work on Intel X86/ATOM devices</string>
 
     <string name="vpn_default_world">World (Location)</string>
@@ -359,6 +358,7 @@
     <string name="restore_backup">Restore Backup</string>
     <string name="create_a_backup_first">Create a backup first</string>
     <string name="name_can_t_be_empty">Name can\'t be empty</string>
+    <string name="fields_can_t_be_empty">Fields can\'t be empty</string>
     <string name="start_tor_again_for_finish_the_process">Start Tor again for finish the process</string>
     <string name="confirm_service_deletion">Confirm service deletion</string>
     <string name="click_again_for_backup">Click again for backup</string>
@@ -367,4 +367,13 @@
     <string name="copy_cookie_to_clipboard">Copy cookie to clipboard</string>
     <string name="auth_cookie_was_not_configured">Auth cookie was not configured</string>
     <string name="please_restart_Orbot_to_enable_the_changes">Please restart Orbot to enable the changes</string>
+    <string name="client_cookies">Client cookies</string>
+    <string name="onion">.onion</string>
+    <string name="invalid_onion_address">Invalid .onion address</string>
+    <string name="cookie_from_QR">Read from QR</string>
+    <string name="backup_cookie">Backup cookie</string>
+    <string name="delete_cookie">Delete cookie</string>
+    <string name="confirm_cookie_deletion">Confirm cookie deletion</string>
+    <string name="hosted_services">Hosted Services</string>
+    <string name="share_as_qr">Share as QR</string>
 </resources>
diff --git a/orbotservice/src/main/java/org/torproject/android/service/TorService.java b/orbotservice/src/main/java/org/torproject/android/service/TorService.java
index 19fb0d1..8956163 100644
--- a/orbotservice/src/main/java/org/torproject/android/service/TorService.java
+++ b/orbotservice/src/main/java/org/torproject/android/service/TorService.java
@@ -132,7 +132,8 @@ public class TorService extends Service implements TorServiceConstants, OrbotCon
     private Shell mShellPolipo;
 
 
-    private static final Uri CONTENT_URI = Uri.parse("content://org.torproject.android.ui.hiddenservices.providers/hs");
+    private static final Uri HS_CONTENT_URI = Uri.parse("content://org.torproject.android.ui.hiddenservices.providers/hs");
+    private static final Uri COOKIE_CONTENT_URI = Uri.parse("content://org.torproject.android.ui.hiddenservices.providers.cookie/cookie");
 
     public static final class HiddenService implements BaseColumns {
         public static final String NAME = "name";
@@ -148,7 +149,16 @@ public class TorService extends Service implements TorServiceConstants, OrbotCon
         }
     }
 
-    private String[] mProjection = new String[]{
+    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() {
+        }
+    }
+
+    private String[] hsProjection = new String[]{
 			HiddenService._ID,
 			HiddenService.NAME,
 			HiddenService.DOMAIN,
@@ -158,6 +168,12 @@ public class TorService extends Service implements TorServiceConstants, OrbotCon
 			HiddenService.ONION_PORT,
             HiddenService.ENABLED};
 
+    private String[] cookieProjection = new String[]{
+            ClientCookie._ID,
+            ClientCookie.DOMAIN,
+            ClientCookie.AUTH_COOKIE_VALUE,
+            ClientCookie.ENABLED};
+
     public void debug(String msg)
     {
         if (Prefs.useDebugLogging())
@@ -778,7 +794,7 @@ public class TorService extends Service implements TorServiceConstants, OrbotCon
 
             // Tor is running, update new .onion names at db
             ContentResolver mCR = getApplicationContext().getContentResolver();
-            Cursor hidden_services = mCR.query(CONTENT_URI, mProjection, null, null, null);
+            Cursor hidden_services = mCR.query(HS_CONTENT_URI, hsProjection, null, null, null);
             if(hidden_services != null) {
                 try {
                     while (hidden_services.moveToNext()) {
@@ -804,7 +820,7 @@ public class TorService extends Service implements TorServiceConstants, OrbotCon
                                         fields.put(HiddenService.AUTH_COOKIE_VALUE, aux[1]);
                                     }
                                     fields.put(HiddenService.DOMAIN, onionHostname);
-                                    mCR.update(CONTENT_URI, fields, "port=" + HSLocalPort , null);
+                                    mCR.update(HS_CONTENT_URI, fields, "port=" + HSLocalPort , null);
                                 } catch (FileNotFoundException e) {
                                     logException("unable to read onion hostname file",e);
                                     showToolbarNotification(getString(R.string.unable_to_read_hidden_service_name), HS_NOTIFY_ID, R.drawable.ic_stat_notifyerr);
@@ -1792,9 +1808,10 @@ public class TorService extends Service implements TorServiceConstants, OrbotCon
             return false;
         }
 
-        /* ---- Hidden Services ---- */
         ContentResolver mCR = getApplicationContext().getContentResolver();
-        Cursor hidden_services = mCR.query(CONTENT_URI, mProjection, HiddenService.ENABLED + "=1", null, null);
+
+        /* ---- Hidden Services ---- */
+        Cursor hidden_services = mCR.query(HS_CONTENT_URI, hsProjection, HiddenService.ENABLED + "=1", null, null);
         if(hidden_services != null) {
             try {
                 while (hidden_services.moveToNext()) {
@@ -1821,6 +1838,22 @@ public class TorService extends Service implements TorServiceConstants, OrbotCon
             hidden_services.close();
 		}
 
+        /* ---- Client Cookies ---- */
+        Cursor client_cookies = mCR.query(COOKIE_CONTENT_URI, cookieProjection, ClientCookie.ENABLED + "=1", null, null);
+        if(client_cookies != null) {
+            try {
+                while (client_cookies.moveToNext()) {
+                    String domain = client_cookies.getString(client_cookies.getColumnIndex(ClientCookie.DOMAIN));
+                    String cookie = client_cookies.getString(client_cookies.getColumnIndex(ClientCookie.AUTH_COOKIE_VALUE));
+                    extraLines.append("HidServAuth" + ' ' + domain + ' ' + cookie).append('\n');
+                }
+            } catch (Exception e) {
+                    Log.e(OrbotConstants.TAG,"error starting share server",e);
+            }
+
+            client_cookies.close();
+		}
+
         return true;
     }
     





More information about the tor-commits mailing list