aboutsummaryrefslogtreecommitdiff
path: root/app/src/main/java
diff options
context:
space:
mode:
Diffstat (limited to 'app/src/main/java')
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/PasswordStore.java920
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/PasswordStore.kt731
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/SshKeyGen.java264
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/UserPreference.kt6
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/sshkeygen/ShowSshKeyFragment.kt69
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/sshkeygen/SshKeyGenActivity.kt35
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/sshkeygen/SshKeyGenFragment.kt90
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/sshkeygen/SshKeygenTask.kt77
8 files changed, 1006 insertions, 1186 deletions
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
+ }
+ }
+}