diff options
Diffstat (limited to 'app/src')
-rw-r--r-- | app/src/main/AndroidManifest.xml | 4 | ||||
-rw-r--r-- | app/src/main/java/com/zeapo/pwdstore/PasswordStore.java | 920 | ||||
-rw-r--r-- | app/src/main/java/com/zeapo/pwdstore/PasswordStore.kt | 731 | ||||
-rw-r--r-- | app/src/main/java/com/zeapo/pwdstore/SshKeyGen.java | 264 | ||||
-rw-r--r-- | app/src/main/java/com/zeapo/pwdstore/UserPreference.kt | 6 | ||||
-rw-r--r-- | app/src/main/java/com/zeapo/pwdstore/sshkeygen/ShowSshKeyFragment.kt | 69 | ||||
-rw-r--r-- | app/src/main/java/com/zeapo/pwdstore/sshkeygen/SshKeyGenActivity.kt | 35 | ||||
-rw-r--r-- | app/src/main/java/com/zeapo/pwdstore/sshkeygen/SshKeyGenFragment.kt | 90 | ||||
-rw-r--r-- | app/src/main/java/com/zeapo/pwdstore/sshkeygen/SshKeygenTask.kt | 77 | ||||
-rw-r--r-- | app/src/main/res/font/sourcecodepro.ttf | bin | 0 -> 52660 bytes | |||
-rw-r--r-- | app/src/main/res/layout/fragment_ssh_keygen.xml | 9 | ||||
-rw-r--r-- | app/src/main/res/values/strings.xml | 3 |
12 files changed, 1016 insertions, 1192 deletions
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a0eebd82..80297833 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -42,9 +42,6 @@ <activity android:name=".UserPreference" android:parentActivityName=".PasswordStore" /> - - <activity android:name=".SshKeyGen" /> - <service android:name=".autofill.AutofillService" android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"> @@ -69,6 +66,7 @@ android:name=".crypto.PgpActivity" android:parentActivityName=".PasswordStore" /> <activity android:name=".SelectFolderActivity" /> + <activity android:name=".sshkeygen.SshKeyGenActivity" /> </application> </manifest> diff --git a/app/src/main/java/com/zeapo/pwdstore/PasswordStore.java b/app/src/main/java/com/zeapo/pwdstore/PasswordStore.java deleted file mode 100644 index d147f300..00000000 --- a/app/src/main/java/com/zeapo/pwdstore/PasswordStore.java +++ /dev/null @@ -1,920 +0,0 @@ -/* - * Copyright © 2014-2019 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ -package com.zeapo.pwdstore; - -import android.Manifest; -import android.annotation.SuppressLint; -import android.app.Activity; -import android.content.Intent; -import android.content.SharedPreferences; -import android.content.pm.PackageManager; -import android.content.pm.ShortcutInfo; -import android.content.pm.ShortcutManager; -import android.graphics.Color; -import android.graphics.drawable.Icon; -import android.os.Build; -import android.os.Bundle; -import android.text.TextUtils; -import android.util.Log; -import android.view.KeyEvent; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; -import androidx.annotation.NonNull; -import androidx.appcompat.app.AppCompatActivity; -import androidx.appcompat.widget.AppCompatTextView; -import androidx.appcompat.widget.SearchView; -import androidx.core.app.ActivityCompat; -import androidx.core.content.ContextCompat; -import androidx.fragment.app.FragmentManager; -import androidx.fragment.app.FragmentTransaction; -import androidx.preference.PreferenceManager; -import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import com.google.android.material.snackbar.Snackbar; -import com.zeapo.pwdstore.crypto.PgpActivity; -import com.zeapo.pwdstore.git.GitActivity; -import com.zeapo.pwdstore.git.GitAsyncTask; -import com.zeapo.pwdstore.git.GitOperation; -import com.zeapo.pwdstore.ui.adapters.PasswordRecyclerAdapter; -import com.zeapo.pwdstore.utils.PasswordItem; -import com.zeapo.pwdstore.utils.PasswordRepository; -import java.io.File; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Set; -import org.apache.commons.io.FileUtils; -import org.apache.commons.io.FilenameUtils; -import org.eclipse.jgit.api.Git; -import org.eclipse.jgit.api.errors.GitAPIException; -import org.eclipse.jgit.lib.Repository; -import org.eclipse.jgit.revwalk.RevCommit; - -public class PasswordStore extends AppCompatActivity { - - public static final int REQUEST_CODE_SIGN = 9910; - public static final int REQUEST_CODE_ENCRYPT = 9911; - public static final int REQUEST_CODE_SIGN_AND_ENCRYPT = 9912; - public static final int REQUEST_CODE_DECRYPT_AND_VERIFY = 9913; - public static final int REQUEST_CODE_GET_KEY = 9914; - public static final int REQUEST_CODE_GET_KEY_IDS = 9915; - public static final int REQUEST_CODE_EDIT = 9916; - public static final int REQUEST_CODE_SELECT_FOLDER = 9917; - private static final String TAG = PasswordStore.class.getName(); - private static final int CLONE_REPO_BUTTON = 401; - private static final int NEW_REPO_BUTTON = 402; - private static final int HOME = 403; - private static final int REQUEST_EXTERNAL_STORAGE = 50; - private SharedPreferences settings; - private Activity activity; - private PasswordFragment plist; - private ShortcutManager shortcutManager; - private MenuItem searchItem = null; - private SearchView searchView; - - private static boolean isPrintable(char c) { - Character.UnicodeBlock block = Character.UnicodeBlock.of(c); - return (!Character.isISOControl(c)) - && block != null - && block != Character.UnicodeBlock.SPECIALS; - } - - @Override - public boolean onKeyDown(int keyCode, KeyEvent event) { - // open search view on search key, or Ctr+F - if ((keyCode == KeyEvent.KEYCODE_SEARCH - || keyCode == KeyEvent.KEYCODE_F && event.isCtrlPressed()) - && !searchItem.isActionViewExpanded()) { - searchItem.expandActionView(); - return true; - } - - // open search view on any printable character and query for it - char c = (char) event.getUnicodeChar(); - boolean printable = isPrintable(c); - if (printable && !searchItem.isActionViewExpanded()) { - searchItem.expandActionView(); - searchView.setQuery(Character.toString(c), true); - return true; - } - - return super.onKeyDown(keyCode, event); - } - - @Override - @SuppressLint("NewApi") - protected void onCreate(Bundle savedInstanceState) { - settings = PreferenceManager.getDefaultSharedPreferences(this.getApplicationContext()); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { - shortcutManager = getSystemService(ShortcutManager.class); - } - activity = this; - - // If user opens app with permission granted then revokes and returns, - // prevent attempt to create password list fragment - if (savedInstanceState != null - && (!settings.getBoolean("git_external", false) - || ContextCompat.checkSelfPermission( - activity, Manifest.permission.READ_EXTERNAL_STORAGE) - != PackageManager.PERMISSION_GRANTED)) { - savedInstanceState = null; - } - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_pwdstore); - } - - @Override - public void onResume() { - super.onResume(); - // do not attempt to checkLocalRepository() if no storage permission: immediate crash - if (settings.getBoolean("git_external", false)) { - if (ContextCompat.checkSelfPermission( - activity, Manifest.permission.READ_EXTERNAL_STORAGE) - != PackageManager.PERMISSION_GRANTED) { - - if (ActivityCompat.shouldShowRequestPermissionRationale( - activity, Manifest.permission.READ_EXTERNAL_STORAGE)) { - // TODO: strings.xml - Snackbar snack = - Snackbar.make( - findViewById(R.id.main_layout), - "The store is on the sdcard but the app does not have permission to access it. Please give permission.", - Snackbar.LENGTH_INDEFINITE) - .setAction( - R.string.dialog_ok, - view -> - ActivityCompat.requestPermissions( - activity, - new String[] { - Manifest.permission - .READ_EXTERNAL_STORAGE - }, - REQUEST_EXTERNAL_STORAGE)); - snack.show(); - View view = snack.getView(); - AppCompatTextView tv = - view.findViewById(com.google.android.material.R.id.snackbar_text); - tv.setTextColor(Color.WHITE); - tv.setMaxLines(10); - } else { - // No explanation needed, we can request the permission. - ActivityCompat.requestPermissions( - activity, - new String[] {Manifest.permission.READ_EXTERNAL_STORAGE}, - REQUEST_EXTERNAL_STORAGE); - } - } else { - checkLocalRepository(); - } - - } else { - checkLocalRepository(); - } - } - - @Override - public void onRequestPermissionsResult( - int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { - // If request is cancelled, the result arrays are empty. - if (requestCode == REQUEST_EXTERNAL_STORAGE) { - if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - checkLocalRepository(); - } - } - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - // Inflate the menu; this adds items to the action bar if it is present. - getMenuInflater().inflate(R.menu.main_menu, menu); - searchItem = menu.findItem(R.id.action_search); - searchView = (SearchView) searchItem.getActionView(); - - searchView.setOnQueryTextListener( - new SearchView.OnQueryTextListener() { - @Override - public boolean onQueryTextSubmit(String s) { - return true; - } - - @Override - public boolean onQueryTextChange(String s) { - filterListAdapter(s); - return true; - } - }); - - // When using the support library, the setOnActionExpandListener() method is - // static and accepts the MenuItem object as an argument - searchItem.setOnActionExpandListener( - new MenuItem.OnActionExpandListener() { - @Override - public boolean onMenuItemActionCollapse(MenuItem item) { - refreshListAdapter(); - return true; - } - - @Override - public boolean onMenuItemActionExpand(MenuItem item) { - return true; - } - }); - return super.onCreateOptionsMenu(menu); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - // Handle action bar item clicks here. The action bar will - // automatically handle clicks on the Home/Up button, so long - // as you specify a parent activity in AndroidManifest.xml. - int id = item.getItemId(); - Intent intent; - - final MaterialAlertDialogBuilder initBefore = - new MaterialAlertDialogBuilder(this) - .setMessage(this.getResources().getString(R.string.creation_dialog_text)) - .setPositiveButton(this.getResources().getString(R.string.dialog_ok), null); - - switch (id) { - case R.id.user_pref: - try { - intent = new Intent(this, UserPreference.class); - startActivity(intent); - } catch (Exception e) { - System.out.println("Exception caught :("); - e.printStackTrace(); - } - return true; - case R.id.git_push: - if (!PasswordRepository.isInitialized()) { - initBefore.show(); - break; - } - - intent = new Intent(this, GitActivity.class); - intent.putExtra("Operation", GitActivity.REQUEST_PUSH); - startActivityForResult(intent, GitActivity.REQUEST_PUSH); - return true; - - case R.id.git_pull: - if (!PasswordRepository.isInitialized()) { - initBefore.show(); - break; - } - - intent = new Intent(this, GitActivity.class); - intent.putExtra("Operation", GitActivity.REQUEST_PULL); - startActivityForResult(intent, GitActivity.REQUEST_PULL); - return true; - - case R.id.git_sync: - if (!PasswordRepository.isInitialized()) { - initBefore.show(); - break; - } - - intent = new Intent(this, GitActivity.class); - intent.putExtra("Operation", GitActivity.REQUEST_SYNC); - startActivityForResult(intent, GitActivity.REQUEST_SYNC); - return true; - - case R.id.refresh: - updateListAdapter(); - return true; - - case android.R.id.home: - this.onBackPressed(); - break; - - default: - break; - } - - return super.onOptionsItemSelected(item); - } - - public void openSettings(View view) { - Intent intent; - - try { - intent = new Intent(this, UserPreference.class); - startActivity(intent); - } catch (Exception e) { - System.out.println("Exception caught :("); - e.printStackTrace(); - } - } - - public void cloneExistingRepository(View view) { - initRepository(CLONE_REPO_BUTTON); - } - - public void createNewRepository(View view) { - initRepository(NEW_REPO_BUTTON); - } - - private void createRepository() { - if (!PasswordRepository.isInitialized()) { - PasswordRepository.initialize(this); - } - - final File localDir = PasswordRepository.getRepositoryDirectory(getApplicationContext()); - try { - if (!localDir.mkdir()) throw new IllegalStateException("Failed to create directory!"); - PasswordRepository.createRepository(localDir); - if (new File(localDir.getAbsolutePath() + "/.gpg-id").createNewFile()) { - settings.edit().putBoolean("repository_initialized", true).apply(); - } else { - throw new IllegalStateException("Failed to initialize repository state."); - } - } catch (Exception e) { - e.printStackTrace(); - if (!localDir.delete()) { - Log.d(TAG, "Failed to delete local repository"); - } - return; - } - checkLocalRepository(); - } - - private void initializeRepositoryInfo() { - final String externalRepoPath = settings.getString("git_external_repo", null); - if (settings.getBoolean("git_external", false) && externalRepoPath != null) { - File dir = new File(externalRepoPath); - - if (dir.exists() - && dir.isDirectory() - && !PasswordRepository.getPasswords( - dir, - PasswordRepository.getRepositoryDirectory(this), - getSortOrder()) - .isEmpty()) { - - PasswordRepository.closeRepository(); - checkLocalRepository(); - return; // if not empty, just show me the passwords! - } - } - - final Set<String> keyIds = settings.getStringSet("openpgp_key_ids_set", new HashSet<>()); - - if (keyIds.isEmpty()) - new MaterialAlertDialogBuilder(this) - .setMessage(this.getResources().getString(R.string.key_dialog_text)) - .setPositiveButton( - this.getResources().getString(R.string.dialog_positive), - (dialogInterface, i) -> { - Intent intent = new Intent(activity, UserPreference.class); - startActivityForResult(intent, GitActivity.REQUEST_INIT); - }) - .setNegativeButton( - this.getResources().getString(R.string.dialog_negative), null) - .show(); - - createRepository(); - } - - private void checkLocalRepository() { - Repository repo = PasswordRepository.initialize(this); - if (repo == null) { - Intent intent = new Intent(activity, UserPreference.class); - intent.putExtra("operation", "git_external"); - startActivityForResult(intent, HOME); - } else { - checkLocalRepository( - PasswordRepository.getRepositoryDirectory(getApplicationContext())); - } - } - - private void checkLocalRepository(File localDir) { - FragmentManager fragmentManager = getSupportFragmentManager(); - FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction(); - if (localDir != null && settings.getBoolean("repository_initialized", false)) { - Log.d(TAG, "Check, dir: " + localDir.getAbsolutePath()); - // do not push the fragment if we already have it - if (fragmentManager.findFragmentByTag("PasswordsList") == null - || settings.getBoolean("repo_changed", false)) { - settings.edit().putBoolean("repo_changed", false).apply(); - - plist = new PasswordFragment(); - Bundle args = new Bundle(); - args.putString( - "Path", - PasswordRepository.getRepositoryDirectory(getApplicationContext()) - .getAbsolutePath()); - - // if the activity was started from the autofill settings, the - // intent is to match a clicked pwd with app. pass this to fragment - if (getIntent().getBooleanExtra("matchWith", false)) { - args.putBoolean("matchWith", true); - } - - plist.setArguments(args); - - getSupportActionBar().show(); - getSupportActionBar().setDisplayHomeAsUpEnabled(false); - - fragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE); - - fragmentTransaction.replace(R.id.main_layout, plist, "PasswordsList"); - fragmentTransaction.commit(); - } - } else { - getSupportActionBar().hide(); - - fragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE); - - ToCloneOrNot cloneFrag = new ToCloneOrNot(); - fragmentTransaction.replace(R.id.main_layout, cloneFrag, "ToCloneOrNot"); - fragmentTransaction.commit(); - } - } - - @Override - public void onBackPressed() { - if ((null != plist) && plist.isNotEmpty()) { - plist.popBack(); - } else { - super.onBackPressed(); - } - - if (null != plist && !plist.isNotEmpty()) { - getSupportActionBar().setDisplayHomeAsUpEnabled(false); - } - } - - private String getRelativePath(String fullPath, String repositoryPath) { - return fullPath.replace(repositoryPath, "").replaceAll("/+", "/"); - } - - public long getLastChangedTimestamp(String fullPath) { - File repoPath = PasswordRepository.getRepositoryDirectory(this); - Repository repository = PasswordRepository.getRepository(repoPath); - - if (repository == null) { - Log.d(TAG, "getLastChangedTimestamp: No git repository"); - return new File(fullPath).lastModified(); - } - - Git git = new Git(repository); - String relativePath = - getRelativePath(fullPath, repoPath.getAbsolutePath()) - .substring(1); // Removes leading '/' - - Iterator<RevCommit> iterator; - try { - iterator = git.log().addPath(relativePath).call().iterator(); - } catch (GitAPIException e) { - Log.e(TAG, "getLastChangedTimestamp: GITAPIException", e); - return -1; - } - - if (!iterator.hasNext()) { - Log.w(TAG, "getLastChangedTimestamp: No commits for file: " + relativePath); - return -1; - } - - return ((long) iterator.next().getCommitTime()) * 1000; - } - - public void decryptPassword(PasswordItem item) { - Intent decryptIntent = new Intent(this, PgpActivity.class); - Intent authDecryptIntent = new Intent(this, LaunchActivity.class); - for (Intent intent : new Intent[] {decryptIntent, authDecryptIntent}) { - intent.putExtra("NAME", item.toString()); - intent.putExtra("FILE_PATH", item.getFile().getAbsolutePath()); - intent.putExtra( - "REPO_PATH", - PasswordRepository.getRepositoryDirectory(getApplicationContext()) - .getAbsolutePath()); - intent.putExtra( - "LAST_CHANGED_TIMESTAMP", - getLastChangedTimestamp(item.getFile().getAbsolutePath())); - intent.putExtra("OPERATION", "DECRYPT"); - } - - // Adds shortcut - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { - ShortcutInfo shortcut = - new ShortcutInfo.Builder(this, item.getFullPathToParent()) - .setShortLabel(item.toString()) - .setLongLabel(item.getFullPathToParent() + item.toString()) - .setIcon(Icon.createWithResource(this, R.mipmap.ic_launcher)) - .setIntent(authDecryptIntent.setAction("DECRYPT_PASS")) // Needs action - .build(); - List<ShortcutInfo> shortcuts = shortcutManager.getDynamicShortcuts(); - - if (shortcuts.size() >= shortcutManager.getMaxShortcutCountPerActivity() - && shortcuts.size() > 0) { - shortcuts.remove(shortcuts.size() - 1); - shortcuts.add(0, shortcut); - shortcutManager.setDynamicShortcuts(shortcuts); - } else { - shortcutManager.addDynamicShortcuts(Collections.singletonList(shortcut)); - } - } - startActivityForResult(decryptIntent, REQUEST_CODE_DECRYPT_AND_VERIFY); - } - - public void editPassword(PasswordItem item) { - Intent intent = new Intent(this, PgpActivity.class); - intent.putExtra("NAME", item.toString()); - intent.putExtra("FILE_PATH", item.getFile().getAbsolutePath()); - intent.putExtra("PARENT_PATH", getCurrentDir().getAbsolutePath()); - intent.putExtra( - "REPO_PATH", - PasswordRepository.getRepositoryDirectory(getApplicationContext()) - .getAbsolutePath()); - intent.putExtra("OPERATION", "EDIT"); - startActivityForResult(intent, REQUEST_CODE_EDIT); - } - - public void createPassword() { - if (!PasswordRepository.isInitialized()) { - new MaterialAlertDialogBuilder(this) - .setMessage(this.getResources().getString(R.string.creation_dialog_text)) - .setPositiveButton( - this.getResources().getString(R.string.dialog_ok), - (dialogInterface, i) -> {}) - .show(); - return; - } - - if (settings.getStringSet("openpgp_key_ids_set", new HashSet<>()).isEmpty()) { - new MaterialAlertDialogBuilder(this) - .setTitle(this.getResources().getString(R.string.no_key_selected_dialog_title)) - .setMessage(this.getResources().getString(R.string.no_key_selected_dialog_text)) - .setPositiveButton( - this.getResources().getString(R.string.dialog_ok), - (dialogInterface, i) -> { - Intent intent = new Intent(activity, UserPreference.class); - startActivity(intent); - }) - .show(); - return; - } - - File currentDir = getCurrentDir(); - Log.i(TAG, "Adding file to : " + currentDir.getAbsolutePath()); - - Intent intent = new Intent(this, PgpActivity.class); - intent.putExtra("FILE_PATH", getCurrentDir().getAbsolutePath()); - intent.putExtra( - "REPO_PATH", - PasswordRepository.getRepositoryDirectory(getApplicationContext()) - .getAbsolutePath()); - intent.putExtra("OPERATION", "ENCRYPT"); - startActivityForResult(intent, REQUEST_CODE_ENCRYPT); - } - - // deletes passwords in order from top to bottom - public void deletePasswords( - final PasswordRecyclerAdapter adapter, final Set<Integer> selectedItems) { - final Iterator it = selectedItems.iterator(); - if (!it.hasNext()) { - return; - } - final int position = (int) it.next(); - final PasswordItem item = adapter.getValues().get(position); - new MaterialAlertDialogBuilder(this) - .setMessage( - getResources().getString(R.string.delete_dialog_text, item.getLongName())) - .setPositiveButton( - getResources().getString(R.string.dialog_yes), - (dialogInterface, i) -> { - item.getFile().delete(); - adapter.remove(position); - it.remove(); - adapter.updateSelectedItems(position, selectedItems); - - commitChange( - getResources() - .getString( - R.string.git_commit_remove_text, - item.getLongName())); - deletePasswords(adapter, selectedItems); - }) - .setNegativeButton( - this.getResources().getString(R.string.dialog_no), - (dialogInterface, i) -> { - it.remove(); - deletePasswords(adapter, selectedItems); - }) - .show(); - } - - public void movePasswords(ArrayList<PasswordItem> values) { - Intent intent = new Intent(this, SelectFolderActivity.class); - ArrayList<String> fileLocations = new ArrayList<>(); - for (PasswordItem passwordItem : values) { - fileLocations.add(passwordItem.getFile().getAbsolutePath()); - } - intent.putExtra("Files", fileLocations); - intent.putExtra("Operation", "SELECTFOLDER"); - startActivityForResult(intent, REQUEST_CODE_SELECT_FOLDER); - } - - /** clears adapter's content and updates it with a fresh list of passwords from the root */ - public void updateListAdapter() { - if ((null != plist)) { - plist.updateAdapter(); - } - } - - /** Updates the adapter with the current view of passwords */ - private void refreshListAdapter() { - if ((null != plist)) { - plist.refreshAdapter(); - } - } - - private void filterListAdapter(String filter) { - if ((null != plist)) { - plist.filterAdapter(filter); - } - } - - private File getCurrentDir() { - if ((null != plist)) { - return plist.getCurrentDir(); - } - return PasswordRepository.getRepositoryDirectory(getApplicationContext()); - } - - private void commitChange(final String message) { - new GitOperation(PasswordRepository.getRepositoryDirectory(activity), activity) { - @Override - public void execute() { - Log.d(TAG, "Committing with message " + message); - Git git = new Git(getRepository()); - GitAsyncTask tasks = new GitAsyncTask(activity, false, true, this); - tasks.execute( - git.add().addFilepattern("."), - git.commit().setAll(true).setMessage(message)); - } - }.execute(); - } - - protected void onActivityResult(int requestCode, int resultCode, Intent data) { - if (resultCode == RESULT_OK) { - switch (requestCode) { - case GitActivity.REQUEST_CLONE: - // if we get here with a RESULT_OK then it's probably OK :) - settings.edit().putBoolean("repository_initialized", true).apply(); - break; - case REQUEST_CODE_DECRYPT_AND_VERIFY: - // if went from decrypt->edit and user saved changes or HOTP counter was - // incremented, we need to commitChange - if (data != null && data.getBooleanExtra("needCommit", false)) { - if (data.getStringExtra("OPERATION").equals("EDIT")) { - commitChange( - this.getResources() - .getString( - R.string.git_commit_edit_text, - data.getExtras().getString("LONG_NAME"))); - } else { - commitChange( - this.getResources() - .getString( - R.string.git_commit_increment_text, - data.getExtras().getString("LONG_NAME"))); - } - } - refreshListAdapter(); - break; - case REQUEST_CODE_ENCRYPT: - commitChange( - this.getResources() - .getString( - R.string.git_commit_add_text, - data.getExtras().getString("LONG_NAME"))); - refreshListAdapter(); - break; - case REQUEST_CODE_EDIT: - commitChange( - this.getResources() - .getString( - R.string.git_commit_edit_text, - data.getExtras().getString("LONG_NAME"))); - refreshListAdapter(); - break; - case GitActivity.REQUEST_INIT: - case NEW_REPO_BUTTON: - initializeRepositoryInfo(); - break; - case GitActivity.REQUEST_SYNC: - case GitActivity.REQUEST_PULL: - updateListAdapter(); - break; - case HOME: - checkLocalRepository(); - break; - case CLONE_REPO_BUTTON: - // duplicate code - if (settings.getBoolean("git_external", false) - && settings.getString("git_external_repo", null) != null) { - String externalRepoPath = settings.getString("git_external_repo", null); - File dir = externalRepoPath != null ? new File(externalRepoPath) : null; - - if (dir != null - && dir.exists() - && dir.isDirectory() - && !FileUtils.listFiles(dir, null, true).isEmpty() - && !PasswordRepository.getPasswords( - dir, - PasswordRepository.getRepositoryDirectory(this), - getSortOrder()) - .isEmpty()) { - PasswordRepository.closeRepository(); - checkLocalRepository(); - return; // if not empty, just show me the passwords! - } - } - Intent intent = new Intent(activity, GitActivity.class); - intent.putExtra("Operation", GitActivity.REQUEST_CLONE); - startActivityForResult(intent, GitActivity.REQUEST_CLONE); - break; - case REQUEST_CODE_SELECT_FOLDER: - Log.d( - TAG, - "Moving passwords to " + data.getStringExtra("SELECTED_FOLDER_PATH")); - Log.d(TAG, TextUtils.join(", ", data.getStringArrayListExtra("Files"))); - File target = new File(data.getStringExtra("SELECTED_FOLDER_PATH")); - if (!target.isDirectory()) { - Log.e(TAG, "Tried moving passwords to a non-existing folder."); - break; - } - - String repositoryPath = - PasswordRepository.getRepositoryDirectory(getApplicationContext()) - .getAbsolutePath(); - - // TODO move this to an async task - for (String fileString : data.getStringArrayListExtra("Files")) { - File source = new File(fileString); - if (!source.exists()) { - Log.e(TAG, "Tried moving something that appears non-existent."); - continue; - } - - File destinationFile = - new File(target.getAbsolutePath() + "/" + source.getName()); - - String basename = FilenameUtils.getBaseName(source.getAbsolutePath()); - - String sourceLongName = - PgpActivity.getLongName( - source.getParent(), repositoryPath, basename); - - String destinationLongName = - PgpActivity.getLongName( - target.getAbsolutePath(), repositoryPath, basename); - - if (destinationFile.exists()) { - Log.e(TAG, "Trying to move a file that already exists."); - // TODO: Add option to cancel overwrite. Will be easier once this is an - // async task. - new MaterialAlertDialogBuilder(this) - .setTitle( - getResources() - .getString(R.string.password_exists_title)) - .setMessage( - getResources() - .getString( - R.string.password_exists_message, - destinationLongName, - sourceLongName)) - .setPositiveButton("Okay", null) - .show(); - } - - if (!source.renameTo(destinationFile)) { - // TODO this should show a warning to the user - Log.e(TAG, "Something went wrong while moving."); - } else { - commitChange( - this.getResources() - .getString( - R.string.git_commit_move_text, - sourceLongName, - destinationLongName)); - } - } - updateListAdapter(); - if (plist != null) { - plist.dismissActionMode(); - } - break; - } - } - super.onActivityResult(requestCode, resultCode, data); - } - - private void initRepository(final int operation) { - PasswordRepository.closeRepository(); - - new MaterialAlertDialogBuilder(this) - .setTitle(this.getResources().getString(R.string.location_dialog_title)) - .setMessage(this.getResources().getString(R.string.location_dialog_text)) - .setPositiveButton( - this.getResources().getString(R.string.location_hidden), - (dialog, whichButton) -> { - settings.edit().putBoolean("git_external", false).apply(); - - switch (operation) { - case NEW_REPO_BUTTON: - initializeRepositoryInfo(); - break; - case CLONE_REPO_BUTTON: - PasswordRepository.initialize(PasswordStore.this); - - Intent intent = new Intent(activity, GitActivity.class); - intent.putExtra("Operation", GitActivity.REQUEST_CLONE); - startActivityForResult(intent, GitActivity.REQUEST_CLONE); - break; - } - }) - .setNegativeButton( - this.getResources().getString(R.string.location_sdcard), - (dialog, whichButton) -> { - settings.edit().putBoolean("git_external", true).apply(); - - String externalRepo = settings.getString("git_external_repo", null); - - if (externalRepo == null) { - Intent intent = new Intent(activity, UserPreference.class); - intent.putExtra("operation", "git_external"); - startActivityForResult(intent, operation); - } else { - new MaterialAlertDialogBuilder(activity) - .setTitle( - getResources() - .getString( - R.string.directory_selected_title)) - .setMessage( - getResources() - .getString( - R.string.directory_selected_message, - externalRepo)) - .setPositiveButton( - getResources().getString(R.string.use), - (dialog1, which) -> { - switch (operation) { - case NEW_REPO_BUTTON: - initializeRepositoryInfo(); - break; - case CLONE_REPO_BUTTON: - PasswordRepository.initialize( - PasswordStore.this); - - Intent intent = - new Intent( - activity, - GitActivity.class); - intent.putExtra( - "Operation", - GitActivity.REQUEST_CLONE); - startActivityForResult( - intent, - GitActivity.REQUEST_CLONE); - break; - } - }) - .setNegativeButton( - getResources().getString(R.string.change), - (dialog12, which) -> { - Intent intent = - new Intent( - activity, UserPreference.class); - intent.putExtra("operation", "git_external"); - startActivityForResult(intent, operation); - }) - .show(); - } - }) - .show(); - } - - public void matchPasswordWithApp(PasswordItem item) { - String path = - item.getFile() - .getAbsolutePath() - .replace( - PasswordRepository.getRepositoryDirectory(getApplicationContext()) - + "/", - "") - .replace(".gpg", ""); - Intent data = new Intent(); - data.putExtra("path", path); - setResult(RESULT_OK, data); - finish(); - } - - private PasswordRepository.PasswordSortOrder getSortOrder() { - return PasswordRepository.PasswordSortOrder.getSortOrder(settings); - } -} diff --git a/app/src/main/java/com/zeapo/pwdstore/PasswordStore.kt b/app/src/main/java/com/zeapo/pwdstore/PasswordStore.kt new file mode 100644 index 00000000..2cf41318 --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/PasswordStore.kt @@ -0,0 +1,731 @@ +/* + * Copyright © 2014-2019 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ +package com.zeapo.pwdstore + +import android.Manifest +import android.annotation.SuppressLint +import android.app.Activity +import android.content.Intent +import android.content.SharedPreferences +import android.content.pm.PackageManager +import android.content.pm.ShortcutInfo.Builder +import android.content.pm.ShortcutManager +import android.graphics.Color +import android.graphics.drawable.Icon +import android.os.Build +import android.os.Bundle +import android.text.TextUtils +import android.util.Log +import android.view.KeyEvent +import android.view.Menu +import android.view.MenuItem +import android.view.MenuItem.OnActionExpandListener +import android.view.View +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.widget.AppCompatTextView +import androidx.appcompat.widget.SearchView +import androidx.appcompat.widget.SearchView.OnQueryTextListener +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat +import androidx.fragment.app.FragmentManager +import androidx.preference.PreferenceManager +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.snackbar.Snackbar +import com.zeapo.pwdstore.crypto.PgpActivity +import com.zeapo.pwdstore.crypto.PgpActivity.Companion.getLongName +import com.zeapo.pwdstore.git.GitActivity +import com.zeapo.pwdstore.git.GitAsyncTask +import com.zeapo.pwdstore.git.GitOperation +import com.zeapo.pwdstore.ui.adapters.PasswordRecyclerAdapter +import com.zeapo.pwdstore.utils.PasswordItem +import com.zeapo.pwdstore.utils.PasswordRepository +import com.zeapo.pwdstore.utils.PasswordRepository.Companion.closeRepository +import com.zeapo.pwdstore.utils.PasswordRepository.Companion.createRepository +import com.zeapo.pwdstore.utils.PasswordRepository.Companion.getPasswords +import com.zeapo.pwdstore.utils.PasswordRepository.Companion.getRepository +import com.zeapo.pwdstore.utils.PasswordRepository.Companion.getRepositoryDirectory +import com.zeapo.pwdstore.utils.PasswordRepository.Companion.initialize +import com.zeapo.pwdstore.utils.PasswordRepository.Companion.isInitialized +import com.zeapo.pwdstore.utils.PasswordRepository.PasswordSortOrder.Companion.getSortOrder +import java.io.File +import java.lang.Character.UnicodeBlock +import org.apache.commons.io.FileUtils +import org.apache.commons.io.FilenameUtils +import org.eclipse.jgit.api.Git +import org.eclipse.jgit.api.errors.GitAPIException +import org.eclipse.jgit.revwalk.RevCommit + +class PasswordStore : AppCompatActivity() { + + private lateinit var activity: PasswordStore + private lateinit var searchItem: MenuItem + private lateinit var searchView: SearchView + private lateinit var settings: SharedPreferences + private var plist: PasswordFragment? = null + private var shortcutManager: ShortcutManager? = null + + override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { + // open search view on search key, or Ctr+F + if ((keyCode == KeyEvent.KEYCODE_SEARCH || keyCode == KeyEvent.KEYCODE_F && event.isCtrlPressed) && + !searchItem.isActionViewExpanded) { + searchItem.expandActionView() + return true + } + + // open search view on any printable character and query for it + val c = event.unicodeChar.toChar() + val printable = isPrintable(c) + if (printable && !searchItem.isActionViewExpanded) { + searchItem.expandActionView() + searchView.setQuery(c.toString(), true) + return true + } + return super.onKeyDown(keyCode, event) + } + + @SuppressLint("NewApi") + override fun onCreate(savedInstanceState: Bundle?) { + activity = this + settings = PreferenceManager.getDefaultSharedPreferences(this.applicationContext) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { + shortcutManager = getSystemService(ShortcutManager::class.java) + } + + // If user opens app with permission granted then revokes and returns, + // prevent attempt to create password list fragment + var savedInstance = savedInstanceState + if (savedInstanceState != null && (!settings.getBoolean("git_external", false) || + ContextCompat.checkSelfPermission( + activity, Manifest.permission.READ_EXTERNAL_STORAGE) + != PackageManager.PERMISSION_GRANTED)) { + savedInstance = null + } + super.onCreate(savedInstance) + setContentView(R.layout.activity_pwdstore) + } + + public override fun onResume() { + super.onResume() + // do not attempt to checkLocalRepository() if no storage permission: immediate crash + if (settings.getBoolean("git_external", false)) { + if (ContextCompat.checkSelfPermission(activity, Manifest.permission.READ_EXTERNAL_STORAGE) + != PackageManager.PERMISSION_GRANTED) { + if (ActivityCompat.shouldShowRequestPermissionRationale(activity, Manifest.permission.READ_EXTERNAL_STORAGE)) { + val snack = Snackbar.make( + findViewById(R.id.main_layout), + getString(R.string.access_sdcard_text), + Snackbar.LENGTH_INDEFINITE) + .setAction(R.string.dialog_ok) { + ActivityCompat.requestPermissions( + activity, + arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), + REQUEST_EXTERNAL_STORAGE) + } + snack.show() + val view = snack.view + val tv: AppCompatTextView = view.findViewById(com.google.android.material.R.id.snackbar_text) + tv.setTextColor(Color.WHITE) + tv.maxLines = 10 + } else { + // No explanation needed, we can request the permission. + ActivityCompat.requestPermissions( + activity, + arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), + REQUEST_EXTERNAL_STORAGE) + } + } else { + checkLocalRepository() + } + } else { + checkLocalRepository() + } + } + + override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) { + // If request is cancelled, the result arrays are empty. + if (requestCode == REQUEST_EXTERNAL_STORAGE) { + if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + checkLocalRepository() + } + } + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + // Inflate the menu; this adds items to the action bar if it is present. + menuInflater.inflate(R.menu.main_menu, menu) + searchItem = menu.findItem(R.id.action_search) + searchView = searchItem.actionView as SearchView + searchView.setOnQueryTextListener( + object : OnQueryTextListener { + override fun onQueryTextSubmit(s: String): Boolean { + return true + } + + override fun onQueryTextChange(s: String): Boolean { + filterListAdapter(s) + return true + } + }) + + // When using the support library, the setOnActionExpandListener() method is + // static and accepts the MenuItem object as an argument + searchItem.setOnActionExpandListener( + object : OnActionExpandListener { + override fun onMenuItemActionCollapse(item: MenuItem): Boolean { + refreshListAdapter() + return true + } + + override fun onMenuItemActionExpand(item: MenuItem): Boolean { + return true + } + }) + return super.onCreateOptionsMenu(menu) + } + + // Handle action bar item clicks here. The action bar will + // automatically handle clicks on the Home/Up button, so long + // as you specify a parent activity in AndroidManifest.xml. + override fun onOptionsItemSelected(item: MenuItem): Boolean { + val id = item.itemId + val intent: Intent + val initBefore = MaterialAlertDialogBuilder(this) + .setMessage(this.resources.getString(R.string.creation_dialog_text)) + .setPositiveButton(this.resources.getString(R.string.dialog_ok), null) + when (id) { + R.id.user_pref -> { + try { + intent = Intent(this, UserPreference::class.java) + startActivity(intent) + } catch (e: Exception) { + e.printStackTrace() + } + return true + } + R.id.git_push -> { + if (!isInitialized) { + initBefore.show() + return false + } + intent = Intent(this, GitActivity::class.java) + intent.putExtra("Operation", GitActivity.REQUEST_PUSH) + startActivityForResult(intent, GitActivity.REQUEST_PUSH) + return true + } + R.id.git_pull -> { + if (!isInitialized) { + initBefore.show() + return false + } + intent = Intent(this, GitActivity::class.java) + intent.putExtra("Operation", GitActivity.REQUEST_PULL) + startActivityForResult(intent, GitActivity.REQUEST_PULL) + return true + } + R.id.git_sync -> { + if (!isInitialized) { + initBefore.show() + return false + } + intent = Intent(this, GitActivity::class.java) + intent.putExtra("Operation", GitActivity.REQUEST_SYNC) + startActivityForResult(intent, GitActivity.REQUEST_SYNC) + return true + } + R.id.refresh -> { + updateListAdapter() + return true + } + android.R.id.home -> onBackPressed() + else -> { + } + } + return super.onOptionsItemSelected(item) + } + + fun openSettings(view: View?) { + val intent: Intent + try { + intent = Intent(this, UserPreference::class.java) + startActivity(intent) + } catch (e: Exception) { + e.printStackTrace() + } + } + + fun cloneExistingRepository(view: View?) { + initRepository(CLONE_REPO_BUTTON) + } + + fun createNewRepository(view: View?) { + initRepository(NEW_REPO_BUTTON) + } + + private fun createRepository() { + if (!isInitialized) { + initialize(this) + } + val localDir = getRepositoryDirectory(applicationContext) + try { + check(localDir.mkdir()) { "Failed to create directory!" } + createRepository(localDir) + if (File(localDir.absolutePath + "/.gpg-id").createNewFile()) { + settings.edit().putBoolean("repository_initialized", true).apply() + } else { + throw IllegalStateException("Failed to initialize repository state.") + } + } catch (e: Exception) { + e.printStackTrace() + if (!localDir.delete()) { + Log.d(TAG, "Failed to delete local repository") + } + return + } + checkLocalRepository() + } + + private fun initializeRepositoryInfo() { + val externalRepoPath = settings.getString("git_external_repo", null) + if (settings.getBoolean("git_external", false) && externalRepoPath != null) { + val dir = File(externalRepoPath) + if (dir.exists() && dir.isDirectory && + getPasswords(dir, getRepositoryDirectory(this), sortOrder).isNotEmpty()) { + closeRepository() + checkLocalRepository() + return // if not empty, just show me the passwords! + } + } + val keyIds = settings.getStringSet("openpgp_key_ids_set", HashSet()) + if (keyIds != null && keyIds.isEmpty()) { + MaterialAlertDialogBuilder(this) + .setMessage(this.resources.getString(R.string.key_dialog_text)) + .setPositiveButton(this.resources.getString(R.string.dialog_positive)) { _, _ -> + val intent = Intent(activity, UserPreference::class.java) + startActivityForResult(intent, GitActivity.REQUEST_INIT) + } + .setNegativeButton(this.resources.getString(R.string.dialog_negative), null) + .show() + } + createRepository() + } + + private fun checkLocalRepository() { + val repo = initialize(this) + if (repo == null) { + val intent = Intent(activity, UserPreference::class.java) + intent.putExtra("operation", "git_external") + startActivityForResult(intent, HOME) + } else { + checkLocalRepository(getRepositoryDirectory(applicationContext)) + } + } + + private fun checkLocalRepository(localDir: File?) { + val fragmentManager = supportFragmentManager + val fragmentTransaction = fragmentManager.beginTransaction() + if (localDir != null && settings.getBoolean("repository_initialized", false)) { + Log.d(TAG, "Check, dir: " + localDir.absolutePath) + // do not push the fragment if we already have it + if (fragmentManager.findFragmentByTag("PasswordsList") == null || + settings.getBoolean("repo_changed", false)) { + settings.edit().putBoolean("repo_changed", false).apply() + plist = PasswordFragment() + val args = Bundle() + args.putString("Path", getRepositoryDirectory(applicationContext).absolutePath) + + // if the activity was started from the autofill settings, the + // intent is to match a clicked pwd with app. pass this to fragment + if (intent.getBooleanExtra("matchWith", false)) { + args.putBoolean("matchWith", true) + } + plist!!.arguments = args + supportActionBar!!.show() + supportActionBar!!.setDisplayHomeAsUpEnabled(false) + fragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE) + fragmentTransaction.replace(R.id.main_layout, plist!!, "PasswordsList") + fragmentTransaction.commit() + } + } else { + supportActionBar!!.hide() + fragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE) + val cloneFrag = ToCloneOrNot() + fragmentTransaction.replace(R.id.main_layout, cloneFrag, "ToCloneOrNot") + fragmentTransaction.commit() + } + } + + override fun onBackPressed() { + if (null != plist && plist!!.isNotEmpty) { + plist!!.popBack() + } else { + super.onBackPressed() + } + if (null != plist && !plist!!.isNotEmpty) { + supportActionBar!!.setDisplayHomeAsUpEnabled(false) + } + } + + private fun getRelativePath(fullPath: String, repositoryPath: String): String { + return fullPath.replace(repositoryPath, "").replace("/+".toRegex(), "/") + } + + private fun getLastChangedTimestamp(fullPath: String): Long { + val repoPath = getRepositoryDirectory(this) + val repository = getRepository(repoPath) + if (repository == null) { + Log.d(TAG, "getLastChangedTimestamp: No git repository") + return File(fullPath).lastModified() + } + val git = Git(repository) + val relativePath = getRelativePath(fullPath, repoPath.absolutePath).substring(1) // Removes leading '/' + val iterator: Iterator<RevCommit> + iterator = try { + git.log().addPath(relativePath).call().iterator() + } catch (e: GitAPIException) { + Log.e(TAG, "getLastChangedTimestamp: GITAPIException", e) + return -1 + } + if (!iterator.hasNext()) { + Log.w(TAG, "getLastChangedTimestamp: No commits for file: $relativePath") + return -1 + } + return iterator.next().commitTime.toLong() * 1000 + } + + fun decryptPassword(item: PasswordItem) { + val decryptIntent = Intent(this, PgpActivity::class.java) + val authDecryptIntent = Intent(this, LaunchActivity::class.java) + for (intent in arrayOf(decryptIntent, authDecryptIntent)) { + intent.putExtra("NAME", item.toString()) + intent.putExtra("FILE_PATH", item.file.absolutePath) + intent.putExtra("REPO_PATH", getRepositoryDirectory(applicationContext).absolutePath) + intent.putExtra("LAST_CHANGED_TIMESTAMP", getLastChangedTimestamp(item.file.absolutePath)) + intent.putExtra("OPERATION", "DECRYPT") + } + + // Adds shortcut + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { + val shortcut = Builder(this, item.fullPathToParent) + .setShortLabel(item.toString()) + .setLongLabel(item.fullPathToParent + item.toString()) + .setIcon(Icon.createWithResource(this, R.mipmap.ic_launcher)) + .setIntent(authDecryptIntent.setAction("DECRYPT_PASS")) // Needs action + .build() + val shortcuts = shortcutManager!!.dynamicShortcuts + if (shortcuts.size >= shortcutManager!!.maxShortcutCountPerActivity && shortcuts.size > 0) { + shortcuts.removeAt(shortcuts.size - 1) + shortcuts.add(0, shortcut) + shortcutManager!!.dynamicShortcuts = shortcuts + } else { + shortcutManager!!.addDynamicShortcuts(listOf(shortcut)) + } + } + startActivityForResult(decryptIntent, REQUEST_CODE_DECRYPT_AND_VERIFY) + } + + fun editPassword(item: PasswordItem) { + val intent = Intent(this, PgpActivity::class.java) + intent.putExtra("NAME", item.toString()) + intent.putExtra("FILE_PATH", item.file.absolutePath) + intent.putExtra("PARENT_PATH", currentDir!!.absolutePath) + intent.putExtra("REPO_PATH", getRepositoryDirectory(applicationContext).absolutePath) + intent.putExtra("OPERATION", "EDIT") + startActivityForResult(intent, REQUEST_CODE_EDIT) + } + + fun createPassword() { + if (!isInitialized) { + MaterialAlertDialogBuilder(this) + .setMessage(this.resources.getString(R.string.creation_dialog_text)) + .setPositiveButton(this.resources.getString(R.string.dialog_ok), null) + .show() + return + } + if (settings.getStringSet("openpgp_key_ids_set", HashSet()).isNullOrEmpty()) { + MaterialAlertDialogBuilder(this) + .setTitle(this.resources.getString(R.string.no_key_selected_dialog_title)) + .setMessage(this.resources.getString(R.string.no_key_selected_dialog_text)) + .setPositiveButton(this.resources.getString(R.string.dialog_ok)) { _, _ -> + val intent = Intent(activity, UserPreference::class.java) + startActivity(intent) + } + .show() + return + } + val currentDir = currentDir + Log.i(TAG, "Adding file to : " + currentDir!!.absolutePath) + val intent = Intent(this, PgpActivity::class.java) + intent.putExtra("FILE_PATH", currentDir.absolutePath) + intent.putExtra("REPO_PATH", getRepositoryDirectory(applicationContext).absolutePath) + intent.putExtra("OPERATION", "ENCRYPT") + startActivityForResult(intent, REQUEST_CODE_ENCRYPT) + } + + // deletes passwords in order from top to bottom + fun deletePasswords(adapter: PasswordRecyclerAdapter, selectedItems: MutableSet<Int>) { + val it: MutableIterator<*> = selectedItems.iterator() + if (!it.hasNext()) { + return + } + val position = it.next() as Int + val item = adapter.values[position] + MaterialAlertDialogBuilder(this) + .setMessage(resources.getString(R.string.delete_dialog_text, item.longName)) + .setPositiveButton(resources.getString(R.string.dialog_yes)) { _, _ -> + item.file.delete() + adapter.remove(position) + it.remove() + adapter.updateSelectedItems(position, selectedItems) + commitChange(resources.getString(R.string.git_commit_remove_text, item.longName)) + deletePasswords(adapter, selectedItems) + } + .setNegativeButton(this.resources.getString(R.string.dialog_no)) { _, _ -> + it.remove() + deletePasswords(adapter, selectedItems) + } + .show() + } + + fun movePasswords(values: ArrayList<PasswordItem>) { + val intent = Intent(this, SelectFolderActivity::class.java) + val fileLocations = ArrayList<String>() + for ((_, _, _, file) in values) { + fileLocations.add(file.absolutePath) + } + intent.putExtra("Files", fileLocations) + intent.putExtra("Operation", "SELECTFOLDER") + startActivityForResult(intent, REQUEST_CODE_SELECT_FOLDER) + } + + /** clears adapter's content and updates it with a fresh list of passwords from the root */ + fun updateListAdapter() { + plist?.updateAdapter() + } + + /** Updates the adapter with the current view of passwords */ + private fun refreshListAdapter() { + plist?.refreshAdapter() + } + + private fun filterListAdapter(filter: String) { + plist?.filterAdapter(filter) + } + + private val currentDir: File? + get() = plist?.currentDir ?: getRepositoryDirectory(applicationContext) + + private fun commitChange(message: String) { + object : GitOperation(getRepositoryDirectory(activity), activity) { + override fun execute() { + Log.d(TAG, "Committing with message $message") + val git = Git(repository) + val tasks = GitAsyncTask(activity, false, true, this) + tasks.execute(git.add().addFilepattern("."), git.commit().setAll(true).setMessage(message)) + } + }.execute() + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + if (resultCode == Activity.RESULT_OK) { + when (requestCode) { + // if we get here with a RESULT_OK then it's probably OK :) + GitActivity.REQUEST_CLONE -> settings.edit().putBoolean("repository_initialized", true).apply() + // if went from decrypt->edit and user saved changes or HOTP counter was + // incremented, we need to commitChange + REQUEST_CODE_DECRYPT_AND_VERIFY -> { + if (data != null && data.getBooleanExtra("needCommit", false)) { + if (data.getStringExtra("OPERATION") == "EDIT") { + commitChange(this.resources + .getString( + R.string.git_commit_edit_text, + data.extras!!.getString("LONG_NAME"))) + } else { + commitChange(this.resources + .getString( + R.string.git_commit_increment_text, + data.extras!!.getString("LONG_NAME"))) + } + } + refreshListAdapter() + } + REQUEST_CODE_ENCRYPT -> { + commitChange(this.resources + .getString( + R.string.git_commit_add_text, + data!!.extras!!.getString("LONG_NAME"))) + refreshListAdapter() + } + REQUEST_CODE_EDIT -> { + commitChange( + this.resources + .getString( + R.string.git_commit_edit_text, + data!!.extras!!.getString("LONG_NAME"))) + refreshListAdapter() + } + GitActivity.REQUEST_INIT, NEW_REPO_BUTTON -> initializeRepositoryInfo() + GitActivity.REQUEST_SYNC, GitActivity.REQUEST_PULL -> updateListAdapter() + HOME -> checkLocalRepository() + // duplicate code + CLONE_REPO_BUTTON -> { + if (settings.getBoolean("git_external", false) && + settings.getString("git_external_repo", null) != null) { + val externalRepoPath = settings.getString("git_external_repo", null) + val dir = externalRepoPath?.let { File(it) } + if (dir != null && + dir.exists() && + dir.isDirectory && + !FileUtils.listFiles(dir, null, true).isEmpty() && + getPasswords(dir, getRepositoryDirectory(this), sortOrder).isNotEmpty()) { + closeRepository() + checkLocalRepository() + return // if not empty, just show me the passwords! + } + } + val intent = Intent(activity, GitActivity::class.java) + intent.putExtra("Operation", GitActivity.REQUEST_CLONE) + startActivityForResult(intent, GitActivity.REQUEST_CLONE) + } + REQUEST_CODE_SELECT_FOLDER -> { + Log.d(TAG, "Moving passwords to " + data!!.getStringExtra("SELECTED_FOLDER_PATH")) + Log.d(TAG, TextUtils.join(", ", requireNotNull(data.getStringArrayListExtra("Files")))) + + val target = File(requireNotNull(data.getStringExtra("SELECTED_FOLDER_PATH"))) + val repositoryPath = getRepositoryDirectory(applicationContext).absolutePath + if (!target.isDirectory) { + Log.e(TAG, "Tried moving passwords to a non-existing folder.") + return + } + + // TODO move this to an async task + for (fileString in requireNotNull(data.getStringArrayListExtra("Files"))) { + val source = File(fileString) + if (!source.exists()) { + Log.e(TAG, "Tried moving something that appears non-existent.") + continue + } + val destinationFile = File(target.absolutePath + "/" + source.name) + val basename = FilenameUtils.getBaseName(source.absolutePath) + val sourceLongName = getLongName(requireNotNull(source.parent), repositoryPath, basename) + val destinationLongName = getLongName(target.absolutePath, repositoryPath, basename) + if (destinationFile.exists()) { + Log.e(TAG, "Trying to move a file that already exists.") + // TODO: Add option to cancel overwrite. Will be easier once this is an async task. + MaterialAlertDialogBuilder(this) + .setTitle(resources.getString(R.string.password_exists_title)) + .setMessage(resources + .getString( + R.string.password_exists_message, + destinationLongName, + sourceLongName)) + .setPositiveButton("Okay", null) + .show() + } + if (!source.renameTo(destinationFile)) { + // TODO this should show a warning to the user + Log.e(TAG, "Something went wrong while moving.") + } else { + commitChange(this.resources + .getString( + R.string.git_commit_move_text, + sourceLongName, + destinationLongName)) + } + } + updateListAdapter() + if (plist != null) { + plist!!.dismissActionMode() + } + } + } + } + super.onActivityResult(requestCode, resultCode, data) + } + + private fun initRepository(operation: Int) { + closeRepository() + MaterialAlertDialogBuilder(this) + .setTitle(this.resources.getString(R.string.location_dialog_title)) + .setMessage(this.resources.getString(R.string.location_dialog_text)) + .setPositiveButton(this.resources.getString(R.string.location_hidden)) { _, _ -> + settings.edit().putBoolean("git_external", false).apply() + when (operation) { + NEW_REPO_BUTTON -> initializeRepositoryInfo() + CLONE_REPO_BUTTON -> { + initialize(this@PasswordStore) + val intent = Intent(activity, GitActivity::class.java) + intent.putExtra("Operation", GitActivity.REQUEST_CLONE) + startActivityForResult(intent, GitActivity.REQUEST_CLONE) + } + } + } + .setNegativeButton(this.resources.getString(R.string.location_sdcard)) { _, _ -> + settings.edit().putBoolean("git_external", true).apply() + val externalRepo = settings.getString("git_external_repo", null) + if (externalRepo == null) { + val intent = Intent(activity, UserPreference::class.java) + intent.putExtra("operation", "git_external") + startActivityForResult(intent, operation) + } else { + MaterialAlertDialogBuilder(activity) + .setTitle(resources.getString(R.string.directory_selected_title)) + .setMessage(resources.getString(R.string.directory_selected_message, externalRepo)) + .setPositiveButton(resources.getString(R.string.use)) { _, _ -> + when (operation) { + NEW_REPO_BUTTON -> initializeRepositoryInfo() + CLONE_REPO_BUTTON -> { + initialize(this@PasswordStore) + val intent = Intent(activity, GitActivity::class.java) + intent.putExtra("Operation", GitActivity.REQUEST_CLONE) + startActivityForResult(intent, GitActivity.REQUEST_CLONE) + } + } + } + .setNegativeButton(resources.getString(R.string.change)) { _, _ -> + val intent = Intent(activity, UserPreference::class.java) + intent.putExtra("operation", "git_external") + startActivityForResult(intent, operation) + } + .show() + } + } + .show() + } + + fun matchPasswordWithApp(item: PasswordItem) { + val path = item.file + .absolutePath + .replace(getRepositoryDirectory(applicationContext).toString() + "/", "") + .replace(".gpg", "") + val data = Intent() + data.putExtra("path", path) + setResult(Activity.RESULT_OK, data) + finish() + } + + private val sortOrder: PasswordRepository.PasswordSortOrder + get() = getSortOrder(settings) + + companion object { + const val REQUEST_CODE_SIGN = 9910 + const val REQUEST_CODE_ENCRYPT = 9911 + const val REQUEST_CODE_SIGN_AND_ENCRYPT = 9912 + const val REQUEST_CODE_DECRYPT_AND_VERIFY = 9913 + const val REQUEST_CODE_GET_KEY = 9914 + const val REQUEST_CODE_GET_KEY_IDS = 9915 + const val REQUEST_CODE_EDIT = 9916 + const val REQUEST_CODE_SELECT_FOLDER = 9917 + private val TAG = PasswordStore::class.java.name + private const val CLONE_REPO_BUTTON = 401 + private const val NEW_REPO_BUTTON = 402 + private const val HOME = 403 + private const val REQUEST_EXTERNAL_STORAGE = 50 + private fun isPrintable(c: Char): Boolean { + val block = UnicodeBlock.of(c) + return (!Character.isISOControl(c) && + block != null && block !== UnicodeBlock.SPECIALS) + } + } +} diff --git a/app/src/main/java/com/zeapo/pwdstore/SshKeyGen.java b/app/src/main/java/com/zeapo/pwdstore/SshKeyGen.java deleted file mode 100644 index 1bbe5235..00000000 --- a/app/src/main/java/com/zeapo/pwdstore/SshKeyGen.java +++ /dev/null @@ -1,264 +0,0 @@ -/* - * Copyright © 2014-2019 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ -package com.zeapo.pwdstore; - -import android.annotation.SuppressLint; -import android.app.Dialog; -import android.app.ProgressDialog; -import android.content.ClipData; -import android.content.ClipboardManager; -import android.content.Context; -import android.content.SharedPreferences; -import android.graphics.Typeface; -import android.os.AsyncTask; -import android.os.Bundle; -import android.text.InputType; -import android.view.LayoutInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.view.inputmethod.InputMethodManager; -import android.widget.ArrayAdapter; -import android.widget.Button; -import android.widget.CheckBox; -import android.widget.EditText; -import android.widget.Spinner; -import android.widget.Toast; -import androidx.annotation.NonNull; -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.app.AppCompatActivity; -import androidx.appcompat.widget.AppCompatTextView; -import androidx.fragment.app.DialogFragment; -import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentActivity; -import androidx.preference.PreferenceManager; -import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import com.google.android.material.textfield.TextInputEditText; -import com.jcraft.jsch.JSch; -import com.jcraft.jsch.KeyPair; -import java.io.File; -import java.io.FileOutputStream; -import java.lang.ref.WeakReference; -import java.nio.charset.StandardCharsets; -import org.apache.commons.io.FileUtils; - -public class SshKeyGen extends AppCompatActivity { - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - if (getSupportActionBar() != null) getSupportActionBar().setDisplayHomeAsUpEnabled(true); - - setTitle("Generate SSH Key"); - - if (savedInstanceState == null) { - getSupportFragmentManager() - .beginTransaction() - .replace(android.R.id.content, new SshKeyGenFragment()) - .commit(); - } - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - if (item.getItemId() == android.R.id.home) { - // The back arrow in the action bar should act the same as the back button. - onBackPressed(); - return true; - } - return super.onOptionsItemSelected(item); - } - - // Invoked when 'Generate' button of SshKeyGenFragment clicked. Generates a - // private and public key, then replaces the SshKeyGenFragment with a - // ShowSshKeyFragment which displays the public key. - public void generate(View view) { - String length = - Integer.toString((Integer) ((Spinner) findViewById(R.id.length)).getSelectedItem()); - String passphrase = ((EditText) findViewById(R.id.passphrase)).getText().toString(); - String comment = ((EditText) findViewById(R.id.comment)).getText().toString(); - new KeyGenerateTask(this).execute(length, passphrase, comment); - - InputMethodManager imm = - (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); - imm.hideSoftInputFromWindow(view.getWindowToken(), 0); - } - - // SSH key generation UI - public static class SshKeyGenFragment extends Fragment { - public SshKeyGenFragment() {} - - @Override - public View onCreateView( - LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - final View v = inflater.inflate(R.layout.fragment_ssh_keygen, container, false); - Typeface monoTypeface = - Typeface.createFromAsset( - requireContext().getAssets(), "fonts/sourcecodepro.ttf"); - - Spinner spinner = v.findViewById(R.id.length); - Integer[] lengths = new Integer[] {2048, 4096}; - ArrayAdapter<Integer> adapter = - new ArrayAdapter<>( - requireContext(), - android.R.layout.simple_spinner_dropdown_item, - lengths); - spinner.setAdapter(adapter); - - ((TextInputEditText) v.findViewById(R.id.passphrase)).setTypeface(monoTypeface); - - final CheckBox checkbox = v.findViewById(R.id.show_passphrase); - checkbox.setOnCheckedChangeListener( - (buttonView, isChecked) -> { - final TextInputEditText editText = v.findViewById(R.id.passphrase); - final int selection = editText.getSelectionEnd(); - if (isChecked) { - editText.setInputType( - InputType.TYPE_CLASS_TEXT - | InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD); - } else { - editText.setInputType( - InputType.TYPE_CLASS_TEXT - | InputType.TYPE_TEXT_VARIATION_PASSWORD); - } - editText.setSelection(selection); - }); - - return v; - } - } - - // Displays the generated public key .ssh_key.pub - public static class ShowSshKeyFragment extends DialogFragment { - public ShowSshKeyFragment() {} - - @NonNull - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - final FragmentActivity activity = requireActivity(); - final MaterialAlertDialogBuilder builder = - new MaterialAlertDialogBuilder(requireContext()); - LayoutInflater inflater = activity.getLayoutInflater(); - @SuppressLint("InflateParams") - final View v = inflater.inflate(R.layout.fragment_show_ssh_key, null); - builder.setView(v); - - AppCompatTextView textView = v.findViewById(R.id.public_key); - File file = new File(activity.getFilesDir() + "/.ssh_key.pub"); - try { - textView.setText(FileUtils.readFileToString(file, StandardCharsets.UTF_8)); - } catch (Exception e) { - System.out.println("Exception caught :("); - e.printStackTrace(); - } - - builder.setPositiveButton( - getResources().getString(R.string.dialog_ok), - (dialog, which) -> { - if (activity instanceof SshKeyGen) activity.finish(); - }); - - builder.setNegativeButton( - getResources().getString(R.string.dialog_cancel), (dialog, which) -> {}); - - builder.setNeutralButton(getResources().getString(R.string.ssh_keygen_copy), null); - - final AlertDialog ad = builder.setTitle("Your public key").create(); - ad.setOnShowListener( - dialog -> { - Button b = ad.getButton(AlertDialog.BUTTON_NEUTRAL); - b.setOnClickListener( - v1 -> { - AppCompatTextView textView1 = - getDialog().findViewById(R.id.public_key); - ClipboardManager clipboard = - (ClipboardManager) - activity.getSystemService( - Context.CLIPBOARD_SERVICE); - ClipData clip = - ClipData.newPlainText( - "public key", textView1.getText().toString()); - clipboard.setPrimaryClip(clip); - }); - }); - return ad; - } - } - - private static class KeyGenerateTask extends AsyncTask<String, Void, Exception> { - private ProgressDialog pd; - private WeakReference<SshKeyGen> weakReference; - - private KeyGenerateTask(final SshKeyGen activity) { - weakReference = new WeakReference<>(activity); - } - - @Override - protected void onPreExecute() { - super.onPreExecute(); - pd = ProgressDialog.show(weakReference.get(), "", "Generating keys"); - } - - protected Exception doInBackground(String... strings) { - int length = Integer.parseInt(strings[0]); - String passphrase = strings[1]; - String comment = strings[2]; - - JSch jsch = new JSch(); - try { - KeyPair kp = KeyPair.genKeyPair(jsch, KeyPair.RSA, length); - - File file = new File(weakReference.get().getFilesDir() + "/.ssh_key"); - FileOutputStream out = new FileOutputStream(file, false); - if (passphrase.length() > 0) { - kp.writePrivateKey(out, passphrase.getBytes()); - } else { - kp.writePrivateKey(out); - } - - file = new File(weakReference.get().getFilesDir() + "/.ssh_key.pub"); - out = new FileOutputStream(file, false); - kp.writePublicKey(out, comment); - return null; - } catch (Exception e) { - System.out.println("Exception caught :("); - e.printStackTrace(); - return e; - } - } - - @Override - protected void onPostExecute(Exception e) { - super.onPostExecute(e); - pd.dismiss(); - if (e == null) { - Toast.makeText(weakReference.get(), "SSH-key generated", Toast.LENGTH_LONG).show(); - DialogFragment df = new ShowSshKeyFragment(); - df.show(weakReference.get().getSupportFragmentManager(), "public_key"); - SharedPreferences prefs = - PreferenceManager.getDefaultSharedPreferences(weakReference.get()); - SharedPreferences.Editor editor = prefs.edit(); - editor.putBoolean("use_generated_key", true); - editor.apply(); - } else { - new MaterialAlertDialogBuilder(weakReference.get()) - .setTitle("Error while trying to generate the ssh-key") - .setMessage( - weakReference - .get() - .getResources() - .getString(R.string.ssh_key_error_dialog_text) - + e.getMessage()) - .setPositiveButton( - weakReference.get().getResources().getString(R.string.dialog_ok), - (dialogInterface, i) -> { - // pass - }) - .show(); - } - } - } -} diff --git a/app/src/main/java/com/zeapo/pwdstore/UserPreference.kt b/app/src/main/java/com/zeapo/pwdstore/UserPreference.kt index aa4f852c..d2b08132 100644 --- a/app/src/main/java/com/zeapo/pwdstore/UserPreference.kt +++ b/app/src/main/java/com/zeapo/pwdstore/UserPreference.kt @@ -32,6 +32,8 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.zeapo.pwdstore.autofill.AutofillPreferenceActivity import com.zeapo.pwdstore.crypto.PgpActivity import com.zeapo.pwdstore.git.GitActivity +import com.zeapo.pwdstore.sshkeygen.ShowSshKeyFragment +import com.zeapo.pwdstore.sshkeygen.SshKeyGenActivity import com.zeapo.pwdstore.utils.PasswordRepository import com.zeapo.pwdstore.utils.auth.AuthenticationResult import com.zeapo.pwdstore.utils.auth.Authenticator @@ -141,7 +143,7 @@ class UserPreference : AppCompatActivity() { } viewSshKeyPreference?.onPreferenceClickListener = ClickListener { - val df = SshKeyGen.ShowSshKeyFragment() + val df = ShowSshKeyFragment() df.show(requireFragmentManager(), "public_key") true } @@ -377,7 +379,7 @@ class UserPreference : AppCompatActivity() { * Opens a key generator to generate a public/private key pair */ fun makeSshKey(fromPreferences: Boolean) { - val intent = Intent(applicationContext, SshKeyGen::class.java) + val intent = Intent(applicationContext, SshKeyGenActivity::class.java) startActivity(intent) if (!fromPreferences) { setResult(Activity.RESULT_OK) diff --git a/app/src/main/java/com/zeapo/pwdstore/sshkeygen/ShowSshKeyFragment.kt b/app/src/main/java/com/zeapo/pwdstore/sshkeygen/ShowSshKeyFragment.kt new file mode 100644 index 00000000..5f27090f --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/sshkeygen/ShowSshKeyFragment.kt @@ -0,0 +1,69 @@ +/* + * Copyright © 2014-2019 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ +package com.zeapo.pwdstore.sshkeygen + +import android.annotation.SuppressLint +import android.app.Dialog +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.os.Bundle +import android.view.View +import android.widget.TextView +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.DialogFragment +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.zeapo.pwdstore.R +import java.io.File +import java.nio.charset.StandardCharsets +import org.apache.commons.io.FileUtils + +class ShowSshKeyFragment : DialogFragment() { + + private lateinit var activity: SshKeyGenActivity + private lateinit var builder: MaterialAlertDialogBuilder + private lateinit var publicKey: TextView + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + activity = requireActivity() as SshKeyGenActivity + builder = MaterialAlertDialogBuilder(activity) + } + + @SuppressLint("InflateParams") + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val view = activity.layoutInflater.inflate(R.layout.fragment_show_ssh_key, null) + publicKey = view.findViewById(R.id.public_key) + readKeyFromFile() + createMaterialDialog(view) + val ad = builder.create() + ad.setOnShowListener { + val b = ad.getButton(AlertDialog.BUTTON_NEUTRAL) + b.setOnClickListener { + val clipboard = activity.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + val clip = ClipData.newPlainText("public key", publicKey.text.toString()) + clipboard.setPrimaryClip(clip) + } + } + return ad + } + + private fun createMaterialDialog(view: View) { + builder.setView(view) + builder.setTitle(getString(R.string.your_public_key)) + builder.setPositiveButton(getString(R.string.dialog_ok)) { _, _ -> activity.finish() } + builder.setNegativeButton(getString(R.string.dialog_cancel), null) + builder.setNeutralButton(resources.getString(R.string.ssh_keygen_copy), null) + } + + private fun readKeyFromFile() { + val file = File(activity.filesDir.toString() + "/.ssh_key.pub") + try { + publicKey.text = FileUtils.readFileToString(file, StandardCharsets.UTF_8) + } catch (e: Exception) { + e.printStackTrace() + } + } +} diff --git a/app/src/main/java/com/zeapo/pwdstore/sshkeygen/SshKeyGenActivity.kt b/app/src/main/java/com/zeapo/pwdstore/sshkeygen/SshKeyGenActivity.kt new file mode 100644 index 00000000..21d82a3b --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/sshkeygen/SshKeyGenActivity.kt @@ -0,0 +1,35 @@ +/* + * Copyright © 2014-2019 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ +package com.zeapo.pwdstore.sshkeygen + +import android.os.Bundle +import android.view.MenuItem +import androidx.appcompat.app.AppCompatActivity + +class SshKeyGenActivity : AppCompatActivity() { + + public override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + title = "Generate SSH Key" + if (savedInstanceState == null) { + supportFragmentManager + .beginTransaction() + .replace(android.R.id.content, SshKeyGenFragment()) + .commit() + } + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + // The back arrow in the action bar should act the same as the back button. + return when (item.itemId) { + android.R.id.home -> { + onBackPressed() + true + } + else -> super.onOptionsItemSelected(item) + } + } +} diff --git a/app/src/main/java/com/zeapo/pwdstore/sshkeygen/SshKeyGenFragment.kt b/app/src/main/java/com/zeapo/pwdstore/sshkeygen/SshKeyGenFragment.kt new file mode 100644 index 00000000..19011758 --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/sshkeygen/SshKeyGenFragment.kt @@ -0,0 +1,90 @@ +/* + * Copyright © 2014-2019 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ +package com.zeapo.pwdstore.sshkeygen + +import android.os.Bundle +import android.text.InputType +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.inputmethod.InputMethodManager +import android.widget.ArrayAdapter +import android.widget.Button +import android.widget.CheckBox +import android.widget.EditText +import android.widget.Spinner +import androidx.core.content.getSystemService +import androidx.fragment.app.Fragment +import com.google.android.material.textfield.TextInputEditText +import com.zeapo.pwdstore.R + +class SshKeyGenFragment : Fragment() { + + private lateinit var checkBox: CheckBox + private lateinit var comment: EditText + private lateinit var generate: Button + private lateinit var passphrase: TextInputEditText + private lateinit var spinner: Spinner + private lateinit var activity: SshKeyGenActivity + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return inflater.inflate(R.layout.fragment_ssh_keygen, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + activity = requireActivity() as SshKeyGenActivity + findViews(view) + val lengths = arrayOf(2048, 4096) + val adapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_dropdown_item, lengths) + spinner.adapter = adapter + generate.setOnClickListener { generate() } + checkBox.setOnCheckedChangeListener { _, isChecked: Boolean -> + val selection = passphrase.selectionEnd + if (isChecked) { + passphrase.inputType = ( + InputType.TYPE_CLASS_TEXT + or InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD) + } else { + passphrase.inputType = ( + InputType.TYPE_CLASS_TEXT + or InputType.TYPE_TEXT_VARIATION_PASSWORD) + } + passphrase.setSelection(selection) + } + } + + private fun findViews(view: View) { + checkBox = view.findViewById(R.id.show_passphrase) + comment = view.findViewById(R.id.comment) + generate = view.findViewById(R.id.generate) + passphrase = view.findViewById(R.id.passphrase) + spinner = view.findViewById(R.id.length) + } + + // Invoked when 'Generate' button of SshKeyGenFragment clicked. Generates a + // private and public key, then replaces the SshKeyGenFragment with a + // ShowSshKeyFragment which displays the public key. + fun generate() { + val length = (spinner.selectedItem as Int).toString() + val passphrase = passphrase.text.toString() + val comment = comment.text.toString() + KeyGenerateTask(activity).execute(length, passphrase, comment) + hideKeyboard() + } + + private fun hideKeyboard() { + val imm = activity.getSystemService<InputMethodManager>() + var view = activity.currentFocus + if (view == null) { + view = View(activity) + } + imm?.hideSoftInputFromWindow(view.windowToken, 0) + } +} diff --git a/app/src/main/java/com/zeapo/pwdstore/sshkeygen/SshKeygenTask.kt b/app/src/main/java/com/zeapo/pwdstore/sshkeygen/SshKeygenTask.kt new file mode 100644 index 00000000..194d1023 --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/sshkeygen/SshKeygenTask.kt @@ -0,0 +1,77 @@ +/* + * Copyright © 2014-2019 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ +package com.zeapo.pwdstore.sshkeygen + +import android.app.ProgressDialog +import android.os.AsyncTask +import android.widget.Toast +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.DialogFragment +import androidx.preference.PreferenceManager +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.jcraft.jsch.JSch +import com.jcraft.jsch.KeyPair +import com.zeapo.pwdstore.R +import java.io.File +import java.io.FileOutputStream +import java.lang.ref.WeakReference + +class KeyGenerateTask(activity: AppCompatActivity) : AsyncTask<String?, Void?, Exception?>() { + private var pd: ProgressDialog? = null + private val weakReference = WeakReference(activity) + override fun onPreExecute() { + super.onPreExecute() + pd = ProgressDialog.show(weakReference.get(), "", "Generating keys") + } + + override fun doInBackground(vararg strings: String?): Exception? { + val length = strings[0]?.toInt() + val passphrase = strings[1] + val comment = strings[2] + val jsch = JSch() + try { + val kp = length?.let { KeyPair.genKeyPair(jsch, KeyPair.RSA, it) } + var file = File(weakReference.get()!!.filesDir.toString() + "/.ssh_key") + var out = FileOutputStream(file, false) + if (passphrase?.isNotEmpty()!!) { + kp?.writePrivateKey(out, passphrase.toByteArray()) + } else { + kp?.writePrivateKey(out) + } + file = File(weakReference.get()!!.filesDir.toString() + "/.ssh_key.pub") + out = FileOutputStream(file, false) + kp?.writePublicKey(out, comment) + return null + } catch (e: Exception) { + e.printStackTrace() + return e + } + } + + override fun onPostExecute(e: Exception?) { + super.onPostExecute(e) + val activity = weakReference.get() + if (activity is AppCompatActivity) { + pd!!.dismiss() + if (e == null) { + Toast.makeText(activity, "SSH-key generated", Toast.LENGTH_LONG).show() + val df: DialogFragment = ShowSshKeyFragment() + df.show(activity.supportFragmentManager, "public_key") + val prefs = PreferenceManager.getDefaultSharedPreferences(weakReference.get()) + val editor = prefs.edit() + editor.putBoolean("use_generated_key", true) + editor.apply() + } else { + MaterialAlertDialogBuilder(weakReference.get()) + .setTitle(activity.getString(R.string.error_generate_ssh_key)) + .setMessage(activity.getString(R.string.ssh_key_error_dialog_text) + e.message) + .setPositiveButton(activity.getString(R.string.dialog_ok), null) + .show() + } + } else { + // TODO: When activity is destroyed + } + } +} diff --git a/app/src/main/res/font/sourcecodepro.ttf b/app/src/main/res/font/sourcecodepro.ttf Binary files differnew file mode 100644 index 00000000..6eb48e7d --- /dev/null +++ b/app/src/main/res/font/sourcecodepro.ttf diff --git a/app/src/main/res/layout/fragment_ssh_keygen.xml b/app/src/main/res/layout/fragment_ssh_keygen.xml index 6b9e1013..cee878fd 100644 --- a/app/src/main/res/layout/fragment_ssh_keygen.xml +++ b/app/src/main/res/layout/fragment_ssh_keygen.xml @@ -36,6 +36,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:importantForAccessibility="no" + android:fontFamily="@font/sourcecodepro" android:inputType="textPassword" /> </com.google.android.material.textfield.TextInputLayout> @@ -62,12 +63,14 @@ android:inputType="text" /> </com.google.android.material.textfield.TextInputLayout> - <Button + <com.google.android.material.button.MaterialButton + android:id="@+id/generate" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="8dp" - android:onClick="generate" - android:text="@string/ssh_keygen_generate" /> + android:text="@string/ssh_keygen_generate" + android:textColor="@android:color/white" + app:backgroundTint="?attr/colorSecondary" /> </LinearLayout> </ScrollView> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0c8950e6..9ac5a850 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -271,4 +271,7 @@ <string name="biometric_auth_summary">When enabled, Password Store will prompt you for your fingerprint when launching the app</string> <string name="biometric_auth_summary_error">Fingerprint hardware not accessible or missing</string> <string name="ssh_openkeystore_clear_keyid">Clear remembered OpenKeystore SSH Key ID</string> + <string name="access_sdcard_text">The store is on the sdcard but the app does not have permission to access it. Please give permission.</string> + <string name="your_public_key">Your public key</string> + <string name="error_generate_ssh_key">Error while trying to generate the ssh-key</string> </resources> |