New Gradle project structure

This commit is contained in:
Dominik Schürmann
2014-01-27 14:18:25 +01:00
parent 08e57019b0
commit cb3ca37db9
359 changed files with 0 additions and 215 deletions

View File

@@ -0,0 +1,869 @@
/*
* Copyright (C) 2012-2013 Dominik Schürmann <dominik@dominikschuermann.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.service;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import org.spongycastle.openpgp.PGPKeyRing;
import org.spongycastle.openpgp.PGPPublicKeyRing;
import org.spongycastle.openpgp.PGPSecretKey;
import org.spongycastle.openpgp.PGPSecretKeyRing;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.Id;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.helper.FileHelper;
import org.sufficientlysecure.keychain.helper.OtherHelper;
import org.sufficientlysecure.keychain.helper.Preferences;
import org.sufficientlysecure.keychain.pgp.PgpConversionHelper;
import org.sufficientlysecure.keychain.pgp.PgpHelper;
import org.sufficientlysecure.keychain.pgp.PgpImportExport;
import org.sufficientlysecure.keychain.pgp.PgpKeyOperation;
import org.sufficientlysecure.keychain.pgp.PgpOperation;
import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
import org.sufficientlysecure.keychain.provider.KeychainContract.DataStream;
import org.sufficientlysecure.keychain.provider.ProviderHelper;
import org.sufficientlysecure.keychain.ui.adapter.ImportKeysListEntry;
import org.sufficientlysecure.keychain.util.HkpKeyServer;
import org.sufficientlysecure.keychain.util.InputData;
import org.sufficientlysecure.keychain.util.KeyServer.KeyInfo;
import org.sufficientlysecure.keychain.util.Log;
import org.sufficientlysecure.keychain.util.ProgressDialogUpdater;
import android.app.IntentService;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.os.Message;
import android.os.Messenger;
import android.os.RemoteException;
/**
* This Service contains all important long lasting operations for APG. It receives Intents with
* data from the activities or other apps, queues these intents, executes them, and stops itself
* after doing them.
*/
public class KeychainIntentService extends IntentService implements ProgressDialogUpdater {
/* extras that can be given by intent */
public static final String EXTRA_MESSENGER = "messenger";
public static final String EXTRA_DATA = "data";
/* possible actions */
public static final String ACTION_ENCRYPT_SIGN = Constants.INTENT_PREFIX + "ENCRYPT_SIGN";
public static final String ACTION_DECRYPT_VERIFY = Constants.INTENT_PREFIX + "DECRYPT_VERIFY";
public static final String ACTION_SAVE_KEYRING = Constants.INTENT_PREFIX + "SAVE_KEYRING";
public static final String ACTION_GENERATE_KEY = Constants.INTENT_PREFIX + "GENERATE_KEY";
public static final String ACTION_GENERATE_DEFAULT_RSA_KEYS = Constants.INTENT_PREFIX
+ "GENERATE_DEFAULT_RSA_KEYS";
public static final String ACTION_DELETE_FILE_SECURELY = Constants.INTENT_PREFIX
+ "DELETE_FILE_SECURELY";
public static final String ACTION_IMPORT_KEYRING = Constants.INTENT_PREFIX + "IMPORT_KEYRING";
public static final String ACTION_EXPORT_KEYRING = Constants.INTENT_PREFIX + "EXPORT_KEYRING";
public static final String ACTION_UPLOAD_KEYRING = Constants.INTENT_PREFIX + "UPLOAD_KEYRING";
public static final String ACTION_QUERY_KEYRING = Constants.INTENT_PREFIX + "QUERY_KEYRING";
public static final String ACTION_SIGN_KEYRING = Constants.INTENT_PREFIX + "SIGN_KEYRING";
/* keys for data bundle */
// encrypt, decrypt, import export
public static final String TARGET = "target";
// possible targets:
public static final int TARGET_BYTES = 1;
public static final int TARGET_FILE = 2;
public static final int TARGET_STREAM = 3;
// encrypt
public static final String ENCRYPT_SECRET_KEY_ID = "secret_key_id";
public static final String ENCRYPT_USE_ASCII_ARMOR = "use_ascii_armor";
public static final String ENCRYPT_ENCRYPTION_KEYS_IDS = "encryption_keys_ids";
public static final String ENCRYPT_COMPRESSION_ID = "compression_id";
public static final String ENCRYPT_GENERATE_SIGNATURE = "generate_signature";
public static final String ENCRYPT_SIGN_ONLY = "sign_only";
public static final String ENCRYPT_MESSAGE_BYTES = "message_bytes";
public static final String ENCRYPT_INPUT_FILE = "input_file";
public static final String ENCRYPT_OUTPUT_FILE = "output_file";
public static final String ENCRYPT_PROVIDER_URI = "provider_uri";
// decrypt/verify
public static final String DECRYPT_SIGNED_ONLY = "signed_only";
public static final String DECRYPT_RETURN_BYTES = "return_binary";
public static final String DECRYPT_CIPHERTEXT_BYTES = "ciphertext_bytes";
public static final String DECRYPT_ASSUME_SYMMETRIC = "assume_symmetric";
public static final String DECRYPT_LOOKUP_UNKNOWN_KEY = "lookup_unknownKey";
// save keyring
public static final String SAVE_KEYRING_NEW_PASSPHRASE = "new_passphrase";
public static final String SAVE_KEYRING_CURRENT_PASSPHRASE = "current_passphrase";
public static final String SAVE_KEYRING_USER_IDS = "user_ids";
public static final String SAVE_KEYRING_KEYS = "keys";
public static final String SAVE_KEYRING_KEYS_USAGES = "keys_usages";
public static final String SAVE_KEYRING_MASTER_KEY_ID = "master_key_id";
public static final String SAVE_KEYRING_CAN_SIGN = "can_sign";
// generate key
public static final String GENERATE_KEY_ALGORITHM = "algorithm";
public static final String GENERATE_KEY_KEY_SIZE = "key_size";
public static final String GENERATE_KEY_SYMMETRIC_PASSPHRASE = "passphrase";
public static final String GENERATE_KEY_MASTER_KEY = "master_key";
// delete file securely
public static final String DELETE_FILE = "deleteFile";
// import key
public static final String IMPORT_INPUT_STREAM = "import_input_stream";
public static final String IMPORT_FILENAME = "import_filename";
public static final String IMPORT_BYTES = "import_bytes";
public static final String IMPORT_KEY_LIST = "import_key_list";
// export key
public static final String EXPORT_OUTPUT_STREAM = "export_output_stream";
public static final String EXPORT_FILENAME = "export_filename";
public static final String EXPORT_KEY_TYPE = "export_key_type";
public static final String EXPORT_ALL = "export_all";
public static final String EXPORT_KEY_RING_MASTER_KEY_ID = "export_key_ring_id";
// upload key
public static final String UPLOAD_KEY_SERVER = "upload_key_server";
public static final String UPLOAD_KEY_KEYRING_ROW_ID = "upload_key_ring_id";
// query key
public static final String QUERY_KEY_SERVER = "query_key_server";
public static final String QUERY_KEY_TYPE = "query_key_type";
public static final String QUERY_KEY_STRING = "query_key_string";
public static final String QUERY_KEY_ID = "query_key_id";
// sign key
public static final String SIGN_KEY_MASTER_KEY_ID = "sign_key_master_key_id";
public static final String SIGN_KEY_PUB_KEY_ID = "sign_key_pub_key_id";
/*
* possible data keys as result send over messenger
*/
// keys
public static final String RESULT_NEW_KEY = "new_key";
public static final String RESULT_NEW_KEY2 = "new_key2";
// encrypt
public static final String RESULT_SIGNATURE_BYTES = "signature_data";
public static final String RESULT_SIGNATURE_STRING = "signature_text";
public static final String RESULT_ENCRYPTED_STRING = "encrypted_message";
public static final String RESULT_ENCRYPTED_BYTES = "encrypted_data";
public static final String RESULT_URI = "result_uri";
// decrypt/verify
public static final String RESULT_DECRYPTED_STRING = "decrypted_message";
public static final String RESULT_DECRYPTED_BYTES = "decrypted_data";
public static final String RESULT_SIGNATURE = "signature";
public static final String RESULT_SIGNATURE_KEY_ID = "signature_key_id";
public static final String RESULT_SIGNATURE_USER_ID = "signature_user_id";
public static final String RESULT_SIGNATURE_SUCCESS = "signature_success";
public static final String RESULT_SIGNATURE_UNKNOWN = "signature_unknown";
public static final String RESULT_SIGNATURE_LOOKUP_KEY = "lookup_key";
// import
public static final String RESULT_IMPORT_ADDED = "added";
public static final String RESULT_IMPORT_UPDATED = "updated";
public static final String RESULT_IMPORT_BAD = "bad";
// export
public static final String RESULT_EXPORT = "exported";
// query
public static final String RESULT_QUERY_KEY_DATA = "query_key_data";
public static final String RESULT_QUERY_KEY_SEARCH_RESULT = "query_key_search_result";
Messenger mMessenger;
public KeychainIntentService() {
super("ApgService");
}
/**
* The IntentService calls this method from the default worker thread with the intent that
* started the service. When this method returns, IntentService stops the service, as
* appropriate.
*/
@Override
protected void onHandleIntent(Intent intent) {
Bundle extras = intent.getExtras();
if (extras == null) {
Log.e(Constants.TAG, "Extras bundle is null!");
return;
}
if (!(extras.containsKey(EXTRA_MESSENGER) || extras.containsKey(EXTRA_DATA) || (intent
.getAction() == null))) {
Log.e(Constants.TAG,
"Extra bundle must contain a messenger, a data bundle, and an action!");
return;
}
mMessenger = (Messenger) extras.get(EXTRA_MESSENGER);
Bundle data = extras.getBundle(EXTRA_DATA);
OtherHelper.logDebugBundle(data, "EXTRA_DATA");
String action = intent.getAction();
// execute action from extra bundle
if (ACTION_ENCRYPT_SIGN.equals(action)) {
try {
/* Input */
int target = data.getInt(TARGET);
long secretKeyId = data.getLong(ENCRYPT_SECRET_KEY_ID);
String encryptionPassphrase = data.getString(GENERATE_KEY_SYMMETRIC_PASSPHRASE);
boolean useAsciiArmor = data.getBoolean(ENCRYPT_USE_ASCII_ARMOR);
long encryptionKeyIds[] = data.getLongArray(ENCRYPT_ENCRYPTION_KEYS_IDS);
int compressionId = data.getInt(ENCRYPT_COMPRESSION_ID);
boolean generateSignature = data.getBoolean(ENCRYPT_GENERATE_SIGNATURE);
boolean signOnly = data.getBoolean(ENCRYPT_SIGN_ONLY);
InputStream inStream = null;
long inLength = -1;
InputData inputData = null;
OutputStream outStream = null;
String streamFilename = null;
switch (target) {
case TARGET_BYTES: /* encrypting bytes directly */
byte[] bytes = data.getByteArray(ENCRYPT_MESSAGE_BYTES);
inStream = new ByteArrayInputStream(bytes);
inLength = bytes.length;
inputData = new InputData(inStream, inLength);
outStream = new ByteArrayOutputStream();
break;
case TARGET_FILE: /* encrypting file */
String inputFile = data.getString(ENCRYPT_INPUT_FILE);
String outputFile = data.getString(ENCRYPT_OUTPUT_FILE);
// check if storage is ready
if (!FileHelper.isStorageMounted(inputFile)
|| !FileHelper.isStorageMounted(outputFile)) {
throw new PgpGeneralException(
getString(R.string.error_external_storage_not_ready));
}
inStream = new FileInputStream(inputFile);
File file = new File(inputFile);
inLength = file.length();
inputData = new InputData(inStream, inLength);
outStream = new FileOutputStream(outputFile);
break;
case TARGET_STREAM: /* Encrypting stream from content uri */
Uri providerUri = (Uri) data.getParcelable(ENCRYPT_PROVIDER_URI);
// InputStream
InputStream in = getContentResolver().openInputStream(providerUri);
inLength = PgpHelper.getLengthOfStream(in);
inputData = new InputData(in, inLength);
// OutputStream
try {
while (true) {
streamFilename = PgpHelper.generateRandomFilename(32);
if (streamFilename == null) {
throw new PgpGeneralException("couldn't generate random file name");
}
openFileInput(streamFilename).close();
}
} catch (FileNotFoundException e) {
// found a name that isn't used yet
}
outStream = openFileOutput(streamFilename, Context.MODE_PRIVATE);
break;
default:
throw new PgpGeneralException("No target choosen!");
}
/* Operation */
PgpOperation operation = new PgpOperation(this, this, inputData, outStream);
if (generateSignature) {
Log.d(Constants.TAG, "generating signature...");
operation.generateSignature(useAsciiArmor, false, secretKeyId,
PassphraseCacheService.getCachedPassphrase(this, secretKeyId),
Preferences.getPreferences(this).getDefaultHashAlgorithm(), Preferences
.getPreferences(this).getForceV3Signatures());
} else if (signOnly) {
Log.d(Constants.TAG, "sign only...");
operation.signText(secretKeyId, PassphraseCacheService.getCachedPassphrase(
this, secretKeyId), Preferences.getPreferences(this)
.getDefaultHashAlgorithm(), Preferences.getPreferences(this)
.getForceV3Signatures());
} else {
Log.d(Constants.TAG, "encrypt...");
operation.signAndEncrypt(useAsciiArmor, compressionId, encryptionKeyIds,
encryptionPassphrase, Preferences.getPreferences(this)
.getDefaultEncryptionAlgorithm(), secretKeyId, Preferences
.getPreferences(this).getDefaultHashAlgorithm(), Preferences
.getPreferences(this).getForceV3Signatures(),
PassphraseCacheService.getCachedPassphrase(this, secretKeyId));
}
outStream.close();
/* Output */
Bundle resultData = new Bundle();
switch (target) {
case TARGET_BYTES:
if (useAsciiArmor) {
String output = new String(
((ByteArrayOutputStream) outStream).toByteArray());
if (generateSignature) {
resultData.putString(RESULT_SIGNATURE_STRING, output);
} else {
resultData.putString(RESULT_ENCRYPTED_STRING, output);
}
} else {
byte output[] = ((ByteArrayOutputStream) outStream).toByteArray();
if (generateSignature) {
resultData.putByteArray(RESULT_SIGNATURE_BYTES, output);
} else {
resultData.putByteArray(RESULT_ENCRYPTED_BYTES, output);
}
}
break;
case TARGET_FILE:
// nothing, file was written, just send okay
break;
case TARGET_STREAM:
String uri = DataStream.buildDataStreamUri(streamFilename).toString();
resultData.putString(RESULT_URI, uri);
break;
}
OtherHelper.logDebugBundle(resultData, "resultData");
sendMessageToHandler(KeychainIntentServiceHandler.MESSAGE_OKAY, resultData);
} catch (Exception e) {
sendErrorToHandler(e);
}
} else if (ACTION_DECRYPT_VERIFY.equals(action)) {
try {
/* Input */
int target = data.getInt(TARGET);
long secretKeyId = data.getLong(ENCRYPT_SECRET_KEY_ID);
byte[] bytes = data.getByteArray(DECRYPT_CIPHERTEXT_BYTES);
boolean signedOnly = data.getBoolean(DECRYPT_SIGNED_ONLY);
boolean returnBytes = data.getBoolean(DECRYPT_RETURN_BYTES);
boolean assumeSymmetricEncryption = data.getBoolean(DECRYPT_ASSUME_SYMMETRIC);
boolean lookupUnknownKey = data.getBoolean(DECRYPT_LOOKUP_UNKNOWN_KEY);
InputStream inStream = null;
long inLength = -1;
InputData inputData = null;
OutputStream outStream = null;
String streamFilename = null;
switch (target) {
case TARGET_BYTES: /* decrypting bytes directly */
inStream = new ByteArrayInputStream(bytes);
inLength = bytes.length;
inputData = new InputData(inStream, inLength);
outStream = new ByteArrayOutputStream();
break;
case TARGET_FILE: /* decrypting file */
String inputFile = data.getString(ENCRYPT_INPUT_FILE);
String outputFile = data.getString(ENCRYPT_OUTPUT_FILE);
// check if storage is ready
if (!FileHelper.isStorageMounted(inputFile)
|| !FileHelper.isStorageMounted(outputFile)) {
throw new PgpGeneralException(
getString(R.string.error_external_storage_not_ready));
}
// InputStream
inLength = -1;
inStream = new FileInputStream(inputFile);
File file = new File(inputFile);
inLength = file.length();
inputData = new InputData(inStream, inLength);
// OutputStream
outStream = new FileOutputStream(outputFile);
break;
case TARGET_STREAM: /* decrypting stream from content uri */
Uri providerUri = (Uri) data.getParcelable(ENCRYPT_PROVIDER_URI);
// InputStream
InputStream in = getContentResolver().openInputStream(providerUri);
inLength = PgpHelper.getLengthOfStream(in);
inputData = new InputData(in, inLength);
// OutputStream
try {
while (true) {
streamFilename = PgpHelper.generateRandomFilename(32);
if (streamFilename == null) {
throw new PgpGeneralException("couldn't generate random file name");
}
openFileInput(streamFilename).close();
}
} catch (FileNotFoundException e) {
// found a name that isn't used yet
}
outStream = openFileOutput(streamFilename, Context.MODE_PRIVATE);
break;
default:
throw new PgpGeneralException("No target choosen!");
}
/* Operation */
Bundle resultData = new Bundle();
// verifyText and decrypt returning additional resultData values for the
// verification of signatures
PgpOperation operation = new PgpOperation(this, this, inputData, outStream);
if (signedOnly) {
resultData = operation.verifyText(lookupUnknownKey);
} else {
resultData = operation.decryptAndVerify(
PassphraseCacheService.getCachedPassphrase(this, secretKeyId),
assumeSymmetricEncryption);
}
outStream.close();
/* Output */
switch (target) {
case TARGET_BYTES:
if (returnBytes) {
byte output[] = ((ByteArrayOutputStream) outStream).toByteArray();
resultData.putByteArray(RESULT_DECRYPTED_BYTES, output);
} else {
String output = new String(
((ByteArrayOutputStream) outStream).toByteArray());
resultData.putString(RESULT_DECRYPTED_STRING, output);
}
break;
case TARGET_FILE:
// nothing, file was written, just send okay and verification bundle
break;
case TARGET_STREAM:
String uri = DataStream.buildDataStreamUri(streamFilename).toString();
resultData.putString(RESULT_URI, uri);
break;
}
OtherHelper.logDebugBundle(resultData, "resultData");
sendMessageToHandler(KeychainIntentServiceHandler.MESSAGE_OKAY, resultData);
} catch (Exception e) {
sendErrorToHandler(e);
}
} else if (ACTION_SAVE_KEYRING.equals(action)) {
try {
/* Input */
String oldPassPhrase = data.getString(SAVE_KEYRING_CURRENT_PASSPHRASE);
String newPassPhrase = data.getString(SAVE_KEYRING_NEW_PASSPHRASE);
boolean canSign = true;
if (data.containsKey(SAVE_KEYRING_CAN_SIGN)) {
canSign = data.getBoolean(SAVE_KEYRING_CAN_SIGN);
}
if (newPassPhrase == null) {
newPassPhrase = oldPassPhrase;
}
ArrayList<String> userIds = data.getStringArrayList(SAVE_KEYRING_USER_IDS);
ArrayList<PGPSecretKey> keys = PgpConversionHelper.BytesToPGPSecretKeyList(data
.getByteArray(SAVE_KEYRING_KEYS));
ArrayList<Integer> keysUsages = data.getIntegerArrayList(SAVE_KEYRING_KEYS_USAGES);
long masterKeyId = data.getLong(SAVE_KEYRING_MASTER_KEY_ID);
PgpKeyOperation keyOperations = new PgpKeyOperation(this, this);
/* Operation */
if (!canSign) {
keyOperations.changeSecretKeyPassphrase(
ProviderHelper.getPGPSecretKeyRingByKeyId(this, masterKeyId),
oldPassPhrase, newPassPhrase);
} else {
keyOperations.buildSecretKey(userIds, keys, keysUsages, masterKeyId,
oldPassPhrase, newPassPhrase);
}
PassphraseCacheService.addCachedPassphrase(this, masterKeyId, newPassPhrase);
/* Output */
sendMessageToHandler(KeychainIntentServiceHandler.MESSAGE_OKAY);
} catch (Exception e) {
sendErrorToHandler(e);
}
} else if (ACTION_GENERATE_KEY.equals(action)) {
try {
/* Input */
int algorithm = data.getInt(GENERATE_KEY_ALGORITHM);
String passphrase = data.getString(GENERATE_KEY_SYMMETRIC_PASSPHRASE);
int keysize = data.getInt(GENERATE_KEY_KEY_SIZE);
PGPSecretKey masterKey = null;
if (data.containsKey(GENERATE_KEY_MASTER_KEY)) {
masterKey = PgpConversionHelper.BytesToPGPSecretKey(data
.getByteArray(GENERATE_KEY_MASTER_KEY));
}
/* Operation */
PgpKeyOperation keyOperations = new PgpKeyOperation(this, this);
PGPSecretKeyRing newKeyRing = keyOperations.createKey(algorithm, keysize,
passphrase, masterKey);
/* Output */
Bundle resultData = new Bundle();
resultData.putByteArray(RESULT_NEW_KEY,
PgpConversionHelper.PGPSecretKeyRingToBytes(newKeyRing));
OtherHelper.logDebugBundle(resultData, "resultData");
sendMessageToHandler(KeychainIntentServiceHandler.MESSAGE_OKAY, resultData);
} catch (Exception e) {
sendErrorToHandler(e);
}
} else if (ACTION_GENERATE_DEFAULT_RSA_KEYS.equals(action)) {
// generate one RSA 4096 key for signing and one subkey for encrypting!
try {
/* Input */
String passphrase = data.getString(GENERATE_KEY_SYMMETRIC_PASSPHRASE);
/* Operation */
PgpKeyOperation keyOperations = new PgpKeyOperation(this, this);
PGPSecretKeyRing masterKeyRing = keyOperations.createKey(Id.choice.algorithm.rsa,
4096, passphrase, null);
PGPSecretKeyRing subKeyRing = keyOperations.createKey(Id.choice.algorithm.rsa,
4096, passphrase, masterKeyRing.getSecretKey());
/* Output */
Bundle resultData = new Bundle();
resultData.putByteArray(RESULT_NEW_KEY,
PgpConversionHelper.PGPSecretKeyRingToBytes(masterKeyRing));
resultData.putByteArray(RESULT_NEW_KEY2,
PgpConversionHelper.PGPSecretKeyRingToBytes(subKeyRing));
OtherHelper.logDebugBundle(resultData, "resultData");
sendMessageToHandler(KeychainIntentServiceHandler.MESSAGE_OKAY, resultData);
} catch (Exception e) {
sendErrorToHandler(e);
}
} else if (ACTION_DELETE_FILE_SECURELY.equals(action)) {
try {
/* Input */
String deleteFile = data.getString(DELETE_FILE);
/* Operation */
try {
PgpHelper.deleteFileSecurely(this, this, new File(deleteFile));
} catch (FileNotFoundException e) {
throw new PgpGeneralException(
getString(R.string.error_file_not_found, deleteFile));
} catch (IOException e) {
throw new PgpGeneralException(getString(R.string.error_file_delete_failed,
deleteFile));
}
/* Output */
sendMessageToHandler(KeychainIntentServiceHandler.MESSAGE_OKAY);
} catch (Exception e) {
sendErrorToHandler(e);
}
} else if (ACTION_IMPORT_KEYRING.equals(action)) {
try {
/* Input */
int target = data.getInt(TARGET);
/* Operation */
InputStream inStream = null;
long inLength = -1;
InputData inputData = null;
switch (target) {
case TARGET_BYTES: /* import key from bytes directly */
byte[] bytes = data.getByteArray(IMPORT_BYTES);
inStream = new ByteArrayInputStream(bytes);
inLength = bytes.length;
inputData = new InputData(inStream, inLength);
break;
case TARGET_FILE: /* import key from file */
String inputFile = data.getString(IMPORT_FILENAME);
inStream = new FileInputStream(inputFile);
File file = new File(inputFile);
inLength = file.length();
inputData = new InputData(inStream, inLength);
break;
case TARGET_STREAM:
// TODO: not implemented
break;
}
Bundle resultData = new Bundle();
ArrayList<Long> keyIds = (ArrayList<Long>) data.getSerializable(IMPORT_KEY_LIST);
PgpImportExport pgpImportExport = new PgpImportExport(this, this);
resultData = pgpImportExport.importKeyRings(inputData, keyIds);
sendMessageToHandler(KeychainIntentServiceHandler.MESSAGE_OKAY, resultData);
} catch (Exception e) {
sendErrorToHandler(e);
}
} else if (ACTION_EXPORT_KEYRING.equals(action)) {
try {
/* Input */
int keyType = Id.type.public_key;
if (data.containsKey(EXPORT_KEY_TYPE)) {
keyType = data.getInt(EXPORT_KEY_TYPE);
}
String outputFile = data.getString(EXPORT_FILENAME);
boolean exportAll = data.getBoolean(EXPORT_ALL);
long keyRingMasterKeyId = -1;
if (!exportAll) {
keyRingMasterKeyId = data.getLong(EXPORT_KEY_RING_MASTER_KEY_ID);
}
/* Operation */
// check if storage is ready
if (!FileHelper.isStorageMounted(outputFile)) {
throw new PgpGeneralException(getString(R.string.error_external_storage_not_ready));
}
// OutputStream
FileOutputStream outStream = new FileOutputStream(outputFile);
ArrayList<Long> keyRingMasterKeyIds = new ArrayList<Long>();
if (exportAll) {
// get all key ring row ids based on export type
if (keyType == Id.type.public_key) {
keyRingMasterKeyIds = ProviderHelper.getPublicKeyRingsMasterKeyIds(this);
} else {
keyRingMasterKeyIds = ProviderHelper.getSecretKeyRingsMasterKeyIds(this);
}
} else {
keyRingMasterKeyIds.add(keyRingMasterKeyId);
}
Bundle resultData = new Bundle();
PgpImportExport pgpImportExport = new PgpImportExport(this, this);
resultData = pgpImportExport
.exportKeyRings(keyRingMasterKeyIds, keyType, outStream);
sendMessageToHandler(KeychainIntentServiceHandler.MESSAGE_OKAY, resultData);
} catch (Exception e) {
sendErrorToHandler(e);
}
} else if (ACTION_UPLOAD_KEYRING.equals(action)) {
try {
/* Input */
int keyRingRowId = data.getInt(UPLOAD_KEY_KEYRING_ROW_ID);
String keyServer = data.getString(UPLOAD_KEY_SERVER);
/* Operation */
HkpKeyServer server = new HkpKeyServer(keyServer);
PGPPublicKeyRing keyring = ProviderHelper.getPGPPublicKeyRingByRowId(this,
keyRingRowId);
if (keyring != null) {
PgpImportExport pgpImportExport = new PgpImportExport(this, null);
boolean uploaded = pgpImportExport.uploadKeyRingToServer(server,
(PGPPublicKeyRing) keyring);
if (!uploaded) {
throw new PgpGeneralException("Unable to export key to selected server");
}
}
sendMessageToHandler(KeychainIntentServiceHandler.MESSAGE_OKAY);
} catch (Exception e) {
sendErrorToHandler(e);
}
} else if (ACTION_QUERY_KEYRING.equals(action)) {
try {
/* Input */
int queryType = data.getInt(QUERY_KEY_TYPE);
String keyServer = data.getString(QUERY_KEY_SERVER);
String queryString = data.getString(QUERY_KEY_STRING);
long keyId = data.getLong(QUERY_KEY_ID);
/* Operation */
Bundle resultData = new Bundle();
HkpKeyServer server = new HkpKeyServer(keyServer);
if (queryType == Id.keyserver.search) {
ArrayList<KeyInfo> searchResult = server.search(queryString);
resultData.putParcelableArrayList(RESULT_QUERY_KEY_SEARCH_RESULT, searchResult);
} else if (queryType == Id.keyserver.get) {
String keyData = server.get(keyId);
resultData.putString(RESULT_QUERY_KEY_DATA, keyData);
}
sendMessageToHandler(KeychainIntentServiceHandler.MESSAGE_OKAY, resultData);
} catch (Exception e) {
sendErrorToHandler(e);
}
} else if (ACTION_SIGN_KEYRING.equals(action)) {
try {
/* Input */
long masterKeyId = data.getLong(SIGN_KEY_MASTER_KEY_ID);
long pubKeyId = data.getLong(SIGN_KEY_PUB_KEY_ID);
/* Operation */
String signaturePassPhrase = PassphraseCacheService.getCachedPassphrase(this,
masterKeyId);
PgpKeyOperation keyOperation = new PgpKeyOperation(this, this);
PGPPublicKeyRing signedPubKeyRing = keyOperation.signKey(masterKeyId, pubKeyId,
signaturePassPhrase);
// store the signed key in our local cache
PgpImportExport pgpImportExport = new PgpImportExport(this, null);
int retval = pgpImportExport.storeKeyRingInCache(signedPubKeyRing);
if (retval != Id.return_value.ok && retval != Id.return_value.updated) {
throw new PgpGeneralException("Failed to store signed key in local cache");
}
sendMessageToHandler(KeychainIntentServiceHandler.MESSAGE_OKAY);
} catch (Exception e) {
sendErrorToHandler(e);
}
}
}
private void sendErrorToHandler(Exception e) {
Log.e(Constants.TAG, "ApgService Exception: ", e);
e.printStackTrace();
Bundle data = new Bundle();
data.putString(KeychainIntentServiceHandler.DATA_ERROR, e.getMessage());
sendMessageToHandler(KeychainIntentServiceHandler.MESSAGE_EXCEPTION, null, data);
}
private void sendMessageToHandler(Integer arg1, Integer arg2, Bundle data) {
Message msg = Message.obtain();
msg.arg1 = arg1;
if (arg2 != null) {
msg.arg2 = arg2;
}
if (data != null) {
msg.setData(data);
}
try {
mMessenger.send(msg);
} catch (RemoteException e) {
Log.w(Constants.TAG, "Exception sending message, Is handler present?", e);
} catch (NullPointerException e) {
Log.w(Constants.TAG, "Messenger is null!", e);
}
}
private void sendMessageToHandler(Integer arg1, Bundle data) {
sendMessageToHandler(arg1, null, data);
}
private void sendMessageToHandler(Integer arg1) {
sendMessageToHandler(arg1, null, null);
}
/**
* Set progress of ProgressDialog by sending message to handler on UI thread
*/
public void setProgress(String message, int progress, int max) {
Log.d(Constants.TAG, "Send message by setProgress with progress=" + progress + ", max="
+ max);
Bundle data = new Bundle();
if (message != null) {
data.putString(KeychainIntentServiceHandler.DATA_MESSAGE, message);
}
data.putInt(KeychainIntentServiceHandler.DATA_PROGRESS, progress);
data.putInt(KeychainIntentServiceHandler.DATA_PROGRESS_MAX, max);
sendMessageToHandler(KeychainIntentServiceHandler.MESSAGE_UPDATE_PROGRESS, null, data);
}
public void setProgress(int resourceId, int progress, int max) {
setProgress(getString(resourceId), progress, max);
}
public void setProgress(int progress, int max) {
setProgress(null, progress, max);
}
}

