package info.guardianproject.pixelknot.crypto;
/*
* Based on code from K9Mail:
* http://code.google.com/p/k9mail/source/browse/k9mail/branches/apg-integration/src/com/fsck/k9/crypto/Apg.java
*/
import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.ContentUris;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager.NameNotFoundException;
import android.database.Cursor;
import android.net.Uri;
import android.support.v4.app.Fragment;
import android.widget.Toast;
import info.guardianproject.pixelknot.R;
import java.io.File;
import java.util.Vector;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class Apg extends CryptoProvider
{
static final long serialVersionUID = 0x21071235;
public static final String NAME = "apg";
private static final String mApgPackageName = "org.thialfihar.android.apg";
private static final int mMinRequiredVersion = 16;
public static final String AUTHORITY = "org.thialfihar.android.apg.provider";
public static final Uri CONTENT_URI_SECRET_KEY_RING_BY_KEY_ID =
Uri.parse("content://" + AUTHORITY + "/key_rings/secret/key_id/");
public static final Uri CONTENT_URI_SECRET_KEY_RING_BY_EMAILS =
Uri.parse("content://" + AUTHORITY + "/key_rings/secret/emails/");
public static final Uri CONTENT_URI_PUBLIC_KEY_RING_BY_KEY_ID =
Uri.parse("content://" + AUTHORITY + "/key_rings/public/key_id/");
public static final Uri CONTENT_URI_PUBLIC_KEY_RING_BY_EMAILS =
Uri.parse("content://" + AUTHORITY + "/key_rings/public/emails/");
public static class Intent
{
public static final String DECRYPT = "org.thialfihar.android.apg.intent.DECRYPT";
public static final String ENCRYPT = "org.thialfihar.android.apg.intent.ENCRYPT";
public static final String DECRYPT_FILE = "org.thialfihar.android.apg.intent.DECRYPT_FILE";
public static final String ENCRYPT_FILE = "org.thialfihar.android.apg.intent.ENCRYPT_FILE";
public static final String DECRYPT_AND_RETURN = "org.thialfihar.android.apg.intent.DECRYPT_AND_RETURN";
public static final String ENCRYPT_AND_RETURN = "org.thialfihar.android.apg.intent.ENCRYPT_AND_RETURN";
public static final String SELECT_PUBLIC_KEYS = "org.thialfihar.android.apg.intent.SELECT_PUBLIC_KEYS";
public static final String SELECT_SECRET_KEY = "org.thialfihar.android.apg.intent.SELECT_SECRET_KEY";
}
public static final String EXTRA_TEXT = "text";
public static final String EXTRA_DATA = "data";
public static final String EXTRA_STATUS = "status";
public static final String EXTRA_ERROR = "error";
public static final String EXTRA_DECRYPTED_MESSAGE = "decryptedMessage";
public static final String EXTRA_ENCRYPTED_MESSAGE = "encryptedMessage";
public static final String EXTRA_SIGNATURE = "signature";
public static final String EXTRA_SIGNATURE_KEY_ID = "signatureKeyId";
public static final String EXTRA_SIGNATURE_USER_ID = "signatureUserId";
public static final String EXTRA_SIGNATURE_SUCCESS = "signatureSuccess";
public static final String EXTRA_SIGNATURE_UNKNOWN = "signatureUnknown";
public static final String EXTRA_USER_ID = "userId";
public static final String EXTRA_KEY_ID = "keyId";
public static final String EXTRA_REPLY_TO = "replyTo";
public static final String EXTRA_SEND_TO = "sendTo";
public static final String EXTRA_SUBJECT = "subject";
public static final String EXTRA_ENCRYPTION_KEY_IDS = "encryptionKeyIds";
public static final String EXTRA_SELECTION = "selection";
public static final String EXTRA_MESSAGE = "message";
public static final String EXTRA_PROGRESS = "progress";
public static final String EXTRA_MAX = "max";
public static final String EXTRA_ACCOUNT = "account";
/*
* because "can only use lower 16 bits for request code"...
*
* public static final int DECRYPT_MESSAGE = 0x21070001;
* public static final int ENCRYPT_MESSAGE = 0x21070002;
* public static final int SELECT_PUBLIC_KEYS = 0x21070003;
* public static final int SELECT_SECRET_KEY = 554106884;
*/
public static final int DECRYPT_MESSAGE = 303;
public static final int ENCRYPT_MESSAGE = 302;
public static final int SELECT_PUBLIC_KEYS = 301;
public static final int SELECT_SECRET_KEY = 300;
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);
private static Apg _apgInstance;
public synchronized static Apg getInstance()
{
if (_apgInstance == null)
_apgInstance = new Apg();
return _apgInstance;
}
/**
* Check whether APG is installed and at a high enough version.
*
* @param context
* @return whether a suitable version of APG was found
*/
@Override
public boolean isAvailable(Context context)
{
try
{
PackageInfo pi = context.getPackageManager().getPackageInfo(mApgPackageName, 0);
if (pi.versionCode >= mMinRequiredVersion)
{
return true;
}
else
{
Toast.makeText(context,
R.string.apg_error_apg_version_not_supported, Toast.LENGTH_SHORT).show();
}
}
catch (NameNotFoundException e)
{
// not found
}
return false;
}
/**
* Select the signature key.
*
* @param activity
* @return success or failure
*/
@Override
public boolean selectSecretKey(Activity activity)
{
android.content.Intent intent = new android.content.Intent(Intent.SELECT_SECRET_KEY);
try
{
activity.startActivityForResult(intent, Apg.SELECT_SECRET_KEY);
return true;
}
catch (ActivityNotFoundException e)
{
Toast.makeText(activity,
R.string.apg_error_activity_not_found,
Toast.LENGTH_SHORT).show();
return false;
}
}
public boolean selectSecretKey(Fragment fragment)
{
android.content.Intent intent = new android.content.Intent(Intent.SELECT_SECRET_KEY);
try
{
fragment.startActivityForResult(intent, Apg.SELECT_SECRET_KEY);
return true;
}
catch (ActivityNotFoundException e)
{
Toast.makeText(fragment.getActivity(),
R.string.apg_error_activity_not_found,
Toast.LENGTH_SHORT).show();
return false;
}
}
/**
* Select encryption keys.
*
* @param activity
* @param emails The emails that should be used for preselection.
* @return success or failure
*/
@Override
public boolean selectEncryptionKeys(Activity activity, String emails)
{
android.content.Intent intent = new android.content.Intent(Apg.Intent.SELECT_PUBLIC_KEYS);
long[] initialKeyIds = null;
if (!hasEncryptionKeys())
{
Vector<Long> keyIds = new Vector<Long>();
if (hasSignatureKey())
{
keyIds.add(getSignatureKeyId());
}
try
{
Uri contentUri = Uri.withAppendedPath(
Apg.CONTENT_URI_PUBLIC_KEY_RING_BY_EMAILS,
emails);
Cursor c = activity.getContentResolver().query(contentUri,
new String[] { "master_key_id" },
null, null, null);
if (c != null)
{
while (c.moveToNext())
{
keyIds.add(c.getLong(0));
}
}
if (c != null)
{
c.close();
}
}
catch (SecurityException e)
{
Toast.makeText(activity,
activity.getResources().getString(R.string.insufficient_apg_permissions),
Toast.LENGTH_LONG).show();
}
if (keyIds.size() > 0)
{
initialKeyIds = new long[keyIds.size()];
for (int i = 0, size = keyIds.size(); i < size; ++i)
{
initialKeyIds[i] = keyIds.get(i);
}
}
}
else
{
initialKeyIds = mEncryptionKeyIds;
}
intent.putExtra(Apg.EXTRA_SELECTION, initialKeyIds);
try
{
activity.startActivityForResult(intent, Apg.SELECT_PUBLIC_KEYS);
return true;
}
catch (ActivityNotFoundException e)
{
Toast.makeText(activity,
R.string.apg_error_activity_not_found,
Toast.LENGTH_SHORT).show();
return false;
}
}
public boolean selectEncryptionKeys(Fragment fragment, Activity activity, String emails)
{
android.content.Intent intent = new android.content.Intent(Apg.Intent.SELECT_PUBLIC_KEYS);
long[] initialKeyIds = null;
if (!hasEncryptionKeys())
{
Vector<Long> keyIds = new Vector<Long>();
if (hasSignatureKey())
{
keyIds.add(getSignatureKeyId());
}
try
{
Uri contentUri = Uri.withAppendedPath(
Apg.CONTENT_URI_PUBLIC_KEY_RING_BY_EMAILS,
emails);
Cursor c = activity.getContentResolver().query(contentUri,
new String[] { "master_key_id" },
null, null, null);
if (c != null)
{
while (c.moveToNext())
{
keyIds.add(c.getLong(0));
}
}
if (c != null)
{
c.close();
}
}
catch (SecurityException e)
{
Toast.makeText(activity,
fragment.getResources().getString(R.string.insufficient_apg_permissions),
Toast.LENGTH_LONG).show();
}
if (keyIds.size() > 0)
{
initialKeyIds = new long[keyIds.size()];
for (int i = 0, size = keyIds.size(); i < size; ++i)
{
initialKeyIds[i] = keyIds.get(i);
}
}
}
else
{
initialKeyIds = mEncryptionKeyIds;
}
intent.putExtra(Apg.EXTRA_SELECTION, initialKeyIds);
try
{
fragment.startActivityForResult(intent, Apg.SELECT_PUBLIC_KEYS);
return true;
}
catch (ActivityNotFoundException e)
{
Toast.makeText(activity,
R.string.apg_error_activity_not_found,
Toast.LENGTH_SHORT).show();
return false;
}
}
/**
* Get secret key ids based on a given email.
*
* @param context
* @param email The email in question.
* @return key ids
*/
@Override
public long[] getSecretKeyIdsFromEmail(Context context, String email)
{
long ids[] = null;
try
{
Uri contentUri = Uri.withAppendedPath(Apg.CONTENT_URI_SECRET_KEY_RING_BY_EMAILS,
email);
Cursor c = context.getContentResolver().query(contentUri,
new String[] { "master_key_id" },
null, null, null);
if (c != null && c.getCount() > 0)
{
ids = new long[c.getCount()];
while (c.moveToNext())
{
ids[c.getPosition()] = c.getLong(0);
}
}
if (c != null)
{
c.close();
}
}
catch (SecurityException e)
{
Toast.makeText(context,
context.getResources().getString(R.string.insufficient_apg_permissions),
Toast.LENGTH_LONG).show();
}
return ids;
}
/**
* Get the user id based on the key id.
*
* @param context
* @param keyId
* @return user id
*/
@Override
public String getUserId(Context context, long keyId)
{
String userId = null;
try
{
Uri contentUri = ContentUris.withAppendedId(
Apg.CONTENT_URI_SECRET_KEY_RING_BY_KEY_ID,
keyId);
Cursor c = context.getContentResolver().query(contentUri,
new String[] { "user_id" },
null, null, null);
if (c != null && c.moveToFirst())
{
userId = c.getString(0);
}
if (c != null)
{
c.close();
}
}
catch (SecurityException e)
{
Toast.makeText(context,
context.getResources().getString(R.string.insufficient_apg_permissions),
Toast.LENGTH_LONG).show();
}
if (userId == null)
{
userId = context.getString(R.string.unknown_crypto_signature_user_id);
}
return userId;
}
/**
* Get the user id based on the key id.
*
* @param context
* @param keyId
* @return user id
*/
public String getPublicUserId(Context context, long keyId)
{
String userId = null;
try
{
Uri contentUri = ContentUris.withAppendedId(
Apg.CONTENT_URI_PUBLIC_KEY_RING_BY_KEY_ID,
keyId);
Cursor c = context.getContentResolver().query(contentUri,
new String[] { "user_id" },
null, null, null);
if (c != null && c.moveToFirst())
{
userId = c.getString(0);
}
if (c != null)
{
c.close();
}
}
catch (SecurityException e)
{
Toast.makeText(context,
context.getResources().getString(R.string.insufficient_apg_permissions),
Toast.LENGTH_LONG).show();
}
if (userId == null)
{
userId = context.getString(R.string.unknown_crypto_signature_user_id);
}
return userId;
}
/**
* Handle the activity results that concern us.
*
* @param activity
* @param requestCode
* @param resultCode
* @param data
* @return handled or not
*/
@Override
public boolean onActivityResult(Activity activity, int requestCode, int resultCode,
android.content.Intent data)
{
switch (requestCode)
{
case Apg.SELECT_SECRET_KEY:
if (resultCode != Activity.RESULT_OK || data == null)
{
break;
}
setSignatureKeyId(data.getLongExtra(Apg.EXTRA_KEY_ID, 0));
setSignatureUserId(data.getStringExtra(Apg.EXTRA_USER_ID));
//((MessageCompose) activity).updateEncryptLayout();
break;
case Apg.SELECT_PUBLIC_KEYS:
if (resultCode != Activity.RESULT_OK || data == null)
{
mEncryptionKeyIds = null;
// ((MessageCompose) activity).onEncryptionKeySelectionDone();
break;
}
mEncryptionKeyIds = data.getLongArrayExtra(Apg.EXTRA_SELECTION);
// ((MessageCompose) activity).onEncryptionKeySelectionDone();
break;
case Apg.ENCRYPT_MESSAGE:
if (resultCode != Activity.RESULT_OK || data == null)
{
mEncryptedData = null;
// ((MessageCompose) activity).onEncryptDone();
break;
}
mEncryptedData = data.getStringExtra(Apg.EXTRA_ENCRYPTED_MESSAGE);
// this was a stupid bug in an earlier version, just gonna leave this in for an APG
// version or two
if (mEncryptedData == null) {
mEncryptedData = data.getStringExtra(Apg.EXTRA_DECRYPTED_MESSAGE);
}
if (mEncryptedData != null)
{
// ((MessageCompose) activity).onEncryptDone();
}
break;
case Apg.DECRYPT_MESSAGE:
if (resultCode != Activity.RESULT_OK || data == null)
{
break;
}
mSignatureUserId = data.getStringExtra(Apg.EXTRA_SIGNATURE_USER_ID);
mSignatureKeyId = data.getLongExtra(Apg.EXTRA_SIGNATURE_KEY_ID, 0);
mSignatureSuccess = data.getBooleanExtra(Apg.EXTRA_SIGNATURE_SUCCESS, false);
mSignatureUnknown = data.getBooleanExtra(Apg.EXTRA_SIGNATURE_UNKNOWN, false);
mDecryptedData = data.getStringExtra(Apg.EXTRA_DECRYPTED_MESSAGE);
//((MessageView) activity).onDecryptDone();
break;
default:
return false;
}
return true;
}
/**
* Start the encrypt activity.
*
* @param activity
* @param data
* @return success or failure
*/
@Override
public boolean encrypt(Activity activity, String data)
{
android.content.Intent intent = new android.content.Intent(Intent.ENCRYPT_AND_RETURN);
intent.setType("text/plain");
intent.putExtra(Apg.EXTRA_TEXT, data);
intent.putExtra(Apg.EXTRA_ENCRYPTION_KEY_IDS, mEncryptionKeyIds);
intent.putExtra(Apg.EXTRA_SIGNATURE_KEY_ID, mSignatureKeyId);
try
{
activity.startActivityForResult(intent, Apg.ENCRYPT_MESSAGE);
return true;
}
catch (ActivityNotFoundException e)
{
Toast.makeText(activity,
R.string.apg_error_activity_not_found,
Toast.LENGTH_SHORT).show();
return false;
}
}
/**
* Start the encrypt activity.
*
* @param activity
* @param data
* @return success or failure
*/
public boolean encryptFile(Activity activity, File fileToEncrypt)
{
android.content.Intent intent = new android.content.Intent(Intent.ENCRYPT_FILE);
intent.setData(Uri.fromFile(fileToEncrypt));
//intent.putExtra(Apg.EXTRA_TEXT, data);
intent.putExtra(Apg.EXTRA_ENCRYPTION_KEY_IDS, mEncryptionKeyIds);
intent.putExtra(Apg.EXTRA_SIGNATURE_KEY_ID, mSignatureKeyId);
try
{
activity.startActivityForResult(intent, Apg.ENCRYPT_MESSAGE);
return true;
}
catch (ActivityNotFoundException e)
{
Toast.makeText(activity,
R.string.apg_error_activity_not_found,
Toast.LENGTH_SHORT).show();
return false;
}
}
/**
* Start the encrypt activity.
*
* @param activity
* @param data
* @return success or failure
*/
public boolean encrypt(Activity activity, String mimeType, byte[] data)
{
android.content.Intent intent = new android.content.Intent(Intent.ENCRYPT_AND_RETURN);
intent.setType(mimeType);
intent.putExtra(Apg.EXTRA_DATA, data);
intent.putExtra(Apg.EXTRA_ENCRYPTION_KEY_IDS, mEncryptionKeyIds);
intent.putExtra(Apg.EXTRA_SIGNATURE_KEY_ID, mSignatureKeyId);
try
{
activity.startActivityForResult(intent, Apg.ENCRYPT_MESSAGE);
return true;
}
catch (ActivityNotFoundException e)
{
Toast.makeText(activity,
R.string.apg_error_activity_not_found,
Toast.LENGTH_SHORT).show();
return false;
}
}
/**
* Start the decrypt activity.
*
* @param activity
* @param data
* @return success or failure
*/
@Override
public boolean decrypt(Activity activity, String data)
{
android.content.Intent intent = new android.content.Intent(Apg.Intent.DECRYPT_AND_RETURN);
intent.setType("text/plain");
if (data == null)
{
return false;
}
try
{
intent.putExtra(EXTRA_TEXT, data);
activity.startActivityForResult(intent, Apg.DECRYPT_MESSAGE);
return true;
}
catch (ActivityNotFoundException e)
{
Toast.makeText(activity,
R.string.apg_error_activity_not_found,
Toast.LENGTH_SHORT).show();
return false;
}
}
public boolean decrypt(Activity activity, String mimeType, byte[] data)
{
android.content.Intent intent = new android.content.Intent(Apg.Intent.DECRYPT_AND_RETURN);
intent.setType(mimeType);
if (data == null)
{
return false;
}
try
{
intent.putExtra(EXTRA_DATA, data);
activity.startActivityForResult(intent, Apg.DECRYPT_MESSAGE);
return true;
}
catch (ActivityNotFoundException e)
{
Toast.makeText(activity,
R.string.apg_error_activity_not_found,
Toast.LENGTH_SHORT).show();
return false;
}
}
public boolean isEncrypted(String data)
{
if (data == null)
{
return false;
}
Matcher matcher = PGP_MESSAGE.matcher(data);
return matcher.matches();
}
public boolean isSigned(String data)
{
if (data == null)
{
return false;
}
Matcher matcher = PGP_SIGNED_MESSAGE.matcher(data);
return matcher.matches();
}
/**
* Get the name of the provider.
*
* @return provider name
*/
@Override
public String getName()
{
return NAME;
}
/**
* Test the APG installation.
*
* @return success or failure
*/
public boolean test(Context context)
{
if (!isAvailable(context))
{
return false;
}
try
{
// try out one content provider to check permissions
Uri contentUri = ContentUris.withAppendedId(
Apg.CONTENT_URI_SECRET_KEY_RING_BY_KEY_ID,
12345);
Cursor c = context.getContentResolver().query(contentUri,
new String[] { "user_id" },
null, null, null);
if (c != null)
{
c.close();
}
}
catch (SecurityException e)
{
// if there was a problem, then let the user know, this will not stop K9/APG from
// working, but some features won't be available, so we can still return "true"
Toast.makeText(context,
context.getResources().getString(R.string.insufficient_apg_permissions),
Toast.LENGTH_LONG).show();
}
return true;
}
}