org.eatabrick.vecna.Vecna.java Source code

Java tutorial

Introduction

Here is the source code for org.eatabrick.vecna.Vecna.java

Source

/* Copyright (c) 2014 Alan Berndt
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to
 * deal in the Software without restriction, including without limitation the
 * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
 * sell copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
 * IN THE SOFTWARE.
 */

package org.eatabrick.vecna;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.security.NoSuchAlgorithmException;
import java.security.Security;
import java.util.Iterator;

import org.spongycastle.jce.provider.BouncyCastleProvider;
import org.spongycastle.openpgp.PGPCompressedData;
import org.spongycastle.openpgp.PGPEncryptedDataList;
import org.spongycastle.openpgp.PGPException;
import org.spongycastle.openpgp.PGPLiteralData;
import org.spongycastle.openpgp.PGPObjectFactory;
import org.spongycastle.openpgp.PGPPrivateKey;
import org.spongycastle.openpgp.PGPPublicKeyEncryptedData;
import org.spongycastle.openpgp.PGPSecretKey;
import org.spongycastle.openpgp.PGPSecretKeyRingCollection;
import org.spongycastle.openpgp.PGPUtil;

import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.content.ClipData;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.text.InputType;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.EditorInfo;
import android.widget.AdapterView.OnItemLongClickListener;
import android.widget.AdapterView;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;

import android.support.v4.view.MenuItemCompat;
import android.support.v7.widget.SearchView;

public class Vecna extends ActionBarListActivity implements SearchView.OnQueryTextListener {
    private final static String TAG = "Vecna";

    private PasswordEntryAdapter adapter;
    private SharedPreferences settings;
    private String passphrase = "";

    private String extraInformation = "";

    private class ReadEntriesTask extends AsyncTask<String, Integer, Integer> {
        ProgressDialog progress;

        protected void onPreExecute() {
            adapter.clear();
            adapter.notifyDataSetChanged();
            adapter.setNotifyOnChange(false);

            extraInformation = "";

            progress = new ProgressDialog(Vecna.this);
            progress.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
            progress.setMessage(getString(R.string.progress_initial));
            progress.setCancelable(false);
            progress.setMax(6);

            progress.show();
        }

        protected Integer doInBackground(String... string) {
            PGPSecretKeyRingCollection keyring = null;
            FileInputStream dataStream = null;

            publishProgress(R.string.progress_keyfile);
            try {
                File keyFile = new File(settings.getString("key_file", ""));
                FileInputStream keyStream = new FileInputStream(keyFile);
                keyring = new PGPSecretKeyRingCollection(PGPUtil.getDecoderStream(keyStream));
            } catch (Exception e) {
                return R.string.error_key_file_not_found;
            }

            publishProgress(R.string.progress_passfile);
            try {
                File dataFile = new File(settings.getString("passwords", ""));
                dataStream = new FileInputStream(dataFile);
            } catch (Exception e) {
                return R.string.error_password_file_not_found;
            }

            try {
                PGPObjectFactory factory;

                publishProgress(R.string.progress_find_data);

                factory = new PGPObjectFactory(PGPUtil.getDecoderStream(dataStream));
                PGPEncryptedDataList dataList = null;

                Object o;
                while ((o = factory.nextObject()) != null) {
                    if (o instanceof PGPEncryptedDataList) {
                        dataList = (PGPEncryptedDataList) o;
                        break;
                    }
                }

                if (dataList == null)
                    return R.string.error_no_data_in_file;

                publishProgress(R.string.progress_find_key);

                PGPSecretKey keySecret = null;
                PGPPrivateKey keyPrivate = null;

                PGPPublicKeyEncryptedData dataEncrypted = null;
                Iterator it = dataList.getEncryptedDataObjects();
                while (keyPrivate == null && it.hasNext()) {
                    dataEncrypted = (PGPPublicKeyEncryptedData) it.next();

                    keySecret = keyring.getSecretKey(dataEncrypted.getKeyID());
                    keyPrivate = keySecret.extractPrivateKey(string[0].toCharArray(), "SC");
                }

                if (keyPrivate == null)
                    return R.string.error_secret_key_missing;

                publishProgress(R.string.progress_decrypt);

                factory = new PGPObjectFactory(dataEncrypted.getDataStream(keyPrivate, "SC"));
                ByteArrayOutputStream dataFinal = new ByteArrayOutputStream();
                if (!populateStreamFromFactory(factory, dataFinal)) {
                    return R.string.error_encryption;
                }

                publishProgress(R.string.progress_parse);

                String[] lines = dataFinal.toString().split("\n");
                for (int i = 0; i < lines.length; ++i) {
                    adapter.add(new Entry(lines[i]));
                }
            } catch (PGPException e) {
                e.printStackTrace();
                if (e.getCause() instanceof NoSuchAlgorithmException) {
                    extraInformation = e.getCause().getLocalizedMessage();
                    return R.string.error_algorithm;
                }
                return R.string.error_pgp_key;
            } catch (Exception e) {
                e.printStackTrace();
                return R.string.error_unknown;
            }

            return 0;
        }

