Merge pull request #277 from openpgp-keychain/new-api

New api
This commit is contained in:
Dominik Schürmann 2014-02-17 20:03:38 +01:00
commit 2e3d0d4441
86 changed files with 2765 additions and 2603 deletions

View File

@ -1,10 +1,11 @@
// please leave this here, so this library builds on its own
buildscript { buildscript {
repositories { repositories {
mavenCentral() mavenCentral()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:0.8.0' classpath 'com.android.tools.build:gradle:0.8.3'
} }
} }
@ -20,7 +21,7 @@ android {
buildToolsVersion "19.0.1" buildToolsVersion "19.0.1"
defaultConfig { defaultConfig {
minSdkVersion 8 minSdkVersion 9
targetSdkVersion 19 targetSdkVersion 19
} }

View File

@ -2,19 +2,19 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.sufficientlysecure.keychain.demo" package="org.sufficientlysecure.keychain.demo"
android:versionCode="2" android:versionCode="2"
android:versionName="1.1" > android:versionName="1.1">
<uses-sdk <uses-sdk
android:minSdkVersion="8" android:minSdkVersion="9"
android:targetSdkVersion="19" /> android:targetSdkVersion="19" />
<application <application
android:allowBackup="true" android:allowBackup="true"
android:icon="@drawable/ic_launcher" android:icon="@drawable/ic_launcher"
android:label="OpenPGP Keychain API Demo" > android:label="OpenPGP Keychain API Demo">
<activity <activity
android:name=".BaseActivity" android:name=".BaseActivity"
android:label="OpenPGP Keychain API Demo" > android:label="OpenPGP Keychain API Demo">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
@ -26,9 +26,8 @@
android:label="OpenPGP Provider" android:label="OpenPGP Provider"
android:windowSoftInputMode="stateHidden" /> android:windowSoftInputMode="stateHidden" />
<activity <activity
android:name=".AidlDemoActivity2" android:name=".IntentActivity"
android:label="Aidl Demo (ACCESS_KEYS permission)" android:label="Intents" />
android:windowSoftInputMode="stateHidden" />
</application> </application>
</manifest> </manifest>

View File

@ -1,168 +0,0 @@
///*
// * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
// *
// * Licensed under the Apache License, Version 2.0 (the "License");
// * you may not use this file except in compliance with the License.
// * You may obtain a copy of the License at
// *
// * http://www.apache.org/licenses/LICENSE-2.0
// *
// * Unless required by applicable law or agreed to in writing, software
// * distributed under the License is distributed on an "AS IS" BASIS,
// * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// * See the License for the specific language governing permissions and
// * limitations under the License.
// */
//
//package org.sufficientlysecure.keychain.demo;
//
//import java.util.ArrayList;
//import java.util.List;
//
//import org.sufficientlysecure.keychain.demo.R;
//import org.sufficientlysecure.keychain.integration.KeychainData;
//import org.sufficientlysecure.keychain.integration.KeychainIntentHelper;
//import org.sufficientlysecure.keychain.service.IKeychainKeyService;
//import org.sufficientlysecure.keychain.service.handler.IKeychainGetKeyringsHandler;
//
//import android.annotation.SuppressLint;
//import android.app.Activity;
//import android.app.AlertDialog;
//import android.content.ComponentName;
//import android.content.Context;
//import android.content.Intent;
//import android.content.ServiceConnection;
//import android.os.Bundle;
//import android.os.IBinder;
//import android.os.RemoteException;
//import android.util.Base64;
//import android.view.View;
//import android.widget.TextView;
//
//public class AidlDemoActivity2 extends Activity {
// Activity mActivity;
//
// TextView mKeyringsTextView;
//
// KeychainIntentHelper mKeychainIntentHelper;
// KeychainData mKeychainData;
//
// byte[] keysBytes;
// ArrayList<String> keysStrings;
//
// private IKeychainKeyService service = null;
// private ServiceConnection svcConn = new ServiceConnection() {
// public void onServiceConnected(ComponentName className, IBinder binder) {
// service = IKeychainKeyService.Stub.asInterface(binder);
// }
//
// public void onServiceDisconnected(ComponentName className) {
// service = null;
// }
// };
//
// @Override
// public void onCreate(Bundle icicle) {
// super.onCreate(icicle);
// setContentView(R.layout.aidl_demo2);
//
// mActivity = this;
//
// mKeyringsTextView = (TextView) findViewById(R.id.aidl_demo_keyrings);
//
// mKeychainIntentHelper = new KeychainIntentHelper(mActivity);
// mKeychainData = new KeychainData();
//
// bindService(new Intent(IKeychainKeyService.class.getName()), svcConn,
// Context.BIND_AUTO_CREATE);
// }
//
// public void getKeyringsStringsOnClick(View view) {
// try {
// service.getPublicKeyRings(mKeychainData.getPublicKeys(), true, getKeyringsHandler);
// } catch (RemoteException e) {
// exceptionImplementation(-1, e.toString());
// }
// }
//
// public void getKeyringsBytesOnClick(View view) {
// try {
// service.getPublicKeyRings(mKeychainData.getPublicKeys(), false, getKeyringsHandler);
// } catch (RemoteException e) {
// exceptionImplementation(-1, e.toString());
// }
// }
//
// @SuppressLint("NewApi")
// private void updateView() {
// if (keysBytes != null) {
// mKeyringsTextView.setText(Base64.encodeToString(keysBytes, Base64.DEFAULT));
// } else if (keysStrings != null) {
// mKeyringsTextView.setText("");
// for (String output : keysStrings) {
// mKeyringsTextView.append(output);
// }
// }
// }
//
// @Override
// public void onDestroy() {
// super.onDestroy();
//
// unbindService(svcConn);
// }
//
// private void exceptionImplementation(int exceptionId, String error) {
// AlertDialog.Builder builder = new AlertDialog.Builder(this);
// builder.setTitle("Exception!").setMessage(error).setPositiveButton("OK", null).show();
// }
//
// private final IKeychainGetKeyringsHandler.Stub getKeyringsHandler = new IKeychainGetKeyringsHandler.Stub() {
//
// @Override
// public void onException(final int exceptionId, final String message) throws RemoteException {
// runOnUiThread(new Runnable() {
// public void run() {
// exceptionImplementation(exceptionId, message);
// }
// });
// }
//
// @Override
// public void onSuccess(final byte[] outputBytes, final List<String> outputStrings)
// throws RemoteException {
// runOnUiThread(new Runnable() {
// public void run() {
// if (outputBytes != null) {
// keysBytes = outputBytes;
// keysStrings = null;
// } else if (outputStrings != null) {
// keysBytes = null;
// keysStrings = (ArrayList<String>) outputStrings;
// }
// updateView();
// }
// });
//
// }
//
// };
//
// public void selectEncryptionKeysOnClick(View view) {
// mKeychainIntentHelper.selectPublicKeys("user@example.com");
// }
//
// @Override
// protected void onActivityResult(int requestCode, int resultCode, Intent data) {
// // this updates the mKeychainData object to the result of the methods
// boolean result = mKeychainIntentHelper.onActivityResult(requestCode, resultCode, data,
// mKeychainData);
// if (result) {
// updateView();
// }
//
// // continue with other activity results
// super.onActivityResult(requestCode, resultCode, data);
// }
//
//}

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de> * Copyright (C) 2012-2014 Dominik Schürmann <dominik@dominikschuermann.de>
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -16,9 +16,6 @@
package org.sufficientlysecure.keychain.demo; package org.sufficientlysecure.keychain.demo;
import org.sufficientlysecure.keychain.demo.R;
import android.app.Activity;
import android.content.Intent; import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.preference.Preference; import android.preference.Preference;
@ -27,13 +24,8 @@ import android.preference.PreferenceActivity;
import android.widget.Toast; import android.widget.Toast;
public class BaseActivity extends PreferenceActivity { public class BaseActivity extends PreferenceActivity {
private Activity mActivity;
private Preference mIntentDemo; private Preference mIntentDemo;
private Preference mContentProviderDemo;
private Preference mCryptoProvider; private Preference mCryptoProvider;
private Preference mAidlDemo;
private Preference mAidlDemo2;
/** /**
* Called when the activity is first created. * Called when the activity is first created.
@ -42,23 +34,17 @@ public class BaseActivity extends PreferenceActivity {
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
mActivity = this;
// load preferences from xml // load preferences from xml
addPreferencesFromResource(R.xml.base_preference); addPreferencesFromResource(R.xml.base_preference);
// find preferences // find preferences
mIntentDemo = (Preference) findPreference("intent_demo"); mIntentDemo = (Preference) findPreference("intent_demo");
mContentProviderDemo = (Preference) findPreference("content_provider_demo");
mCryptoProvider = (Preference) findPreference("openpgp_provider_demo"); mCryptoProvider = (Preference) findPreference("openpgp_provider_demo");
mAidlDemo = (Preference) findPreference("aidl_demo");
mAidlDemo2 = (Preference) findPreference("aidl_demo2");
mIntentDemo.setOnPreferenceClickListener(new OnPreferenceClickListener() { mIntentDemo.setOnPreferenceClickListener(new OnPreferenceClickListener() {
@Override @Override
public boolean onPreferenceClick(Preference preference) { public boolean onPreferenceClick(Preference preference) {
// startActivity(new Intent(mActivity, IntentDemoActivity.class)); startActivity(new Intent(BaseActivity.this, IntentActivity.class));
Toast.makeText(BaseActivity.this, "Not implemented!", Toast.LENGTH_LONG).show();
return false; return false;
} }
@ -67,21 +53,11 @@ public class BaseActivity extends PreferenceActivity {
mCryptoProvider.setOnPreferenceClickListener(new OnPreferenceClickListener() { mCryptoProvider.setOnPreferenceClickListener(new OnPreferenceClickListener() {
@Override @Override
public boolean onPreferenceClick(Preference preference) { public boolean onPreferenceClick(Preference preference) {
startActivity(new Intent(mActivity, OpenPgpProviderActivity.class)); startActivity(new Intent(BaseActivity.this, OpenPgpProviderActivity.class));
return false; return false;
} }
}); });
// mAidlDemo2.setOnPreferenceClickListener(new OnPreferenceClickListener() {
// @Override
// public boolean onPreferenceClick(Preference preference) {
// startActivity(new Intent(mActivity, AidlDemoActivity2.class));
//
// return false;
// }
// });
} }
} }

View File

@ -0,0 +1,584 @@
/*
* Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sufficientlysecure.keychain.demo;
import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.preference.Preference;
import android.preference.Preference.OnPreferenceClickListener;
import android.preference.PreferenceActivity;
import android.provider.MediaStore;
import android.util.Log;
import android.widget.Toast;
import org.sufficientlysecure.keychain.api.OpenKeychainIntents;
import java.io.UnsupportedEncodingException;
public class IntentActivity extends PreferenceActivity {
private static final int SELECT_PHOTO = 100;
/**
* Called when the activity is first created.
*/
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// load preferences from xml
addPreferencesFromResource(R.xml.intent_preference);
// find preferences
Preference encrypt = (Preference) findPreference("ENCRYPT");
Preference encryptUri = (Preference) findPreference("ENCRYPT_URI");
Preference decrypt = (Preference) findPreference("DECRYPT");
Preference import_key = (Preference) findPreference("IMPORT_KEY");
Preference import_key_from_keyserver = (Preference) findPreference("IMPORT_KEY_FROM_KEYSERVER");
Preference import_key_from_qr_code = (Preference) findPreference("IMPORT_KEY_FROM_QR_CODE");
Preference openpgp4fpr = (Preference) findPreference("openpgp4fpr");
// To prevent Android Studio from complaining...
assert encrypt != null;
assert encryptUri != null;
assert decrypt != null;
assert import_key != null;
assert import_key_from_keyserver != null;
assert import_key_from_qr_code != null;
assert openpgp4fpr != null;
encrypt.setOnPreferenceClickListener(new OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
try {
Intent intent = new Intent(OpenKeychainIntents.ENCRYPT);
intent.putExtra(OpenKeychainIntents.ENCRYPT_EXTRA_TEXT, "Hello world!");
startActivity(intent);
} catch (ActivityNotFoundException e) {
Toast.makeText(IntentActivity.this, "Activity not found!", Toast.LENGTH_LONG).show();
}
return false;
}
});
encryptUri.setOnPreferenceClickListener(new OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
Intent photoPickerIntent = new Intent(Intent.ACTION_PICK);
photoPickerIntent.setType("image/*");
startActivityForResult(photoPickerIntent, SELECT_PHOTO);
return false;
}
});
decrypt.setOnPreferenceClickListener(new OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
try {
Intent intent = new Intent(OpenKeychainIntents.DECRYPT);
intent.putExtra(OpenKeychainIntents.DECRYPT_EXTRA_TEXT, TEST_SIGNED_MESSAGE);
startActivity(intent);
} catch (ActivityNotFoundException e) {
Toast.makeText(IntentActivity.this, "Activity not found!", Toast.LENGTH_LONG).show();
}
return false;
}
});
import_key.setOnPreferenceClickListener(new OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
try {
Intent intent = new Intent(OpenKeychainIntents.IMPORT_KEY);
byte[] pubkey = null;
try {
pubkey = TEST_PUBKEY.getBytes("UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
intent.putExtra(OpenKeychainIntents.IMPORT_KEY_EXTRA_KEY_BYTES, pubkey);
startActivity(intent);
} catch (ActivityNotFoundException e) {
Toast.makeText(IntentActivity.this, "Activity not found!", Toast.LENGTH_LONG).show();
}
return false;
}
});
import_key_from_keyserver.setOnPreferenceClickListener(new OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
try {
Intent intent = new Intent(OpenKeychainIntents.IMPORT_KEY_FROM_KEYSERVER);
intent.putExtra(OpenKeychainIntents.IMPORT_KEY_FROM_KEYSERVER_QUERY, "Richard Stallman");
startActivity(intent);
} catch (ActivityNotFoundException e) {
Toast.makeText(IntentActivity.this, "Activity not found!", Toast.LENGTH_LONG).show();
}
return false;
}
});
import_key_from_qr_code.setOnPreferenceClickListener(new OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
try {
Intent intent = new Intent(OpenKeychainIntents.IMPORT_KEY_FROM_QR_CODE);
startActivity(intent);
} catch (ActivityNotFoundException e) {
Toast.makeText(IntentActivity.this, "Activity not found!", Toast.LENGTH_LONG).show();
}
return false;
}
});
openpgp4fpr.setOnPreferenceClickListener(new OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
try {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse("openpgp4fpr:73EE2314F65FA92EC2390D3A718C070100012282"));
startActivity(intent);
} catch (ActivityNotFoundException e) {
Toast.makeText(IntentActivity.this, "Activity not found!", Toast.LENGTH_LONG).show();
}
return false;
}
});
}
protected void onActivityResult(int requestCode, int resultCode, Intent imageReturnedIntent) {
super.onActivityResult(requestCode, resultCode, imageReturnedIntent);
switch (requestCode) {
case SELECT_PHOTO:
if (resultCode == RESULT_OK) {
Uri selectedImage = imageReturnedIntent.getData();
String[] filePathColumn = {MediaStore.Images.Media.DATA};
Cursor cursor = getContentResolver().query(
selectedImage, filePathColumn, null, null, null);
cursor.moveToFirst();
int columnIndex = cursor.getColumnIndex(filePathColumn[0]);
String filePath = cursor.getString(columnIndex);
cursor.close();
// TODO: after fixing DECRYPT, we could use Uri selectedImage directly
Log.d(Constants.TAG, "filePath: " + filePath);
try {
Intent intent = new Intent(OpenKeychainIntents.ENCRYPT);
Uri dataUri = Uri.parse("file://" + filePath);
Log.d(Constants.TAG, "Uri: " + dataUri);
intent.setData(dataUri);
startActivity(intent);
} catch (ActivityNotFoundException e) {
Toast.makeText(IntentActivity.this, "Activity not found!", Toast.LENGTH_LONG).show();
}
}
}
}
private static final String TEST_SIGNED_MESSAGE = "-----BEGIN PGP SIGNED MESSAGE-----\n" +
"Hash: SHA1\n" +
"\n" +
"Hello world!\n" +
"-----BEGIN PGP SIGNATURE-----\n" +
"Version: GnuPG v1.4.12 (GNU/Linux)\n" +
"Comment: Using GnuPG with Thunderbird - http://www.enigmail.net/\n" +
"\n" +
"iQEcBAEBAgAGBQJS/7vTAAoJEHGMBwEAASKCkGYH/2jBLzamVyqd61jrjMQM0jUv\n" +
"MkDcPUxPrYH3wZOO0HcgdBQEo66GZEC2ATmo8izJUMk35Q5jas99k0ac9pXhPUPE\n" +
"5qDXdQS10S07R6J0SeDYFWDSyrSiDTCZpFkVu3JGP/3S0SkMYXPzfYlh8Ciuxu7i\n" +
"FR5dmIiz3VQaBgTBSCBFEomNFM5ypynBJqKIzIty8v0NbV72Rtg6Xg76YqWQ/6MC\n" +
"/MlT3y3++HhfpEmLf5WLEXljbuZ4SfCybgYXG9gBzhJu3+gmBoSicdYTZDHSxBBR\n" +
"BwI+ueLbhgRz+gU+WJFE7xNw35xKtBp1C4PR0iKI8rZCSHLjsRVzor7iwDaR51M=\n" +
"=3Ydc\n" +
"-----END PGP SIGNATURE-----";
private static final String TEST_PUBKEY = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" +
"Version: GnuPG v1.4.12 (GNU/Linux)\n" +
"\n" +
"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\n" +
"k3wUxv46o0y15asU\n" +
"=Bbew\n" +
"-----END PGP PUBLIC KEY BLOCK-----\n";
}

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de> * Copyright (C) 2013-2014 Dominik Schürmann <dominik@dominikschuermann.de>
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -16,181 +16,103 @@
package org.sufficientlysecure.keychain.demo; package org.sufficientlysecure.keychain.demo;
import java.util.ArrayList;
import java.util.List;
import org.openintents.openpgp.IOpenPgpKeyIdsCallback;
import org.openintents.openpgp.OpenPgpData;
import org.openintents.openpgp.OpenPgpError;
import org.openintents.openpgp.OpenPgpServiceConnection;
import org.openintents.openpgp.OpenPgpSignatureResult;
import org.openintents.openpgp.IOpenPgpCallback;
import org.openintents.openpgp.IOpenPgpService;
import android.app.Activity; import android.app.Activity;
import android.app.AlertDialog; import android.app.PendingIntent;
import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.content.pm.ResolveInfo; import android.content.IntentSender;
import android.graphics.drawable.Drawable; import android.content.SharedPreferences;
import android.os.Bundle; import android.os.Bundle;
import android.os.RemoteException; import android.preference.PreferenceManager;
import android.text.TextUtils;
import android.util.Log; import android.util.Log;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.widget.Button;
import android.widget.ArrayAdapter;
import android.widget.EditText; import android.widget.EditText;
import android.widget.ListAdapter;
import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
public class OpenPgpProviderActivity extends Activity { import org.openintents.openpgp.OpenPgpError;
Activity mActivity; import org.openintents.openpgp.util.OpenPgpApi;
import org.openintents.openpgp.util.OpenPgpConstants;
import org.openintents.openpgp.util.OpenPgpServiceConnection;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
public class OpenPgpProviderActivity extends Activity {
EditText mMessage; EditText mMessage;
EditText mCiphertext; EditText mCiphertext;
EditText mEncryptUserIds; EditText mEncryptUserIds;
Button mSign;
Button mEncrypt;
Button mSignAndEncrypt;
Button mDecryptAndVerify;
private OpenPgpServiceConnection mCryptoServiceConnection; OpenPgpServiceConnection mServiceConnection;
public static final int REQUEST_CODE_SIGN = 9910;
public static final int REQUEST_CODE_ENCRYPT = 9911;
public static final int REQUEST_CODE_SIGN_AND_ENCRYPT = 9912;
public static final int REQUEST_CODE_DECRYPT_AND_VERIFY = 9913;
@Override @Override
public void onCreate(Bundle icicle) { public void onCreate(Bundle icicle) {
super.onCreate(icicle); super.onCreate(icicle);
setContentView(R.layout.crypto_provider_demo); setContentView(R.layout.openpgp_provider);
mActivity = this;
mMessage = (EditText) findViewById(R.id.crypto_provider_demo_message); mMessage = (EditText) findViewById(R.id.crypto_provider_demo_message);
mCiphertext = (EditText) findViewById(R.id.crypto_provider_demo_ciphertext); mCiphertext = (EditText) findViewById(R.id.crypto_provider_demo_ciphertext);
mEncryptUserIds = (EditText) findViewById(R.id.crypto_provider_demo_encrypt_user_id); mEncryptUserIds = (EditText) findViewById(R.id.crypto_provider_demo_encrypt_user_id);
mSign = (Button) findViewById(R.id.crypto_provider_demo_sign);
mEncrypt = (Button) findViewById(R.id.crypto_provider_demo_encrypt);
mSignAndEncrypt = (Button) findViewById(R.id.crypto_provider_demo_sign_and_encrypt);
mDecryptAndVerify = (Button) findViewById(R.id.crypto_provider_demo_decrypt_and_verify);
selectCryptoProvider(); mSign.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
sign(new Bundle());
}
});
mEncrypt.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
encrypt(new Bundle());
}
});
mSignAndEncrypt.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
signAndEncrypt(new Bundle());
}
});
mDecryptAndVerify.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
decryptAndVerify(new Bundle());
}
});
SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(this);
String providerPackageName = settings.getString("openpgp_provider_list", "");
if (TextUtils.isEmpty(providerPackageName)) {
Toast.makeText(this, "No OpenPGP Provider selected!", Toast.LENGTH_LONG).show();
finish();
} else {
// bind to service
mServiceConnection = new OpenPgpServiceConnection(
OpenPgpProviderActivity.this, providerPackageName);
mServiceConnection.bindToService();
}
} }
/**
* Callback from remote openpgp service
*/
final IOpenPgpKeyIdsCallback.Stub getKeysEncryptCallback = new IOpenPgpKeyIdsCallback.Stub() {
@Override
public void onSuccess(final long[] keyIds) throws RemoteException {
Log.d(Constants.TAG, "getKeysEncryptCallback keyId " + keyIds[0]);
mActivity.runOnUiThread(new Runnable() {
@Override
public void run() {
// encrypt after getting key ids
String inputStr = mMessage.getText().toString();
OpenPgpData input = new OpenPgpData(inputStr);
Log.d(Constants.TAG, "getKeysEncryptCallback inputStr " + inputStr);
try {
mCryptoServiceConnection.getService().encrypt(input,
new OpenPgpData(OpenPgpData.TYPE_STRING), keyIds, encryptCallback);
} catch (RemoteException e) {
Log.e(Constants.TAG, "CryptoProviderDemo", e);
}
}
});
}
@Override
public void onError(OpenPgpError error) throws RemoteException {
handleError(error);
}
};
final IOpenPgpKeyIdsCallback.Stub getKeysSignAndEncryptCallback = new IOpenPgpKeyIdsCallback.Stub() {
@Override
public void onSuccess(final long[] keyIds) throws RemoteException {
Log.d(Constants.TAG, "getKeysSignAndEncryptCallback keyId " + keyIds[0]);
mActivity.runOnUiThread(new Runnable() {
@Override
public void run() {
// encrypt after getting key ids
String inputStr = mMessage.getText().toString();
OpenPgpData input = new OpenPgpData(inputStr);
try {
mCryptoServiceConnection.getService().signAndEncrypt(input,
new OpenPgpData(OpenPgpData.TYPE_STRING), keyIds, encryptCallback);
} catch (RemoteException e) {
Log.e(Constants.TAG, "CryptoProviderDemo", e);
}
}
});
}
@Override
public void onError(OpenPgpError error) throws RemoteException {
handleError(error);
}
};
final IOpenPgpCallback.Stub encryptCallback = new IOpenPgpCallback.Stub() {
@Override
public void onSuccess(final OpenPgpData output, OpenPgpSignatureResult signatureResult)
throws RemoteException {
Log.d(Constants.TAG, "encryptCallback");
runOnUiThread(new Runnable() {
@Override
public void run() {
mCiphertext.setText(output.getString());
}
});
}
@Override
public void onError(OpenPgpError error) throws RemoteException {
handleError(error);
}
};
final IOpenPgpCallback.Stub decryptAndVerifyCallback = new IOpenPgpCallback.Stub() {
@Override
public void onSuccess(final OpenPgpData output, final OpenPgpSignatureResult signatureResult)
throws RemoteException {
Log.d(Constants.TAG, "decryptAndVerifyCallback");
runOnUiThread(new Runnable() {
@Override
public void run() {
mMessage.setText(output.getString());
if (signatureResult != null) {
Toast.makeText(OpenPgpProviderActivity.this,
"signature result:\n" + signatureResult.toString(),
Toast.LENGTH_LONG).show();
}
}
});
}
@Override
public void onError(OpenPgpError error) throws RemoteException {
handleError(error);
}
};
private void handleError(final OpenPgpError error) { private void handleError(final OpenPgpError error) {
mActivity.runOnUiThread(new Runnable() { runOnUiThread(new Runnable() {
@Override @Override
public void run() { public void run() {
Toast.makeText(mActivity, Toast.makeText(OpenPgpProviderActivity.this,
"onError id:" + error.getErrorId() + "\n\n" + error.getMessage(), "onError id:" + error.getErrorId() + "\n\n" + error.getMessage(),
Toast.LENGTH_LONG).show(); Toast.LENGTH_LONG).show();
Log.e(Constants.TAG, "onError getErrorId:" + error.getErrorId()); Log.e(Constants.TAG, "onError getErrorId:" + error.getErrorId());
@ -199,46 +121,147 @@ public class OpenPgpProviderActivity extends Activity {
}); });
} }
public void encryptOnClick(View view) { /**
* Takes input from message or ciphertext EditText and turns it into a ByteArrayInputStream
*
* @param ciphertext
* @return
*/
private InputStream getInputstream(boolean ciphertext) {
InputStream is = null;
try { try {
mCryptoServiceConnection.getService().getKeyIds( String inputStr = null;
mEncryptUserIds.getText().toString().split(","), true, getKeysEncryptCallback); if (ciphertext) {
} catch (RemoteException e) { inputStr = mCiphertext.getText().toString();
Log.e(Constants.TAG, "CryptoProviderDemo", e); } else {
inputStr = mMessage.getText().toString();
}
is = new ByteArrayInputStream(inputStr.getBytes("UTF-8"));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return is;
}
private class MyCallback implements OpenPgpApi.IOpenPgpCallback {
boolean returnToCiphertextField;
ByteArrayOutputStream os;
int requestCode;
private MyCallback(boolean returnToCiphertextField, ByteArrayOutputStream os, int requestCode) {
this.returnToCiphertextField = returnToCiphertextField;
this.os = os;
this.requestCode = requestCode;
}
@Override
public void onReturn(Bundle result) {
switch (result.getInt(OpenPgpConstants.RESULT_CODE)) {
case OpenPgpConstants.RESULT_CODE_SUCCESS: {
try {
Log.d(OpenPgpConstants.TAG, "result: " + os.toByteArray().length
+ " str=" + os.toString("UTF-8"));
if (returnToCiphertextField) {
mCiphertext.setText(os.toString("UTF-8"));
} else {
mMessage.setText(os.toString("UTF-8"));
}
} catch (UnsupportedEncodingException e) {
Log.e(Constants.TAG, "UnsupportedEncodingException", e);
}
break;
}
case OpenPgpConstants.RESULT_CODE_USER_INTERACTION_REQUIRED: {
PendingIntent pi = result.getParcelable(OpenPgpConstants.RESULT_INTENT);
try {
OpenPgpProviderActivity.this.startIntentSenderForResult(pi.getIntentSender(),
requestCode, null, 0, 0, 0);
} catch (IntentSender.SendIntentException e) {
Log.e(Constants.TAG, "SendIntentException", e);
}
break;
}
case OpenPgpConstants.RESULT_CODE_ERROR: {
OpenPgpError error = result.getParcelable(OpenPgpConstants.RESULT_ERRORS);
handleError(error);
break;
}
}
} }
} }
public void signOnClick(View view) { public void sign(Bundle params) {
String inputStr = mMessage.getText().toString(); params.putBoolean(OpenPgpConstants.PARAMS_REQUEST_ASCII_ARMOR, true);
OpenPgpData input = new OpenPgpData(inputStr);
try { InputStream is = getInputstream(false);
mCryptoServiceConnection.getService().sign(input, final ByteArrayOutputStream os = new ByteArrayOutputStream();
new OpenPgpData(OpenPgpData.TYPE_STRING), encryptCallback);
} catch (RemoteException e) { OpenPgpApi api = new OpenPgpApi(this, mServiceConnection.getService());
Log.e(Constants.TAG, "CryptoProviderDemo", e); api.sign(params, is, os, new MyCallback(true, os, REQUEST_CODE_SIGN));
}
} }
public void signAndEncryptOnClick(View view) { public void encrypt(Bundle params) {
try { params.putStringArray(OpenPgpConstants.PARAMS_USER_IDS, mEncryptUserIds.getText().toString().split(","));
mCryptoServiceConnection.getService().getKeyIds( params.putBoolean(OpenPgpConstants.PARAMS_REQUEST_ASCII_ARMOR, true);
mEncryptUserIds.getText().toString().split(","), true,
getKeysSignAndEncryptCallback); InputStream is = getInputstream(false);
} catch (RemoteException e) { final ByteArrayOutputStream os = new ByteArrayOutputStream();
Log.e(Constants.TAG, "CryptoProviderDemo", e);
} OpenPgpApi api = new OpenPgpApi(this, mServiceConnection.getService());
api.encrypt(params, is, os, new MyCallback(true, os, REQUEST_CODE_ENCRYPT));
} }
public void decryptAndVerifyOnClick(View view) { public void signAndEncrypt(Bundle params) {
String inputStr = mCiphertext.getText().toString(); params.putStringArray(OpenPgpConstants.PARAMS_USER_IDS, mEncryptUserIds.getText().toString().split(","));
OpenPgpData input = new OpenPgpData(inputStr); params.putBoolean(OpenPgpConstants.PARAMS_REQUEST_ASCII_ARMOR, true);
try { InputStream is = getInputstream(false);
mCryptoServiceConnection.getService().decryptAndVerify(input, final ByteArrayOutputStream os = new ByteArrayOutputStream();
new OpenPgpData(OpenPgpData.TYPE_STRING), decryptAndVerifyCallback);
} catch (RemoteException e) { OpenPgpApi api = new OpenPgpApi(this, mServiceConnection.getService());
Log.e(Constants.TAG, "CryptoProviderDemo", e); api.signAndEncrypt(params, is, os, new MyCallback(true, os, REQUEST_CODE_SIGN_AND_ENCRYPT));
}
public void decryptAndVerify(Bundle params) {
params.putBoolean(OpenPgpConstants.PARAMS_REQUEST_ASCII_ARMOR, true);
InputStream is = getInputstream(true);
final ByteArrayOutputStream os = new ByteArrayOutputStream();
OpenPgpApi api = new OpenPgpApi(this, mServiceConnection.getService());
api.decryptAndVerify(params, is, os, new MyCallback(false, os, REQUEST_CODE_DECRYPT_AND_VERIFY));
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
Log.d(Constants.TAG, "onActivityResult resultCode: " + resultCode);
// try again after user interaction
if (resultCode == RESULT_OK) {
Bundle params = data.getBundleExtra(OpenPgpConstants.PI_RESULT_PARAMS);
switch (requestCode) {
case REQUEST_CODE_SIGN: {
sign(params);
break;
}
case REQUEST_CODE_ENCRYPT: {
encrypt(params);
break;
}
case REQUEST_CODE_SIGN_AND_ENCRYPT: {
signAndEncrypt(params);
break;
}
case REQUEST_CODE_DECRYPT_AND_VERIFY: {
decryptAndVerify(params);
break;
}
}
} }
} }
@ -246,107 +269,9 @@ public class OpenPgpProviderActivity extends Activity {
public void onDestroy() { public void onDestroy() {
super.onDestroy(); super.onDestroy();
if (mCryptoServiceConnection != null) { if (mServiceConnection != null) {
mCryptoServiceConnection.unbindFromService(); mServiceConnection.unbindFromService();
} }
} }
private static class OpenPgpProviderElement {
private String packageName;
private String simpleName;
private Drawable icon;
public OpenPgpProviderElement(String packageName, String simpleName, Drawable icon) {
this.packageName = packageName;
this.simpleName = simpleName;
this.icon = icon;
}
@Override
public String toString() {
return simpleName;
}
}
private void selectCryptoProvider() {
Intent intent = new Intent(IOpenPgpService.class.getName());
final ArrayList<OpenPgpProviderElement> providerList = new ArrayList<OpenPgpProviderElement>();
List<ResolveInfo> resInfo = getPackageManager().queryIntentServices(intent, 0);
if (!resInfo.isEmpty()) {
for (ResolveInfo resolveInfo : resInfo) {
if (resolveInfo.serviceInfo == null)
continue;
String packageName = resolveInfo.serviceInfo.packageName;
String simpleName = String.valueOf(resolveInfo.serviceInfo
.loadLabel(getPackageManager()));
Drawable icon = resolveInfo.serviceInfo.loadIcon(getPackageManager());
providerList.add(new OpenPgpProviderElement(packageName, simpleName, icon));
}
}
AlertDialog.Builder alert = new AlertDialog.Builder(this);
alert.setTitle("Select OpenPGP Provider!");
alert.setCancelable(false);
if (!providerList.isEmpty()) {
// add "disable OpenPGP provider"
providerList.add(0, new OpenPgpProviderElement(null, "Disable OpenPGP Provider",
getResources().getDrawable(android.R.drawable.ic_menu_close_clear_cancel)));
// Init ArrayAdapter with OpenPGP Providers
ListAdapter adapter = new ArrayAdapter<OpenPgpProviderElement>(this,
android.R.layout.select_dialog_item, android.R.id.text1, providerList) {
public View getView(int position, View convertView, ViewGroup parent) {
// User super class to create the View
View v = super.getView(position, convertView, parent);
TextView tv = (TextView) v.findViewById(android.R.id.text1);
// Put the image on the TextView
tv.setCompoundDrawablesWithIntrinsicBounds(providerList.get(position).icon,
null, null, null);
// Add margin between image and text (support various screen densities)
int dp5 = (int) (5 * getResources().getDisplayMetrics().density + 0.5f);
tv.setCompoundDrawablePadding(dp5);
return v;
}
};
alert.setSingleChoiceItems(adapter, -1, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int position) {
String packageName = providerList.get(position).packageName;
if (packageName == null) {
dialog.cancel();
finish();
}
// bind to service
mCryptoServiceConnection = new OpenPgpServiceConnection(
OpenPgpProviderActivity.this, packageName);
mCryptoServiceConnection.bindToService();
dialog.dismiss();
}
});
} else {
alert.setMessage("No OpenPGP Provider installed!");
}
alert.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
dialog.cancel();
finish();
}
});
AlertDialog ad = alert.create();
ad.show();
}
} }

