/* Copyright © 2013-2014, Silent Circle, LLC. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Any redistribution, use, or modification is done solely for personal benefit and not for any commercial purpose or for monetary gain * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name Silent Circle nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL SILENT CIRCLE, LLC BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package com.silentcircle.keymngr; import android.annotation.TargetApi; import android.app.AlertDialog; import android.app.Dialog; import android.content.ComponentName; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.ServiceConnection; import android.os.Build; import android.os.Bundle; import android.os.Environment; import android.os.Handler; import android.os.IBinder; import android.os.Message; import android.support.v4.app.DialogFragment; import android.support.v4.app.FragmentManager; import android.text.Editable; import android.text.InputType; import android.text.TextUtils; import android.text.TextWatcher; import android.util.Log; import android.view.View; import android.view.inputmethod.EditorInfo; import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.CheckBox; import android.widget.EditText; import android.widget.ListView; import android.widget.TextView; import android.widget.Toast; import com.actionbarsherlock.app.ActionBar; import com.actionbarsherlock.app.SherlockFragmentActivity; import com.actionbarsherlock.view.Menu; import com.actionbarsherlock.view.MenuItem; import com.silentcircle.contacts.R; import java.io.File; import java.lang.ref.WeakReference; import java.util.Set; public class KeyManagerActivity extends SherlockFragmentActivity { static private String TAG = "KeyManagerActivity"; static private String KEY_CHAIN_READY = "com.silentcircle.keymngr.action.READY"; private static final int KEY_STORE_READY = 1; private static final int KEY_STORE_FAILED = 2; private static final int UPDATE_APP_REGISTERED = 3; private KeyService keyService; private EditText passwordInput; private EditText passwordInput2; private EditText passwordInputOld; private TextView pwStrength; private CheckBox passwordShow; private ActionBar actionBar; private ListView listView; private ArrayAdapter<String> registeredApps; private Button lockUnlock; private PasswordFilter pwFilter = new PasswordFilter(); private boolean readyIntent; private boolean lockedDuringPwChange; private boolean storeCreation; private boolean mExternalStorageAvailable; private boolean mExternalStorageWriteable; private UnlockClick unlockListener = new UnlockClick(); private LockClick lockListener = new LockClick(); /** * Internal handler to receive and process key store state messages. */ private static InternalHandler mHandler = null; // Code Section that handles the KeyService creation and binding private ServiceConnection keyConnection = new ServiceConnection() { public void onServiceConnected(ComponentName className, IBinder service) { // This is called when the connection with the service has been // established, giving us the service object we can use to // interact with the service. Because we have bound to a explicit // service that we know is running in our own process, we can // cast its IBinder to a concrete class and directly access it. keyService = ((KeyService.LocalBinder) service).getService(); serviceBound(); } public void onServiceDisconnected(ComponentName className) { // This is called when the connection with the service has been // unexpectedly disconnected -- that is, its process crashed. // Because it is running in our same process, we should never // see this happen. keyService = null; } }; private void doBindService() { // Establish a connection with the service. We use an explicit // class name because we want a specific service implementation that // we know will be running in our own process (and thus won't be // supporting component replacement by other applications). bindService(new Intent(KeyManagerActivity.this, KeyService.class), keyConnection, Context.BIND_AUTO_CREATE); } private void doUnbindService() { if (keyService != null) { // Detach our existing connection. unbindService(keyConnection); } } /* * The lifecycle functions */ @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); PRNGFixes.apply(); mHandler = new InternalHandler(this); Intent intent = new Intent(this, KeyService.class); startService(intent); doBindService(); } @Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. getSupportMenuInflater().inflate(R.menu.key_manager, menu); return true; } public boolean onPrepareOptionsMenu(Menu menu) { // enable password changing only if key store is open, i.e. correct password entered menu.findItem(R.id.change_password).setVisible(KeyService.isReady()); menu.findItem(R.id.backup_store).setVisible(mExternalStorageWriteable); menu.findItem(R.id.restore_store).setVisible(mExternalStorageAvailable); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { super.onOptionsItemSelected(item); switch (item.getItemId()) { case R.id.key_manager_help: showKeyManagerInfo(R.string.info_about_key_manager); break; case R.id.change_password: storeCreation = true; // PW change is similar to key store creation changePassword(); break; case R.id.backup_store: { final File file = Environment.getExternalStorageDirectory(); if (!file.exists() || !file.isDirectory() || !file.canWrite()) { showInputInfo(getString(R.string.backup_access)); return true; } final File backupFile = new File(file, "sc_keymngrdb.scsave"); if (!KeyStoreDatabase.backupStoreTo(backupFile)) { // Should this go to a async task? showInputInfo(getString(R.string.backup_failed)); return true; } Toast.makeText(this, getString(R.string.backup_created), Toast.LENGTH_LONG).show(); break; } case R.id.restore_store: { final File file = new File(Environment.getExternalStorageDirectory(), "sc_keymngrdb.scsave"); if (!file.exists() || !file.canRead()) { showInputInfo(getString(R.string.restore_access)); return true; } // User selected to restore the database instead of creating a new one // Remove the creation specific setting, then restore and show the normal // password screen. if (storeCreation) { passwordInput.removeTextChangedListener(pwFilter); passwordInput2.setVisibility(View.GONE); pwStrength.setVisibility(View.GONE); listView.setVisibility(View.VISIBLE); storeCreation = false; } else { // restore an existing, open DB, thus overwriting it keyService.closeDatabase(); ProviderDbBackend.sendLockRequests(); } if (!KeyStoreDatabase.restoreStoreFrom(file)) { showInputInfo(getString(R.string.restore_failed)); return true; } showNormalScreen(); Toast.makeText(this, getString(R.string.restore_created), Toast.LENGTH_LONG).show(); break; } } return true; } @Override protected void onStart() { super.onStart(); } @Override protected void onResume() { super.onResume(); if (keyService == null) return; showNormalScreen(); updateListOfApps(); } @Override protected void onPause() { super.onPause(); } @Override protected void onStop() { super.onStop(); } @Override protected void onDestroy() { super.onDestroy(); doUnbindService(); } @Override protected void onNewIntent (Intent intent) { processIntent(intent); } private void serviceBound() { processIntent(getIntent()); } // This is called after the KeyService was bound, thus it's save to use KeyService functions private void processIntent(Intent intent) { if (intent == null) { finish(); return; } String action = intent.getAction(); readyIntent = KEY_CHAIN_READY.equals(action); // If we got a KEY_CHAIN_READY and the KeyService is ready: everything OK. if (readyIntent && KeyService.isReady()) { setResult(RESULT_OK); finish(); return; } setupActionBar(); setContentView(R.layout.key_manager_activity); lockUnlock = (Button)findViewById(R.id.lockUnlock); passwordInput = (EditText) findViewById(R.id.passwordInput); passwordShow = (CheckBox)findViewById((R.id.passwordShow)); listView = (ListView)findViewById(R.id.listView); TextView txt = new TextView(this); txt.setText(getString(R.string.registered_apps)); listView.addHeaderView(txt); registeredApps = new ArrayAdapter(this, android.R.layout.simple_list_item_1); listView.setAdapter(registeredApps); updateListOfApps(); if (!KeyStoreDatabase.isDbFileAvailable()) { updateExternalStorageState(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { invalidateOptionsMenu(); } storeCreation = true; createKeyStore(); showKeyManagerInfo(R.string.info_about_key_manager); return; } showNormalScreen(); } private void setupActionBar() { actionBar = getSupportActionBar(); if (actionBar == null) return; actionBar.setDisplayShowTitleEnabled(false); actionBar.setDisplayShowHomeEnabled(true); actionBar.setHomeButtonEnabled(false); actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD); } private void createKeyStore() { passwordInput.addTextChangedListener(pwFilter); passwordInput.requestFocus(); passwordInput2 = (EditText) findViewById(R.id.passwordInput2); passwordInput2.setVisibility(View.VISIBLE); pwStrength = (TextView) findViewById(R.id.passwordStrength); pwStrength.setVisibility(View.VISIBLE); passwordShow.setVisibility(View.VISIBLE); lockUnlock.setText(R.string.create); listView.setVisibility(View.INVISIBLE); lockUnlock.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { if (checkPassword(passwordInput.getText(), passwordInput2.getText())) { if (keyService.openOrCreateDatabase(passwordInput.getText())) { passwordInput.removeTextChangedListener(pwFilter); passwordInput2.setVisibility(View.GONE); pwStrength.setVisibility(View.GONE); listView.setVisibility(View.VISIBLE); storeCreation = false; showNormalScreen(); ProviderDbBackend.sendUnlockRequests(); mHandler.sendEmptyMessage(KEY_STORE_READY); } else { showInputInfo(getString(R.string.cannot_create_store)); } } passwordInput2.setText(null); passwordInput.setText(null); passwordInput.requestFocus(); } }); } /* * Very similar to create key store, but not the same :-) * database.rawExecSQL(String.format("PRAGMA key = '%s'", newPassword); */ private void changePassword() { passwordInputOld = (EditText) findViewById(R.id.oldPasswordInput); passwordInputOld.setVisibility(View.VISIBLE); passwordInput.addTextChangedListener(pwFilter); passwordInput.setHint(R.string.password_hint_new); // ask user for a new password passwordInput.requestFocus(); passwordInput.setVisibility(View.VISIBLE); passwordInput.setImeOptions(EditorInfo.IME_ACTION_NEXT); passwordInput2 = (EditText) findViewById(R.id.passwordInput2); passwordInput2.setVisibility(View.VISIBLE); pwStrength = (TextView) findViewById(R.id.passwordStrength); pwStrength.setVisibility(View.VISIBLE); passwordShow.setVisibility(View.VISIBLE); lockUnlock.setText(R.string.perform_change); listView.setVisibility(View.INVISIBLE); lockUnlock.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { if (checkPassword(passwordInput.getText(), passwordInput2.getText())) { keyService.closeDatabase(); if (!keyService.openOrCreateDatabase(passwordInputOld.getText())) { showInputInfo(getString(R.string.old_password_wrong)); passwordInputOld.setText(null); passwordInputOld.requestFocus(); if (!lockedDuringPwChange) { ProviderDbBackend.sendLockRequests(); lockedDuringPwChange = true; } } else if (ProviderDbBackend.changePassword(passwordInput.getText())) { if (lockedDuringPwChange) { ProviderDbBackend.sendUnlockRequests(); lockedDuringPwChange = false; } passwordInput.removeTextChangedListener(pwFilter); passwordInput.setHint(R.string.password_hint); passwordInput2.setVisibility(View.GONE); passwordInputOld.setVisibility(View.GONE); passwordInputOld.setText(null); pwStrength.setVisibility(View.GONE); listView.setVisibility(View.VISIBLE); storeCreation = false; showNormalScreen(); } else { showInputInfo(getString(R.string.cannot_change_password)); } } else { passwordInput.requestFocus(); } passwordInput2.setText(null); passwordInput.setText(null); } }); } // Click on "unlock" gets the password and checks if it works with key store. private class UnlockClick implements View.OnClickListener { @Override public void onClick(View view) { if (passwordInput.getText() == null || passwordInput.getText().length() == 0) { showInputInfo(getString(R.string.no_password)); return; } if (keyService.openOrCreateDatabase(passwordInput.getText())) { showNormalScreen(); ProviderDbBackend.sendUnlockRequests(); mHandler.sendEmptyMessage(KEY_STORE_READY); } else { keyService.closeDatabase(); // close DB, but do not stop service showInputInfo(getString(R.string.cannot_load_store)); passwordInput.requestFocus(); } passwordInput.setText(null); } } // LongClick on "lock" locks the key store and sends the lock notification. private class LockClick implements View.OnLongClickListener { @Override public boolean onLongClick(View view) { keyService.closeDatabase(); // clear keys etc, but do not stop service showNormalScreen(); ProviderDbBackend.sendLockRequests(); return true; } } // Shows the normal screen, depending on key store state. Also switches the click listeners // on the button. @TargetApi(Build.VERSION_CODES.HONEYCOMB) private void showNormalScreen() { if (storeCreation) return; // If READY then set for LOCK if (KeyService.isReady()) { lockUnlock.setText(R.string.lock); lockUnlock.setOnClickListener(null); lockUnlock.setOnLongClickListener(lockListener); passwordInput.setVisibility(View.GONE); passwordShow.setVisibility(View.GONE); passwordShow.setChecked(false); showPasswordCheck(passwordShow); } else { passwordInput.setVisibility(View.VISIBLE); passwordInput.setImeOptions(EditorInfo.IME_ACTION_DONE); passwordInput.requestFocus(); lockUnlock.setText(R.string.unlock); passwordShow.setVisibility(View.VISIBLE); lockUnlock.setOnClickListener(unlockListener); lockUnlock.setOnLongClickListener(null); } updateExternalStorageState(); keyService.updateNotification(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { invalidateOptionsMenu(); } } private void updateListOfApps() { if (registeredApps == null) return; registeredApps.clear(); Set<String> registeredNames = KeyService.getRegisteredApps().keySet(); for (String name : registeredNames) { String appName = KeyService.getRegisteredApps().get(name).displayName; registeredApps.add(appName); } registeredApps.notifyDataSetChanged(); listView.invalidate(); } /** * Switch between visible and invisible password. * * @param v the Checkbox */ public void showPasswordCheck(View v) { CheckBox cbv = (CheckBox)v; if (cbv.isChecked()) { passwordInput.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); if (passwordInput2 != null) passwordInput2.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); if (passwordInputOld != null) passwordInputOld.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); } else { passwordInput.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); if (passwordInput2 != null) passwordInput2.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); if (passwordInputOld != null) passwordInputOld.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); } if (!TextUtils.isEmpty(passwordInput.getText())) passwordInput.setSelection(passwordInput.getText().length()); if (passwordInput2 != null && !TextUtils.isEmpty(passwordInput.getText())) passwordInput2.setSelection(passwordInput2.getText().length()); if (passwordInputOld != null && !TextUtils.isEmpty(passwordInput.getText())) passwordInputOld.setSelection(passwordInputOld.getText().length()); } /** * A simple check if two passwords are equal. * * Returns false if one or both passwords are null or empty or if the passwords * don't match. * * @param pw1 first password * @param pw2 second password * @return true if both passwords match and are not empty. */ private boolean checkPassword(CharSequence pw1, CharSequence pw2) { if (pw1 == null || pw2 == null || pw1.length() == 0 || pw2.length() == 0) { showInputInfo(getString(R.string.no_password)); return false; } if (pw1.length() != pw2.length()) { showInputInfo(getString(R.string.password_match)); return false; } int len = pw1.length(); for (int i = 0; i < len; i++) { if (pw1.charAt(i) != pw2.charAt(i)) { showInputInfo(getString(R.string.password_match)); return false; } } return true; } void updateExternalStorageState() { String state = Environment.getExternalStorageState(); if (Environment.MEDIA_MOUNTED.equals(state)) { mExternalStorageAvailable = mExternalStorageWriteable = true; } else if (Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) { mExternalStorageAvailable = true; mExternalStorageWriteable = false; } else { mExternalStorageAvailable = mExternalStorageWriteable = false; } } /** * A simple password strength check. * * This check just looks for length and some different characters to give an indication * about a password strength. * * @author werner * */ private class PasswordFilter implements TextWatcher { public void onTextChanged(CharSequence s, int start, int before, int count) { } public void beforeTextChanged(CharSequence s, int start, int count, int after) { } public void afterTextChanged(Editable s) { String str = s.toString(); int strLength = str.length(); int strength = 0; if (strLength == 0) { pwStrength.setText(null); return; } boolean digit = false; boolean lower = false; boolean upper = false; boolean other = false; for (int i = 0; i < strLength; i++) { char chr = str.charAt(i); if (Character.isDigit(chr)) digit = true; else if (Character.isLowerCase(chr)) lower = true; else if (Character.isUpperCase(chr)) upper = true; else other = true; } strength += (digit) ? 1 : 0; strength += (lower) ? 1 : 0; strength += (upper) ? 1 : 0; strength += (other) ? 1 : 0; pwStrength.setText(getString(R.string.pwstrength_weak)); if (((strength >= 2 && strLength >= 7) || (strength >= 3 && strLength >= 6)) || strLength > 8) { pwStrength.setText(getString(R.string.pwstrength_good)); } if ((strength >= 3 && strLength >= 7) || strLength > 10) { pwStrength.setText(getString(R.string.pwstrength_strong)); } } } static void updateAppList() { if (mHandler != null) { mHandler.sendEmptyMessage(UPDATE_APP_REGISTERED); } } /** * Internal message handler class. * * @author werner * */ private static class InternalHandler extends Handler { private final WeakReference<KeyManagerActivity> mTarget; InternalHandler(KeyManagerActivity parent) { mTarget = new WeakReference<KeyManagerActivity>(parent); } @Override public void handleMessage(Message msg) { KeyManagerActivity parent = mTarget.get(); if (parent == null) return; if (msg.what == UPDATE_APP_REGISTERED) { parent.updateListOfApps(); return; } if (!parent.readyIntent) return; switch (msg.what) { case KEY_STORE_READY: parent.setResult(RESULT_OK); break; case KEY_STORE_FAILED: parent.setResult(RESULT_CANCELED); break; } parent.finish(); } } private void showInputInfo(String msg) { InfoMsgDialogFragment infoMsg = InfoMsgDialogFragment.newInstance(msg); FragmentManager fragmentManager = getSupportFragmentManager(); infoMsg.show(fragmentManager, "SilentCircleKeyManagerInfo"); } /* * Dialog classes to display Error and Information messages. */ private static String MESSAGE = "message"; public static class InfoMsgDialogFragment extends DialogFragment { public static InfoMsgDialogFragment newInstance(String msg) { InfoMsgDialogFragment f = new InfoMsgDialogFragment(); Bundle args = new Bundle(); args.putString(MESSAGE, msg); f.setArguments(args); return f; } @Override public Dialog onCreateDialog(Bundle savedInstanceState) { // Use the Builder class for convenient dialog construction AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); builder.setTitle(getString(R.string.information_dialog)) .setMessage(getArguments().getString(MESSAGE)) .setPositiveButton(getString(R.string.confirm_dialog), new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { } }); // Create the AlertDialog object and return it return builder.create(); } } private void showKeyManagerInfo(int msgId) { InfoKeyManagerDialog infoMsg = InfoKeyManagerDialog.newInstance(msgId); FragmentManager fragmentManager = getSupportFragmentManager(); infoMsg.show(fragmentManager, "InfoKeyManagerDialog"); } public static class InfoKeyManagerDialog extends DialogFragment { private static String MESSAGE_ID = "messageId"; public static InfoKeyManagerDialog newInstance(int msgId) { InfoKeyManagerDialog f = new InfoKeyManagerDialog(); Bundle args = new Bundle(); args.putInt(MESSAGE_ID, msgId); f.setArguments(args); return f; } @Override public Dialog onCreateDialog(Bundle savedInstanceState) { // Use the Builder class for convenient dialog construction AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); builder.setTitle(R.string.information_dialog) .setMessage(getArguments().getInt(MESSAGE_ID)) .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { } }); // Create the AlertDialog object and return it return builder.create(); } } }