Modify/specify KEX/SHK algorithms
-Implemented ability to manipulate server_host_key (SHK) via SFTP Credentials dialog (like KEX) -Implemented a few basic wildcard/relative algorithm list manipulation features: - Prepend to existing list: +alg_name - Append to end of existing list: alg_name+ - Remove a specific value: -alg_name - Remove values matching prefix: -alg_prefix* - Remove values matching suffix: -*alg_suffix - Remove values matching substring: -*alg_substring* - Remove values matching prefix and suffix: -alg*name - Otherwise CSV of values completely replace original config values
This commit is contained in:
@@ -50,16 +50,16 @@ public class SftpStorage extends JavaFileStorageBase {
|
||||
private static final String SFTP_KEYPASSPHRASE_OPTION_NAME = "phrase";
|
||||
|
||||
public static final String SSH_CFG_KEX = "kex";
|
||||
private static final String[] SSH_CFG_KEYS = new String[] {
|
||||
SSH_CFG_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
|
||||
@@ -213,7 +213,8 @@ public class SftpStorage extends JavaFileStorageBase {
|
||||
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_KEX),
|
||||
cInfo.configOpts.get(SSH_CFG_SERVER_HOST_KEY));
|
||||
} catch (Exception e) {
|
||||
throw convertException(e);
|
||||
}
|
||||
@@ -435,7 +436,7 @@ public class SftpStorage extends JavaFileStorageBase {
|
||||
ChannelSftp init(ConnectionInfo cInfo) throws JSchException, UnsupportedEncodingException {
|
||||
jsch = new JSch();
|
||||
|
||||
Log.d("KP2AJFS", "init SFTP");
|
||||
Log.d(TAG, "init SFTP");
|
||||
|
||||
String base_dir = getBaseDir();
|
||||
jsch.setKnownHosts(base_dir + "/known_hosts");
|
||||
@@ -448,19 +449,10 @@ public class SftpStorage extends JavaFileStorageBase {
|
||||
|
||||
}
|
||||
|
||||
Log.e("KP2AJFS[thread]", "getting session...");
|
||||
Log.e(THREAD_TAG, "getting session...");
|
||||
Session session = jsch.getSession(cInfo.username, cInfo.host, cInfo.port);
|
||||
Log.e("KP2AJFS", "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()) {
|
||||
Log.d("KP2AJFS", "Setting SSH config: " + e.getKey() + "=" + e.getValue());
|
||||
session.setConfig(e.getKey(), e.getValue());
|
||||
}
|
||||
|
||||
sessionConfigure(session, cInfo);
|
||||
sessionConnect(session, cInfo);
|
||||
|
||||
Channel channel = session.openChannel("sftp");
|
||||
@@ -471,14 +463,34 @@ public class SftpStorage extends JavaFileStorageBase {
|
||||
|
||||
}
|
||||
|
||||
private void sessionConnect(Session session, ConnectionInfo ci) throws JSchException {
|
||||
if (ci.connectTimeoutSec != UNSET_SFTP_CONNECT_TIMEOUT) {
|
||||
session.connect(ci.connectTimeoutSec * 1000);
|
||||
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();
|
||||
}
|
||||
@@ -529,6 +541,17 @@ public class SftpStorage extends JavaFileStorageBase {
|
||||
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 {
|
||||
@@ -578,8 +601,7 @@ public class SftpStorage extends JavaFileStorageBase {
|
||||
ci.keyPassphrase = options.get(SFTP_KEYPASSPHRASE_OPTION_NAME);
|
||||
}
|
||||
|
||||
// TODO: Support for prepending/appending config values (instead of complete replacement)?
|
||||
for (String cfgKey : SSH_CFG_KEYS) {
|
||||
for (String cfgKey : SSH_CFG_CSV_EXPANDABLE) {
|
||||
if (options.containsKey(cfgKey)) {
|
||||
ci.configOpts.put(cfgKey, options.get(cfgKey));
|
||||
}
|
||||
@@ -662,7 +684,7 @@ public class SftpStorage extends JavaFileStorageBase {
|
||||
String username, String password,
|
||||
int connectTimeoutSec,
|
||||
String keyName, String keyPassphrase,
|
||||
String kexAlgorithms)
|
||||
String kexAlgorithms, String shkAlgorithms)
|
||||
throws UnsupportedEncodingException {
|
||||
|
||||
StringBuilder uri = new StringBuilder(getProtocolPrefix()).append(encode(username));
|
||||
@@ -686,10 +708,11 @@ public class SftpStorage extends JavaFileStorageBase {
|
||||
.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());
|
||||
|
||||
// FIXME: Remove this!
|
||||
Log.d("KP2AJFS", "buildFullPath returns uri: " + uri.toString());
|
||||
Log.d(TAG, "buildFullPath returns uri: " + uri);
|
||||
// FIXME <end>
|
||||
|
||||
return uri.toString();
|
||||
|
@@ -0,0 +1,134 @@
|
||||
package keepass2android.javafilestorage;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
class SshConfigCsvValueResolver {
|
||||
interface Matcher {
|
||||
boolean matches(String s);
|
||||
}
|
||||
private final String cfgKey;
|
||||
private static final String TAG = "KP2AJFS[sshcfg]";
|
||||
private final List<String> prepends;
|
||||
private final List<String> appends;
|
||||
private final List<Matcher> removes;
|
||||
private final List<String> replaces;
|
||||
|
||||
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(",")) {
|
||||
if (iVal.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
int evLen = iVal.length();
|
||||
if (iVal.startsWith("+") && evLen > 1) {
|
||||
prepends.add(iVal.substring(1));
|
||||
} else if (iVal.endsWith("+") && evLen > 1) {
|
||||
appends.add(iVal.substring(0, evLen - 1));
|
||||
} else if (iVal.startsWith("-") && 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);
|
||||
}
|
||||
|
||||
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(",", newValues);
|
||||
}
|
||||
|
||||
private List<String> createResolvedValues(String existingValues) {
|
||||
List<String> newValues = new ArrayList<>(prepends);
|
||||
for (String a : existingValues.split(",")) {
|
||||
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('*');
|
||||
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) == '*' && vLen > 1) {
|
||||
if (vLen > 2 && v.charAt(vLen - 1) == '*') {
|
||||
//substring
|
||||
subStr = v.substring(1, vLen - 1);
|
||||
} else {
|
||||
// endsWith
|
||||
suffix = v.substring(1);
|
||||
}
|
||||
} else if (v.charAt(vLen - 1) == '*' && 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();
|
||||
}
|
||||
}
|
@@ -824,9 +824,10 @@ public class MainActivity extends Activity implements JavaFileStorage.FileStorag
|
||||
EditText etKeyPassphrase = view.findViewById(R.id.private_key_passphrase);
|
||||
String keyPassphrase = etKeyPassphrase.getText().toString();
|
||||
|
||||
// TODO: Add kex and shk configurability to SFTP dialog
|
||||
onReceivePathForFileSelect(requestCode, sftpStorage1.buildFullPath(
|
||||
host, port, initialDir, user, pwd, connectTimeout,
|
||||
keyName, keyPassphrase, null));
|
||||
keyName, keyPassphrase, null, null));
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
@@ -204,8 +204,12 @@ namespace keepass2android
|
||||
{
|
||||
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))
|
||||
if (!string.IsNullOrEmpty(ci.Password))
|
||||
{
|
||||
authModeSpinner.SetSelection(SftpModeSpinnerPasswd);
|
||||
} else if (!string.IsNullOrEmpty(ci.KeyName))
|
||||
@@ -254,10 +258,11 @@ namespace keepass2android
|
||||
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);
|
||||
kexAlgorithms, shkAlgorithms);
|
||||
|
||||
onStartBrowse(sftpPath);
|
||||
});
|
||||
|
@@ -180,5 +180,20 @@
|
||||
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>
|
||||
|
@@ -608,7 +608,9 @@
|
||||
<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 algorithm names</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>
|
||||
|
||||
|
Reference in New Issue
Block a user