View File

@@ -0,0 +1,110 @@
/*
* Copyright (C) 2012-2013 Dominik Schürmann <dominik@dominikschuermann.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.service;
import org.sufficientlysecure.keychain.ui.dialog.ProgressDialogFragment;
import org.sufficientlysecure.keychain.R;
import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.support.v4.app.FragmentActivity;
import android.widget.Toast;
public class KeychainIntentServiceHandler extends Handler {
// possible messages send from this service to handler on ui
public static final int MESSAGE_OKAY = 1;
public static final int MESSAGE_EXCEPTION = 2;
public static final int MESSAGE_UPDATE_PROGRESS = 3;
// possible data keys for messages
public static final String DATA_ERROR = "error";
public static final String DATA_PROGRESS = "progress";
public static final String DATA_PROGRESS_MAX = "max";
public static final String DATA_MESSAGE = "message";
public static final String DATA_MESSAGE_ID = "message_id";
Activity mActivity;
ProgressDialogFragment mProgressDialogFragment;
public KeychainIntentServiceHandler(Activity activity) {
this.mActivity = activity;
}
public KeychainIntentServiceHandler(Activity activity, ProgressDialogFragment progressDialogFragment) {
this.mActivity = activity;
this.mProgressDialogFragment = progressDialogFragment;
}
public KeychainIntentServiceHandler(Activity activity, int progressDialogMessageId, int progressDialogStyle) {
this.mActivity = activity;
this.mProgressDialogFragment = ProgressDialogFragment.newInstance(progressDialogMessageId,
progressDialogStyle);
}
public void showProgressDialog(FragmentActivity activity) {
mProgressDialogFragment.show(activity.getSupportFragmentManager(), "progressDialog");
}
@Override
public void handleMessage(Message message) {
Bundle data = message.getData();
switch (message.arg1) {
case MESSAGE_OKAY:
mProgressDialogFragment.dismiss();
break;
case MESSAGE_EXCEPTION:
mProgressDialogFragment.dismiss();
// show error from service
if (data.containsKey(DATA_ERROR)) {
Toast.makeText(mActivity,
mActivity.getString(R.string.error_message, data.getString(DATA_ERROR)),
Toast.LENGTH_SHORT).show();
}
break;
case MESSAGE_UPDATE_PROGRESS:
if (data.containsKey(DATA_PROGRESS) && data.containsKey(DATA_PROGRESS_MAX)) {
// update progress from service
if (data.containsKey(DATA_MESSAGE)) {
mProgressDialogFragment.setProgress(data.getString(DATA_MESSAGE),
data.getInt(DATA_PROGRESS), data.getInt(DATA_PROGRESS_MAX));
} else if (data.containsKey(DATA_MESSAGE_ID)) {
mProgressDialogFragment.setProgress(data.getInt(DATA_MESSAGE_ID),
data.getInt(DATA_PROGRESS), data.getInt(DATA_PROGRESS_MAX));
} else {
mProgressDialogFragment.setProgress(data.getInt(DATA_PROGRESS),
data.getInt(DATA_PROGRESS_MAX));
}
}
break;
default:
break;
}
}
}

View File

@@ -0,0 +1,370 @@
/*
* Copyright (C) 2012-2013 Dominik Schürmann <dominik@dominikschuermann.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.service;
import java.util.Date;
import java.util.HashMap;
import org.spongycastle.openpgp.PGPException;
import org.spongycastle.openpgp.PGPPrivateKey;
import org.spongycastle.openpgp.PGPSecretKey;
import org.spongycastle.openpgp.PGPSecretKeyRing;
import org.spongycastle.openpgp.operator.PBESecretKeyDecryptor;
import org.spongycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.Id;
import org.sufficientlysecure.keychain.helper.Preferences;
import org.sufficientlysecure.keychain.pgp.PgpKeyHelper;
import org.sufficientlysecure.keychain.provider.ProviderHelper;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Binder;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Message;
import android.os.Messenger;
import android.os.RemoteException;
import android.util.Log;
/**
* This service runs in its own process, but is available to all other processes as the main
* passphrase cache. Use the static methods addCachedPassphrase and getCachedPassphrase for
* convenience.
*
*/
public class PassphraseCacheService extends Service {
public static final String TAG = Constants.TAG + ": PassphraseCacheService";
public static final String ACTION_PASSPHRASE_CACHE_ADD = Constants.INTENT_PREFIX
+ "PASSPHRASE_CACHE_ADD";
public static final String ACTION_PASSPHRASE_CACHE_GET = Constants.INTENT_PREFIX
+ "PASSPHRASE_CACHE_GET";
public static final String BROADCAST_ACTION_PASSPHRASE_CACHE_SERVICE = Constants.INTENT_PREFIX
+ "PASSPHRASE_CACHE_BROADCAST";
public static final String EXTRA_TTL = "ttl";
public static final String EXTRA_KEY_ID = "key_id";
public static final String EXTRA_PASSPHRASE = "passphrase";
public static final String EXTRA_MESSENGER = "messenger";
private static final int REQUEST_ID = 0;
private static final long DEFAULT_TTL = 15;
private BroadcastReceiver mIntentReceiver;
private HashMap<Long, String> mPassphraseCache = new HashMap<Long, String>();
Context mContext;
/**
* This caches a new passphrase in memory by sending a new command to the service. An android
* service is only run once. Thus, when the service is already started, new commands just add
* new events to the alarm manager for new passphrases to let them timeout in the future.
*
* @param context
* @param keyId
* @param passphrase
*/
public static void addCachedPassphrase(Context context, long keyId, String passphrase) {
Log.d(TAG, "cacheNewPassphrase() for " + keyId);
Intent intent = new Intent(context, PassphraseCacheService.class);
intent.setAction(ACTION_PASSPHRASE_CACHE_ADD);
intent.putExtra(EXTRA_TTL, Preferences.getPreferences(context).getPassPhraseCacheTtl());
intent.putExtra(EXTRA_PASSPHRASE, passphrase);
intent.putExtra(EXTRA_KEY_ID, keyId);
context.startService(intent);
}
/**
* Gets a cached passphrase from memory by sending an intent to the service. This method is
* designed to wait until the service returns the passphrase.
*
* @param context
* @param keyId
* @return passphrase or null (if no passphrase is cached for this keyId)
*/
public static String getCachedPassphrase(Context context, long keyId) {
Log.d(TAG, "getCachedPassphrase() get masterKeyId for " + keyId);
Intent intent = new Intent(context, PassphraseCacheService.class);
intent.setAction(ACTION_PASSPHRASE_CACHE_GET);
final Object mutex = new Object();
final Bundle returnBundle = new Bundle();
HandlerThread handlerThread = new HandlerThread("getPassphraseThread");
handlerThread.start();
Handler returnHandler = new Handler(handlerThread.getLooper()) {
@Override
public void handleMessage(Message message) {
if (message.obj != null) {
String passphrase = ((Bundle) message.obj).getString(EXTRA_PASSPHRASE);
returnBundle.putString(EXTRA_PASSPHRASE, passphrase);
}
synchronized (mutex) {
mutex.notify();
}
// quit handlerThread
getLooper().quit();
}
};
// Create a new Messenger for the communication back
Messenger messenger = new Messenger(returnHandler);
intent.putExtra(EXTRA_KEY_ID, keyId);
intent.putExtra(EXTRA_MESSENGER, messenger);
// send intent to this service
context.startService(intent);
// Wait on mutex until passphrase is returned to handlerThread
synchronized (mutex) {
try {
mutex.wait(3000);
} catch (InterruptedException e) {
}
}
if (returnBundle.containsKey(EXTRA_PASSPHRASE)) {
return returnBundle.getString(EXTRA_PASSPHRASE);
} else {
return null;
}
}
/**
* Internal implementation to get cached passphrase.
*
* @param keyId
* @return
*/
private String getCachedPassphraseImpl(long keyId) {
Log.d(TAG, "getCachedPassphraseImpl() get masterKeyId for " + keyId);
// try to get master key id which is used as an identifier for cached passphrases
long masterKeyId = keyId;
if (masterKeyId != Id.key.symmetric) {
PGPSecretKeyRing keyRing = ProviderHelper.getPGPSecretKeyRingByKeyId(this, keyId);
if (keyRing == null) {
return null;
}
PGPSecretKey masterKey = PgpKeyHelper.getMasterKey(keyRing);
if (masterKey == null) {
return null;
}
masterKeyId = masterKey.getKeyID();
}
Log.d(TAG, "getCachedPassphraseImpl() for masterKeyId " + masterKeyId);
// get cached passphrase
String cachedPassphrase = mPassphraseCache.get(masterKeyId);
if (cachedPassphrase == null) {
// if key has no passphrase -> cache and return empty passphrase
if (!hasPassphrase(this, masterKeyId)) {
Log.d(Constants.TAG, "Key has no passphrase! Caches and returns empty passphrase!");
addCachedPassphrase(this, masterKeyId, "");
return "";
} else {
return null;
}
}
// set it again to reset the cache life cycle
Log.d(TAG, "Cache passphrase again when getting it!");
addCachedPassphrase(this, masterKeyId, cachedPassphrase);
return cachedPassphrase;
}
/**
* Checks if key has a passphrase.
*
* @param secretKeyId
* @return true if it has a passphrase
*/
public static boolean hasPassphrase(Context context, long secretKeyId) {
// check if the key has no passphrase
try {
PGPSecretKey secretKey = PgpKeyHelper.getMasterKey(ProviderHelper
.getPGPSecretKeyRingByKeyId(context, secretKeyId));
PBESecretKeyDecryptor keyDecryptor = new JcePBESecretKeyDecryptorBuilder().setProvider(
"SC").build("".toCharArray());
PGPPrivateKey testKey = secretKey.extractPrivateKey(keyDecryptor);
if (testKey != null) {
return false;
}
} catch (PGPException e) {
// silently catch
}
return true;
}
/**
* Register BroadcastReceiver that is unregistered when service is destroyed. This
* BroadcastReceiver hears on intents with ACTION_PASSPHRASE_CACHE_SERVICE to then timeout
* specific passphrases in memory.
*/
private void registerReceiver() {
if (mIntentReceiver == null) {
mIntentReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
Log.d(TAG, "Received broadcast...");
if (action.equals(BROADCAST_ACTION_PASSPHRASE_CACHE_SERVICE)) {
long keyId = intent.getLongExtra(EXTRA_KEY_ID, -1);
timeout(context, keyId);
}
}
};
IntentFilter filter = new IntentFilter();
filter.addAction(BROADCAST_ACTION_PASSPHRASE_CACHE_SERVICE);
registerReceiver(mIntentReceiver, filter);
}
}
/**
* Build pending intent that is executed by alarm manager to time out a specific passphrase
*
* @param context
* @param keyId
* @return
*/
private static PendingIntent buildIntent(Context context, long keyId) {
Intent intent = new Intent(BROADCAST_ACTION_PASSPHRASE_CACHE_SERVICE);
intent.putExtra(EXTRA_KEY_ID, keyId);
PendingIntent sender = PendingIntent.getBroadcast(context, REQUEST_ID, intent,
PendingIntent.FLAG_CANCEL_CURRENT);
return sender;
}
/**
* Executed when service is started by intent
*/
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.d(TAG, "onStartCommand()");
// register broadcastreceiver
registerReceiver();
if (intent != null && intent.getAction() != null) {
if (ACTION_PASSPHRASE_CACHE_ADD.equals(intent.getAction())) {
long ttl = intent.getLongExtra(EXTRA_TTL, DEFAULT_TTL);
long keyId = intent.getLongExtra(EXTRA_KEY_ID, -1);
String passphrase = intent.getStringExtra(EXTRA_PASSPHRASE);
Log.d(TAG,
"Received ACTION_PASSPHRASE_CACHE_ADD intent in onStartCommand() with keyId: "
+ keyId + ", ttl: " + ttl);
// add keyId and passphrase to memory
mPassphraseCache.put(keyId, passphrase);
if (ttl > 0) {
// register new alarm with keyId for this passphrase
long triggerTime = new Date().getTime() + (ttl * 1000);
AlarmManager am = (AlarmManager) this.getSystemService(Context.ALARM_SERVICE);
am.set(AlarmManager.RTC_WAKEUP, triggerTime, buildIntent(this, keyId));
}
} else if (ACTION_PASSPHRASE_CACHE_GET.equals(intent.getAction())) {
long keyId = intent.getLongExtra(EXTRA_KEY_ID, -1);
Messenger messenger = intent.getParcelableExtra(EXTRA_MESSENGER);
String passphrase = getCachedPassphraseImpl(keyId);
Message msg = Message.obtain();
Bundle bundle = new Bundle();
bundle.putString(EXTRA_PASSPHRASE, passphrase);
msg.obj = bundle;
try {
messenger.send(msg);
} catch (RemoteException e) {
Log.e(Constants.TAG, "Sending message failed", e);
}
} else {
Log.e(Constants.TAG, "Intent or Intent Action not supported!");
}
}
return START_STICKY;
}
/**
* Called when one specific passphrase for keyId timed out
*
* @param context
* @param keyId
*/
private void timeout(Context context, long keyId) {
// remove passphrase corresponding to keyId from memory
mPassphraseCache.remove(keyId);
Log.d(TAG, "Timeout of keyId " + keyId + ", removed from memory!");
// stop whole service if no cached passphrases remaining
if (mPassphraseCache.isEmpty()) {
Log.d(TAG, "No passphrases remaining in memory, stopping service!");
stopSelf();
}
}
@Override
public void onCreate() {
super.onCreate();
mContext = this;
Log.d(Constants.TAG, "PassphraseCacheService, onCreate()");
}
@Override
public void onDestroy() {
super.onDestroy();
Log.d(Constants.TAG, "PassphraseCacheService, onDestroy()");
unregisterReceiver(mIntentReceiver);
}
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
public class PassphraseCacheBinder extends Binder {
public PassphraseCacheService getService() {
return PassphraseCacheService.this;
}
}
private final IBinder mBinder = new PassphraseCacheBinder();
}