        protected void onProgressUpdate(Integer... messages) {
            progress.setMessage(getString(messages[0]));
            progress.incrementProgressBy(1);
        }

        protected void onPostExecute(Integer result) {
            progress.dismiss();

            adapter.setNotifyOnChange(true);
            adapter.notifyDataSetChanged();

            if (result > 0) {
                AlertDialog.Builder builder = new AlertDialog.Builder(Vecna.this);

                builder.setTitle(getString(R.string.error));
                builder.setMessage(String.format(getString(result), extraInformation));
                builder.setCancelable(false);
                builder.setPositiveButton(android.R.string.ok, null);

                builder.show();
            }

            // reset passphrase if they enter it incorrectly
            if (result == R.string.error_pgp_key || result == R.string.error_algorithm) {
                passphrase = "";
                setEmptyText(R.string.locked);
            } else {
                setEmptyText(R.string.empty);
            }

            supportInvalidateOptionsMenu();
            findViewById(android.R.id.list).requestFocus();
        }
    }

    @Override
    public boolean onQueryTextChange(String newText) {
        adapter.getFilter().filter(newText);
        return true;
    }

    @Override
    public boolean onQueryTextSubmit(String query) {
        return true;
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        settings = PreferenceManager.getDefaultSharedPreferences(getBaseContext());

        adapter = new PasswordEntryAdapter(this);
        adapter.setNotifyOnChange(false);
        setListAdapter(adapter);

        Security.addProvider(new BouncyCastleProvider());

        if (savedInstanceState != null) {
            passphrase = savedInstanceState.getString("passphrase");
            adapter.populate(savedInstanceState.getStringArray("entries"));
            adapter.notifyDataSetChanged();
        }

        getListView().setLongClickable(true);
        getListView().setOnItemLongClickListener(new OnItemLongClickListener() {
            public boolean onItemLongClick(AdapterView<?> parent, View v, int pos, long id) {
                onListItemLongClick(parent, v, pos, id);
                return true;
            }
        });
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        MenuInflater inflater = getMenuInflater();
        inflater.inflate(R.menu.main, menu);

        SearchView search = (SearchView) MenuItemCompat.getActionView(menu.findItem(R.id.search));

        if (search != null) {
            search.setOnQueryTextListener(this);
            search.setImeOptions(EditorInfo.IME_FLAG_NO_EXTRACT_UI);
        } else {
            Log.d(TAG, "Couldn't find search item");
        }

        return true;
    }

