Convert java files to kotlin (#570)

* Break SshKeyGen into multiple files

* Use tinted material button

* Convert PasswordStore to kotlin

* Remove SshKeyGen

* Remove explicit imports and other tweaks

Signed-off-by: Aditya Wasan <adityawasan55@gmail.com>
This commit is contained in:
Aditya Wasan 2019-11-13 00:55:56 +05:30 committed by Harsh Shandilya
parent 5749c97d7c
commit 9acad2abf6
12 changed files with 1016 additions and 1192 deletions

View file

@ -42,9 +42,6 @@
<activity <activity
android:name=".UserPreference" android:name=".UserPreference"
android:parentActivityName=".PasswordStore" /> android:parentActivityName=".PasswordStore" />
<activity android:name=".SshKeyGen" />
<service <service
android:name=".autofill.AutofillService" android:name=".autofill.AutofillService"
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"> android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
@ -69,6 +66,7 @@
android:name=".crypto.PgpActivity" android:name=".crypto.PgpActivity"
android:parentActivityName=".PasswordStore" /> android:parentActivityName=".PasswordStore" />
<activity android:name=".SelectFolderActivity" /> <activity android:name=".SelectFolderActivity" />
<activity android:name=".sshkeygen.SshKeyGenActivity" />
</application> </application>
</manifest> </manifest>

View file

@ -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);
}
}

View file

@ -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)
}
}
}

View file

@ -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();
}
}
}
}

View file

@ -32,6 +32,8 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.zeapo.pwdstore.autofill.AutofillPreferenceActivity import com.zeapo.pwdstore.autofill.AutofillPreferenceActivity
import com.zeapo.pwdstore.crypto.PgpActivity import com.zeapo.pwdstore.crypto.PgpActivity
import com.zeapo.pwdstore.git.GitActivity 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.PasswordRepository
import com.zeapo.pwdstore.utils.auth.AuthenticationResult import com.zeapo.pwdstore.utils.auth.AuthenticationResult
import com.zeapo.pwdstore.utils.auth.Authenticator import com.zeapo.pwdstore.utils.auth.Authenticator
@ -141,7 +143,7 @@ class UserPreference : AppCompatActivity() {
} }
viewSshKeyPreference?.onPreferenceClickListener = ClickListener { viewSshKeyPreference?.onPreferenceClickListener = ClickListener {
val df = SshKeyGen.ShowSshKeyFragment() val df = ShowSshKeyFragment()
df.show(requireFragmentManager(), "public_key") df.show(requireFragmentManager(), "public_key")
true true
} }
@ -377,7 +379,7 @@ class UserPreference : AppCompatActivity() {
* Opens a key generator to generate a public/private key pair * Opens a key generator to generate a public/private key pair
*/ */
fun makeSshKey(fromPreferences: Boolean) { fun makeSshKey(fromPreferences: Boolean) {
val intent = Intent(applicationContext, SshKeyGen::class.java) val intent = Intent(applicationContext, SshKeyGenActivity::class.java)
startActivity(intent) startActivity(intent)
if (!fromPreferences) { if (!fromPreferences) {
setResult(Activity.RESULT_OK) setResult(Activity.RESULT_OK)

View file

@ -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()
}
}
}

View file

@ -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)
}
}
}

View file

@ -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)
}
}

View file

@ -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
}
}
}

Binary file not shown.

View file

@ -36,6 +36,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:importantForAccessibility="no" android:importantForAccessibility="no"
android:fontFamily="@font/sourcecodepro"
android:inputType="textPassword" /> android:inputType="textPassword" />
</com.google.android.material.textfield.TextInputLayout> </com.google.android.material.textfield.TextInputLayout>
@ -62,12 +63,14 @@
android:inputType="text" /> android:inputType="text" />
</com.google.android.material.textfield.TextInputLayout> </com.google.android.material.textfield.TextInputLayout>
<Button <com.google.android.material.button.MaterialButton
android:id="@+id/generate"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="8dp" 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> </LinearLayout>
</ScrollView> </ScrollView>

View file

@ -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">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="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="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> </resources>