View File

@ -1,40 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content" >
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" >
<Button
android:id="@+id/aidl_demo_select_encryption_key"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="selectEncryptionKeysOnClick"
android:text="Select encryption key(s)" />
<EditText
android:id="@+id/aidl_demo_keyrings"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="keyrings output"
android:textAppearance="@android:style/TextAppearance.Small" />
<Button
android:id="@+id/aidl_demo_get_keyrings_strings"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="getKeyringsStringsOnClick"
android:text="getKeyrings as Strings" />
<Button
android:id="@+id/aidl_demo_get_keyrings_bytes"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="getKeyringsBytesOnClick"
android:text="getKeyringsBytes" />
</LinearLayout>
</ScrollView>

View File

@ -1,83 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Encrypt UserIds (split with &apos;,&apos;)"
android:textAppearance="?android:attr/textAppearanceMedium" />
<EditText
android:id="@+id/crypto_provider_demo_encrypt_user_id"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="dominik@dominikschuermann.de"
android:textAppearance="@android:style/TextAppearance.Small" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Message"
android:textAppearance="?android:attr/textAppearanceMedium" />
<EditText
android:id="@+id/crypto_provider_demo_message"
android:layout_width="match_parent"
android:layout_height="100dip"
android:scrollHorizontally="true"
android:scrollbars="vertical"
android:text="message"
android:textAppearance="@android:style/TextAppearance.Small" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Ciphertext"
android:textAppearance="?android:attr/textAppearanceMedium" />
<EditText
android:id="@+id/crypto_provider_demo_ciphertext"
android:layout_width="match_parent"
android:layout_height="100dip"
android:text="ciphertext"
android:textAppearance="@android:style/TextAppearance.Small" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content" >
<Button
android:id="@+id/crypto_provider_demo_encrypt"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:onClick="encryptOnClick"
android:text="Encrypt" />
<Button
android:id="@+id/crypto_provider_demo_sign"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:onClick="signOnClick"
android:text="Sign" />
<Button
android:id="@+id/crypto_provider_demo_encrypt_and_sign"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="signAndEncryptOnClick"
android:text="Sign and Encrypt" />
</LinearLayout>
<Button
android:id="@+id/crypto_provider_demo_decrypt_and_verify"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="decryptAndVerifyOnClick"
android:text="Decrypt and Verify" />
</LinearLayout>

View File

@ -12,7 +12,7 @@
android:id="@+id/Button02" android:id="@+id/Button02"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:onClick="encryptOnClick" android:onClick="encrypt"
android:text="Encrypt" /> android:text="Encrypt" />
<Button <Button
@ -54,7 +54,7 @@
android:id="@+id/intent_demo_encrypt" android:id="@+id/intent_demo_encrypt"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:onClick="encryptOnClick" android:onClick="encrypt"
android:text="Encrypt" /> android:text="Encrypt" />
<Button <Button

View File

@ -0,0 +1,108 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/parent_scroll"
android:fillViewport="true"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:padding="8dp"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Encrypt UserIds (split with &apos;,&apos;)"
android:textAppearance="?android:attr/textAppearanceMedium" />
<EditText
android:id="@+id/crypto_provider_demo_encrypt_user_id"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="dominik@dominikschuermann.de"
android:textAppearance="@android:style/TextAppearance.Small" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Message"
android:textAppearance="?android:attr/textAppearanceMedium" />
<ScrollView
android:id="@+id/child_scroll1"
android:fillViewport="true"
android:layout_width="match_parent"
android:layout_height="120dp">
<EditText
android:id="@+id/crypto_provider_demo_message"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollHorizontally="true"
android:scrollbars="vertical"
android:text="message"
android:textAppearance="@android:style/TextAppearance.Small" />
</ScrollView>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Ciphertext"
android:textAppearance="?android:attr/textAppearanceMedium" />
<ScrollView
android:id="@+id/child_scroll2"
android:fillViewport="true"
android:layout_width="match_parent"
android:layout_height="120dp">
<EditText
android:id="@+id/crypto_provider_demo_ciphertext"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="ciphertext"
android:textAppearance="@android:style/TextAppearance.Small" />
</ScrollView>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<Button
android:id="@+id/crypto_provider_demo_sign"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Sign"
android:layout_gravity="center_vertical" />
<Button
android:id="@+id/crypto_provider_demo_encrypt"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Encrypt"
android:layout_gravity="center_vertical" />
<Button
android:id="@+id/crypto_provider_demo_sign_and_encrypt"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Sign and Encrypt"
android:layout_gravity="center_vertical" />
</LinearLayout>
<Button
android:id="@+id/crypto_provider_demo_decrypt_and_verify"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Decrypt and Verify" />
</LinearLayout>
</ScrollView>

View File

@ -1,23 +1,18 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" > <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceCategory android:title="Intent" > <PreferenceCategory android:title="OpenKeychain Intents">
<Preference <Preference
android:key="intent_demo" android:key="intent_demo"
android:title="Intent Demo" /> android:title="Intent Demo" />
</PreferenceCategory> </PreferenceCategory>
<!-- <PreferenceCategory android:title="AIDL" > --> <PreferenceCategory android:title="OpenPGP Provider API">
<!-- <Preference --> <org.openintents.openpgp.util.OpenPgpListPreference
<!-- android:key="aidl_demo2" -->
<!-- android:title="AIDL Demo (ACCESS_KEYS permission)" /> -->
<!-- </PreferenceCategory> -->
<PreferenceCategory android:title="OpenPGP Provider" >
<org.openintents.openpgp.OpenPgpListPreference
android:key="openpgp_provider_list" android:key="openpgp_provider_list"
android:title="Select OpenPGP Provider!" /> android:title="Select OpenPGP Provider!" />
<Preference <Preference
android:key="openpgp_provider_demo" android:key="openpgp_provider_demo"
android:title="OpenPGP Provider" /> android:title="OpenPGP Provider Demo" />
</PreferenceCategory> </PreferenceCategory>
</PreferenceScreen> </PreferenceScreen>

View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceCategory android:title="Intents (org.sufficientlysecure.keychain.action.)">
<Preference
android:key="ENCRYPT"
android:title="ENCRYPT" />
<Preference
android:key="ENCRYPT_URI"
android:title="ENCRYPT with Uri" />
<Preference
android:key="DECRYPT"
android:title="DECRYPT" />
<Preference
android:key="IMPORT_KEY"
android:title="IMPORT_KEY" />
<Preference
android:key="IMPORT_KEY_FROM_KEYSERVER"
android:title="IMPORT_KEY_FROM_KEYSERVER" />
<Preference
android:key="IMPORT_KEY_FROM_QR_CODE"
android:title="IMPORT_KEY_FROM_QR_CODE" />
</PreferenceCategory>
<PreferenceCategory android:title="Special Intents">
<Preference
android:key="openpgp4fpr"
android:title="VIEW openpgp4fpr:73EE2314F65FA92EC2390D3A718C070100012282" />
</PreferenceCategory>
</PreferenceScreen>

View File

@ -1,6 +1,6 @@
#Sun Feb 09 19:10:32 CET 2014 #Fri Feb 14 01:26:40 CET 2014
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=http\://services.gradle.org/distributions/gradle-1.10-bin.zip distributionUrl=http\://services.gradle.org/distributions/gradle-1.10-all.zip

View File

@ -5,7 +5,7 @@
android:versionName="1.0" > android:versionName="1.0" >
<uses-sdk <uses-sdk
android:minSdkVersion="8" android:minSdkVersion="9"
android:targetSdkVersion="19" /> android:targetSdkVersion="19" />
<application/> <application/>

View File

