diff options
Diffstat (limited to 'OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote')
13 files changed, 2169 insertions, 0 deletions
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/AccountSettings.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/AccountSettings.java new file mode 100644 index 000000000..832cbc752 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/AccountSettings.java @@ -0,0 +1,85 @@ +/* + * 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.remote; + +import org.spongycastle.bcpg.HashAlgorithmTags; +import org.spongycastle.openpgp.PGPEncryptedData; +import org.sufficientlysecure.keychain.Id; + +public class AccountSettings { + private String mAccountName; + private long mKeyId = Id.key.none; + private int mEncryptionAlgorithm; + private int mHashAlgorithm; + private int mCompression; + + public AccountSettings() { + + } + + public AccountSettings(String accountName) { + super(); + this.mAccountName = accountName; + + // defaults: + this.mEncryptionAlgorithm = PGPEncryptedData.AES_256; + this.mHashAlgorithm = HashAlgorithmTags.SHA512; + this.mCompression = Id.choice.compression.zlib; + } + + public String getAccountName() { + return mAccountName; + } + + public void setAccountName(String mAccountName) { + this.mAccountName = mAccountName; + } + + public long getKeyId() { + return mKeyId; + } + + public void setKeyId(long scretKeyId) { + this.mKeyId = scretKeyId; + } + + public int getEncryptionAlgorithm() { + return mEncryptionAlgorithm; + } + + public void setEncryptionAlgorithm(int encryptionAlgorithm) { + this.mEncryptionAlgorithm = encryptionAlgorithm; + } + + public int getHashAlgorithm() { + return mHashAlgorithm; + } + + public void setHashAlgorithm(int hashAlgorithm) { + this.mHashAlgorithm = hashAlgorithm; + } + + public int getCompression() { + return mCompression; + } + + public void setCompression(int compression) { + this.mCompression = compression; + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/AppSettings.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/AppSettings.java new file mode 100644 index 000000000..a3f9f84c9 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/AppSettings.java @@ -0,0 +1,50 @@ +/* + * 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.remote; + +public class AppSettings { + private String mPackageName; + private byte[] mPackageSignature; + + public AppSettings() { + + } + + public AppSettings(String packageName, byte[] packageSignature) { + super(); + this.mPackageName = packageName; + this.mPackageSignature = packageSignature; + } + + public String getPackageName() { + return mPackageName; + } + + public void setPackageName(String packageName) { + this.mPackageName = packageName; + } + + public byte[] getPackageSignature() { + return mPackageSignature; + } + + public void setPackageSignature(byte[] packageSignature) { + this.mPackageSignature = packageSignature; + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/OpenPgpService.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/OpenPgpService.java new file mode 100644 index 000000000..b38fea5a9 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/OpenPgpService.java @@ -0,0 +1,484 @@ +/* + * 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.remote; + +import android.app.PendingIntent; +import android.content.Intent; +import android.database.Cursor; +import android.net.Uri; +import android.os.IBinder; +import android.os.ParcelFileDescriptor; + +import org.openintents.openpgp.IOpenPgpService; +import org.openintents.openpgp.OpenPgpError; +import org.openintents.openpgp.OpenPgpSignatureResult; +import org.openintents.openpgp.util.OpenPgpApi; +import org.spongycastle.util.Arrays; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.Id; +import org.sufficientlysecure.keychain.pgp.PgpDecryptVerify; +import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyResult; +import org.sufficientlysecure.keychain.pgp.PgpSignEncrypt; +import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException; +import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; +import org.sufficientlysecure.keychain.provider.KeychainContract.ApiAccounts; +import org.sufficientlysecure.keychain.provider.ProviderHelper; +import org.sufficientlysecure.keychain.remote.ui.RemoteServiceActivity; +import org.sufficientlysecure.keychain.service.PassphraseCacheService; +import org.sufficientlysecure.keychain.ui.ImportKeysActivity; +import org.sufficientlysecure.keychain.util.InputData; +import org.sufficientlysecure.keychain.util.Log; + +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Set; + +public class OpenPgpService extends RemoteService { + + /** + * Search database for key ids based on emails. + * + * @param encryptionUserIds + * @return + */ + private Intent getKeyIdsFromEmails(Intent data, String[] encryptionUserIds) { + // find key ids to given emails in database + ArrayList<Long> keyIds = new ArrayList<Long>(); + + boolean missingUserIdsCheck = false; + boolean duplicateUserIdsCheck = false; + ArrayList<String> missingUserIds = new ArrayList<String>(); + ArrayList<String> duplicateUserIds = new ArrayList<String>(); + + for (String email : encryptionUserIds) { + Uri uri = KeyRings.buildUnifiedKeyRingsFindByEmailUri(email); + Cursor cur = getContentResolver().query(uri, null, null, null, null); + if (cur.moveToFirst()) { + long id = cur.getLong(cur.getColumnIndex(KeyRings.MASTER_KEY_ID)); + keyIds.add(id); + } else { + missingUserIdsCheck = true; + missingUserIds.add(email); + Log.d(Constants.TAG, "user id missing"); + } + if (cur.moveToNext()) { + duplicateUserIdsCheck = true; + duplicateUserIds.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 (missingUserIdsCheck || duplicateUserIdsCheck) { + // build PendingIntent + Intent intent = new Intent(getBaseContext(), RemoteServiceActivity.class); + intent.setAction(RemoteServiceActivity.ACTION_SELECT_PUB_KEYS); + intent.putExtra(RemoteServiceActivity.EXTRA_SELECTED_MASTER_KEY_IDS, keyIdsArray); + intent.putExtra(RemoteServiceActivity.EXTRA_MISSING_USER_IDS, missingUserIds); + intent.putExtra(RemoteServiceActivity.EXTRA_DUBLICATE_USER_IDS, duplicateUserIds); + intent.putExtra(RemoteServiceActivity.EXTRA_DATA, data); + + PendingIntent pi = PendingIntent.getActivity(getBaseContext(), 0, + intent, + PendingIntent.FLAG_CANCEL_CURRENT); + + // return PendingIntent to be executed by client + Intent result = new Intent(); + result.putExtra(OpenPgpApi.RESULT_INTENT, pi); + result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED); + return result; + } + + if (keyIdsArray.length == 0) { + return null; + } + + Intent result = new Intent(); + result.putExtra(OpenPgpApi.EXTRA_KEY_IDS, keyIdsArray); + result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_SUCCESS); + return result; + } + + private Intent getPassphraseBundleIntent(Intent data, long keyId) { + // build PendingIntent for passphrase input + Intent intent = new Intent(getBaseContext(), RemoteServiceActivity.class); + intent.setAction(RemoteServiceActivity.ACTION_CACHE_PASSPHRASE); + intent.putExtra(RemoteServiceActivity.EXTRA_SECRET_KEY_ID, keyId); + // pass params through to activity that it can be returned again later to repeat pgp operation + intent.putExtra(RemoteServiceActivity.EXTRA_DATA, data); + PendingIntent pi = PendingIntent.getActivity(getBaseContext(), 0, + intent, + PendingIntent.FLAG_CANCEL_CURRENT); + + // return PendingIntent to be executed by client + Intent result = new Intent(); + result.putExtra(OpenPgpApi.RESULT_INTENT, pi); + result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED); + return result; + } + + private Intent signImpl(Intent data, ParcelFileDescriptor input, + ParcelFileDescriptor output, AccountSettings accSettings) { + try { + boolean asciiArmor = data.getBooleanExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true); + + // get passphrase from cache, if key has "no" passphrase, this returns an empty String + String passphrase; + if (data.hasExtra(OpenPgpApi.EXTRA_PASSPHRASE)) { + passphrase = data.getStringExtra(OpenPgpApi.EXTRA_PASSPHRASE); + } else { + passphrase = PassphraseCacheService.getCachedPassphrase(getContext(), accSettings.getKeyId()); + } + if (passphrase == null) { + // get PendingIntent for passphrase input, add it to given params and return to client + Intent passphraseBundle = getPassphraseBundleIntent(data, accSettings.getKeyId()); + return passphraseBundle; + } + + // Get Input- and OutputStream from ParcelFileDescriptor + InputStream is = new ParcelFileDescriptor.AutoCloseInputStream(input); + OutputStream os = new ParcelFileDescriptor.AutoCloseOutputStream(output); + try { + long inputLength = is.available(); + InputData inputData = new InputData(is, inputLength); + + // sign-only + PgpSignEncrypt.Builder builder = new PgpSignEncrypt.Builder(getContext(), inputData, os); + builder.enableAsciiArmorOutput(asciiArmor) + .signatureHashAlgorithm(accSettings.getHashAlgorithm()) + .signatureForceV3(false) + .signatureKeyId(accSettings.getKeyId()) + .signaturePassphrase(passphrase); + builder.build().execute(); + } finally { + is.close(); + os.close(); + } + + Intent result = new Intent(); + result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_SUCCESS); + return result; + } catch (Exception e) { + Intent result = new Intent(); + result.putExtra(OpenPgpApi.RESULT_ERROR, + new OpenPgpError(OpenPgpError.GENERIC_ERROR, e.getMessage())); + result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR); + return result; + } + } + + private Intent encryptAndSignImpl(Intent data, ParcelFileDescriptor input, + ParcelFileDescriptor output, AccountSettings accSettings, + boolean sign) { + try { + boolean asciiArmor = data.getBooleanExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true); + + long[] keyIds; + if (data.hasExtra(OpenPgpApi.EXTRA_KEY_IDS)) { + keyIds = data.getLongArrayExtra(OpenPgpApi.EXTRA_KEY_IDS); + } else if (data.hasExtra(OpenPgpApi.EXTRA_USER_IDS)) { + // get key ids based on given user ids + String[] userIds = data.getStringArrayExtra(OpenPgpApi.EXTRA_USER_IDS); + // give params through to activity... + Intent result = getKeyIdsFromEmails(data, userIds); + + if (result.getIntExtra(OpenPgpApi.RESULT_CODE, 0) == OpenPgpApi.RESULT_CODE_SUCCESS) { + keyIds = result.getLongArrayExtra(OpenPgpApi.EXTRA_KEY_IDS); + } else { + // if not success -> result contains a PendingIntent for user interaction + return result; + } + } else { + Intent result = new Intent(); + result.putExtra(OpenPgpApi.RESULT_ERROR, + new OpenPgpError(OpenPgpError.GENERIC_ERROR, + "Missing parameter user_ids or key_ids!")); + result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR); + return result; + } + + // add own key for encryption + keyIds = Arrays.copyOf(keyIds, keyIds.length + 1); + keyIds[keyIds.length - 1] = accSettings.getKeyId(); + + // build InputData and write into OutputStream + // Get Input- and OutputStream from ParcelFileDescriptor + InputStream is = new ParcelFileDescriptor.AutoCloseInputStream(input); + OutputStream os = new ParcelFileDescriptor.AutoCloseOutputStream(output); + try { + long inputLength = is.available(); + InputData inputData = new InputData(is, inputLength); + + PgpSignEncrypt.Builder builder = new PgpSignEncrypt.Builder(getContext(), inputData, os); + builder.enableAsciiArmorOutput(asciiArmor) + .compressionId(accSettings.getCompression()) + .symmetricEncryptionAlgorithm(accSettings.getEncryptionAlgorithm()) + .encryptionKeyIds(keyIds); + + if (sign) { + String passphrase; + if (data.hasExtra(OpenPgpApi.EXTRA_PASSPHRASE)) { + passphrase = data.getStringExtra(OpenPgpApi.EXTRA_PASSPHRASE); + } else { + passphrase = PassphraseCacheService.getCachedPassphrase(getContext(), + accSettings.getKeyId()); + } + if (passphrase == null) { + // get PendingIntent for passphrase input, add it to given params and return to client + Intent passphraseBundle = getPassphraseBundleIntent(data, accSettings.getKeyId()); + return passphraseBundle; + } + + // sign and encrypt + builder.signatureHashAlgorithm(accSettings.getHashAlgorithm()) + .signatureForceV3(false) + .signatureKeyId(accSettings.getKeyId()) + .signaturePassphrase(passphrase); + } else { + // encrypt only + builder.signatureKeyId(Id.key.none); + } + // execute PGP operation! + builder.build().execute(); + } finally { + is.close(); + os.close(); + } + + Intent result = new Intent(); + result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_SUCCESS); + return result; + } catch (Exception e) { + Intent result = new Intent(); + result.putExtra(OpenPgpApi.RESULT_ERROR, + new OpenPgpError(OpenPgpError.GENERIC_ERROR, e.getMessage())); + result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR); + return result; + } + } + + private Intent decryptAndVerifyImpl(Intent data, ParcelFileDescriptor input, + ParcelFileDescriptor output, Set<Long> allowedKeyIds) { + try { + // Get Input- and OutputStream from ParcelFileDescriptor + InputStream is = new ParcelFileDescriptor.AutoCloseInputStream(input); + OutputStream os = new ParcelFileDescriptor.AutoCloseOutputStream(output); + + Intent result = new Intent(); + try { + + String passphrase = data.getStringExtra(OpenPgpApi.EXTRA_PASSPHRASE); + long inputLength = is.available(); + InputData inputData = new InputData(is, inputLength); + + PgpDecryptVerify.Builder builder = new PgpDecryptVerify.Builder(this, inputData, os); + builder.allowSymmetricDecryption(false) // no support for symmetric encryption + .allowedKeyIds(allowedKeyIds) // allow only private keys associated with + // accounts of this app + .passphrase(passphrase); + + // TODO: currently does not support binary signed-only content + PgpDecryptVerifyResult decryptVerifyResult = builder.build().execute(); + + if (PgpDecryptVerifyResult.KEY_PASSHRASE_NEEDED == decryptVerifyResult.getStatus()) { + // get PendingIntent for passphrase input, add it to given params and return to client + Intent passphraseBundle = + getPassphraseBundleIntent(data, decryptVerifyResult.getKeyIdPassphraseNeeded()); + return passphraseBundle; + } else if (PgpDecryptVerifyResult.SYMMETRIC_PASSHRASE_NEEDED == + decryptVerifyResult.getStatus()) { + throw new PgpGeneralException("Decryption of symmetric content not supported by API!"); + } + + OpenPgpSignatureResult signatureResult = decryptVerifyResult.getSignatureResult(); + if (signatureResult != null) { + if (signatureResult.getStatus() == OpenPgpSignatureResult.SIGNATURE_UNKNOWN_PUB_KEY) { + // If signature is unknown we return an _additional_ PendingIntent + // to retrieve the missing key + Intent intent = new Intent(getBaseContext(), ImportKeysActivity.class); + intent.setAction(ImportKeysActivity.ACTION_IMPORT_KEY_FROM_KEYSERVER_AND_RETURN); + intent.putExtra(ImportKeysActivity.EXTRA_KEY_ID, signatureResult.getKeyId()); + intent.putExtra(ImportKeysActivity.EXTRA_PENDING_INTENT_DATA, data); + + PendingIntent pi = PendingIntent.getActivity(getBaseContext(), 0, + intent, + PendingIntent.FLAG_CANCEL_CURRENT); + + result.putExtra(OpenPgpApi.RESULT_INTENT, pi); + } + + result.putExtra(OpenPgpApi.RESULT_SIGNATURE, signatureResult); + } + + } finally { + is.close(); + os.close(); + } + + result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_SUCCESS); + return result; + } catch (Exception e) { + Intent result = new Intent(); + result.putExtra(OpenPgpApi.RESULT_ERROR, + new OpenPgpError(OpenPgpError.GENERIC_ERROR, e.getMessage())); + result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR); + return result; + } + } + + private Intent getKeyImpl(Intent data) { + try { + long keyId = data.getLongExtra(OpenPgpApi.EXTRA_KEY_ID, 0); + + if (ProviderHelper.getPGPPublicKeyRing(this, keyId) == null) { + Intent result = new Intent(); + + // If keys are not in db we return an additional PendingIntent + // to retrieve the missing key + Intent intent = new Intent(getBaseContext(), ImportKeysActivity.class); + intent.setAction(ImportKeysActivity.ACTION_IMPORT_KEY_FROM_KEYSERVER_AND_RETURN); + intent.putExtra(ImportKeysActivity.EXTRA_KEY_ID, keyId); + intent.putExtra(ImportKeysActivity.EXTRA_PENDING_INTENT_DATA, data); + + PendingIntent pi = PendingIntent.getActivity(getBaseContext(), 0, + intent, + PendingIntent.FLAG_CANCEL_CURRENT); + + result.putExtra(OpenPgpApi.RESULT_INTENT, pi); + result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED); + return result; + } else { + Intent result = new Intent(); + result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_SUCCESS); + + // TODO: also return PendingIntent that opens the key view activity + + return result; + } + } catch (Exception e) { + Intent result = new Intent(); + result.putExtra(OpenPgpApi.RESULT_ERROR, + new OpenPgpError(OpenPgpError.GENERIC_ERROR, e.getMessage())); + result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR); + return result; + } + } + + private Intent getKeyIdsImpl(Intent data) { + // get key ids based on given user ids + String[] userIds = data.getStringArrayExtra(OpenPgpApi.EXTRA_USER_IDS); + Intent result = getKeyIdsFromEmails(data, userIds); + return result; + } + + /** + * Check requirements: + * - params != null + * - has supported API version + * - is allowed to call the service (access has been granted) + * + * @param data + * @return null if everything is okay, or a Bundle with an error/PendingIntent + */ + private Intent checkRequirements(Intent data) { + // params Bundle is required! + if (data == null) { + Intent result = new Intent(); + OpenPgpError error = new OpenPgpError(OpenPgpError.GENERIC_ERROR, "params Bundle required!"); + result.putExtra(OpenPgpApi.RESULT_ERROR, error); + result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR); + return result; + } + + // version code is required and needs to correspond to version code of service! + if (data.getIntExtra(OpenPgpApi.EXTRA_API_VERSION, -1) != OpenPgpApi.API_VERSION) { + Intent result = new Intent(); + OpenPgpError error = new OpenPgpError + (OpenPgpError.INCOMPATIBLE_API_VERSIONS, "Incompatible API versions!"); + result.putExtra(OpenPgpApi.RESULT_ERROR, error); + result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR); + return result; + } + + // check if caller is allowed to access openpgp keychain + Intent result = isAllowed(data); + if (result != null) { + return result; + } + + return null; + } + + // TODO: multi-threading + private final IOpenPgpService.Stub mBinder = new IOpenPgpService.Stub() { + + @Override + public Intent execute(Intent data, ParcelFileDescriptor input, ParcelFileDescriptor output) { + Intent errorResult = checkRequirements(data); + if (errorResult != null) { + return errorResult; + } + + String accName; + if (data.getStringExtra(OpenPgpApi.EXTRA_ACCOUNT_NAME) != null) { + accName = data.getStringExtra(OpenPgpApi.EXTRA_ACCOUNT_NAME); + } else { + accName = "default"; + } + final AccountSettings accSettings = getAccSettings(accName); + if (accSettings == null) { + return getCreateAccountIntent(data, accName); + } + + String action = data.getAction(); + if (OpenPgpApi.ACTION_SIGN.equals(action)) { + return signImpl(data, input, output, accSettings); + } else if (OpenPgpApi.ACTION_ENCRYPT.equals(action)) { + return encryptAndSignImpl(data, input, output, accSettings, false); + } else if (OpenPgpApi.ACTION_SIGN_AND_ENCRYPT.equals(action)) { + return encryptAndSignImpl(data, input, output, accSettings, true); + } else if (OpenPgpApi.ACTION_DECRYPT_VERIFY.equals(action)) { + String currentPkg = getCurrentCallingPackage(); + Set<Long> allowedKeyIds = + ProviderHelper.getAllKeyIdsForApp(mContext, + ApiAccounts.buildBaseUri(currentPkg)); + return decryptAndVerifyImpl(data, input, output, allowedKeyIds); + } else if (OpenPgpApi.ACTION_GET_KEY.equals(action)) { + return getKeyImpl(data); + } else if (OpenPgpApi.ACTION_GET_KEY_IDS.equals(action)) { + return getKeyIdsImpl(data); + } else { + return null; + } + } + + }; + + @Override + public IBinder onBind(Intent intent) { + return mBinder; + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/RemoteService.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/RemoteService.java new file mode 100644 index 000000000..16a800022 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/RemoteService.java @@ -0,0 +1,261 @@ +/* + * 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.remote; + +import android.app.PendingIntent; +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 org.openintents.openpgp.OpenPgpError; +import org.openintents.openpgp.util.OpenPgpApi; +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.remote.ui.RemoteServiceActivity; +import org.sufficientlysecure.keychain.util.Log; + +import java.util.ArrayList; +import java.util.Arrays; + +/** + * Abstract service class for remote APIs that handle app registration and user input. + */ +public abstract class RemoteService extends Service { + Context mContext; + + public Context getContext() { + return mContext; + } + + protected Intent isAllowed(Intent data) { + try { + if (isCallerAllowed(false)) { + return null; + } else { + String packageName = getCurrentCallingPackage(); + Log.d(Constants.TAG, "isAllowed packageName: " + packageName); + + byte[] packageSignature; + try { + packageSignature = getPackageSignature(packageName); + } catch (NameNotFoundException e) { + Log.e(Constants.TAG, "Should not happen, returning!", e); + // return error + Intent result = new Intent(); + result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR); + result.putExtra(OpenPgpApi.RESULT_ERROR, + new OpenPgpError(OpenPgpError.GENERIC_ERROR, e.getMessage())); + return result; + } + Log.e(Constants.TAG, "Not allowed to use service! return PendingIntent for registration!"); + + Intent intent = new Intent(getBaseContext(), RemoteServiceActivity.class); + intent.setAction(RemoteServiceActivity.ACTION_REGISTER); + intent.putExtra(RemoteServiceActivity.EXTRA_PACKAGE_NAME, packageName); + intent.putExtra(RemoteServiceActivity.EXTRA_PACKAGE_SIGNATURE, packageSignature); + intent.putExtra(RemoteServiceActivity.EXTRA_DATA, data); + + PendingIntent pi = PendingIntent.getActivity(getBaseContext(), 0, + intent, + PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_ONE_SHOT); + + // return PendingIntent to be executed by client + Intent result = new Intent(); + result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED); + result.putExtra(OpenPgpApi.RESULT_INTENT, pi); + + return result; + } + } catch (WrongPackageSignatureException e) { + Log.e(Constants.TAG, "wrong signature!", e); + + Intent intent = new Intent(getBaseContext(), RemoteServiceActivity.class); + intent.setAction(RemoteServiceActivity.ACTION_ERROR_MESSAGE); + intent.putExtra(RemoteServiceActivity.EXTRA_ERROR_MESSAGE, + getString(R.string.api_error_wrong_signature)); + intent.putExtra(RemoteServiceActivity.EXTRA_DATA, data); + + PendingIntent pi = PendingIntent.getActivity(getBaseContext(), 0, + intent, + PendingIntent.FLAG_CANCEL_CURRENT); + + // return PendingIntent to be executed by client + Intent result = new Intent(); + result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED); + result.putExtra(OpenPgpApi.RESULT_INTENT, pi); + + return result; + } + } + + 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; + } + + /** + * Returns package name associated with the UID, which is assigned to the process that sent you the + * current transaction that is being processed :) + * + * @return package name + */ + protected String getCurrentCallingPackage() { + // TODO: + // callingPackages contains more than one entry when sharedUserId has been used... + String[] callingPackages = getPackageManager().getPackagesForUid(Binder.getCallingUid()); + String currentPkg = callingPackages[0]; + Log.d(Constants.TAG, "currentPkg: " + currentPkg); + + return currentPkg; + } + + /** + * Retrieves AccountSettings from database for the application calling this remote service + * + * @return + */ + protected AccountSettings getAccSettings(String accountName) { + String currentPkg = getCurrentCallingPackage(); + Log.d(Constants.TAG, "accountName: " + accountName); + + Uri uri = KeychainContract.ApiAccounts.buildByPackageAndAccountUri(currentPkg, accountName); + + AccountSettings settings = ProviderHelper.getApiAccountSettings(this, uri); + + return settings; // can be null! + } + + protected Intent getCreateAccountIntent(Intent data, String accountName) { + String packageName = getCurrentCallingPackage(); + Log.d(Constants.TAG, "accountName: " + accountName); + + Intent intent = new Intent(getBaseContext(), RemoteServiceActivity.class); + intent.setAction(RemoteServiceActivity.ACTION_CREATE_ACCOUNT); + intent.putExtra(RemoteServiceActivity.EXTRA_PACKAGE_NAME, packageName); + intent.putExtra(RemoteServiceActivity.EXTRA_ACC_NAME, accountName); + intent.putExtra(RemoteServiceActivity.EXTRA_DATA, data); + + PendingIntent pi = PendingIntent.getActivity(getBaseContext(), 0, + intent, + PendingIntent.FLAG_CANCEL_CURRENT); + + // return PendingIntent to be executed by client + Intent result = new Intent(); + result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED); + result.putExtra(OpenPgpApi.RESULT_INTENT, pi); + + return result; + } + + /** + * 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, "Uid 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, "isPackageAllowed 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)"); + } + } + + Log.d(Constants.TAG, "Package is NOT allowed! packageName: " + packageName); + return false; + } + + @Override + public void onCreate() { + super.onCreate(); + mContext = this; + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/WrongPackageSignatureException.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/WrongPackageSignatureException.java new file mode 100644 index 000000000..6f44a65e9 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/WrongPackageSignatureException.java @@ -0,0 +1,27 @@ +/* + * 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.remote; + +public class WrongPackageSignatureException extends Exception { + + private static final long serialVersionUID = -8294642703122196028L; + + public WrongPackageSignatureException(String message) { + super(message); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AccountSettingsActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AccountSettingsActivity.java new file mode 100644 index 000000000..123ed526f --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AccountSettingsActivity.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package org.sufficientlysecure.keychain.remote.ui; + +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.support.v7.app.ActionBarActivity; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; + +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.remote.AccountSettings; +import org.sufficientlysecure.keychain.util.Log; + +public class AccountSettingsActivity extends ActionBarActivity { + private Uri mAccountUri; + + private AccountSettingsFragment mAccountSettingsFragment; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // Inflate a "Done" custom action bar + ActionBarHelper.setOneButtonView(getSupportActionBar(), + R.string.api_settings_save, R.drawable.ic_action_done, + new View.OnClickListener() { + @Override + public void onClick(View v) { + // "Done" + save(); + } + }); + + setContentView(R.layout.api_account_settings_activity); + + mAccountSettingsFragment = (AccountSettingsFragment) getSupportFragmentManager().findFragmentById( + R.id.api_account_settings_fragment); + + Intent intent = getIntent(); + mAccountUri = intent.getData(); + if (mAccountUri == null) { + Log.e(Constants.TAG, "Intent data missing. Should be Uri of app!"); + finish(); + return; + } else { + Log.d(Constants.TAG, "uri: " + mAccountUri); + loadData(mAccountUri); + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + super.onCreateOptionsMenu(menu); + getMenuInflater().inflate(R.menu.api_account_settings, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.menu_account_settings_delete: + deleteAccount(); + return true; + case R.id.menu_account_settings_cancel: + finish(); + return true; + } + return super.onOptionsItemSelected(item); + } + + private void loadData(Uri accountUri) { + AccountSettings settings = ProviderHelper.getApiAccountSettings(this, accountUri); + mAccountSettingsFragment.setAccSettings(settings); + } + + private void deleteAccount() { + if (getContentResolver().delete(mAccountUri, null, null) <= 0) { + throw new RuntimeException(); + } + finish(); + } + + private void save() { + ProviderHelper.updateApiAccount(this, mAccountSettingsFragment.getAccSettings(), mAccountUri); + finish(); + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AccountSettingsFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AccountSettingsFragment.java new file mode 100644 index 000000000..0a3ec3c3b --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AccountSettingsFragment.java @@ -0,0 +1,201 @@ +/* + * 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.remote.ui; + +import android.app.Activity; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemSelectedListener; +import android.widget.Spinner; +import android.widget.TextView; + +import com.beardedhen.androidbootstrap.BootstrapButton; + +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.provider.KeychainContract; +import org.sufficientlysecure.keychain.provider.ProviderHelper; +import org.sufficientlysecure.keychain.remote.AccountSettings; +import org.sufficientlysecure.keychain.ui.EditKeyActivity; +import org.sufficientlysecure.keychain.ui.SelectSecretKeyLayoutFragment; +import org.sufficientlysecure.keychain.ui.adapter.KeyValueSpinnerAdapter; +import org.sufficientlysecure.keychain.util.AlgorithmNames; + +public class AccountSettingsFragment extends Fragment implements + SelectSecretKeyLayoutFragment.SelectSecretKeyCallback { + + private static final int REQUEST_CODE_CREATE_KEY = 0x00008884; + + // model + private AccountSettings mAccSettings; + + // view + private TextView mAccNameView; + private Spinner mEncryptionAlgorithm; + private Spinner mHashAlgorithm; + private Spinner mCompression; + + private SelectSecretKeyLayoutFragment mSelectKeyFragment; + private BootstrapButton mCreateKeyButton; + + KeyValueSpinnerAdapter mEncryptionAdapter; + KeyValueSpinnerAdapter mHashAdapter; + KeyValueSpinnerAdapter mCompressionAdapter; + + public AccountSettings getAccSettings() { + return mAccSettings; + } + + public void setAccSettings(AccountSettings accountSettings) { + this.mAccSettings = accountSettings; + + mAccNameView.setText(accountSettings.getAccountName()); + mSelectKeyFragment.selectKey(accountSettings.getKeyId()); + mEncryptionAlgorithm.setSelection(mEncryptionAdapter.getPosition(accountSettings + .getEncryptionAlgorithm())); + mHashAlgorithm.setSelection(mHashAdapter.getPosition(accountSettings.getHashAlgorithm())); + mCompression.setSelection(mCompressionAdapter.getPosition(accountSettings.getCompression())); + } + + /** + * Inflate the layout for this fragment + */ + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.api_account_settings_fragment, container, false); + initView(view); + return view; + } + + /** + * Set error String on key selection + * + * @param error + */ + public void setErrorOnSelectKeyFragment(String error) { + mSelectKeyFragment.setError(error); + } + + private void initView(View view) { + mSelectKeyFragment = (SelectSecretKeyLayoutFragment) getFragmentManager().findFragmentById( + R.id.api_account_settings_select_key_fragment); + mSelectKeyFragment.setCallback(this); + + mAccNameView = (TextView) view.findViewById(R.id.api_account_settings_acc_name); + mEncryptionAlgorithm = (Spinner) view + .findViewById(R.id.api_account_settings_encryption_algorithm); + mHashAlgorithm = (Spinner) view.findViewById(R.id.api_account_settings_hash_algorithm); + mCompression = (Spinner) view.findViewById(R.id.api_account_settings_compression); + mCreateKeyButton = (BootstrapButton) view.findViewById(R.id.api_account_settings_create_key); + + mCreateKeyButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + createKey(); + } + }); + + AlgorithmNames algorithmNames = new AlgorithmNames(getActivity()); + + mEncryptionAdapter = new KeyValueSpinnerAdapter(getActivity(), + algorithmNames.getEncryptionNames()); + mEncryptionAlgorithm.setAdapter(mEncryptionAdapter); + mEncryptionAlgorithm.setOnItemSelectedListener(new OnItemSelectedListener() { + + @Override + public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { + mAccSettings.setEncryptionAlgorithm((int) id); + } + + @Override + public void onNothingSelected(AdapterView<?> parent) { + } + }); + + mHashAdapter = new KeyValueSpinnerAdapter(getActivity(), algorithmNames.getHashNames()); + mHashAlgorithm.setAdapter(mHashAdapter); + mHashAlgorithm.setOnItemSelectedListener(new OnItemSelectedListener() { + + @Override + public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { + mAccSettings.setHashAlgorithm((int) id); + } + + @Override + public void onNothingSelected(AdapterView<?> parent) { + } + }); + + mCompressionAdapter = new KeyValueSpinnerAdapter(getActivity(), + algorithmNames.getCompressionNames()); + mCompression.setAdapter(mCompressionAdapter); + mCompression.setOnItemSelectedListener(new OnItemSelectedListener() { + + @Override + public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { + mAccSettings.setCompression((int) id); + } + + @Override + public void onNothingSelected(AdapterView<?> parent) { + } + }); + } + + private void createKey() { + Intent intent = new Intent(getActivity(), EditKeyActivity.class); + intent.setAction(EditKeyActivity.ACTION_CREATE_KEY); + intent.putExtra(EditKeyActivity.EXTRA_GENERATE_DEFAULT_KEYS, true); + // set default user id to account name + intent.putExtra(EditKeyActivity.EXTRA_USER_IDS, mAccSettings.getAccountName()); + startActivityForResult(intent, REQUEST_CODE_CREATE_KEY); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + switch (requestCode) { + case REQUEST_CODE_CREATE_KEY: { + if (resultCode == Activity.RESULT_OK) { + // select newly created key + long masterKeyId = ProviderHelper.getMasterKeyId(getActivity(), data.getData()); + mSelectKeyFragment.selectKey(masterKeyId); + } + break; + } + + default: + super.onActivityResult(requestCode, resultCode, data); + + break; + } + } + + /** + * callback from select secret key fragment + */ + @Override + public void onKeySelected(long secretKeyId) { + mAccSettings.setKeyId(secretKeyId); + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AccountsListFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AccountsListFragment.java new file mode 100644 index 000000000..4d99e1923 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AccountsListFragment.java @@ -0,0 +1,198 @@ +/* + * Copyright (C) 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.remote.ui; + +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.net.Uri; +import android.os.Bundle; +import android.support.v4.app.ListFragment; +import android.support.v4.app.LoaderManager; +import android.support.v4.content.CursorLoader; +import android.support.v4.content.Loader; +import android.support.v4.widget.CursorAdapter; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.ListView; +import android.widget.TextView; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.provider.KeychainContract; +import org.sufficientlysecure.keychain.ui.widget.FixedListView; +import org.sufficientlysecure.keychain.util.Log; + +public class AccountsListFragment extends ListFragment implements + LoaderManager.LoaderCallbacks<Cursor> { + + private static final String ARG_DATA_URI = "uri"; + + // This is the Adapter being used to display the list's data. + AccountsAdapter mAdapter; + + private Uri mDataUri; + + /** + * Creates new instance of this fragment + */ + public static AccountsListFragment newInstance(Uri dataUri) { + AccountsListFragment frag = new AccountsListFragment(); + + Bundle args = new Bundle(); + args.putParcelable(ARG_DATA_URI, dataUri); + + frag.setArguments(args); + + return frag; + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View layout = super.onCreateView(inflater, container, + savedInstanceState); + ListView lv = (ListView) layout.findViewById(android.R.id.list); + ViewGroup parent = (ViewGroup) lv.getParent(); + + /* + * http://stackoverflow.com/a/15880684 + * Remove ListView and add FixedListView in its place. + * This is done here programatically to be still able to use the progressBar of ListFragment. + * + * We want FixedListView to be able to put this ListFragment inside a ScrollView + */ + int lvIndex = parent.indexOfChild(lv); + parent.removeViewAt(lvIndex); + FixedListView newLv = new FixedListView(getActivity()); + newLv.setId(android.R.id.list); + parent.addView(newLv, lvIndex, lv.getLayoutParams()); + return layout; + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + mDataUri = getArguments().getParcelable(ARG_DATA_URI); + + getListView().setOnItemClickListener(new OnItemClickListener() { + @Override + public void onItemClick(AdapterView<?> adapterView, View view, int position, long id) { + String selectedAccountName = mAdapter.getItemAccountName(position); + Uri accountUri = mDataUri.buildUpon().appendEncodedPath(selectedAccountName).build(); + Log.d(Constants.TAG, "accountUri: " + accountUri); + + // edit account settings + Intent intent = new Intent(getActivity(), AccountSettingsActivity.class); + intent.setData(accountUri); + 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_settings_accounts_empty)); + + // 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 AccountsAdapter(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[]{ + KeychainContract.ApiAccounts._ID, // 0 + KeychainContract.ApiAccounts.ACCOUNT_NAME // 1 + }; + + 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. + + // Now create and return a CursorLoader that will take care of + // creating a Cursor for the data being displayed. + return new CursorLoader(getActivity(), mDataUri, PROJECTION, null, null, + KeychainContract.ApiAccounts.ACCOUNT_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); + } + + private class AccountsAdapter extends CursorAdapter { + private LayoutInflater mInflater; + + public AccountsAdapter(Context context, Cursor c, int flags) { + super(context, c, flags); + + mInflater = LayoutInflater.from(context); + } + + /** + * Similar to CursorAdapter.getItemId(). + * Required to build Uris for api accounts, which are not based on row ids + * + * @param position + * @return + */ + public String getItemAccountName(int position) { + if (mDataValid && mCursor != null) { + if (mCursor.moveToPosition(position)) { + return mCursor.getString(1); + } else { + return null; + } + } else { + return null; + } + } + + @Override + public void bindView(View view, Context context, Cursor cursor) { + TextView text = (TextView) view.findViewById(R.id.api_accounts_adapter_item_name); + + String accountName = cursor.getString(1); + text.setText(accountName); + } + + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + return mInflater.inflate(R.layout.api_accounts_adapter_list_item, null); + } + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AppSettingsActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AppSettingsActivity.java new file mode 100644 index 000000000..818c296c1 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AppSettingsActivity.java @@ -0,0 +1,134 @@ +/* + * 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.remote.ui; + +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.os.Bundle; +import android.support.v7.app.ActionBar; +import android.support.v7.app.ActionBarActivity; +import android.view.Menu; +import android.view.MenuItem; + +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.remote.AppSettings; +import org.sufficientlysecure.keychain.util.Log; + +public class AppSettingsActivity extends ActionBarActivity { + private Uri mAppUri; + + private AppSettingsFragment mSettingsFragment; + private AccountsListFragment mAccountsListFragment; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // let the actionbar look like Android's contact app + ActionBar actionBar = getSupportActionBar(); + actionBar.setDisplayHomeAsUpEnabled(true); + actionBar.setIcon(android.R.color.transparent); + actionBar.setHomeButtonEnabled(true); + + 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(savedInstanceState, mAppUri); + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + super.onCreateOptionsMenu(menu); + getMenuInflater().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; + } + return super.onOptionsItemSelected(item); + } + + private void loadData(Bundle savedInstanceState, Uri appUri) { + AppSettings settings = ProviderHelper.getApiAppSettings(this, appUri); + mSettingsFragment.setAppSettings(settings); + + String appName; + PackageManager pm = getPackageManager(); + try { + ApplicationInfo ai = pm.getApplicationInfo(settings.getPackageName(), 0); + appName = (String) pm.getApplicationLabel(ai); + } catch (PackageManager.NameNotFoundException e) { + // fallback + appName = settings.getPackageName(); + } + setTitle(appName); + + Uri accountsUri = appUri.buildUpon().appendPath(KeychainContract.PATH_ACCOUNTS).build(); + Log.d(Constants.TAG, "accountsUri: " + accountsUri); + startListFragment(savedInstanceState, accountsUri); + } + + private void startListFragment(Bundle savedInstanceState, Uri dataUri) { + // 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 + mAccountsListFragment = AccountsListFragment.newInstance(dataUri); + + // Add the fragment to the 'fragment_container' FrameLayout + // NOTE: We use commitAllowingStateLoss() to prevent weird crashes! + getSupportFragmentManager().beginTransaction() + .replace(R.id.api_accounts_list_fragment, mAccountsListFragment) + .commitAllowingStateLoss(); + // do it immediately! + getSupportFragmentManager().executePendingTransactions(); + } + + private void revokeAccess() { + if (getContentResolver().delete(mAppUri, null, null) <= 0) { + throw new RuntimeException(); + } + finish(); + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AppSettingsFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AppSettingsFragment.java new file mode 100644 index 000000000..a6db02708 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AppSettingsFragment.java @@ -0,0 +1,108 @@ +/* + * 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.remote.ui; + +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.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import org.spongycastle.util.encoders.Hex; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.remote.AppSettings; +import org.sufficientlysecure.keychain.util.Log; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +public class AppSettingsFragment extends Fragment { + + // model + private AppSettings mAppSettings; + + // view + private TextView mAppNameView; + private ImageView mAppIconView; + private TextView mPackageName; + private TextView mPackageSignature; + + public AppSettings getAppSettings() { + return mAppSettings; + } + + public void setAppSettings(AppSettings appSettings) { + this.mAppSettings = appSettings; + updateView(appSettings); + } + + /** + * 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); + mAppNameView = (TextView) view.findViewById(R.id.api_app_settings_app_name); + mAppIconView = (ImageView) view.findViewById(R.id.api_app_settings_app_icon); + mPackageName = (TextView) view.findViewById(R.id.api_app_settings_package_name); + mPackageSignature = (TextView) view.findViewById(R.id.api_app_settings_package_signature); + return view; + } + + private void updateView(AppSettings appSettings) { + // get application name and icon from package manager + String appName; + Drawable appIcon = null; + PackageManager pm = getActivity().getApplicationContext().getPackageManager(); + try { + ApplicationInfo ai = pm.getApplicationInfo(appSettings.getPackageName(), 0); + + appName = (String) pm.getApplicationLabel(ai); + appIcon = pm.getApplicationIcon(ai); + } catch (NameNotFoundException e) { + // fallback + appName = appSettings.getPackageName(); + } + mAppNameView.setText(appName); + mAppIconView.setImageDrawable(appIcon); + + // advanced info: package name + mPackageName.setText(appSettings.getPackageName()); + + // advanced info: package signature SHA-256 + 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); + } + } + + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AppsListActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AppsListActivity.java new file mode 100644 index 000000000..f86d279f0 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AppsListActivity.java @@ -0,0 +1,35 @@ +/* + * 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.remote.ui; + +import android.os.Bundle; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.ui.DrawerActivity; + +public class AppsListActivity extends DrawerActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.api_apps_list_activity); + + setupDrawerNavigation(savedInstanceState); + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AppsListFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AppsListFragment.java new file mode 100644 index 000000000..9d0e6d3ef --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AppsListFragment.java @@ -0,0 +1,174 @@ +/* + * 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.remote.ui; + +import android.content.Context; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.database.Cursor; +import android.net.Uri; +import android.os.Bundle; +import android.support.v4.app.ListFragment; +import android.support.v4.app.LoaderManager; +import android.support.v4.content.CursorLoader; +import android.support.v4.content.Loader; +import android.support.v4.widget.CursorAdapter; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.ImageView; +import android.widget.TextView; + +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.provider.KeychainContract; +import org.sufficientlysecure.keychain.provider.KeychainContract.ApiApps; + +public class AppsListFragment extends ListFragment 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) { + String selectedPackageName = mAdapter.getItemPackageName(position); + // edit app settings + Intent intent = new Intent(getActivity(), AppSettingsActivity.class); + intent.setData(KeychainContract.ApiApps.buildByPackageNameUri(selectedPackageName)); + 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, // 0 + ApiApps.PACKAGE_NAME // 1 + }; + + 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); + } + + private class RegisteredAppsAdapter extends CursorAdapter { + + private LayoutInflater mInflater; + private PackageManager mPM; + + public RegisteredAppsAdapter(Context context, Cursor c, int flags) { + super(context, c, flags); + + mInflater = LayoutInflater.from(context); + mPM = context.getApplicationContext().getPackageManager(); + } + + /** + * Similar to CursorAdapter.getItemId(). + * Required to build Uris for api apps, which are not based on row ids + * + * @param position + * @return + */ + public String getItemPackageName(int position) { + if (mDataValid && mCursor != null) { + if (mCursor.moveToPosition(position)) { + return mCursor.getString(1); + } else { + return null; + } + } else { + return null; + } + } + + @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 = mPM.getApplicationInfo(packageName, 0); + + text.setText(mPM.getApplicationLabel(ai)); + icon.setImageDrawable(mPM.getApplicationIcon(ai)); + } catch (final PackageManager.NameNotFoundException e) { + // fallback + text.setText(packageName); + } + } else { + // fallback + text.setText(packageName); + } + + } + + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + return mInflater.inflate(R.layout.api_apps_adapter_list_item, null); + } + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/RemoteServiceActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/RemoteServiceActivity.java new file mode 100644 index 000000000..ab95f2691 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/RemoteServiceActivity.java @@ -0,0 +1,303 @@ +/* + * 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.remote.ui; + +import android.content.Intent; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.support.v7.app.ActionBarActivity; +import android.view.View; + +import org.openintents.openpgp.util.OpenPgpApi; +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.provider.KeychainContract; +import org.sufficientlysecure.keychain.provider.ProviderHelper; +import org.sufficientlysecure.keychain.remote.AccountSettings; +import org.sufficientlysecure.keychain.remote.AppSettings; +import org.sufficientlysecure.keychain.ui.SelectPublicKeyFragment; +import org.sufficientlysecure.keychain.ui.dialog.PassphraseDialogFragment; +import org.sufficientlysecure.keychain.util.Log; + +import java.util.ArrayList; + +public class RemoteServiceActivity extends ActionBarActivity { + + public static final String ACTION_REGISTER = Constants.INTENT_PREFIX + "API_ACTIVITY_REGISTER"; + public static final String ACTION_CREATE_ACCOUNT = Constants.INTENT_PREFIX + + "API_ACTIVITY_CREATE_ACCOUNT"; + 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"; + + public static final String EXTRA_DATA = "data"; + + // 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"; + // create acc action + public static final String EXTRA_ACC_NAME = "acc_name"; + // select pub keys action + public static final String EXTRA_SELECTED_MASTER_KEY_IDS = "master_key_ids"; + public static final String EXTRA_MISSING_USER_IDS = "missing_user_ids"; + public static final String EXTRA_DUBLICATE_USER_IDS = "dublicate_user_ids"; + // error message + public static final String EXTRA_ERROR_MESSAGE = "error_message"; + + // register view + private AppSettingsFragment mAppSettingsFragment; + // create acc view + private AccountSettingsFragment mAccSettingsFragment; + // select pub keys view + private SelectPublicKeyFragment mSelectFragment; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + handleActions(getIntent(), savedInstanceState); + } + + protected void handleActions(Intent intent, Bundle savedInstanceState) { + + String action = intent.getAction(); + final Bundle extras = intent.getExtras(); + + + if (ACTION_REGISTER.equals(action)) { + final String packageName = extras.getString(EXTRA_PACKAGE_NAME); + final byte[] packageSignature = extras.getByteArray(EXTRA_PACKAGE_SIGNATURE); + Log.d(Constants.TAG, "ACTION_REGISTER packageName: " + packageName); + + // Inflate a "Done"/"Cancel" custom action bar view + ActionBarHelper.setTwoButtonView(getSupportActionBar(), + R.string.api_register_allow, R.drawable.ic_action_done, + new View.OnClickListener() { + @Override + public void onClick(View v) { + // Allow + + ProviderHelper.insertApiApp(RemoteServiceActivity.this, + mAppSettingsFragment.getAppSettings()); + + // give data through for new service call + Intent resultData = extras.getParcelable(EXTRA_DATA); + RemoteServiceActivity.this.setResult(RESULT_OK, resultData); + RemoteServiceActivity.this.finish(); + } + }, R.string.api_register_disallow, R.drawable.ic_action_cancel, + new View.OnClickListener() { + @Override + public void onClick(View v) { + // Disallow + RemoteServiceActivity.this.setResult(RESULT_CANCELED); + RemoteServiceActivity.this.finish(); + } + } + ); + + setContentView(R.layout.api_remote_register_app); + + mAppSettingsFragment = (AppSettingsFragment) getSupportFragmentManager().findFragmentById( + R.id.api_app_settings_fragment); + + AppSettings settings = new AppSettings(packageName, packageSignature); + mAppSettingsFragment.setAppSettings(settings); + } else if (ACTION_CREATE_ACCOUNT.equals(action)) { + final String packageName = extras.getString(EXTRA_PACKAGE_NAME); + final String accName = extras.getString(EXTRA_ACC_NAME); + + // Inflate a "Done"/"Cancel" custom action bar view + ActionBarHelper.setTwoButtonView(getSupportActionBar(), + R.string.api_settings_save, R.drawable.ic_action_done, + new View.OnClickListener() { + @Override + public void onClick(View v) { + // Save + + // user needs to select a key! + if (mAccSettingsFragment.getAccSettings().getKeyId() == Id.key.none) { + mAccSettingsFragment.setErrorOnSelectKeyFragment( + getString(R.string.api_register_error_select_key)); + } else { + ProviderHelper.insertApiAccount(RemoteServiceActivity.this, + KeychainContract.ApiAccounts.buildBaseUri(packageName), + mAccSettingsFragment.getAccSettings()); + + // give data through for new service call + Intent resultData = extras.getParcelable(EXTRA_DATA); + RemoteServiceActivity.this.setResult(RESULT_OK, resultData); + RemoteServiceActivity.this.finish(); + } + } + }, R.string.api_settings_cancel, R.drawable.ic_action_cancel, + new View.OnClickListener() { + @Override + public void onClick(View v) { + // Cancel + RemoteServiceActivity.this.setResult(RESULT_CANCELED); + RemoteServiceActivity.this.finish(); + } + } + ); + + setContentView(R.layout.api_remote_create_account); + + mAccSettingsFragment = (AccountSettingsFragment) getSupportFragmentManager().findFragmentById( + R.id.api_account_settings_fragment); + + AccountSettings settings = new AccountSettings(accName); + mAccSettingsFragment.setAccSettings(settings); + } else if (ACTION_CACHE_PASSPHRASE.equals(action)) { + long secretKeyId = extras.getLong(EXTRA_SECRET_KEY_ID); + final Intent resultData = extras.getParcelable(EXTRA_DATA); + + PassphraseDialogFragment.show(this, secretKeyId, + new Handler() { + @Override + public void handleMessage(Message message) { + if (message.what == PassphraseDialogFragment.MESSAGE_OKAY) { + // return given params again, for calling the service method again + RemoteServiceActivity.this.setResult(RESULT_OK, resultData); + } else { + RemoteServiceActivity.this.setResult(RESULT_CANCELED); + } + + RemoteServiceActivity.this.finish(); + } + }); + + } 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); + + // TODO: do this with spannable instead of HTML to prevent parsing failures with weird user ids + 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.setTwoButtonView(getSupportActionBar(), + R.string.btn_okay, R.drawable.ic_action_done, + new View.OnClickListener() { + @Override + public void onClick(View v) { + // add key ids to params Bundle for new request + Intent resultData = extras.getParcelable(EXTRA_DATA); + resultData.putExtra(OpenPgpApi.EXTRA_KEY_IDS, + mSelectFragment.getSelectedMasterKeyIds()); + + RemoteServiceActivity.this.setResult(RESULT_OK, resultData); + RemoteServiceActivity.this.finish(); + } + }, R.string.btn_do_not_save, R.drawable.ic_action_cancel, new View.OnClickListener() { + @Override + public void onClick(View v) { + // cancel + RemoteServiceActivity.this.setResult(RESULT_CANCELED); + RemoteServiceActivity.this.finish(); + } + } + ); + + setContentView(R.layout.api_remote_select_pub_keys); + + // 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 = "<font color=\"red\">" + errorMessage + "</font>"; + + // Inflate a "Done" custom action bar view + ActionBarHelper.setOneButtonView(getSupportActionBar(), + R.string.btn_okay, R.drawable.ic_action_done, + new View.OnClickListener() { + + @Override + public void onClick(View v) { + RemoteServiceActivity.this.setResult(RESULT_CANCELED); + RemoteServiceActivity.this.finish(); + } + }); + + setContentView(R.layout.api_remote_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, "Action does not exist!"); + setResult(RESULT_CANCELED); + finish(); + } + } +} |