Backend:
-Generalize SFTP query param option map building -Add "key" and "phrase" as SFTP query params key: custom private key name phrase: passphrase used to unlock key -Add CRUD support for custom private keys Key files are stored in "user_keys" subdirectory File names are constructed by (sanitized) key name Basic support for private key content validation -Existing and new key-related functionality moved into SftpPublicPrivateKeyUtils class UI: -Add custom private key support to SFTP Credentials dialog Add a new auth mode item (authModeSpinner) Add Spinner showing saved private key names, with an option to create a new one (top). Add Delete Private Key button; deletes the selected key in Spinner Testing: -Add custom private key CRUD support to JavaFileStorageTest app via file chooser SFTP Credentials panel
This commit is contained in:
@@ -0,0 +1,216 @@
|
||||
package keepass2android.javafilestorage;
|
||||
|
||||
import android.util.Pair;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.StringReader;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.jcraft.jsch.JSch;
|
||||
import com.jcraft.jsch.JSchException;
|
||||
import com.jcraft.jsch.KeyPair;
|
||||
|
||||
class SftpPublicPrivateKeyUtils {
|
||||
|
||||
private enum Validity {
|
||||
NOT_ATTEMPTED, VALID, NOT_VALID;
|
||||
}
|
||||
|
||||
private static final String SFTP_CUSTOM_KEY_DIRNAME = "user_keys";
|
||||
|
||||
private static final String KP2A_PRIVATE_KEY_FILENAME = "id_kp2a_rsa";
|
||||
|
||||
private final File appBaseDir;
|
||||
|
||||
/**
|
||||
* Do NOT access this variable directly! Use {@link #baseDir()} instead.
|
||||
*/
|
||||
private final File customKeyBaseDir;
|
||||
private volatile Validity validDir = Validity.NOT_ATTEMPTED;
|
||||
|
||||
SftpPublicPrivateKeyUtils(String appBaseDir) {
|
||||
// Assume app base directory exists already
|
||||
this.appBaseDir = new File(appBaseDir);
|
||||
|
||||
// Intentionally skipping existence/creation checking in constructor
|
||||
// See baseDir()
|
||||
this.customKeyBaseDir = new File(appBaseDir, SFTP_CUSTOM_KEY_DIRNAME);
|
||||
}
|
||||
|
||||
private Pair<File, Boolean> baseDir() {
|
||||
if (validDir == Validity.NOT_ATTEMPTED) {
|
||||
synchronized (this) {
|
||||
if (!customKeyBaseDir.exists()) {
|
||||
customKeyBaseDir.mkdirs();
|
||||
}
|
||||
if (customKeyBaseDir.exists() && customKeyBaseDir.isDirectory()) {
|
||||
validDir = Validity.VALID;
|
||||
} else {
|
||||
validDir = Validity.NOT_VALID;
|
||||
}
|
||||
}
|
||||
}
|
||||
return new Pair<>(customKeyBaseDir, validDir == Validity.VALID);
|
||||
}
|
||||
|
||||
boolean deleteCustomKey(String keyName) throws FileNotFoundException {
|
||||
File f = getCustomKeyFile(keyName);
|
||||
return f.isFile() && f.delete();
|
||||
}
|
||||
|
||||
String[] getCustomKeyNames() {
|
||||
Pair<File, Boolean> base = baseDir();
|
||||
if (!base.second) {
|
||||
// Log it?
|
||||
return new String[]{};
|
||||
}
|
||||
return base.first.list();
|
||||
}
|
||||
|
||||
void savePrivateKeyContent(String keyName, String keyContent) throws IOException, Exception {
|
||||
keyContent = PrivateKeyValidator.ensureValidContent(keyContent);
|
||||
|
||||
File f = getCustomKeyFile(keyName);
|
||||
try (BufferedWriter w = new BufferedWriter(new FileWriter(f))) {
|
||||
w.write(keyContent);
|
||||
}
|
||||
}
|
||||
|
||||
String getCustomKeyFilePath(String customKeyName) throws FileNotFoundException {
|
||||
return getCustomKeyFile(customKeyName).getAbsolutePath();
|
||||
}
|
||||
|
||||
String resolveKeyFilePath(JSch jschInst, @Nullable String customKeyName) {
|
||||
// Custom private key configured
|
||||
if (customKeyName != null) {
|
||||
try {
|
||||
return getCustomKeyFilePath(customKeyName);
|
||||
} catch (FileNotFoundException e) {
|
||||
System.out.println(e);
|
||||
}
|
||||
}
|
||||
// Use KP2A's public/private key
|
||||
String keyFilePath = getAppKeyFileName();
|
||||
try{
|
||||
createKeyPair(jschInst, keyFilePath);
|
||||
} catch (Exception ex) {
|
||||
System.out.println(ex);
|
||||
}
|
||||
return keyFilePath;
|
||||
}
|
||||
|
||||
String createKeyPair(JSch jschInst) throws IOException, JSchException {
|
||||
return createKeyPair(jschInst, getAppKeyFileName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Exposed for testing purposes only
|
||||
* @param keyName
|
||||
* @return
|
||||
*/
|
||||
String getSanitizedCustomKeyName(String keyName) {
|
||||
return PrivateKeyValidator.sanitizeKeyAsFilename(keyName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Exposed for testing purposes only.
|
||||
* @param keyContent
|
||||
* @return
|
||||
* @throws Exception
|
||||
*/
|
||||
String getValidatedCustomKeyContent(String keyContent) throws Exception {
|
||||
return PrivateKeyValidator.ensureValidContent(keyContent);
|
||||
}
|
||||
|
||||
private String createKeyPair(JSch jschInst, String key_filename) throws JSchException, IOException {
|
||||
String public_key_filename = key_filename + ".pub";
|
||||
File file = new File(key_filename);
|
||||
if (file.exists())
|
||||
return public_key_filename;
|
||||
int type = KeyPair.RSA;
|
||||
KeyPair kpair = KeyPair.genKeyPair(jschInst, type, 4096);
|
||||
kpair.writePrivateKey(key_filename);
|
||||
|
||||
kpair.writePublicKey(public_key_filename, "generated by Keepass2Android");
|
||||
//ret = "Fingerprint: " + kpair.getFingerPrint();
|
||||
kpair.dispose();
|
||||
return public_key_filename;
|
||||
}
|
||||
|
||||
private String getAppKeyFileName() {
|
||||
return new File(appBaseDir, KP2A_PRIVATE_KEY_FILENAME).getAbsolutePath();
|
||||
}
|
||||
|
||||
private File getCustomKeyFile(String customKeyName) throws FileNotFoundException {
|
||||
Pair<File, Boolean> base = baseDir();
|
||||
if (!base.second) {
|
||||
throw new FileNotFoundException("Custom key directory");
|
||||
}
|
||||
|
||||
String keyFileName = PrivateKeyValidator.sanitizeKeyAsFilename(customKeyName);
|
||||
if (!keyFileName.isEmpty()) {
|
||||
File keyFile = new File(base.first, keyFileName);
|
||||
// Protect against bad actors trying to navigate away from the base directory.
|
||||
// This is probably overkill, given sanitizeKeyAsFilename(...) but better safe than sorry.
|
||||
if (base.first.equals(keyFile.getParentFile())) {
|
||||
return keyFile;
|
||||
}
|
||||
}
|
||||
// The key was sanitized to nothing, or the parent check above failed.
|
||||
throw new FileNotFoundException("Malformed key name");
|
||||
}
|
||||
|
||||
|
||||
private static class PrivateKeyValidator {
|
||||
private static final Pattern CONTENT_FIRST_LINE = Pattern.compile("^-+BEGIN\\s[^\\s]+\\sPRIVATE\\sKEY-+$");
|
||||
private static final Pattern CONTENT_LAST_LINE = Pattern.compile("^-+END\\s[^\\s]+\\sPRIVATE\\sKEY-+$");
|
||||
|
||||
/**
|
||||
* Key-to-filename sanitizer solution sourced from:
|
||||
* <a href="https://www.b4x.com/android/forum/threads/sanitize-filename.82558/" />
|
||||
*/
|
||||
private static final Pattern KEY_SANITIZER = Pattern.compile("([^\\p{L}\\s\\d\\-_~,;:\\[\\]\\(\\).'])",
|
||||
Pattern.CASE_INSENSITIVE);
|
||||
|
||||
static String sanitizeKeyAsFilename(String key) {
|
||||
return KEY_SANITIZER.matcher(key.trim()).replaceAll("");
|
||||
}
|
||||
|
||||
static String ensureValidContent(String content) throws Exception {
|
||||
content = content.trim();
|
||||
|
||||
boolean isValid = true;
|
||||
try (BufferedReader r = new BufferedReader(new StringReader(content))) {
|
||||
boolean validFirst = false;
|
||||
String line;
|
||||
String last = null;
|
||||
while ((line = r.readLine()) != null) {
|
||||
if (!validFirst) {
|
||||
if (CONTENT_FIRST_LINE.matcher(line).matches()) {
|
||||
validFirst = true;
|
||||
} else {
|
||||
isValid = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
last = line;
|
||||
}
|
||||
if (!isValid || last == null || !CONTENT_LAST_LINE.matcher(last).matches()) {
|
||||
throw new RuntimeException("Malformed private key content");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
android.util.Log.d(SftpStorage.class.getName(), "Invalid key content", e);
|
||||
throw e;
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package keepass2android.javafilestorage;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
@@ -20,7 +19,6 @@ import com.jcraft.jsch.ChannelSftp;
|
||||
import com.jcraft.jsch.ChannelSftp.LsEntry;
|
||||
import com.jcraft.jsch.JSch;
|
||||
import com.jcraft.jsch.JSchException;
|
||||
import com.jcraft.jsch.KeyPair;
|
||||
import com.jcraft.jsch.Session;
|
||||
import com.jcraft.jsch.SftpATTRS;
|
||||
import com.jcraft.jsch.SftpException;
|
||||
@@ -30,11 +28,32 @@ import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
|
||||
@SuppressWarnings("unused") // Exposed by JavaFileStorageBindings
|
||||
public class SftpStorage extends JavaFileStorageBase {
|
||||
@FunctionalInterface
|
||||
interface ValueResolver<T> {
|
||||
/**
|
||||
* Takes a raw value and resolves it to either a String containing the String representation
|
||||
* of that value, or null. The latter signifying that the raw value could not be "resolved".
|
||||
*
|
||||
* @param value
|
||||
* @return String, or null if not resolvable
|
||||
*/
|
||||
String resolve(T value);
|
||||
}
|
||||
|
||||
public static final int DEFAULT_SFTP_PORT = 22;
|
||||
public static final int UNSET_SFTP_CONNECT_TIMEOUT = -1;
|
||||
private static final String SFTP_CONNECT_TIMEOUT_OPTION_NAME = "connectTimeout";
|
||||
private static final String SFTP_KEYNAME_OPTION_NAME = "key";
|
||||
private static final String SFTP_KEYPASSPHRASE_OPTION_NAME = "phrase";
|
||||
|
||||
private static final ValueResolver<Integer> cTimeoutResolver = c ->
|
||||
c == null || c == UNSET_SFTP_CONNECT_TIMEOUT ? null : String.valueOf(c);
|
||||
|
||||
private static final ValueResolver<String> nonBlankStringResolver = s ->
|
||||
s == null || s.isBlank() ? null : s;
|
||||
|
||||
|
||||
JSch jsch;
|
||||
|
||||
@@ -44,39 +63,36 @@ public class SftpStorage extends JavaFileStorageBase {
|
||||
public String username;
|
||||
public String password;
|
||||
public String localPath;
|
||||
public String keyName;
|
||||
public String keyPassphrase;
|
||||
public int port;
|
||||
public int connectTimeoutSec = UNSET_SFTP_CONNECT_TIMEOUT;
|
||||
|
||||
|
||||
public String toString() {
|
||||
return "ConnectionInfo{host=" + host + ",port=" + port + ",user=" + username +
|
||||
",pwd=<hidden>,path=" + localPath + ",connectTimeout=" + connectTimeoutSec +
|
||||
",pwd=<hidden>,localPath=" + localPath + ",key=" + keyName +
|
||||
",phrase=<hidden>,connectTimeout=" + connectTimeoutSec +
|
||||
"}";
|
||||
}
|
||||
}
|
||||
|
||||
boolean hasOptions() {
|
||||
return connectTimeoutSec != UNSET_SFTP_CONNECT_TIMEOUT;
|
||||
private static Map<String, String> buildOptionMap(ConnectionInfo ci, boolean includeSensitive) {
|
||||
OptionMapBuilder b = new OptionMapBuilder()
|
||||
.addOption(SFTP_CONNECT_TIMEOUT_OPTION_NAME, ci.connectTimeoutSec, cTimeoutResolver)
|
||||
.addOption(SFTP_KEYNAME_OPTION_NAME, ci.keyName, nonBlankStringResolver);
|
||||
if (includeSensitive) {
|
||||
b.addOption(SFTP_KEYPASSPHRASE_OPTION_NAME, ci.keyPassphrase, nonBlankStringResolver);
|
||||
}
|
||||
return b.build();
|
||||
}
|
||||
|
||||
private static Map<String, String> buildOptionMap(ConnectionInfo ci) {
|
||||
return buildOptionMap(ci.connectTimeoutSec);
|
||||
}
|
||||
|
||||
private static Map<String, String> buildOptionMap(int connectTimeoutSec) {
|
||||
Map<String, String> opts = new HashMap<>();
|
||||
if (connectTimeoutSec != UNSET_SFTP_CONNECT_TIMEOUT) {
|
||||
opts.put(SFTP_CONNECT_TIMEOUT_OPTION_NAME, String.valueOf(connectTimeoutSec));
|
||||
}
|
||||
return opts;
|
||||
}
|
||||
|
||||
|
||||
Context _appContext;
|
||||
private final SftpPublicPrivateKeyUtils _keyUtils;
|
||||
|
||||
public SftpStorage(Context appContext) {
|
||||
_appContext = appContext;
|
||||
|
||||
_keyUtils = new SftpPublicPrivateKeyUtils(getBaseDir());
|
||||
}
|
||||
|
||||
private static final String SFTP_PROTOCOL_ID = "sftp";
|
||||
@@ -184,7 +200,8 @@ public class SftpStorage extends JavaFileStorageBase {
|
||||
tryDisconnect(c);
|
||||
|
||||
return buildFullPath(cInfo.host, cInfo.port, newPath,
|
||||
cInfo.username, cInfo.password, cInfo.connectTimeoutSec);
|
||||
cInfo.username, cInfo.password, cInfo.connectTimeoutSec,
|
||||
cInfo.keyName, cInfo.keyPassphrase);
|
||||
} catch (Exception e) {
|
||||
throw convertException(e);
|
||||
}
|
||||
@@ -402,28 +419,23 @@ public class SftpStorage extends JavaFileStorageBase {
|
||||
return java.net.URLEncoder.encode(unencoded, UTF_8);
|
||||
}
|
||||
|
||||
|
||||
ChannelSftp init(ConnectionInfo cInfo) throws JSchException, UnsupportedEncodingException {
|
||||
jsch = new JSch();
|
||||
|
||||
String base_dir = getBaseDir();
|
||||
jsch.setKnownHosts(base_dir + "/known_hosts");
|
||||
|
||||
String key_filename = getKeyFileName();
|
||||
try{
|
||||
createKeyPair(key_filename);
|
||||
} catch (Exception ex) {
|
||||
System.out.println(ex);
|
||||
}
|
||||
String key_filepath = _keyUtils.resolveKeyFilePath(jsch, cInfo.keyName);
|
||||
|
||||
try {
|
||||
jsch.addIdentity(key_filename);
|
||||
} catch (java.lang.Exception e)
|
||||
{
|
||||
jsch.addIdentity(key_filepath);
|
||||
} catch (java.lang.Exception e) {
|
||||
|
||||
}
|
||||
|
||||
Session session = jsch.getSession(cInfo.username, cInfo.host, cInfo.port);
|
||||
UserInfo ui = new SftpUserInfo(cInfo.password, _appContext);
|
||||
UserInfo ui = new SftpUserInfo(cInfo.password, cInfo.keyPassphrase, _appContext);
|
||||
session.setUserInfo(ui);
|
||||
|
||||
session.setConfig("PreferredAuthentications", "publickey,password");
|
||||
@@ -434,7 +446,6 @@ public class SftpStorage extends JavaFileStorageBase {
|
||||
channel.connect();
|
||||
ChannelSftp c = (ChannelSftp) channel;
|
||||
|
||||
logDebug("success: init Sftp");
|
||||
return c;
|
||||
|
||||
}
|
||||
@@ -451,38 +462,60 @@ public class SftpStorage extends JavaFileStorageBase {
|
||||
return _appContext.getFilesDir().getAbsolutePath();
|
||||
}
|
||||
|
||||
private String getKeyFileName() {
|
||||
return getBaseDir() + "/id_kp2a_rsa";
|
||||
public boolean deleteCustomKey(String keyName) throws FileNotFoundException {
|
||||
return _keyUtils.deleteCustomKey(keyName);
|
||||
}
|
||||
|
||||
public String[] getCustomKeyNames() {
|
||||
return _keyUtils.getCustomKeyNames();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused") // Exposed by JavaFileStorageBindings
|
||||
public String createKeyPair() throws IOException, JSchException {
|
||||
return createKeyPair(getKeyFileName());
|
||||
|
||||
return _keyUtils.createKeyPair(jsch);
|
||||
}
|
||||
|
||||
private String createKeyPair(String key_filename) throws JSchException, IOException {
|
||||
String public_key_filename = key_filename + ".pub";
|
||||
File file = new File(key_filename);
|
||||
if (file.exists())
|
||||
return public_key_filename;
|
||||
int type = KeyPair.RSA;
|
||||
KeyPair kpair = KeyPair.genKeyPair(jsch, type, 4096);
|
||||
kpair.writePrivateKey(key_filename);
|
||||
|
||||
kpair.writePublicKey(public_key_filename, "generated by Keepass2Android");
|
||||
//ret = "Fingerprint: " + kpair.getFingerPrint();
|
||||
kpair.dispose();
|
||||
return public_key_filename;
|
||||
|
||||
@SuppressWarnings("unused") // Exposed by JavaFileStorageBindings
|
||||
public void savePrivateKeyContent(String keyName, String keyContent) throws IOException, Exception {
|
||||
_keyUtils.savePrivateKeyContent(keyName, keyContent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Exposed for testing purposes only.
|
||||
* @param keyName
|
||||
* @return
|
||||
*/
|
||||
public String sanitizeCustomKeyName(String keyName) {
|
||||
return _keyUtils.getSanitizedCustomKeyName(keyName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Exposed for testing purposes only.
|
||||
* @param keyContent
|
||||
* @return
|
||||
* @throws Exception
|
||||
*/
|
||||
public String getValidatedCustomKeyContent(String keyContent) throws Exception {
|
||||
return _keyUtils.getValidatedCustomKeyContent(keyContent);
|
||||
}
|
||||
|
||||
|
||||
public ConnectionInfo splitStringToConnectionInfo(String filename)
|
||||
throws UnsupportedEncodingException {
|
||||
ConnectionInfo ci = new ConnectionInfo();
|
||||
ci.host = extractUserPwdHostPort(filename);
|
||||
|
||||
String userPwd = ci.host.substring(0, ci.host.indexOf('@'));
|
||||
ci.username = decode(userPwd.substring(0, userPwd.indexOf(":")));
|
||||
ci.password = decode(userPwd.substring(userPwd.indexOf(":")+1));
|
||||
int sepIdx = userPwd.indexOf(":");
|
||||
if (sepIdx > 0) {
|
||||
ci.username = decode(userPwd.substring(0, sepIdx));
|
||||
ci.password = decode(userPwd.substring(sepIdx + 1));
|
||||
} else {
|
||||
ci.username = userPwd;
|
||||
ci.password = null;
|
||||
}
|
||||
|
||||
ci.host = ci.host.substring(ci.host.indexOf('@') + 1);
|
||||
ci.port = DEFAULT_SFTP_PORT;
|
||||
int portSeparatorIndex = ci.host.indexOf(":");
|
||||
@@ -503,6 +536,12 @@ public class SftpStorage extends JavaFileStorageBase {
|
||||
logDebug(SFTP_CONNECT_TIMEOUT_OPTION_NAME + " option not a number: " + optVal);
|
||||
}
|
||||
}
|
||||
if (options.containsKey(SFTP_KEYNAME_OPTION_NAME)) {
|
||||
ci.keyName = options.get(SFTP_KEYNAME_OPTION_NAME);
|
||||
}
|
||||
if (options.containsKey(SFTP_KEYPASSPHRASE_OPTION_NAME)) {
|
||||
ci.keyPassphrase = options.get(SFTP_KEYPASSPHRASE_OPTION_NAME);
|
||||
}
|
||||
return ci;
|
||||
}
|
||||
|
||||
@@ -544,9 +583,7 @@ public class SftpStorage extends JavaFileStorageBase {
|
||||
.append("@")
|
||||
.append(ci.host)
|
||||
.append(ci.localPath);
|
||||
if (ci.hasOptions()) {
|
||||
appendOptions(dName, buildOptionMap(ci));
|
||||
}
|
||||
appendOptions(dName, buildOptionMap(ci, false));
|
||||
return dName.toString();
|
||||
}
|
||||
catch (Exception e)
|
||||
@@ -580,11 +617,15 @@ public class SftpStorage extends JavaFileStorageBase {
|
||||
|
||||
public String buildFullPath(String host, int port, String localPath,
|
||||
String username, String password,
|
||||
int connectTimeoutSec)
|
||||
int connectTimeoutSec,
|
||||
String keyName, String keyPassphrase)
|
||||
throws UnsupportedEncodingException {
|
||||
StringBuilder uri = new StringBuilder(getProtocolPrefix())
|
||||
.append(encode(username)).append(":").append(encode(password))
|
||||
.append("@").append(host);
|
||||
|
||||
StringBuilder uri = new StringBuilder(getProtocolPrefix()).append(encode(username));
|
||||
if (password != null) {
|
||||
uri.append(":").append(encode(password));
|
||||
}
|
||||
uri.append("@").append(host);
|
||||
|
||||
if (port != DEFAULT_SFTP_PORT) {
|
||||
uri.append(":").append(port);
|
||||
@@ -593,8 +634,11 @@ public class SftpStorage extends JavaFileStorageBase {
|
||||
uri.append(localPath);
|
||||
}
|
||||
|
||||
Map<String, String> opts = buildOptionMap(connectTimeoutSec);
|
||||
appendOptions(uri, opts);
|
||||
appendOptions(uri, new OptionMapBuilder()
|
||||
.addOption(SFTP_CONNECT_TIMEOUT_OPTION_NAME, connectTimeoutSec, cTimeoutResolver)
|
||||
.addOption(SFTP_KEYNAME_OPTION_NAME, keyName, nonBlankStringResolver)
|
||||
.addOption(SFTP_KEYPASSPHRASE_OPTION_NAME, keyPassphrase, nonBlankStringResolver)
|
||||
.build());
|
||||
|
||||
return uri.toString();
|
||||
}
|
||||
@@ -635,4 +679,31 @@ public class SftpStorage extends JavaFileStorageBase {
|
||||
return o1.getKey().compareTo(o2.getKey());
|
||||
}
|
||||
}
|
||||
|
||||
private static class OptionMapBuilder {
|
||||
private final Map<String, String> options = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Attempts to add a raw value <code>oVal</code> to the underlying option map with key <code>oName</code>
|
||||
* iff the <code>resolver</code> produces a non-null output when invoked using the raw value.
|
||||
*
|
||||
* @param oName the name/key associated with the value, if added
|
||||
* @param oVal the raw value attempting to be added
|
||||
* @param resolver the resolver that determines if the value will be added
|
||||
*
|
||||
* @return OptionMapBuilder (updated)
|
||||
* @param <T> the raw value type
|
||||
*/
|
||||
<T> OptionMapBuilder addOption(final String oName, T oVal, ValueResolver<T> resolver) {
|
||||
String resolved = resolver.resolve(oVal);
|
||||
if (resolved != null) {
|
||||
options.put(oName, resolved);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
Map<String, String> build() {
|
||||
return new HashMap<>(options);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,17 +113,19 @@ public class SftpUserInfo implements UserInfo {
|
||||
Context _appContext;
|
||||
|
||||
String _password;
|
||||
String _passphrase;
|
||||
|
||||
public SftpUserInfo(String password, Context appContext)
|
||||
public SftpUserInfo(String password, String passphrase, Context appContext)
|
||||
{
|
||||
_password = password;
|
||||
_passphrase = passphrase;
|
||||
_appContext = appContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPassphrase() {
|
||||
|
||||
return null;
|
||||
return _passphrase;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -134,12 +136,12 @@ public class SftpUserInfo implements UserInfo {
|
||||
|
||||
@Override
|
||||
public boolean promptPassword(String message) {
|
||||
return true;
|
||||
return _password != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean promptPassphrase(String message) {
|
||||
return false; //passphrase not supported
|
||||
return _passphrase != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -697,12 +697,11 @@ public class MainActivity extends Activity implements JavaFileStorage.FileStorag
|
||||
if (protocolId.equals("sftp"))
|
||||
{
|
||||
final View view = getLayoutInflater().inflate(R.layout.sftp_credentials, null);
|
||||
final SftpStorage sftpStorage = (SftpStorage)storageToTest;
|
||||
|
||||
view.findViewById(R.id.send_public_key).setOnClickListener(v -> {
|
||||
Intent sendIntent = new Intent();
|
||||
|
||||
|
||||
SftpStorage sftpStorage = (SftpStorage)storageToTest;
|
||||
try {
|
||||
String pub_filename = sftpStorage.createKeyPair();
|
||||
|
||||
@@ -715,51 +714,121 @@ public class MainActivity extends Activity implements JavaFileStorage.FileStorag
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Toast.makeText(this,"Failed to create key pair: " + ex.getMessage(), Toast.LENGTH_LONG);
|
||||
return;
|
||||
Toast.makeText(this,"Failed to create key pair: " + ex.getMessage(), Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
|
||||
view.findViewById(R.id.list_private_keys).setOnClickListener(v -> {
|
||||
String[] keys = sftpStorage.getCustomKeyNames();
|
||||
Toast.makeText(this, "keys: " + String.join(",", keys), Toast.LENGTH_LONG).show();
|
||||
});
|
||||
|
||||
view.findViewById(R.id.add_private_key).setOnClickListener(v -> {
|
||||
EditText etKeyName = view.findViewById(R.id.private_key_name);
|
||||
String keyName = etKeyName.getText().toString();
|
||||
EditText etKeyContent = view.findViewById(R.id.private_key_content);
|
||||
String keyContent = etKeyContent.getText().toString();
|
||||
|
||||
try {
|
||||
sftpStorage.savePrivateKeyContent(keyName, keyContent);
|
||||
Toast.makeText(this, "Add successful", Toast.LENGTH_LONG).show();
|
||||
}
|
||||
catch (Exception e) {
|
||||
Toast.makeText(this, "Add failed: " + e.getMessage(), Toast.LENGTH_LONG).show();
|
||||
}
|
||||
});
|
||||
|
||||
view.findViewById(R.id.delete_private_key).setOnClickListener(v -> {
|
||||
EditText etKeyName = view.findViewById(R.id.private_key_name);
|
||||
String keyName = etKeyName.getText().toString();
|
||||
|
||||
String exMessage = null;
|
||||
boolean success = false;
|
||||
try {
|
||||
success = sftpStorage.deleteCustomKey(keyName);
|
||||
}
|
||||
catch (Exception e) {
|
||||
exMessage = e.getMessage();
|
||||
}
|
||||
StringBuilder msg = new StringBuilder("Delete ");
|
||||
msg.append(success ? "succeeded" : "FAILED");
|
||||
if (exMessage != null) {
|
||||
msg.append(" (").append(exMessage).append(")");
|
||||
}
|
||||
Toast.makeText(this, msg.toString(), Toast.LENGTH_LONG).show();
|
||||
});
|
||||
|
||||
view.findViewById(R.id.validate_private_key).setOnClickListener(v -> {
|
||||
EditText etKeyName = view.findViewById(R.id.private_key_name);
|
||||
String inKeyName = etKeyName.getText().toString();
|
||||
|
||||
if (!inKeyName.isEmpty()) {
|
||||
String keyResponse;
|
||||
try {
|
||||
keyResponse = sftpStorage.sanitizeCustomKeyName(inKeyName);
|
||||
} catch (Exception e) {
|
||||
keyResponse = "EX:" + e.getMessage();
|
||||
}
|
||||
String msg = "key: [" + inKeyName + "] -> [" + keyResponse + "]";
|
||||
Toast.makeText(this, msg, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
EditText etKeyContent = view.findViewById(R.id.private_key_content);
|
||||
String inKeyContent = etKeyContent.getText().toString();
|
||||
String msg;
|
||||
if (!inKeyContent.isEmpty()) {
|
||||
try {
|
||||
// We could print the key, but I don't it's that helpful
|
||||
sftpStorage.getValidatedCustomKeyContent(inKeyContent);
|
||||
msg = "Key content is valid";
|
||||
} catch (Exception e) {
|
||||
msg = "Invalid key content: " + e.getMessage();
|
||||
}
|
||||
Toast.makeText(this, msg, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
});
|
||||
|
||||
new AlertDialog.Builder(this)
|
||||
.setView(view)
|
||||
.setTitle("Enter SFTP credentials")
|
||||
.setPositiveButton("OK",new DialogInterface.OnClickListener() {
|
||||
.setPositiveButton("OK", (dialog, which) -> {
|
||||
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
Toast.makeText(MainActivity.this, "Hey", Toast.LENGTH_LONG).show();
|
||||
|
||||
Toast.makeText(MainActivity.this, "Hey", Toast.LENGTH_LONG).show();
|
||||
|
||||
SftpStorage sftpStorage = (SftpStorage)storageToTest;
|
||||
try {
|
||||
EditText etHost = ((EditText)view.findViewById(R.id.sftp_host));
|
||||
String host = etHost.getText().toString();
|
||||
EditText etUser = ((EditText)view.findViewById(R.id.sftp_user));
|
||||
String user = etUser.getText().toString();
|
||||
EditText etPwd = ((EditText)view.findViewById(R.id.sftp_password));
|
||||
String pwd = etPwd.getText().toString();
|
||||
EditText etPort = ((EditText)view.findViewById(R.id.sftp_port));
|
||||
int port = Integer.parseInt(etPort.getText().toString());
|
||||
EditText etInitDir = ((EditText)view.findViewById(R.id.sftp_initial_dir));
|
||||
String initialDir = etInitDir.getText().toString();
|
||||
EditText etConnectTimeout = ((EditText)view.findViewById(R.id.sftp_connect_timeout));
|
||||
int connectTimeout = SftpStorage.UNSET_SFTP_CONNECT_TIMEOUT;
|
||||
String ctStr = etConnectTimeout.getText().toString();
|
||||
if (!ctStr.isEmpty()) {
|
||||
try {
|
||||
int ct = Integer.parseInt(ctStr);
|
||||
if (connectTimeout != ct) {
|
||||
connectTimeout = ct;
|
||||
}
|
||||
} catch (NumberFormatException parseEx) {
|
||||
SftpStorage sftpStorage1 = (SftpStorage)storageToTest;
|
||||
try {
|
||||
EditText etHost = view.findViewById(R.id.sftp_host);
|
||||
String host = etHost.getText().toString();
|
||||
EditText etUser = view.findViewById(R.id.sftp_user);
|
||||
String user = etUser.getText().toString();
|
||||
EditText etPwd = view.findViewById(R.id.sftp_password);
|
||||
String pwd = etPwd.getText().toString();
|
||||
EditText etPort = view.findViewById(R.id.sftp_port);
|
||||
int port = Integer.parseInt(etPort.getText().toString());
|
||||
EditText etInitDir = view.findViewById(R.id.sftp_initial_dir);
|
||||
String initialDir = etInitDir.getText().toString();
|
||||
EditText etConnectTimeout = view.findViewById(R.id.sftp_connect_timeout);
|
||||
int connectTimeout = SftpStorage.UNSET_SFTP_CONNECT_TIMEOUT;
|
||||
String ctStr = etConnectTimeout.getText().toString();
|
||||
if (!ctStr.isEmpty()) {
|
||||
try {
|
||||
int ct = Integer.parseInt(ctStr);
|
||||
if (connectTimeout != ct) {
|
||||
connectTimeout = ct;
|
||||
}
|
||||
} catch (NumberFormatException parseEx) {
|
||||
}
|
||||
onReceivePathForFileSelect(requestCode, sftpStorage.buildFullPath( host, port, initialDir, user, pwd, connectTimeout));
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
// TODO Auto-generated catch block
|
||||
e.printStackTrace();
|
||||
}
|
||||
EditText etKeyName = view.findViewById(R.id.private_key_name);
|
||||
String keyName = etKeyName.getText().toString();
|
||||
EditText etKeyPassphrase = view.findViewById(R.id.private_key_passphrase);
|
||||
String keyPassphrase = etKeyPassphrase.getText().toString();
|
||||
|
||||
onReceivePathForFileSelect(requestCode, sftpStorage1.buildFullPath(
|
||||
host, port, initialDir, user, pwd, connectTimeout,
|
||||
keyName, keyPassphrase));
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
})
|
||||
.create()
|
||||
|
||||
@@ -77,5 +77,59 @@
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="send public key" />
|
||||
<TextView
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_horizontal"
|
||||
android:layout_marginTop="20dp"
|
||||
android:textStyle="bold"
|
||||
android:text="Private Keys Functions" />
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content">
|
||||
<Button android:id="@+id/list_private_keys"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="List" />
|
||||
<Button android:id="@+id/add_private_key"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="Add" />
|
||||
<Button android:id="@+id/delete_private_key"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="Delete" />
|
||||
<Button android:id="@+id/validate_private_key"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="Validate" />
|
||||
</LinearLayout>
|
||||
<EditText android:id="@+id/private_key_name"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:singleLine="true"
|
||||
android:inputType="textNoSuggestions"
|
||||
android:text=""
|
||||
android:hint="key name" />
|
||||
<EditText android:id="@+id/private_key_content"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textMultiLine"
|
||||
android:lines="5"
|
||||
android:text=""
|
||||
android:hint="key content" />
|
||||
<EditText android:id="@+id/private_key_passphrase"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:singleLine="true"
|
||||
android:inputType="textNoSuggestions"
|
||||
android:text=""
|
||||
android:hint="key passphrase (optional)" />
|
||||
|
||||
</LinearLayout>
|
||||
@@ -42,6 +42,12 @@ namespace keepass2android
|
||||
private readonly string _schemeSeparator = "://";
|
||||
private bool _tryGetPermanentAccess;
|
||||
|
||||
private const int SftpModeSpinnerPasswd = 0;
|
||||
private const int SftpModeSpinnerPubKey = 1;
|
||||
private const int SftpModeSpinnerCustomKey = 2;
|
||||
|
||||
private const int SftpKeySpinnerCreateNewIdx = 0;
|
||||
|
||||
public string DefaultExtension { get; set; }
|
||||
|
||||
public FileSelectHelper(Activity activity, bool isForSave, bool tryGetPermanentAccess, int requestCode)
|
||||
@@ -57,81 +63,198 @@ namespace keepass2android
|
||||
#if !EXCLUDE_JAVAFILESTORAGE && !NoNet
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
|
||||
View dlgContents = activity.LayoutInflater.Inflate(Resource.Layout.sftpcredentials, null);
|
||||
var ctx = activity.ApplicationContext;
|
||||
var fileStorage = new Keepass2android.Javafilestorage.SftpStorage(ctx);
|
||||
|
||||
var spinner = dlgContents.FindViewById<Spinner>(Resource.Id.sftp_auth_mode_spinner);
|
||||
dlgContents.FindViewById<Button>(Resource.Id.send_public_key_button).Click += (sender, args) =>
|
||||
{
|
||||
var fileStorage = new Keepass2android.Javafilestorage.SftpStorage(activity.ApplicationContext);
|
||||
string pub_filename = fileStorage.CreateKeyPair();
|
||||
LinearLayout addNewBtn = dlgContents.FindViewById<LinearLayout>(Resource.Id.sftp_add_key_group);
|
||||
Button deleteBtn = dlgContents.FindViewById<Button>(Resource.Id.sftp_delete_key_button);
|
||||
EditText keyNameTxt = dlgContents.FindViewById<EditText>(Resource.Id.sftp_key_name);
|
||||
EditText keyContentTxt = dlgContents.FindViewById<EditText>(Resource.Id.sftp_key_content);
|
||||
|
||||
Intent sendIntent = new Intent();
|
||||
sendIntent.SetAction(Intent.ActionSend);
|
||||
sendIntent.PutExtra(Intent.ExtraText, System.IO.File.ReadAllText(pub_filename));
|
||||
var keySpinner = dlgContents.FindViewById<Spinner>(Resource.Id.sftp_key_names);
|
||||
var keyNamesAdapter = new ArrayAdapter(ctx, Android.Resource.Layout.SimpleSpinnerDropDownItem, new List<string>());
|
||||
UpdatePrivateKeyNames(keyNamesAdapter, fileStorage, ctx);
|
||||
keyNamesAdapter.SetDropDownViewResource(Android.Resource.Layout.SimpleSpinnerDropDownItem);
|
||||
keySpinner.Adapter = keyNamesAdapter;
|
||||
keySpinner.SetSelection(SftpKeySpinnerCreateNewIdx);
|
||||
|
||||
sendIntent.PutExtra(Intent.ExtraSubject, "Keepass2Android sftp public key");
|
||||
sendIntent.SetType("text/plain");
|
||||
activity.StartActivity(Intent.CreateChooser(sendIntent, "Send public key to..."));
|
||||
};
|
||||
keySpinner.ItemSelected += (sender, args) =>
|
||||
{
|
||||
if (keySpinner.SelectedItemPosition == SftpKeySpinnerCreateNewIdx)
|
||||
{
|
||||
keyNameTxt.Text = "";
|
||||
keyContentTxt.Text = "";
|
||||
addNewBtn.Visibility = ViewStates.Visible;
|
||||
deleteBtn.Visibility = ViewStates.Gone;
|
||||
}
|
||||
else
|
||||
{
|
||||
addNewBtn.Visibility = ViewStates.Gone;
|
||||
deleteBtn.Visibility = ViewStates.Visible;
|
||||
}
|
||||
};
|
||||
|
||||
var authModeSpinner = dlgContents.FindViewById<Spinner>(Resource.Id.sftp_auth_mode_spinner);
|
||||
dlgContents.FindViewById<Button>(Resource.Id.send_public_key_button).Click += (sender, args) =>
|
||||
{
|
||||
string pub_filename = fileStorage.CreateKeyPair();
|
||||
|
||||
spinner.ItemSelected += (sender, args) =>
|
||||
{
|
||||
if (spinner.SelectedItemPosition == 0)
|
||||
{
|
||||
dlgContents.FindViewById<EditText>(Resource.Id.sftp_password).Visibility = ViewStates.Visible;
|
||||
dlgContents.FindViewById<Button>(Resource.Id.send_public_key_button).Visibility = ViewStates.Gone;
|
||||
}
|
||||
else
|
||||
{
|
||||
dlgContents.FindViewById<EditText>(Resource.Id.sftp_password).Visibility = ViewStates.Gone;
|
||||
dlgContents.FindViewById<Button>(Resource.Id.send_public_key_button).Visibility = ViewStates.Visible;
|
||||
Intent sendIntent = new Intent();
|
||||
sendIntent.SetAction(Intent.ActionSend);
|
||||
sendIntent.PutExtra(Intent.ExtraText, System.IO.File.ReadAllText(pub_filename));
|
||||
|
||||
sendIntent.PutExtra(Intent.ExtraSubject, "Keepass2Android sftp public key");
|
||||
sendIntent.SetType("text/plain");
|
||||
activity.StartActivity(Intent.CreateChooser(sendIntent, "Send public key to..."));
|
||||
};
|
||||
|
||||
dlgContents.FindViewById<Button>(Resource.Id.sftp_save_key_button).Click += (sender, args) =>
|
||||
{
|
||||
string keyName = keyNameTxt.Text;
|
||||
string keyContent = keyContentTxt.Text;
|
||||
|
||||
string toastMsg = null;
|
||||
if (!string.IsNullOrEmpty(keyName) && !string.IsNullOrEmpty(keyContent))
|
||||
{
|
||||
try
|
||||
{
|
||||
fileStorage.SavePrivateKeyContent(keyName, keyContent);
|
||||
keyNameTxt.Text = "";
|
||||
keyContentTxt.Text = "";
|
||||
toastMsg = ctx.GetString(Resource.String.private_key_saved);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
toastMsg = ctx.GetString(Resource.String.private_key_save_failed,
|
||||
new Java.Lang.Object[] { e.Message });
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
toastMsg = ctx.GetString(Resource.String.private_key_info);
|
||||
}
|
||||
|
||||
if (toastMsg!= null) {
|
||||
Toast.MakeText(_activity, toastMsg, ToastLength.Long).Show();
|
||||
}
|
||||
|
||||
UpdatePrivateKeyNames(keyNamesAdapter, fileStorage, ctx);
|
||||
keySpinner.SetSelection(ResolveKeySpinnerSelection(keyNamesAdapter, keyName));
|
||||
};
|
||||
|
||||
dlgContents.FindViewById<Button>(Resource.Id.sftp_delete_key_button).Click += (sender, args) =>
|
||||
{
|
||||
int selectedKey = dlgContents.FindViewById<Spinner>(Resource.Id.sftp_key_names).SelectedItemPosition;
|
||||
string keyName = ResolveSelectedKeyName(keyNamesAdapter, selectedKey);
|
||||
if (!string.IsNullOrEmpty(keyName))
|
||||
{
|
||||
bool deleted = fileStorage.DeleteCustomKey(keyName);
|
||||
|
||||
int msgId = deleted ? Resource.String.private_key_delete : Resource.String.private_key_delete_failed;
|
||||
string msg = ctx.GetString(msgId, new Java.Lang.Object[] { keyName });
|
||||
Toast.MakeText(_activity, msg, ToastLength.Long).Show();
|
||||
|
||||
UpdatePrivateKeyNames(keyNamesAdapter, fileStorage, ctx);
|
||||
keySpinner.SetSelection(SftpKeySpinnerCreateNewIdx);
|
||||
}
|
||||
};
|
||||
|
||||
authModeSpinner.ItemSelected += (sender, args) =>
|
||||
{
|
||||
var passwordBox = dlgContents.FindViewById<EditText>(Resource.Id.sftp_password);
|
||||
var publicKeyButton = dlgContents.FindViewById<Button>(Resource.Id.send_public_key_button);
|
||||
var keyfileGroup = dlgContents.FindViewById<LinearLayout>(Resource.Id.sftp_keyfile_group);
|
||||
|
||||
switch (authModeSpinner.SelectedItemPosition)
|
||||
{
|
||||
case SftpModeSpinnerPasswd:
|
||||
passwordBox.Visibility = ViewStates.Visible;
|
||||
publicKeyButton.Visibility = ViewStates.Gone;
|
||||
keyfileGroup.Visibility = ViewStates.Gone;
|
||||
break;
|
||||
case SftpModeSpinnerPubKey:
|
||||
passwordBox.Visibility = ViewStates.Gone;
|
||||
publicKeyButton.Visibility = ViewStates.Visible;
|
||||
keyfileGroup.Visibility = ViewStates.Gone;
|
||||
break;
|
||||
case SftpModeSpinnerCustomKey:
|
||||
passwordBox.Visibility = ViewStates.Gone;
|
||||
publicKeyButton.Visibility = ViewStates.Gone;
|
||||
keyfileGroup.Visibility = ViewStates.Visible;
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
if (!defaultPath.EndsWith(_schemeSeparator))
|
||||
{
|
||||
var fileStorage = new SftpStorage(activity.ApplicationContext);
|
||||
SftpStorage.ConnectionInfo ci = fileStorage.SplitStringToConnectionInfo(defaultPath);
|
||||
dlgContents.FindViewById<EditText>(Resource.Id.sftp_host).Text = ci.Host;
|
||||
dlgContents.FindViewById<EditText>(Resource.Id.sftp_port).Text = ci.Port.ToString();
|
||||
dlgContents.FindViewById<EditText>(Resource.Id.sftp_user).Text = ci.Username;
|
||||
dlgContents.FindViewById<EditText>(Resource.Id.sftp_password).Text = ci.Password;
|
||||
dlgContents.FindViewById<EditText>(Resource.Id.sftp_key_name).Text = ci.KeyName;
|
||||
dlgContents.FindViewById<EditText>(Resource.Id.sftp_key_passphrase).Text = ci.KeyPassphrase;
|
||||
dlgContents.FindViewById<EditText>(Resource.Id.sftp_initial_dir).Text = ci.LocalPath;
|
||||
if (ci.ConnectTimeoutSec != SftpStorage.UnsetSftpConnectTimeout)
|
||||
{
|
||||
dlgContents.FindViewById<EditText>(Resource.Id.sftp_connect_timeout).Text = ci.ConnectTimeoutSec.ToString();
|
||||
}
|
||||
if (string.IsNullOrEmpty(ci.Password))
|
||||
{
|
||||
spinner.SetSelection(1);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(ci.Password))
|
||||
{
|
||||
authModeSpinner.SetSelection(SftpModeSpinnerPasswd);
|
||||
} else if (!string.IsNullOrEmpty(ci.KeyName))
|
||||
{
|
||||
authModeSpinner.SetSelection(SftpModeSpinnerCustomKey);
|
||||
keySpinner.SetSelection(ResolveKeySpinnerSelection(keyNamesAdapter, ci.KeyName));
|
||||
} else
|
||||
{
|
||||
authModeSpinner.SetSelection(SftpModeSpinnerPubKey);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
builder.SetView(dlgContents);
|
||||
builder.SetPositiveButton(Android.Resource.String.Ok,
|
||||
(sender, args) =>
|
||||
{
|
||||
string host = dlgContents.FindViewById<EditText>(Resource.Id.sftp_host).Text;
|
||||
string portText = dlgContents.FindViewById<EditText>(Resource.Id.sftp_port).Text;
|
||||
int port = SftpStorage.DefaultSftpPort;
|
||||
if (!string.IsNullOrEmpty(portText))
|
||||
int.TryParse(portText, out port);
|
||||
string user = dlgContents.FindViewById<EditText>(Resource.Id.sftp_user).Text;
|
||||
string password = dlgContents.FindViewById<EditText>(Resource.Id.sftp_password).Text;
|
||||
string initialPath = dlgContents.FindViewById<EditText>(Resource.Id.sftp_initial_dir).Text;
|
||||
if (string.IsNullOrEmpty(initialPath))
|
||||
initialPath = "/";
|
||||
string connectTimeoutText = dlgContents.FindViewById<EditText>(Resource.Id.sftp_connect_timeout).Text;
|
||||
int connectTimeout = SftpStorage.UnsetSftpConnectTimeout;
|
||||
if (!string.IsNullOrEmpty(connectTimeoutText)) {
|
||||
int.TryParse(connectTimeoutText, out connectTimeout);
|
||||
}
|
||||
builder.SetPositiveButton(Android.Resource.String.Ok, (sender, args) => {
|
||||
int idx = 0;
|
||||
string host = dlgContents.FindViewById<EditText>(Resource.Id.sftp_host).Text;
|
||||
string portText = dlgContents.FindViewById<EditText>(Resource.Id.sftp_port).Text;
|
||||
int port = SftpStorage.DefaultSftpPort;
|
||||
if (!string.IsNullOrEmpty(portText))
|
||||
int.TryParse(portText, out port);
|
||||
string user = dlgContents.FindViewById<EditText>(Resource.Id.sftp_user).Text;
|
||||
|
||||
string sftpPath = new SftpStorage(activity.ApplicationContext)
|
||||
.BuildFullPath(host, port, initialPath, user, password, connectTimeout);
|
||||
onStartBrowse(sftpPath);
|
||||
});
|
||||
EventHandler<DialogClickEventArgs> evtH = new EventHandler<DialogClickEventArgs>((sender, e) => onCancel());
|
||||
string password = null;
|
||||
string keyName = null, keyPassphrase = null;
|
||||
switch (dlgContents.FindViewById<Spinner>(Resource.Id.sftp_auth_mode_spinner).SelectedItemPosition)
|
||||
{
|
||||
case SftpModeSpinnerPasswd:
|
||||
password = dlgContents.FindViewById<EditText>(Resource.Id.sftp_password).Text;
|
||||
break;
|
||||
case SftpModeSpinnerPubKey:
|
||||
break;
|
||||
case SftpModeSpinnerCustomKey:
|
||||
keyName = ResolveSelectedKeyName(keyNamesAdapter, keySpinner.SelectedItemPosition);
|
||||
keyPassphrase = dlgContents.FindViewById<EditText>(Resource.Id.sftp_key_passphrase).Text;
|
||||
break;
|
||||
}
|
||||
|
||||
string initialPath = dlgContents.FindViewById<EditText>(Resource.Id.sftp_initial_dir).Text;
|
||||
if (string.IsNullOrEmpty(initialPath))
|
||||
initialPath = "/";
|
||||
string connectTimeoutText = dlgContents.FindViewById<EditText>(Resource.Id.sftp_connect_timeout).Text;
|
||||
int connectTimeout = SftpStorage.UnsetSftpConnectTimeout;
|
||||
if (!string.IsNullOrEmpty(connectTimeoutText))
|
||||
{
|
||||
int.TryParse(connectTimeoutText, out connectTimeout);
|
||||
}
|
||||
|
||||
string sftpPath = fileStorage.BuildFullPath(
|
||||
host, port, initialPath, user, password, connectTimeout, keyName, keyPassphrase);
|
||||
|
||||
onStartBrowse(sftpPath);
|
||||
});
|
||||
EventHandler<DialogClickEventArgs> evtH = new EventHandler<DialogClickEventArgs>((sender, e) => onCancel());
|
||||
|
||||
builder.SetNegativeButton(Android.Resource.String.Cancel, evtH);
|
||||
builder.SetTitle(activity.GetString(Resource.String.enter_sftp_login_title));
|
||||
@@ -139,9 +262,41 @@ namespace keepass2android
|
||||
|
||||
dialog.Show();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowHttpDialog(Activity activity, Util.FileSelectedHandler onStartBrowse, Action onCancel, string defaultPath)
|
||||
#if !EXCLUDE_JAVAFILESTORAGE && !NoNet
|
||||
private void UpdatePrivateKeyNames(ArrayAdapter dataView, SftpStorage storage, Context ctx)
|
||||
{
|
||||
dataView.Clear();
|
||||
dataView.Add(ctx.GetString(Resource.String.private_key_create_new));
|
||||
foreach (string keyName in storage.GetCustomKeyNames())
|
||||
dataView.Add(keyName);
|
||||
}
|
||||
|
||||
private int ResolveKeySpinnerSelection(ArrayAdapter dataView, string keyName)
|
||||
{
|
||||
int idx = -1;
|
||||
for (int i = 0; i < dataView.Count; i++)
|
||||
{
|
||||
string itemName = dataView.GetItem(i).ToString();
|
||||
if (string.Equals(keyName, itemName)) {
|
||||
idx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return idx < 0 ? SftpKeySpinnerCreateNewIdx : idx;
|
||||
}
|
||||
|
||||
private string ResolveSelectedKeyName(ArrayAdapter dataView, int selectedItem)
|
||||
{
|
||||
if (selectedItem != SftpKeySpinnerCreateNewIdx && selectedItem > 0 && selectedItem < dataView.Count)
|
||||
return dataView.GetItem(selectedItem).ToString();
|
||||
else
|
||||
return null;
|
||||
}
|
||||
#endif
|
||||
|
||||
private void ShowHttpDialog(Activity activity, Util.FileSelectedHandler onStartBrowse, Action onCancel, string defaultPath)
|
||||
{
|
||||
#if !EXCLUDE_JAVAFILESTORAGE && !NoNet
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
|
||||
|
||||
@@ -61,17 +61,80 @@
|
||||
</LinearLayout>
|
||||
|
||||
<EditText
|
||||
android:id="@+id/sftp_password"
|
||||
android:id="@+id/sftp_password"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textPassword"
|
||||
android:singleLine="true"
|
||||
android:hint="@string/hint_pass"
|
||||
android:importantForAccessibility="no"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/send_public_key_button"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/send_public_key" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/sftp_keyfile_group"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content">
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/private_key_select" />
|
||||
<Spinner
|
||||
android:id="@+id/sftp_key_names"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="3dp" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/sftp_add_key_group"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content">
|
||||
<EditText android:id="@+id/sftp_key_name"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/hint_sftp_key_name" />
|
||||
<EditText
|
||||
android:id="@+id/sftp_key_content"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textMultiLine"
|
||||
android:minLines="1"
|
||||
android:maxLines="6"
|
||||
android:hint="@string/hint_sftp_key_content" />
|
||||
<Button
|
||||
android:id="@+id/sftp_save_key_button"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/save_key" />
|
||||
</LinearLayout>
|
||||
<Button
|
||||
android:id="@+id/sftp_delete_key_button"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/delete_key" />
|
||||
<EditText
|
||||
android:id="@+id/sftp_key_passphrase"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textPassword"
|
||||
android:singleLine="true"
|
||||
android:hint="@string/hint_pass"
|
||||
android:importantForAccessibility="no"/>
|
||||
<Button android:id="@+id/send_public_key_button"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/send_public_key" />
|
||||
android:inputType="textPassword"
|
||||
android:hint="@string/hint_sftp_key_passphrase" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
<TextView android:id="@+id/initial_dir"
|
||||
|
||||
@@ -167,6 +167,7 @@
|
||||
<string name="hint_keyfile">key file</string>
|
||||
<string name="hint_length">length</string>
|
||||
<string name="hint_pass">password</string>
|
||||
<string name="hint_keyfile_path">SSH private key path</string>
|
||||
<string name="hint_login_pass">Password</string>
|
||||
<string name="hint_title">name</string>
|
||||
<string name="hint_url">URL</string>
|
||||
@@ -590,6 +591,19 @@
|
||||
<string name="enter_sftp_login_title">Enter SFTP login data:</string>
|
||||
<string name="sftp_auth_mode">Authentication mode</string>
|
||||
<string name="send_public_key">Send public key...</string>
|
||||
<string name="select_private_keyfile">Select private key...</string>
|
||||
<string name="hint_sftp_key_name">New key name</string>
|
||||
<string name="hint_sftp_key_content">New key content</string>
|
||||
<string name="private_key_saved">Private key saved</string>
|
||||
<string name="private_key_save_failed">FAILED to save private key: %1$s</string>
|
||||
<string name="private_key_info">Enter key name and content to save</string>
|
||||
<string name="private_key_delete">Deleted private key: %1$s</string>
|
||||
<string name="private_key_delete_failed">FAILED to deleted private key: %1$s</string>
|
||||
<string name="save_key">Save Private Key</string>
|
||||
<string name="delete_key">Delete Private Key</string>
|
||||
<string name="private_key_select">Selected private key</string>
|
||||
<string name="private_key_create_new">[Add New...]</string>
|
||||
<string name="hint_sftp_key_passphrase">Key passphrase (optional)</string>
|
||||
|
||||
<string name="enter_ftp_login_title">Enter FTP login data:</string>
|
||||
|
||||
@@ -1322,7 +1336,8 @@ Initial public release
|
||||
</string-array>
|
||||
<string-array name="sftp_auth_modes">
|
||||
<item>Password</item>
|
||||
<item>Private/Public key</item>
|
||||
<item>K2A Private/Public key</item>
|
||||
<item>Custom Private key</item>
|
||||
</string-array>
|
||||
<string-array name="AcceptAllServerCertificates_options">
|
||||
<item>Ignore certificate validation failures</item>
|
||||
|
||||
Reference in New Issue
Block a user