@ -1,10 +1,11 @@
// please leave this here, so this library builds on its own
buildscript { buildscript {
repositories { repositories {
mavenCentral() mavenCentral()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:0.8.0' classpath 'com.android.tools.build:gradle:0.8.3'
} }
} }
@ -14,6 +15,19 @@ android {
compileSdkVersion 19 compileSdkVersion 19
buildToolsVersion '19.0.1' buildToolsVersion '19.0.1'
// NOTE: We are using the old folder structure to also support Eclipse
sourceSets {
main {
manifest.srcFile 'AndroidManifest.xml'
java.srcDirs = ['src']
resources.srcDirs = ['src']
aidl.srcDirs = ['src']
renderscript.srcDirs = ['src']
res.srcDirs = ['res']
assets.srcDirs = ['assets']
}
}
// Do not abort build if lint finds errors // Do not abort build if lint finds errors
lintOptions { lintOptions {
abortOnError false abortOnError false

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="openpgp_list_preference_none">None</string>
</resources>

View File

@ -1,45 +0,0 @@
/*
* Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openintents.openpgp;
import org.openintents.openpgp.OpenPgpData;
import org.openintents.openpgp.OpenPgpSignatureResult;
import org.openintents.openpgp.OpenPgpError;
interface IOpenPgpCallback {
/**
* onSuccess returns on successful OpenPGP operations.
*
* @param output
* contains resulting output (decrypted content (when input was encrypted)
* or content without signature (when input was signed-only))
* @param signatureResult
* signatureResult is only non-null if decryptAndVerify() was called and the content
* was encrypted or signed-and-encrypted.
*/
oneway void onSuccess(in OpenPgpData output, in OpenPgpSignatureResult signatureResult);
/**
* onError returns on errors or when allowUserInteraction was set to false, but user interaction
* was required execute an OpenPGP operation.
*
* @param error
* See OpenPgpError class for more information.
*/
oneway void onError(in OpenPgpError error);
}

View File

@ -1,39 +0,0 @@
/*
* Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openintents.openpgp;
import org.openintents.openpgp.OpenPgpError;
interface IOpenPgpKeyIdsCallback {
/**
* onSuccess returns on successful getKeyIds operations.
*
* @param keyIds
* returned key ids
*/
oneway void onSuccess(in long[] keyIds);
/**
* onError returns on errors or when allowUserInteraction was set to false, but user interaction
* was required execute an OpenPGP operation.
*
* @param error
* See OpenPgpError class for more information.
*/
oneway void onError(in OpenPgpError error);
}

View File

@ -1,143 +0,0 @@
/*
* Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openintents.openpgp;
import org.openintents.openpgp.OpenPgpData;
import org.openintents.openpgp.IOpenPgpCallback;
import org.openintents.openpgp.IOpenPgpKeyIdsCallback;
/**
* All methods are oneway, which means they are asynchronous and non-blocking.
* Results are returned to the callback, which has to be implemented on client side.
*/
interface IOpenPgpService {
/**
* Sign
*
* After successful signing, callback's onSuccess will contain the resulting output.
*
* @param input
* OpenPgpData object containing String, byte[], ParcelFileDescriptor, or Uri
* @param output
* Request output format by defining OpenPgpData object
*
* new OpenPgpData(OpenPgpData.TYPE_STRING)
* Returns as String
* (OpenPGP Radix-64, 33 percent overhead compared to binary, see http://tools.ietf.org/html/rfc4880#page-53)
* new OpenPgpData(OpenPgpData.TYPE_BYTE_ARRAY)
* Returns as byte[]
* new OpenPgpData(uri)
* Writes output to given Uri
* new OpenPgpData(fileDescriptor)
* Writes output to given ParcelFileDescriptor
* @param callback
* Callback where to return results
*/
oneway void sign(in OpenPgpData input, in OpenPgpData output, in IOpenPgpCallback callback);
/**
* Encrypt
*
* After successful encryption, callback's onSuccess will contain the resulting output.
*
* @param input
* OpenPgpData object containing String, byte[], ParcelFileDescriptor, or Uri
* @param output
* Request output format by defining OpenPgpData object
*
* new OpenPgpData(OpenPgpData.TYPE_STRING)
* Returns as String
* (OpenPGP Radix-64, 33 percent overhead compared to binary, see http://tools.ietf.org/html/rfc4880#page-53)
* new OpenPgpData(OpenPgpData.TYPE_BYTE_ARRAY)
* Returns as byte[]
* new OpenPgpData(uri)
* Writes output to given Uri
* new OpenPgpData(fileDescriptor)
* Writes output to given ParcelFileDescriptor
* @param keyIds
* Key Ids of recipients. Can be retrieved with getKeyIds()
* @param callback
* Callback where to return results
*/
oneway void encrypt(in OpenPgpData input, in OpenPgpData output, in long[] keyIds, in IOpenPgpCallback callback);
/**
* Sign then encrypt
*
* After successful signing and encryption, callback's onSuccess will contain the resulting output.
*
* @param input
* OpenPgpData object containing String, byte[], ParcelFileDescriptor, or Uri
* @param output
* Request output format by defining OpenPgpData object
*
* new OpenPgpData(OpenPgpData.TYPE_STRING)
* Returns as String
* (OpenPGP Radix-64, 33 percent overhead compared to binary, see http://tools.ietf.org/html/rfc4880#page-53)
* new OpenPgpData(OpenPgpData.TYPE_BYTE_ARRAY)
* Returns as byte[]
* new OpenPgpData(uri)
* Writes output to given Uri
* new OpenPgpData(fileDescriptor)
* Writes output to given ParcelFileDescriptor
* @param keyIds
* Key Ids of recipients. Can be retrieved with getKeyIds()
* @param callback
* Callback where to return results
*/
oneway void signAndEncrypt(in OpenPgpData input, in OpenPgpData output, in long[] keyIds, in IOpenPgpCallback callback);
/**
* Decrypts and verifies given input bytes. This methods handles encrypted-only, signed-and-encrypted,
* and also signed-only input.
*
* After successful decryption/verification, callback's onSuccess will contain the resulting output.
* The signatureResult in onSuccess is only non-null if signed-and-encrypted or signed-only inputBytes were given.
*
* @param input
* OpenPgpData object containing String, byte[], ParcelFileDescriptor, or Uri
* @param output
* Request output format by defining OpenPgpData object
*
* new OpenPgpData(OpenPgpData.TYPE_STRING)
* Returns as String
* (OpenPGP Radix-64, 33 percent overhead compared to binary, see http://tools.ietf.org/html/rfc4880#page-53)
* new OpenPgpData(OpenPgpData.TYPE_BYTE_ARRAY)
* Returns as byte[]
* new OpenPgpData(uri)
* Writes output to given Uri
* new OpenPgpData(fileDescriptor)
* Writes output to given ParcelFileDescriptor
* @param callback
* Callback where to return results
*/
oneway void decryptAndVerify(in OpenPgpData input, in OpenPgpData output, in IOpenPgpCallback callback);
/**
* Get available key ids based on given user ids
*
* @param ids
* User Ids (emails) of recipients OR key ids
* @param allowUserInteraction
* Enable user interaction to lookup and import unknown keys
* @param callback
* Callback where to return results (different type than callback in other functions!)
*/
oneway void getKeyIds(in String[] ids, in boolean allowUserInteraction, in IOpenPgpKeyIdsCallback callback);
}

View File

@ -1,20 +0,0 @@
/*
* Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openintents.openpgp;
// Declare OpenPgpData so AIDL can find it and knows that it implements the parcelable protocol.
parcelable OpenPgpData;

View File

@ -1,20 +0,0 @@
/*
* Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openintents.openpgp;
// Declare OpenPgpError so AIDL can find it and knows that it implements the parcelable protocol.
parcelable OpenPgpError;

View File

@ -1,20 +0,0 @@
/*
* Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openintents.openpgp;
// Declare OpenPgpSignatureResult so AIDL can find it and knows that it implements the parcelable protocol.
parcelable OpenPgpSignatureResult;

View File

@ -1,10 +0,0 @@
package org.openintents.openpgp;
public class OpenPgpConstants {
public static final String TAG = "OpenPgp API";
public static final int REQUIRED_API_VERSION = 1;
public static final String SERVICE_INTENT = "org.openintents.openpgp.IOpenPgpService";
}

View File

@ -1,127 +0,0 @@
/*
* Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openintents.openpgp;
import android.net.Uri;
import android.os.Parcel;
import android.os.ParcelFileDescriptor;
import android.os.Parcelable;
public class OpenPgpData implements Parcelable {
public static final int TYPE_STRING = 0;
public static final int TYPE_BYTE_ARRAY = 1;
public static final int TYPE_FILE_DESCRIPTOR = 2;
public static final int TYPE_URI = 3;
int type;
String string;
byte[] bytes = new byte[0];
ParcelFileDescriptor fileDescriptor;
Uri uri;
public int getType() {
return type;
}
public String getString() {
return string;
}
public byte[] getBytes() {
return bytes;
}
public ParcelFileDescriptor getFileDescriptor() {
return fileDescriptor;
}
public Uri getUri() {
return uri;
}
public OpenPgpData() {
}
/**
* Not a real constructor. This can be used to define requested output type.
*
* @param type
*/
public OpenPgpData(int type) {
this.type = type;
}
public OpenPgpData(String string) {
this.string = string;
this.type = TYPE_STRING;
}
public OpenPgpData(byte[] bytes) {
this.bytes = bytes;
this.type = TYPE_BYTE_ARRAY;
}
public OpenPgpData(ParcelFileDescriptor fileDescriptor) {
this.fileDescriptor = fileDescriptor;
this.type = TYPE_FILE_DESCRIPTOR;
}
public OpenPgpData(Uri uri) {
this.uri = uri;
this.type = TYPE_URI;
}
public OpenPgpData(OpenPgpData b) {
this.string = b.string;
this.bytes = b.bytes;
this.fileDescriptor = b.fileDescriptor;
this.uri = b.uri;
}
public int describeContents() {
return 0;
}
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(type);
dest.writeString(string);
dest.writeInt(bytes.length);
dest.writeByteArray(bytes);
dest.writeParcelable(fileDescriptor, 0);
dest.writeParcelable(uri, 0);
}
public static final Creator<OpenPgpData> CREATOR = new Creator<OpenPgpData>() {
public OpenPgpData createFromParcel(final Parcel source) {
OpenPgpData vr = new OpenPgpData();
vr.type = source.readInt();
vr.string = source.readString();
vr.bytes = new byte[source.readInt()];
source.readByteArray(vr.bytes);
vr.fileDescriptor = source.readParcelable(ParcelFileDescriptor.class.getClassLoader());
vr.fileDescriptor = source.readParcelable(Uri.class.getClassLoader());
return vr;
}
public OpenPgpData[] newArray(final int size) {
return new OpenPgpData[size];
}
};
}

View File

