Java tutorial
/* * Copyright (c) 2015 - Qeo LLC * * The source code form of this Qeo Open Source Project component is subject * to the terms of the Clear BSD license. * * You can redistribute it and/or modify it under the terms of the Clear BSD * License (http://directory.fsf.org/wiki/License:ClearBSD). See LICENSE file * for more details. * * The Qeo Open Source Project also includes third party Open Source Software. * See LICENSE file for more details. */ package org.qeo.android.service; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.qeo.android.internal.IServiceQeoCallback; import org.qeo.android.service.db.TableManifestMeta; import org.qeo.android.service.db.TableManifestRW; import android.content.BroadcastReceiver; import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.os.Bundle; import android.os.RemoteException; import android.support.v4.content.LocalBroadcastManager; /** * Class to contain security information (eg the manifest) from the connected application. */ public class ApplicationSecurity { private static final Logger LOG = Logger.getLogger("ApplicationSecurity"); /**Action to be broadcasted when the manifest dialog is dismissed.*/ public static final String ACTION_MANIFEST_DIALOG_FINISHED = "actionManifestDialogFinished"; /** The name of the title field in the intent. */ public static final String INTENT_EXTRA_TITLE = "Title"; /** The name of the permissions field in the intent. */ public static final String INTENT_EXTRA_PERMISSIONS = "Permissions"; /** The name of the UID field in the intent. */ public static final String INTENT_EXTRA_UID = "INTENT_EXTRA_UID"; /** The name of the result field in the intent. */ public static final String INTENT_EXTRA_RESULT = "Result"; private final Set<Long> mRegisteredReadersWriters; private final QeoService mService; private final int mUid; private final int mPid; private final String mAppLabel; private final String mPkgName; private IServiceQeoCallback mManifestDialogCallback; /** * Enum to indicate the section of the manifest file. */ private static enum ParseContext { META, APPLICATION; } /** * Enum to indicate read and/or write state. */ private static enum RW { /** Read only. */ R, /** Write only. */ W, /** Read/write. */ RW; } /* * The next 4 fields are considered temporary. They will be initialized in parseManifest and they will be cleared in * insertAppInfo and insertReadersWriters. */ private String mAppName = null; private int mVersion = -1; private int mAppVersion = -1; private final Map<String, RW> mReadersWriters = new HashMap<String, RW>(); /** * Initialize a new instance. * * @param service Reference to the Qeo service * @param uid the uid belonging to this content * @param pid the pid belonging to this content */ public ApplicationSecurity(QeoService service, int uid, int pid) { LOG.fine("Create new application security for uid: " + uid + ", pid: " + pid); mService = service; mUid = uid; mPid = pid; mRegisteredReadersWriters = new HashSet<Long>(); try { PackageManager pm = mService.getPackageManager(); // Packages with sharedUserId set in manifest will have their uid concatenated in getNameForUid call // What will happen if multiple packages with same sharedUserId make user of Qeo?? mPkgName = pm.getNameForUid(mUid).split(":")[0]; if (mPkgName.equals("org.qeo.android.service")) { // special case, will only be triggered by unit tests mAppVersion = 1; mAppLabel = "junit"; } else { mAppVersion = pm.getPackageInfo(mPkgName, 0).versionCode; ApplicationInfo appInfo = pm.getPackageInfo(mPkgName, 0).applicationInfo; if (appInfo == null) { throw new SecurityException("Can't get application name for uid " + uid); } mAppLabel = appInfo.loadLabel(pm).toString(); if (mAppLabel == null || mAppLabel.isEmpty()) { throw new SecurityException("Can't get application name for uid " + uid); } } LOG.fine("Application security created: " + mPkgName + " -- " + mAppVersion + " -- " + mAppLabel); } catch (NameNotFoundException e) { throw new SecurityException("Can't get application version for uid " + uid, e); } mManifestDialogCallback = null; } /** * Register reader/writer id to be used for this application. * * @param id The id of the reader/writer */ public void registerReaderWriter(long id) { if (QeoDefaults.isProxySecurityEnabled()) { LOG.fine("register reader/writer: uid: " + mUid + " rw: " + id); synchronized (mRegisteredReadersWriters) { mRegisteredReadersWriters.add(id); } } } /** * Unregister reader/writer id to be used for this application. * * @param id The id of the reader/writer */ public void unRegisterReaderWriter(long id) { if (QeoDefaults.isProxySecurityEnabled()) { LOG.fine("unregister reader/writer: uid: " + mUid + " rw: " + id); synchronized (mRegisteredReadersWriters) { mRegisteredReadersWriters.remove(id); } } } /** * Checks if this reader/writer is allowed to be used in this application. * * @param id The id of the reader/writer * @throws SecurityException if not allowed */ public void checkRegisteredReaderWriter(long id) { if (QeoDefaults.isProxySecurityEnabled()) { LOG.fine("check reader/writer: uid: " + mUid + " rw: " + id); synchronized (mRegisteredReadersWriters) { if (!mRegisteredReadersWriters.contains(id)) { throw new SecurityException("Not allowed to use reader/writer " + id); } } } } /** * Insert manifest appinfo block the database. Clear the mAppName and mVersion when everything is saved in the * database. */ public void insertAppInfo() { if (mAppName == null) { throw new SecurityException("\"appname\" not set in security manifest"); } if (mVersion == -1) { throw new SecurityException("\"version\" not set in security manifest"); } SQLiteDatabase db = mService.getDatabase(); ContentValues values = new ContentValues(); values.put(TableManifestMeta.C_ID, mUid); values.put(TableManifestMeta.C_PKG_NAME, mPkgName); values.put(TableManifestMeta.C_APP_NAME, mAppName); values.put(TableManifestMeta.C_VERSION, mVersion); values.put(TableManifestMeta.C_APP_VERSION, mAppVersion); db.insertWithOnConflict(TableManifestMeta.NAME, null, values, SQLiteDatabase.CONFLICT_REPLACE); mAppName = null; mVersion = 0; } /** * Insert allowed readers/writers into the database. Clear the mReadersWriters map when everything is saved in the * database. */ private synchronized void insertReadersWriters() { SQLiteDatabase db = mService.getDatabase(); // first delete all known readers/writers for this uid to ensure old ones get removed String where = TableManifestRW.C_UID + "=?"; db.delete(TableManifestRW.NAME, where, new String[] { Integer.toString(mUid) }); // add all for (Map.Entry<String, RW> entry : mReadersWriters.entrySet()) { ContentValues values = new ContentValues(); values.put(TableManifestRW.C_NAME, entry.getKey()); values.put(TableManifestRW.C_UID, mUid); values.put(TableManifestRW.C_PKG_NAME, mPkgName); LOG.fine("Add rw to table: " + entry.getKey() + " -- " + mUid + " -- " + mPkgName); switch (entry.getValue()) { case R: // read only values.put(TableManifestRW.C_READ, 1); values.put(TableManifestRW.C_WRITE, 0); break; case W: // write only values.put(TableManifestRW.C_READ, 0); values.put(TableManifestRW.C_WRITE, 1); break; case RW: // read/write only values.put(TableManifestRW.C_READ, 1); values.put(TableManifestRW.C_WRITE, 1); break; default: throw new IllegalStateException("value not handled"); } db.insertOrThrow(TableManifestRW.NAME, null, values); } mReadersWriters.clear(); } /** * Get the applicationversion of the application at the time the manifest was stored. * * @return The version number if known, -1 otherwise */ public int getAppVersion() { SQLiteDatabase db = mService.getDatabase(); String[] columns = new String[] { TableManifestMeta.C_APP_VERSION }; String selection = TableManifestMeta.C_ID + "=? AND " + TableManifestMeta.C_PKG_NAME + "=?"; String[] selectionArgs = new String[] { Integer.toString(mUid), mPkgName }; Cursor cursor = db.query(TableManifestMeta.NAME, columns, selection, selectionArgs, null, null, null); if (cursor.moveToFirst()) { int version = cursor.getInt(0); cursor.close(); return version; } return -1; // not yet known } private boolean isAllowedReaderWriter(String rw, boolean read) { SQLiteDatabase db = mService.getDatabase(); String[] columns = new String[] { TableManifestRW.C_UID }; LOG.fine("isAllowedReaderWriter: " + rw + " -- " + mUid + " -- " + mPkgName); String selection = TableManifestRW.C_NAME + " = ? AND " + TableManifestRW.C_UID + " = ? AND " + TableManifestRW.C_PKG_NAME + " = ? AND " + (read ? TableManifestRW.C_READ : TableManifestRW.C_WRITE) + " = ?"; String[] selectionArgs = new String[] { rw, Integer.toString(mUid), mPkgName, "1" }; Cursor cursor = db.query(TableManifestRW.NAME, columns, selection, selectionArgs, null, null, null); boolean ok = cursor.getCount() == 1; cursor.close(); return ok; } /** * Check if the reader class is allowed for this application. * * @param reader The class name * @return true if allowed, false otherwise */ public boolean isAllowedReader(String reader) { return isAllowedReaderWriter(reader, true); } /** * Check if the writer class is allowed for this application. * * @param writer The class name * @return true if allowed, false otherwise */ public boolean isAllowedWriter(String writer) { return isAllowedReaderWriter(writer, false); } /** * Evaluate the manifest data of this class and return the result using the cb. * * @param serviceImpl Implementation of the service functions. * @param cb The callback on which the onManifestReady should be called * @throws RemoteException Thrown when calling the onManifestReady callback fails. */ public synchronized void evaluateManifest(QeoServiceImpl serviceImpl, IServiceQeoCallback cb) throws RemoteException { ArrayList<String> permissions = new ArrayList<String>(); // verify if all readers/writer are already known in the database boolean allKnown = true; for (Map.Entry<String, RW> entry : mReadersWriters.entrySet()) { String name = entry.getKey(); String permissionValue = null; switch (entry.getValue()) { case R: if (!isAllowedReader(name)) { allKnown = false; } permissionValue = "Read"; break; case W: if (!isAllowedWriter(name)) { allKnown = false; } permissionValue = "Write"; break; case RW: if (!isAllowedReader(name) || !isAllowedWriter(name)) { allKnown = false; } permissionValue = "Read/Write"; break; default: throw new IllegalStateException("unhandled case: " + entry.getValue()); } permissions.add(name + ": " + permissionValue); } if (!allKnown) { LOG.info("Qeo manifest accept needed for application with uid " + mUid); showDialog(serviceImpl, cb, permissions); } else { LOG.fine("Qeo manifest was already accepted for application with uid " + mUid); cb.onManifestReady(true); } } /** * Parse the manifest and temporarily store its data. * * @param manifest The manifest to be parsed */ public synchronized void parseManifest(String[] manifest) { ParseContext parseContext = null; Pattern pEmptyLine = Pattern.compile("^\\s*$"); Pattern pCommentLine = Pattern.compile("^([^#]*)#.*$"); Pattern pContext = Pattern.compile("^\\s*\\[(\\S+)\\]\\s*$"); Pattern pkeyValue = Pattern.compile("^\\s*(\\S+)\\s*=\\s*(['\"])?([^'\"]+)(['\"])?\\s*$"); Matcher m; for (String lineFull : manifest) { String line = lineFull; m = pCommentLine.matcher(line); if (m.matches()) { line = m.group(1); // throw away comments } if (pEmptyLine.matcher(line).matches()) { continue; // ignore } line = line.trim(); // get context info m = pContext.matcher(line); if (m.matches()) { try { parseContext = ParseContext.valueOf(m.group(1).toUpperCase(Locale.US)); } catch (IllegalArgumentException e) { throw new SecurityException("Invalid keyword in manifest file: " + line); } continue; } if (parseContext == null) { throw new SecurityException("Manifest should start with [...] block"); } m = pkeyValue.matcher(line); if (!m.matches()) { throw new SecurityException("Invalid line in manifest: " + line); } String key = m.group(1); String open = m.group(2); String value = m.group(3); String close = m.group(4); if ((open == null && close == null) || (open != null && close != null && open.equals(close))) { // ok switch (parseContext) { case META: if (key.equals("appname")) { mAppName = value; } else if (key.equals("version")) { mVersion = Integer.parseInt(value); } else { throw new SecurityException("Invalid line in manifest in [meta]: " + line); } break; case APPLICATION: if (key.contains(".")) { throw new SecurityException( "Topic name in manifest should not contain dots, wrong line: " + line); } key = key.replaceAll("::", "."); if (value.equals("r")) { mReadersWriters.put(key, RW.R); } else if (value.equals("w")) { mReadersWriters.put(key, RW.W); } else if (value.equals("rw")) { mReadersWriters.put(key, RW.RW); } else { throw new SecurityException("Manifest parse error: Can't handle value \"" + value + "\" for application " + key); } break; default: throw new IllegalStateException("Error parsing manifest"); } } else { throw new SecurityException("Invalid line in manifest: " + line); } } } /** * Show a dialog containing all permissions for this application. * * @param permissions the permissions to be displayed * @throws RemoteException Thrown when calling the onManifestReady callback fails */ private void showDialog(QeoServiceImpl serviceImpl, IServiceQeoCallback cb, List<String> permissions) throws RemoteException { Boolean[] popupDisabled = { false }; serviceImpl.checkPopupDisabled(popupDisabled); LOG.fine("Manifest popup " + (mService.isManifestPopupDisabled() || popupDisabled[0] ? "disabled" : "enabled")); if (mService.isManifestPopupDisabled() || popupDisabled[0]) { // popup disabled, just accept everything insertAppInfo(); insertReadersWriters(); cb.onManifestReady(true); } else { // launching activity to show the manifest popup try { Class<?> clazz = Class.forName("org.qeo.android.security.ManifestActivity"); Intent intent = new Intent(mService, clazz); Bundle args = new Bundle(); // save the callback in order to be able to use it in the onDialogFinished broadcast mManifestDialogCallback = cb; // Listen for intent signaling completion of manifest dialog LocalBroadcastManager.getInstance(mService).registerReceiver(mOnDialogFinished, new IntentFilter(ACTION_MANIFEST_DIALOG_FINISHED)); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); args.putInt(INTENT_EXTRA_UID, mUid); args.putString(INTENT_EXTRA_TITLE, mAppLabel + " is requesting the following Qeo permissions"); args.putStringArray(INTENT_EXTRA_PERMISSIONS, permissions.toArray(new String[permissions.size()])); intent.putExtras(args); mService.getApplicationContext().startActivity(intent); } catch (ClassNotFoundException e) { LOG.log(Level.SEVERE, "Error launching manifest activity", e); } } } /** * The broadcast receiver that gets notified when the user finished the Manifest dialog. */ private final BroadcastReceiver mOnDialogFinished = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { LOG.fine("Manifest dialog completed broadcast event received"); // Check if this broadcast intent is for us (based on mUid). if (mUid == intent.getIntExtra(INTENT_EXTRA_UID, -1)) { LocalBroadcastManager.getInstance(mService).unregisterReceiver(this); boolean result = intent.getBooleanExtra(INTENT_EXTRA_RESULT, false); if (result) { LOG.fine("Save Qeo manifest data in database"); insertAppInfo(); insertReadersWriters(); } try { if (mManifestDialogCallback != null) { LOG.fine("Sending onManifestReady callback for " + mUid + "/" + mPid); mManifestDialogCallback.onManifestReady(result); } else { LOG.warning("No callback found for uid " + mUid + " to send onManifestReady callback"); } } catch (RemoteException e) { // Not much we can do here... LOG.log(Level.WARNING, "Error calling onManifestReady for uid " + mUid, e); } } else { LOG.fine("Manifest dialog completed broadcast wrong uid: " + mUid + ",got " + intent.getIntExtra(INTENT_EXTRA_UID, -1)); } } }; }