Merge pull request #2386 from hyproman/bug-2366-WIP-ssh-custom-alg-cfg
Bug 2366 SSH Custom Algorithms Configuration
This commit is contained in:
@@ -58,12 +58,12 @@ namespace keepass2android
|
||||
|
||||
}
|
||||
|
||||
private static string LogFilename
|
||||
public static string LogFilename
|
||||
{
|
||||
get { return Application.Context.FilesDir.CanonicalPath +"/keepass2android.log"; }
|
||||
}
|
||||
|
||||
private static bool LogToFile
|
||||
public static bool LogToFile
|
||||
{
|
||||
get
|
||||
{
|
||||
|
||||
@@ -97,7 +97,28 @@ public class FileEntry {
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder s = new StringBuilder("JavaFileStorage.FileEntry{").append(displayName).append("|")
|
||||
.append("path=").append(path).append(",sz=").append(sizeInBytes)
|
||||
.append(",").append(isDirectory ? "dir" : "file")
|
||||
.append(",lastMod=").append(lastModifiedTime);
|
||||
|
||||
StringBuilder perms = new StringBuilder();
|
||||
if (canRead)
|
||||
perms.append("r");
|
||||
if (canWrite)
|
||||
perms.append("w");
|
||||
if (perms.length() > 0) {
|
||||
s.append(",").append(perms);
|
||||
}
|
||||
|
||||
if (userData != null && userData.length() > 0)
|
||||
s.append(",userData=").append(userData);
|
||||
|
||||
return s.append("}").toString();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
package keepass2android.javafilestorage;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import com.jcraft.jsch.Logger;
|
||||
|
||||
import java.io.FileWriter;
|
||||
import java.io.PrintWriter;
|
||||
import java.util.Map;
|
||||
|
||||
public class Kp2aJSchLogger implements Logger {
|
||||
|
||||
private static final String PREFIX = "KP2AJFS[JSch]";
|
||||
|
||||
private interface ILogger {
|
||||
void log(String message);
|
||||
}
|
||||
|
||||
private interface EntryToLogFactory {
|
||||
ILogger create(LogEntry e);
|
||||
}
|
||||
|
||||
private static final EntryToLogFactory ANDROID_FACTORY = e -> e.logger;
|
||||
|
||||
private static final class LogEntry {
|
||||
private final String levelTag;
|
||||
private final ILogger logger;
|
||||
|
||||
LogEntry(String levelTag, ILogger logger) {
|
||||
this.levelTag = levelTag;
|
||||
this.logger = logger;
|
||||
}
|
||||
}
|
||||
private static final ILogger DEBUG = msg -> Log.d(PREFIX, msg);
|
||||
private static final LogEntry DEBUG_ENTRY = new LogEntry("D", DEBUG);
|
||||
private static final ILogger ERROR = msg -> Log.e(PREFIX, msg);
|
||||
private static final LogEntry DEFAULT_ENTRY = DEBUG_ENTRY;
|
||||
|
||||
private static final Map<Integer, LogEntry> loggers = Map.of(
|
||||
Logger.DEBUG, DEBUG_ENTRY,
|
||||
Logger.INFO, new LogEntry("I", msg -> Log.i(PREFIX, msg)),
|
||||
Logger.WARN, new LogEntry("W", msg -> Log.w(PREFIX, msg)),
|
||||
Logger.ERROR, new LogEntry("E", ERROR),
|
||||
Logger.FATAL, new LogEntry("F", msg -> Log.wtf(PREFIX, msg))
|
||||
);
|
||||
|
||||
|
||||
private final EntryToLogFactory logFactory;
|
||||
|
||||
static Kp2aJSchLogger createAndroidLogger() {
|
||||
return new Kp2aJSchLogger(ANDROID_FACTORY);
|
||||
}
|
||||
|
||||
static Kp2aJSchLogger createFileLogger(String logFilename) {
|
||||
final String fName = logFilename;
|
||||
return new Kp2aJSchLogger(e -> createFileLogger(e, fName));
|
||||
}
|
||||
|
||||
private Kp2aJSchLogger(EntryToLogFactory logFactory) {
|
||||
this.logFactory = logFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabled(int level) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(int level, String message) {
|
||||
if (isEnabled(level))
|
||||
getLogger(level).log(message);
|
||||
}
|
||||
|
||||
private ILogger getLogger(int level) {
|
||||
LogEntry entry = loggers.get(level);
|
||||
if (entry == null)
|
||||
entry = DEFAULT_ENTRY;
|
||||
|
||||
return logFactory.create(entry);
|
||||
}
|
||||
|
||||
private static ILogger createFileLogger(LogEntry entry, String fName) {
|
||||
try {
|
||||
final PrintWriter p = new PrintWriter(new FileWriter(fName, true));
|
||||
return msg -> {
|
||||
try {
|
||||
String fullMsg = String.join(" ", entry.levelTag, PREFIX, msg);
|
||||
p.println(fullMsg);
|
||||
} catch (Exception e) {
|
||||
ERROR.log(e.getMessage());
|
||||
} finally {
|
||||
p.close();
|
||||
}
|
||||
};
|
||||
} catch (Exception e) {
|
||||
ERROR.log(e.getMessage());
|
||||
return entry.logger;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,20 +2,24 @@ 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;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
|
||||
import com.jcraft.jsch.Channel;
|
||||
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.Logger;
|
||||
import com.jcraft.jsch.Session;
|
||||
import com.jcraft.jsch.SftpATTRS;
|
||||
import com.jcraft.jsch.SftpException;
|
||||
@@ -26,10 +30,38 @@ import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
|
||||
@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;
|
||||
JSch jsch;
|
||||
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";
|
||||
|
||||
public static final String SSH_CFG_KEX = "kex";
|
||||
public static final String SSH_CFG_SERVER_HOST_KEY = "server_host_key";
|
||||
private static final Set<String> SSH_CFG_CSV_EXPANDABLE = Set.of(SSH_CFG_KEX, SSH_CFG_SERVER_HOST_KEY);
|
||||
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;
|
||||
|
||||
private static final String TAG = "KP2AJFS";
|
||||
private static final String THREAD_TAG = TAG + "[thread]";
|
||||
private JSch jsch;
|
||||
|
||||
public class ConnectionInfo
|
||||
{
|
||||
@@ -37,13 +69,42 @@ 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 final Map<String, String> configOpts = new HashMap<>();
|
||||
|
||||
|
||||
public String toString() {
|
||||
return "ConnectionInfo{host=" + host + ",port=" + port + ",user=" + username +
|
||||
",pwd=<hidden>,localPath=" + localPath + ",key=" + keyName +
|
||||
",phrase=<hidden>,connectTimeout=" + connectTimeoutSec +
|
||||
",cfgOpts=" + configOpts +
|
||||
"}";
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
// Assume all config options are not sensitive and use the same resolver...
|
||||
for (Map.Entry<String, String> entry : ci.configOpts.entrySet()) {
|
||||
b.addOption(entry.getKey(), entry.getValue(), nonBlankStringResolver);
|
||||
}
|
||||
if (includeSensitive) {
|
||||
b.addOption(SFTP_KEYPASSPHRASE_OPTION_NAME, ci.keyPassphrase, nonBlankStringResolver);
|
||||
}
|
||||
return b.build();
|
||||
}
|
||||
|
||||
Context _appContext;
|
||||
private final SftpPublicPrivateKeyUtils _keyUtils;
|
||||
|
||||
public SftpStorage(Context appContext) {
|
||||
_appContext = appContext;
|
||||
|
||||
_keyUtils = new SftpPublicPrivateKeyUtils(getBaseDir());
|
||||
}
|
||||
|
||||
private static final String SFTP_PROTOCOL_ID = "sftp";
|
||||
@@ -65,15 +126,15 @@ public class SftpStorage extends JavaFileStorageBase {
|
||||
|
||||
@Override
|
||||
public InputStream openFileForRead(String path) throws Exception {
|
||||
|
||||
ChannelSftp c = init(path);
|
||||
ConnectionInfo cInfo = splitStringToConnectionInfo(path);
|
||||
ChannelSftp c = init(cInfo);
|
||||
|
||||
try {
|
||||
byte[] buff = new byte[8000];
|
||||
|
||||
int bytesRead = 0;
|
||||
|
||||
InputStream in = c.get(extractSessionPath(path));
|
||||
InputStream in = c.get(cInfo.localPath);
|
||||
ByteArrayOutputStream bao = new ByteArrayOutputStream();
|
||||
|
||||
while ((bytesRead = in.read(buff)) != -1) {
|
||||
@@ -105,14 +166,15 @@ public class SftpStorage extends JavaFileStorageBase {
|
||||
public void uploadFile(String path, byte[] data, boolean writeTransactional)
|
||||
throws Exception {
|
||||
|
||||
ChannelSftp c = init(path);
|
||||
ConnectionInfo cInfo = splitStringToConnectionInfo(path);
|
||||
ChannelSftp c = init(cInfo);
|
||||
try {
|
||||
InputStream in = new ByteArrayInputStream(data);
|
||||
String targetPath = extractSessionPath(path);
|
||||
String targetPath = cInfo.localPath;
|
||||
if (writeTransactional)
|
||||
{
|
||||
//upload to temporary location:
|
||||
String tmpPath = targetPath+".tmp";
|
||||
String tmpPath = targetPath + ".tmp";
|
||||
c.put(in, tmpPath);
|
||||
//remove previous file:
|
||||
try
|
||||
@@ -128,9 +190,9 @@ public class SftpStorage extends JavaFileStorageBase {
|
||||
}
|
||||
else
|
||||
{
|
||||
c.put(in, targetPath);
|
||||
c.put(in, targetPath);
|
||||
}
|
||||
|
||||
|
||||
tryDisconnect(c);
|
||||
} catch (Exception e) {
|
||||
tryDisconnect(c);
|
||||
@@ -142,53 +204,98 @@ public class SftpStorage extends JavaFileStorageBase {
|
||||
@Override
|
||||
public String createFolder(String parentPath, String newDirName)
|
||||
throws Exception {
|
||||
|
||||
ConnectionInfo cInfo = splitStringToConnectionInfo(parentPath);
|
||||
try {
|
||||
ChannelSftp c = init(parentPath);
|
||||
String newPath = concatPaths(parentPath, newDirName);
|
||||
c.mkdir(extractSessionPath(newPath));
|
||||
ChannelSftp c = init(cInfo);
|
||||
String newPath = concatPaths(cInfo.localPath, newDirName);
|
||||
c.mkdir(newPath);
|
||||
tryDisconnect(c);
|
||||
return newPath;
|
||||
|
||||
return buildFullPath(cInfo.host, cInfo.port, newPath,
|
||||
cInfo.username, cInfo.password, cInfo.connectTimeoutSec,
|
||||
cInfo.keyName, cInfo.keyPassphrase,
|
||||
cInfo.configOpts.get(SSH_CFG_KEX),
|
||||
cInfo.configOpts.get(SSH_CFG_SERVER_HOST_KEY));
|
||||
} catch (Exception e) {
|
||||
throw convertException(e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private String extractUserPwdHostPort(String path) {
|
||||
String withoutProtocol = path
|
||||
.substring(getProtocolPrefix().length());
|
||||
return withoutProtocol.substring(0, withoutProtocol.indexOf("/"));
|
||||
}
|
||||
|
||||
private String extractSessionPath(String newPath) {
|
||||
String withoutProtocol = newPath
|
||||
.substring(getProtocolPrefix().length());
|
||||
return withoutProtocol.substring(withoutProtocol.indexOf("/"));
|
||||
int pathStartIdx = withoutProtocol.indexOf("/");
|
||||
int pathEndIdx = withoutProtocol.indexOf("?");
|
||||
if (pathEndIdx < 0) {
|
||||
pathEndIdx = withoutProtocol.length();
|
||||
}
|
||||
return withoutProtocol.substring(pathStartIdx, pathEndIdx);
|
||||
}
|
||||
|
||||
private String extractUserPwdHost(String path) {
|
||||
|
||||
private Map<String, String> extractOptionsMap(String path) throws UnsupportedEncodingException {
|
||||
String withoutProtocol = path
|
||||
.substring(getProtocolPrefix().length());
|
||||
return withoutProtocol.substring(0,withoutProtocol.indexOf("/"));
|
||||
|
||||
Map<String, String> options = new HashMap<>();
|
||||
|
||||
int extraOptsIdx = withoutProtocol.indexOf("?");
|
||||
if (extraOptsIdx > 0 && extraOptsIdx + 1 < withoutProtocol.length()) {
|
||||
String optsString = withoutProtocol.substring(extraOptsIdx + 1);
|
||||
String[] parts = optsString.split("&");
|
||||
for (String p : parts) {
|
||||
int sepIdx = p.indexOf('=');
|
||||
if (sepIdx > 0) {
|
||||
String key = decode(p.substring(0, sepIdx));
|
||||
String value = decode(p.substring(sepIdx + 1));
|
||||
options.put(key, value);
|
||||
} else {
|
||||
options.put(decode(p), "true");
|
||||
}
|
||||
}
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
private String concatPaths(String parentPath, String newDirName) {
|
||||
String res = parentPath;
|
||||
if (!res.endsWith("/"))
|
||||
res += "/";
|
||||
res += newDirName;
|
||||
return res;
|
||||
StringBuilder fp = new StringBuilder(parentPath);
|
||||
if (!parentPath.endsWith("/"))
|
||||
fp.append("/");
|
||||
return fp.append(newDirName).toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String createFilePath(String parentPath, String newFileName)
|
||||
public String createFilePath(final String parentUri, String newFileName)
|
||||
throws Exception {
|
||||
if (parentPath.endsWith("/") == false)
|
||||
parentPath += "/";
|
||||
return parentPath + newFileName;
|
||||
|
||||
String parentPath = parentUri;
|
||||
String params = null;
|
||||
int paramsIdx = parentUri.lastIndexOf("?");
|
||||
if (paramsIdx > 0) {
|
||||
params = parentUri.substring(paramsIdx);
|
||||
parentPath = parentPath.substring(0, paramsIdx);
|
||||
}
|
||||
|
||||
String newPath = concatPaths(parentPath, newFileName);
|
||||
|
||||
if (params != null) {
|
||||
newPath += params;
|
||||
}
|
||||
return newPath;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<FileEntry> listFiles(String parentPath) throws Exception {
|
||||
ConnectionInfo cInfo = splitStringToConnectionInfo(parentPath);
|
||||
ChannelSftp c = init(cInfo);
|
||||
|
||||
ChannelSftp c = init(parentPath);
|
||||
return listFiles(parentPath, c);
|
||||
|
||||
}
|
||||
|
||||
private void setFromAttrs(FileEntry fileEntry, SftpATTRS attrs) {
|
||||
@@ -212,23 +319,27 @@ public class SftpStorage extends JavaFileStorageBase {
|
||||
if (sftpEx.id == ChannelSftp.SSH_FX_NO_SUCH_FILE)
|
||||
return new FileNotFoundException(sftpEx.getMessage());
|
||||
}
|
||||
|
||||
|
||||
return e;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileEntry getFileEntry(String filename) throws Exception {
|
||||
|
||||
ChannelSftp c = init(filename);
|
||||
ConnectionInfo cInfo = splitStringToConnectionInfo(filename);
|
||||
ChannelSftp c = init(cInfo);
|
||||
try {
|
||||
FileEntry fileEntry = new FileEntry();
|
||||
String sessionPath = extractSessionPath(filename);
|
||||
SftpATTRS attr = c.stat(sessionPath);
|
||||
SftpATTRS attr = c.stat(cInfo.localPath);
|
||||
setFromAttrs(fileEntry, attr);
|
||||
|
||||
// Full URI
|
||||
fileEntry.path = filename;
|
||||
fileEntry.displayName = getFilename(sessionPath);
|
||||
|
||||
fileEntry.displayName = getFilename(cInfo.localPath);
|
||||
|
||||
tryDisconnect(c);
|
||||
|
||||
return fileEntry;
|
||||
} catch (Exception e) {
|
||||
logDebug("Exception in getFileEntry! " + e);
|
||||
@@ -239,8 +350,9 @@ public class SftpStorage extends JavaFileStorageBase {
|
||||
|
||||
@Override
|
||||
public void delete(String path) throws Exception {
|
||||
ConnectionInfo cInfo = splitStringToConnectionInfo(path);
|
||||
ChannelSftp c = init(cInfo);
|
||||
|
||||
ChannelSftp c = init(path);
|
||||
delete(path, c);
|
||||
}
|
||||
|
||||
@@ -264,10 +376,11 @@ public class SftpStorage extends JavaFileStorageBase {
|
||||
tryDisconnect(c);
|
||||
throw convertException(e);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
private List<FileEntry> listFiles(String path, ChannelSftp c) throws Exception {
|
||||
|
||||
try {
|
||||
List<FileEntry> res = new ArrayList<FileEntry>();
|
||||
@SuppressWarnings("rawtypes")
|
||||
@@ -283,7 +396,7 @@ public class SftpStorage extends JavaFileStorageBase {
|
||||
||(lsEntry.getFilename().equals(".."))
|
||||
)
|
||||
continue;
|
||||
|
||||
|
||||
FileEntry fileEntry = new FileEntry();
|
||||
fileEntry.displayName = lsEntry.getFilename();
|
||||
fileEntry.path = createFilePath(path, fileEntry.displayName);
|
||||
@@ -313,97 +426,161 @@ public class SftpStorage extends JavaFileStorageBase {
|
||||
throws UnsupportedEncodingException {
|
||||
return java.net.URLDecoder.decode(encodedString, UTF_8);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected String encode(final String unencoded)
|
||||
throws UnsupportedEncodingException {
|
||||
return java.net.URLEncoder.encode(unencoded, UTF_8);
|
||||
}
|
||||
|
||||
ChannelSftp init(String filename) throws JSchException, UnsupportedEncodingException {
|
||||
jsch = new JSch();
|
||||
ConnectionInfo ci = splitStringToConnectionInfo(filename);
|
||||
|
||||
Log.d("KP2AJFS", "init SFTP");
|
||||
|
||||
ChannelSftp init(ConnectionInfo cInfo) throws JSchException, UnsupportedEncodingException {
|
||||
jsch = new JSch();
|
||||
|
||||
Log.d(TAG, "init SFTP");
|
||||
|
||||
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) {
|
||||
|
||||
}
|
||||
|
||||
Log.e("KP2AJFS[thread]", "getting session...");
|
||||
Session session = jsch.getSession(ci.username, ci.host, ci.port);
|
||||
Log.e("KP2AJFS", "creating SftpUserInfo");
|
||||
UserInfo ui = new SftpUserInfo(ci.password,_appContext);
|
||||
session.setUserInfo(ui);
|
||||
Log.e(THREAD_TAG, "getting session...");
|
||||
Session session = jsch.getSession(cInfo.username, cInfo.host, cInfo.port);
|
||||
|
||||
session.setConfig("PreferredAuthentications", "publickey,password");
|
||||
|
||||
session.connect();
|
||||
sessionConfigure(session, cInfo);
|
||||
sessionConnect(session, cInfo);
|
||||
|
||||
Channel channel = session.openChannel("sftp");
|
||||
channel.connect();
|
||||
ChannelSftp c = (ChannelSftp) channel;
|
||||
|
||||
logDebug("success: init Sftp");
|
||||
return c;
|
||||
|
||||
}
|
||||
|
||||
private void sessionConnect(Session session, ConnectionInfo cInfo) throws JSchException {
|
||||
if (cInfo.connectTimeoutSec != UNSET_SFTP_CONNECT_TIMEOUT) {
|
||||
session.connect(cInfo.connectTimeoutSec * 1000);
|
||||
} else {
|
||||
session.connect();
|
||||
}
|
||||
}
|
||||
|
||||
private void sessionConfigure(Session session, ConnectionInfo cInfo) {
|
||||
Log.e(TAG, "creating SftpUserInfo");
|
||||
UserInfo ui = new SftpUserInfo(cInfo.password, cInfo.keyPassphrase, _appContext);
|
||||
session.setUserInfo(ui);
|
||||
|
||||
session.setConfig("PreferredAuthentications", "publickey,password");
|
||||
|
||||
for (Map.Entry<String, String> e : cInfo.configOpts.entrySet()) {
|
||||
String cfgKey = e.getKey();
|
||||
String before = session.getConfig(cfgKey);
|
||||
String after = e.getValue();
|
||||
|
||||
if (SSH_CFG_CSV_EXPANDABLE.contains(cfgKey)) {
|
||||
SshConfigCsvValueResolver resolver = new SshConfigCsvValueResolver(cfgKey, after);
|
||||
after = resolver.resolve(before);
|
||||
}
|
||||
session.setConfig(cfgKey, after);
|
||||
}
|
||||
}
|
||||
|
||||
private String getBaseDir() {
|
||||
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);
|
||||
@SuppressWarnings("unused") // Exposed by JavaFileStorageBindings
|
||||
public void savePrivateKeyContent(String keyName, String keyContent) throws IOException, Exception {
|
||||
_keyUtils.savePrivateKeyContent(keyName, keyContent);
|
||||
}
|
||||
|
||||
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 setJschLogging(boolean enabled, String logFilename) {
|
||||
Logger impl = null;
|
||||
if (enabled) {
|
||||
if (logFilename != null) {
|
||||
impl = Kp2aJSchLogger.createFileLogger(logFilename);
|
||||
} else {
|
||||
impl = Kp2aJSchLogger.createAndroidLogger();
|
||||
}
|
||||
}
|
||||
JSch.setLogger(impl);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Exposed for testing purposes only.
|
||||
* @param currentValues
|
||||
* @param spec
|
||||
* @return
|
||||
* @throws Exception
|
||||
*/
|
||||
public String resolveCsvValues(String currentValues, String spec) {
|
||||
return new SshConfigCsvValueResolver("test", spec)
|
||||
.resolve(currentValues);
|
||||
}
|
||||
|
||||
public ConnectionInfo splitStringToConnectionInfo(String filename)
|
||||
throws UnsupportedEncodingException {
|
||||
|
||||
ConnectionInfo ci = new ConnectionInfo();
|
||||
ci.host = extractUserPwdHost(filename);
|
||||
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.lastIndexOf(":");
|
||||
|
||||
int portSeparatorIndex = ci.host.lastIndexOf(':');
|
||||
if (portSeparatorIndex >= 0)
|
||||
{
|
||||
ci.port = Integer.parseInt(ci.host.substring(portSeparatorIndex+1));
|
||||
ci.port = Integer.parseInt(ci.host.substring(portSeparatorIndex + 1));
|
||||
ci.host = ci.host.substring(0, portSeparatorIndex);
|
||||
}
|
||||
// Encode/decode required to support IPv6 (colons break host:port parse logic)
|
||||
@@ -411,6 +588,30 @@ public class SftpStorage extends JavaFileStorageBase {
|
||||
ci.host = decode(ci.host);
|
||||
|
||||
ci.localPath = extractSessionPath(filename);
|
||||
|
||||
Map<String, String> options = extractOptionsMap(filename);
|
||||
|
||||
if (options.containsKey(SFTP_CONNECT_TIMEOUT_OPTION_NAME)) {
|
||||
String optVal = options.get(SFTP_CONNECT_TIMEOUT_OPTION_NAME);
|
||||
try {
|
||||
ci.connectTimeoutSec = Integer.parseInt(optVal);
|
||||
} catch (NumberFormatException nan) {
|
||||
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);
|
||||
}
|
||||
|
||||
for (String cfgKey : SSH_CFG_CSV_EXPANDABLE) {
|
||||
if (options.containsKey(cfgKey)) {
|
||||
ci.configOpts.put(cfgKey, options.get(cfgKey));
|
||||
}
|
||||
}
|
||||
|
||||
return ci;
|
||||
}
|
||||
|
||||
@@ -447,12 +648,18 @@ public class SftpStorage extends JavaFileStorageBase {
|
||||
try
|
||||
{
|
||||
ConnectionInfo ci = splitStringToConnectionInfo(path);
|
||||
return getProtocolPrefix()+ci.username+"@"+ci.host+ci.localPath;
|
||||
StringBuilder dName = new StringBuilder(getProtocolPrefix())
|
||||
.append(ci.username)
|
||||
.append("@")
|
||||
.append(ci.host)
|
||||
.append(ci.localPath);
|
||||
appendOptions(dName, buildOptionMap(ci, false));
|
||||
return dName.toString();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return extractSessionPath(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -474,26 +681,105 @@ public class SftpStorage extends JavaFileStorageBase {
|
||||
@Override
|
||||
public void onActivityResult(FileStorageSetupActivity activity,
|
||||
int requestCode, int resultCode, Intent data) {
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
public String buildFullPath( String host, int port, String localPath, String username, String password) throws UnsupportedEncodingException
|
||||
{
|
||||
public String buildFullPath(String host, int port, String localPath,
|
||||
String username, String password,
|
||||
int connectTimeoutSec,
|
||||
String keyName, String keyPassphrase,
|
||||
String kexAlgorithms, String shkAlgorithms)
|
||||
throws UnsupportedEncodingException {
|
||||
|
||||
StringBuilder uri = new StringBuilder(getProtocolPrefix()).append(encode(username));
|
||||
if (password != null) {
|
||||
uri.append(":").append(encode(password));
|
||||
}
|
||||
uri.append("@");
|
||||
// Encode/decode required to support IPv6 (colons break host:port parse logic)
|
||||
// See Bug #2350
|
||||
host = encode(host);
|
||||
uri.append(encode(host));
|
||||
|
||||
if (port != DEFAULT_SFTP_PORT)
|
||||
host += ":"+String.valueOf(port);
|
||||
return getProtocolPrefix()+encode(username)+":"+encode(password)+"@"+host+localPath;
|
||||
|
||||
if (port != DEFAULT_SFTP_PORT) {
|
||||
uri.append(":").append(port);
|
||||
}
|
||||
if (localPath != null && localPath.startsWith("/")) {
|
||||
uri.append(localPath);
|
||||
}
|
||||
|
||||
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)
|
||||
.addOption(SSH_CFG_KEX, kexAlgorithms, nonBlankStringResolver)
|
||||
.addOption(SSH_CFG_SERVER_HOST_KEY, shkAlgorithms, nonBlankStringResolver)
|
||||
.build());
|
||||
|
||||
return uri.toString();
|
||||
}
|
||||
|
||||
private void appendOptions(StringBuilder uri, Map<String, String> opts)
|
||||
throws UnsupportedEncodingException {
|
||||
|
||||
boolean first = true;
|
||||
// Sort for stability/consistency
|
||||
Set<Map.Entry<String, String>> sortedEntries = new TreeSet<>(new EntryComparator<>());
|
||||
sortedEntries.addAll(opts.entrySet());
|
||||
for (Map.Entry<String, String> me : sortedEntries) {
|
||||
if (first) {
|
||||
uri.append("?");
|
||||
first = false;
|
||||
} else {
|
||||
uri.append("&");
|
||||
}
|
||||
uri.append(encode(me.getKey())).append("=").append(encode(me.getValue()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void prepareFileUsage(Context appContext, String path) {
|
||||
//nothing to do
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* A comparator that compares Map.Entry objects by their keys, via natural ordering.
|
||||
*
|
||||
* @param <T> the Map.Entry key type, that must implement Comparable.
|
||||
*/
|
||||
private static class EntryComparator<T extends Comparable<T>> implements Comparator<Map.Entry<T, ?>> {
|
||||
@Override
|
||||
public int compare(Map.Entry<T, ?> o1, Map.Entry<T, ?> o2) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,17 +116,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
|
||||
@@ -137,12 +139,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
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
package keepass2android.javafilestorage;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A class that manipulates CSV String values based on a list of CSV "spec" definitions, where each definition
|
||||
* can describe one of the following:
|
||||
*
|
||||
* - Prepend to existing list: +something
|
||||
* - Append to end of existing list: something+
|
||||
* - Remove a specific value: -something
|
||||
* - Remove values matching prefix: -something*
|
||||
* - Remove values matching suffix: -*something
|
||||
* - Remove values matching substring: -*something*
|
||||
* - Remove values matching prefix and suffix: -some*thing
|
||||
*
|
||||
* Otherwise CSV of values completely replace original config values
|
||||
*
|
||||
* Examples:
|
||||
* <code>
|
||||
* var r = new SshConfigCsvValueResolver("foo", "addToEnd+,-remove*,+addToBeginning,-*del*");
|
||||
* r.resolve("one,removeTwo,three,removeThree,four") --> "addToBeginning,one,three,four,addToEnd"
|
||||
* r.resolve("one,my-del,del-me,two,foodelbar,three") --> "addToBeginning,one,two,three,addToEnd"
|
||||
*
|
||||
* r = new SshConfigCsvValueResolver("foo", "replace,the,config");
|
||||
* r.resolve("one,two,three,four") --> "replace,the,config"
|
||||
* </code>
|
||||
*
|
||||
*/
|
||||
class SshConfigCsvValueResolver {
|
||||
interface Matcher {
|
||||
boolean matches(String s);
|
||||
}
|
||||
private final String cfgKey;
|
||||
private static final String TAG = "KP2AJFS[sshcfg]";
|
||||
|
||||
private static final String DELIM = ",";
|
||||
private static final char ADD = '+';
|
||||
private static final char REMOVE = '-';
|
||||
private static final char WILD = '*';
|
||||
private final List<String> prepends;
|
||||
private final List<String> appends;
|
||||
private final List<Matcher> removes;
|
||||
private final List<String> replaces;
|
||||
|
||||
/**
|
||||
* Creates a new resolver.
|
||||
*
|
||||
* @param cfgKey - configuration key name (used for logging)
|
||||
* @param incomingSpec - A CSV String of "spec" definitions that will be used to
|
||||
* (potentially) modify incoming CSV String values.
|
||||
*/
|
||||
SshConfigCsvValueResolver(String cfgKey, String incomingSpec) {
|
||||
List<String> prepends = new ArrayList<>();
|
||||
List<String> appends = new ArrayList<>();
|
||||
List<Matcher> removes = new ArrayList<>();
|
||||
List<String> replaces = new ArrayList<>();
|
||||
|
||||
for (String iVal : incomingSpec.split(DELIM)) {
|
||||
if (iVal.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
int evLen = iVal.length();
|
||||
if (iVal.charAt(0) == ADD && evLen > 1) {
|
||||
prepends.add(iVal.substring(1));
|
||||
} else if (iVal.charAt(iVal.length() - 1) == ADD && evLen > 1) {
|
||||
appends.add(iVal.substring(0, evLen - 1));
|
||||
} else if (iVal.charAt(iVal.length() - 1) == REMOVE && evLen > 1) {
|
||||
removes.add(createMatcher(iVal.substring(1)));
|
||||
} else {
|
||||
// This looks like a straight replace
|
||||
replaces.add(iVal);
|
||||
}
|
||||
}
|
||||
this.cfgKey = cfgKey;
|
||||
this.prepends = Collections.unmodifiableList(prepends);
|
||||
this.appends = Collections.unmodifiableList(appends);
|
||||
this.removes = Collections.unmodifiableList(removes);
|
||||
this.replaces = Collections.unmodifiableList(replaces);
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a CSV String and (potentially) modifies it according to the "spec" entries of this resolver.
|
||||
*
|
||||
* @param existingValues - the original CSV String
|
||||
* @return an updated representation of <code>existingValues</code>, based on the defined "spec"
|
||||
* entries of this resolver.
|
||||
*/
|
||||
public String resolve(String existingValues) {
|
||||
List<String> newValues;
|
||||
// If there's even one replace, it wins over everything and the rest is thrown out
|
||||
if (!replaces.isEmpty()) {
|
||||
if (!(prepends.isEmpty() || appends.isEmpty() || removes.isEmpty())) {
|
||||
Log.w(TAG, "Discarded SSH cfg parts: key=" + cfgKey +
|
||||
", prepends=" + prepends + ", appends=" + appends +
|
||||
", removes=" + removes);
|
||||
}
|
||||
newValues = replaces;
|
||||
} else {
|
||||
// Otherwise we rebuild from existing and incoming values
|
||||
newValues = createResolvedValues(existingValues);
|
||||
}
|
||||
return String.join(DELIM, newValues);
|
||||
}
|
||||
|
||||
private List<String> createResolvedValues(String existingValues) {
|
||||
List<String> newValues = new ArrayList<>(prepends);
|
||||
for (String a : existingValues.split(DELIM)) {
|
||||
if (!shouldRemove(a)) {
|
||||
newValues.add(a);
|
||||
}
|
||||
}
|
||||
newValues.addAll(appends);
|
||||
return newValues;
|
||||
}
|
||||
|
||||
private boolean shouldRemove(String s) {
|
||||
s = normalize(s);
|
||||
for (Matcher m : removes) {
|
||||
if (m.matches(s)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private Matcher createMatcher(String val) {
|
||||
final String v = normalize(val);
|
||||
Matcher impl = s -> v.equals(s);
|
||||
|
||||
int wildcardIdx = v.indexOf(WILD);
|
||||
if (wildcardIdx < 0) {
|
||||
return impl;
|
||||
}
|
||||
|
||||
// *blah *blah* blah* some*thing
|
||||
// endsWith substring startsWith startsWith && endsWith
|
||||
String subStr = null;
|
||||
String suffix = null;
|
||||
String prefix = null;
|
||||
int vLen = v.length();
|
||||
|
||||
if (v.charAt(0) == WILD && vLen > 1) {
|
||||
if (vLen > 2 && v.charAt(vLen - 1) == WILD) {
|
||||
//substring
|
||||
subStr = v.substring(1, vLen - 1);
|
||||
} else {
|
||||
// endsWith
|
||||
suffix = v.substring(1);
|
||||
}
|
||||
} else if (v.charAt(vLen - 1) == WILD && vLen > 1) {
|
||||
// beginsWith
|
||||
prefix = v.substring(0, v.length() - 1);
|
||||
} else if (wildcardIdx > 0) {
|
||||
// startsWith && endsWith
|
||||
prefix = v.substring(0, wildcardIdx);
|
||||
suffix = v.substring(wildcardIdx + 1);
|
||||
}
|
||||
|
||||
if (subStr != null) {
|
||||
final String sub = subStr;
|
||||
impl = s -> s.contains(sub);
|
||||
} else if (prefix != null || suffix != null) {
|
||||
final String pre = prefix;
|
||||
final String suf = suffix;
|
||||
impl = s -> (pre == null || s.startsWith(pre)) && (suf == null || s.endsWith(suf));
|
||||
}
|
||||
return impl;
|
||||
}
|
||||
|
||||
private static String normalize(String s) {
|
||||
return s == null ? null : s.toLowerCase();
|
||||
}
|
||||
}
|
||||
@@ -538,12 +538,12 @@ public class MainActivity extends Activity implements JavaFileStorage.FileStorag
|
||||
}
|
||||
|
||||
static JavaFileStorage createStorageToTest(Context ctx, Context appContext, boolean simulateRestart) {
|
||||
//storageToTest = new SftpStorage(ctx.getApplicationContext());
|
||||
storageToTest = new SftpStorage(ctx.getApplicationContext());
|
||||
//storageToTest = new PCloudFileStorage(ctx, "yCeH59Ffgtm");
|
||||
//storageToTest = new SkyDriveFileStorage("000000004010C234", appContext);
|
||||
|
||||
|
||||
storageToTest = new GoogleDriveAppDataFileStorage();
|
||||
//storageToTest = new GoogleDriveAppDataFileStorage();
|
||||
/*storageToTest = new WebDavStorage(new ICertificateErrorHandler() {
|
||||
@Override
|
||||
public boolean onValidationError(String error) {
|
||||
@@ -690,6 +690,13 @@ public class MainActivity extends Activity implements JavaFileStorage.FileStorag
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private void populateCsvMockValues(View view) {
|
||||
EditText etSpecs = view.findViewById(R.id.mock_csv_specs);
|
||||
etSpecs.setText("-bar,+first,-*d*");
|
||||
EditText etCfgs = view.findViewById(R.id.mock_csv_cfg);
|
||||
etCfgs.setText("foo,del1,bar,del2");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void performManualFileSelect(boolean isForSave, final int requestCode,
|
||||
String protocolId)
|
||||
@@ -697,12 +704,13 @@ 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;
|
||||
|
||||
populateCsvMockValues(view);
|
||||
|
||||
view.findViewById(R.id.send_public_key).setOnClickListener(v -> {
|
||||
Intent sendIntent = new Intent();
|
||||
|
||||
|
||||
SftpStorage sftpStorage = (SftpStorage)storageToTest;
|
||||
try {
|
||||
String pub_filename = sftpStorage.createKeyPair();
|
||||
|
||||
@@ -715,39 +723,140 @@ 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();
|
||||
}
|
||||
});
|
||||
|
||||
view.findViewById(R.id.resolve_mock_csv).setOnClickListener(v -> {
|
||||
EditText etSpecs = view.findViewById(R.id.mock_csv_specs);
|
||||
String specs = etSpecs.getText().toString();
|
||||
EditText etCfg = view.findViewById(R.id.mock_csv_cfg);
|
||||
String cfg = etCfg.getText().toString();
|
||||
if (!specs.isBlank() && !cfg.isBlank()) {
|
||||
String result = sftpStorage.resolveCsvValues(cfg, specs);
|
||||
Toast.makeText(this, result, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
});
|
||||
|
||||
view.findViewById(R.id.reset_mock_csv).setOnClickListener(v -> {
|
||||
populateCsvMockValues(view);
|
||||
});
|
||||
|
||||
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();
|
||||
onReceivePathForFileSelect(requestCode, sftpStorage.buildFullPath( host, port, initialDir, user, pwd));
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
// TODO Auto-generated catch block
|
||||
e.printStackTrace();
|
||||
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) {
|
||||
}
|
||||
}
|
||||
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();
|
||||
EditText etKex = view.findViewById(R.id.kex);
|
||||
String kex = etKex.getText().toString();
|
||||
EditText etShk = view.findViewById(R.id.shk);
|
||||
String shk = etShk.getText().toString();
|
||||
|
||||
onReceivePathForFileSelect(requestCode, sftpStorage1.buildFullPath(
|
||||
host, port, initialDir, user, pwd, connectTimeout,
|
||||
keyName, keyPassphrase, kex, shk));
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
})
|
||||
.create()
|
||||
|
||||
@@ -3,69 +3,217 @@
|
||||
android:orientation="vertical"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent"
|
||||
android:layout_margin="12dip"
|
||||
>
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content">
|
||||
<EditText
|
||||
android:layout_margin="12dip">
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content">
|
||||
<EditText
|
||||
android:id="@+id/sftp_host"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="10"
|
||||
android:singleLine="true"
|
||||
android:inputType="textNoSuggestions"
|
||||
android:text=""
|
||||
android:hint="@string/hint_sftp_host" />
|
||||
<TextView
|
||||
android:hint="@string/hint_sftp_host" />
|
||||
<TextView
|
||||
android:id="@+id/portsep"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text=":" />
|
||||
<EditText
|
||||
<EditText
|
||||
android:id="@+id/sftp_port"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="15"
|
||||
android:singleLine="true"
|
||||
android:inputType="number"
|
||||
android:text="22"
|
||||
android:hint="@string/hint_sftp_port" />
|
||||
</LinearLayout>
|
||||
<EditText
|
||||
android:inputType="number"
|
||||
android:text="22"
|
||||
android:hint="@string/hint_sftp_port" />
|
||||
<EditText
|
||||
android:id="@+id/sftp_connect_timeout"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="14"
|
||||
android:singleLine="true"
|
||||
android:inputType="number"
|
||||
android:text=""
|
||||
android:hint="@string/hint_sftp_connect_timeout" />
|
||||
</LinearLayout>
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content">
|
||||
<EditText
|
||||
android:id="@+id/sftp_user"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:singleLine="true"
|
||||
android:text=""
|
||||
android:hint="@string/hint_username" />
|
||||
|
||||
<EditText
|
||||
android:hint="@string/hint_username" />
|
||||
<EditText
|
||||
android:id="@+id/sftp_password"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:inputType="textPassword"
|
||||
android:singleLine="true"
|
||||
android:text=""
|
||||
android:hint="@string/hint_pass"
|
||||
android:importantForAccessibility="no" />
|
||||
|
||||
<TextView android:id="@+id/initial_dir"
|
||||
android:hint="@string/hint_pass"
|
||||
android:importantForAccessibility="no" />
|
||||
</LinearLayout>
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content">
|
||||
<TextView android:id="@+id/initial_dir"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="4dip"
|
||||
android:layout_marginTop="4dip"
|
||||
|
||||
android:text="@string/initial_directory" />
|
||||
<EditText
|
||||
<EditText
|
||||
android:id="@+id/sftp_initial_dir"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="8dip"
|
||||
android:singleLine="true"
|
||||
android:text="/home/philipp"
|
||||
/>
|
||||
<Button android:id="@+id/send_public_key"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="send public key" />
|
||||
|
||||
android:text="/home/philipp"
|
||||
/>
|
||||
</LinearLayout>
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content">
|
||||
<EditText android:id="@+id/kex"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:singleLine="true"
|
||||
android:inputType="textNoSuggestions"
|
||||
android:text=""
|
||||
android:hint="KEX Algs" />
|
||||
<EditText android:id="@+id/shk"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:singleLine="true"
|
||||
android:inputType="textNoSuggestions"
|
||||
android:text=""
|
||||
android:hint="Server Host Key Algs" />
|
||||
</LinearLayout>
|
||||
<Button android:id="@+id/send_public_key"
|
||||
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="15dp"
|
||||
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>
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content">
|
||||
<EditText android:id="@+id/private_key_name"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:singleLine="true"
|
||||
android:inputType="textNoSuggestions"
|
||||
android:text=""
|
||||
android:hint="key name" />
|
||||
<EditText android:id="@+id/private_key_passphrase"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:singleLine="true"
|
||||
android:inputType="textNoSuggestions"
|
||||
android:text=""
|
||||
android:hint="passphrase (optional)" />
|
||||
</LinearLayout>
|
||||
<EditText android:id="@+id/private_key_content"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textMultiLine"
|
||||
android:lines="4"
|
||||
android:text=""
|
||||
android:hint="key content" />
|
||||
<TextView
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_horizontal"
|
||||
android:layout_marginTop="15dp"
|
||||
android:textStyle="bold"
|
||||
android:text="CSV Resolver Functions" />
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content">
|
||||
<EditText android:id="@+id/mock_csv_specs"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:singleLine="true"
|
||||
android:inputType="textNoSuggestions"
|
||||
android:text=""
|
||||
android:hint="Test specs" />
|
||||
<EditText android:id="@+id/mock_csv_cfg"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:singleLine="true"
|
||||
android:inputType="textNoSuggestions"
|
||||
android:text=""
|
||||
android:hint="Test config" />
|
||||
</LinearLayout>
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content">
|
||||
<Button android:id="@+id/reset_mock_csv"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginLeft="50dp"
|
||||
android:layout_marginRight="5dp"
|
||||
android:text="Reset" />
|
||||
<Button android:id="@+id/resolve_mock_csv"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginRight="50dp"
|
||||
android:layout_marginLeft="5dp"
|
||||
android:text="Resolve" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
@@ -333,6 +333,7 @@
|
||||
|
||||
<string name="hint_sftp_host">host</string>
|
||||
<string name="hint_sftp_port">port</string>
|
||||
<string name="hint_sftp_connect_timeout">timeout sec</string>
|
||||
|
||||
<string name="select_storage_type">Select the storage type:</string>
|
||||
|
||||
@@ -524,6 +525,6 @@ Initial public release
|
||||
<item>Do not accept invalid certificates</item>
|
||||
</string-array>
|
||||
|
||||
<string name="initial_directory">Initial directory (optional):</string>
|
||||
<string name="initial_directory">Initial dir (optional):</string>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -221,7 +221,6 @@ public class FileChooserActivity extends FragmentActivity {
|
||||
|
||||
public static final String EXTRA_RESULT_FILE_EXISTS = CLASSNAME + ".result_file_exists";
|
||||
|
||||
|
||||
|
||||
/*
|
||||
* CONTROLS
|
||||
|
||||
@@ -269,7 +269,6 @@ public class FragmentFiles extends Fragment implements
|
||||
FileChooserActivity.EXTRA_MAX_FILE_COUNT, 1000);
|
||||
mFileAdapter = new BaseFileAdapter(getActivity(), mFilterMode,
|
||||
mIsMultiSelection);
|
||||
|
||||
|
||||
/*
|
||||
* History.
|
||||
@@ -2268,12 +2267,15 @@ public class FragmentFiles extends Fragment implements
|
||||
}
|
||||
|
||||
if (mIsSaveDialog) {
|
||||
mTextSaveas.setText(BaseFileProviderUtils.getFileName(cursor));
|
||||
String fileName = BaseFileProviderUtils.getFileName(cursor);
|
||||
Uri uri = BaseFileProviderUtils.getUri(cursor);
|
||||
|
||||
mTextSaveas.setText(fileName);
|
||||
/*
|
||||
* Always set tag after setting text, or tag will be reset to
|
||||
* null.
|
||||
*/
|
||||
mTextSaveas.setTag(BaseFileProviderUtils.getUri(cursor));
|
||||
mTextSaveas.setTag(uri);
|
||||
}
|
||||
|
||||
if (mDoubleTapToChooseFiles) {
|
||||
@@ -2286,10 +2288,12 @@ public class FragmentFiles extends Fragment implements
|
||||
if (mIsMultiSelection)
|
||||
return;
|
||||
|
||||
if (mIsSaveDialog)
|
||||
if (mIsSaveDialog) {
|
||||
checkSaveasFilenameAndFinish();
|
||||
else
|
||||
}
|
||||
else {
|
||||
finish(BaseFileProviderUtils.getUri(cursor));
|
||||
}
|
||||
}// single tap to choose files
|
||||
}// onItemClick()
|
||||
};// mViewFilesOnItemClickListener
|
||||
|
||||
@@ -15,4 +15,24 @@ public class FileEntry {
|
||||
isDirectory = false;
|
||||
canRead = canWrite = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder s = new StringBuilder("kp2afilechooser.FileEntry{")
|
||||
.append(displayName).append("|")
|
||||
.append("path=").append(path).append(",sz=").append(sizeInBytes)
|
||||
.append(",").append(isDirectory ? "dir" : "file")
|
||||
.append(",lastMod=").append(lastModifiedTime);
|
||||
|
||||
StringBuilder perms = new StringBuilder();
|
||||
if (canRead)
|
||||
perms.append("r");
|
||||
if (canWrite)
|
||||
perms.append("w");
|
||||
if (perms.length() > 0) {
|
||||
s.append(",").append(perms);
|
||||
}
|
||||
|
||||
return s.append("}").toString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ public class Kp2aFileChooserBridge {
|
||||
.buildUpon()
|
||||
.appendPath(defaultPath)
|
||||
.build());
|
||||
|
||||
return intent;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -306,10 +306,9 @@ public abstract class Kp2aFileProvider extends BaseFileProvider {
|
||||
String parentPath = getParentPath(path);
|
||||
|
||||
|
||||
if (parentPath == null)
|
||||
{
|
||||
if (parentPath == null) {
|
||||
if (Utils.doLog())
|
||||
Log.d(CLASSNAME, "parent file is null");
|
||||
Log.d(CLASSNAME, "parent file is null");
|
||||
return null;
|
||||
}
|
||||
FileEntry e;
|
||||
@@ -501,10 +500,10 @@ public abstract class Kp2aFileProvider extends BaseFileProvider {
|
||||
RowBuilder newRow = matrixCursor.newRow();
|
||||
newRow.add(id);// _ID
|
||||
newRow.add(BaseFile
|
||||
.genContentIdUriBase(
|
||||
getAuthority())
|
||||
.buildUpon().appendPath(f.path)
|
||||
.build().toString());
|
||||
.genContentIdUriBase(
|
||||
getAuthority())
|
||||
.buildUpon().appendPath(f.path)
|
||||
.build().toString());
|
||||
newRow.add(f.path);
|
||||
if (f.displayName == null)
|
||||
Log.w("KP2AJ", "displayName is null for " + f.path);
|
||||
@@ -549,7 +548,7 @@ public abstract class Kp2aFileProvider extends BaseFileProvider {
|
||||
//puts the file entry in the cache for later reuse with retrieveFileInfo
|
||||
private void updateFileEntryCache(FileEntry f) {
|
||||
if (f != null)
|
||||
fileEntryMap.put(f.path, f);
|
||||
fileEntryMap.put(f.path, f);
|
||||
}
|
||||
//removes the file entry from the cache (if cached). Should be called whenever the file changes
|
||||
private void removeFromCache(String filename, boolean recursive) {
|
||||
@@ -584,7 +583,7 @@ public abstract class Kp2aFileProvider extends BaseFileProvider {
|
||||
|
||||
//returns the file entry from the cache if present or queries the concrete provider method to return the file info
|
||||
private FileEntry getFileEntryCached(String filename) {
|
||||
//check if enry is cached:
|
||||
//check if entry is cached:
|
||||
FileEntry cachedEntry = fileEntryMap.get(filename);
|
||||
if (cachedEntry != null)
|
||||
{
|
||||
@@ -728,7 +727,7 @@ public abstract class Kp2aFileProvider extends BaseFileProvider {
|
||||
if (targetParent != null && targetParent.startsWith(source))
|
||||
{
|
||||
if (Utils.doLog())
|
||||
Log.d("KP2A_FC_P", source+" is parent of "+target);
|
||||
Log.d("KP2A_FC_P", source + " is parent of " + target);
|
||||
return BaseFileProviderUtils.newClosedCursor();
|
||||
}
|
||||
if (Utils.doLog())
|
||||
@@ -768,28 +767,37 @@ public abstract class Kp2aFileProvider extends BaseFileProvider {
|
||||
|
||||
private String getParentPath(String path)
|
||||
{
|
||||
path = removeTrailingSlash(path);
|
||||
if (path.indexOf("://") == -1)
|
||||
{
|
||||
Log.d("KP2A_FC_P", "invalid path: " + path);
|
||||
return null;
|
||||
}
|
||||
String pathWithoutProtocol = path.substring(path.indexOf("://")+3);
|
||||
int lastSlashPos = path.lastIndexOf("/");
|
||||
if (pathWithoutProtocol.indexOf("/") == -1)
|
||||
{
|
||||
Log.d("KP2A_FC_P", "parent of " + path +" is null");
|
||||
return null;
|
||||
}
|
||||
else
|
||||
{
|
||||
String parent = path.substring(0, lastSlashPos)+"/";
|
||||
Log.d("KP2A_FC_P", "parent of " + path +" is "+parent);
|
||||
return parent;
|
||||
}
|
||||
String params = null;
|
||||
int paramsIdx = path.lastIndexOf("?");
|
||||
if (paramsIdx > 0) {
|
||||
params = path.substring(paramsIdx);
|
||||
path = path.substring(0, paramsIdx);
|
||||
}
|
||||
|
||||
path = removeTrailingSlash(path);
|
||||
if (path.indexOf("://") == -1)
|
||||
{
|
||||
Log.d("KP2A_FC_P", "invalid path: " + path);
|
||||
return null;
|
||||
}
|
||||
String pathWithoutProtocol = path.substring(path.indexOf("://") + 3);
|
||||
int lastSlashPos = path.lastIndexOf("/");
|
||||
if (pathWithoutProtocol.indexOf("/") == -1)
|
||||
{
|
||||
Log.d("KP2A_FC_P", "parent of " + path + " is null");
|
||||
return null;
|
||||
}
|
||||
else
|
||||
{
|
||||
String parent = path.substring(0, lastSlashPos) + "/";
|
||||
if (params != null) {
|
||||
parent += params;
|
||||
}
|
||||
Log.d("KP2A_FC_P", "parent of " + path +" is " + parent);
|
||||
return parent;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
protected abstract FileEntry getFileEntry(String path, StringBuilder errorMessageBuilder) throws Exception;
|
||||
|
||||
|
||||
@@ -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,71 +63,210 @@ 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 Keepass2android.Javafilestorage.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 (string.IsNullOrEmpty(ci.Password))
|
||||
if (ci.ConnectTimeoutSec != SftpStorage.UnsetSftpConnectTimeout)
|
||||
{
|
||||
spinner.SetSelection(1);
|
||||
dlgContents.FindViewById<EditText>(Resource.Id.sftp_connect_timeout).Text = ci.ConnectTimeoutSec.ToString();
|
||||
}
|
||||
|
||||
if (ci.ConfigOpts.Contains(SftpStorage.SshCfgKex))
|
||||
{
|
||||
dlgContents.FindViewById<EditText>(Resource.Id.sftp_kex).Text = ci.ConfigOpts[SftpStorage.SshCfgKex].ToString();
|
||||
}
|
||||
if (ci.ConfigOpts.Contains(SftpStorage.SshCfgServerHostKey))
|
||||
{
|
||||
dlgContents.FindViewById<EditText>(Resource.Id.sftp_shk).Text = ci.ConfigOpts[SftpStorage.SshCfgServerHostKey].ToString();
|
||||
}
|
||||
|
||||
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 = Keepass2android.Javafilestorage.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 sftpPath = new Keepass2android.Javafilestorage.SftpStorage(activity.ApplicationContext).BuildFullPath(host, port, initialPath, user,
|
||||
password);
|
||||
onStartBrowse(sftpPath);
|
||||
});
|
||||
EventHandler<DialogClickEventArgs> evtH = new EventHandler<DialogClickEventArgs>((sender, e) => onCancel());
|
||||
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 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 kexAlgorithms = dlgContents.FindViewById<EditText>(Resource.Id.sftp_kex).Text;
|
||||
string shkAlgorithms = dlgContents.FindViewById<EditText>(Resource.Id.sftp_shk).Text;
|
||||
|
||||
string sftpPath = fileStorage.BuildFullPath(
|
||||
host, port, initialPath, user, password, connectTimeout, keyName, keyPassphrase,
|
||||
kexAlgorithms, shkAlgorithms);
|
||||
|
||||
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));
|
||||
@@ -129,9 +274,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"
|
||||
@@ -88,6 +151,49 @@
|
||||
android:singleLine="true"
|
||||
android:text="/"
|
||||
/>
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
<TextView android:id="@+id/connect_timeout"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="4dip"
|
||||
android:layout_marginTop="4dip"
|
||||
android:text="@string/connect_timeout" />
|
||||
<EditText
|
||||
android:id="@+id/sftp_connect_timeout"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:singleLine="true"
|
||||
android:inputType="number" />
|
||||
|
||||
<TextView android:id="@+id/sftp_kex_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="4dip"
|
||||
android:layout_marginTop="4dip"
|
||||
android:text="@string/sftp_kex_title" />
|
||||
<EditText
|
||||
android:id="@+id/sftp_kex"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textNoSuggestions"
|
||||
android:hint="@string/hint_sftp_kex"
|
||||
android:singleLine="true"
|
||||
android:text=""
|
||||
/>
|
||||
<TextView android:id="@+id/sftp_shk_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="4dip"
|
||||
android:layout_marginTop="4dip"
|
||||
android:text="@string/sftp_shk_title" />
|
||||
<EditText
|
||||
android:id="@+id/sftp_shk"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textNoSuggestions"
|
||||
android:hint="@string/hint_sftp_shk"
|
||||
android:singleLine="true"
|
||||
android:text=""
|
||||
/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
@@ -98,6 +98,7 @@
|
||||
<string name="TrayTotp_SeedField_key">TrayTotp_SeedField_key</string>
|
||||
<string name="TrayTotp_prefs_key">TrayTotp_prefs_key</string>
|
||||
<string name="DebugLog_key">DebugLog_key</string>
|
||||
<string name="JSchDebug_key">JSchDebug_key</string>
|
||||
<string name="DebugLog_prefs_key">DebugLog_prefs_key</string>
|
||||
<string name="DebugLog_send_key">DebugLog_send</string>
|
||||
<string name="AutofillDisabledQueriesPreference_key">AutofillDisabledQueriesPreference_key</string>
|
||||
|
||||
@@ -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>
|
||||
@@ -589,9 +590,27 @@
|
||||
<string name="hint_sftp_host">host (ex: 192.168.0.1)</string>
|
||||
<string name="hint_sftp_port">port</string>
|
||||
<string name="initial_directory">Initial directory (optional):</string>
|
||||
<string name="connect_timeout">Connection timeout seconds (optional):"</string>
|
||||
<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="sftp_kex_title">Key Exchange (KEX) Algorithm(s) (optional)</string>
|
||||
<string name="hint_sftp_kex">"Comma-separated names/spec</string>
|
||||
<string name="sftp_shk_title">Server Host Key Algorithm(s) (optional)</string>
|
||||
<string name="hint_sftp_shk">"Comma-separated names/spec</string>
|
||||
|
||||
<string name="enter_ftp_login_title">Enter FTP login data:</string>
|
||||
|
||||
@@ -680,6 +699,7 @@
|
||||
<string name="TrayTotp_prefs">TrayTotp</string>
|
||||
<string name="DebugLog_prefs_prefs">Log-File for Debugging</string>
|
||||
<string name="DebugLog_title">Use log file</string>
|
||||
<string name="JSchDebug_title">SFTP debug logging</string>
|
||||
<string name="DebugLog_summary">Write app output to a local log file</string>
|
||||
<string name="DebugLog_send">Send debug log...</string>
|
||||
|
||||
@@ -1325,7 +1345,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>
|
||||
|
||||
@@ -670,10 +670,17 @@
|
||||
android:defaultValue="false"
|
||||
android:title="@string/DebugLog_title"
|
||||
android:key="@string/DebugLog_key" />
|
||||
|
||||
<Preference
|
||||
android:enabled="true"
|
||||
android:title="@string/DebugLog_send"
|
||||
android:key="@string/DebugLog_send_key" />
|
||||
<CheckBoxPreference
|
||||
android:enabled="true"
|
||||
android:persistent="false"
|
||||
android:defaultValue="false"
|
||||
android:title="@string/JSchDebug_title"
|
||||
android:key="@string/JSchDebug_key" />
|
||||
</PreferenceScreen>
|
||||
</PreferenceScreen>
|
||||
|
||||
|
||||
@@ -178,6 +178,12 @@ namespace keepass2android
|
||||
FindPreference(GetString(Resource.String.DebugLog_key)).PreferenceChange += OnDebugLogChanged;
|
||||
FindPreference(GetString(Resource.String.DebugLog_send_key)).PreferenceClick += OnSendDebug;
|
||||
|
||||
#if !EXCLUDE_JAVAFILESTORAGE && !NoNet
|
||||
FindPreference(GetString(Resource.String.JSchDebug_key)).PreferenceChange += OnJSchDebugChanged;
|
||||
#else
|
||||
FindPreference(GetString(Resource.String.JSchDebug_key)).Enabled = false;
|
||||
#endif
|
||||
|
||||
HashSet<string> supportedLocales = new HashSet<string>() { "en", "af", "ar", "az", "be", "bg", "ca", "cs", "da", "de", "el", "es", "eu", "fa", "fi", "fr", "gl", "he", "hr", "hu", "id", "in", "it", "iw", "ja", "ko", "ml", "nb", "nl", "nn", "no", "pl", "pt", "ro", "ru", "si", "sk", "sl", "sr", "sv", "tr", "uk", "vi", "zh" };
|
||||
var languagePref = (ListPreference)FindPreference(GetString(Resource.String.app_language_pref_key));
|
||||
new AppLanguageManager(this, languagePref, supportedLocales);
|
||||
@@ -381,18 +387,37 @@ namespace keepass2android
|
||||
|
||||
private void OnDebugLogChanged(object sender, Preference.PreferenceChangeEventArgs e)
|
||||
{
|
||||
if ((bool)e.NewValue)
|
||||
{
|
||||
Kp2aLog.CreateLogFile();
|
||||
}
|
||||
if ((bool)e.NewValue)
|
||||
Kp2aLog.CreateLogFile();
|
||||
else
|
||||
{
|
||||
Kp2aLog.FinishLogFile();
|
||||
}
|
||||
Kp2aLog.FinishLogFile();
|
||||
|
||||
}
|
||||
#if !EXCLUDE_JAVAFILESTORAGE && !NoNet
|
||||
bool jschLogEnable = PreferenceManager.GetDefaultSharedPreferences(Application.Context)
|
||||
.GetBoolean(Application.Context.GetString(Resource.String.JSchDebug_key), false);
|
||||
SetJSchLogging(jschLogEnable);
|
||||
#endif
|
||||
}
|
||||
|
||||
private void AlgorithmPrefChange(object sender, Preference.PreferenceChangeEventArgs preferenceChangeEventArgs)
|
||||
#if !EXCLUDE_JAVAFILESTORAGE && !NoNet
|
||||
private void OnJSchDebugChanged(object sender, Preference.PreferenceChangeEventArgs e)
|
||||
{
|
||||
SetJSchLogging((bool)e.NewValue);
|
||||
}
|
||||
|
||||
private void SetJSchLogging(bool enabled)
|
||||
{
|
||||
var sftpStorage = new Keepass2android.Javafilestorage.SftpStorage(Context);
|
||||
string? logFilename = null;
|
||||
if (Kp2aLog.LogToFile)
|
||||
{
|
||||
logFilename = Kp2aLog.LogFilename;
|
||||
}
|
||||
sftpStorage.SetJschLogging(enabled, logFilename);
|
||||
}
|
||||
#endif
|
||||
|
||||
private void AlgorithmPrefChange(object sender, Preference.PreferenceChangeEventArgs preferenceChangeEventArgs)
|
||||
{
|
||||
var db = App.Kp2a.CurrentDb;
|
||||
var previousCipher = db.KpDatabase.DataCipherUuid;
|
||||
@@ -835,7 +860,7 @@ namespace keepass2android
|
||||
|
||||
#if DEBUG
|
||||
preference.Enabled = (usageCount > 1);
|
||||
#else
|
||||
#else
|
||||
preference.Enabled = (usageCount > 50);
|
||||
#endif
|
||||
preference.PreferenceChange += delegate(object sender, Preference.PreferenceChangeEventArgs args)
|
||||
|
||||
Reference in New Issue
Block a user