View File

@@ -0,0 +1,10 @@
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

@@ -0,0 +1,10 @@
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

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

View File

@@ -0,0 +1,10 @@
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

@@ -0,0 +1,94 @@
/*
* Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.service.remote;
import org.spongycastle.bcpg.HashAlgorithmTags;
import org.spongycastle.openpgp.PGPEncryptedData;
import org.sufficientlysecure.keychain.Id;
public class AppSettings {
private String packageName;
private byte[] packageSignature;
private long keyId = Id.key.none;
private int encryptionAlgorithm;
private int hashAlgorithm;
private int compression;
public AppSettings() {
}
public AppSettings(String packageName, byte[] packageSignature) {
super();
this.packageName = packageName;
this.packageSignature = packageSignature;
// defaults:
this.encryptionAlgorithm = PGPEncryptedData.AES_256;
this.hashAlgorithm = HashAlgorithmTags.SHA512;
this.compression = Id.choice.compression.zlib;
}
public String getPackageName() {
return packageName;
}
public void setPackageName(String packageName) {
this.packageName = packageName;
}
public byte[] getPackageSignature() {
return packageSignature;
}
public void setPackageSignature(byte[] packageSignature) {
this.packageSignature = packageSignature;
}
public long getKeyId() {
return keyId;
}
public void setKeyId(long scretKeyId) {
this.keyId = scretKeyId;
}
public int getEncryptionAlgorithm() {
return encryptionAlgorithm;
}
public void setEncryptionAlgorithm(int encryptionAlgorithm) {
this.encryptionAlgorithm = encryptionAlgorithm;
}
public int getHashAlgorithm() {
return hashAlgorithm;
}
public void setHashAlgorithm(int hashAlgorithm) {
this.hashAlgorithm = hashAlgorithm;
}
public int getCompression() {
return compression;
}
public void setCompression(int compression) {
this.compression = compression;
}
}

View File

@@ -0,0 +1,109 @@
/*
* Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.service.remote;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.helper.ActionBarHelper;
import org.sufficientlysecure.keychain.provider.ProviderHelper;
import org.sufficientlysecure.keychain.util.Log;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import com.actionbarsherlock.app.SherlockFragmentActivity;
import com.actionbarsherlock.view.Menu;
import com.actionbarsherlock.view.MenuItem;
public class AppSettingsActivity extends SherlockFragmentActivity {
private Uri mAppUri;
private AppSettingsFragment mSettingsFragment;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Inflate a "Done" custom action bar
ActionBarHelper.setDoneView(getSupportActionBar(), R.string.api_settings_save,
new View.OnClickListener() {
@Override
public void onClick(View v) {
// "Done"
save();
}
});
setContentView(R.layout.api_app_settings_activity);
mSettingsFragment = (AppSettingsFragment) getSupportFragmentManager().findFragmentById(
R.id.api_app_settings_fragment);
Intent intent = getIntent();
mAppUri = intent.getData();
if (mAppUri == null) {
Log.e(Constants.TAG, "Intent data missing. Should be Uri of app!");
finish();
return;
} else {
Log.d(Constants.TAG, "uri: " + mAppUri);
loadData(mAppUri);
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
getSupportMenuInflater().inflate(R.menu.api_app_settings, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_api_settings_revoke:
revokeAccess();
return true;
case R.id.menu_api_settings_cancel:
finish();
return true;
}
return super.onOptionsItemSelected(item);
}
private void loadData(Uri appUri) {
AppSettings settings = ProviderHelper.getApiAppSettings(this, appUri);
mSettingsFragment.setAppSettings(settings);
}
private void revokeAccess() {
if (getContentResolver().delete(mAppUri, null, null) <= 0) {
throw new RuntimeException();
}
finish();
}
private void save() {
ProviderHelper.updateApiApp(this, mSettingsFragment.getAppSettings(), mAppUri);
finish();
}
}

View File

@@ -0,0 +1,234 @@
/*
* Copyright (C) 2013-2014 Dominik Schürmann <dominik@dominikschuermann.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.service.remote;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import org.spongycastle.util.encoders.Hex;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.ui.SelectSecretKeyLayoutFragment;
import org.sufficientlysecure.keychain.ui.adapter.KeyValueSpinnerAdapter;
import org.sufficientlysecure.keychain.util.AlgorithmNames;
import org.sufficientlysecure.keychain.util.Log;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemSelectedListener;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.Spinner;
import android.widget.TextView;
import com.beardedhen.androidbootstrap.BootstrapButton;
public class AppSettingsFragment extends Fragment implements
SelectSecretKeyLayoutFragment.SelectSecretKeyCallback {
// model
private AppSettings appSettings;
// view
private LinearLayout mAdvancedSettingsContainer;
private BootstrapButton mAdvancedSettingsButton;
private TextView mAppNameView;
private ImageView mAppIconView;
private Spinner mEncryptionAlgorithm;
private Spinner mHashAlgorithm;
private Spinner mCompression;
private TextView mPackageName;
private TextView mPackageSignature;
private SelectSecretKeyLayoutFragment mSelectKeyFragment;
KeyValueSpinnerAdapter encryptionAdapter;
KeyValueSpinnerAdapter hashAdapter;
KeyValueSpinnerAdapter compressionAdapter;
public AppSettings getAppSettings() {
return appSettings;
}
public void setAppSettings(AppSettings appSettings) {
this.appSettings = appSettings;
setPackage(appSettings.getPackageName());
mPackageName.setText(appSettings.getPackageName());
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
md.update(appSettings.getPackageSignature());
byte[] digest = md.digest();
String signature = new String(Hex.encode(digest));
mPackageSignature.setText(signature);
} catch (NoSuchAlgorithmException e) {
Log.e(Constants.TAG, "Should not happen!", e);
}
mSelectKeyFragment.selectKey(appSettings.getKeyId());
mEncryptionAlgorithm.setSelection(encryptionAdapter.getPosition(appSettings
.getEncryptionAlgorithm()));
mHashAlgorithm.setSelection(hashAdapter.getPosition(appSettings.getHashAlgorithm()));
mCompression.setSelection(compressionAdapter.getPosition(appSettings.getCompression()));
}
/**
* Inflate the layout for this fragment
*/
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.api_app_settings_fragment, container, false);
initView(view);
return view;
}
private void initView(View view) {
mSelectKeyFragment = (SelectSecretKeyLayoutFragment) getFragmentManager().findFragmentById(
R.id.api_app_settings_select_key_fragment);
mSelectKeyFragment.setCallback(this);
mAdvancedSettingsButton = (BootstrapButton) view
.findViewById(R.id.api_app_settings_advanced_button);
mAdvancedSettingsContainer = (LinearLayout) view
.findViewById(R.id.api_app_settings_advanced);
mAppNameView = (TextView) view.findViewById(R.id.api_app_settings_app_name);
mAppIconView = (ImageView) view.findViewById(R.id.api_app_settings_app_icon);
mEncryptionAlgorithm = (Spinner) view
.findViewById(R.id.api_app_settings_encryption_algorithm);
mHashAlgorithm = (Spinner) view.findViewById(R.id.api_app_settings_hash_algorithm);
mCompression = (Spinner) view.findViewById(R.id.api_app_settings_compression);
mPackageName = (TextView) view.findViewById(R.id.api_app_settings_package_name);
mPackageSignature = (TextView) view.findViewById(R.id.api_app_settings_package_signature);
AlgorithmNames algorithmNames = new AlgorithmNames(getActivity());
encryptionAdapter = new KeyValueSpinnerAdapter(getActivity(),
algorithmNames.getEncryptionNames());
mEncryptionAlgorithm.setAdapter(encryptionAdapter);
mEncryptionAlgorithm.setOnItemSelectedListener(new OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
appSettings.setEncryptionAlgorithm((int) id);
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
}
});
hashAdapter = new KeyValueSpinnerAdapter(getActivity(), algorithmNames.getHashNames());
mHashAlgorithm.setAdapter(hashAdapter);
mHashAlgorithm.setOnItemSelectedListener(new OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
appSettings.setHashAlgorithm((int) id);
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
}
});
compressionAdapter = new KeyValueSpinnerAdapter(getActivity(),
algorithmNames.getCompressionNames());
mCompression.setAdapter(compressionAdapter);
mCompression.setOnItemSelectedListener(new OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
appSettings.setCompression((int) id);
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
}
});
final Animation visibleAnimation = new AlphaAnimation(0.0f, 1.0f);
visibleAnimation.setDuration(250);
final Animation invisibleAnimation = new AlphaAnimation(1.0f, 0.0f);
invisibleAnimation.setDuration(250);
// TODO: Better: collapse/expand animation
// final Animation animation2 = new TranslateAnimation(Animation.RELATIVE_TO_SELF, 0.0f,
// Animation.RELATIVE_TO_SELF, 0.0f, Animation.RELATIVE_TO_SELF, -1.0f,
// Animation.RELATIVE_TO_SELF, 0.0f);
// animation2.setDuration(150);
mAdvancedSettingsButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (mAdvancedSettingsContainer.getVisibility() == View.VISIBLE) {
mAdvancedSettingsContainer.startAnimation(invisibleAnimation);
mAdvancedSettingsContainer.setVisibility(View.GONE);
mAdvancedSettingsButton.setText(getString(R.string.api_settings_show_advanced));
mAdvancedSettingsButton.setLeftIcon("fa-caret-up");
} else {
mAdvancedSettingsContainer.startAnimation(visibleAnimation);
mAdvancedSettingsContainer.setVisibility(View.VISIBLE);
mAdvancedSettingsButton.setText(getString(R.string.api_settings_hide_advanced));
mAdvancedSettingsButton.setLeftIcon("fa-caret-down");
}
}
});
}
private void setPackage(String packageName) {
PackageManager pm = getActivity().getApplicationContext().getPackageManager();
// get application name and icon from package manager
String appName = null;
Drawable appIcon = null;
try {
ApplicationInfo ai = pm.getApplicationInfo(packageName, 0);
appName = (String) pm.getApplicationLabel(ai);
appIcon = pm.getApplicationIcon(ai);
} catch (final NameNotFoundException e) {
// fallback
appName = packageName;
}
mAppNameView.setText(appName);
mAppIconView.setImageDrawable(appIcon);
}
/**
* callback from select secret key fragment
*/
@Override
public void onKeySelected(long secretKeyId) {
appSettings.setKeyId(secretKeyId);
}
}