    @Override
    public boolean onPrepareOptionsMenu(Menu menu) {
        boolean unlocked = !isLocked();
        ((MenuItem) menu.findItem(R.id.search)).setVisible(unlocked);
        ((MenuItem) menu.findItem(R.id.refresh)).setVisible(unlocked);
        ((MenuItem) menu.findItem(R.id.lock)).setVisible(unlocked);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
        case R.id.lock:
            // clear the passphrase and passwords from memory
            passphrase = "";
            setEmptyText(R.string.locked);
            adapter.clear();
            adapter.notifyDataSetChanged();
            supportInvalidateOptionsMenu();
            return true;
        case R.id.refresh:
            // reload the password file
            updateEntries();
            return true;
        case R.id.settings:
            Intent settingsIntent = new Intent(this, Preferences.class);
            startActivityForResult(settingsIntent, 0);
            return true;
        case R.id.help:
            Intent browserIntent = new Intent(Intent.ACTION_VIEW,
                    Uri.parse("https://github.com/bentglasstube/vecna/blob/master/README.md"));
            startActivity(browserIntent);
            return true;
        default:
            return super.onOptionsItemSelected(item);
        }
    }

    @Override
    protected void onResume() {
        super.onResume();

        // Automatically show passwords on launch
        if (adapter.getCount() == 0)
            updateEntries();
    }

    @Override
    protected void onListItemClick(ListView parent, View v, int pos, long id) {
        copyPassword((Entry) adapter.getItem(pos));
    }

    public void emptyClicked(View v) {
        // get the passphrase if we don't know it
        if (isLocked())
            getPassphrase();
    }

    protected void onListItemLongClick(AdapterView parent, View v, int pos, long id) {
        // show a crazy dialog for the entry
        final Entry entry = (Entry) adapter.getItem(pos);

        AlertDialog.Builder builder = new AlertDialog.Builder(this);

        LayoutInflater inflater = (LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE);
        final View layout = inflater.inflate(R.layout.entry, (ViewGroup) findViewById(R.id.layout_root));

        ((TextView) layout.findViewById(R.id.account)).setText(entry.account);
        ((TextView) layout.findViewById(R.id.user)).setText(entry.user);
        ((TextView) layout.findViewById(R.id.password)).setText(entry.password);

        builder.setView(layout);

        builder.setPositiveButton(R.string.show_entry_copy, new DialogInterface.OnClickListener() {
            public void onClick(DialogInterface dialog, int id) {
                copyPassword(entry);
            }
        });

        builder.setNegativeButton(R.string.show_entry_close, null);
        builder.setNeutralButton(R.string.show_entry_show, null);

        final AlertDialog dialog = builder.create();

        dialog.setOnShowListener(new DialogInterface.OnShowListener() {
            public void onShow(DialogInterface dialogInterface) {
                final Button show = dialog.getButton(AlertDialog.BUTTON_NEUTRAL);
                show.setOnClickListener(new View.OnClickListener() {
                    public void onClick(View view) {
                        EditText password = (EditText) layout.findViewById(R.id.password);

                        if (show.getText().equals(getString(R.string.show_entry_show))) {
                            Log.d(TAG, "Show clicked");
                            password.setInputType(
                                    InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD);
                            show.setText(R.string.show_entry_hide);
                        } else {
                            Log.d(TAG, "Hide clicked");
                            password.setInputType(
                                    InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
                            show.setText(R.string.show_entry_show);
                        }
                    }
                });
            }
        });

        dialog.show();
    }

    @Override
    public void onSaveInstanceState(Bundle savedInstanceState) {
        super.onSaveInstanceState(savedInstanceState);
        savedInstanceState.putString("passphrase", passphrase);
        savedInstanceState.putStringArray("entries", adapter.toStringArray());
    }

    private void getPassphrase() {
        final EditText pass = new EditText(this);
        pass.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);

        AlertDialog.Builder builder = new AlertDialog.Builder(this);

        builder.setMessage(getString(R.string.prompt));
        builder.setView(pass);
        builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
            public void onClick(DialogInterface dialog, int which) {
                passphrase = pass.getText().toString();
                if (passphrase.length() != 0)
                    new ReadEntriesTask().execute(passphrase);
            }
        });

        builder.show();
    }

    private void updateEntries() {
        if (settings.getString("passwords", "").length() == 0 || settings.getString("key_file", "").length() == 0) {
            setEmptyText(R.string.settings);
        } else {
            setEmptyText(R.string.locked);
            if (passphrase.length() == 0) {
                getPassphrase();
            } else {
                new ReadEntriesTask().execute(passphrase);
            }
        }
    }

    private void setEmptyText(int stringResource) {
        ((TextView) findViewById(android.R.id.empty)).setText(stringResource);
    }

    private boolean isLocked() {
        return passphrase.length() == 0;
    }

    @SuppressWarnings("deprecation")
    private void copyPassword(Entry entry) {

        int sdk = Build.VERSION.SDK_INT;
        if (sdk < 11) {
            ((android.text.ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE)).setText(entry.password);
        } else {
            ClipData data = ClipData.newPlainText("simple text", entry.password);
            ((android.content.ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE)).setPrimaryClip(data);
        }

        Toast.makeText(Vecna.this, getString(R.string.copied, entry.account), Toast.LENGTH_SHORT).show();
    }

    private boolean populateStreamFromFactory(PGPObjectFactory factory, ByteArrayOutputStream stream)
            throws IOException, PGPException {
        Object message = factory.nextObject();
        ;

        while (message != null) {
            if (message instanceof PGPCompressedData) {
                PGPObjectFactory subFactory = new PGPObjectFactory(((PGPCompressedData) message).getDataStream());
                if (populateStreamFromFactory(subFactory, stream)) {
                    return true;
                }
            } else if (message instanceof PGPLiteralData) {
                InputStream s = ((PGPLiteralData) message).getInputStream();

                int ch;
                while ((ch = s.read()) >= 0) {
                    stream.write(ch);
                }
                return true;
            } else {
                Log.d(TAG, "Unknown PGP object: " + message.getClass());
            }

            message = factory.nextObject();
        }

        return false;
    }
}