package org.thialfihar.android.apg; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import org.thialfihar.android.apg.provider.KeyRings; import org.thialfihar.android.apg.provider.Keys; import org.thialfihar.android.apg.provider.UserIds; import android.content.ContentResolver; import android.content.Intent; import android.database.Cursor; import android.database.sqlite.SQLiteQueryBuilder; import android.net.Uri; import android.os.Bundle; import android.os.IBinder; import android.util.Log; public class ApgService extends Service { private final static String TAG = "ApgService"; public static final boolean LOCAL_LOGV = true; public static final boolean LOCAL_LOGD = true; @Override public IBinder onBind(Intent intent) { if( LOCAL_LOGD ) Log.d(TAG, "bound"); return mBinder; } /** error status */ private static enum error { ARGUMENTS_MISSING, APG_FAILURE, NO_MATCHING_SECRET_KEY, PRIVATE_KEY_PASSPHRASE_WRONG, PRIVATE_KEY_PASSPHRASE_MISSING; public int shiftedOrdinal() { return ordinal() + 100; } } private static enum call { encrypt_with_passphrase, encrypt_with_public_key, decrypt, get_keys } /** all arguments that can be passed by calling application */ public static enum arg { MESSAGE, // message to encrypt or to decrypt SYMMETRIC_PASSPHRASE, // key for symmetric en/decryption PUBLIC_KEYS, // public keys for encryption ENCRYPTION_ALGORYTHM, // encryption algorithm HASH_ALGORYTHM, // hash algorithm ARMORED_OUTPUT, // whether to armor output FORCE_V3_SIGNATURE, // whether to force v3 signature COMPRESSION, // what compression to use for encrypted output SIGNATURE_KEY, // key for signing PRIVATE_KEY_PASSPHRASE, // passphrase for encrypted private key KEY_TYPE, // type of key (private or public) BLOB, // blob passed } /** all things that might be returned */ private static enum ret { ERRORS, // string array list with errors WARNINGS, // string array list with warnings ERROR, // numeric error RESULT, // en-/decrypted FINGERPRINTS, // fingerprints of keys USER_IDS, // user ids } /** required arguments for each AIDL function */ private static final HashMap> FUNCTIONS_REQUIRED_ARGS = new HashMap>(); static { HashSet args = new HashSet(); args.add(arg.SYMMETRIC_PASSPHRASE); FUNCTIONS_REQUIRED_ARGS.put(call.encrypt_with_passphrase.name(), args); args = new HashSet(); args.add(arg.PUBLIC_KEYS); FUNCTIONS_REQUIRED_ARGS.put(call.encrypt_with_public_key.name(), args); args = new HashSet(); FUNCTIONS_REQUIRED_ARGS.put(call.decrypt.name(), args); args = new HashSet(); args.add(arg.KEY_TYPE); FUNCTIONS_REQUIRED_ARGS.put(call.get_keys.name(), args); } /** optional arguments for each AIDL function */ private static final HashMap> FUNCTIONS_OPTIONAL_ARGS = new HashMap>(); static { HashSet args = new HashSet(); args.add(arg.ENCRYPTION_ALGORYTHM); args.add(arg.HASH_ALGORYTHM); args.add(arg.ARMORED_OUTPUT); args.add(arg.FORCE_V3_SIGNATURE); args.add(arg.COMPRESSION); args.add(arg.PRIVATE_KEY_PASSPHRASE); args.add(arg.SIGNATURE_KEY); args.add(arg.BLOB); args.add(arg.MESSAGE); FUNCTIONS_OPTIONAL_ARGS.put(call.encrypt_with_passphrase.name(), args); FUNCTIONS_OPTIONAL_ARGS.put(call.encrypt_with_public_key.name(), args); args = new HashSet(); args.add(arg.SYMMETRIC_PASSPHRASE); args.add(arg.PUBLIC_KEYS); args.add(arg.PRIVATE_KEY_PASSPHRASE); args.add(arg.MESSAGE); args.add(arg.BLOB); FUNCTIONS_OPTIONAL_ARGS.put(call.decrypt.name(), args); } /** a map from ApgService parameters to function calls to get the default */ private static final HashMap FUNCTIONS_DEFAULTS = new HashMap(); static { FUNCTIONS_DEFAULTS.put(arg.ENCRYPTION_ALGORYTHM, "getDefaultEncryptionAlgorithm"); FUNCTIONS_DEFAULTS.put(arg.HASH_ALGORYTHM, "getDefaultHashAlgorithm"); FUNCTIONS_DEFAULTS.put(arg.ARMORED_OUTPUT, "getDefaultAsciiArmour"); FUNCTIONS_DEFAULTS.put(arg.FORCE_V3_SIGNATURE, "getForceV3Signatures"); FUNCTIONS_DEFAULTS.put(arg.COMPRESSION, "getDefaultMessageCompression"); } /** a map of the default function names to their method */ private static final HashMap FUNCTIONS_DEFAULTS_METHODS = new HashMap(); static { try { FUNCTIONS_DEFAULTS_METHODS.put("getDefaultEncryptionAlgorithm", Preferences.class.getMethod("getDefaultEncryptionAlgorithm")); FUNCTIONS_DEFAULTS_METHODS.put("getDefaultHashAlgorithm", Preferences.class.getMethod("getDefaultHashAlgorithm")); FUNCTIONS_DEFAULTS_METHODS.put("getDefaultAsciiArmour", Preferences.class.getMethod("getDefaultAsciiArmour")); FUNCTIONS_DEFAULTS_METHODS.put("getForceV3Signatures", Preferences.class.getMethod("getForceV3Signatures")); FUNCTIONS_DEFAULTS_METHODS.put("getDefaultMessageCompression", Preferences.class.getMethod("getDefaultMessageCompression")); } catch (Exception e) { Log.e(TAG, "Function method exception: " + e.getMessage()); } } private static void writeToOutputStream(InputStream is, OutputStream os) throws IOException { byte[] buffer = new byte[8]; int len = 0; while( (len = is.read(buffer)) != -1) { os.write(buffer, 0, len); } } private static Cursor getKeyEntries(HashMap pParams) { SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); qb.setTables(KeyRings.TABLE_NAME + " INNER JOIN " + Keys.TABLE_NAME + " ON " + "(" + KeyRings.TABLE_NAME + "." + KeyRings._ID + " = " + Keys.TABLE_NAME + "." + Keys.KEY_RING_ID + " AND " + Keys.TABLE_NAME + "." + Keys.IS_MASTER_KEY + " = '1'" + ") " + " INNER JOIN " + UserIds.TABLE_NAME + " ON " + "(" + Keys.TABLE_NAME + "." + Keys._ID + " = " + UserIds.TABLE_NAME + "." + UserIds.KEY_ID + " AND " + UserIds.TABLE_NAME + "." + UserIds.RANK + " = '0') "); String orderBy = pParams.containsKey("order_by") ? (String) pParams.get("order_by") : UserIds.TABLE_NAME + "." + UserIds.USER_ID + " ASC"; String typeVal[] = null; String typeWhere = null; if (pParams.containsKey("key_type")) { typeWhere = KeyRings.TABLE_NAME + "." + KeyRings.TYPE + " = ?"; typeVal = new String[] { "" + pParams.get("key_type") }; } return qb.query(Apg.getDatabase().db(), (String[]) pParams.get("columns"), typeWhere, typeVal, null, null, orderBy); } /** * maps a fingerprint or user id of a key to a master key in database * * @param search_key * fingerprint or user id to search for * @return master key if found, or 0 */ private static long getMasterKey(String pSearchKey, Bundle pReturn) { if (pSearchKey == null || pSearchKey.length() != 8) { return 0; } ArrayList keyList = new ArrayList(); keyList.add(pSearchKey); long[] keys = getMasterKey(keyList, pReturn); if (keys.length > 0) { return keys[0]; } else { return 0; } } /** * maps fingerprints or user ids of keys to master keys in database * * @param search_keys * a list of keys (fingerprints or user ids) to look for in * database * @return an array of master keys */ private static long[] getMasterKey(ArrayList pSearchKeys, Bundle pReturn) { HashMap qParams = new HashMap(); qParams.put("columns", new String[] { KeyRings.TABLE_NAME + "." + KeyRings.MASTER_KEY_ID, // 0 UserIds.TABLE_NAME + "." + UserIds.USER_ID, // 1 }); qParams.put("key_type", Id.database.type_public); Cursor mCursor = getKeyEntries(qParams); if( LOCAL_LOGV ) Log.v(TAG, "going through installed user keys"); ArrayList masterKeys = new ArrayList(); while (mCursor.moveToNext()) { long curMkey = mCursor.getLong(0); String curUser = mCursor.getString(1); String curFprint = Apg.getSmallFingerPrint(curMkey); if( LOCAL_LOGV ) Log.v(TAG, "current user: " + curUser + " (" + curFprint + ")"); if (pSearchKeys.contains(curFprint) || pSearchKeys.contains(curUser)) { if( LOCAL_LOGV ) Log.v(TAG, "master key found for: " + curFprint); masterKeys.add(curMkey); pSearchKeys.remove(curFprint); } else { if( LOCAL_LOGV ) Log.v(TAG, "Installed key " + curFprint + " is not in the list of public keys to encrypt with"); } } mCursor.close(); long[] masterKeyLongs = new long[masterKeys.size()]; int i = 0; for (Long key : masterKeys) { masterKeyLongs[i++] = key; } if (i == 0) { Log.w(TAG, "Found not one public key"); pReturn.getStringArrayList(ret.WARNINGS.name()).add("Searched for public key(s) but found not one"); } for (String key : pSearchKeys) { Log.w(TAG, "Searched for key " + key + " but cannot find it in APG"); pReturn.getStringArrayList(ret.WARNINGS.name()).add("Searched for key " + key + " but cannot find it in APG"); } return masterKeyLongs; } /** * Add default arguments if missing * * @param args * the bundle to add default parameters to if missing */ private void addDefaultArguments(String pCall, Bundle pArgs) { // check whether there are optional elements defined for that call if (FUNCTIONS_OPTIONAL_ARGS.containsKey(pCall)) { Preferences preferences = Preferences.getPreferences(getBaseContext(), true); Iterator iter = FUNCTIONS_DEFAULTS.keySet().iterator(); while (iter.hasNext()) { arg currentArg = iter.next(); String currentKey = currentArg.name(); if (!pArgs.containsKey(currentKey) && FUNCTIONS_OPTIONAL_ARGS.get(pCall).contains(currentArg)) { String currentFunctionName = FUNCTIONS_DEFAULTS.get(currentArg); try { Class returnType = FUNCTIONS_DEFAULTS_METHODS.get(currentFunctionName).getReturnType(); if (returnType == String.class) { pArgs.putString(currentKey, (String) FUNCTIONS_DEFAULTS_METHODS.get(currentFunctionName).invoke(preferences)); } else if (returnType == boolean.class) { pArgs.putBoolean(currentKey, (Boolean) FUNCTIONS_DEFAULTS_METHODS.get(currentFunctionName).invoke(preferences)); } else if (returnType == int.class) { pArgs.putInt(currentKey, (Integer) FUNCTIONS_DEFAULTS_METHODS.get(currentFunctionName).invoke(preferences)); } else { Log.e(TAG, "Unknown return type " + returnType.toString() + " for default option"); } } catch (Exception e) { Log.e(TAG, "Exception in add_default_arguments " + e.getMessage()); } } } } } /** * updates a Bundle with default return values * * @param pReturn * the Bundle to update */ private void addDefaultReturns(Bundle pReturn) { ArrayList errors = new ArrayList(); ArrayList warnings = new ArrayList(); pReturn.putStringArrayList(ret.ERRORS.name(), errors); pReturn.putStringArrayList(ret.WARNINGS.name(), warnings); } /** * checks for required arguments and adds them to the error if missing * * @param function * the functions required arguments to check for * @param pArgs * the Bundle of arguments to check * @param pReturn * the bundle to write errors to */ private void checkForRequiredArgs(String pFunction, Bundle pArgs, Bundle pReturn) { if (FUNCTIONS_REQUIRED_ARGS.containsKey(pFunction)) { Iterator iter = FUNCTIONS_REQUIRED_ARGS.get(pFunction).iterator(); while (iter.hasNext()) { String curArg = iter.next().name(); if (!pArgs.containsKey(curArg)) { pReturn.getStringArrayList(ret.ERRORS.name()).add("Argument missing: " + curArg); } } } if(pFunction.equals(call.encrypt_with_passphrase.name()) || pFunction.equals(call.encrypt_with_public_key.name()) || pFunction.equals(call.decrypt.name())) { // check that either MESSAGE or BLOB are there if( !pArgs.containsKey(arg.MESSAGE.name()) && !pArgs.containsKey(arg.BLOB.name())) { pReturn.getStringArrayList(ret.ERRORS.name()).add("Arguments missing: Neither MESSAGE nor BLOG found"); } } } /** * checks for unknown arguments and add them to warning if found * * @param function * the functions name to check against * @param pArgs * the Bundle of arguments to check * @param pReturn * the bundle to write warnings to */ private void checkForUnknownArgs(String pFunction, Bundle pArgs, Bundle pReturn) { HashSet allArgs = new HashSet(); if (FUNCTIONS_REQUIRED_ARGS.containsKey(pFunction)) { allArgs.addAll(FUNCTIONS_REQUIRED_ARGS.get(pFunction)); } if (FUNCTIONS_OPTIONAL_ARGS.containsKey(pFunction)) { allArgs.addAll(FUNCTIONS_OPTIONAL_ARGS.get(pFunction)); } ArrayList unknownArgs = new ArrayList(); Iterator iter = pArgs.keySet().iterator(); while (iter.hasNext()) { String curKey = iter.next(); try { arg curArg = arg.valueOf(curKey); if (!allArgs.contains(curArg)) { pReturn.getStringArrayList(ret.WARNINGS.name()).add("Unknown argument: " + curKey); unknownArgs.add(curKey); } } catch (Exception e) { pReturn.getStringArrayList(ret.WARNINGS.name()).add("Unknown argument: " + curKey); unknownArgs.add(curKey); } } // remove unknown arguments so our bundle has just what we need for (String arg : unknownArgs) { pArgs.remove(arg); } } private boolean prepareArgs(String pCall, Bundle pArgs, Bundle pReturn) { Apg.initialize(getBaseContext()); /* add default return values for all functions */ addDefaultReturns(pReturn); /* add default arguments if missing */ addDefaultArguments(pCall, pArgs); if( LOCAL_LOGV ) Log.v(TAG, "add_default_arguments"); /* check for required arguments */ checkForRequiredArgs(pCall, pArgs, pReturn); if( LOCAL_LOGV ) Log.v(TAG, "check_required_args"); /* check for unknown arguments and add to warning if found */ checkForUnknownArgs(pCall, pArgs, pReturn); if( LOCAL_LOGV ) Log.v(TAG, "check_unknown_args"); /* return if errors happened */ if (pReturn.getStringArrayList(ret.ERRORS.name()).size() != 0) { if( LOCAL_LOGV ) Log.v(TAG, "Errors after preparing, not executing "+pCall); pReturn.putInt(ret.ERROR.name(), error.ARGUMENTS_MISSING.shiftedOrdinal()); return false; } if( LOCAL_LOGV ) Log.v(TAG, "error return"); return true; } private boolean encrypt(Bundle pArgs, Bundle pReturn) { boolean isBlob = pArgs.containsKey(arg.BLOB.name()); long pubMasterKeys[] = {}; if (pArgs.containsKey(arg.PUBLIC_KEYS.name())) { ArrayList list = pArgs.getStringArrayList(arg.PUBLIC_KEYS.name()); ArrayList pubKeys = new ArrayList(); if( LOCAL_LOGV ) Log.v(TAG, "Long size: " + list.size()); Iterator iter = list.iterator(); while (iter.hasNext()) { pubKeys.add(iter.next()); } pubMasterKeys = getMasterKey(pubKeys, pReturn); } InputStream inStream = null; if(isBlob) { ContentResolver cr = getContentResolver(); try { inStream = cr.openInputStream(Uri.parse(pArgs.getString(arg.BLOB.name()))); } catch (Exception e) { Log.e(TAG, "... exception on opening blob", e); } } else { inStream = new ByteArrayInputStream(pArgs.getString(arg.MESSAGE.name()).getBytes()); } InputData in = new InputData(inStream, 0); // XXX Size second param? OutputStream out = new ByteArrayOutputStream(); if( LOCAL_LOGV ) Log.v(TAG, "About to encrypt"); try { Apg.encrypt(getBaseContext(), // context in, // input stream out, // output stream pArgs.getBoolean(arg.ARMORED_OUTPUT.name()), // ARMORED_OUTPUT pubMasterKeys, // encryption keys getMasterKey(pArgs.getString(arg.SIGNATURE_KEY.name()), pReturn), // signature key pArgs.getString(arg.PRIVATE_KEY_PASSPHRASE.name()), // signature passphrase null, // progress pArgs.getInt(arg.ENCRYPTION_ALGORYTHM.name()), // encryption pArgs.getInt(arg.HASH_ALGORYTHM.name()), // hash pArgs.getInt(arg.COMPRESSION.name()), // compression pArgs.getBoolean(arg.FORCE_V3_SIGNATURE.name()), // mPreferences.getForceV3Signatures(), pArgs.getString(arg.SYMMETRIC_PASSPHRASE.name()) // passPhrase ); } catch (Exception e) { Log.e(TAG, "Exception in encrypt"); String msg = e.getMessage(); if (msg.equals(getBaseContext().getString(R.string.error_noSignaturePassPhrase))) { pReturn.getStringArrayList(ret.ERRORS.name()).add("Cannot encrypt (" + arg.PRIVATE_KEY_PASSPHRASE.name() + " missing): " + msg); pReturn.putInt(ret.ERROR.name(), error.PRIVATE_KEY_PASSPHRASE_MISSING.shiftedOrdinal()); } else if (msg.equals(getBaseContext().getString(R.string.error_couldNotExtractPrivateKey))) { pReturn.getStringArrayList(ret.ERRORS.name()).add("Cannot encrypt (" + arg.PRIVATE_KEY_PASSPHRASE.name() + " probably wrong): " + msg); pReturn.putInt(ret.ERROR.name(), error.PRIVATE_KEY_PASSPHRASE_WRONG.shiftedOrdinal()); } else { pReturn.getStringArrayList(ret.ERRORS.name()).add("Internal failure (" + e.getClass() + ") in APG when encrypting: " + e.getMessage()); pReturn.putInt(ret.ERROR.name(), error.APG_FAILURE.shiftedOrdinal()); } return false; } if( LOCAL_LOGV ) Log.v(TAG, "Encrypted"); if(isBlob) { ContentResolver cr = getContentResolver(); try { OutputStream outStream = cr.openOutputStream(Uri.parse(pArgs.getString(arg.BLOB.name()))); writeToOutputStream(new ByteArrayInputStream(out.toString().getBytes()), outStream); outStream.close(); } catch (Exception e) { Log.e(TAG, "... exception on writing blob", e); } } else { pReturn.putString(ret.RESULT.name(), out.toString()); } return true; } private final IApgService.Stub mBinder = new IApgService.Stub() { public boolean getKeys(Bundle pArgs, Bundle pReturn) { prepareArgs("get_keys", pArgs, pReturn); HashMap qParams = new HashMap(); qParams.put("columns", new String[] { KeyRings.TABLE_NAME + "." + KeyRings.MASTER_KEY_ID, // 0 UserIds.TABLE_NAME + "." + UserIds.USER_ID, // 1 }); qParams.put("key_type", pArgs.getInt(arg.KEY_TYPE.name())); Cursor cursor = getKeyEntries(qParams); ArrayList fPrints = new ArrayList(); ArrayList ids = new ArrayList(); while (cursor.moveToNext()) { if( LOCAL_LOGV ) Log.v(TAG, "adding key "+Apg.getSmallFingerPrint(cursor.getLong(0))); fPrints.add(Apg.getSmallFingerPrint(cursor.getLong(0))); ids.add(cursor.getString(1)); } cursor.close(); pReturn.putStringArrayList(ret.FINGERPRINTS.name(), fPrints); pReturn.putStringArrayList(ret.USER_IDS.name(), ids); return true; } public boolean encryptWithPublicKey(Bundle pArgs, Bundle pReturn) { if (!prepareArgs("encrypt_with_public_key", pArgs, pReturn)) { return false; } return encrypt(pArgs, pReturn); } public boolean encryptWithPassphrase(Bundle pArgs, Bundle pReturn) { if (!prepareArgs("encrypt_with_passphrase", pArgs, pReturn)) { return false; } return encrypt(pArgs, pReturn); } public boolean decrypt(Bundle pArgs, Bundle pReturn) { if (!prepareArgs("decrypt", pArgs, pReturn)) { return false; } boolean isBlob = pArgs.containsKey(arg.BLOB.name()); String passphrase = pArgs.getString(arg.SYMMETRIC_PASSPHRASE.name()) != null ? pArgs.getString(arg.SYMMETRIC_PASSPHRASE.name()) : pArgs .getString(arg.PRIVATE_KEY_PASSPHRASE.name()); InputStream inStream = null; if(isBlob) { ContentResolver cr = getContentResolver(); try { inStream = cr.openInputStream(Uri.parse(pArgs.getString(arg.BLOB.name()))); } catch (Exception e) { Log.e(TAG, "... exception on opening blob", e); } } else { inStream = new ByteArrayInputStream(pArgs.getString(arg.MESSAGE.name()).getBytes()); } InputData in = new InputData(inStream, 0); // XXX what size in second parameter? OutputStream out = new ByteArrayOutputStream(); if( LOCAL_LOGV ) Log.v(TAG, "About to decrypt"); try { Apg.decrypt(getBaseContext(), in, out, passphrase, null, // progress pArgs.getString(arg.SYMMETRIC_PASSPHRASE.name()) != null // symmetric ); } catch (Exception e) { Log.e(TAG, "Exception in decrypt"); String msg = e.getMessage(); if (msg.equals(getBaseContext().getString(R.string.error_noSecretKeyFound))) { pReturn.getStringArrayList(ret.ERRORS.name()).add("Cannot decrypt: " + msg); pReturn.putInt(ret.ERROR.name(), error.NO_MATCHING_SECRET_KEY.shiftedOrdinal()); } else if (msg.equals(getBaseContext().getString(R.string.error_wrongPassPhrase))) { pReturn.getStringArrayList(ret.ERRORS.name()).add("Cannot decrypt (" + arg.PRIVATE_KEY_PASSPHRASE.name() + " wrong/missing): " + msg); pReturn.putInt(ret.ERROR.name(), error.PRIVATE_KEY_PASSPHRASE_WRONG.shiftedOrdinal()); } else { pReturn.getStringArrayList(ret.ERRORS.name()).add("Internal failure (" + e.getClass() + ") in APG when decrypting: " + msg); pReturn.putInt(ret.ERROR.name(), error.APG_FAILURE.shiftedOrdinal()); } return false; } if( LOCAL_LOGV ) Log.v(TAG, "... decrypted"); if(isBlob) { ContentResolver cr = getContentResolver(); try { OutputStream outStream = cr.openOutputStream(Uri.parse(pArgs.getString(arg.BLOB.name()))); writeToOutputStream(new ByteArrayInputStream(out.toString().getBytes()), outStream); outStream.close(); } catch (Exception e) { Log.e(TAG, "... exception on writing blob", e); } } else { pReturn.putString(ret.RESULT.name(), out.toString()); } return true; } }; }