View File

@@ -0,0 +1,122 @@
/*
* Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.service.remote;
import java.io.ByteArrayOutputStream;
import java.io.PrintWriter;
import java.security.cert.X509Certificate;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.PasswordCallback;
import org.spongycastle.openpgp.PGPPrivateKey;
import org.spongycastle.openpgp.PGPSecretKey;
import org.spongycastle.openssl.PEMWriter;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.pgp.PgpKeyHelper;
import org.sufficientlysecure.keychain.pgp.PgpToX509;
import org.sufficientlysecure.keychain.util.Log;
import android.content.Intent;
import android.os.IBinder;
import android.os.RemoteException;
public class ExtendedApiService extends RemoteService {
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
private void selfSignedX509CertSafe(String subjAltNameURI, IExtendedApiCallback callback,
AppSettings appSettings) {
// TODO: for pgp keyrings with password
CallbackHandler pgpPwdCallbackHandler = new PgpToX509.PredefinedPasswordCallbackHandler("");
try {
long keyId = appSettings.getKeyId();
PGPSecretKey pgpSecretKey = PgpKeyHelper.getSigningKey(this, keyId);
PasswordCallback pgpSecKeyPasswordCallBack = new PasswordCallback("pgp passphrase?",
false);
pgpPwdCallbackHandler.handle(new Callback[] { pgpSecKeyPasswordCallBack });
PGPPrivateKey pgpPrivKey = pgpSecretKey.extractPrivateKey(
pgpSecKeyPasswordCallBack.getPassword(), Constants.BOUNCY_CASTLE_PROVIDER_NAME);
pgpSecKeyPasswordCallBack.clearPassword();
X509Certificate selfSignedCert = PgpToX509.createSelfSignedCert(pgpSecretKey,
pgpPrivKey, subjAltNameURI);
// Write x509cert and privKey into files
// FileOutputStream fosCert = context.openFileOutput(CERT_FILENAME,
// Context.MODE_PRIVATE);
ByteArrayOutputStream outStream = new ByteArrayOutputStream();
PEMWriter pemWriterCert = new PEMWriter(new PrintWriter(outStream));
pemWriterCert.writeObject(selfSignedCert);
pemWriterCert.close();
byte[] outputBytes = outStream.toByteArray();
callback.onSuccess(outputBytes);
} catch (Exception e) {
Log.e(Constants.TAG, "ExtendedApiService", e);
try {
callback.onError(e.getMessage());
} catch (RemoteException e1) {
Log.e(Constants.TAG, "ExtendedApiService", e);
}
}
// TODO: no private key at the moment! Don't give it to others
// PrivateKey privKey = pgpPrivKey.getKey();
// FileOutputStream fosKey = context.openFileOutput(PRIV_KEY_FILENAME,
// Context.MODE_PRIVATE);
// PEMWriter pemWriterKey = new PEMWriter(new PrintWriter(fosKey));
// pemWriterKey.writeObject(privKey);
// pemWriterKey.close();
}
private final IExtendedApiService.Stub mBinder = new IExtendedApiService.Stub() {
@Override
public void encrypt(byte[] inputBytes, String passphrase, IExtendedApiCallback callback)
throws RemoteException {
// TODO : implement
}
@Override
public void selfSignedX509Cert(final String subjAltNameURI,
final IExtendedApiCallback callback) throws RemoteException {
final AppSettings settings = getAppSettings();
Runnable r = new Runnable() {
@Override
public void run() {
selfSignedX509CertSafe(subjAltNameURI, callback, settings);
}
};
checkAndEnqueue(r);
}
};
}

View File

@@ -0,0 +1,602 @@
/*
* Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.service.remote;
import java.io.ByteArrayInputStream;
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.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.IBinder;
import android.os.Message;
import android.os.RemoteException;
public class OpenPgpService extends RemoteService {
private String getCachedPassphrase(long keyId, boolean allowUserInteraction)
throws UserInteractionRequiredException {
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.
*
* @param encryptionUserIds
* @return
*/
private long[] getKeyIdsFromEmails(String[] encryptionUserIds, boolean allowUserInteraction)
throws UserInteractionRequiredException {
// find key ids to given emails in database
ArrayList<Long> keyIds = new ArrayList<Long>();
boolean missingUserIdsCheck = false;
boolean dublicateUserIdsCheck = false;
ArrayList<String> missingUserIds = new ArrayList<String>();
ArrayList<String> dublicateUserIds = new ArrayList<String>();
for (String email : encryptionUserIds) {
Uri uri = KeychainContract.KeyRings.buildPublicKeyRingsByEmailsUri(email);
Cursor cur = getContentResolver().query(uri, null, null, null, null);
if (cur.moveToFirst()) {
long id = cur.getLong(cur.getColumnIndex(KeychainContract.KeyRings.MASTER_KEY_ID));
keyIds.add(id);
} else {
missingUserIdsCheck = true;
missingUserIds.add(email);
Log.d(Constants.TAG, "user id missing");
}
if (cur.moveToNext()) {
dublicateUserIdsCheck = true;
dublicateUserIds.add(email);
Log.d(Constants.TAG, "more than one user id with the same email");
}
}
// convert to long[]
long[] keyIdsArray = new long[keyIds.size()];
for (int i = 0; i < keyIdsArray.length; i++) {
keyIdsArray[i] = keyIds.get(i);
}
// allow the user to verify pub key selection
if (allowUserInteraction && (missingUserIdsCheck || dublicateUserIdsCheck)) {
SelectPubKeysActivityCallback callback = new SelectPubKeysActivityCallback();
Bundle extras = new Bundle();
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,
extras);
if (callback.isSuccess()) {
Log.d(Constants.TAG, "New selection of pub keys!");
keyIdsArray = callback.getPubKeyIds();
} else {
Log.d(Constants.TAG, "Pub key selection canceled!");
return null;
}
}
// if 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) {
return null;
}
return keyIdsArray;
}
public class SelectPubKeysActivityCallback extends UserInputCallback {
public static final String PUB_KEY_IDS = "pub_key_ids";
private boolean success = false;
private long[] pubKeyIds;
public boolean isSuccess() {
return success;
}
public long[] getPubKeyIds() {
return pubKeyIds;
}
@Override
public void handleUserInput(Message msg) {
if (msg.arg1 == OKAY) {
success = true;
pubKeyIds = msg.getData().getLongArray(PUB_KEY_IDS);
} else {
success = false;
}
}
};
private synchronized void getKeyIdsSafe(String[] userIds, boolean allowUserInteraction,
IOpenPgpKeyIdsCallback callback, AppSettings appSettings) {
try {
long[] keyIds = getKeyIdsFromEmails(userIds, allowUserInteraction);
if (keyIds == null) {
throw new NoUserIdsException("No user ids!");
}
callback.onSuccess(keyIds);
} catch (UserInteractionRequiredException e) {
callbackOpenPgpError(callback, OpenPgpError.USER_INTERACTION_REQUIRED, e.getMessage());
} catch (NoUserIdsException e) {
callbackOpenPgpError(callback, OpenPgpError.NO_USER_IDS, e.getMessage());
} catch (Exception e) {
callbackOpenPgpError(callback, OpenPgpError.GENERIC_ERROR, e.getMessage());
}
}
private synchronized void encryptAndSignSafe(OpenPgpData inputData,
final OpenPgpData outputData, long[] keyIds, boolean allowUserInteraction,
IOpenPgpCallback callback, AppSettings appSettings, boolean sign) {
try {
// TODO: other options of OpenPgpData!
byte[] inputBytes = getInput(inputData);
boolean asciiArmor = false;
if (outputData.getType() == OpenPgpData.TYPE_STRING) {
asciiArmor = true;
}
// add own key for encryption
keyIds = Arrays.copyOf(keyIds, keyIds.length + 1);
keyIds[keyIds.length - 1] = appSettings.getKeyId();
// build InputData and write into OutputStream
InputStream inputStream = new ByteArrayInputStream(inputBytes);
long inputLength = inputBytes.length;
InputData inputDt = new InputData(inputStream, inputLength);
OutputStream outputStream = new ByteArrayOutputStream();
PgpOperation operation = new PgpOperation(getContext(), null, inputDt, outputStream);
if (sign) {
String passphrase = getCachedPassphrase(appSettings.getKeyId(),
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 {
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);
passphrase = getCachedPassphrase(secretKeyId, allowUserInteraction);
if (passphrase == null) {
throw new WrongPassphraseException("No or wrong passphrase!");
}
}
// build InputData and write into OutputStream
InputStream inputStream = new ByteArrayInputStream(inputBytes);
long inputLength = inputBytes.length;
InputData inputData = new InputData(inputStream, inputLength);
OutputStream outputStream = new ByteArrayOutputStream();
Bundle outputBundle;
PgpOperation operation = new PgpOperation(getContext(), null, inputData, outputStream);
if (signedOnly) {
// TODO: download missing keys from keyserver?
outputBundle = operation.verifyText(false);
} 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) {
callbackOpenPgpError(callback, OpenPgpError.GENERIC_ERROR, e.getMessage());
}
}
/**
* Returns error to IOpenPgpCallback
*
* @param callback
* @param errorId
* @param message
*/
private void callbackOpenPgpError(IOpenPgpCallback 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);
}
}
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);
}
}
private final IOpenPgpService.Stub mBinder = new IOpenPgpService.Stub() {
@Override
public void encrypt(final OpenPgpData input, final OpenPgpData output, final long[] keyIds,
final IOpenPgpCallback callback) throws RemoteException {
final AppSettings settings = getAppSettings();
Runnable r = new Runnable() {
@Override
public void run() {
encryptAndSignSafe(input, output, keyIds, true, callback, settings, false);
}
};
checkAndEnqueue(r);
}
@Override
public void signAndEncrypt(final OpenPgpData input, final OpenPgpData output,
final long[] keyIds, final IOpenPgpCallback callback) throws RemoteException {
final AppSettings settings = getAppSettings();
Runnable r = new Runnable() {
@Override
public void run() {
encryptAndSignSafe(input, output, keyIds, true, callback, settings, true);
}
};
checkAndEnqueue(r);
}
@Override
public void sign(final OpenPgpData input, final OpenPgpData output,
final IOpenPgpCallback callback) throws RemoteException {
final AppSettings settings = getAppSettings();
Runnable r = new Runnable() {
@Override
public void run() {
signSafe(getInput(input), true, callback, settings);
}
};
checkAndEnqueue(r);
}
@Override
public void decryptAndVerify(final OpenPgpData input, final OpenPgpData output,
final IOpenPgpCallback callback) throws RemoteException {
final AppSettings settings = getAppSettings();
Runnable r = new Runnable() {
@Override
public void run() {
decryptAndVerifySafe(getInput(input), true, callback, settings);
}
};
checkAndEnqueue(r);
}
@Override
public void getKeyIds(final String[] userIds, final boolean allowUserInteraction,
final IOpenPgpKeyIdsCallback callback) throws RemoteException {
final AppSettings settings = getAppSettings();
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
public IBinder onBind(Intent intent) {
return mBinder;
}
}

View File

@@ -0,0 +1,76 @@
/*
* Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.service.remote;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.provider.KeychainContract.ApiApps;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.database.Cursor;
import android.support.v4.widget.CursorAdapter;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
public class RegisteredAppsAdapter extends CursorAdapter {
private LayoutInflater mInflater;
private PackageManager pm;
public RegisteredAppsAdapter(Context context, Cursor c, int flags) {
super(context, c, flags);
mInflater = LayoutInflater.from(context);
pm = context.getApplicationContext().getPackageManager();
}
@Override
public void bindView(View view, Context context, Cursor cursor) {
TextView text = (TextView) view.findViewById(R.id.api_apps_adapter_item_name);
ImageView icon = (ImageView) view.findViewById(R.id.api_apps_adapter_item_icon);
String packageName = cursor.getString(cursor.getColumnIndex(ApiApps.PACKAGE_NAME));
if (packageName != null) {
// get application name
try {
ApplicationInfo ai = pm.getApplicationInfo(packageName, 0);
text.setText(pm.getApplicationLabel(ai));
icon.setImageDrawable(pm.getApplicationIcon(ai));
} catch (final NameNotFoundException e) {
// fallback
text.setText(packageName);
}
} else {
// fallback
text.setText(packageName);
}
}
@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
return mInflater.inflate(R.layout.api_apps_adapter_list_item, null);
}
}

View File

@@ -0,0 +1,36 @@
/*
* Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.service.remote;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.ui.DrawerActivity;
import android.os.Bundle;
public class RegisteredAppsListActivity extends DrawerActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.api_apps_list_activity);
setupDrawerNavigation(savedInstanceState);
}
}

View File

@@ -0,0 +1,103 @@
/*
* Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.service.remote;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.provider.KeychainContract;
import org.sufficientlysecure.keychain.provider.KeychainContract.ApiApps;
import android.content.ContentUris;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.view.View;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import com.actionbarsherlock.app.SherlockListFragment;
public class RegisteredAppsListFragment extends SherlockListFragment implements
LoaderManager.LoaderCallbacks<Cursor> {
// This is the Adapter being used to display the list's data.
RegisteredAppsAdapter mAdapter;
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
getListView().setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> adapterView, View view, int position, long id) {
// edit app settings
Intent intent = new Intent(getActivity(), AppSettingsActivity.class);
intent.setData(ContentUris.withAppendedId(KeychainContract.ApiApps.CONTENT_URI, id));
startActivity(intent);
}
});
// Give some text to display if there is no data. In a real
// application this would come from a resource.
setEmptyText(getString(R.string.api_no_apps));
// We have a menu item to show in action bar.
setHasOptionsMenu(true);
// Create an empty adapter we will use to display the loaded data.
mAdapter = new RegisteredAppsAdapter(getActivity(), null, 0);
setListAdapter(mAdapter);
// Prepare the loader. Either re-connect with an existing one,
// or start a new one.
getLoaderManager().initLoader(0, null, this);
}
// These are the Contacts rows that we will retrieve.
static final String[] PROJECTION = new String[] { ApiApps._ID, ApiApps.PACKAGE_NAME };
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
// This is called when a new Loader needs to be created. This
// sample only has one Loader, so we don't care about the ID.
// First, pick the base URI to use depending on whether we are
// currently filtering.
Uri baseUri = ApiApps.CONTENT_URI;
// Now create and return a CursorLoader that will take care of
// creating a Cursor for the data being displayed.
return new CursorLoader(getActivity(), baseUri, PROJECTION, null, null,
ApiApps.PACKAGE_NAME + " COLLATE LOCALIZED ASC");
}
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
// Swap the new cursor in. (The framework will take care of closing the
// old cursor once we return.)
mAdapter.swapCursor(data);
}
public void onLoaderReset(Loader<Cursor> loader) {
// This is called when the last Cursor provided to onLoadFinished()
// above is about to be closed. We need to make sure we are no
// longer using it.
mAdapter.swapCursor(null);
}
}

View File

@@ -0,0 +1,354 @@
/*
* Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.service.remote;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.TimeUnit;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.provider.KeychainContract;
import org.sufficientlysecure.keychain.provider.ProviderHelper;
import org.sufficientlysecure.keychain.service.exception.WrongPackageSignatureException;
import org.sufficientlysecure.keychain.util.Log;
import org.sufficientlysecure.keychain.util.PausableThreadPoolExecutor;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.Signature;
import android.net.Uri;
import android.os.Binder;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.os.Messenger;
/**
* Abstract service class for remote APIs that handle app registration and user input.
*/
public abstract class RemoteService extends Service {
Context mContext;
private final ArrayBlockingQueue<Runnable> mPoolQueue = new ArrayBlockingQueue<Runnable>(100);
// TODO: Are these parameters okay?
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() {
return mContext;
}
/**
* Should be used from Stub implementations of AIDL interfaces to enqueue a runnable for
* execution
*
* @param r
*/
protected void checkAndEnqueue(Runnable r) {
try {
if (isCallerAllowed(false)) {
mThreadPool.execute(r);
Log.d(Constants.TAG, "Enqueued runnable…");
} else {
String[] callingPackages = getPackageManager().getPackagesForUid(
Binder.getCallingUid());
// TODO: currently simply uses first entry
String packageName = callingPackages[0];
byte[] packageSignature;
try {
packageSignature = getPackageSignature(packageName);
} catch (NameNotFoundException e) {
Log.e(Constants.TAG, "Should not happen, returning!", e);
return;
}
Log.e(Constants.TAG,
"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,
extras);
if (callback.isAllowed()) {
mThreadPool.execute(r);
Log.d(Constants.TAG, "Enqueued runnable…");
} else {
Log.d(Constants.TAG, "User disallowed app!");
}
}
} 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);
}
}
private byte[] getPackageSignature(String packageName) throws NameNotFoundException {
PackageInfo pkgInfo = getPackageManager().getPackageInfo(packageName,
PackageManager.GET_SIGNATURES);
Signature[] signatures = pkgInfo.signatures;
// TODO: Only first signature?!
byte[] packageSignature = signatures[0].toByteArray();
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
*
* @return
*/
protected AppSettings getAppSettings() {
String[] callingPackages = getPackageManager().getPackagesForUid(Binder.getCallingUid());
// get app settings for this package
for (int i = 0; i < callingPackages.length; i++) {
String currentPkg = callingPackages[i];
Uri uri = KeychainContract.ApiApps.buildByPackageNameUri(currentPkg);
AppSettings settings = ProviderHelper.getApiAppSettings(this, uri);
if (settings != null)
return settings;
}
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
* process) is in the list of allowed package names.
*
* @param allowOnlySelf
* allow only Keychain app itself
* @return true if process is allowed to use this service
* @throws WrongPackageSignatureException
*/
private boolean isCallerAllowed(boolean allowOnlySelf) throws WrongPackageSignatureException {
return isUidAllowed(Binder.getCallingUid(), allowOnlySelf);
}
private boolean isUidAllowed(int uid, boolean allowOnlySelf)
throws WrongPackageSignatureException {
if (android.os.Process.myUid() == uid) {
return true;
}
if (allowOnlySelf) { // barrier
return false;
}
String[] callingPackages = getPackageManager().getPackagesForUid(uid);
// is calling package allowed to use this service?
for (int i = 0; i < callingPackages.length; i++) {
String currentPkg = callingPackages[i];
if (isPackageAllowed(currentPkg)) {
return true;
}
}
Log.d(Constants.TAG, "Caller is NOT allowed!");
return false;
}
/**
* Checks if packageName is a registered app for the API. Does not return true for own package!
*
* @param packageName
* @return
* @throws WrongPackageSignatureException
*/
private boolean isPackageAllowed(String packageName) throws WrongPackageSignatureException {
Log.d(Constants.TAG, "packageName: " + packageName);
ArrayList<String> allowedPkgs = ProviderHelper.getRegisteredApiApps(this);
Log.d(Constants.TAG, "allowed: " + allowedPkgs);
// check if package is allowed to use our service
if (allowedPkgs.contains(packageName)) {
Log.d(Constants.TAG, "Package is allowed! packageName: " + packageName);
// check package signature
byte[] currentSig;
try {
currentSig = getPackageSignature(packageName);
} catch (NameNotFoundException e) {
throw new WrongPackageSignatureException(e.getMessage());
}
byte[] storedSig = ProviderHelper.getApiAppSignature(this, packageName);
if (Arrays.equals(currentSig, storedSig)) {
Log.d(Constants.TAG,
"Package signature is correct! (equals signature from database)");
return true;
} else {
throw new WrongPackageSignatureException(
"PACKAGE NOT ALLOWED! Signature wrong! (Signature not equals signature from database)");
}
}
return false;
}
@Override
public void onCreate() {
super.onCreate();
mContext = this;
}
}

