package org.witness.informacam; import info.guardianproject.onionkit.ui.OrbotHelper; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.StringBufferInputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.Enumeration; import java.util.List; import java.util.Vector; import org.spongycastle.openpgp.PGPException; import org.witness.informacam.crypto.CredentialManager; import org.witness.informacam.crypto.KeyUtility; import org.witness.informacam.crypto.SignatureService; import org.witness.informacam.informa.Cron; import org.witness.informacam.informa.InformaService; import org.witness.informacam.json.JSONArray; import org.witness.informacam.json.JSONException; import org.witness.informacam.json.JSONObject; import org.witness.informacam.json.JSONTokener; import org.witness.informacam.models.Model; import org.witness.informacam.models.credentials.IKeyStore; import org.witness.informacam.models.credentials.ISecretKey; import org.witness.informacam.models.credentials.IUser; import org.witness.informacam.models.forms.IForm; import org.witness.informacam.models.forms.IInstalledForms; import org.witness.informacam.models.j3m.IDCIMDescriptor; import org.witness.informacam.models.media.IMedia; import org.witness.informacam.models.media.IMediaManifest; import org.witness.informacam.models.notifications.INotification; import org.witness.informacam.models.notifications.INotificationsManifest; import org.witness.informacam.models.organizations.IInstalledOrganizations; import org.witness.informacam.models.organizations.IOrganization; import org.witness.informacam.models.transport.ITransportManifest; import org.witness.informacam.models.transport.ITransportStub; import org.witness.informacam.models.utils.ILanguageMap; import org.witness.informacam.storage.FormUtility; import org.witness.informacam.storage.IOService; import org.witness.informacam.storage.IOUtility; import org.witness.informacam.transport.TransportUtility; import org.witness.informacam.utils.Constants.Actions; import org.witness.informacam.utils.Constants.App; import org.witness.informacam.utils.Constants.App.Informa; import org.witness.informacam.utils.Constants.App.Storage; import org.witness.informacam.utils.Constants.App.Storage.Type; import org.witness.informacam.utils.Constants.Codes; import org.witness.informacam.utils.Constants.IManifest; import org.witness.informacam.utils.Constants.InformaCamEventListener; import org.witness.informacam.utils.Constants.ListAdapterListener; import org.witness.informacam.utils.Constants.Logger; import org.witness.informacam.utils.Constants.Models; import org.witness.informacam.utils.Constants.Models.IMedia.MimeType; import org.witness.informacam.utils.Constants.Suckers; import org.witness.informacam.utils.InformaCamBroadcaster.InformaCamStatusListener; import org.witness.informacam.utils.InnerBroadcaster; import android.app.Activity; import android.app.AlarmManager; import android.app.Application; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.preference.PreferenceManager; import android.util.Base64; import android.util.Log; import dalvik.system.DexFile; public class InformaCam extends Application { /* * set dynamic debug settings in initData */ private final static String LOG = App.LOG; private List<InnerBroadcaster> broadcasters = new Vector<InnerBroadcaster>(); public IMediaManifest mediaManifest = new IMediaManifest(); public IInstalledOrganizations installedOrganizations = new IInstalledOrganizations(); public INotificationsManifest notificationsManifest = new INotificationsManifest(); public ILanguageMap languageMap = new ILanguageMap(); public ITransportManifest transportManifest = new ITransportManifest(); public IUser user; Intent ioServiceIntent, signatureServiceIntent, uploaderServiceIntent, informaServiceIntent; public IOService ioService = null; public SignatureService signatureService = null; public Handler h = new Handler(); public List<String> models = new ArrayList<String>(); NotificationManager notificationManager; private int processId; private static InformaCam mInstance = null; private ListAdapterListener mListAdapterListener = null; private InformaCamStatusListener mStatusListener = null; private InformaCamEventListener mEventListener = null; private CredentialManager credentialManager = null; private PendingIntent cronPI = null; //we really need to get rid of this public static InformaCam getInstance () { return mInstance; } public void initBroadcasters() { for(BroadcastReceiver br : broadcasters) { if(!((InnerBroadcaster) br).isMounted()) { ((InnerBroadcaster) br).setMounted(true); registerReceiver(br, ((InnerBroadcaster) br).intentFilter); } } } @Override public void onCreate() { super.onCreate(); mInstance = this; processId = android.os.Process.myPid(); Logger.d(LOG, "InformaCam service started via intent"); notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); broadcasters.add(new InnerBroadcaster(new IntentFilter(Actions.ASSOCIATE_SERVICE), processId) { @Override public void onReceive(Context context, Intent intent) { super.onReceive(context, intent); if(!isIntended) { return; } if(intent.getAction().equals(Actions.ASSOCIATE_SERVICE)) { int serviceCode = intent.getIntExtra(Codes.Keys.SERVICE, 0); switch(serviceCode) { case Codes.Routes.INFORMA_SERVICE: break; } } } }); broadcasters.add(new InnerBroadcaster(new IntentFilter(Actions.UPLOADER_UPDATE), processId) { @Override public void onReceive(Context context, Intent intent) { super.onReceive(context, intent); if(!isIntended) { return; } if(intent.getAction().equals(Actions.UPLOADER_UPDATE)) { switch(intent.getIntExtra(Codes.Keys.UPLOADER, 0)) { case Codes.Transport.MUST_INSTALL_TOR: break; case Codes.Transport.MUST_START_TOR: break; } } } }); broadcasters.add(new InnerBroadcaster(new IntentFilter(Actions.DISASSOCIATE_SERVICE), processId) { @Override public void onReceive(Context context, Intent intent) { super.onReceive(context, intent); if(!isIntended) { return; } if(intent.getAction().equals(Actions.DISASSOCIATE_SERVICE)) { switch(intent.getIntExtra(Codes.Keys.SERVICE, 0)) { case Codes.Routes.INFORMA_SERVICE: sendBroadcast(new Intent() .setAction(Actions.INFORMA_STOP) .putExtra(Codes.Extras.RESTRICT_TO_PROCESS, processId)); break; } } } }); initBroadcasters(); informaServiceIntent = new Intent(this, InformaService.class); signatureService = new SignatureService(InformaCam.this); ioService = new IOService(InformaCam.this); startInforma(); startup(); } public void startup() { Logger.d(LOG, "NOW we init!"); user = new IUser(); setModels(); final int INIT = 1; final int LOGIN = 2; final int RUN = 3; int startCode = LOGIN; boolean runForeground = PreferenceManager.getDefaultSharedPreferences(this).getBoolean("prefRunForeground", false); try { FileInputStream fis = this.openFileInput(IManifest.USER); if(fis.available() == 0) { startCode = INIT; } else { setCredentialManager(new CredentialManager(this, !ioService.isMounted(),false,runForeground)); byte[] ubytes = new byte[fis.available()]; fis.read(ubytes); user.inflate(ubytes); if(credentialManager.getStatus() == Codes.Status.UNLOCKED) { startCode = RUN; } else if(credentialManager.getStatus() == Codes.Status.LOCKED) { startCode = LOGIN; } } } catch (FileNotFoundException e) { Logger.d(LOG, "CONSIDERED HANDLED:\n" + e.toString()); startCode = INIT; } catch (Exception e) { Logger.e(LOG, e); startCode = INIT; } Bundle data = new Bundle(); switch(startCode) { case INIT: // we launch our wizard! data.putInt(Codes.Extras.MESSAGE_CODE, Codes.Messages.Wizard.INIT); break; case LOGIN: // we log in! user.isLoggedIn = false; try { ioService.saveBlob(user, new java.io.File(IManifest.USER)); data.putInt(Codes.Extras.MESSAGE_CODE, Codes.Messages.Login.DO_LOGIN); } catch (IOException ioe) { Logger.e(LOG, ioe); startCode = INIT; } break; case RUN: data.putInt(Codes.Extras.MESSAGE_CODE, Codes.Messages.Home.INIT); break; } Intent intent = new Intent(Actions.INFORMACAM_START) .putExtra(Codes.Keys.SERVICE, data) .putExtra(Codes.Extras.RESTRICT_TO_PROCESS, processId); sendBroadcast(intent); } public void initData() throws PGPException, IllegalAccessException, InstantiationException, IOException { ISecretKey sKey = (ISecretKey) getModel(new ISecretKey()); if (sKey != null && sKey.secretKey != null) { signatureService.initKey(sKey); mediaManifest = (IMediaManifest) getModel(mediaManifest); if(mediaManifest.getMediaList().size() > 0) { for(IMedia m : mediaManifest.getMediaList()) { m.isNew = false; } } installedOrganizations = (IInstalledOrganizations) getModel(installedOrganizations); notificationsManifest = (INotificationsManifest) getModel(notificationsManifest); if(notificationsManifest.notifications.size() > 0) { notificationsManifest.sortBy(Models.INotificationManifest.Sort.DATE_DESC); saveState(notificationsManifest); } transportManifest = (ITransportManifest) getModel(transportManifest); languageMap = (ILanguageMap) getModel(languageMap); /** * XXX * this is to set some testing preferences * i.e. Debug.testUser_1(); */ Debug.testUser_1(); } else throw new PGPException("Secret Key is null"); } private void setModels() { new Thread () { public void run () { try { String apkName = getPackageResourcePath(); java.io.File dexDump = getDir(IManifest.DEX, Context.MODE_PRIVATE); if(!dexDump.exists()) { dexDump.mkdir(); } DexFile dex = DexFile.loadDex(apkName, new java.io.File(dexDump, "dex").getAbsolutePath(), 0); for(Enumeration<String> classes = dex.entries(); classes.hasMoreElements() ;) { String clz = classes.nextElement(); if(clz.contains("org.witness.informacam.models")) { models.add(clz); } } } catch (IOException e) { Logger.e(LOG, e); } } }.start(); } public void shutdown() { for(BroadcastReceiver br : broadcasters) { try { ((InnerBroadcaster) br).setMounted(false); unregisterReceiver(br); } catch(IllegalArgumentException e) { Log.w(LOG,e); } } //saveStates(); try { ioService.unmount(); } catch (IllegalStateException ise) { Log.w(Informa.LOG,"Can't unmount cleanly due to async task threads, but that's okay!"); } //reset credential manager setCredentialManager(new CredentialManager(this, !ioService.isMounted(),false,false)); stopService(informaServiceIntent); Intent intent = new Intent(Actions.INFORMACAM_STOP) .putExtra(Codes.Extras.RESTRICT_TO_PROCESS, processId); sendBroadcast(intent); } public void saveStates() { try { //saveState(user, new java.io.File(IManifest.USER)); saveState(mediaManifest, new info.guardianproject.iocipher.File(IManifest.MEDIA)); } catch(NullPointerException e) { Logger.e(LOG, e); } catch(IOException e) { Logger.e(LOG, e); } } @SuppressWarnings("unused") private boolean saveState(Model model, java.io.File cache) throws IOException { return ioService.saveBlob(model.asJson().toString().getBytes(), cache); } private boolean saveState(Model model, info.guardianproject.iocipher.File cache) throws IOException { return ioService.saveBlob(model.asJson().toString().getBytes(), cache); } public boolean saveState(Model model) { return saveState(model, 0); } private boolean saveState(Model model, int tries) { boolean result = false; synchronized (model) { try { if(model.getClass().getName().equals(IKeyStore.class.getName())) { result = saveState(model, new info.guardianproject.iocipher.File(IManifest.KEY_STORE_MANIFEST)); } else if(model.getClass().getName().equals(IInstalledOrganizations.class.getName())) { result = saveState(model, new info.guardianproject.iocipher.File(IManifest.ORGS)); if(mListAdapterListener != null) { h.post(new Runnable() { @Override public void run() { mListAdapterListener.updateAdapter(Codes.Adapters.ORGANIZATIONS); } }); } } else if(model.getClass().getName().equals(IMediaManifest.class.getName())) { result = saveState(model, new info.guardianproject.iocipher.File(IManifest.MEDIA)); } else if(model.getClass().getName().equals(ISecretKey.class.getName())) { result = saveState(model, new info.guardianproject.iocipher.File(Models.IUser.SECRET)); } else if(model.getClass().getName().equals(INotificationsManifest.class.getName())) { result = saveState(model, new info.guardianproject.iocipher.File(IManifest.NOTIFICATIONS)); if(mListAdapterListener != null) { h.post(new Runnable() { @Override public void run() { mListAdapterListener.updateAdapter(Codes.Adapters.NOTIFICATIONS); } }); } } else if(model.getClass().getName().equals(IDCIMDescriptor.class.getName())) { result = saveState(model, new info.guardianproject.iocipher.File(IManifest.DCIM)); } else if(model.getClass().getName().equals(IUser.class.getName())) { result = ioService.saveBlob(model, new java.io.File(IManifest.USER)); } else if(model.getClass().getName().equals(ILanguageMap.class.getName())) { result = saveState(model, new info.guardianproject.iocipher.File(IManifest.LANG)); } else if(model.getClass().getName().equals(ITransportManifest.class.getName())) { result = saveState(model, new info.guardianproject.iocipher.File(IManifest.TRANSPORT)); } else if(model.getClass().getName().equals(IInstalledForms.class.getName())) { result = saveState(model, new info.guardianproject.iocipher.File(IManifest.FORMS)); } } catch (IOException ioe) { Logger.e(LOG, ioe); } } return result; } public Model getModel(Model model) throws IOException, IllegalAccessException, InstantiationException { byte[] bytes = null; if(model.getClass().getName().equals(IInstalledOrganizations.class.getName())) { bytes = ioService.getBytes(IManifest.ORGS, Type.IOCIPHER); } else if(model.getClass().getName().equals(IKeyStore.class.getName())) { bytes = ioService.getBytes(IManifest.KEY_STORE_MANIFEST, Type.IOCIPHER); } else if(model.getClass().getName().equals(IMediaManifest.class.getName())) { bytes = ioService.getBytes(IManifest.MEDIA, Type.IOCIPHER); } else if(model.getClass().getName().equals(ISecretKey.class.getName())) { bytes = ioService.getBytes(Models.IUser.SECRET, Type.IOCIPHER); } else if(model.getClass().getName().equals(INotificationsManifest.class.getName())) { bytes = ioService.getBytes(IManifest.NOTIFICATIONS, Type.IOCIPHER); } else if(model.getClass().getName().equals(ILanguageMap.class.getName())) { bytes = ioService.getBytes(IManifest.LANG, Type.IOCIPHER); } else if(model.getClass().getName().equals(ITransportManifest.class.getName())) { bytes = ioService.getBytes(IManifest.TRANSPORT, Type.IOCIPHER); } else if(model.getClass().getName().equals(IInstalledForms.class.getName())) { bytes = ioService.getBytes(IManifest.FORMS, Type.IOCIPHER); } if(!Arrays.equals(bytes, new byte[0])) { Log.d(Informa.LOG,"Inflating model: " + model.getClass().getName()); model.inflate(bytes); } return model; } public boolean attemptLogout() { if(credentialManager.logout()) { shutdown(); return true; } return false; } public boolean attemptLogin(char[] password) { return credentialManager.login(password); } public int getProcess() { return processId; } public void update(Bundle data) { Message message = new Message(); message.setData(data); if (mEventListener != null) mEventListener.onUpdate(message); } public void updateNotification(INotification notification, Handler callback) throws InstantiationException, IllegalAccessException { notificationsManifest.getById(notification._id).inflate(notification.asJson()); saveState(notificationsManifest); /* if(saveState(notificationsManifest) && mListAdapterListener != null) { callback.post(new Runnable() { @Override public void run() { mListAdapterListener.updateAdapter(Codes.Adapters.NOTIFICATIONS); } }); } */ } public void addNotification(INotification notification, Handler callback) { addNotification(notification, false, callback); } public void addNotification(INotification notification, boolean showOnTop, Handler callback) { if(notificationsManifest.notifications == null) { notificationsManifest.notifications = new ArrayList<INotification>(); } notificationsManifest.notifications.add(notification); saveState(notificationsManifest); } public void setListAdapterListener (ListAdapterListener lal) { mListAdapterListener = lal; } public void setStatusListener (InformaCamStatusListener sl) { mStatusListener = sl; } public InformaCamStatusListener getStatusListener () { return mStatusListener; } public void setEventListener (InformaCamEventListener sl) { mEventListener = sl; } public InformaCamEventListener getEventListener () { return mEventListener; } public IOrganization installICTD(JSONObject ictd, Handler callback, Context a) { IOrganization organization = null; JSONArray forms = null; try { forms = (JSONArray) ictd.get(Models.IOrganization.FORMS); } catch (JSONException e) { Logger.e(LOG, e); } if(forms != null) { for(int f=0; f<forms.length(); f++) { try { ByteArrayOutputStream baos = IOUtility.unGZipBytes(Base64.decode(forms.getString(f).getBytes(), Base64.DEFAULT)); IForm form = FormUtility.importAndParse(new ByteArrayInputStream(baos.toByteArray())); if(form != null) { // Logger.d(LOG, String.format("adding form\n%s", form.asJson().toString())); forms.put(f, form.asJson()); } baos.close(); } catch (JSONException e) { Logger.e(LOG, e); } catch (Exception e) { Logger.e(LOG, e); } } } try { ByteArrayOutputStream baos = IOUtility.unGZipBytes(Base64.decode(((String) ictd.get(Models.IOrganization.PUBLIC_KEY)).getBytes(), Base64.DEFAULT)); try { /** * Instead of going by the stated fingerprint in the ICTD, * we'll get that directly from the KeyUtility. * * (protection against bad actors!) */ String fingerprint = KeyUtility.getFingerprintFromKey(Base64.encode(baos.toByteArray(), Base64.DEFAULT)); ictd.put(Models.IOrganization.ORGANIZATION_FINGERPRINT, fingerprint); } catch (PGPException e) { Logger.e(LOG, e); return null; } info.guardianproject.iocipher.File publicKeyFile = new info.guardianproject.iocipher.File(Storage.ORGS_ROOT, ictd.getString(Models.IOrganization.ORGANIZATION_FINGERPRINT) + ".asc"); if(InformaCam.getInstance().ioService.saveBlob(baos.toByteArray(), publicKeyFile)) { ictd.put(Models.IOrganization.PUBLIC_KEY, publicKeyFile.getAbsolutePath()); } baos.close(); if(ictd.has(Models.IOrganization.ORGANIZATION_ICON)) { info.guardianproject.iocipher.File iconFile = new info.guardianproject.iocipher.File(Storage.ORGS_ROOT, ictd.getString(Models.IOrganization.ORGANIZATION_FINGERPRINT) + ".jpg"); if(IOUtility.unGZipAndSave( Base64.decode(((String) ictd.get(Models.IOrganization.ORGANIZATION_ICON)).getBytes(), Base64.DEFAULT), iconFile.getAbsolutePath(), Type.IOCIPHER) ) { ictd.put(Models.IOrganization.ORGANIZATION_ICON, iconFile.getAbsolutePath()); } } organization = new IOrganization(ictd); // Logger.d(LOG, "HERE IS NEW ORG:\n" + organization.asJson().toString()); } catch(JSONException e) { Logger.e(LOG, e); return null; } catch (Exception e) { Logger.e(LOG, e); } if(organization != null) { installedOrganizations.addOrganization(organization, a); saveState(installedOrganizations); INotification notification = new INotification(getString(R.string.key_sent), getString(R.string.you_have_successfully_installed, organization.organizationName), Models.INotification.Type.NEW_KEY); addNotification(notification, callback); } return organization; } public IOrganization installICTD(Uri ictdURI, Handler callback, Activity a) { try { InputStream is = getContentResolver().openInputStream(ictdURI); byte[] rc = new byte[is.available()]; is.read(rc); is.close(); return installICTD((JSONObject) new JSONTokener(new String(rc)).nextValue(), callback, a); } catch (FileNotFoundException e) { Logger.e(LOG, e); } catch (IOException e) { Logger.e(LOG, e); } catch (SecurityException e) { Logger.e(LOG, e); } catch (JSONException e) { Logger.e(LOG, e); } return null; } private void startInforma() { startService(informaServiceIntent); } private void stopInforma() { stopService(informaServiceIntent); } @Override public void onTerminate() { super.onTerminate(); Logger.d(LOG, "InformaCam (Application) is terminating."); this.shutdown(); credentialManager.logout(); } public Intent exportCredentials() { java.io.File credentials = getPublicCredentials(); if (credentials != null) { Intent intent = new Intent() .setAction(Intent.ACTION_SEND) .putExtra(Intent.EXTRA_STREAM, Uri.fromFile(credentials)) .setType("*/*"); return Intent.createChooser(intent, getString(R.string.send)); } else return null; } public void importAsset(String assetPath, String destinationPath, int assetSource, int destinationSource, Model model) throws InstantiationException, IllegalAccessException { byte[] data = ioService.getBytes(assetPath, assetSource); if(!Arrays.equals(data, new byte[0])) { if(model != null) { model.inflate(data); } if(destinationSource == Type.IOCIPHER) { info.guardianproject.iocipher.File asset = new info.guardianproject.iocipher.File(destinationPath); ioService.saveBlob(data, asset); } else if(destinationSource == Type.INTERNAL_STORAGE) { java.io.File asset = new java.io.File(destinationPath); try { ioService.saveBlob(data, asset); } catch (IOException e) { Logger.e(LOG, e); } } } } public Intent exportAsset(String assetPath, int source) { java.io.File asset = new java.io.File(Storage.EXTERNAL_DIR, System.currentTimeMillis() + "_tempAsset.json"); try { if(ioService.saveBlob(ioService.getBytes(assetPath, source), asset, true)) { Intent intent = new Intent() .setAction(Intent.ACTION_SEND) .putExtra(Intent.EXTRA_STREAM, Uri.fromFile(asset)) .setType("*/*"); return Intent.createChooser(intent, getString(R.string.send)); } } catch (IOException e) { Logger.e(LOG, e); } return null; } public void resendCredentials(IOrganization organization) { INotification notification = new INotification(getResources().getString(R.string.key_sent), getResources().getString(R.string.you_have_sent_your_credentials_to_x, organization.organizationName), Models.INotification.Type.NEW_KEY); notification.taskComplete = false; addNotification(notification, null); ITransportStub transportStub = new ITransportStub(organization, notification); transportStub.setAsset(Models.IUser.PUBLIC_CREDENTIALS, Models.IUser.PUBLIC_CREDENTIALS, MimeType.ZIP, Type.IOCIPHER); transportStub.callbackCode = Models.ITransportStub.CallbackCodes.UPDATE_ORGANIZATION_HAS_KEY; TransportUtility.initTransport(transportStub); } public boolean hasCredentialManager() { if(credentialManager != null) { return true; } return false; } public CredentialManager getCredentialManager () { return credentialManager; } public void setCredentialManager(CredentialManager credentialManager) { java.io.File informaCamDump = new java.io.File(Storage.EXTERNAL_DIR); if(!informaCamDump.exists()) { informaCamDump.mkdir(); } else { if(!informaCamDump.isDirectory()) { informaCamDump.delete(); informaCamDump = new java.io.File(Storage.EXTERNAL_DIR); informaCamDump.mkdir(); } } this.credentialManager = credentialManager; this.credentialManager.onResume(); } public java.io.File getPublicCredentials() { return ioService.getPublicCredentials(); } public int getCredentialManagerStatus() { if (credentialManager != null) return credentialManager.getStatus(); else return Codes.Status.UNINITIALIZED; } public boolean isOutsideTheLoop(String action) { if(Arrays.asList(Actions.OUTSIDE_THE_LOOP).contains(action)) { return true; } return false; } public boolean isConnectedToTor() { OrbotHelper oh = new OrbotHelper(this); if(!oh.isOrbotInstalled() || !oh.isOrbotRunning()) { return false; } return true; } public void startCron() { startCron(Suckers.DEFAULT_CRON_INTERVAL); } public void startCron(int minutes) { Log.d(LOG, "START CRON WITH " + minutes); Intent startCron = new Intent(this, Cron.class); cronPI = PendingIntent.getService(this, 0, startCron, 0); AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE); alarmManager.setRepeating(AlarmManager.RTC_WAKEUP, System.currentTimeMillis(), minutes * 60 * 1000, cronPI); } public void stopCron() { AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE); alarmManager.cancel(cronPI); } }