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:
parent
5749c97d7c
commit
9acad2abf6
12 changed files with 1016 additions and 1192 deletions
|
@ -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>
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
731
app/src/main/java/com/zeapo/pwdstore/PasswordStore.kt
Normal file
731
app/src/main/java/com/zeapo/pwdstore/PasswordStore.kt
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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)
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
BIN
app/src/main/res/font/sourcecodepro.ttf
Normal file
BIN
app/src/main/res/font/sourcecodepro.ttf
Normal file
Binary file not shown.
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in a new issue