From cb3ca37db9c5c3da05df804979e99628fff6f7b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Sch=C3=BCrmann?= Date: Mon, 27 Jan 2014 14:18:25 +0100 Subject: New Gradle project structure --- .../keychain/service/remote/OpenPgpService.java | 602 +++++++++++++++++++++ 1 file changed, 602 insertions(+) create mode 100644 OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/service/remote/OpenPgpService.java (limited to 'OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/service/remote/OpenPgpService.java') diff --git a/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/service/remote/OpenPgpService.java b/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/service/remote/OpenPgpService.java new file mode 100644 index 000000000..575f76a22 --- /dev/null +++ b/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/service/remote/OpenPgpService.java @@ -0,0 +1,602 @@ +/* + * Copyright (C) 2013 Dominik Schürmann + * + * 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 . + */ + +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 keyIds = new ArrayList(); + + boolean missingUserIdsCheck = false; + boolean dublicateUserIdsCheck = false; + ArrayList missingUserIds = new ArrayList(); + ArrayList dublicateUserIds = new ArrayList(); + + 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; + } + +} -- cgit v1.2.3