View File

@@ -0,0 +1,352 @@
/*
* Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.service.remote;
import java.util.ArrayList;
import org.sufficientlysecure.htmltextview.HtmlTextView;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.Id;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.helper.ActionBarHelper;
import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
import org.sufficientlysecure.keychain.provider.ProviderHelper;
import org.sufficientlysecure.keychain.ui.SelectPublicKeyFragment;
import org.sufficientlysecure.keychain.ui.dialog.PassphraseDialogFragment;
import org.sufficientlysecure.keychain.util.Log;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.os.Messenger;
import android.os.RemoteException;
import android.view.View;
import android.widget.Toast;
import com.actionbarsherlock.app.SherlockFragmentActivity;
public class RemoteServiceActivity extends SherlockFragmentActivity {
public static final String ACTION_REGISTER = Constants.INTENT_PREFIX + "API_ACTIVITY_REGISTER";
public static final String ACTION_CACHE_PASSPHRASE = Constants.INTENT_PREFIX
+ "API_ACTIVITY_CACHE_PASSPHRASE";
public static final String ACTION_SELECT_PUB_KEYS = Constants.INTENT_PREFIX
+ "API_ACTIVITY_SELECT_PUB_KEYS";
public static final String ACTION_ERROR_MESSAGE = Constants.INTENT_PREFIX
+ "API_ACTIVITY_ERROR_MESSAGE";
public static final String EXTRA_MESSENGER = "messenger";
// passphrase action
public static final String EXTRA_SECRET_KEY_ID = "secret_key_id";
// register action
public static final String EXTRA_PACKAGE_NAME = "package_name";
public static final String EXTRA_PACKAGE_SIGNATURE = "package_signature";
// select pub keys action
public static final String EXTRA_SELECTED_MASTER_KEY_IDS = "master_key_ids";
public static final String EXTRA_MISSING_USER_IDS = "missing_user_ids";
public static final String EXTRA_DUBLICATE_USER_IDS = "dublicate_user_ids";
// error message
public static final String EXTRA_ERROR_MESSAGE = "error_message";
private Messenger mMessenger;
// register view
private AppSettingsFragment mSettingsFragment;
// select pub keys view
private SelectPublicKeyFragment mSelectFragment;
// has the user clicked one of the buttons
// or do we need to handle the callback in onStop()
private boolean finishHandled;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
handleActions(getIntent(), savedInstanceState);
}
@Override
protected void onStop() {
super.onStop();
if (!finishHandled) {
Message msg = Message.obtain();
msg.arg1 = RemoteService.RegisterActivityCallback.CANCEL;
try {
mMessenger.send(msg);
} catch (RemoteException e) {
Log.e(Constants.TAG, "CryptoServiceActivity", e);
}
}
}
protected void handleActions(Intent intent, Bundle savedInstanceState) {
finishHandled = false;
String action = intent.getAction();
Bundle extras = intent.getExtras();
if (extras == null) {
extras = new Bundle();
}
mMessenger = extras.getParcelable(EXTRA_MESSENGER);
/**
* com.android.crypto actions
*/
if (ACTION_REGISTER.equals(action)) {
final String packageName = extras.getString(EXTRA_PACKAGE_NAME);
final byte[] packageSignature = extras.getByteArray(EXTRA_PACKAGE_SIGNATURE);
// Inflate a "Done"/"Cancel" custom action bar view
ActionBarHelper.setDoneCancelView(getSupportActionBar(), R.string.api_register_allow,
new View.OnClickListener() {
@Override
public void onClick(View v) {
// Allow
// user needs to select a key!
if (mSettingsFragment.getAppSettings().getKeyId() == Id.key.none) {
Toast.makeText(RemoteServiceActivity.this,
R.string.api_register_error_select_key, Toast.LENGTH_LONG)
.show();
} else {
ProviderHelper.insertApiApp(RemoteServiceActivity.this,
mSettingsFragment.getAppSettings());
Message msg = Message.obtain();
msg.arg1 = RemoteService.RegisterActivityCallback.OKAY;
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;
finish();
}
}
}, R.string.api_register_disallow, new View.OnClickListener() {
@Override
public void onClick(View v) {
// Disallow
Message msg = Message.obtain();
msg.arg1 = RemoteService.RegisterActivityCallback.CANCEL;
try {
mMessenger.send(msg);
} catch (RemoteException e) {
Log.e(Constants.TAG, "CryptoServiceActivity", e);
}
finishHandled = true;
finish();
}
});
setContentView(R.layout.api_app_register_activity);
mSettingsFragment = (AppSettingsFragment) getSupportFragmentManager().findFragmentById(
R.id.api_app_settings_fragment);
AppSettings settings = new AppSettings(packageName, packageSignature);
mSettingsFragment.setAppSettings(settings);
} else if (ACTION_CACHE_PASSPHRASE.equals(action)) {
long secretKeyId = extras.getLong(EXTRA_SECRET_KEY_ID);
showPassphraseDialog(secretKeyId);
} else if (ACTION_SELECT_PUB_KEYS.equals(action)) {
long[] selectedMasterKeyIds = intent.getLongArrayExtra(EXTRA_SELECTED_MASTER_KEY_IDS);
ArrayList<String> missingUserIds = intent
.getStringArrayListExtra(EXTRA_MISSING_USER_IDS);
ArrayList<String> dublicateUserIds = intent
.getStringArrayListExtra(EXTRA_DUBLICATE_USER_IDS);
String text = new String();
text += "<b>" + getString(R.string.api_select_pub_keys_text) + "</b>";
text += "<br/><br/>";
if (missingUserIds != null && missingUserIds.size() > 0) {
text += getString(R.string.api_select_pub_keys_missing_text);
text += "<br/>";
text += "<ul>";
for (String userId : missingUserIds) {
text += "<li>" + userId + "</li>";
}
text += "</ul>";
text += "<br/>";
}
if (dublicateUserIds != null && dublicateUserIds.size() > 0) {
text += getString(R.string.api_select_pub_keys_dublicates_text);
text += "<br/>";
text += "<ul>";
for (String userId : dublicateUserIds) {
text += "<li>" + userId + "</li>";
}
text += "</ul>";
}
// Inflate a "Done"/"Cancel" custom action bar view
ActionBarHelper.setDoneCancelView(getSupportActionBar(), R.string.btn_okay,
new View.OnClickListener() {
@Override
public void onClick(View v) {
// ok
Message msg = Message.obtain();
msg.arg1 = OpenPgpService.SelectPubKeysActivityCallback.OKAY;
Bundle data = new Bundle();
data.putLongArray(
OpenPgpService.SelectPubKeysActivityCallback.PUB_KEY_IDS,
mSelectFragment.getSelectedMasterKeyIds());
msg.setData(data);
try {
mMessenger.send(msg);
} catch (RemoteException e) {
Log.e(Constants.TAG, "CryptoServiceActivity", e);
}
finishHandled = true;
finish();
}
}, R.string.btn_do_not_save, new View.OnClickListener() {
@Override
public void onClick(View v) {
// cancel
Message msg = Message.obtain();
msg.arg1 = OpenPgpService.SelectPubKeysActivityCallback.CANCEL;
;
try {
mMessenger.send(msg);
} catch (RemoteException e) {
Log.e(Constants.TAG, "CryptoServiceActivity", e);
}
finishHandled = true;
finish();
}
});
setContentView(R.layout.api_app_select_pub_keys_activity);
// set text on view
HtmlTextView textView = (HtmlTextView) findViewById(R.id.api_select_pub_keys_text);
textView.setHtmlFromString(text);
/* Load select pub keys fragment */
// Check that the activity is using the layout version with
// the fragment_container FrameLayout
if (findViewById(R.id.api_select_pub_keys_fragment_container) != null) {
// However, if we're being restored from a previous state,
// then we don't need to do anything and should return or else
// we could end up with overlapping fragments.
if (savedInstanceState != null) {
return;
}
// Create an instance of the fragment
mSelectFragment = SelectPublicKeyFragment.newInstance(selectedMasterKeyIds);
// Add the fragment to the 'fragment_container' FrameLayout
getSupportFragmentManager().beginTransaction()
.add(R.id.api_select_pub_keys_fragment_container, mSelectFragment).commit();
}
} else if (ACTION_ERROR_MESSAGE.equals(action)) {
String errorMessage = intent.getStringExtra(EXTRA_ERROR_MESSAGE);
String text = new String();
text += "<font color=\"red\">" + errorMessage + "</font>";
// Inflate a "Done" custom action bar view
ActionBarHelper.setDoneView(getSupportActionBar(), R.string.btn_okay,
new View.OnClickListener() {
@Override
public void onClick(View v) {
finish();
}
});
setContentView(R.layout.api_app_error_message);
// set text on view
HtmlTextView textView = (HtmlTextView) findViewById(R.id.api_app_error_message_text);
textView.setHtmlFromString(text);
} else {
Log.e(Constants.TAG, "Wrong action!");
finish();
}
}
/**
* Shows passphrase dialog to cache a new passphrase the user enters for using it later for
* encryption. Based on mSecretKeyId it asks for a passphrase to open a private key or it asks
* for a symmetric passphrase
*/
private void showPassphraseDialog(long secretKeyId) {
// Message is received after passphrase is cached
Handler returnHandler = new Handler() {
@Override
public void handleMessage(Message message) {
if (message.what == PassphraseDialogFragment.MESSAGE_OKAY) {
Message msg = Message.obtain();
msg.arg1 = OpenPgpService.PassphraseActivityCallback.OKAY;
try {
mMessenger.send(msg);
} catch (RemoteException e) {
Log.e(Constants.TAG, "CryptoServiceActivity", e);
}
} else {
Message msg = Message.obtain();
msg.arg1 = OpenPgpService.PassphraseActivityCallback.CANCEL;
try {
mMessenger.send(msg);
} catch (RemoteException e) {
Log.e(Constants.TAG, "CryptoServiceActivity", e);
}
}
finishHandled = true;
finish();
}
};
// Create a new Messenger for the communication back
Messenger messenger = new Messenger(returnHandler);
try {
PassphraseDialogFragment passphraseDialog = PassphraseDialogFragment.newInstance(this,
messenger, secretKeyId);
passphraseDialog.show(getSupportFragmentManager(), "passphraseDialog");
} catch (PgpGeneralException e) {
Log.d(Constants.TAG, "No passphrase for this secret key, encrypt directly!");
// send message to handler to start encryption directly
returnHandler.sendEmptyMessage(PassphraseDialogFragment.MESSAGE_OKAY);
}
}
}