aboutsummaryrefslogtreecommitdiffstats
path: root/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/openpgp_api
diff options
context:
space:
mode:
authorDominik Schürmann <dominik@dominikschuermann.de>2013-09-13 10:05:43 +0200
committerDominik Schürmann <dominik@dominikschuermann.de>2013-09-13 10:05:43 +0200
commitca8f8e3ff7dd4742f65b541e4a8304433c093f87 (patch)
tree09c08b46bd262879841f844228cbc546314d3f68 /OpenPGP-Keychain/src/org/sufficientlysecure/keychain/openpgp_api
parent86c84ddbb34c44f127a0f1d4a6031d02e1b76779 (diff)
downloadopen-keychain-ca8f8e3ff7dd4742f65b541e4a8304433c093f87.tar.gz
open-keychain-ca8f8e3ff7dd4742f65b541e4a8304433c093f87.tar.bz2
open-keychain-ca8f8e3ff7dd4742f65b541e4a8304433c093f87.zip
unify naming of api package and process
Diffstat (limited to 'OpenPGP-Keychain/src/org/sufficientlysecure/keychain/openpgp_api')
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/openpgp_api/AppSettings.java84
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/openpgp_api/AppSettingsActivity.java109
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/openpgp_api/AppSettingsFragment.java304
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/openpgp_api/OpenPgpService.java747
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/openpgp_api/OpenPgpServiceActivity.java325
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/openpgp_api/RegisteredAppsAdapter.java76
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/openpgp_api/RegisteredAppsListActivity.java61
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/openpgp_api/RegisteredAppsListFragment.java107
8 files changed, 1813 insertions, 0 deletions
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/openpgp_api/AppSettings.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/openpgp_api/AppSettings.java
new file mode 100644
index 000000000..982d5fe40
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/openpgp_api/AppSettings.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.openpgp_api;
+
+import org.spongycastle.bcpg.HashAlgorithmTags;
+import org.spongycastle.openpgp.PGPEncryptedData;
+import org.sufficientlysecure.keychain.Id;
+
+public class AppSettings {
+ private String packageName;
+ private long keyId = Id.key.none;
+ private int encryptionAlgorithm;
+ private int hashAlgorithm;
+ private int compression;
+
+ public AppSettings() {
+
+ }
+
+ public AppSettings(String packageName) {
+ super();
+ this.packageName = packageName;
+ // defaults:
+ this.encryptionAlgorithm = PGPEncryptedData.AES_128;
+ this.hashAlgorithm = HashAlgorithmTags.SHA256;
+ this.compression = Id.choice.compression.zlib;
+ }
+
+ public String getPackageName() {
+ return packageName;
+ }
+
+ public void setPackageName(String packageName) {
+ this.packageName = packageName;
+ }
+
+ public long getKeyId() {
+ return keyId;
+ }
+
+ public void setKeyId(long scretKeyId) {
+ this.keyId = scretKeyId;
+ }
+
+ public int getEncryptionAlgorithm() {
+ return encryptionAlgorithm;
+ }
+
+ public void setEncryptionAlgorithm(int encryptionAlgorithm) {
+ this.encryptionAlgorithm = encryptionAlgorithm;
+ }
+
+ public int getHashAlgorithm() {
+ return hashAlgorithm;
+ }
+
+ public void setHashAlgorithm(int hashAlgorithm) {
+ this.hashAlgorithm = hashAlgorithm;
+ }
+
+ public int getCompression() {
+ return compression;
+ }
+
+ public void setCompression(int compression) {
+ this.compression = compression;
+ }
+
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/openpgp_api/AppSettingsActivity.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/openpgp_api/AppSettingsActivity.java
new file mode 100644
index 000000000..030a2bbb8
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/openpgp_api/AppSettingsActivity.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.openpgp_api;
+
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.helper.ActionBarHelper;
+import org.sufficientlysecure.keychain.provider.ProviderHelper;
+import org.sufficientlysecure.keychain.util.Log;
+
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.view.View;
+
+import com.actionbarsherlock.app.SherlockFragmentActivity;
+import com.actionbarsherlock.view.Menu;
+import com.actionbarsherlock.view.MenuItem;
+
+public class AppSettingsActivity extends SherlockFragmentActivity {
+ private Uri mAppUri;
+
+ private AppSettingsFragment settingsFragment;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // Inflate a "Done" custom action bar view to serve as the "Up" affordance.
+ ActionBarHelper.setDoneView(getSupportActionBar(), R.string.api_settings_save,
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ // "Done"
+ save();
+ }
+ });
+
+ setContentView(R.layout.api_app_settings_activity);
+
+ settingsFragment = (AppSettingsFragment) getSupportFragmentManager().findFragmentById(
+ R.id.api_app_settings_fragment);
+
+ Intent intent = getIntent();
+ mAppUri = intent.getData();
+ if (mAppUri == null) {
+ Log.e(Constants.TAG, "Intent data missing. Should be Uri of app!");
+ finish();
+ return;
+ } else {
+ Log.d(Constants.TAG, "uri: " + mAppUri);
+ loadData(mAppUri);
+ }
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ super.onCreateOptionsMenu(menu);
+ getSupportMenuInflater().inflate(R.menu.api_app_settings, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.menu_api_settings_revoke:
+ revokeAccess();
+ return true;
+ case R.id.menu_api_settings_cancel:
+ finish();
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ private void loadData(Uri appUri) {
+ AppSettings settings = ProviderHelper.getApiAppSettings(this, appUri);
+ settingsFragment.setAppSettings(settings);
+ }
+
+ private void revokeAccess() {
+ if (getContentResolver().delete(mAppUri, null, null) <= 0) {
+ throw new RuntimeException();
+ }
+ finish();
+ }
+
+ private void save() {
+ ProviderHelper.updateApiApp(this, settingsFragment.getAppSettings(), mAppUri);
+
+ finish();
+ }
+
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/openpgp_api/AppSettingsFragment.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/openpgp_api/AppSettingsFragment.java
new file mode 100644
index 000000000..ad463d316
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/openpgp_api/AppSettingsFragment.java
@@ -0,0 +1,304 @@
+/*
+ * Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.openpgp_api;
+
+import java.util.HashMap;
+
+import org.spongycastle.bcpg.HashAlgorithmTags;
+import org.spongycastle.openpgp.PGPEncryptedData;
+import org.spongycastle.openpgp.PGPSecretKey;
+import org.spongycastle.openpgp.PGPSecretKeyRing;
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.Id;
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.helper.PgpHelper;
+import org.sufficientlysecure.keychain.provider.ProviderHelper;
+import org.sufficientlysecure.keychain.ui.SelectSecretKeyActivity;
+import org.sufficientlysecure.keychain.util.KeyValueSpinnerAdapter;
+import org.sufficientlysecure.keychain.util.Log;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.view.animation.AlphaAnimation;
+import android.view.animation.Animation;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemSelectedListener;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.Spinner;
+import android.widget.TextView;
+
+public class AppSettingsFragment extends Fragment {
+
+ // model
+ private AppSettings appSettings;
+
+ // view
+ private LinearLayout mAdvancedSettingsContainer;
+ private Button mAdvancedSettingsButton;
+ private TextView mAppNameView;
+ private ImageView mAppIconView;
+ private TextView mKeyUserId;
+ private TextView mKeyUserIdRest;
+ private Button mSelectKeyButton;
+ private Spinner mEncryptionAlgorithm;
+ private Spinner mHashAlgorithm;
+ private Spinner mCompression;
+
+ KeyValueSpinnerAdapter encryptionAdapter;
+ KeyValueSpinnerAdapter hashAdapter;
+ KeyValueSpinnerAdapter compressionAdapter;
+
+ public AppSettings getAppSettings() {
+ return appSettings;
+ }
+
+ public void setAppSettings(AppSettings appSettings) {
+ this.appSettings = appSettings;
+ setPackage(appSettings.getPackageName());
+ updateSelectedKeyView(appSettings.getKeyId());
+ mEncryptionAlgorithm.setSelection(encryptionAdapter.getPosition(appSettings
+ .getEncryptionAlgorithm()));
+ mHashAlgorithm.setSelection(hashAdapter.getPosition(appSettings.getHashAlgorithm()));
+ mCompression.setSelection(compressionAdapter.getPosition(appSettings.getCompression()));
+ }
+
+ /**
+ * Inflate the layout for this fragment
+ */
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.api_app_settings_fragment, container, false);
+ initView(view);
+ return view;
+ }
+
+ private void initView(View view) {
+ mAdvancedSettingsButton = (Button) view.findViewById(R.id.api_app_settings_advanced_button);
+ mAdvancedSettingsContainer = (LinearLayout) view
+ .findViewById(R.id.api_app_settings_advanced);
+
+ mAppNameView = (TextView) view.findViewById(R.id.api_app_settings_app_name);
+ mAppIconView = (ImageView) view.findViewById(R.id.api_app_settings_app_icon);
+ mKeyUserId = (TextView) view.findViewById(R.id.api_app_settings_user_id);
+ mKeyUserIdRest = (TextView) view.findViewById(R.id.api_app_settings_user_id_rest);
+ mSelectKeyButton = (Button) view.findViewById(R.id.api_app_settings_select_key_button);
+ mEncryptionAlgorithm = (Spinner) view
+ .findViewById(R.id.api_app_settings_encryption_algorithm);
+ mHashAlgorithm = (Spinner) view.findViewById(R.id.api_app_settings_hash_algorithm);
+ mCompression = (Spinner) view.findViewById(R.id.api_app_settings_compression);
+
+ HashMap<Integer, String> encryptionMap = new HashMap<Integer, String>();
+ encryptionMap.put(PGPEncryptedData.AES_128, "AES-128");
+ encryptionMap.put(PGPEncryptedData.AES_192, "AES-192");
+ encryptionMap.put(PGPEncryptedData.AES_256, "AES-256");
+ encryptionMap.put(PGPEncryptedData.BLOWFISH, "Blowfish");
+ encryptionMap.put(PGPEncryptedData.TWOFISH, "Twofish");
+ encryptionMap.put(PGPEncryptedData.CAST5, "CAST5");
+ encryptionMap.put(PGPEncryptedData.DES, "DES");
+ encryptionMap.put(PGPEncryptedData.TRIPLE_DES, "Triple DES");
+ encryptionMap.put(PGPEncryptedData.IDEA, "IDEA");
+
+ encryptionAdapter = new KeyValueSpinnerAdapter(getActivity(), encryptionMap);
+ mEncryptionAlgorithm.setAdapter(encryptionAdapter);
+ mEncryptionAlgorithm.setOnItemSelectedListener(new OnItemSelectedListener() {
+
+ @Override
+ public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
+ appSettings.setEncryptionAlgorithm((int) id);
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView<?> parent) {
+ }
+ });
+
+ HashMap<Integer, String> hashMap = new HashMap<Integer, String>();
+ hashMap.put(HashAlgorithmTags.MD5, "MD5");
+ hashMap.put(HashAlgorithmTags.RIPEMD160, "RIPEMD-160");
+ hashMap.put(HashAlgorithmTags.SHA1, "SHA-1");
+ hashMap.put(HashAlgorithmTags.SHA224, "SHA-224");
+ hashMap.put(HashAlgorithmTags.SHA256, "SHA-256");
+ hashMap.put(HashAlgorithmTags.SHA384, "SHA-384");
+ hashMap.put(HashAlgorithmTags.SHA512, "SHA-512");
+
+ hashAdapter = new KeyValueSpinnerAdapter(getActivity(), hashMap);
+ mHashAlgorithm.setAdapter(hashAdapter);
+ mHashAlgorithm.setOnItemSelectedListener(new OnItemSelectedListener() {
+
+ @Override
+ public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
+ appSettings.setHashAlgorithm((int) id);
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView<?> parent) {
+ }
+ });
+
+ HashMap<Integer, String> compressionMap = new HashMap<Integer, String>();
+ compressionMap.put(Id.choice.compression.none, getString(R.string.choice_none) + " ("
+ + getString(R.string.fast) + ")");
+ compressionMap.put(Id.choice.compression.zip, "ZIP (" + getString(R.string.fast) + ")");
+ compressionMap.put(Id.choice.compression.zlib, "ZLIB (" + getString(R.string.fast) + ")");
+ compressionMap.put(Id.choice.compression.bzip2, "BZIP2 (" + getString(R.string.very_slow)
+ + ")");
+
+ compressionAdapter = new KeyValueSpinnerAdapter(getActivity(), compressionMap);
+ mCompression.setAdapter(compressionAdapter);
+ mCompression.setOnItemSelectedListener(new OnItemSelectedListener() {
+
+ @Override
+ public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
+ appSettings.setCompression((int) id);
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView<?> parent) {
+ }
+ });
+
+ mSelectKeyButton.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ selectSecretKey();
+ }
+ });
+
+ final Animation visibleAnimation = new AlphaAnimation(0.0f, 1.0f);
+ visibleAnimation.setDuration(250);
+ final Animation invisibleAnimation = new AlphaAnimation(1.0f, 0.0f);
+ invisibleAnimation.setDuration(250);
+
+ // TODO: Better: collapse/expand animation
+ // final Animation animation2 = new TranslateAnimation(Animation.RELATIVE_TO_SELF, 0.0f,
+ // Animation.RELATIVE_TO_SELF, 0.0f, Animation.RELATIVE_TO_SELF, -1.0f,
+ // Animation.RELATIVE_TO_SELF, 0.0f);
+ // animation2.setDuration(150);
+
+ mAdvancedSettingsButton.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ if (mAdvancedSettingsContainer.getVisibility() == View.VISIBLE) {
+ mAdvancedSettingsContainer.startAnimation(invisibleAnimation);
+ mAdvancedSettingsContainer.setVisibility(View.INVISIBLE);
+ mAdvancedSettingsButton.setText(R.string.api_settings_show_advanced);
+ } else {
+ mAdvancedSettingsContainer.startAnimation(visibleAnimation);
+ mAdvancedSettingsContainer.setVisibility(View.VISIBLE);
+ mAdvancedSettingsButton.setText(R.string.api_settings_hide_advanced);
+ }
+ }
+ });
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ }
+
+ private void selectSecretKey() {
+ Intent intent = new Intent(getActivity(), SelectSecretKeyActivity.class);
+ startActivityForResult(intent, Id.request.secret_keys);
+ }
+
+ private void setPackage(String packageName) {
+ PackageManager pm = getActivity().getApplicationContext().getPackageManager();
+
+ // get application name and icon from package manager
+ String appName = null;
+ Drawable appIcon = null;
+ try {
+ ApplicationInfo ai = pm.getApplicationInfo(packageName, 0);
+
+ appName = (String) pm.getApplicationLabel(ai);
+ appIcon = pm.getApplicationIcon(ai);
+ } catch (final NameNotFoundException e) {
+ // fallback
+ appName = packageName;
+ }
+ mAppNameView.setText(appName);
+ mAppIconView.setImageDrawable(appIcon);
+ }
+
+ private void updateSelectedKeyView(long secretKeyId) {
+ if (secretKeyId == Id.key.none) {
+ mKeyUserId.setText(R.string.api_settings_no_key);
+ mKeyUserIdRest.setText("");
+ } else {
+ String uid = getResources().getString(R.string.unknownUserId);
+ String uidExtra = "";
+ PGPSecretKeyRing keyRing = ProviderHelper.getPGPSecretKeyRingByMasterKeyId(
+ getActivity(), secretKeyId);
+ if (keyRing != null) {
+ PGPSecretKey key = PgpHelper.getMasterKey(keyRing);
+ if (key != null) {
+ String userId = PgpHelper.getMainUserIdSafe(getActivity(), key);
+ String chunks[] = userId.split(" <", 2);
+ uid = chunks[0];
+ if (chunks.length > 1) {
+ uidExtra = "<" + chunks[1];
+ }
+ }
+ }
+ mKeyUserId.setText(uid);
+ mKeyUserIdRest.setText(uidExtra);
+ }
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ Log.d(Constants.TAG, "onactivityresult " + requestCode + " " + resultCode);
+ switch (requestCode) {
+
+ case Id.request.secret_keys: {
+ long secretKeyId;
+ if (resultCode == Activity.RESULT_OK) {
+ Bundle bundle = data.getExtras();
+ secretKeyId = bundle.getLong(SelectSecretKeyActivity.RESULT_EXTRA_MASTER_KEY_ID);
+
+ } else {
+ secretKeyId = Id.key.none;
+ }
+ appSettings.setKeyId(secretKeyId);
+ updateSelectedKeyView(secretKeyId);
+ break;
+ }
+
+ default: {
+ break;
+ }
+ }
+
+ super.onActivityResult(requestCode, resultCode, data);
+ }
+
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/openpgp_api/OpenPgpService.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/openpgp_api/OpenPgpService.java
new file mode 100644
index 000000000..beefa56b4
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/openpgp_api/OpenPgpService.java
@@ -0,0 +1,747 @@
+/*
+ * Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.openpgp_api;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.TimeUnit;
+import java.util.regex.Matcher;
+
+import org.openintents.openpgp.IOpenPgpCallback;
+import org.openintents.openpgp.IOpenPgpService;
+import org.openintents.openpgp.OpenPgpError;
+import org.openintents.openpgp.OpenPgpSignatureResult;
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.Id;
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.helper.PgpMain;
+import org.sufficientlysecure.keychain.helper.Preferences;
+import org.sufficientlysecure.keychain.provider.KeychainContract;
+import org.sufficientlysecure.keychain.provider.ProviderHelper;
+import org.sufficientlysecure.keychain.service.KeychainIntentService;
+import org.sufficientlysecure.keychain.service.PassphraseCacheService;
+import org.sufficientlysecure.keychain.util.InputData;
+import org.sufficientlysecure.keychain.util.Log;
+import org.sufficientlysecure.keychain.util.PausableThreadPoolExecutor;
+
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.Messenger;
+import android.os.RemoteException;
+
+public class OpenPgpService extends Service {
+ Context mContext;
+
+ final ArrayBlockingQueue<Runnable> mPoolQueue = new ArrayBlockingQueue<Runnable>(100);
+ // TODO: Are these parameters okay?
+ PausableThreadPoolExecutor mThreadPool = new PausableThreadPoolExecutor(2, 4, 10,
+ TimeUnit.SECONDS, mPoolQueue);
+
+ final Object userInputLock = new Object();
+
+ private class MyBaseCallback implements Handler.Callback {
+ public static final int OKAY = 1;
+ public static final int CANCEL = 0;
+
+ @Override
+ public boolean handleMessage(Message msg) {
+ return false;
+ }
+
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ mContext = this;
+ Log.d(Constants.TAG, "CryptoService, onCreate()");
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ Log.d(Constants.TAG, "CryptoService, onDestroy()");
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return mBinder;
+ }
+
+ private String getCachedPassphrase(long keyId) {
+ String passphrase = PassphraseCacheService.getCachedPassphrase(mContext, keyId);
+
+ if (passphrase == null) {
+ Log.d(Constants.TAG, "No passphrase! Activity required!");
+
+ // start passphrase dialog
+ Bundle extras = new Bundle();
+ extras.putLong(OpenPgpServiceActivity.EXTRA_SECRET_KEY_ID, keyId);
+
+ PassphraseActivityCallback callback = new PassphraseActivityCallback();
+ Messenger messenger = new Messenger(new Handler(getMainLooper(), callback));
+
+ pauseQueueAndStartServiceActivity(OpenPgpServiceActivity.ACTION_CACHE_PASSPHRASE,
+ messenger, extras);
+
+ if (callback.isSuccess()) {
+ Log.d(Constants.TAG, "New passphrase entered!");
+
+ // get again after it was entered
+ passphrase = PassphraseCacheService.getCachedPassphrase(mContext, keyId);
+ } else {
+ Log.d(Constants.TAG, "Passphrase dialog canceled!");
+
+ return null;
+ }
+
+ }
+
+ return passphrase;
+ }
+
+ public class PassphraseActivityCallback extends MyBaseCallback {
+
+ private boolean success = false;
+
+ public boolean isSuccess() {
+ return success;
+ }
+
+ @Override
+ public boolean handleMessage(Message msg) {
+ if (msg.arg1 == OKAY) {
+ success = true;
+ } else {
+ success = false;
+ }
+
+ // resume
+ synchronized (userInputLock) {
+ userInputLock.notifyAll();
+ }
+ mThreadPool.resume();
+ return true;
+ }
+ };
+
+ /**
+ * Search database for key ids based on emails.
+ *
+ * @param encryptionUserIds
+ * @return
+ */
+ private long[] getKeyIdsFromEmails(String[] encryptionUserIds, long ownKeyId) {
+ // find key ids to given emails in database
+ ArrayList<Long> keyIds = new ArrayList<Long>();
+
+ boolean missingUserIdsCheck = false;
+ boolean dublicateUserIdsCheck = false;
+ ArrayList<String> missingUserIds = new ArrayList<String>();
+ ArrayList<String> dublicateUserIds = new ArrayList<String>();
+
+ for (String email : encryptionUserIds) {
+ Uri uri = KeychainContract.KeyRings.buildPublicKeyRingsByEmailsUri(email);
+ Cursor cur = getContentResolver().query(uri, null, null, null, null);
+ if (cur.moveToFirst()) {
+ long id = cur.getLong(cur.getColumnIndex(KeychainContract.KeyRings.MASTER_KEY_ID));
+ keyIds.add(id);
+ } else {
+ missingUserIdsCheck = true;
+ missingUserIds.add(email);
+ Log.d(Constants.TAG, "user id missing");
+ }
+ if (cur.moveToNext()) {
+ dublicateUserIdsCheck = true;
+ dublicateUserIds.add(email);
+ Log.d(Constants.TAG, "more than one user id with the same email");
+ }
+ }
+
+ // also encrypt to our self (so that we can decrypt it later!)
+ keyIds.add(ownKeyId);
+
+ // convert to long[]
+ long[] keyIdsArray = new long[keyIds.size()];
+ for (int i = 0; i < keyIdsArray.length; i++) {
+ keyIdsArray[i] = keyIds.get(i);
+ }
+
+ if (missingUserIdsCheck || dublicateUserIdsCheck) {
+ SelectPubKeysActivityCallback callback = new SelectPubKeysActivityCallback();
+ Messenger messenger = new Messenger(new Handler(getMainLooper(), callback));
+
+ Bundle extras = new Bundle();
+ extras.putLongArray(OpenPgpServiceActivity.EXTRA_SELECTED_MASTER_KEY_IDS, keyIdsArray);
+ extras.putStringArrayList(OpenPgpServiceActivity.EXTRA_MISSING_USER_IDS, missingUserIds);
+ extras.putStringArrayList(OpenPgpServiceActivity.EXTRA_DUBLICATE_USER_IDS,
+ dublicateUserIds);
+
+ pauseQueueAndStartServiceActivity(OpenPgpServiceActivity.ACTION_SELECT_PUB_KEYS,
+ messenger, extras);
+
+ if (callback.isSuccess()) {
+ Log.d(Constants.TAG, "New selection of pub keys!");
+ keyIdsArray = callback.getPubKeyIds();
+ } else {
+ Log.d(Constants.TAG, "Pub key selection canceled!");
+ return null;
+ }
+ }
+
+ if (keyIdsArray.length == 0) {
+ return null;
+ }
+ return keyIdsArray;
+ }
+
+ public class SelectPubKeysActivityCallback extends MyBaseCallback {
+ public static final String PUB_KEY_IDS = "pub_key_ids";
+
+ private boolean success = false;
+ private long[] pubKeyIds;
+
+ public boolean isSuccess() {
+ return success;
+ }
+
+ public long[] getPubKeyIds() {
+ return pubKeyIds;
+ }
+
+ @Override
+ public boolean handleMessage(Message msg) {
+ if (msg.arg1 == OKAY) {
+ success = true;
+ pubKeyIds = msg.getData().getLongArray(PUB_KEY_IDS);
+ } else {
+ success = false;
+ }
+
+ // resume
+ synchronized (userInputLock) {
+ userInputLock.notifyAll();
+ }
+ mThreadPool.resume();
+ return true;
+ }
+ };
+
+ private synchronized void encryptAndSignSafe(byte[] inputBytes, String[] encryptionUserIds,
+ boolean asciiArmor, IOpenPgpCallback callback, AppSettings appSettings, boolean sign)
+ throws RemoteException {
+ try {
+ // build InputData and write into OutputStream
+ InputStream inputStream = new ByteArrayInputStream(inputBytes);
+ long inputLength = inputBytes.length;
+ InputData inputData = new InputData(inputStream, inputLength);
+
+ OutputStream outputStream = new ByteArrayOutputStream();
+
+ long[] keyIds = getKeyIdsFromEmails(encryptionUserIds, appSettings.getKeyId());
+ if (keyIds == null) {
+ callback.onError(new OpenPgpError(OpenPgpError.ID_NO_USER_IDS, "No user ids!"));
+ return;
+ }
+
+ if (sign) {
+ String passphrase = getCachedPassphrase(appSettings.getKeyId());
+ if (passphrase == null) {
+ callback.onError(new OpenPgpError(OpenPgpError.ID_NO_OR_WRONG_PASSPHRASE,
+ "No or wrong passphrase!"));
+ return;
+ }
+
+ PgpMain.encryptAndSign(mContext, null, inputData, outputStream, asciiArmor,
+ appSettings.getCompression(), keyIds, null,
+ appSettings.getEncryptionAlgorithm(), appSettings.getKeyId(),
+ appSettings.getHashAlgorithm(), true, passphrase);
+ } else {
+ PgpMain.encryptAndSign(mContext, null, inputData, outputStream, asciiArmor,
+ appSettings.getCompression(), keyIds, null,
+ appSettings.getEncryptionAlgorithm(), Id.key.none,
+ appSettings.getHashAlgorithm(), true, null);
+ }
+
+ outputStream.close();
+
+ byte[] outputBytes = ((ByteArrayOutputStream) outputStream).toByteArray();
+
+ // return over handler on client side
+ callback.onSuccess(outputBytes, null);
+ } catch (Exception e) {
+ Log.e(Constants.TAG, "KeychainService, Exception!", e);
+
+ try {
+ callback.onError(new OpenPgpError(0, e.getMessage()));
+ } catch (Exception t) {
+ Log.e(Constants.TAG, "Error returning exception to client", t);
+ }
+ }
+ }
+
+ // TODO: asciiArmor?!
+ private void signSafe(byte[] inputBytes, IOpenPgpCallback callback, AppSettings appSettings)
+ throws RemoteException {
+ try {
+ Log.d(Constants.TAG, "current therad id: " + Thread.currentThread().getId());
+
+ // build InputData and write into OutputStream
+ InputStream inputStream = new ByteArrayInputStream(inputBytes);
+ long inputLength = inputBytes.length;
+ InputData inputData = new InputData(inputStream, inputLength);
+
+ OutputStream outputStream = new ByteArrayOutputStream();
+
+ String passphrase = getCachedPassphrase(appSettings.getKeyId());
+ if (passphrase == null) {
+ callback.onError(new OpenPgpError(OpenPgpError.ID_NO_OR_WRONG_PASSPHRASE,
+ "No or wrong passphrase!"));
+ return;
+ }
+
+ PgpMain.signText(this, null, inputData, outputStream, appSettings.getKeyId(),
+ passphrase, appSettings.getHashAlgorithm(), Preferences.getPreferences(this)
+ .getForceV3Signatures());
+
+ outputStream.close();
+
+ byte[] outputBytes = ((ByteArrayOutputStream) outputStream).toByteArray();
+
+ // return over handler on client side
+ callback.onSuccess(outputBytes, null);
+ } catch (Exception e) {
+ Log.e(Constants.TAG, "KeychainService, Exception!", e);
+
+ try {
+ callback.onError(new OpenPgpError(0, e.getMessage()));
+ } catch (Exception t) {
+ Log.e(Constants.TAG, "Error returning exception to client", t);
+ }
+ }
+ }
+
+ private synchronized void decryptAndVerifySafe(byte[] inputBytes, IOpenPgpCallback callback,
+ AppSettings appSettings) throws RemoteException {
+ try {
+ // TODO: this is not really needed
+ // checked if it is text with BEGIN and END tags
+ String message = new String(inputBytes);
+ Log.d(Constants.TAG, "in: " + message);
+ boolean signedOnly = false;
+ Matcher matcher = PgpMain.PGP_MESSAGE.matcher(message);
+ if (matcher.matches()) {
+ Log.d(Constants.TAG, "PGP_MESSAGE matched");
+ message = matcher.group(1);
+ // replace non breakable spaces
+ message = message.replaceAll("\\xa0", " ");
+
+ // overwrite inputBytes
+ inputBytes = message.getBytes();
+ } else {
+ matcher = PgpMain.PGP_SIGNED_MESSAGE.matcher(message);
+ if (matcher.matches()) {
+ signedOnly = true;
+ Log.d(Constants.TAG, "PGP_SIGNED_MESSAGE matched");
+ message = matcher.group(1);
+ // replace non breakable spaces
+ message = message.replaceAll("\\xa0", " ");
+
+ // overwrite inputBytes
+ inputBytes = message.getBytes();
+ } else {
+ Log.d(Constants.TAG, "Nothing matched! Binary?");
+ }
+ }
+ // END TODO
+
+ Log.d(Constants.TAG, "in: " + new String(inputBytes));
+
+ // TODO: This allows to decrypt messages with ALL secret keys, not only the one for the
+ // app, Fix this?
+ // long secretKeyId = PgpMain.getDecryptionKeyId(mContext, inputStream);
+ // if (secretKeyId == Id.key.none) {
+ // throw new PgpMain.PgpGeneralException(getString(R.string.error_noSecretKeyFound));
+ // }
+
+ String passphrase = null;
+ boolean assumeSymmetricEncryption = false;
+ if (!signedOnly) {
+ // BEGIN Get key
+ // TODO: this input stream is consumed after PgpMain.getDecryptionKeyId()... do it
+ // better!
+ InputStream inputStream2 = new ByteArrayInputStream(inputBytes);
+
+ // TODO: duplicates functions from DecryptActivity!
+ // TODO: we need activity to input symmetric passphrase
+ long secretKeyId;
+ try {
+ if (inputStream2.markSupported()) {
+ inputStream2.mark(200); // should probably set this to the max size of two
+ // pgpF
+ // objects, if it even needs to be anything other
+ // than
+ // 0.
+ }
+ secretKeyId = PgpMain.getDecryptionKeyId(this, inputStream2);
+ if (secretKeyId == Id.key.none) {
+ throw new PgpMain.PgpGeneralException(
+ getString(R.string.error_noSecretKeyFound));
+ }
+ assumeSymmetricEncryption = false;
+ } catch (PgpMain.NoAsymmetricEncryptionException e) {
+ if (inputStream2.markSupported()) {
+ inputStream2.reset();
+ }
+ secretKeyId = Id.key.symmetric;
+ if (!PgpMain.hasSymmetricEncryption(this, inputStream2)) {
+ throw new PgpMain.PgpGeneralException(
+ getString(R.string.error_noKnownEncryptionFound));
+ }
+ assumeSymmetricEncryption = true;
+ }
+
+ Log.d(Constants.TAG, "secretKeyId " + secretKeyId);
+
+ passphrase = getCachedPassphrase(secretKeyId);
+ if (passphrase == null) {
+ callback.onError(new OpenPgpError(OpenPgpError.ID_NO_OR_WRONG_PASSPHRASE,
+ "No or wrong passphrase!"));
+ return;
+ }
+ }
+
+ // build InputData and write into OutputStream
+ InputStream inputStream = new ByteArrayInputStream(inputBytes);
+ long inputLength = inputBytes.length;
+ InputData inputData = new InputData(inputStream, inputLength);
+
+ OutputStream outputStream = new ByteArrayOutputStream();
+
+ Bundle outputBundle;
+ if (signedOnly) {
+ // TODO: download missing keys from keyserver?
+ outputBundle = PgpMain.verifyText(this, null, inputData, outputStream, false);
+ } else {
+ // TODO: assume symmetric: callback to enter symmetric pass
+ outputBundle = PgpMain.decryptAndVerify(this, null, inputData, outputStream,
+ passphrase, assumeSymmetricEncryption);
+ }
+
+ outputStream.close();
+
+ byte[] outputBytes = ((ByteArrayOutputStream) outputStream).toByteArray();
+
+ // get signature informations from bundle
+ boolean signature = outputBundle.getBoolean(KeychainIntentService.RESULT_SIGNATURE);
+ long signatureKeyId = outputBundle
+ .getLong(KeychainIntentService.RESULT_SIGNATURE_KEY_ID);
+ String signatureUserId = outputBundle
+ .getString(KeychainIntentService.RESULT_SIGNATURE_USER_ID);
+ boolean signatureSuccess = outputBundle
+ .getBoolean(KeychainIntentService.RESULT_SIGNATURE_SUCCESS);
+ boolean signatureUnknown = outputBundle
+ .getBoolean(KeychainIntentService.RESULT_SIGNATURE_UNKNOWN);
+
+ OpenPgpSignatureResult sigResult = null;
+ if (signature) {
+ sigResult = new OpenPgpSignatureResult(signatureUserId, signatureSuccess,
+ signatureUnknown);
+ }
+
+ // return over handler on client side
+ callback.onSuccess(outputBytes, sigResult);
+ } catch (Exception e) {
+ Log.e(Constants.TAG, "KeychainService, Exception!", e);
+
+ try {
+ callback.onError(new OpenPgpError(0, e.getMessage()));
+ } catch (Exception t) {
+ Log.e(Constants.TAG, "Error returning exception to client", t);
+ }
+ }
+ }
+
+ private final IOpenPgpService.Stub mBinder = new IOpenPgpService.Stub() {
+
+ @Override
+ public void encrypt(final byte[] inputBytes, final String[] encryptionUserIds,
+ final boolean asciiArmor, final IOpenPgpCallback callback) throws RemoteException {
+
+ final AppSettings settings = getAppSettings();
+
+ Runnable r = new Runnable() {
+
+ @Override
+ public void run() {
+ try {
+ encryptAndSignSafe(inputBytes, encryptionUserIds, asciiArmor, callback,
+ settings, false);
+ } catch (RemoteException e) {
+ Log.e(Constants.TAG, "CryptoService", e);
+ }
+ }
+ };
+
+ checkAndEnqueue(r);
+ }
+
+ @Override
+ public void encryptAndSign(final byte[] inputBytes, final String[] encryptionUserIds,
+ final boolean asciiArmor, final IOpenPgpCallback callback) throws RemoteException {
+
+ final AppSettings settings = getAppSettings();
+
+ Runnable r = new Runnable() {
+
+ @Override
+ public void run() {
+ try {
+ encryptAndSignSafe(inputBytes, encryptionUserIds, asciiArmor, callback,
+ settings, true);
+ } catch (RemoteException e) {
+ Log.e(Constants.TAG, "CryptoService", e);
+ }
+ }
+ };
+
+ checkAndEnqueue(r);
+ }
+
+ @Override
+ public void sign(final byte[] inputBytes, boolean asciiArmor,
+ final IOpenPgpCallback callback) throws RemoteException {
+ final AppSettings settings = getAppSettings();
+
+ Runnable r = new Runnable() {
+
+ @Override
+ public void run() {
+ try {
+ signSafe(inputBytes, callback, settings);
+ } catch (RemoteException e) {
+ Log.e(Constants.TAG, "CryptoService", e);
+ }
+ }
+ };
+
+ checkAndEnqueue(r);
+
+ }
+
+ @Override
+ public void decryptAndVerify(final byte[] inputBytes, final IOpenPgpCallback callback)
+ throws RemoteException {
+
+ final AppSettings settings = getAppSettings();
+
+ Runnable r = new Runnable() {
+
+ @Override
+ public void run() {
+ try {
+ decryptAndVerifySafe(inputBytes, callback, settings);
+ } catch (RemoteException e) {
+ Log.e(Constants.TAG, "CryptoService", e);
+ }
+ }
+ };
+
+ checkAndEnqueue(r);
+ }
+
+ };
+
+ private void checkAndEnqueue(Runnable r) {
+ if (isCallerAllowed(false)) {
+ mThreadPool.execute(r);
+
+ Log.d(Constants.TAG, "Enqueued runnable…");
+ } else {
+ String[] callingPackages = getPackageManager()
+ .getPackagesForUid(Binder.getCallingUid());
+
+ Log.e(Constants.TAG, "Not allowed to use service! Starting activity for registration!");
+ Bundle extras = new Bundle();
+ // TODO: currently simply uses first entry
+ extras.putString(OpenPgpServiceActivity.EXTRA_PACKAGE_NAME, callingPackages[0]);
+
+ RegisterActivityCallback callback = new RegisterActivityCallback();
+ Messenger messenger = new Messenger(new Handler(getMainLooper(), callback));
+
+ pauseQueueAndStartServiceActivity(OpenPgpServiceActivity.ACTION_REGISTER, messenger,
+ extras);
+
+ if (callback.isAllowed()) {
+ mThreadPool.execute(r);
+ Log.d(Constants.TAG, "Enqueued runnable…");
+ } else {
+ Log.d(Constants.TAG, "User disallowed app!");
+ }
+ }
+ }
+
+ public class RegisterActivityCallback extends MyBaseCallback {
+ public static final String PACKAGE_NAME = "package_name";
+
+ private boolean allowed = false;
+ private String packageName;
+
+ public boolean isAllowed() {
+ return allowed;
+ }
+
+ public String getPackageName() {
+ return packageName;
+ }
+
+ @Override
+ public boolean handleMessage(Message msg) {
+ if (msg.arg1 == OKAY) {
+ allowed = true;
+ packageName = msg.getData().getString(PACKAGE_NAME);
+
+ // resume threads
+ if (isPackageAllowed(packageName, false)) {
+ synchronized (userInputLock) {
+ userInputLock.notifyAll();
+ }
+ mThreadPool.resume();
+ } else {
+ // Should not happen!
+ Log.e(Constants.TAG, "Should not happen! Emergency shutdown!");
+ mThreadPool.shutdownNow();
+ }
+ } else {
+ allowed = false;
+
+ synchronized (userInputLock) {
+ userInputLock.notifyAll();
+ }
+ mThreadPool.resume();
+ }
+ return true;
+ }
+
+ }
+
+ /**
+ * Checks if process that binds to this service (i.e. the package name corresponding to the
+ * process) is in the list of allowed package names.
+ *
+ * @param allowOnlySelf
+ * allow only Keychain app itself
+ * @return true if process is allowed to use this service
+ */
+ private boolean isCallerAllowed(boolean allowOnlySelf) {
+ String[] callingPackages = getPackageManager().getPackagesForUid(Binder.getCallingUid());
+
+ // is calling package allowed to use this service?
+ for (int i = 0; i < callingPackages.length; i++) {
+ String currentPkg = callingPackages[i];
+
+ if (isPackageAllowed(currentPkg, allowOnlySelf)) {
+ return true;
+ }
+ }
+
+ Log.d(Constants.TAG, "Caller is NOT allowed!");
+ return false;
+ }
+
+ private AppSettings getAppSettings() {
+ String[] callingPackages = getPackageManager().getPackagesForUid(Binder.getCallingUid());
+
+ // is calling package allowed to use this service?
+ for (int i = 0; i < callingPackages.length; i++) {
+ String currentPkg = callingPackages[i];
+
+ Uri uri = KeychainContract.ApiApps.buildByPackageNameUri(currentPkg);
+
+ AppSettings settings = ProviderHelper.getApiAppSettings(this, uri);
+
+ return settings;
+ }
+
+ return null;
+ }
+
+ /**
+ * Checks if packageName is a registered app for the API.
+ *
+ * @param packageName
+ * @param allowOnlySelf
+ * allow only Keychain app itself
+ * @return
+ */
+ private boolean isPackageAllowed(String packageName, boolean allowOnlySelf) {
+ Log.d(Constants.TAG, "packageName: " + packageName);
+
+ ArrayList<String> allowedPkgs = ProviderHelper.getRegisteredApiApps(mContext);
+ Log.d(Constants.TAG, "allowed: " + allowedPkgs);
+
+ // check if package is allowed to use our service
+ if (allowedPkgs.contains(packageName) && (!allowOnlySelf)) {
+ Log.d(Constants.TAG, "Package is allowed! packageName: " + packageName);
+
+ return true;
+ } else if (Constants.PACKAGE_NAME.equals(packageName)) {
+ Log.d(Constants.TAG, "Package is OpenPGP Keychain! -> allowed!");
+
+ return true;
+ }
+
+ return false;
+ }
+
+ private void pauseQueueAndStartServiceActivity(String action, Messenger messenger, Bundle extras) {
+ synchronized (userInputLock) {
+ mThreadPool.pause();
+
+ Log.d(Constants.TAG, "starting activity...");
+ Intent intent = new Intent(getBaseContext(), OpenPgpServiceActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ intent.setAction(action);
+
+ extras.putParcelable(OpenPgpServiceActivity.EXTRA_MESSENGER, messenger);
+ intent.putExtras(extras);
+
+ startActivity(intent);
+
+ // lock current thread for user input
+ try {
+ userInputLock.wait();
+ } catch (InterruptedException e) {
+ Log.e(Constants.TAG, "CryptoService", e);
+ }
+ }
+
+ }
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/openpgp_api/OpenPgpServiceActivity.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/openpgp_api/OpenPgpServiceActivity.java
new file mode 100644
index 000000000..775828124
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/openpgp_api/OpenPgpServiceActivity.java
@@ -0,0 +1,325 @@
+/*
+ * Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.openpgp_api;
+
+import java.util.ArrayList;
+
+import org.sufficientlysecure.htmltextview.HtmlTextView;
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.Id;
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.helper.ActionBarHelper;
+import org.sufficientlysecure.keychain.helper.PgpMain;
+import org.sufficientlysecure.keychain.provider.ProviderHelper;
+import org.sufficientlysecure.keychain.ui.SelectPublicKeyFragment;
+import org.sufficientlysecure.keychain.ui.dialog.PassphraseDialogFragment;
+import org.sufficientlysecure.keychain.util.Log;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.os.Messenger;
+import android.os.RemoteException;
+import android.view.View;
+import android.widget.Toast;
+
+import com.actionbarsherlock.app.SherlockFragmentActivity;
+
+public class OpenPgpServiceActivity extends SherlockFragmentActivity {
+
+ public static final String ACTION_REGISTER = Constants.INTENT_PREFIX + "API_ACTIVITY_REGISTER";
+ public static final String ACTION_CACHE_PASSPHRASE = Constants.INTENT_PREFIX
+ + "API_ACTIVITY_CACHE_PASSPHRASE";
+ public static final String ACTION_SELECT_PUB_KEYS = Constants.INTENT_PREFIX
+ + "API_ACTIVITY_SELECT_PUB_KEYS";
+
+ public static final String EXTRA_MESSENGER = "messenger";
+
+ // passphrase action
+ public static final String EXTRA_SECRET_KEY_ID = "secret_key_id";
+ // register action
+ public static final String EXTRA_PACKAGE_NAME = "package_name";
+ // select pub keys action
+ public static final String EXTRA_SELECTED_MASTER_KEY_IDS = "master_key_ids";
+ public static final String EXTRA_MISSING_USER_IDS = "missing_user_ids";
+ public static final String EXTRA_DUBLICATE_USER_IDS = "dublicate_user_ids";
+
+ private Messenger mMessenger;
+
+ // register view
+ private AppSettingsFragment mSettingsFragment;
+ // select pub keys view
+ private SelectPublicKeyFragment mSelectFragment;
+
+ // has the user clicked one of the buttons
+ // or do we need to handle the callback in onStop()
+ private boolean finishHandled;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ handleActions(getIntent(), savedInstanceState);
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+
+ if (!finishHandled) {
+ Message msg = Message.obtain();
+ msg.arg1 = OpenPgpService.RegisterActivityCallback.CANCEL;
+ try {
+ mMessenger.send(msg);
+ } catch (RemoteException e) {
+ Log.e(Constants.TAG, "CryptoServiceActivity", e);
+ }
+ }
+ }
+
+ protected void handleActions(Intent intent, Bundle savedInstanceState) {
+ finishHandled = false;
+
+ String action = intent.getAction();
+ Bundle extras = intent.getExtras();
+
+ if (extras == null) {
+ extras = new Bundle();
+ }
+
+ mMessenger = extras.getParcelable(EXTRA_MESSENGER);
+
+ /**
+ * com.android.crypto actions
+ */
+ if (ACTION_REGISTER.equals(action)) {
+ final String packageName = extras.getString(EXTRA_PACKAGE_NAME);
+
+ // Inflate a "Done"/"Cancel" custom action bar view
+ ActionBarHelper.setDoneCancelView(getSupportActionBar(), R.string.api_register_allow,
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ // Allow
+
+ // user needs to select a key!
+ if (mSettingsFragment.getAppSettings().getKeyId() == Id.key.none) {
+ Toast.makeText(OpenPgpServiceActivity.this,
+ R.string.api_register_error_select_key, Toast.LENGTH_LONG)
+ .show();
+ } else {
+ ProviderHelper.insertApiApp(OpenPgpServiceActivity.this,
+ mSettingsFragment.getAppSettings());
+
+ Message msg = Message.obtain();
+ msg.arg1 = OpenPgpService.RegisterActivityCallback.OKAY;
+ Bundle data = new Bundle();
+ data.putString(OpenPgpService.RegisterActivityCallback.PACKAGE_NAME,
+ packageName);
+ msg.setData(data);
+ try {
+ mMessenger.send(msg);
+ } catch (RemoteException e) {
+ Log.e(Constants.TAG, "CryptoServiceActivity", e);
+ }
+
+ finishHandled = true;
+ finish();
+ }
+ }
+ }, R.string.api_register_disallow, new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ // Disallow
+
+ Message msg = Message.obtain();
+ msg.arg1 = OpenPgpService.RegisterActivityCallback.CANCEL;
+ try {
+ mMessenger.send(msg);
+ } catch (RemoteException e) {
+ Log.e(Constants.TAG, "CryptoServiceActivity", e);
+ }
+
+ finishHandled = true;
+ finish();
+ }
+ });
+
+ setContentView(R.layout.api_app_register_activity);
+
+ mSettingsFragment = (AppSettingsFragment) getSupportFragmentManager().findFragmentById(
+ R.id.api_app_settings_fragment);
+
+ AppSettings settings = new AppSettings(packageName);
+ mSettingsFragment.setAppSettings(settings);
+ } else if (ACTION_CACHE_PASSPHRASE.equals(action)) {
+ long secretKeyId = extras.getLong(EXTRA_SECRET_KEY_ID);
+
+ showPassphraseDialog(secretKeyId);
+ } else if (ACTION_SELECT_PUB_KEYS.equals(action)) {
+ long[] selectedMasterKeyIds = intent.getLongArrayExtra(EXTRA_SELECTED_MASTER_KEY_IDS);
+ ArrayList<String> missingUserIds = intent
+ .getStringArrayListExtra(EXTRA_MISSING_USER_IDS);
+ ArrayList<String> dublicateUserIds = intent
+ .getStringArrayListExtra(EXTRA_DUBLICATE_USER_IDS);
+
+ String text = new String();
+ text += "<b>" + getString(R.string.api_select_pub_keys_text) + "</b>";
+ text += "<br/><br/>";
+ if (missingUserIds != null && missingUserIds.size() > 0) {
+ text += getString(R.string.api_select_pub_keys_missing_text);
+ text += "<br/>";
+ text += "<ul>";
+ for (String userId : missingUserIds) {
+ text += "<li>" + userId + "</li>";
+ }
+ text += "</ul>";
+ text += "<br/>";
+ }
+ if (dublicateUserIds != null && dublicateUserIds.size() > 0) {
+ text += getString(R.string.api_select_pub_keys_dublicates_text);
+ text += "<br/>";
+ text += "<ul>";
+ for (String userId : dublicateUserIds) {
+ text += "<li>" + userId + "</li>";
+ }
+ text += "</ul>";
+ }
+
+ // Inflate a "Done"/"Cancel" custom action bar view
+ ActionBarHelper.setDoneCancelView(getSupportActionBar(), R.string.btn_okay,
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ // ok
+
+ Message msg = Message.obtain();
+ msg.arg1 = OpenPgpService.SelectPubKeysActivityCallback.OKAY;
+ Bundle data = new Bundle();
+ data.putLongArray(
+ OpenPgpService.SelectPubKeysActivityCallback.PUB_KEY_IDS,
+ mSelectFragment.getSelectedMasterKeyIds());
+ msg.setData(data);
+ try {
+ mMessenger.send(msg);
+ } catch (RemoteException e) {
+ Log.e(Constants.TAG, "CryptoServiceActivity", e);
+ }
+
+ finishHandled = true;
+ finish();
+ }
+ }, R.string.btn_doNotSave, new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ // cancel
+
+ Message msg = Message.obtain();
+ msg.arg1 = OpenPgpService.SelectPubKeysActivityCallback.CANCEL;
+ ;
+ try {
+ mMessenger.send(msg);
+ } catch (RemoteException e) {
+ Log.e(Constants.TAG, "CryptoServiceActivity", e);
+ }
+
+ finishHandled = true;
+ finish();
+ }
+ });
+
+ setContentView(R.layout.api_app_select_pub_keys_activity);
+
+ // set text on view
+ HtmlTextView textView = (HtmlTextView) findViewById(R.id.api_select_pub_keys_text);
+ textView.setHtmlFromString(text);
+
+ /* Load select pub keys fragment */
+ // Check that the activity is using the layout version with
+ // the fragment_container FrameLayout
+ if (findViewById(R.id.api_select_pub_keys_fragment_container) != null) {
+
+ // However, if we're being restored from a previous state,
+ // then we don't need to do anything and should return or else
+ // we could end up with overlapping fragments.
+ if (savedInstanceState != null) {
+ return;
+ }
+
+ // Create an instance of the fragment
+ mSelectFragment = SelectPublicKeyFragment.newInstance(selectedMasterKeyIds);
+
+ // Add the fragment to the 'fragment_container' FrameLayout
+ getSupportFragmentManager().beginTransaction()
+ .add(R.id.api_select_pub_keys_fragment_container, mSelectFragment).commit();
+ }
+ } else {
+ Log.e(Constants.TAG, "Wrong action!");
+ finish();
+ }
+ }
+
+ /**
+ * Shows passphrase dialog to cache a new passphrase the user enters for using it later for
+ * encryption. Based on mSecretKeyId it asks for a passphrase to open a private key or it asks
+ * for a symmetric passphrase
+ */
+ private void showPassphraseDialog(long secretKeyId) {
+ // Message is received after passphrase is cached
+ Handler returnHandler = new Handler() {
+ @Override
+ public void handleMessage(Message message) {
+ if (message.what == PassphraseDialogFragment.MESSAGE_OKAY) {
+ Message msg = Message.obtain();
+ msg.arg1 = OpenPgpService.PassphraseActivityCallback.OKAY;
+ try {
+ mMessenger.send(msg);
+ } catch (RemoteException e) {
+ Log.e(Constants.TAG, "CryptoServiceActivity", e);
+ }
+ } else {
+ Message msg = Message.obtain();
+ msg.arg1 = OpenPgpService.PassphraseActivityCallback.CANCEL;
+ try {
+ mMessenger.send(msg);
+ } catch (RemoteException e) {
+ Log.e(Constants.TAG, "CryptoServiceActivity", e);
+ }
+ }
+
+ finishHandled = true;
+ finish();
+ }
+ };
+
+ // Create a new Messenger for the communication back
+ Messenger messenger = new Messenger(returnHandler);
+
+ try {
+ PassphraseDialogFragment passphraseDialog = PassphraseDialogFragment.newInstance(this,
+ messenger, secretKeyId);
+
+ passphraseDialog.show(getSupportFragmentManager(), "passphraseDialog");
+ } catch (PgpMain.PgpGeneralException e) {
+ Log.d(Constants.TAG, "No passphrase for this secret key, encrypt directly!");
+ // send message to handler to start encryption directly
+ returnHandler.sendEmptyMessage(PassphraseDialogFragment.MESSAGE_OKAY);
+ }
+ }
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/openpgp_api/RegisteredAppsAdapter.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/openpgp_api/RegisteredAppsAdapter.java
new file mode 100644
index 000000000..bddf3d4a8
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/openpgp_api/RegisteredAppsAdapter.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.openpgp_api;
+
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.provider.KeychainContract.ApiApps;
+
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+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.ImageView;
+import android.widget.TextView;
+
+public class RegisteredAppsAdapter extends CursorAdapter {
+
+ private LayoutInflater mInflater;
+ private PackageManager pm;
+
+ public RegisteredAppsAdapter(Context context, Cursor c, int flags) {
+ super(context, c, flags);
+
+ mInflater = LayoutInflater.from(context);
+ pm = context.getApplicationContext().getPackageManager();
+ }
+
+ @Override
+ public void bindView(View view, Context context, Cursor cursor) {
+ TextView text = (TextView) view.findViewById(R.id.api_apps_adapter_item_name);
+ ImageView icon = (ImageView) view.findViewById(R.id.api_apps_adapter_item_icon);
+
+ String packageName = cursor.getString(cursor.getColumnIndex(ApiApps.PACKAGE_NAME));
+ if (packageName != null) {
+ // get application name
+ try {
+ ApplicationInfo ai = pm.getApplicationInfo(packageName, 0);
+
+ text.setText(pm.getApplicationLabel(ai));
+ icon.setImageDrawable(pm.getApplicationIcon(ai));
+ } catch (final NameNotFoundException e) {
+ // fallback
+ text.setText(packageName);
+ }
+ } else {
+ // fallback
+ text.setText(packageName);
+ }
+
+ }
+
+ @Override
+ public View newView(Context context, Cursor cursor, ViewGroup parent) {
+ return mInflater.inflate(R.layout.api_apps_adapter_list_item, null);
+ }
+
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/openpgp_api/RegisteredAppsListActivity.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/openpgp_api/RegisteredAppsListActivity.java
new file mode 100644
index 000000000..d8d7c3d5c
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/openpgp_api/RegisteredAppsListActivity.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.openpgp_api;
+
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.ui.MainActivity;
+
+import com.actionbarsherlock.app.ActionBar;
+import com.actionbarsherlock.app.SherlockFragmentActivity;
+import com.actionbarsherlock.view.MenuItem;
+
+import android.content.Intent;
+import android.os.Bundle;
+
+public class RegisteredAppsListActivity extends SherlockFragmentActivity {
+ private ActionBar mActionBar;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ mActionBar = getSupportActionBar();
+
+ setContentView(R.layout.api_apps_list_activity);
+
+ mActionBar.setDisplayShowTitleEnabled(true);
+ mActionBar.setDisplayHomeAsUpEnabled(true);
+ }
+
+ /**
+ * Menu Options
+ */
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ // app icon in Action Bar clicked; go home
+ Intent intent = new Intent(this, MainActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ startActivity(intent);
+ return true;
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/openpgp_api/RegisteredAppsListFragment.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/openpgp_api/RegisteredAppsListFragment.java
new file mode 100644
index 000000000..391b86d72
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/openpgp_api/RegisteredAppsListFragment.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.openpgp_api;
+
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.provider.KeychainContract;
+import org.sufficientlysecure.keychain.provider.KeychainContract.ApiApps;
+
+import android.content.ContentUris;
+import android.content.Intent;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.v4.app.LoaderManager;
+import android.support.v4.content.CursorLoader;
+import android.support.v4.content.Loader;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+
+import com.actionbarsherlock.app.SherlockListFragment;
+
+public class RegisteredAppsListFragment extends SherlockListFragment implements
+ LoaderManager.LoaderCallbacks<Cursor> {
+
+ // This is the Adapter being used to display the list's data.
+ RegisteredAppsAdapter mAdapter;
+
+ // If non-null, this is the current filter the user has provided.
+ String mCurFilter;
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ getListView().setOnItemClickListener(new OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView<?> adapterView, View view, int position, long id) {
+ // edit app settings
+ Intent intent = new Intent(getActivity(), AppSettingsActivity.class);
+ intent.setData(ContentUris.withAppendedId(KeychainContract.ApiApps.CONTENT_URI, id));
+ startActivity(intent);
+ }
+ });
+
+ // Give some text to display if there is no data. In a real
+ // application this would come from a resource.
+ setEmptyText(getString(R.string.api_no_apps));
+
+ // We have a menu item to show in action bar.
+ setHasOptionsMenu(true);
+
+ // Create an empty adapter we will use to display the loaded data.
+ mAdapter = new RegisteredAppsAdapter(getActivity(), null, 0);
+ setListAdapter(mAdapter);
+
+ // Prepare the loader. Either re-connect with an existing one,
+ // or start a new one.
+ getLoaderManager().initLoader(0, null, this);
+ }
+
+ // These are the Contacts rows that we will retrieve.
+ static final String[] CONSUMERS_SUMMARY_PROJECTION = new String[] { ApiApps._ID,
+ ApiApps.PACKAGE_NAME };
+
+ public Loader<Cursor> onCreateLoader(int id, Bundle args) {
+ // This is called when a new Loader needs to be created. This
+ // sample only has one Loader, so we don't care about the ID.
+ // First, pick the base URI to use depending on whether we are
+ // currently filtering.
+ Uri baseUri = ApiApps.CONTENT_URI;
+
+ // Now create and return a CursorLoader that will take care of
+ // creating a Cursor for the data being displayed.
+ return new CursorLoader(getActivity(), baseUri, CONSUMERS_SUMMARY_PROJECTION, null, null,
+ ApiApps.PACKAGE_NAME + " COLLATE LOCALIZED ASC");
+ }
+
+ public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
+ // Swap the new cursor in. (The framework will take care of closing the
+ // old cursor once we return.)
+ mAdapter.swapCursor(data);
+ }
+
+ public void onLoaderReset(Loader<Cursor> loader) {
+ // This is called when the last Cursor provided to onLoadFinished()
+ // above is about to be closed. We need to make sure we are no
+ // longer using it.
+ mAdapter.swapCursor(null);
+ }
+
+} \ No newline at end of file