This commit is contained in:
Philipp Crocoll
2023-10-23 11:14:28 +02:00
1167 changed files with 2033 additions and 327 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -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,100 +426,192 @@ 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.indexOf(":");
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)
// See Bug #2350
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;
}
@@ -443,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
@@ -470,22 +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
{
if (port != DEFAULT_SFTP_PORT)
host += ":"+String.valueOf(port);
return getProtocolPrefix()+encode(username)+":"+encode(password)+"@"+host+localPath;
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
uri.append(encode(host));
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);
}
}
}

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 511 B

After

Width:  |  Height:  |  Size: 391 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 760 B

After

Width:  |  Height:  |  Size: 634 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 913 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 730 B

After

Width:  |  Height:  |  Size: 434 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 940 B

After

Width:  |  Height:  |  Size: 540 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 461 B

After

Width:  |  Height:  |  Size: 277 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 332 B

After

Width:  |  Height:  |  Size: 192 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 498 B

After

Width:  |  Height:  |  Size: 326 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 811 B

After

Width:  |  Height:  |  Size: 659 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 715 B

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1001 B

After

Width:  |  Height:  |  Size: 577 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 892 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 745 B

After

Width:  |  Height:  |  Size: 451 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 614 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 903 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 833 B

After

Width:  |  Height:  |  Size: 432 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 226 B

After

Width:  |  Height:  |  Size: 166 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 807 B

After

Width:  |  Height:  |  Size: 507 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 690 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 681 B

After

Width:  |  Height:  |  Size: 427 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 548 B

After

Width:  |  Height:  |  Size: 345 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 438 B

After

Width:  |  Height:  |  Size: 274 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 200 B

After

Width:  |  Height:  |  Size: 152 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 972 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 379 B

After

Width:  |  Height:  |  Size: 227 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 301 B

After

Width:  |  Height:  |  Size: 186 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 404 B

After

Width:  |  Height:  |  Size: 237 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 413 B

After

Width:  |  Height:  |  Size: 240 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 341 B

After

Width:  |  Height:  |  Size: 214 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 388 B

After

Width:  |  Height:  |  Size: 227 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 413 B

After

Width:  |  Height:  |  Size: 244 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 367 B

After

Width:  |  Height:  |  Size: 221 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 417 B

After

Width:  |  Height:  |  Size: 246 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 417 B

After

Width:  |  Height:  |  Size: 245 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 753 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 843 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 215 B

After

Width:  |  Height:  |  Size: 159 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1012 B

After

Width:  |  Height:  |  Size: 999 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 774 B

After

Width:  |  Height:  |  Size: 637 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 983 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 697 B

After

Width:  |  Height:  |  Size: 505 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 994 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 792 B

After

Width:  |  Height:  |  Size: 515 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 589 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 729 B

After

Width:  |  Height:  |  Size: 533 B

Some files were not shown because too many files have changed in this diff Show More