Back to project page encrypted-camera.
The source code is released under:
Apache License
If you think the Android project encrypted-camera listed in this page is inappropriate, such as containing malicious code/tools or violating the copyright, please email info at java2s dot com, thanks.
/* * Copyright (C) 2014 Andrew Reitz// w ww .j av a 2 s. c om * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.andrewreitz.encryptedcamera.ui.fragment; import android.app.FragmentManager; import android.app.Notification; import android.app.NotificationManager; import android.app.ProgressDialog; import android.content.Context; import android.content.Intent; import android.os.AsyncTask; import android.os.Bundle; import android.preference.Preference; import android.preference.PreferenceFragment; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import com.andrewreitz.encryptedcamera.BuildConfig; import com.andrewreitz.encryptedcamera.EncryptedCameraApp; import com.andrewreitz.encryptedcamera.R; import com.andrewreitz.encryptedcamera.bus.EncryptionEvent; import com.andrewreitz.encryptedcamera.di.annotation.EncryptedDirectory; import com.andrewreitz.encryptedcamera.di.annotation.ForActivity; import com.andrewreitz.encryptedcamera.di.annotation.InternalDecryptedDirectory; import com.andrewreitz.encryptedcamera.di.annotation.UnlockNotification; import com.andrewreitz.encryptedcamera.encryption.EncryptionProvider; import com.andrewreitz.encryptedcamera.encryption.KeyManager; import com.andrewreitz.encryptedcamera.externalstoreage.ExternalStorageManager; import com.andrewreitz.encryptedcamera.filesystem.SecureDelete; import com.andrewreitz.encryptedcamera.sharedpreference.AppPreferenceManager; import com.andrewreitz.encryptedcamera.ui.activity.BaseActivity; import com.andrewreitz.encryptedcamera.ui.activity.AboutActivity; import com.andrewreitz.encryptedcamera.ui.activity.GalleryActivity; import com.andrewreitz.encryptedcamera.ui.dialog.ErrorDialog; import com.andrewreitz.encryptedcamera.ui.dialog.FirstRunDialog; import com.andrewreitz.encryptedcamera.ui.dialog.PasswordDialog; import com.andrewreitz.encryptedcamera.ui.dialog.SetPasswordDialog; import com.google.common.collect.ImmutableList; import com.google.common.io.Files; import com.squareup.otto.Bus; import com.squareup.otto.Subscribe; import org.jraf.android.backport.switchwidget.SwitchPreference; import org.mindrot.jbcrypt.BCrypt; import java.io.File; import java.io.IOException; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.security.cert.CertificateException; import java.security.spec.InvalidKeySpecException; import java.util.List; import javax.crypto.SecretKey; import javax.inject.Inject; import timber.log.Timber; import static com.andrewreitz.encryptedcamera.bus.EncryptionEvent.EncryptionState.NONE; public class AppPreferenceFragment extends PreferenceFragment implements SetPasswordDialog.SetPasswordDialogListener, Preference.OnPreferenceChangeListener, PasswordDialog.PasswordDialogListener, ErrorDialog.ErrorDialogCallback { private static final int NOTIFICATION_ID = 1337; @Inject NotificationManager notificationManager; @Inject KeyManager keyManager; @Inject AppPreferenceManager preferenceManager; @Inject @UnlockNotification Notification unlockNotification; @Inject @EncryptedDirectory File encryptedDirectory; @Inject @InternalDecryptedDirectory File unencryptedInternalDirectory; @Inject ExternalStorageManager externalStorageManager; @Inject EncryptionProvider encryptionProvider; @Inject FragmentManager fragmentManager; @Inject Bus bus; @Inject SecureDelete secureDelete; @Inject SecureRandom secureRandom; @Inject @ForActivity Context context; private SwitchPreference switchPreferenceDecrypt; private SwitchPreference switchPreferencePassword; private FileCryptographyTask runningTask; private EncryptionEvent.EncryptionState encryptionState = NONE; @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); BaseActivity.get(this).inject(this); if (!preferenceManager.hasSeenFirstRunFragment()) { FirstRunDialog dialog = FirstRunDialog.newInstance(); dialog.show(fragmentManager, "dialog_first_run"); preferenceManager.setHasSeenFirstLaunchFragment(true); } } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); addPreferencesFromResource(R.xml.app_preference_screen); } @Override @SuppressWarnings("ConstantConditions") public void onResume() { super.onResume(); bus.register(this); switchPreferenceDecrypt = (SwitchPreference) findPreference(getString(R.string.pref_key_decrypt)); switchPreferenceDecrypt.setOnPreferenceChangeListener(this); switchPreferencePassword = (SwitchPreference) findPreference(getString(R.string.pref_key_use_password)); switchPreferencePassword.setOnPreferenceChangeListener(this); findPreference(getString(R.string.pref_key_version)).setSummary(BuildConfig.VERSION_NAME); findPreference(getString(R.string.pref_key_about)).setIntent( new Intent(context, AboutActivity.class)); findPreference(getString(R.string.pref_key_gallery)).setIntent( new Intent(context, GalleryActivity.class)); } @Override public void onPause() { super.onPause(); if (runningTask != null) { runningTask.getProgressDialog().dismiss(); runningTask.cancel(false); } bus.unregister(this); } @Override public void onPasswordSet(final String password) { File appExternalDirectory = getAppExternalDirectory(); if (appExternalDirectory == null) return; executeDecryptFileTask(appExternalDirectory, new FileCryptographyTask.TaskFinishedCallback() { @Override public void onSuccess() { if (setNewPassword(password)) return; File appExternalDirectory = getAppExternalDirectory(); if (appExternalDirectory == null) return; encryptSdDirectory(appExternalDirectory); } @Override public void onError() { Timber.e("error encrypting images after password was set"); } }); } private boolean setNewPassword(String password) { byte[] salt = new byte[10]; secureRandom.nextBytes(salt); try { SecretKey secretKey = keyManager.generateKeyWithPassword(password.toCharArray(), salt); encryptionProvider.setSecretKey(secretKey); keyManager.saveKey(EncryptedCameraApp.KEY_STORE_ALIAS, secretKey); keyManager.saveKeyStore(); } catch (NoSuchAlgorithmException | InvalidKeySpecException | CertificateException | KeyStoreException | IOException e) { Timber.e(e, "Error saving encryption key with password"); ErrorDialog.newInstance(getString(R.string.encryption_error), getString(R.string.error_saving_encryption_key)); return true; } preferenceManager.setPassword(password); preferenceManager.setSalt(salt); preferenceManager.setHasPassword(true); switchPreferencePassword.setChecked(true); return false; } @Override public void onPasswordEntered(String password) { if (!doPasswordCheck(password)) { showIncorrectPasswordDialog(); return; } if (!setSecretKey(password)) return; decryptToSdDirectory(externalStorageManager.getAppExternalDirectory()); } @Override public void onPasswordCancel() { switchPreferenceDecrypt.setChecked(false); } @Override public void onPasswordSetCancel() { } @Override public void onErrorDialogDismissed() { // TODO Remove All Get Activities and make a manager //noinspection ConstantConditions getActivity().finish(); } @Override public boolean onPreferenceChange(Preference preference, Object newValue) { if (checkNotCurrentlyEncrypting()) return false; //newValue should always be a boolean but just to be sure if (!Boolean.class.isInstance(newValue)) { throw new IllegalArgumentException("newValue is not a boolean"); } boolean value = (boolean) newValue; //noinspection ConstantConditions if (preference.getKey().equals(getString(R.string.pref_key_use_password))) { return handleUsePasswordPreference(value); } else if (preference.getKey().equals(getString(R.string.pref_key_decrypt))) { return handleDecryptedPreference(value); } throw new RuntimeException("Unknown preference passed in preference == " + preference.getKey()); } private boolean checkNotCurrentlyEncrypting() { if (!encryptionState.equals(NONE)) { showErrorDialog(getString(R.string.error), EncryptionEvent.EncryptionState.DECRYPTING == encryptionState ? getString( R.string.error_currently_decrypting) : getString(R.string.error_currently_encrypting), "error_decrypting_in_progress"); return true; } return false; } @SuppressWarnings("UnusedDeclaration") @Subscribe public void handleEncryptionEvent( EncryptionEvent event) { if (event.state.equals(NONE)) { getActivity().setProgressBarIndeterminateVisibility(false); } else { getActivity().setProgressBarIndeterminateVisibility(true); } encryptionState = event.state; } private boolean handleDecryptedPreference(boolean value) { handleEncryption(value); return false; } private boolean handleUsePasswordPreference(boolean checked) { if (preferenceManager.isDecrypted()) { // don't allow changing password while photos are decrypted showErrorDialog(getString(R.string.error), getString(R.string.error_change_password_while_decrypted), "error_change_password_while_decrypted"); return false; } else if (checked && !preferenceManager.hasPassword()) { // check if a password has already been set do to the filtering done for passwords SetPasswordDialog.newInstance(this).show(fragmentManager, "password_dialog"); return false; } else { if (preferenceManager.hasPassword()) { turnOffPassword(); return false; } else { createKeyNoPassword(); return true; } } } private void turnOffPassword() { PasswordDialog.newInstance(new PasswordDialog.PasswordDialogListener() { // Create custom because one in activity does not meet our needs @Override public void onPasswordEntered(String password) { if (!doPasswordCheck(password)) { showIncorrectPasswordDialog(); return; } if (!setSecretKey(password)) return; if (decryptFilesInternally(password)) return; reEncryptFilesInternally(); } @Override public void onPasswordCancel() { } }).show(fragmentManager, "get_password_dialog"); } private void reEncryptFilesInternally() { try { //noinspection ConstantConditions for (File in : encryptedDirectory.listFiles()) { File out = new File(in.getPath().replace(".tmp", "")); encryptionProvider.encrypt(in, out); //noinspection ResultOfMethodCallIgnored in.delete(); } switchPreferencePassword.setChecked(false); } catch (InvalidKeyException | IOException | InvalidAlgorithmParameterException e) { Timber.w(e, "Error encrypting files without a password"); // if this exception really happens the application won't work // We should really crash. throw new RuntimeException(e); } } private boolean decryptFilesInternally(@NonNull String password) { if (!doPasswordCheck(password)) { if (!doPasswordCheck(password)) { showIncorrectPasswordDialog(); return false; } } try { //noinspection ConstantConditions for (File in : encryptedDirectory.listFiles()) { File out = new File(encryptedDirectory, in.getName() + ".tmp"); encryptionProvider.decrypt(in, out); //noinspection ResultOfMethodCallIgnored in.delete(); } } catch (InvalidKeyException | IOException | InvalidAlgorithmParameterException e) { Timber.w(e, "error unencrypting internally"); showErrorDialog(getString(R.string.error), getString(R.string.error_incorrect_password), "error_dialog_removing_password"); return true; } return false; } private boolean createKeyNoPassword() { // Create a keystore for encryption that does not require a password try { SecretKey secretKey = keyManager.generateKeyNoPassword(); encryptionProvider.setSecretKey(secretKey); keyManager.saveKey(EncryptedCameraApp.KEY_STORE_ALIAS, secretKey); keyManager.saveKeyStore(); return true; } catch (NoSuchAlgorithmException | KeyStoreException | IOException | CertificateException e) { // The app really wouldn't work at this point Timber.e(e, "Error saving encryption key, no password"); showErrorDialog(getString(R.string.encryption_error), getString(R.string.error_saving_encryption_key), "error_dialog_generate_key_no_password"); } return false; } /** true for decrypt, false for encrypt */ private void handleEncryption(boolean decrypt) { File appExternalDirectory = getAppExternalDirectory(); if (appExternalDirectory == null) return; if (decrypt) { if (preferenceManager.hasPassword()) { PasswordDialog passwordDialog = PasswordDialog.newInstance(this); //noinspection ConstantConditions passwordDialog.show(fragmentManager, "dialog_get_password"); } else { decryptToSdDirectory(appExternalDirectory); } } else { encryptSdDirectory(appExternalDirectory); } } /** * Would pull this out and just set once, but we need to constantly check due to possibility of * changing state */ private File getAppExternalDirectory() { File appExternalDirectory = externalStorageManager.getAppExternalDirectory(); if (appExternalDirectory == null || !externalStorageManager.checkSdCardIsInReadAndWriteState()) { //noinspection ConstantConditions showErrorDialog(getString(R.string.error), getString(R.string.error_sdcard_message), "error_dialog_sdcard"); return null; } return appExternalDirectory; } private void decryptToSdDirectory(@NonNull File appExternalDirectory) { this.decryptToSdDirectory(appExternalDirectory, null); } private void decryptToSdDirectory(@NonNull File appExternalDirectory, @Nullable String password) { if (password != null && !doPasswordCheck(password)) { showIncorrectPasswordDialog(); return; } executeDecryptFileTask(appExternalDirectory, new FileCryptographyTask.TaskFinishedCallback() { @Override public void onSuccess() { notificationManager.notify(NOTIFICATION_ID, unlockNotification); switchPreferenceDecrypt.setChecked(true); } @Override public void onError() { // there was an error reset the switch preferences switchPreferenceDecrypt.setChecked(false); } }); } private void executeDecryptFileTask(@NonNull File appExternalDirectory, FileCryptographyTask.TaskFinishedCallback callback) { //noinspection ConstantConditions runningTask = new DecryptFilesTask(appExternalDirectory, encryptionProvider, context, ImmutableList.copyOf(encryptedDirectory.listFiles()), callback, getString(R.string.decrypting_files), bus); //noinspection unchecked runningTask.execute(); } private void showIncorrectPasswordDialog() { showErrorDialog(getString(R.string.error), getString(R.string.error_incorrect_password), "error_dialog_encrypt_pw"); } private void encryptSdDirectory(File appExternalDirectory) { executeEncryptFileTask(appExternalDirectory, new FileCryptographyTask.TaskFinishedCallback() { @Override public void onSuccess() { notificationManager.cancel(NOTIFICATION_ID); switchPreferenceDecrypt.setChecked(false); } @Override public void onError() { switchPreferenceDecrypt.setChecked(true); } }); } private void executeEncryptFileTask(File appExternalDirectory, FileCryptographyTask.TaskFinishedCallback callback) { //noinspection ConstantConditions runningTask = new EncryptFilesTask(unencryptedInternalDirectory, encryptedDirectory, secureDelete, context, ImmutableList.copyOf(appExternalDirectory.listFiles()), encryptionProvider, callback, getString(R.string.encrypting_files), bus); runningTask.execute(); } private boolean setSecretKey(String password) { try { // recreate the secret key and give it to the encryption provider SecretKey key = keyManager.generateKeyWithPassword(password.toCharArray(), preferenceManager.getSalt()); encryptionProvider.setSecretKey(key); return true; } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { Timber.w(e, "Error recreating secret key. This probably should never happen"); showErrorDialog(getString(R.string.error), getString(R.string.error_terrible), "error_dialog_recreate_key"); } return false; } private void showErrorDialog(String error, String message, String tag) { showErrorDialog(error, message, tag, null); } private void showErrorDialog(@Nullable String error, @Nullable String message, @Nullable String tag, @Nullable ErrorDialog.ErrorDialogCallback callback) { ErrorDialog errorDialog = ErrorDialog.newInstance(error, message); if (callback != null) { errorDialog.setCallback(callback); } errorDialog.show(fragmentManager, tag); } private boolean doPasswordCheck(@NonNull String password) { String passwordHash = preferenceManager.getPasswordHash(); return BCrypt.checkpw(password, passwordHash); } private static abstract class FileCryptographyTask extends AsyncTask<Void, Void, Boolean> { private final Context context; private final List<File> files; private final TaskFinishedCallback callback; private final String progressMessage; private final Bus bus; private ProgressDialog progressDialog; FileCryptographyTask(@NonNull Context context, @NonNull List<File> files, @NonNull TaskFinishedCallback callback, @NonNull String progressMessage, @NonNull Bus bus) { this.context = context; this.files = files; this.callback = callback; this.progressMessage = progressMessage; this.bus = bus; } @Override final protected void onPreExecute() { progressDialog = new ProgressDialog(context); progressDialog.setCancelable(true); progressDialog.setMessage(progressMessage); progressDialog.setCanceledOnTouchOutside(false); progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); progressDialog.setProgress(0); progressDialog.setMax(getFiles().size()); progressDialog.show(); } @Override final protected void onProgressUpdate(Void... values) { getProgressDialog().setProgress(1 + getProgressDialog().getProgress()); } @Override protected void onCancelled(Boolean success) { finishedExecuting(success); } @Override protected void onPostExecute(Boolean success) { if (!isCancelled()) { progressDialog.dismiss(); } finishedExecuting(success); } protected Context getContext() { return context; } protected List<File> getFiles() { return files; } protected ProgressDialog getProgressDialog() { return progressDialog; } protected Bus getBus() { return bus; } private void finishedExecuting(boolean success) { getBus().post(new EncryptionEvent(NONE)); if (success) { callback.onSuccess(); } else { callback.onError(); } } public interface TaskFinishedCallback { void onSuccess(); void onError(); } } private static final class DecryptFilesTask extends FileCryptographyTask { private final File appExternalDirectory; private final EncryptionProvider encryptionProvider; DecryptFilesTask(@NonNull File appExternalDirectory, @NonNull EncryptionProvider encryptionProvider, @NonNull Context context, @NonNull List<File> files, @NonNull TaskFinishedCallback callback, @NonNull String progressMessage, @NonNull Bus bus) { super(context, files, callback, progressMessage, bus); this.appExternalDirectory = appExternalDirectory; this.encryptionProvider = encryptionProvider; bus.post(new EncryptionEvent(EncryptionEvent.EncryptionState.DECRYPTING)); } @Override protected Boolean doInBackground(Void... params) { boolean success = true; for (File encrypted : getFiles()) { File unencrypted = new File(appExternalDirectory, encrypted.getName()); try { encryptionProvider.decrypt(encrypted, unencrypted); //noinspection ResultOfMethodCallIgnored encrypted.delete(); } catch (InvalidKeyException | IOException | InvalidAlgorithmParameterException e) { Timber.d(e, "unable to decrypt and move file %s to sdcard", encrypted.getPath()); // Deleted the file that was put on the sdcard and was not the full file //noinspection ResultOfMethodCallIgnored unencrypted.delete(); success = false; } publishProgress(); } return success; } } private final static class EncryptFilesTask extends FileCryptographyTask { private final File appInternalUnencryptedDirectory; private final File appInternalEncryptedDirectory; private final SecureDelete secureDelete; private final EncryptionProvider encryptionProvider; EncryptFilesTask(@NonNull File appInternalUnencryptedDirectory, @NonNull File appInternalEncryptedDirectory, @NonNull SecureDelete secureDelete, @NonNull Context context, @NonNull List<File> files, @NonNull EncryptionProvider encryptionProvider, @NonNull TaskFinishedCallback callback, @NonNull String progressMessage, @NonNull Bus bus) { super(context, files, callback, progressMessage, bus); this.appInternalEncryptedDirectory = appInternalEncryptedDirectory; this.appInternalUnencryptedDirectory = appInternalUnencryptedDirectory; this.secureDelete = secureDelete; this.encryptionProvider = encryptionProvider; bus.post(new EncryptionEvent(EncryptionEvent.EncryptionState.ENCRYPTING)); } @Override protected Boolean doInBackground(Void... params) { boolean success = true; for (File unencryptedFile : getFiles()) { File unencryptedInternal = new File(appInternalUnencryptedDirectory, unencryptedFile.getName()); File encryptedFile = new File(appInternalEncryptedDirectory, unencryptedFile.getName()); try { // Copy the file internally so the user can't mess with it while we are encrypting Files.copy(unencryptedFile, unencryptedInternal); // File moved internally now delete the original secureDelete.secureDelete(unencryptedFile); //noinspection ResultOfMethodCallIgnored encryptedFile.createNewFile(); encryptionProvider.encrypt(unencryptedInternal, encryptedFile); //noinspection ResultOfMethodCallIgnored unencryptedInternal.delete(); } catch (IOException | InvalidKeyException | InvalidAlgorithmParameterException e) { Timber.e(e, "Error encrypting and saving image"); success = false; } publishProgress(); } return success; } } }