@ -1,52 +0,0 @@
/*
* Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openintents.openpgp;
import java.util.List;
import java.util.regex.Pattern;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ResolveInfo;
public class OpenPgpHelper {
private Context context;
public static Pattern PGP_MESSAGE = Pattern.compile(
".*?(-----BEGIN PGP MESSAGE-----.*?-----END PGP MESSAGE-----).*", Pattern.DOTALL);
public static Pattern PGP_SIGNED_MESSAGE = Pattern
.compile(
".*?(-----BEGIN PGP SIGNED MESSAGE-----.*?-----BEGIN PGP SIGNATURE-----.*?-----END PGP SIGNATURE-----).*",
Pattern.DOTALL);
public OpenPgpHelper(Context context) {
super();
this.context = context;
}
public boolean isAvailable() {
Intent intent = new Intent(OpenPgpConstants.SERVICE_INTENT);
List<ResolveInfo> resInfo = context.getPackageManager().queryIntentServices(intent, 0);
if (!resInfo.isEmpty()) {
return true;
} else {
return false;
}
}
}

View File

@ -0,0 +1,85 @@
/*
* Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openintents.openpgp;
interface IOpenPgpService {
/**
* General extras
* --------------
*
* Bundle params:
* int api_version (required)
* boolean ascii_armor (request ascii armor for ouput)
*
* returned Bundle:
* int result_code (0, 1, or 2 (see OpenPgpConstants))
* OpenPgpError error (if result_code == 0)
* Intent intent (if result_code == 2)
*
*/
/**
* Sign only
*
* optional params:
* String passphrase (for key passphrase)
*/
Bundle sign(in Bundle params, in ParcelFileDescriptor input, in ParcelFileDescriptor output);
/**
* Encrypt
*
* Bundle params:
* long[] key_ids
* or
* String[] user_ids (= emails of recipients) (if more than one key has this user_id, a PendingIntent is returned)
*
* optional params:
* String passphrase (for key passphrase)
*/
Bundle encrypt(in Bundle params, in ParcelFileDescriptor input, in ParcelFileDescriptor output);
/**
* Sign and encrypt
*
* Bundle params:
* same as in encrypt()
*/
Bundle signAndEncrypt(in Bundle params, in ParcelFileDescriptor input, in ParcelFileDescriptor output);
/**
* Decrypts and verifies given input bytes. This methods handles encrypted-only, signed-and-encrypted,
* and also signed-only input.
*
* returned Bundle:
* OpenPgpSignatureResult signature_result
*/
Bundle decryptAndVerify(in Bundle params, in ParcelFileDescriptor input, in ParcelFileDescriptor output);
/**
* Retrieves key ids based on given user ids (=emails)
*
* Bundle params:
* String[] user_ids
*
* returned Bundle:
* long[] key_ids
*/
Bundle getKeyIds(in Bundle params);
}

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de> * Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -20,10 +20,13 @@ import android.os.Parcel;
import android.os.Parcelable; import android.os.Parcelable;
public class OpenPgpError implements Parcelable { public class OpenPgpError implements Parcelable {
public static final int CLIENT_SIDE_ERROR = -1;
public static final int GENERIC_ERROR = 0; public static final int GENERIC_ERROR = 0;
public static final int NO_OR_WRONG_PASSPHRASE = 1; public static final int INCOMPATIBLE_API_VERSIONS = 1;
public static final int NO_USER_IDS = 2;
public static final int USER_INTERACTION_REQUIRED = 3; public static final int NO_OR_WRONG_PASSPHRASE = 2;
public static final int NO_USER_IDS = 3;
int errorId; int errorId;
String message; String message;

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de> * Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -22,14 +22,14 @@ import android.os.Parcelable;
public class OpenPgpSignatureResult implements Parcelable { public class OpenPgpSignatureResult implements Parcelable {
// generic error on signature verification // generic error on signature verification
public static final int SIGNATURE_ERROR = 0; public static final int SIGNATURE_ERROR = 0;
// successfully verified signature, with trusted public key // successfully verified signature, with certified public key
public static final int SIGNATURE_SUCCESS_TRUSTED = 1; public static final int SIGNATURE_SUCCESS_CERTIFIED = 1;
// no public key was found for this signature verification // no public key was found for this signature verification
// you can retrieve the key with // you can retrieve the key with
// getKeys(new String[] {String.valueOf(signatureResult.getKeyId)}, true, callback) // getKeys(new String[] {String.valueOf(signatureResult.getKeyId)}, true, callback)
public static final int SIGNATURE_UNKNOWN_PUB_KEY = 2; public static final int SIGNATURE_UNKNOWN_PUB_KEY = 2;
// successfully verified signature, but with untrusted public key // successfully verified signature, but with certified public key
public static final int SIGNATURE_SUCCESS_UNTRUSTED = 3; public static final int SIGNATURE_SUCCESS_UNCERTIFIED = 3;
int status; int status;
boolean signatureOnly; boolean signatureOnly;

View File

@ -0,0 +1,198 @@
/*
* Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openintents.openpgp.util;
import android.content.Context;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.ParcelFileDescriptor;
import android.util.Log;
import org.openintents.openpgp.IOpenPgpService;
import org.openintents.openpgp.OpenPgpError;
import java.io.InputStream;
import java.io.OutputStream;
public class OpenPgpApi {
IOpenPgpService mService;
Context mContext;
private static final int OPERATION_SIGN = 0;
private static final int OPERATION_ENCRYPT = 1;
private static final int OPERATION_SIGN_ENCRYPT = 2;
private static final int OPERATION_DECRYPT_VERIFY = 3;
private static final int OPERATION_GET_KEY_IDS = 4;
public OpenPgpApi(Context context, IOpenPgpService service) {
this.mContext = context;
this.mService = service;
}
public Bundle sign(InputStream is, final OutputStream os) {
return executeApi(OPERATION_SIGN, new Bundle(), is, os);
}
public Bundle sign(Bundle params, InputStream is, final OutputStream os) {
return executeApi(OPERATION_SIGN, params, is, os);
}
public void sign(Bundle params, InputStream is, final OutputStream os, IOpenPgpCallback callback) {
executeApiAsync(OPERATION_SIGN, params, is, os, callback);
}
public Bundle encrypt(InputStream is, final OutputStream os) {
return executeApi(OPERATION_ENCRYPT, new Bundle(), is, os);
}
public Bundle encrypt(Bundle params, InputStream is, final OutputStream os) {
return executeApi(OPERATION_ENCRYPT, params, is, os);
}
public void encrypt(Bundle params, InputStream is, final OutputStream os, IOpenPgpCallback callback) {
executeApiAsync(OPERATION_ENCRYPT, params, is, os, callback);
}
public Bundle signAndEncrypt(InputStream is, final OutputStream os) {
return executeApi(OPERATION_SIGN_ENCRYPT, new Bundle(), is, os);
}
public Bundle signAndEncrypt(Bundle params, InputStream is, final OutputStream os) {
return executeApi(OPERATION_SIGN_ENCRYPT, params, is, os);
}
public void signAndEncrypt(Bundle params, InputStream is, final OutputStream os, IOpenPgpCallback callback) {
executeApiAsync(OPERATION_SIGN_ENCRYPT, params, is, os, callback);
}
public Bundle decryptAndVerify(InputStream is, final OutputStream os) {
return executeApi(OPERATION_DECRYPT_VERIFY, new Bundle(), is, os);
}
public Bundle decryptAndVerify(Bundle params, InputStream is, final OutputStream os) {
return executeApi(OPERATION_DECRYPT_VERIFY, params, is, os);
}
public void decryptAndVerify(Bundle params, InputStream is, final OutputStream os, IOpenPgpCallback callback) {
executeApiAsync(OPERATION_DECRYPT_VERIFY, params, is, os, callback);
}
public Bundle getKeyIds(Bundle params) {
return executeApi(OPERATION_GET_KEY_IDS, params, null, null);
}
public interface IOpenPgpCallback {
void onReturn(final Bundle result);
}
private class OpenPgpAsyncTask extends AsyncTask<Void, Integer, Bundle> {
int operationId;
Bundle params;
InputStream is;
OutputStream os;
IOpenPgpCallback callback;
private OpenPgpAsyncTask(int operationId, Bundle params, InputStream is, OutputStream os, IOpenPgpCallback callback) {
this.operationId = operationId;
this.params = params;
this.is = is;
this.os = os;
this.callback = callback;
}
@Override
protected Bundle doInBackground(Void... unused) {
return executeApi(operationId, params, is, os);
}
protected void onPostExecute(Bundle result) {
callback.onReturn(result);
}
}
private void executeApiAsync(int operationId, Bundle params, InputStream is, OutputStream os, IOpenPgpCallback callback) {
new OpenPgpAsyncTask(operationId, params, is, os, callback).execute((Void[]) null);
}
private Bundle executeApi(int operationId, Bundle params, InputStream is, OutputStream os) {
try {
params.putInt(OpenPgpConstants.PARAMS_API_VERSION, OpenPgpConstants.API_VERSION);
Bundle result = null;
if (operationId == OPERATION_GET_KEY_IDS) {
result = mService.getKeyIds(params);
return result;
} else {
// send the input and output pfds
ParcelFileDescriptor input = ParcelFileDescriptorUtil.pipeFrom(is,
new ParcelFileDescriptorUtil.IThreadListener() {
@Override
public void onThreadFinished(Thread thread) {
Log.d(OpenPgpConstants.TAG, "Copy to service finished");
}
});
ParcelFileDescriptor output = ParcelFileDescriptorUtil.pipeTo(os,
new ParcelFileDescriptorUtil.IThreadListener() {
@Override
public void onThreadFinished(Thread thread) {
Log.d(OpenPgpConstants.TAG, "Service finished writing!");
}
});
// blocks until result is ready
switch (operationId) {
case OPERATION_SIGN:
result = mService.sign(params, input, output);
break;
case OPERATION_ENCRYPT:
result = mService.encrypt(params, input, output);
break;
case OPERATION_SIGN_ENCRYPT:
result = mService.signAndEncrypt(params, input, output);
break;
case OPERATION_DECRYPT_VERIFY:
result = mService.decryptAndVerify(params, input, output);
break;
}
// close() is required to halt the TransferThread
output.close();
// set class loader to current context to allow unparcelling
// of OpenPgpError and OpenPgpSignatureResult
// http://stackoverflow.com/a/3806769
result.setClassLoader(mContext.getClassLoader());
return result;
}
} catch (Exception e) {
Log.e(OpenPgpConstants.TAG, "Exception", e);
Bundle result = new Bundle();
result.putInt(OpenPgpConstants.RESULT_CODE, OpenPgpConstants.RESULT_CODE_ERROR);
result.putParcelable(OpenPgpConstants.RESULT_ERRORS,
new OpenPgpError(OpenPgpError.CLIENT_SIDE_ERROR, e.getMessage()));
return result;
}
}
}

View File

@ -0,0 +1,53 @@
/*
* Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openintents.openpgp.util;
public class OpenPgpConstants {
public static final String TAG = "OpenPgp API";
public static final int API_VERSION = 1;
public static final String SERVICE_INTENT = "org.openintents.openpgp.IOpenPgpService";
/* Bundle params */
public static final String PARAMS_API_VERSION = "api_version";
// request ASCII Armor for output
// OpenPGP Radix-64, 33 percent overhead compared to binary, see http://tools.ietf.org/html/rfc4880#page-53)
public static final String PARAMS_REQUEST_ASCII_ARMOR = "ascii_armor";
// (for encrypt method)
public static final String PARAMS_USER_IDS = "user_ids";
public static final String PARAMS_KEY_IDS = "key_ids";
// optional parameter:
public static final String PARAMS_PASSPHRASE = "passphrase";
/* Service Bundle returns */
public static final String RESULT_CODE = "result_code";
public static final String RESULT_SIGNATURE = "signature";
public static final String RESULT_ERRORS = "error";
public static final String RESULT_INTENT = "intent";
// get actual error object from RESULT_ERRORS
public static final int RESULT_CODE_ERROR = 0;
// success!
public static final int RESULT_CODE_SUCCESS = 1;
// executeServiceMethod intent and do it again with params from intent
public static final int RESULT_CODE_USER_INTERACTION_REQUIRED = 2;
/* PendingIntent returns */
public static final String PI_RESULT_PARAMS = "params";
}

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de> * Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -14,18 +14,14 @@
* limitations under the License. * limitations under the License.
*/ */
package org.openintents.openpgp; package org.openintents.openpgp.util;
import java.util.ArrayList;
import java.util.List;
import android.app.AlertDialog.Builder; import android.app.AlertDialog.Builder;
import android.content.Context; import android.content.Context;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo; import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo; import android.content.res.TypedArray;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.preference.DialogPreference; import android.preference.DialogPreference;
import android.util.AttributeSet; import android.util.AttributeSet;
@ -35,33 +31,21 @@ import android.widget.ArrayAdapter;
import android.widget.ListAdapter; import android.widget.ListAdapter;
import android.widget.TextView; import android.widget.TextView;
import java.util.ArrayList;
import java.util.List;
import org.sufficientlysecure.keychain.api.R;
/**
* Does not extend ListPreference, but is very similar to it!
* http://grepcode.com/file_/repository.grepcode.com/java/ext/com.google.android/android/4.4_r1/android/preference/ListPreference.java/?v=source
*/
public class OpenPgpListPreference extends DialogPreference { public class OpenPgpListPreference extends DialogPreference {
ArrayList<OpenPgpProviderEntry> mProviderList = new ArrayList<OpenPgpProviderEntry>(); private ArrayList<OpenPgpProviderEntry> mProviderList = new ArrayList<OpenPgpProviderEntry>();
private String mSelectedPackage; private String mSelectedPackage;
public OpenPgpListPreference(Context context, AttributeSet attrs) { public OpenPgpListPreference(Context context, AttributeSet attrs) {
super(context, attrs); super(context, attrs);
List<ResolveInfo> resInfo = context.getPackageManager().queryIntentServices(
new Intent(OpenPgpConstants.SERVICE_INTENT), PackageManager.GET_META_DATA);
if (!resInfo.isEmpty()) {
for (ResolveInfo resolveInfo : resInfo) {
if (resolveInfo.serviceInfo == null)
continue;
String packageName = resolveInfo.serviceInfo.packageName;
String simpleName = String.valueOf(resolveInfo.serviceInfo.loadLabel(context
.getPackageManager()));
Drawable icon = resolveInfo.serviceInfo.loadIcon(context.getPackageManager());
// get api version
ServiceInfo si = resolveInfo.serviceInfo;
int apiVersion = si.metaData.getInt("api_version");
mProviderList.add(new OpenPgpProviderEntry(packageName, simpleName, icon,
apiVersion));
}
}
} }
public OpenPgpListPreference(Context context) { public OpenPgpListPreference(Context context) {
@ -69,20 +53,42 @@ public class OpenPgpListPreference extends DialogPreference {
} }
/** /**
* Can be used to add "no selection" * Public method to add new entries for legacy applications
* *
* @param packageName * @param packageName
* @param simpleName * @param simpleName
* @param icon * @param icon
*/ */
public void addProvider(int position, String packageName, String simpleName, Drawable icon, public void addProvider(int position, String packageName, String simpleName, Drawable icon) {
int apiVersion) { mProviderList.add(position, new OpenPgpProviderEntry(packageName, simpleName, icon));
mProviderList.add(position, new OpenPgpProviderEntry(packageName, simpleName, icon,
apiVersion));
} }
@Override @Override
protected void onPrepareDialogBuilder(Builder builder) { protected void onPrepareDialogBuilder(Builder builder) {
// get providers
mProviderList.clear();
Intent intent = new Intent(OpenPgpConstants.SERVICE_INTENT);
List<ResolveInfo> resInfo = getContext().getPackageManager().queryIntentServices(intent, 0);
if (!resInfo.isEmpty()) {
for (ResolveInfo resolveInfo : resInfo) {
if (resolveInfo.serviceInfo == null)
continue;
String packageName = resolveInfo.serviceInfo.packageName;
String simpleName = String.valueOf(resolveInfo.serviceInfo.loadLabel(getContext()
.getPackageManager()));
Drawable icon = resolveInfo.serviceInfo.loadIcon(getContext().getPackageManager());
mProviderList.add(new OpenPgpProviderEntry(packageName, simpleName, icon));
}
}
// add "none"-entry
mProviderList.add(0, new OpenPgpProviderEntry("",
getContext().getString(R.string.openpgp_list_preference_none),
getContext().getResources().getDrawable(R.drawable.ic_action_cancel_launchersize)));
// Init ArrayAdapter with OpenPGP Providers // Init ArrayAdapter with OpenPGP Providers
ListAdapter adapter = new ArrayAdapter<OpenPgpProviderEntry>(getContext(), ListAdapter adapter = new ArrayAdapter<OpenPgpProviderEntry>(getContext(),
android.R.layout.select_dialog_singlechoice, android.R.id.text1, mProviderList) { android.R.layout.select_dialog_singlechoice, android.R.id.text1, mProviderList) {
@ -99,15 +105,6 @@ public class OpenPgpListPreference extends DialogPreference {
int dp10 = (int) (10 * getContext().getResources().getDisplayMetrics().density + 0.5f); int dp10 = (int) (10 * getContext().getResources().getDisplayMetrics().density + 0.5f);
tv.setCompoundDrawablePadding(dp10); tv.setCompoundDrawablePadding(dp10);
// disable if it has the wrong api_version
if (mProviderList.get(position).apiVersion == OpenPgpConstants.REQUIRED_API_VERSION) {
tv.setEnabled(true);
} else {
tv.setEnabled(false);
tv.setText(tv.getText() + " (API v" + mProviderList.get(position).apiVersion
+ ", needs v" + OpenPgpConstants.REQUIRED_API_VERSION + ")");
}
return v; return v;
} }
}; };
@ -169,6 +166,16 @@ public class OpenPgpListPreference extends DialogPreference {
return getEntryByValue(mSelectedPackage); return getEntryByValue(mSelectedPackage);
} }
@Override
protected Object onGetDefaultValue(TypedArray a, int index) {
return a.getString(index);
}
@Override
protected void onSetInitialValue(boolean restoreValue, Object defaultValue) {
setValue(restoreValue ? getPersistedString(mSelectedPackage) : (String) defaultValue);
}
public String getEntryByValue(String packageName) { public String getEntryByValue(String packageName) {
for (OpenPgpProviderEntry app : mProviderList) { for (OpenPgpProviderEntry app : mProviderList) {
if (app.packageName.equals(packageName)) { if (app.packageName.equals(packageName)) {
@ -183,14 +190,11 @@ public class OpenPgpListPreference extends DialogPreference {
private String packageName; private String packageName;
private String simpleName; private String simpleName;
private Drawable icon; private Drawable icon;
private int apiVersion;
public OpenPgpProviderEntry(String packageName, String simpleName, Drawable icon, public OpenPgpProviderEntry(String packageName, String simpleName, Drawable icon) {
int apiVersion) {
this.packageName = packageName; this.packageName = packageName;
this.simpleName = simpleName; this.simpleName = simpleName;
this.icon = icon; this.icon = icon;
this.apiVersion = apiVersion;
} }
@Override @Override

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de> * Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package org.openintents.openpgp; package org.openintents.openpgp.util;
import org.openintents.openpgp.IOpenPgpService; import org.openintents.openpgp.IOpenPgpService;
@ -65,7 +65,8 @@ public class OpenPgpServiceConnection {
* @return * @return
*/ */
public boolean bindToService() { public boolean bindToService() {
if (mService == null && !mBound) { // if not already connected // if not already connected
if (mService == null && !mBound) {
try { try {
Log.d(OpenPgpConstants.TAG, "not bound yet"); Log.d(OpenPgpConstants.TAG, "not bound yet");

View File

@ -0,0 +1,64 @@
/*
* Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openintents.openpgp.util;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ResolveInfo;
public class OpenPgpUtils {
public static final Pattern PGP_MESSAGE = Pattern.compile(
".*?(-----BEGIN PGP MESSAGE-----.*?-----END PGP MESSAGE-----).*",
Pattern.DOTALL);
public static final Pattern PGP_SIGNED_MESSAGE = Pattern.compile(
".*?(-----BEGIN PGP SIGNED MESSAGE-----.*?-----BEGIN PGP SIGNATURE-----.*?-----END PGP SIGNATURE-----).*",
Pattern.DOTALL);
public static final int PARSE_RESULT_NO_PGP = -1;
public static final int PARSE_RESULT_MESSAGE = 0;
public static final int PARSE_RESULT_SIGNED_MESSAGE = 1;
public static int parseMessage(String message) {
Matcher matcherSigned = PGP_SIGNED_MESSAGE.matcher(message);
Matcher matcherMessage = PGP_MESSAGE.matcher(message);
if (matcherMessage.matches()) {
return PARSE_RESULT_MESSAGE;
} else if (matcherSigned.matches()) {
return PARSE_RESULT_SIGNED_MESSAGE;
} else {
return PARSE_RESULT_NO_PGP;
}
}
public static boolean isAvailable(Context context) {
Intent intent = new Intent(OpenPgpConstants.SERVICE_INTENT);
List<ResolveInfo> resInfo = context.getPackageManager().queryIntentServices(intent, 0);
if (!resInfo.isEmpty()) {
return true;
} else {
return false;
}
}
}

View File

@ -0,0 +1,104 @@
/*
* Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
* 2013 Flow (http://stackoverflow.com/questions/18212152/transfer-inputstream-to-another-service-across-process-boundaries-with-parcelf)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openintents.openpgp.util;
import android.os.ParcelFileDescriptor;
import android.util.Log;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class ParcelFileDescriptorUtil {
public interface IThreadListener {
void onThreadFinished(final Thread thread);
}
public static ParcelFileDescriptor pipeFrom(InputStream inputStream, IThreadListener listener)
throws IOException {
ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
ParcelFileDescriptor readSide = pipe[0];
ParcelFileDescriptor writeSide = pipe[1];
// start the transfer thread
new TransferThread(inputStream, new ParcelFileDescriptor.AutoCloseOutputStream(writeSide),
listener)
.start();
return readSide;
}
public static ParcelFileDescriptor pipeTo(OutputStream outputStream, IThreadListener listener)
throws IOException {
ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
ParcelFileDescriptor readSide = pipe[0];
ParcelFileDescriptor writeSide = pipe[1];
// start the transfer thread
new TransferThread(new ParcelFileDescriptor.AutoCloseInputStream(readSide), outputStream,
listener)
.start();
return writeSide;
}
static class TransferThread extends Thread {
final InputStream mIn;
final OutputStream mOut;
final IThreadListener mListener;
TransferThread(InputStream in, OutputStream out, IThreadListener listener) {
super("ParcelFileDescriptor Transfer Thread");
mIn = in;
mOut = out;
mListener = listener;
setDaemon(true);
}
@Override
public void run() {
byte[] buf = new byte[1024];
int len;
try {
while ((len = mIn.read(buf)) > 0) {
mOut.write(buf, 0, len);
}
mOut.flush(); // just to be safe
} catch (IOException e) {
Log.e(OpenPgpConstants.TAG, "TransferThread" + getId() + ": writing failed", e);
} finally {
try {
mIn.close();
} catch (IOException e) {
Log.e(OpenPgpConstants.TAG, "TransferThread" + getId(), e);
}
try {
mOut.close();
} catch (IOException e) {
Log.e(OpenPgpConstants.TAG, "TransferThread" + getId(), e);
}
}
if (mListener != null) {
Log.d(OpenPgpConstants.TAG, "TransferThread " + getId() + " finished!");
mListener.onThreadFinished(this);
}
}
}
}

View File

@ -0,0 +1,37 @@
/*
* Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sufficientlysecure.keychain.api;
public class OpenKeychainIntents {
public static final String ENCRYPT = "org.sufficientlysecure.keychain.action.ENCRYPT";
public static final String ENCRYPT_EXTRA_TEXT = "text"; // String
public static final String ENCRYPT_ASCII_ARMOR = "ascii_armor"; // boolean
public static final String DECRYPT = "org.sufficientlysecure.keychain.action.DECRYPT";
public static final String DECRYPT_EXTRA_TEXT = "text"; // String
public static final String IMPORT_KEY = "org.sufficientlysecure.keychain.action.IMPORT_KEY";
public static final String IMPORT_KEY_EXTRA_KEY_BYTES = "key_bytes"; // byte[]
public static final String IMPORT_KEY_FROM_KEYSERVER = "org.sufficientlysecure.keychain.action.IMPORT_KEY_FROM_KEYSERVER";
public static final String IMPORT_KEY_FROM_KEYSERVER_QUERY = "query"; // String
public static final String IMPORT_KEY_FROM_KEYSERVER_FINGERPRINT = "fingerprint"; // String
public static final String IMPORT_KEY_FROM_QR_CODE = "org.sufficientlysecure.keychain.action.IMPORT_KEY_FROM_QR_CODE";
}

View File

@ -20,7 +20,7 @@ android {
buildToolsVersion "19.0.1" buildToolsVersion "19.0.1"
defaultConfig { defaultConfig {
minSdkVersion 8 minSdkVersion 9
targetSdkVersion 19 targetSdkVersion 19
} }

View File

@ -30,7 +30,7 @@
--> -->
<uses-sdk <uses-sdk
android:minSdkVersion="8" android:minSdkVersion="9"
android:targetSdkVersion="19" /> android:targetSdkVersion="19" />
<uses-feature <uses-feature
@ -153,12 +153,19 @@
android:windowSoftInputMode="stateHidden"> android:windowSoftInputMode="stateHidden">
<!-- Keychain's own Actions --> <!-- Keychain's own Actions -->
<!-- ENCRYPT with text as extra -->
<intent-filter> <intent-filter>
<action android:name="org.sufficientlysecure.keychain.action.ENCRYPT" /> <action android:name="org.sufficientlysecure.keychain.action.ENCRYPT" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<!-- ENCRYPT with data Uri -->
<intent-filter>
<action android:name="org.sufficientlysecure.keychain.action.ENCRYPT" />
<data android:mimeType="*/*" /> <category android:name="android.intent.category.DEFAULT" />
<!-- TODO: accept other schemes! -->
<data android:scheme="file" />
</intent-filter> </intent-filter>
<!-- Android's Send Action --> <!-- Android's Send Action -->
<intent-filter android:label="@string/intent_send_encrypt"> <intent-filter android:label="@string/intent_send_encrypt">
@ -176,12 +183,19 @@
android:windowSoftInputMode="stateHidden"> android:windowSoftInputMode="stateHidden">
<!-- Keychain's own Actions --> <!-- Keychain's own Actions -->
<!-- DECRYPT with text as extra -->
<intent-filter> <intent-filter>
<action android:name="org.sufficientlysecure.keychain.action.DECRYPT" /> <action android:name="org.sufficientlysecure.keychain.action.DECRYPT" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<!-- DECRYPT with data Uri -->
<intent-filter>
<action android:name="org.sufficientlysecure.keychain.action.DECRYPT" />
<data android:mimeType="*/*" /> <category android:name="android.intent.category.DEFAULT" />
<!-- TODO: accept other schemes! -->
<data android:scheme="file" />
</intent-filter> </intent-filter>
<!-- Android's Send Action --> <!-- Android's Send Action -->
<intent-filter android:label="@string/intent_send_decrypt"> <intent-filter android:label="@string/intent_send_decrypt">
@ -282,6 +296,7 @@
<data android:mimeType="application/pgp-keys" /> <data android:mimeType="application/pgp-keys" />
</intent-filter> </intent-filter>
<!-- Keychain's own Actions --> <!-- Keychain's own Actions -->
<!-- IMPORT_KEY with files TODO: does this work? -->
<intent-filter android:label="@string/intent_import_key"> <intent-filter android:label="@string/intent_import_key">
<action android:name="org.sufficientlysecure.keychain.action.IMPORT_KEY" /> <action android:name="org.sufficientlysecure.keychain.action.IMPORT_KEY" />
@ -289,11 +304,19 @@
<data android:mimeType="*/*" /> <data android:mimeType="*/*" />
</intent-filter> </intent-filter>
<!-- IMPORT again without mimeType to also allow data only without filename --> <!-- IMPORT_KEY with mimeType 'application/pgp-keys' -->
<intent-filter>
<action android:name="org.sufficientlysecure.keychain.action.IMPORT_KEY" />
<category android:name="android.intent.category.DEFAULT" />
<!-- mime type as defined in http://tools.ietf.org/html/rfc3156, section 7 -->
<data android:mimeType="application/pgp-keys" />
</intent-filter>
<!-- IMPORT_KEY without mimeType to allow import with extras Bundle -->
<intent-filter android:label="@string/intent_import_key"> <intent-filter android:label="@string/intent_import_key">
<action android:name="org.sufficientlysecure.keychain.action.IMPORT_KEY" /> <action android:name="org.sufficientlysecure.keychain.action.IMPORT_KEY" />
<action android:name="org.sufficientlysecure.keychain.action.IMPORT_KEY_FROM_QR_CODE" /> <action android:name="org.sufficientlysecure.keychain.action.IMPORT_KEY_FROM_QR_CODE" />
<action android:name="org.sufficientlysecure.keychain.action.IMPORT_KEY_FROM_KEY_SERVER" /> <action android:name="org.sufficientlysecure.keychain.action.IMPORT_KEY_FROM_KEYSERVER" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
</intent-filter> </intent-filter>
@ -362,10 +385,9 @@
<activity <activity
android:name="org.sufficientlysecure.keychain.service.remote.RemoteServiceActivity" android:name="org.sufficientlysecure.keychain.service.remote.RemoteServiceActivity"
android:exported="false" android:exported="false"
android:label="@string/app_name" android:label="@string/app_name" />
android:launchMode="singleTop" <!--android:launchMode="singleTop"-->
android:process=":remote_api" <!--android:process=":remote_api"-->
android:taskAffinity=":remote_api" />
<activity <activity
android:name="org.sufficientlysecure.keychain.service.remote.RegisteredAppsListActivity" android:name="org.sufficientlysecure.keychain.service.remote.RegisteredAppsListActivity"
android:configChanges="orientation|screenSize|keyboardHidden|keyboard" android:configChanges="orientation|screenSize|keyboardHidden|keyboard"
@ -392,19 +414,19 @@
</service> </service>
<!-- Extended Remote API --> <!-- Extended Remote API -->
<service <!--<service-->
android:name="org.sufficientlysecure.keychain.service.remote.ExtendedApiService" <!--android:name="org.sufficientlysecure.keychain.service.remote.ExtendedApiService"-->
android:enabled="true" <!--android:enabled="true"-->
android:exported="true" <!--android:exported="true"-->
android:process=":remote_api"> <!--android:process=":remote_api">-->
<intent-filter> <!--<intent-filter>-->
<action android:name="org.sufficientlysecure.keychain.service.remote.IExtendedApiService" /> <!--<action android:name="org.sufficientlysecure.keychain.service.remote.IExtendedApiService" />-->
</intent-filter> <!--</intent-filter>-->
<meta-data <!--<meta-data-->
android:name="api_version" <!--android:name="api_version"-->
android:value="1" /> <!--android:value="1" />-->
</service> <!--</service>-->
<!-- TODO: authority! Make this API with content provider uris --> <!-- TODO: authority! Make this API with content provider uris -->
<!-- <provider --> <!-- <provider -->

View File

@ -235,7 +235,7 @@ public class KeychainIntentService extends IntentService implements ProgressDial
String action = intent.getAction(); String action = intent.getAction();
// execute action from extra bundle // executeServiceMethod action from extra bundle
if (ACTION_ENCRYPT_SIGN.equals(action)) { if (ACTION_ENCRYPT_SIGN.equals(action)) {
try { try {
/* Input */ /* Input */

View File

@ -1,10 +0,0 @@
package org.sufficientlysecure.keychain.service.exception;
public class NoUserIdsException extends Exception {
private static final long serialVersionUID = 7009311527126696207L;
public NoUserIdsException(String message) {
super(message);
}
}

View File

@ -1,10 +0,0 @@
package org.sufficientlysecure.keychain.service.exception;
public class UserInteractionRequiredException extends Exception {
private static final long serialVersionUID = -60128148603511936L;
public UserInteractionRequiredException(String message) {
super(message);
}
}

View File

@ -1,10 +0,0 @@
package org.sufficientlysecure.keychain.service.exception;
public class WrongPassphraseException extends Exception {
private static final long serialVersionUID = -5309689232853485740L;
public WrongPassphraseException(String message) {
super(message);
}
}

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de> * Copyright (C) 2013-2014 Dominik Schürmann <dominik@dominikschuermann.de>
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -17,99 +17,38 @@
package org.sufficientlysecure.keychain.service.remote; package org.sufficientlysecure.keychain.service.remote;
import java.io.ByteArrayInputStream; import android.app.PendingIntent;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.regex.Matcher;
import org.openintents.openpgp.IOpenPgpCallback;
import org.openintents.openpgp.IOpenPgpKeyIdsCallback;
import org.openintents.openpgp.IOpenPgpService;
import org.openintents.openpgp.OpenPgpData;
import org.openintents.openpgp.OpenPgpError;
import org.openintents.openpgp.OpenPgpSignatureResult;
import org.spongycastle.util.Arrays;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.Id;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.helper.Preferences;
import org.sufficientlysecure.keychain.pgp.PgpHelper;
import org.sufficientlysecure.keychain.pgp.PgpOperation;
import org.sufficientlysecure.keychain.pgp.exception.NoAsymmetricEncryptionException;
import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
import org.sufficientlysecure.keychain.provider.KeychainContract;
import org.sufficientlysecure.keychain.service.KeychainIntentService;
import org.sufficientlysecure.keychain.service.PassphraseCacheService;
import org.sufficientlysecure.keychain.service.exception.NoUserIdsException;
import org.sufficientlysecure.keychain.service.exception.UserInteractionRequiredException;
import org.sufficientlysecure.keychain.service.exception.WrongPassphraseException;
import org.sufficientlysecure.keychain.util.InputData;
import org.sufficientlysecure.keychain.util.Log;
import android.content.Intent; import android.content.Intent;
import android.database.Cursor; import android.database.Cursor;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.os.IBinder; import android.os.IBinder;
import android.os.Message; import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import org.openintents.openpgp.IOpenPgpService;
import org.openintents.openpgp.OpenPgpError;
import org.openintents.openpgp.OpenPgpSignatureResult;
import org.openintents.openpgp.util.OpenPgpConstants;
import org.spongycastle.util.Arrays;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.Id;
import org.sufficientlysecure.keychain.helper.Preferences;
import org.sufficientlysecure.keychain.pgp.PgpOperation;
import org.sufficientlysecure.keychain.provider.KeychainContract;
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 java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
public class OpenPgpService extends RemoteService { public class OpenPgpService extends RemoteService {
private String getCachedPassphrase(long keyId, boolean allowUserInteraction) private static final int PRIVATE_REQUEST_CODE_PASSPHRASE = 551;
throws UserInteractionRequiredException { private static final int PRIVATE_REQUEST_CODE_USER_IDS = 552;
String passphrase = PassphraseCacheService.getCachedPassphrase(getContext(), keyId);
if (passphrase == null) {
if (!allowUserInteraction) {
throw new UserInteractionRequiredException(
"Passphrase not found in cache, please enter your passphrase!");
}
Log.d(Constants.TAG, "No passphrase! Activity required!");
// start passphrase dialog
PassphraseActivityCallback callback = new PassphraseActivityCallback();
Bundle extras = new Bundle();
extras.putLong(RemoteServiceActivity.EXTRA_SECRET_KEY_ID, keyId);
pauseAndStartUserInteraction(RemoteServiceActivity.ACTION_CACHE_PASSPHRASE, callback,
extras);
if (callback.isSuccess()) {
Log.d(Constants.TAG, "New passphrase entered!");
// get again after it was entered
passphrase = PassphraseCacheService.getCachedPassphrase(getContext(), keyId);
} else {
Log.d(Constants.TAG, "Passphrase dialog canceled!");
return null;
}
}
return passphrase;
}
public class PassphraseActivityCallback extends UserInputCallback {
private boolean success = false;
public boolean isSuccess() {
return success;
}
@Override
public void handleUserInput(Message msg) {
if (msg.arg1 == OKAY) {
success = true;
} else {
success = false;
}
}
};
/** /**
* Search database for key ids based on emails. * Search database for key ids based on emails.
@ -117,8 +56,7 @@ public class OpenPgpService extends RemoteService {
* @param encryptionUserIds * @param encryptionUserIds
* @return * @return
*/ */
private long[] getKeyIdsFromEmails(String[] encryptionUserIds, boolean allowUserInteraction) private Bundle getKeyIdsFromEmails(Bundle params, String[] encryptionUserIds) {
throws UserInteractionRequiredException {
// find key ids to given emails in database // find key ids to given emails in database
ArrayList<Long> keyIds = new ArrayList<Long>(); ArrayList<Long> keyIds = new ArrayList<Long>();
@ -152,96 +90,118 @@ public class OpenPgpService extends RemoteService {
} }
// allow the user to verify pub key selection // allow the user to verify pub key selection
if (allowUserInteraction && (missingUserIdsCheck || dublicateUserIdsCheck)) { if (missingUserIdsCheck || dublicateUserIdsCheck) {
SelectPubKeysActivityCallback callback = new SelectPubKeysActivityCallback(); // build PendingIntent for passphrase input
Intent intent = new Intent(getBaseContext(), RemoteServiceActivity.class);
intent.setAction(RemoteServiceActivity.ACTION_SELECT_PUB_KEYS);
intent.putExtra(RemoteServiceActivity.EXTRA_SELECTED_MASTER_KEY_IDS, keyIdsArray);
intent.putExtra(RemoteServiceActivity.EXTRA_MISSING_USER_IDS, missingUserIds);
intent.putExtra(RemoteServiceActivity.EXTRA_DUBLICATE_USER_IDS, dublicateUserIds);
intent.putExtra(OpenPgpConstants.PI_RESULT_PARAMS, params);
Bundle extras = new Bundle(); PendingIntent pi = PendingIntent.getActivity(getBaseContext(), PRIVATE_REQUEST_CODE_USER_IDS, intent, 0);
extras.putLongArray(RemoteServiceActivity.EXTRA_SELECTED_MASTER_KEY_IDS, keyIdsArray);
extras.putStringArrayList(RemoteServiceActivity.EXTRA_MISSING_USER_IDS, missingUserIds);
extras.putStringArrayList(RemoteServiceActivity.EXTRA_DUBLICATE_USER_IDS,
dublicateUserIds);
pauseAndStartUserInteraction(RemoteServiceActivity.ACTION_SELECT_PUB_KEYS, callback, // return PendingIntent to be executed by client
extras); Bundle result = new Bundle();
result.putInt(OpenPgpConstants.RESULT_CODE, OpenPgpConstants.RESULT_CODE_USER_INTERACTION_REQUIRED);
result.putParcelable(OpenPgpConstants.RESULT_INTENT, pi);
if (callback.isSuccess()) { return result;
Log.d(Constants.TAG, "New selection of pub keys!");
keyIdsArray = callback.getPubKeyIds();
} else {
Log.d(Constants.TAG, "Pub key selection canceled!");
return null;
}
}
// if no user interaction is allow throw exceptions on duplicate or missing pub keys
if (!allowUserInteraction) {
if (missingUserIdsCheck)
throw new UserInteractionRequiredException(
"Pub keys for these user ids are missing:" + missingUserIds.toString());
if (dublicateUserIdsCheck)
throw new UserInteractionRequiredException(
"More than one pub key with these user ids exist:"
+ dublicateUserIds.toString());
} }
if (keyIdsArray.length == 0) { if (keyIdsArray.length == 0) {
return null; return null;
} }
return keyIdsArray;
Bundle result = new Bundle();
result.putInt(OpenPgpConstants.RESULT_CODE, OpenPgpConstants.RESULT_CODE_SUCCESS);
result.putLongArray(OpenPgpConstants.PARAMS_KEY_IDS, keyIdsArray);
return result;
} }
public class SelectPubKeysActivityCallback extends UserInputCallback { private Bundle getPassphraseBundleIntent(Bundle params, long keyId) {
public static final String PUB_KEY_IDS = "pub_key_ids"; // build PendingIntent for passphrase input
Intent intent = new Intent(getBaseContext(), RemoteServiceActivity.class);
intent.setAction(RemoteServiceActivity.ACTION_CACHE_PASSPHRASE);
intent.putExtra(RemoteServiceActivity.EXTRA_SECRET_KEY_ID, keyId);
// pass params through to activity that it can be returned again later to repeat pgp operation
intent.putExtra(OpenPgpConstants.PI_RESULT_PARAMS, params);
PendingIntent pi = PendingIntent.getActivity(getBaseContext(), PRIVATE_REQUEST_CODE_PASSPHRASE, intent, 0);
private boolean success = false; // return PendingIntent to be executed by client
private long[] pubKeyIds; Bundle result = new Bundle();
result.putInt(OpenPgpConstants.RESULT_CODE, OpenPgpConstants.RESULT_CODE_USER_INTERACTION_REQUIRED);
result.putParcelable(OpenPgpConstants.RESULT_INTENT, pi);
public boolean isSuccess() { return result;
return success; }
}
public long[] getPubKeyIds() {
return pubKeyIds;
}
@Override // TODO: asciiArmor?!
public void handleUserInput(Message msg) { private Bundle signImpl(Bundle params, ParcelFileDescriptor input, ParcelFileDescriptor output,
if (msg.arg1 == OKAY) { AppSettings appSettings) {
success = true; try {
pubKeyIds = msg.getData().getLongArray(PUB_KEY_IDS); // get passphrase from cache, if key has "no" passphrase, this returns an empty String
String passphrase;
if (params.containsKey(OpenPgpConstants.PARAMS_PASSPHRASE)) {
passphrase = params.getString(OpenPgpConstants.PARAMS_PASSPHRASE);
} else { } else {
success = false; passphrase = PassphraseCacheService.getCachedPassphrase(getContext(), appSettings.getKeyId());
} }
} if (passphrase == null) {
}; // get PendingIntent for passphrase input, add it to given params and return to client
Bundle passphraseBundle = getPassphraseBundleIntent(params, appSettings.getKeyId());
private synchronized void getKeyIdsSafe(String[] userIds, boolean allowUserInteraction, return passphraseBundle;
IOpenPgpKeyIdsCallback callback, AppSettings appSettings) {
try {
long[] keyIds = getKeyIdsFromEmails(userIds, allowUserInteraction);
if (keyIds == null) {
throw new NoUserIdsException("No user ids!");
} }
callback.onSuccess(keyIds); // Get Input- and OutputStream from ParcelFileDescriptor
} catch (UserInteractionRequiredException e) { InputStream is = new ParcelFileDescriptor.AutoCloseInputStream(input);
callbackOpenPgpError(callback, OpenPgpError.USER_INTERACTION_REQUIRED, e.getMessage()); OutputStream os = new ParcelFileDescriptor.AutoCloseOutputStream(output);
} catch (NoUserIdsException e) { try {
callbackOpenPgpError(callback, OpenPgpError.NO_USER_IDS, e.getMessage()); long inputLength = is.available();
InputData inputData = new InputData(is, inputLength);
PgpOperation operation = new PgpOperation(getContext(), null, inputData, os);
operation.signText(appSettings.getKeyId(), passphrase, appSettings.getHashAlgorithm(),
Preferences.getPreferences(this).getForceV3Signatures());
} finally {
is.close();
os.close();
}
Bundle result = new Bundle();
result.putInt(OpenPgpConstants.RESULT_CODE, OpenPgpConstants.RESULT_CODE_SUCCESS);
return result;
} catch (Exception e) { } catch (Exception e) {
callbackOpenPgpError(callback, OpenPgpError.GENERIC_ERROR, e.getMessage()); Bundle result = new Bundle();
result.putInt(OpenPgpConstants.RESULT_CODE, OpenPgpConstants.RESULT_CODE_ERROR);
result.putParcelable(OpenPgpConstants.RESULT_ERRORS,
new OpenPgpError(OpenPgpError.GENERIC_ERROR, e.getMessage()));
return result;
} }
} }
private synchronized void encryptAndSignSafe(OpenPgpData inputData, private Bundle encryptAndSignImpl(Bundle params, ParcelFileDescriptor input,
final OpenPgpData outputData, long[] keyIds, boolean allowUserInteraction, ParcelFileDescriptor output, AppSettings appSettings,
IOpenPgpCallback callback, AppSettings appSettings, boolean sign) { boolean sign) {
try { try {
// TODO: other options of OpenPgpData! boolean asciiArmor = params.getBoolean(OpenPgpConstants.PARAMS_REQUEST_ASCII_ARMOR, false);
byte[] inputBytes = getInput(inputData);
boolean asciiArmor = false; long[] keyIds;
if (outputData.getType() == OpenPgpData.TYPE_STRING) { if (params.containsKey(OpenPgpConstants.PARAMS_KEY_IDS)) {
asciiArmor = true; keyIds = params.getLongArray(OpenPgpConstants.PARAMS_KEY_IDS);
} else {
// get key ids based on given user ids
String[] userIds = params.getStringArray(OpenPgpConstants.PARAMS_USER_IDS);
// give params through to activity...
Bundle result = getKeyIdsFromEmails(params, userIds);
if (result.getInt(OpenPgpConstants.RESULT_CODE, 0) == OpenPgpConstants.RESULT_CODE_SUCCESS) {
keyIds = result.getLongArray(OpenPgpConstants.PARAMS_KEY_IDS);
} else {
// if not success -> result contains a PendingIntent for user interaction
return result;
}
} }
// add own key for encryption // add own key for encryption
@ -249,350 +209,320 @@ public class OpenPgpService extends RemoteService {
keyIds[keyIds.length - 1] = appSettings.getKeyId(); keyIds[keyIds.length - 1] = appSettings.getKeyId();
// build InputData and write into OutputStream // build InputData and write into OutputStream
InputStream inputStream = new ByteArrayInputStream(inputBytes); // Get Input- and OutputStream from ParcelFileDescriptor
long inputLength = inputBytes.length; InputStream is = new ParcelFileDescriptor.AutoCloseInputStream(input);
InputData inputDt = new InputData(inputStream, inputLength); OutputStream os = new ParcelFileDescriptor.AutoCloseOutputStream(output);
try {
long inputLength = is.available();
InputData inputData = new InputData(is, inputLength);
OutputStream outputStream = new ByteArrayOutputStream(); PgpOperation operation = new PgpOperation(getContext(), null, inputData, os);
if (sign) {
String passphrase;
if (params.containsKey(OpenPgpConstants.PARAMS_PASSPHRASE)) {
passphrase = params.getString(OpenPgpConstants.PARAMS_PASSPHRASE);
} else {
passphrase = PassphraseCacheService.getCachedPassphrase(getContext(),
appSettings.getKeyId());
}
if (passphrase == null) {
// get PendingIntent for passphrase input, add it to given params and return to client
Bundle passphraseBundle = getPassphraseBundleIntent(params, appSettings.getKeyId());
return passphraseBundle;
}
PgpOperation operation = new PgpOperation(getContext(), null, inputDt, outputStream); operation.signAndEncrypt(asciiArmor, appSettings.getCompression(), keyIds, null,
if (sign) { appSettings.getEncryptionAlgorithm(), appSettings.getKeyId(),
String passphrase = getCachedPassphrase(appSettings.getKeyId(), appSettings.getHashAlgorithm(), true, passphrase);
allowUserInteraction);
if (passphrase == null) {
throw new WrongPassphraseException("No or wrong passphrase!");
}
operation.signAndEncrypt(asciiArmor, appSettings.getCompression(), keyIds, null,
appSettings.getEncryptionAlgorithm(), appSettings.getKeyId(),
appSettings.getHashAlgorithm(), true, passphrase);
} else {
operation.signAndEncrypt(asciiArmor, appSettings.getCompression(), keyIds, null,
appSettings.getEncryptionAlgorithm(), Id.key.none,
appSettings.getHashAlgorithm(), true, null);
}
outputStream.close();
byte[] outputBytes = ((ByteArrayOutputStream) outputStream).toByteArray();
OpenPgpData output = null;
if (asciiArmor) {
output = new OpenPgpData(new String(outputBytes));
} else {
output = new OpenPgpData(outputBytes);
}
// return over handler on client side
callback.onSuccess(output, null);
} catch (UserInteractionRequiredException e) {
callbackOpenPgpError(callback, OpenPgpError.USER_INTERACTION_REQUIRED, e.getMessage());
} catch (WrongPassphraseException e) {
callbackOpenPgpError(callback, OpenPgpError.NO_OR_WRONG_PASSPHRASE, e.getMessage());
} catch (Exception e) {
callbackOpenPgpError(callback, OpenPgpError.GENERIC_ERROR, e.getMessage());
}
}
// TODO: asciiArmor?!
private void signSafe(byte[] inputBytes, boolean allowUserInteraction,
IOpenPgpCallback callback, AppSettings appSettings) {
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();
String passphrase = getCachedPassphrase(appSettings.getKeyId(), allowUserInteraction);
if (passphrase == null) {
throw new WrongPassphraseException("No or wrong passphrase!");
}
PgpOperation operation = new PgpOperation(getContext(), null, inputData, outputStream);
operation.signText(appSettings.getKeyId(), passphrase, appSettings.getHashAlgorithm(),
Preferences.getPreferences(this).getForceV3Signatures());
outputStream.close();
byte[] outputBytes = ((ByteArrayOutputStream) outputStream).toByteArray();
OpenPgpData output = new OpenPgpData(new String(outputBytes));
// return over handler on client side
callback.onSuccess(output, null);
} catch (UserInteractionRequiredException e) {
callbackOpenPgpError(callback, OpenPgpError.USER_INTERACTION_REQUIRED, e.getMessage());
} catch (WrongPassphraseException e) {
callbackOpenPgpError(callback, OpenPgpError.NO_OR_WRONG_PASSPHRASE, e.getMessage());
} catch (Exception e) {
callbackOpenPgpError(callback, OpenPgpError.GENERIC_ERROR, e.getMessage());
}
}
private synchronized void decryptAndVerifySafe(byte[] inputBytes, boolean allowUserInteraction,
IOpenPgpCallback callback, AppSettings appSettings) {
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 = PgpHelper.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 = PgpHelper.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 { } else {
Log.d(Constants.TAG, "Nothing matched! Binary?"); operation.signAndEncrypt(asciiArmor, appSettings.getCompression(), keyIds, null,
} appSettings.getEncryptionAlgorithm(), Id.key.none,
} appSettings.getHashAlgorithm(), true, null);
// 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?
String passphrase = null;
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!
long secretKeyId;
try {
if (inputStream2.markSupported()) {
// should probably set this to the max size of two
// pgpF objects, if it even needs to be anything other
// than 0.
inputStream2.mark(200);
}
secretKeyId = PgpHelper.getDecryptionKeyId(this, inputStream2);
if (secretKeyId == Id.key.none) {
throw new PgpGeneralException(getString(R.string.error_no_secret_key_found));
}
} catch (NoAsymmetricEncryptionException e) {
if (inputStream2.markSupported()) {
inputStream2.reset();
}
secretKeyId = Id.key.symmetric;
if (!PgpOperation.hasSymmetricEncryption(this, inputStream2)) {
throw new PgpGeneralException(
getString(R.string.error_no_known_encryption_found));
}
// we do not support symmetric decryption from the API!
throw new Exception("Symmetric decryption is not supported!");
}
Log.d(Constants.TAG, "secretKeyId " + secretKeyId);
passphrase = getCachedPassphrase(secretKeyId, allowUserInteraction);
if (passphrase == null) {
throw new WrongPassphraseException("No or wrong passphrase!");
} }
} finally {
is.close();
os.close();
} }
// build InputData and write into OutputStream Bundle result = new Bundle();
InputStream inputStream = new ByteArrayInputStream(inputBytes); result.putInt(OpenPgpConstants.RESULT_CODE, OpenPgpConstants.RESULT_CODE_SUCCESS);
long inputLength = inputBytes.length; return result;
InputData inputData = new InputData(inputStream, inputLength);
OutputStream outputStream = new ByteArrayOutputStream();
Bundle outputBundle;
PgpOperation operation = new PgpOperation(getContext(), null, inputData, outputStream);
if (signedOnly) {
outputBundle = operation.verifyText();
} else {
outputBundle = operation.decryptAndVerify(passphrase, false);
}
outputStream.close();
byte[] outputBytes = ((ByteArrayOutputStream) outputStream).toByteArray();
// get signature informations from bundle
boolean signature = outputBundle.getBoolean(KeychainIntentService.RESULT_SIGNATURE);
OpenPgpSignatureResult sigResult = null;
if (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);
int signatureStatus = OpenPgpSignatureResult.SIGNATURE_ERROR;
if (signatureSuccess) {
signatureStatus = OpenPgpSignatureResult.SIGNATURE_SUCCESS_TRUSTED;
} else if (signatureUnknown) {
signatureStatus = OpenPgpSignatureResult.SIGNATURE_UNKNOWN_PUB_KEY;
}
sigResult = new OpenPgpSignatureResult(signatureStatus, signatureUserId,
signedOnly, signatureKeyId);
}
OpenPgpData output = new OpenPgpData(new String(outputBytes));
// return over handler on client side
callback.onSuccess(output, sigResult);
} catch (UserInteractionRequiredException e) {
callbackOpenPgpError(callback, OpenPgpError.USER_INTERACTION_REQUIRED, e.getMessage());
} catch (WrongPassphraseException e) {
callbackOpenPgpError(callback, OpenPgpError.NO_OR_WRONG_PASSPHRASE, e.getMessage());
} catch (Exception e) { } catch (Exception e) {
callbackOpenPgpError(callback, OpenPgpError.GENERIC_ERROR, e.getMessage()); Bundle result = new Bundle();
result.putInt(OpenPgpConstants.RESULT_CODE, OpenPgpConstants.RESULT_CODE_ERROR);
result.putParcelable(OpenPgpConstants.RESULT_ERRORS,
new OpenPgpError(OpenPgpError.GENERIC_ERROR, e.getMessage()));
return result;
} }
} }
private Bundle decryptAndVerifyImpl(Bundle params, ParcelFileDescriptor input,
ParcelFileDescriptor output, AppSettings appSettings) {
try {
// Get Input- and OutputStream from ParcelFileDescriptor
InputStream is = new ParcelFileDescriptor.AutoCloseInputStream(input);
OutputStream os = new ParcelFileDescriptor.AutoCloseOutputStream(output);
OpenPgpSignatureResult sigResult = null;
try {
// TODOs API 2.0:
// implement verify-only!
// fix the mess: http://stackoverflow.com/questions/148130/how-do-i-peek-at-the-first-two-bytes-in-an-inputstream
// should we allow to decrypt everything under every key id or only the one set?
// TODO: instead of trying to get the passphrase before
// pause stream when passphrase is missing and then resume
// 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 = PgpHelper.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 = PgpHelper.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?
// String passphrase = null;
// 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!
// long secretKeyId;
// try {
// if (inputStream2.markSupported()) {
// // should probably set this to the max size of two
// // pgpF objects, if it even needs to be anything other
// // than 0.
// inputStream2.mark(200);
// }
// secretKeyId = PgpHelper.getDecryptionKeyId(this, inputStream2);
// if (secretKeyId == Id.key.none) {
// throw new PgpGeneralException(getString(R.string.error_no_secret_key_found));
// }
// } catch (NoAsymmetricEncryptionException e) {
// if (inputStream2.markSupported()) {
// inputStream2.reset();
// }
// secretKeyId = Id.key.symmetric;
// if (!PgpOperation.hasSymmetricEncryption(this, inputStream2)) {
// throw new PgpGeneralException(
// getString(R.string.error_no_known_encryption_found));
// }
// // we do not support symmetric decryption from the API!
// throw new Exception("Symmetric decryption is not supported!");
// }
//
// Log.d(Constants.TAG, "secretKeyId " + secretKeyId);
// NOTE: currently this only gets the passphrase for the saved key
String passphrase;
if (params.containsKey(OpenPgpConstants.PARAMS_PASSPHRASE)) {
passphrase = params.getString(OpenPgpConstants.PARAMS_PASSPHRASE);
} else {
passphrase = PassphraseCacheService.getCachedPassphrase(getContext(), appSettings.getKeyId());
}
if (passphrase == null) {
// get PendingIntent for passphrase input, add it to given params and return to client
Bundle passphraseBundle = getPassphraseBundleIntent(params, appSettings.getKeyId());
return passphraseBundle;
}
// }
// build InputData and write into OutputStream
long inputLength = is.available();
InputData inputData = new InputData(is, inputLength);
Bundle outputBundle;
PgpOperation operation = new PgpOperation(getContext(), null, inputData, os);
if (signedOnly) {
outputBundle = operation.verifyText();
} else {
// BIG TODO: instead of trying to get the passphrase before
// pause stream when passphrase is missing and then resume
outputBundle = operation.decryptAndVerify(passphrase, false);
}
// outputStream.close();
// byte[] outputBytes = ((ByteArrayOutputStream) outputStream).toByteArray();
// get signature informations from bundle
boolean signature = outputBundle.getBoolean(KeychainIntentService.RESULT_SIGNATURE);
if (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);
int signatureStatus = OpenPgpSignatureResult.SIGNATURE_ERROR;
if (signatureSuccess) {
signatureStatus = OpenPgpSignatureResult.SIGNATURE_SUCCESS_CERTIFIED;
} else if (signatureUnknown) {
signatureStatus = OpenPgpSignatureResult.SIGNATURE_UNKNOWN_PUB_KEY;
}
sigResult = new OpenPgpSignatureResult(signatureStatus, signatureUserId,
signedOnly, signatureKeyId);
}
} finally {
is.close();
os.close();
}
Bundle result = new Bundle();
result.putInt(OpenPgpConstants.RESULT_CODE, OpenPgpConstants.RESULT_CODE_SUCCESS);
result.putParcelable(OpenPgpConstants.RESULT_SIGNATURE, sigResult);
return result;
} catch (Exception e) {
Bundle result = new Bundle();
result.putInt(OpenPgpConstants.RESULT_CODE, OpenPgpConstants.RESULT_CODE_ERROR);
result.putParcelable(OpenPgpConstants.RESULT_ERRORS,
new OpenPgpError(OpenPgpError.GENERIC_ERROR, e.getMessage()));
return result;
}
}
private Bundle getKeyIdsImpl(Bundle params) {
// get key ids based on given user ids
String[] userIds = params.getStringArray(OpenPgpConstants.PARAMS_USER_IDS);
Bundle result = getKeyIdsFromEmails(params, userIds);
return result;
}
/** /**
* Returns error to IOpenPgpCallback * Checks that params != null and API version fits
* *
* @param callback * @param params
* @param errorId * @return
* @param message
*/ */
private void callbackOpenPgpError(IOpenPgpCallback callback, int errorId, String message) { private Bundle checkRequirements(Bundle params) {
try { // params Bundle is required!
callback.onError(new OpenPgpError(0, message)); if (params == null) {
} catch (Exception t) { Bundle result = new Bundle();
Log.e(Constants.TAG, OpenPgpError error = new OpenPgpError(OpenPgpError.GENERIC_ERROR, "params Bundle required!");
"Exception while returning OpenPgpError to client via callback.onError()", t); result.putParcelable(OpenPgpConstants.RESULT_ERRORS, error);
} result.putInt(OpenPgpConstants.RESULT_CODE, OpenPgpConstants.RESULT_CODE_ERROR);
} return result;
private void callbackOpenPgpError(IOpenPgpKeyIdsCallback callback, int errorId, String message) {
try {
callback.onError(new OpenPgpError(0, message));
} catch (Exception t) {
Log.e(Constants.TAG,
"Exception while returning OpenPgpError to client via callback.onError()", t);
} }
// version code is required and needs to correspond to version code of service!
if (params.getInt(OpenPgpConstants.PARAMS_API_VERSION) != OpenPgpConstants.API_VERSION) {
Bundle result = new Bundle();
OpenPgpError error = new OpenPgpError(OpenPgpError.INCOMPATIBLE_API_VERSIONS, "Incompatible API versions!");
result.putParcelable(OpenPgpConstants.RESULT_ERRORS, error);
result.putInt(OpenPgpConstants.RESULT_CODE, OpenPgpConstants.RESULT_CODE_ERROR);
return result;
}
// check if caller is allowed to access openpgp keychain
Bundle result = isAllowed(params);
if (result != null) {
return result;
}
return null;
} }
// TODO: multi-threading
private final IOpenPgpService.Stub mBinder = new IOpenPgpService.Stub() { private final IOpenPgpService.Stub mBinder = new IOpenPgpService.Stub() {
@Override @Override
public void encrypt(final OpenPgpData input, final OpenPgpData output, final long[] keyIds, public Bundle sign(Bundle params, final ParcelFileDescriptor input, final ParcelFileDescriptor output) {
final IOpenPgpCallback callback) throws RemoteException { final AppSettings appSettings = getAppSettings();
final AppSettings settings = getAppSettings();
Runnable r = new Runnable() { Bundle errorResult = checkRequirements(params);
@Override if (errorResult != null) {
public void run() { return errorResult;
encryptAndSignSafe(input, output, keyIds, true, callback, settings, false); }
}
};
checkAndEnqueue(r); return signImpl(params, input, output, appSettings);
} }
@Override @Override
public void signAndEncrypt(final OpenPgpData input, final OpenPgpData output, public Bundle encrypt(Bundle params, ParcelFileDescriptor input, ParcelFileDescriptor output) {
final long[] keyIds, final IOpenPgpCallback callback) throws RemoteException { final AppSettings appSettings = getAppSettings();
final AppSettings settings = getAppSettings();
Runnable r = new Runnable() { Bundle errorResult = checkRequirements(params);
@Override if (errorResult != null) {
public void run() { return errorResult;
encryptAndSignSafe(input, output, keyIds, true, callback, settings, true); }
}
};
checkAndEnqueue(r); return encryptAndSignImpl(params, input, output, appSettings, false);
} }
@Override @Override
public void sign(final OpenPgpData input, final OpenPgpData output, public Bundle signAndEncrypt(Bundle params, ParcelFileDescriptor input, ParcelFileDescriptor output) {
final IOpenPgpCallback callback) throws RemoteException { final AppSettings appSettings = getAppSettings();
final AppSettings settings = getAppSettings();
Runnable r = new Runnable() { Bundle errorResult = checkRequirements(params);
@Override if (errorResult != null) {
public void run() { return errorResult;
signSafe(getInput(input), true, callback, settings); }
}
};
checkAndEnqueue(r); return encryptAndSignImpl(params, input, output, appSettings, true);
} }
@Override @Override
public void decryptAndVerify(final OpenPgpData input, final OpenPgpData output, public Bundle decryptAndVerify(Bundle params, ParcelFileDescriptor input, ParcelFileDescriptor output) {
final IOpenPgpCallback callback) throws RemoteException { final AppSettings appSettings = getAppSettings();
final AppSettings settings = getAppSettings(); Bundle errorResult = checkRequirements(params);
if (errorResult != null) {
return errorResult;
}
Runnable r = new Runnable() { return decryptAndVerifyImpl(params, input, output, appSettings);
@Override
public void run() {
decryptAndVerifySafe(getInput(input), true, callback, settings);
}
};
checkAndEnqueue(r);
} }
@Override @Override
public void getKeyIds(final String[] userIds, final boolean allowUserInteraction, public Bundle getKeyIds(Bundle params) {
final IOpenPgpKeyIdsCallback callback) throws RemoteException { Bundle errorResult = checkRequirements(params);
if (errorResult != null) {
return errorResult;
}
final AppSettings settings = getAppSettings(); return getKeyIdsImpl(params);
Runnable r = new Runnable() {
@Override
public void run() {
getKeyIdsSafe(userIds, allowUserInteraction, callback, settings);
}
};
checkAndEnqueue(r);
} }
}; };
private static byte[] getInput(OpenPgpData data) {
// TODO: support Uri and ParcelFileDescriptor
byte[] inBytes = null;
switch (data.getType()) {
case OpenPgpData.TYPE_STRING:
inBytes = data.getString().getBytes();
break;
case OpenPgpData.TYPE_BYTE_ARRAY:
inBytes = data.getBytes();
break;
default:
Log.e(Constants.TAG, "Uri and ParcelFileDescriptor not supported right now!");
break;
}
return inBytes;
}
@Override @Override
public IBinder onBind(Intent intent) { public IBinder onBind(Intent intent) {
return mBinder; return mBinder;

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de> * Copyright (C) 2013-2014 Dominik Schürmann <dominik@dominikschuermann.de>
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -19,17 +19,16 @@ package org.sufficientlysecure.keychain.service.remote;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.TimeUnit;
import org.openintents.openpgp.OpenPgpError;
import org.openintents.openpgp.util.OpenPgpConstants;
import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.provider.KeychainContract; import org.sufficientlysecure.keychain.provider.KeychainContract;
import org.sufficientlysecure.keychain.provider.ProviderHelper; import org.sufficientlysecure.keychain.provider.ProviderHelper;
import org.sufficientlysecure.keychain.service.exception.WrongPackageSignatureException;
import org.sufficientlysecure.keychain.util.Log; import org.sufficientlysecure.keychain.util.Log;
import org.sufficientlysecure.keychain.util.PausableThreadPoolExecutor;
import android.app.PendingIntent;
import android.app.Service; import android.app.Service;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
@ -50,66 +49,19 @@ import android.os.Messenger;
public abstract class RemoteService extends Service { public abstract class RemoteService extends Service {
Context mContext; Context mContext;
private final ArrayBlockingQueue<Runnable> mPoolQueue = new ArrayBlockingQueue<Runnable>(100); private static final int PRIVATE_REQUEST_CODE_REGISTER = 651;
// TODO: Are these parameters okay? private static final int PRIVATE_REQUEST_CODE_ERROR = 652;
private PausableThreadPoolExecutor mThreadPool = new PausableThreadPoolExecutor(2, 4, 10,
TimeUnit.SECONDS, mPoolQueue);
private final Object userInputLock = new Object();
/**
* Override handleUserInput() to handle OKAY (1) and CANCEL (0). After handling the waiting
* threads will be notified and the queue resumed
*/
protected class UserInputCallback extends BaseCallback {
public void handleUserInput(Message msg) {
}
@Override
public boolean handleMessage(Message msg) {
handleUserInput(msg);
// resume
synchronized (userInputLock) {
userInputLock.notifyAll();
}
mThreadPool.resume();
return true;
}
}
/**
* Extends Handler.Callback with OKAY (1), CANCEL (0) variables
*/
private class BaseCallback implements Handler.Callback {
public static final int OKAY = 1;
public static final int CANCEL = 0;
@Override
public boolean handleMessage(Message msg) {
return false;
}
}
public Context getContext() { public Context getContext() {
return mContext; return mContext;
} }
/** protected Bundle isAllowed(Bundle params) {
* Should be used from Stub implementations of AIDL interfaces to enqueue a runnable for
* execution
*
* @param r
*/
protected void checkAndEnqueue(Runnable r) {
try { try {
if (isCallerAllowed(false)) { if (isCallerAllowed(false)) {
mThreadPool.execute(r);
Log.d(Constants.TAG, "Enqueued runnable…"); return null;
} else { } else {
String[] callingPackages = getPackageManager().getPackagesForUid( String[] callingPackages = getPackageManager().getPackagesForUid(
Binder.getCallingUid()); Binder.getCallingUid());
@ -121,32 +73,46 @@ public abstract class RemoteService extends Service {
packageSignature = getPackageSignature(packageName); packageSignature = getPackageSignature(packageName);
} catch (NameNotFoundException e) { } catch (NameNotFoundException e) {
Log.e(Constants.TAG, "Should not happen, returning!", e); Log.e(Constants.TAG, "Should not happen, returning!", e);
return; // return error
Bundle result = new Bundle();
result.putInt(OpenPgpConstants.RESULT_CODE, OpenPgpConstants.RESULT_CODE_ERROR);
result.putParcelable(OpenPgpConstants.RESULT_ERRORS,
new OpenPgpError(OpenPgpError.GENERIC_ERROR, e.getMessage()));
return result;
} }
Log.e(Constants.TAG, Log.e(Constants.TAG, "Not allowed to use service! return PendingIntent for registration!");
"Not allowed to use service! Starting activity for registration!");
Bundle extras = new Bundle();
extras.putString(RemoteServiceActivity.EXTRA_PACKAGE_NAME, packageName);
extras.putByteArray(RemoteServiceActivity.EXTRA_PACKAGE_SIGNATURE, packageSignature);
RegisterActivityCallback callback = new RegisterActivityCallback();
pauseAndStartUserInteraction(RemoteServiceActivity.ACTION_REGISTER, callback, Intent intent = new Intent(getBaseContext(), RemoteServiceActivity.class);
extras); intent.setAction(RemoteServiceActivity.ACTION_REGISTER);
intent.putExtra(RemoteServiceActivity.EXTRA_PACKAGE_NAME, packageName);
intent.putExtra(RemoteServiceActivity.EXTRA_PACKAGE_SIGNATURE, packageSignature);
intent.putExtra(OpenPgpConstants.PI_RESULT_PARAMS, params);
if (callback.isAllowed()) { PendingIntent pi = PendingIntent.getActivity(getBaseContext(), PRIVATE_REQUEST_CODE_REGISTER, intent, 0);
mThreadPool.execute(r);
Log.d(Constants.TAG, "Enqueued runnable…"); // return PendingIntent to be executed by client
} else { Bundle result = new Bundle();
Log.d(Constants.TAG, "User disallowed app!"); result.putInt(OpenPgpConstants.RESULT_CODE, OpenPgpConstants.RESULT_CODE_USER_INTERACTION_REQUIRED);
} result.putParcelable(OpenPgpConstants.RESULT_INTENT, pi);
return result;
} }
} catch (WrongPackageSignatureException e) { } catch (WrongPackageSignatureException e) {
Log.e(Constants.TAG, e.getMessage()); Log.e(Constants.TAG, "wrong signature!", e);
Bundle extras = new Bundle(); Intent intent = new Intent(getBaseContext(), RemoteServiceActivity.class);
extras.putString(RemoteServiceActivity.EXTRA_ERROR_MESSAGE, intent.setAction(RemoteServiceActivity.ACTION_ERROR_MESSAGE);
getString(R.string.api_error_wrong_signature)); intent.putExtra(RemoteServiceActivity.EXTRA_ERROR_MESSAGE, getString(R.string.api_error_wrong_signature));
pauseAndStartUserInteraction(RemoteServiceActivity.ACTION_ERROR_MESSAGE, null, extras); intent.putExtra(OpenPgpConstants.PI_RESULT_PARAMS, params);
PendingIntent pi = PendingIntent.getActivity(getBaseContext(), PRIVATE_REQUEST_CODE_ERROR, intent, 0);
// return PendingIntent to be executed by client
Bundle result = new Bundle();
result.putInt(OpenPgpConstants.RESULT_CODE, OpenPgpConstants.RESULT_CODE_USER_INTERACTION_REQUIRED);
result.putParcelable(OpenPgpConstants.RESULT_INTENT, pi);
return result;
} }
} }
@ -160,38 +126,6 @@ public abstract class RemoteService extends Service {
return packageSignature; return packageSignature;
} }
/**
* Locks current thread and pauses execution of runnables and starts activity for user input
*
* @param action
* @param messenger
* @param extras
*/
protected void pauseAndStartUserInteraction(String action, BaseCallback callback, Bundle extras) {
synchronized (userInputLock) {
mThreadPool.pause();
Log.d(Constants.TAG, "starting activity...");
Intent intent = new Intent(getBaseContext(), RemoteServiceActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setAction(action);
Messenger messenger = new Messenger(new Handler(getMainLooper(), callback));
extras.putParcelable(RemoteServiceActivity.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);
}
}
}
/** /**
* Retrieves AppSettings from database for the application calling this remote service * Retrieves AppSettings from database for the application calling this remote service
* *
@ -215,66 +149,11 @@ public abstract class RemoteService extends Service {
return null; return null;
} }
class RegisterActivityCallback extends BaseCallback {
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
try {
if (isPackageAllowed(packageName)) {
synchronized (userInputLock) {
userInputLock.notifyAll();
}
mThreadPool.resume();
} else {
// Should not happen!
Log.e(Constants.TAG, "Should not happen! Emergency shutdown!");
mThreadPool.shutdownNow();
}
} catch (WrongPackageSignatureException e) {
Log.e(Constants.TAG, e.getMessage());
Bundle extras = new Bundle();
extras.putString(RemoteServiceActivity.EXTRA_ERROR_MESSAGE,
getString(R.string.api_error_wrong_signature));
pauseAndStartUserInteraction(RemoteServiceActivity.ACTION_ERROR_MESSAGE, null,
extras);
}
} 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 * 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. * process) is in the list of allowed package names.
* *
* @param allowOnlySelf * @param allowOnlySelf allow only Keychain app itself
* allow only Keychain app itself
* @return true if process is allowed to use this service * @return true if process is allowed to use this service
* @throws WrongPackageSignatureException * @throws WrongPackageSignatureException
*/ */

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de> * Copyright (C) 2013-2014 Dominik Schürmann <dominik@dominikschuermann.de>
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -17,8 +17,15 @@
package org.sufficientlysecure.keychain.service.remote; package org.sufficientlysecure.keychain.service.remote;
import java.util.ArrayList; import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.os.Messenger;
import android.support.v7.app.ActionBarActivity;
import android.view.View;
import org.openintents.openpgp.util.OpenPgpConstants;
import org.sufficientlysecure.htmltextview.HtmlTextView; import org.sufficientlysecure.htmltextview.HtmlTextView;
import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.Id; import org.sufficientlysecure.keychain.Id;
@ -30,15 +37,7 @@ import org.sufficientlysecure.keychain.ui.SelectPublicKeyFragment;
import org.sufficientlysecure.keychain.ui.dialog.PassphraseDialogFragment; import org.sufficientlysecure.keychain.ui.dialog.PassphraseDialogFragment;
import org.sufficientlysecure.keychain.util.Log; import org.sufficientlysecure.keychain.util.Log;
import android.content.Intent; import java.util.ArrayList;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.os.Messenger;
import android.os.RemoteException;
import android.support.v7.app.ActionBarActivity;
import android.view.View;
import android.widget.Toast;
public class RemoteServiceActivity extends ActionBarActivity { public class RemoteServiceActivity extends ActionBarActivity {
@ -64,17 +63,11 @@ public class RemoteServiceActivity extends ActionBarActivity {
// error message // error message
public static final String EXTRA_ERROR_MESSAGE = "error_message"; public static final String EXTRA_ERROR_MESSAGE = "error_message";
private Messenger mMessenger;
// register view // register view
private AppSettingsFragment mSettingsFragment; private AppSettingsFragment mSettingsFragment;
// select pub keys view // select pub keys view
private SelectPublicKeyFragment mSelectFragment; 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 @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
@ -82,36 +75,12 @@ public class RemoteServiceActivity extends ActionBarActivity {
handleActions(getIntent(), savedInstanceState); handleActions(getIntent(), savedInstanceState);
} }
@Override
protected void onStop() {
super.onStop();
if (!finishHandled) {
Message msg = Message.obtain();
msg.arg1 = RemoteService.RegisterActivityCallback.CANCEL;
try {
mMessenger.send(msg);
} catch (RemoteException e) {
Log.e(Constants.TAG, "CryptoServiceActivity", e);
}
}
}
protected void handleActions(Intent intent, Bundle savedInstanceState) { protected void handleActions(Intent intent, Bundle savedInstanceState) {
finishHandled = false;
String action = intent.getAction(); String action = intent.getAction();
Bundle extras = intent.getExtras(); final Bundle extras = intent.getExtras();
if (extras == null) {
extras = new Bundle();
}
mMessenger = extras.getParcelable(EXTRA_MESSENGER);
/**
* com.android.crypto actions
*/
if (ACTION_REGISTER.equals(action)) { if (ACTION_REGISTER.equals(action)) {
final String packageName = extras.getString(EXTRA_PACKAGE_NAME); final String packageName = extras.getString(EXTRA_PACKAGE_NAME);
final byte[] packageSignature = extras.getByteArray(EXTRA_PACKAGE_SIGNATURE); final byte[] packageSignature = extras.getByteArray(EXTRA_PACKAGE_SIGNATURE);
@ -131,37 +100,21 @@ public class RemoteServiceActivity extends ActionBarActivity {
ProviderHelper.insertApiApp(RemoteServiceActivity.this, ProviderHelper.insertApiApp(RemoteServiceActivity.this,
mSettingsFragment.getAppSettings()); mSettingsFragment.getAppSettings());
Message msg = Message.obtain(); // give params through for new service call
msg.arg1 = RemoteService.RegisterActivityCallback.OKAY; Bundle oldParams = extras.getBundle(OpenPgpConstants.PI_RESULT_PARAMS);
Bundle data = new Bundle();
data.putString(RemoteService.RegisterActivityCallback.PACKAGE_NAME,
packageName);
msg.setData(data);
try {
mMessenger.send(msg);
} catch (RemoteException e) {
Log.e(Constants.TAG, "CryptoServiceActivity", e);
}
finishHandled = true; Intent finishIntent = new Intent();
finish(); finishIntent.putExtra(OpenPgpConstants.PI_RESULT_PARAMS, oldParams);
RemoteServiceActivity.this.setResult(RESULT_OK, finishIntent);
RemoteServiceActivity.this.finish();
} }
} }
}, R.string.api_register_disallow, new View.OnClickListener() { }, R.string.api_register_disallow, new View.OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
// Disallow // Disallow
RemoteServiceActivity.this.setResult(RESULT_CANCELED);
Message msg = Message.obtain(); RemoteServiceActivity.this.finish();
msg.arg1 = RemoteService.RegisterActivityCallback.CANCEL;
try {
mMessenger.send(msg);
} catch (RemoteException e) {
Log.e(Constants.TAG, "CryptoServiceActivity", e);
}
finishHandled = true;
finish();
} }
} }
); );
@ -175,8 +128,9 @@ public class RemoteServiceActivity extends ActionBarActivity {
mSettingsFragment.setAppSettings(settings); mSettingsFragment.setAppSettings(settings);
} else if (ACTION_CACHE_PASSPHRASE.equals(action)) { } else if (ACTION_CACHE_PASSPHRASE.equals(action)) {
long secretKeyId = extras.getLong(EXTRA_SECRET_KEY_ID); long secretKeyId = extras.getLong(EXTRA_SECRET_KEY_ID);
Bundle oldParams = extras.getBundle(OpenPgpConstants.PI_RESULT_PARAMS);
showPassphraseDialog(secretKeyId); showPassphraseDialog(oldParams, secretKeyId);
} else if (ACTION_SELECT_PUB_KEYS.equals(action)) { } else if (ACTION_SELECT_PUB_KEYS.equals(action)) {
long[] selectedMasterKeyIds = intent.getLongArrayExtra(EXTRA_SELECTED_MASTER_KEY_IDS); long[] selectedMasterKeyIds = intent.getLongArrayExtra(EXTRA_SELECTED_MASTER_KEY_IDS);
ArrayList<String> missingUserIds = intent ArrayList<String> missingUserIds = intent
@ -184,8 +138,8 @@ public class RemoteServiceActivity extends ActionBarActivity {
ArrayList<String> dublicateUserIds = intent ArrayList<String> dublicateUserIds = intent
.getStringArrayListExtra(EXTRA_DUBLICATE_USER_IDS); .getStringArrayListExtra(EXTRA_DUBLICATE_USER_IDS);
String text = new String(); // TODO: do this with spannable instead of HTML to prevent parsing failures with weird user ids
text += "<b>" + getString(R.string.api_select_pub_keys_text) + "</b>"; String text = "<b>" + getString(R.string.api_select_pub_keys_text) + "</b>";
text += "<br/><br/>"; text += "<br/><br/>";
if (missingUserIds != null && missingUserIds.size() > 0) { if (missingUserIds != null && missingUserIds.size() > 0) {
text += getString(R.string.api_select_pub_keys_missing_text); text += getString(R.string.api_select_pub_keys_missing_text);
@ -212,40 +166,22 @@ public class RemoteServiceActivity extends ActionBarActivity {
new View.OnClickListener() { new View.OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
// ok // add key ids to params Bundle for new request
Bundle params = extras.getBundle(OpenPgpConstants.PI_RESULT_PARAMS);
Message msg = Message.obtain(); params.putLongArray(OpenPgpConstants.PARAMS_KEY_IDS,
msg.arg1 = OpenPgpService.SelectPubKeysActivityCallback.OKAY;
Bundle data = new Bundle();
data.putLongArray(
OpenPgpService.SelectPubKeysActivityCallback.PUB_KEY_IDS,
mSelectFragment.getSelectedMasterKeyIds()); mSelectFragment.getSelectedMasterKeyIds());
msg.setData(data);
try {
mMessenger.send(msg);
} catch (RemoteException e) {
Log.e(Constants.TAG, "CryptoServiceActivity", e);
}
finishHandled = true; Intent finishIntent = new Intent();
finish(); finishIntent.putExtra(OpenPgpConstants.PI_RESULT_PARAMS, params);
RemoteServiceActivity.this.setResult(RESULT_OK, finishIntent);
RemoteServiceActivity.this.finish();
} }
}, R.string.btn_do_not_save, new View.OnClickListener() { }, R.string.btn_do_not_save, new View.OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
// cancel // cancel
RemoteServiceActivity.this.setResult(RESULT_CANCELED);
Message msg = Message.obtain(); RemoteServiceActivity.this.finish();
msg.arg1 = OpenPgpService.SelectPubKeysActivityCallback.CANCEL;
;
try {
mMessenger.send(msg);
} catch (RemoteException e) {
Log.e(Constants.TAG, "CryptoServiceActivity", e);
}
finishHandled = true;
finish();
} }
} }
); );
@ -278,8 +214,7 @@ public class RemoteServiceActivity extends ActionBarActivity {
} else if (ACTION_ERROR_MESSAGE.equals(action)) { } else if (ACTION_ERROR_MESSAGE.equals(action)) {
String errorMessage = intent.getStringExtra(EXTRA_ERROR_MESSAGE); String errorMessage = intent.getStringExtra(EXTRA_ERROR_MESSAGE);
String text = new String(); String text = "<font color=\"red\">" + errorMessage + "</font>";
text += "<font color=\"red\">" + errorMessage + "</font>";
// Inflate a "Done" custom action bar view // Inflate a "Done" custom action bar view
ActionBarHelper.setDoneView(getSupportActionBar(), R.string.btn_okay, ActionBarHelper.setDoneView(getSupportActionBar(), R.string.btn_okay,
@ -287,7 +222,8 @@ public class RemoteServiceActivity extends ActionBarActivity {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
finish(); RemoteServiceActivity.this.setResult(RESULT_OK);
RemoteServiceActivity.this.finish();
} }
}); });
@ -297,7 +233,8 @@ public class RemoteServiceActivity extends ActionBarActivity {
HtmlTextView textView = (HtmlTextView) findViewById(R.id.api_app_error_message_text); HtmlTextView textView = (HtmlTextView) findViewById(R.id.api_app_error_message_text);
textView.setHtmlFromString(text); textView.setHtmlFromString(text);
} else { } else {
Log.e(Constants.TAG, "Wrong action!"); Log.e(Constants.TAG, "Action does not exist!");
setResult(RESULT_CANCELED);
finish(); finish();
} }
} }
@ -307,31 +244,21 @@ public class RemoteServiceActivity extends ActionBarActivity {
* encryption. Based on mSecretKeyId it asks for a passphrase to open a private key or it asks * encryption. Based on mSecretKeyId it asks for a passphrase to open a private key or it asks
* for a symmetric passphrase * for a symmetric passphrase
*/ */
private void showPassphraseDialog(long secretKeyId) { private void showPassphraseDialog(final Bundle params, long secretKeyId) {
// Message is received after passphrase is cached // Message is received after passphrase is cached
Handler returnHandler = new Handler() { Handler returnHandler = new Handler() {
@Override @Override
public void handleMessage(Message message) { public void handleMessage(Message message) {
if (message.what == PassphraseDialogFragment.MESSAGE_OKAY) { if (message.what == PassphraseDialogFragment.MESSAGE_OKAY) {
Message msg = Message.obtain(); // return given params again, for calling the service method again
msg.arg1 = OpenPgpService.PassphraseActivityCallback.OKAY; Intent finishIntent = new Intent();
try { finishIntent.putExtra(OpenPgpConstants.PI_RESULT_PARAMS, params);
mMessenger.send(msg); RemoteServiceActivity.this.setResult(RESULT_OK, finishIntent);
} catch (RemoteException e) {
Log.e(Constants.TAG, "CryptoServiceActivity", e);
}
} else { } else {
Message msg = Message.obtain(); RemoteServiceActivity.this.setResult(RESULT_CANCELED);
msg.arg1 = OpenPgpService.PassphraseActivityCallback.CANCEL;
try {
mMessenger.send(msg);
} catch (RemoteException e) {
Log.e(Constants.TAG, "CryptoServiceActivity", e);
}
} }
finishHandled = true; RemoteServiceActivity.this.finish();
finish();
} }
}; };
@ -344,9 +271,12 @@ public class RemoteServiceActivity extends ActionBarActivity {
passphraseDialog.show(getSupportFragmentManager(), "passphraseDialog"); passphraseDialog.show(getSupportFragmentManager(), "passphraseDialog");
} catch (PgpGeneralException e) { } catch (PgpGeneralException e) {
Log.d(Constants.TAG, "No passphrase for this secret key, encrypt directly!"); Log.d(Constants.TAG, "No passphrase for this secret key, do pgp operation directly!");
// send message to handler to start encryption directly // return given params again, for calling the service method again
returnHandler.sendEmptyMessage(PassphraseDialogFragment.MESSAGE_OKAY); Intent finishIntent = new Intent();
finishIntent.putExtra(OpenPgpConstants.PI_RESULT_PARAMS, params);
setResult(RESULT_OK, finishIntent);
finish();
} }
} }
} }

View File

@ -1,4 +1,4 @@
package org.sufficientlysecure.keychain.service.exception; package org.sufficientlysecure.keychain.service.remote;
public class WrongPackageSignatureException extends Exception { public class WrongPackageSignatureException extends Exception {

View File

@ -300,7 +300,7 @@ public class DecryptActivity extends DrawerActivity {
String sharedText = intent.getStringExtra(Intent.EXTRA_TEXT); String sharedText = intent.getStringExtra(Intent.EXTRA_TEXT);
if (sharedText != null) { if (sharedText != null) {
// handle like normal text decryption, override action and extras to later // handle like normal text decryption, override action and extras to later
// execute ACTION_DECRYPT in main actions // executeServiceMethod ACTION_DECRYPT in main actions
extras.putString(EXTRA_TEXT, sharedText); extras.putString(EXTRA_TEXT, sharedText);
action = ACTION_DECRYPT; action = ACTION_DECRYPT;
} }

View File

@ -173,7 +173,7 @@ public class EncryptActivity extends DrawerActivity {
String sharedText = intent.getStringExtra(Intent.EXTRA_TEXT); String sharedText = intent.getStringExtra(Intent.EXTRA_TEXT);
if (sharedText != null) { if (sharedText != null) {
// handle like normal text encryption, override action and extras to later // handle like normal text encryption, override action and extras to later
// execute ACTION_ENCRYPT in main actions // executeServiceMethod ACTION_ENCRYPT in main actions
extras.putString(EXTRA_TEXT, sharedText); extras.putString(EXTRA_TEXT, sharedText);
extras.putBoolean(EXTRA_ASCII_ARMOR, true); extras.putBoolean(EXTRA_ASCII_ARMOR, true);
action = ACTION_ENCRYPT; action = ACTION_ENCRYPT;

View File

@ -57,6 +57,7 @@ public class ImportKeysActivity extends DrawerActivity implements ActionBar.OnNa
+ "IMPORT_KEY_FROM_QR_CODE"; + "IMPORT_KEY_FROM_QR_CODE";
public static final String ACTION_IMPORT_KEY_FROM_KEYSERVER = Constants.INTENT_PREFIX public static final String ACTION_IMPORT_KEY_FROM_KEYSERVER = Constants.INTENT_PREFIX
+ "IMPORT_KEY_FROM_KEYSERVER"; + "IMPORT_KEY_FROM_KEYSERVER";
// TODO: implement:
public static final String ACTION_IMPORT_KEY_FROM_KEYSERVER_AND_RETURN = Constants.INTENT_PREFIX public static final String ACTION_IMPORT_KEY_FROM_KEYSERVER_AND_RETURN = Constants.INTENT_PREFIX
+ "IMPORT_KEY_FROM_KEY_SERVER_AND_RETURN"; + "IMPORT_KEY_FROM_KEY_SERVER_AND_RETURN";

View File

@ -295,7 +295,7 @@
<string name="progress_saving">saving…</string> <string name="progress_saving">saving…</string>
<string name="progress_importing">importing…</string> <string name="progress_importing">importing…</string>
<string name="progress_exporting">exporting…</string> <string name="progress_exporting">exporting…</string>
<string name="progress_generating">generating key, this can take a while</string> <string name="progress_generating">generating key, this can take up to 3 minutes</string>
<string name="progress_building_key">building key…</string> <string name="progress_building_key">building key…</string>
<string name="progress_preparing_master_key">preparing master key…</string> <string name="progress_preparing_master_key">preparing master key…</string>
<string name="progress_certifying_master_key">certifying master key…</string> <string name="progress_certifying_master_key">certifying master key…</string>
@ -383,7 +383,7 @@
<string name="api_settings_revoke">Revoke access</string> <string name="api_settings_revoke">Revoke access</string>
<string name="api_settings_package_name">Package Name</string> <string name="api_settings_package_name">Package Name</string>
<string name="api_settings_package_signature">SHA-256 of Package Signature</string> <string name="api_settings_package_signature">SHA-256 of Package Signature</string>
<string name="api_register_text">The following application requests access to OpenPGP Keychain.\n\nAllow permanent access?</string> <string name="api_register_text">The displayed application requests access to OpenPGP Keychain.\nAllow access?\n\nWARNING: If you do not know why this screen appeared, disallow access! You can revoke access later using the \'Registered Applications\' screen.</string>
<string name="api_register_allow">Allow access</string> <string name="api_register_allow">Allow access</string>
<string name="api_register_disallow">Disallow access</string> <string name="api_register_disallow">Disallow access</string>
<string name="api_register_error_select_key">Please select a key!</string> <string name="api_register_error_select_key">Please select a key!</string>

View File

@ -87,8 +87,7 @@ To do automatic encryption/decryption/sign/verify use the OpenPGP Remote API.
* NFC (``android.nfc.action.NDEF_DISCOVERED``) on mime type ``application/pgp-keys`` (as specified in http://tools.ietf.org/html/rfc3156, section 7) * NFC (``android.nfc.action.NDEF_DISCOVERED``) on mime type ``application/pgp-keys`` (as specified in http://tools.ietf.org/html/rfc3156, section 7)
### OpenPGP Remote API ### OpenPGP Remote API
To do asyncronous fast encryption/decryption/sign/verify operations bind to the OpenPGP remote service. To do fast encryption/decryption/sign/verify operations without user interaction bind to the OpenPGP remote service.
The API Demo contains all required AIDL files and a demo activity.
#### Try out the API #### Try out the API
Keychain: https://play.google.com/store/apps/details?id=org.sufficientlysecure.keychain Keychain: https://play.google.com/store/apps/details?id=org.sufficientlysecure.keychain
@ -99,45 +98,25 @@ All apps wanting to use this generic API
just need to include the AIDL files and connect to the service. Other just need to include the AIDL files and connect to the service. Other
OpenPGP apps can implement a service based on this AIDL definition. OpenPGP apps can implement a service based on this AIDL definition.
The API is designed to be as easy as possible to use by apps like The API is designed to be as easy as possible to use by apps like K-9 Mail.
K-9 Mail. The service definition defines The service definition defines sign, encrypt, signAndEncrypt, decryptAndVerify, and getKeyIds.
sign/encrypt/signAndEncrypt/decryptAndVerify [1].
As can be seen the apps themselves never need handle key ids directly. As can be seen in the API Demo, the apps themselves never need to handle key ids directly.
Only user ids (emails) are used to define recipients. If more than one You can use user ids (emails) to define recipients.
pub key exists for an email, OpenPGP Keychain will handle the problem by If more than one public key exists for an email, OpenPGP Keychain will handle the problem by showing a selection screen. Additionally, it is also possible to use key ids.
showing a selection screen.
Also app devs never need to fiddle with private keys. On first Also app devs never need to fiddle with private keys.
operation, OpenPGP Keychain shows an activity to allow or disallow On first operation, OpenPGP Keychain shows an activity to allow or disallow access, while also allowing to choose the private key used for this app.
access, while also allowing to choose the private key used for this app. Please try the Demo app out to see how it works.
Please try the Demo app out to see how it works [4].
#### Integration #### Integration
The API is defined as AIDL interfaces in org.openintents.openpgp packge Copy the api library from "libraries/keychain-api-library" to your project and add it as an dependency to your gradle build.
[2]. All files from [2] needs to be included in the project. Inspect the ode found in "OpenPGP-Keychain-API" to understand how to use the API.
Using the OpenPgpServiceConnection.java [3] you can choose to which
OpenPGP provider you want to connect (other pgp apps can implement the
interfaces). They can be queried as shown in the demo app (see [3] how
to query). If other OpenPGP apps implement the service, no additional
code is required in k9mail per provider. See [3] for a complete example
for integration.
[1] https://github.com/openpgp-keychain/openpgp-keychain/blob/master/OpenPGP-Keychain-API-Demo/src/org/openintents/openpgp/IOpenPgpService.aidl
[2] https://github.com/openpgp-keychain/openpgp-keychain/tree/master/OpenPGP-Keychain-API-Demo/src/org/openintents/openpgp
[3] https://github.com/openpgp-keychain/openpgp-keychain/blob/master/OpenPGP-Keychain-API-Demo/src/org/openintents/openpgp/OpenPgpServiceConnection.java
[3] https://github.com/openpgp-keychain/openpgp-keychain/blob/master/OpenPGP-Keychain-API-Demo/src/org/sufficientlysecure/keychain/demo/OpenPgpProviderActivity.java
[4] https://play.google.com/store/apps/details?id=org.sufficientlysecure.keychain.demo
## Extended Remote API
TODO
## Libraries ## Libraries
### ZXing Barcode Scanner Android Integration ### ZXing Barcode Scanner Android Integration
Classes can be found under "libraries/zxing-android-integration/". Classes can be found under "libraries/zxing-android-integration/".

View File

@ -4,7 +4,7 @@ buildscript {
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:0.8.0' classpath 'com.android.tools.build:gradle:0.8.3'
} }
} }

View File

@ -5,7 +5,7 @@
android:versionName="1.0" > android:versionName="1.0" >
<uses-sdk <uses-sdk
android:minSdkVersion="8" android:minSdkVersion="9"
android:targetSdkVersion="19" /> android:targetSdkVersion="19" />
<application/> <application/>

View File

@ -1,10 +1,33 @@
apply plugin: 'android-library' // please leave this here, so this library builds on its own
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:0.8.3'
}
}
apply plugin: 'android-library'
android { android {
compileSdkVersion 19 compileSdkVersion 19
buildToolsVersion '19.0.1' buildToolsVersion '19.0.1'
// NOTE: We are using the old folder structure to also support Eclipse
sourceSets {
main {
manifest.srcFile 'AndroidManifest.xml'
java.srcDirs = ['src']
resources.srcDirs = ['src']
aidl.srcDirs = ['src']
renderscript.srcDirs = ['src']
res.srcDirs = ['res']
assets.srcDirs = ['assets']
}
}
// Do not abort build if lint finds errors // Do not abort build if lint finds errors
lintOptions { lintOptions {
abortOnError false abortOnError false

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="openpgp_list_preference_none">None</string>
</resources>

View File

@ -1,45 +0,0 @@
/*
* Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openintents.openpgp;
import org.openintents.openpgp.OpenPgpData;
import org.openintents.openpgp.OpenPgpSignatureResult;
import org.openintents.openpgp.OpenPgpError;
interface IOpenPgpCallback {
/**
* onSuccess returns on successful OpenPGP operations.
*
* @param output
* contains resulting output (decrypted content (when input was encrypted)
* or content without signature (when input was signed-only))
* @param signatureResult
* signatureResult is only non-null if decryptAndVerify() was called and the content
* was encrypted or signed-and-encrypted.
*/
oneway void onSuccess(in OpenPgpData output, in OpenPgpSignatureResult signatureResult);
/**
* onError returns on errors or when allowUserInteraction was set to false, but user interaction
* was required execute an OpenPGP operation.
*
* @param error
* See OpenPgpError class for more information.
*/
oneway void onError(in OpenPgpError error);
}

View File

@ -1,39 +0,0 @@
/*
* Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openintents.openpgp;
import org.openintents.openpgp.OpenPgpError;
interface IOpenPgpKeyIdsCallback {
/**
* onSuccess returns on successful getKeyIds operations.
*
* @param keyIds
* returned key ids
*/
oneway void onSuccess(in long[] keyIds);
/**
* onError returns on errors or when allowUserInteraction was set to false, but user interaction
* was required execute an OpenPGP operation.
*
* @param error
* See OpenPgpError class for more information.
*/
oneway void onError(in OpenPgpError error);
}

View File

@ -1,143 +0,0 @@
/*
* Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openintents.openpgp;
import org.openintents.openpgp.OpenPgpData;
import org.openintents.openpgp.IOpenPgpCallback;
import org.openintents.openpgp.IOpenPgpKeyIdsCallback;
/**
* All methods are oneway, which means they are asynchronous and non-blocking.
* Results are returned to the callback, which has to be implemented on client side.
*/
interface IOpenPgpService {
/**
* Sign
*
* After successful signing, callback's onSuccess will contain the resulting output.
*
* @param input
* OpenPgpData object containing String, byte[], ParcelFileDescriptor, or Uri
* @param output
* Request output format by defining OpenPgpData object
*
* new OpenPgpData(OpenPgpData.TYPE_STRING)
* Returns as String
* (OpenPGP Radix-64, 33 percent overhead compared to binary, see http://tools.ietf.org/html/rfc4880#page-53)
* new OpenPgpData(OpenPgpData.TYPE_BYTE_ARRAY)
* Returns as byte[]
* new OpenPgpData(uri)
* Writes output to given Uri
* new OpenPgpData(fileDescriptor)
* Writes output to given ParcelFileDescriptor
* @param callback
* Callback where to return results
*/
oneway void sign(in OpenPgpData input, in OpenPgpData output, in IOpenPgpCallback callback);
/**
* Encrypt
*
* After successful encryption, callback's onSuccess will contain the resulting output.
*
* @param input
* OpenPgpData object containing String, byte[], ParcelFileDescriptor, or Uri
* @param output
* Request output format by defining OpenPgpData object
*
* new OpenPgpData(OpenPgpData.TYPE_STRING)
* Returns as String
* (OpenPGP Radix-64, 33 percent overhead compared to binary, see http://tools.ietf.org/html/rfc4880#page-53)
* new OpenPgpData(OpenPgpData.TYPE_BYTE_ARRAY)
* Returns as byte[]
* new OpenPgpData(uri)
* Writes output to given Uri
* new OpenPgpData(fileDescriptor)
* Writes output to given ParcelFileDescriptor
* @param keyIds
* Key Ids of recipients. Can be retrieved with getKeyIds()
* @param callback
* Callback where to return results
*/
oneway void encrypt(in OpenPgpData input, in OpenPgpData output, in long[] keyIds, in IOpenPgpCallback callback);
/**
* Sign then encrypt
*
* After successful signing and encryption, callback's onSuccess will contain the resulting output.
*
* @param input
* OpenPgpData object containing String, byte[], ParcelFileDescriptor, or Uri
* @param output
* Request output format by defining OpenPgpData object
*
* new OpenPgpData(OpenPgpData.TYPE_STRING)
* Returns as String
* (OpenPGP Radix-64, 33 percent overhead compared to binary, see http://tools.ietf.org/html/rfc4880#page-53)
* new OpenPgpData(OpenPgpData.TYPE_BYTE_ARRAY)
* Returns as byte[]
* new OpenPgpData(uri)
* Writes output to given Uri
* new OpenPgpData(fileDescriptor)
* Writes output to given ParcelFileDescriptor
* @param keyIds
* Key Ids of recipients. Can be retrieved with getKeyIds()
* @param callback
* Callback where to return results
*/
oneway void signAndEncrypt(in OpenPgpData input, in OpenPgpData output, in long[] keyIds, in IOpenPgpCallback callback);
/**
* Decrypts and verifies given input bytes. This methods handles encrypted-only, signed-and-encrypted,
* and also signed-only input.
*
* After successful decryption/verification, callback's onSuccess will contain the resulting output.
* The signatureResult in onSuccess is only non-null if signed-and-encrypted or signed-only inputBytes were given.
*
* @param input
* OpenPgpData object containing String, byte[], ParcelFileDescriptor, or Uri
* @param output
* Request output format by defining OpenPgpData object
*
* new OpenPgpData(OpenPgpData.TYPE_STRING)
* Returns as String
* (OpenPGP Radix-64, 33 percent overhead compared to binary, see http://tools.ietf.org/html/rfc4880#page-53)
* new OpenPgpData(OpenPgpData.TYPE_BYTE_ARRAY)
* Returns as byte[]
* new OpenPgpData(uri)
* Writes output to given Uri
* new OpenPgpData(fileDescriptor)
* Writes output to given ParcelFileDescriptor
* @param callback
* Callback where to return results
*/
oneway void decryptAndVerify(in OpenPgpData input, in OpenPgpData output, in IOpenPgpCallback callback);
/**
* Get available key ids based on given user ids
*
* @param ids
* User Ids (emails) of recipients OR key ids
* @param allowUserInteraction
* Enable user interaction to lookup and import unknown keys
* @param callback
* Callback where to return results (different type than callback in other functions!)
*/
oneway void getKeyIds(in String[] ids, in boolean allowUserInteraction, in IOpenPgpKeyIdsCallback callback);
}

View File

@ -1,20 +0,0 @@
/*
* Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openintents.openpgp;
// Declare OpenPgpData so AIDL can find it and knows that it implements the parcelable protocol.
parcelable OpenPgpData;

View File

@ -1,20 +0,0 @@
/*
* Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openintents.openpgp;
// Declare OpenPgpError so AIDL can find it and knows that it implements the parcelable protocol.
parcelable OpenPgpError;

View File

@ -1,20 +0,0 @@
/*
* Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openintents.openpgp;
// Declare OpenPgpSignatureResult so AIDL can find it and knows that it implements the parcelable protocol.
parcelable OpenPgpSignatureResult;

View File

@ -1,24 +0,0 @@
/*
* Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sufficientlysecure.keychain.service.remote;
interface IExtendedApiCallback {
oneway void onSuccess(in byte[] outputBytes);
oneway void onError(in String error);
}

View File

@ -1,48 +0,0 @@
/*
* Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sufficientlysecure.keychain.service.remote;
import org.sufficientlysecure.keychain.service.remote.IExtendedApiCallback;
/**
* All methods are oneway, which means they are asynchronous and non-blocking.
* Results are returned to the callback, which has to be implemented on client side.
*/
interface IExtendedApiService {
/**
* Symmetric Encrypt
*
* @param inputBytes
* Byte array you want to encrypt
* @param passphrase
* symmetric passhprase
* @param callback
* Callback where to return results
*/
oneway void encrypt(in byte[] inputBytes, in String passphrase, in IExtendedApiCallback callback);
/**
* Generates self signed X509 certificate signed by OpenPGP private key (from app settings)
*
* @param subjAltNameURI
* @param callback
* Callback where to return results
*/
oneway void selfSignedX509Cert(in String subjAltNameURI, in IExtendedApiCallback callback);
}

View File

@ -1,10 +0,0 @@
package org.openintents.openpgp;
public class OpenPgpConstants {
public static final String TAG = "OpenPgp API";
public static final int REQUIRED_API_VERSION = 1;
public static final String SERVICE_INTENT = "org.openintents.openpgp.IOpenPgpService";
}

View File

@ -1,127 +0,0 @@
/*
* Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openintents.openpgp;
import android.net.Uri;
import android.os.Parcel;
import android.os.ParcelFileDescriptor;
import android.os.Parcelable;
public class OpenPgpData implements Parcelable {
public static final int TYPE_STRING = 0;
public static final int TYPE_BYTE_ARRAY = 1;
public static final int TYPE_FILE_DESCRIPTOR = 2;
public static final int TYPE_URI = 3;
int type;
String string;
byte[] bytes = new byte[0];
ParcelFileDescriptor fileDescriptor;
Uri uri;
public int getType() {
return type;
}
public String getString() {
return string;
}
public byte[] getBytes() {
return bytes;
}
public ParcelFileDescriptor getFileDescriptor() {
return fileDescriptor;
}
public Uri getUri() {
return uri;
}
public OpenPgpData() {
}
/**
* Not a real constructor. This can be used to define requested output type.
*
* @param type
*/
public OpenPgpData(int type) {
this.type = type;
}
public OpenPgpData(String string) {
this.string = string;
this.type = TYPE_STRING;
}
public OpenPgpData(byte[] bytes) {
this.bytes = bytes;
this.type = TYPE_BYTE_ARRAY;
}
public OpenPgpData(ParcelFileDescriptor fileDescriptor) {
this.fileDescriptor = fileDescriptor;
this.type = TYPE_FILE_DESCRIPTOR;
}
public OpenPgpData(Uri uri) {
this.uri = uri;
this.type = TYPE_URI;
}
public OpenPgpData(OpenPgpData b) {
this.string = b.string;
this.bytes = b.bytes;
this.fileDescriptor = b.fileDescriptor;
this.uri = b.uri;
}
public int describeContents() {
return 0;
}
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(type);
dest.writeString(string);
dest.writeInt(bytes.length);
dest.writeByteArray(bytes);
dest.writeParcelable(fileDescriptor, 0);
dest.writeParcelable(uri, 0);
}
public static final Creator<OpenPgpData> CREATOR = new Creator<OpenPgpData>() {
public OpenPgpData createFromParcel(final Parcel source) {
OpenPgpData vr = new OpenPgpData();
vr.type = source.readInt();
vr.string = source.readString();
vr.bytes = new byte[source.readInt()];
source.readByteArray(vr.bytes);
vr.fileDescriptor = source.readParcelable(ParcelFileDescriptor.class.getClassLoader());
vr.fileDescriptor = source.readParcelable(Uri.class.getClassLoader());
return vr;
}
public OpenPgpData[] newArray(final int size) {
return new OpenPgpData[size];
}
};
}

View File

@ -1,52 +0,0 @@
/*
* Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openintents.openpgp;
import java.util.List;
import java.util.regex.Pattern;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ResolveInfo;
public class OpenPgpHelper {
private Context context;
public static Pattern PGP_MESSAGE = Pattern.compile(
".*?(-----BEGIN PGP MESSAGE-----.*?-----END PGP MESSAGE-----).*", Pattern.DOTALL);
public static Pattern PGP_SIGNED_MESSAGE = Pattern
.compile(
".*?(-----BEGIN PGP SIGNED MESSAGE-----.*?-----BEGIN PGP SIGNATURE-----.*?-----END PGP SIGNATURE-----).*",
Pattern.DOTALL);
public OpenPgpHelper(Context context) {
super();
this.context = context;
}
public boolean isAvailable() {
Intent intent = new Intent(OpenPgpConstants.SERVICE_INTENT);
List<ResolveInfo> resInfo = context.getPackageManager().queryIntentServices(intent, 0);
if (!resInfo.isEmpty()) {
return true;
} else {
return false;
}
}
}

View File

@ -0,0 +1,85 @@
/*
* Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openintents.openpgp;
interface IOpenPgpService {
/**
* General extras
* --------------
*
* Bundle params:
* int api_version (required)
* boolean ascii_armor (request ascii armor for ouput)
*
* returned Bundle:
* int result_code (0, 1, or 2 (see OpenPgpConstants))
* OpenPgpError error (if result_code == 0)
* Intent intent (if result_code == 2)
*
*/
/**
* Sign only
*
* optional params:
* String passphrase (for key passphrase)
*/
Bundle sign(in Bundle params, in ParcelFileDescriptor input, in ParcelFileDescriptor output);
/**
* Encrypt
*
* Bundle params:
* long[] key_ids
* or
* String[] user_ids (= emails of recipients) (if more than one key has this user_id, a PendingIntent is returned)
*
* optional params:
* String passphrase (for key passphrase)
*/
Bundle encrypt(in Bundle params, in ParcelFileDescriptor input, in ParcelFileDescriptor output);
/**
* Sign and encrypt
*
* Bundle params:
* same as in encrypt()
*/
Bundle signAndEncrypt(in Bundle params, in ParcelFileDescriptor input, in ParcelFileDescriptor output);
/**
* Decrypts and verifies given input bytes. This methods handles encrypted-only, signed-and-encrypted,
* and also signed-only input.
*
* returned Bundle:
* OpenPgpSignatureResult signature_result
*/
Bundle decryptAndVerify(in Bundle params, in ParcelFileDescriptor input, in ParcelFileDescriptor output);
/**
* Retrieves key ids based on given user ids (=emails)
*
* Bundle params:
* String[] user_ids
*
* returned Bundle:
* long[] key_ids
*/
Bundle getKeyIds(in Bundle params);
}

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de> * Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -20,10 +20,13 @@ import android.os.Parcel;
import android.os.Parcelable; import android.os.Parcelable;
public class OpenPgpError implements Parcelable { public class OpenPgpError implements Parcelable {
public static final int CLIENT_SIDE_ERROR = -1;
public static final int GENERIC_ERROR = 0; public static final int GENERIC_ERROR = 0;
public static final int NO_OR_WRONG_PASSPHRASE = 1; public static final int INCOMPATIBLE_API_VERSIONS = 1;
public static final int NO_USER_IDS = 2;
public static final int USER_INTERACTION_REQUIRED = 3; public static final int NO_OR_WRONG_PASSPHRASE = 2;
public static final int NO_USER_IDS = 3;
int errorId; int errorId;
String message; String message;

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de> * Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -22,14 +22,14 @@ import android.os.Parcelable;
public class OpenPgpSignatureResult implements Parcelable { public class OpenPgpSignatureResult implements Parcelable {
// generic error on signature verification // generic error on signature verification
public static final int SIGNATURE_ERROR = 0; public static final int SIGNATURE_ERROR = 0;
// successfully verified signature, with trusted public key // successfully verified signature, with certified public key
public static final int SIGNATURE_SUCCESS_TRUSTED = 1; public static final int SIGNATURE_SUCCESS_CERTIFIED = 1;
// no public key was found for this signature verification // no public key was found for this signature verification
// you can retrieve the key with // you can retrieve the key with
// getKeys(new String[] {String.valueOf(signatureResult.getKeyId)}, true, callback) // getKeys(new String[] {String.valueOf(signatureResult.getKeyId)}, true, callback)
public static final int SIGNATURE_UNKNOWN_PUB_KEY = 2; public static final int SIGNATURE_UNKNOWN_PUB_KEY = 2;
// successfully verified signature, but with untrusted public key // successfully verified signature, but with certified public key
public static final int SIGNATURE_SUCCESS_UNTRUSTED = 3; public static final int SIGNATURE_SUCCESS_UNCERTIFIED = 3;
int status; int status;
boolean signatureOnly; boolean signatureOnly;

View File

@ -0,0 +1,198 @@
/*
* Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openintents.openpgp.util;
import android.content.Context;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.ParcelFileDescriptor;
import android.util.Log;
import org.openintents.openpgp.IOpenPgpService;
import org.openintents.openpgp.OpenPgpError;
import java.io.InputStream;
import java.io.OutputStream;
public class OpenPgpApi {
IOpenPgpService mService;
Context mContext;
private static final int OPERATION_SIGN = 0;
private static final int OPERATION_ENCRYPT = 1;
private static final int OPERATION_SIGN_ENCRYPT = 2;
private static final int OPERATION_DECRYPT_VERIFY = 3;
private static final int OPERATION_GET_KEY_IDS = 4;
public OpenPgpApi(Context context, IOpenPgpService service) {
this.mContext = context;
this.mService = service;
}
public Bundle sign(InputStream is, final OutputStream os) {
return executeApi(OPERATION_SIGN, new Bundle(), is, os);
}
public Bundle sign(Bundle params, InputStream is, final OutputStream os) {
return executeApi(OPERATION_SIGN, params, is, os);
}
public void sign(Bundle params, InputStream is, final OutputStream os, IOpenPgpCallback callback) {
executeApiAsync(OPERATION_SIGN, params, is, os, callback);
}
public Bundle encrypt(InputStream is, final OutputStream os) {
return executeApi(OPERATION_ENCRYPT, new Bundle(), is, os);
}
public Bundle encrypt(Bundle params, InputStream is, final OutputStream os) {
return executeApi(OPERATION_ENCRYPT, params, is, os);
}
public void encrypt(Bundle params, InputStream is, final OutputStream os, IOpenPgpCallback callback) {
executeApiAsync(OPERATION_ENCRYPT, params, is, os, callback);
}
public Bundle signAndEncrypt(InputStream is, final OutputStream os) {
return executeApi(OPERATION_SIGN_ENCRYPT, new Bundle(), is, os);
}
public Bundle signAndEncrypt(Bundle params, InputStream is, final OutputStream os) {
return executeApi(OPERATION_SIGN_ENCRYPT, params, is, os);
}
public void signAndEncrypt(Bundle params, InputStream is, final OutputStream os, IOpenPgpCallback callback) {
executeApiAsync(OPERATION_SIGN_ENCRYPT, params, is, os, callback);
}
public Bundle decryptAndVerify(InputStream is, final OutputStream os) {
return executeApi(OPERATION_DECRYPT_VERIFY, new Bundle(), is, os);
}
public Bundle decryptAndVerify(Bundle params, InputStream is, final OutputStream os) {
return executeApi(OPERATION_DECRYPT_VERIFY, params, is, os);
}
public void decryptAndVerify(Bundle params, InputStream is, final OutputStream os, IOpenPgpCallback callback) {
executeApiAsync(OPERATION_DECRYPT_VERIFY, params, is, os, callback);
}
public Bundle getKeyIds(Bundle params) {
return executeApi(OPERATION_GET_KEY_IDS, params, null, null);
}
public interface IOpenPgpCallback {
void onReturn(final Bundle result);
}
private class OpenPgpAsyncTask extends AsyncTask<Void, Integer, Bundle> {
int operationId;
Bundle params;
InputStream is;
OutputStream os;
IOpenPgpCallback callback;
private OpenPgpAsyncTask(int operationId, Bundle params, InputStream is, OutputStream os, IOpenPgpCallback callback) {
this.operationId = operationId;
this.params = params;
this.is = is;
this.os = os;
this.callback = callback;
}
@Override
protected Bundle doInBackground(Void... unused) {
return executeApi(operationId, params, is, os);
}
protected void onPostExecute(Bundle result) {
callback.onReturn(result);
}
}
private void executeApiAsync(int operationId, Bundle params, InputStream is, OutputStream os, IOpenPgpCallback callback) {
new OpenPgpAsyncTask(operationId, params, is, os, callback).execute((Void[]) null);
}
private Bundle executeApi(int operationId, Bundle params, InputStream is, OutputStream os) {
try {
params.putInt(OpenPgpConstants.PARAMS_API_VERSION, OpenPgpConstants.API_VERSION);
Bundle result = null;
if (operationId == OPERATION_GET_KEY_IDS) {
result = mService.getKeyIds(params);
return result;
} else {
// send the input and output pfds
ParcelFileDescriptor input = ParcelFileDescriptorUtil.pipeFrom(is,
new ParcelFileDescriptorUtil.IThreadListener() {
@Override
public void onThreadFinished(Thread thread) {
Log.d(OpenPgpConstants.TAG, "Copy to service finished");
}
});
ParcelFileDescriptor output = ParcelFileDescriptorUtil.pipeTo(os,
new ParcelFileDescriptorUtil.IThreadListener() {
@Override
public void onThreadFinished(Thread thread) {
Log.d(OpenPgpConstants.TAG, "Service finished writing!");
}
});
// blocks until result is ready
switch (operationId) {
case OPERATION_SIGN:
result = mService.sign(params, input, output);
break;
case OPERATION_ENCRYPT:
result = mService.encrypt(params, input, output);
break;
case OPERATION_SIGN_ENCRYPT:
result = mService.signAndEncrypt(params, input, output);
break;
case OPERATION_DECRYPT_VERIFY:
result = mService.decryptAndVerify(params, input, output);
break;
}
// close() is required to halt the TransferThread
output.close();
// set class loader to current context to allow unparcelling
// of OpenPgpError and OpenPgpSignatureResult
// http://stackoverflow.com/a/3806769
result.setClassLoader(mContext.getClassLoader());
return result;
}
} catch (Exception e) {
Log.e(OpenPgpConstants.TAG, "Exception", e);
Bundle result = new Bundle();
result.putInt(OpenPgpConstants.RESULT_CODE, OpenPgpConstants.RESULT_CODE_ERROR);
result.putParcelable(OpenPgpConstants.RESULT_ERRORS,
new OpenPgpError(OpenPgpError.CLIENT_SIDE_ERROR, e.getMessage()));
return result;
}
}
}

View File

@ -0,0 +1,54 @@
/*
* Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openintents.openpgp.util;
public class OpenPgpConstants {
public static final String TAG = "OpenPgp API";
public static final int API_VERSION = 1;
public static final String SERVICE_INTENT = "org.openintents.openpgp.IOpenPgpService";
/* Bundle params */
public static final String PARAMS_API_VERSION = "api_version";
// request ASCII Armor for output
// OpenPGP Radix-64, 33 percent overhead compared to binary, see http://tools.ietf.org/html/rfc4880#page-53)
public static final String PARAMS_REQUEST_ASCII_ARMOR = "ascii_armor";
// (for encrypt method)
public static final String PARAMS_USER_IDS = "user_ids";
public static final String PARAMS_KEY_IDS = "key_ids";
// optional parameter:
public static final String PARAMS_PASSPHRASE = "passphrase";
/* Service Bundle returns */
public static final String RESULT_CODE = "result_code";
public static final String RESULT_SIGNATURE = "signature";
public static final String RESULT_ERRORS = "error";
public static final String RESULT_INTENT = "intent";
// get actual error object from RESULT_ERRORS
public static final int RESULT_CODE_ERROR = 0;
// success!
public static final int RESULT_CODE_SUCCESS = 1;
// executeServiceMethod intent and do it again with params from intent
public static final int RESULT_CODE_USER_INTERACTION_REQUIRED = 2;
/* PendingIntent returns */
public static final String PI_RESULT_PARAMS = "params";
}

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de> * Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -14,18 +14,14 @@
* limitations under the License. * limitations under the License.
*/ */
package org.openintents.openpgp; package org.openintents.openpgp.util;
import java.util.ArrayList;
import java.util.List;
import android.app.AlertDialog.Builder; import android.app.AlertDialog.Builder;
import android.content.Context; import android.content.Context;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo; import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo; import android.content.res.TypedArray;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.preference.DialogPreference; import android.preference.DialogPreference;
import android.util.AttributeSet; import android.util.AttributeSet;
@ -35,33 +31,21 @@ import android.widget.ArrayAdapter;
import android.widget.ListAdapter; import android.widget.ListAdapter;
import android.widget.TextView; import android.widget.TextView;
import java.util.ArrayList;
import java.util.List;
import org.sufficientlysecure.keychain.api.R;
/**
* Does not extend ListPreference, but is very similar to it!
* http://grepcode.com/file_/repository.grepcode.com/java/ext/com.google.android/android/4.4_r1/android/preference/ListPreference.java/?v=source
*/
public class OpenPgpListPreference extends DialogPreference { public class OpenPgpListPreference extends DialogPreference {
ArrayList<OpenPgpProviderEntry> mProviderList = new ArrayList<OpenPgpProviderEntry>(); private ArrayList<OpenPgpProviderEntry> mProviderList = new ArrayList<OpenPgpProviderEntry>();
private String mSelectedPackage; private String mSelectedPackage;
public OpenPgpListPreference(Context context, AttributeSet attrs) { public OpenPgpListPreference(Context context, AttributeSet attrs) {
super(context, attrs); super(context, attrs);
List<ResolveInfo> resInfo = context.getPackageManager().queryIntentServices(
new Intent(OpenPgpConstants.SERVICE_INTENT), PackageManager.GET_META_DATA);
if (!resInfo.isEmpty()) {
for (ResolveInfo resolveInfo : resInfo) {
if (resolveInfo.serviceInfo == null)
continue;
String packageName = resolveInfo.serviceInfo.packageName;
String simpleName = String.valueOf(resolveInfo.serviceInfo.loadLabel(context
.getPackageManager()));
Drawable icon = resolveInfo.serviceInfo.loadIcon(context.getPackageManager());
// get api version
ServiceInfo si = resolveInfo.serviceInfo;
int apiVersion = si.metaData.getInt("api_version");
mProviderList.add(new OpenPgpProviderEntry(packageName, simpleName, icon,
apiVersion));
}
}
} }
public OpenPgpListPreference(Context context) { public OpenPgpListPreference(Context context) {
@ -69,20 +53,42 @@ public class OpenPgpListPreference extends DialogPreference {
} }
/** /**
* Can be used to add "no selection" * Public method to add new entries for legacy applications
* *
* @param packageName * @param packageName
* @param simpleName * @param simpleName
* @param icon * @param icon
*/ */
public void addProvider(int position, String packageName, String simpleName, Drawable icon, public void addProvider(int position, String packageName, String simpleName, Drawable icon) {
int apiVersion) { mProviderList.add(position, new OpenPgpProviderEntry(packageName, simpleName, icon));
mProviderList.add(position, new OpenPgpProviderEntry(packageName, simpleName, icon,
apiVersion));
} }
@Override @Override
protected void onPrepareDialogBuilder(Builder builder) { protected void onPrepareDialogBuilder(Builder builder) {
// get providers
mProviderList.clear();
Intent intent = new Intent(OpenPgpConstants.SERVICE_INTENT);
List<ResolveInfo> resInfo = getContext().getPackageManager().queryIntentServices(intent, 0);
if (!resInfo.isEmpty()) {
for (ResolveInfo resolveInfo : resInfo) {
if (resolveInfo.serviceInfo == null)
continue;
String packageName = resolveInfo.serviceInfo.packageName;
String simpleName = String.valueOf(resolveInfo.serviceInfo.loadLabel(getContext()
.getPackageManager()));
Drawable icon = resolveInfo.serviceInfo.loadIcon(getContext().getPackageManager());
mProviderList.add(new OpenPgpProviderEntry(packageName, simpleName, icon));
}
}
// add "none"-entry
mProviderList.add(0, new OpenPgpProviderEntry("",
getContext().getString(R.string.openpgp_list_preference_none),
getContext().getResources().getDrawable(R.drawable.ic_action_cancel_launchersize)));
// Init ArrayAdapter with OpenPGP Providers // Init ArrayAdapter with OpenPGP Providers
ListAdapter adapter = new ArrayAdapter<OpenPgpProviderEntry>(getContext(), ListAdapter adapter = new ArrayAdapter<OpenPgpProviderEntry>(getContext(),
android.R.layout.select_dialog_singlechoice, android.R.id.text1, mProviderList) { android.R.layout.select_dialog_singlechoice, android.R.id.text1, mProviderList) {
@ -99,15 +105,6 @@ public class OpenPgpListPreference extends DialogPreference {
int dp10 = (int) (10 * getContext().getResources().getDisplayMetrics().density + 0.5f); int dp10 = (int) (10 * getContext().getResources().getDisplayMetrics().density + 0.5f);
tv.setCompoundDrawablePadding(dp10); tv.setCompoundDrawablePadding(dp10);
// disable if it has the wrong api_version
if (mProviderList.get(position).apiVersion == OpenPgpConstants.REQUIRED_API_VERSION) {
tv.setEnabled(true);
} else {
tv.setEnabled(false);
tv.setText(tv.getText() + " (API v" + mProviderList.get(position).apiVersion
+ ", needs v" + OpenPgpConstants.REQUIRED_API_VERSION + ")");
}
return v; return v;
} }
}; };
@ -169,6 +166,16 @@ public class OpenPgpListPreference extends DialogPreference {
return getEntryByValue(mSelectedPackage); return getEntryByValue(mSelectedPackage);
} }
@Override
protected Object onGetDefaultValue(TypedArray a, int index) {
return a.getString(index);
}
@Override
protected void onSetInitialValue(boolean restoreValue, Object defaultValue) {
setValue(restoreValue ? getPersistedString(mSelectedPackage) : (String) defaultValue);
}
public String getEntryByValue(String packageName) { public String getEntryByValue(String packageName) {
for (OpenPgpProviderEntry app : mProviderList) { for (OpenPgpProviderEntry app : mProviderList) {
if (app.packageName.equals(packageName)) { if (app.packageName.equals(packageName)) {
@ -183,14 +190,11 @@ public class OpenPgpListPreference extends DialogPreference {
private String packageName; private String packageName;
private String simpleName; private String simpleName;
private Drawable icon; private Drawable icon;
private int apiVersion;
public OpenPgpProviderEntry(String packageName, String simpleName, Drawable icon, public OpenPgpProviderEntry(String packageName, String simpleName, Drawable icon) {
int apiVersion) {
this.packageName = packageName; this.packageName = packageName;
this.simpleName = simpleName; this.simpleName = simpleName;
this.icon = icon; this.icon = icon;
this.apiVersion = apiVersion;
} }
@Override @Override

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de> * Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package org.openintents.openpgp; package org.openintents.openpgp.util;
import org.openintents.openpgp.IOpenPgpService; import org.openintents.openpgp.IOpenPgpService;
@ -65,7 +65,8 @@ public class OpenPgpServiceConnection {
* @return * @return
*/ */
public boolean bindToService() { public boolean bindToService() {
if (mService == null && !mBound) { // if not already connected // if not already connected
if (mService == null && !mBound) {
try { try {
Log.d(OpenPgpConstants.TAG, "not bound yet"); Log.d(OpenPgpConstants.TAG, "not bound yet");

View File

@ -0,0 +1,64 @@
/*
* Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openintents.openpgp.util;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ResolveInfo;
public class OpenPgpUtils {
public static final Pattern PGP_MESSAGE = Pattern.compile(
".*?(-----BEGIN PGP MESSAGE-----.*?-----END PGP MESSAGE-----).*",
Pattern.DOTALL);
public static final Pattern PGP_SIGNED_MESSAGE = Pattern.compile(
".*?(-----BEGIN PGP SIGNED MESSAGE-----.*?-----BEGIN PGP SIGNATURE-----.*?-----END PGP SIGNATURE-----).*",
Pattern.DOTALL);
public static final int PARSE_RESULT_NO_PGP = -1;
public static final int PARSE_RESULT_MESSAGE = 0;
public static final int PARSE_RESULT_SIGNED_MESSAGE = 1;
public static int parseMessage(String message) {
Matcher matcherSigned = PGP_SIGNED_MESSAGE.matcher(message);
Matcher matcherMessage = PGP_MESSAGE.matcher(message);
if (matcherMessage.matches()) {
return PARSE_RESULT_MESSAGE;
} else if (matcherSigned.matches()) {
return PARSE_RESULT_SIGNED_MESSAGE;
} else {
return PARSE_RESULT_NO_PGP;
}
}
public static boolean isAvailable(Context context) {
Intent intent = new Intent(OpenPgpConstants.SERVICE_INTENT);
List<ResolveInfo> resInfo = context.getPackageManager().queryIntentServices(intent, 0);
if (!resInfo.isEmpty()) {
return true;
} else {
return false;
}
}
}

View File

@ -0,0 +1,104 @@
/*
* Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
* 2013 Flow (http://stackoverflow.com/questions/18212152/transfer-inputstream-to-another-service-across-process-boundaries-with-parcelf)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openintents.openpgp.util;
import android.os.ParcelFileDescriptor;
import android.util.Log;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class ParcelFileDescriptorUtil {
public interface IThreadListener {
void onThreadFinished(final Thread thread);
}
public static ParcelFileDescriptor pipeFrom(InputStream inputStream, IThreadListener listener)
throws IOException {
ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
ParcelFileDescriptor readSide = pipe[0];
ParcelFileDescriptor writeSide = pipe[1];
// start the transfer thread
new TransferThread(inputStream, new ParcelFileDescriptor.AutoCloseOutputStream(writeSide),
listener)
.start();
return readSide;
}
public static ParcelFileDescriptor pipeTo(OutputStream outputStream, IThreadListener listener)
throws IOException {
ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
ParcelFileDescriptor readSide = pipe[0];
ParcelFileDescriptor writeSide = pipe[1];
// start the transfer thread
new TransferThread(new ParcelFileDescriptor.AutoCloseInputStream(readSide), outputStream,
listener)
.start();
return writeSide;
}
static class TransferThread extends Thread {
final InputStream mIn;
final OutputStream mOut;
final IThreadListener mListener;
TransferThread(InputStream in, OutputStream out, IThreadListener listener) {
super("ParcelFileDescriptor Transfer Thread");
mIn = in;
mOut = out;
mListener = listener;
setDaemon(true);
}
@Override
public void run() {
byte[] buf = new byte[1024];
int len;
try {
while ((len = mIn.read(buf)) > 0) {
mOut.write(buf, 0, len);
}
mOut.flush(); // just to be safe
} catch (IOException e) {
Log.e(OpenPgpConstants.TAG, "TransferThread" + getId() + ": writing failed", e);
} finally {
try {
mIn.close();
} catch (IOException e) {
Log.e(OpenPgpConstants.TAG, "TransferThread" + getId(), e);
}
try {
mOut.close();
} catch (IOException e) {
Log.e(OpenPgpConstants.TAG, "TransferThread" + getId(), e);
}
}
if (mListener != null) {
Log.d(OpenPgpConstants.TAG, "TransferThread " + getId() + " finished!");
mListener.onThreadFinished(this);
}
}
}
}

View File

@ -0,0 +1,37 @@
/*
* Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sufficientlysecure.keychain.api;
public class OpenKeychainIntents {
public static final String ENCRYPT = "org.sufficientlysecure.keychain.action.ENCRYPT";
public static final String ENCRYPT_EXTRA_TEXT = "text"; // String
public static final String ENCRYPT_ASCII_ARMOR = "ascii_armor"; // boolean
public static final String DECRYPT = "org.sufficientlysecure.keychain.action.DECRYPT";
public static final String DECRYPT_EXTRA_TEXT = "text"; // String
public static final String IMPORT_KEY = "org.sufficientlysecure.keychain.action.IMPORT_KEY";
public static final String IMPORT_KEY_EXTRA_KEY_BYTES = "key_bytes"; // byte[]
public static final String IMPORT_KEY_FROM_KEYSERVER = "org.sufficientlysecure.keychain.action.IMPORT_KEY_FROM_KEYSERVER";
public static final String IMPORT_KEY_FROM_KEYSERVER_QUERY = "query"; // String
public static final String IMPORT_KEY_FROM_KEYSERVER_FINGERPRINT = "fingerprint"; // String
public static final String IMPORT_KEY_FROM_QR_CODE = "org.sufficientlysecure.keychain.action.IMPORT_KEY_FROM_QR_CODE";
}