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";
|
private static final String SFTP_KEYPASSPHRASE_OPTION_NAME = "phrase";
|
||||||
|
|
||||||
public static final String SSH_CFG_KEX = "kex";
|
public static final String SSH_CFG_KEX = "kex";
|
||||||
private static final String[] SSH_CFG_KEYS = new String[] {
|
public static final String SSH_CFG_SERVER_HOST_KEY = "server_host_key";
|
||||||
SSH_CFG_KEX
|
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 ->
|
private static final ValueResolver<Integer> cTimeoutResolver = c ->
|
||||||
c == null || c == UNSET_SFTP_CONNECT_TIMEOUT ? null : String.valueOf(c);
|
c == null || c == UNSET_SFTP_CONNECT_TIMEOUT ? null : String.valueOf(c);
|
||||||
|
|
||||||
private static final ValueResolver<String> nonBlankStringResolver = s ->
|
private static final ValueResolver<String> nonBlankStringResolver = s ->
|
||||||
s == null || s.isBlank() ? null : s;
|
s == null || s.isBlank() ? null : s;
|
||||||
|
|
||||||
|
private static final String TAG = "KP2AJFS";
|
||||||
|
private static final String THREAD_TAG = TAG + "[thread]";
|
||||||
private JSch jsch;
|
private JSch jsch;
|
||||||
|
|
||||||
public class ConnectionInfo
|
public class ConnectionInfo
|
||||||
@@ -213,7 +213,8 @@ public class SftpStorage extends JavaFileStorageBase {
|
|||||||
return buildFullPath(cInfo.host, cInfo.port, newPath,
|
return buildFullPath(cInfo.host, cInfo.port, newPath,
|
||||||
cInfo.username, cInfo.password, cInfo.connectTimeoutSec,
|
cInfo.username, cInfo.password, cInfo.connectTimeoutSec,
|
||||||
cInfo.keyName, cInfo.keyPassphrase,
|
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) {
|
} catch (Exception e) {
|
||||||
throw convertException(e);
|
throw convertException(e);
|
||||||
}
|
}
|
||||||
@@ -435,7 +436,7 @@ public class SftpStorage extends JavaFileStorageBase {
|
|||||||
ChannelSftp init(ConnectionInfo cInfo) throws JSchException, UnsupportedEncodingException {
|
ChannelSftp init(ConnectionInfo cInfo) throws JSchException, UnsupportedEncodingException {
|
||||||
jsch = new JSch();
|
jsch = new JSch();
|
||||||
|
|
||||||
Log.d("KP2AJFS", "init SFTP");
|
Log.d(TAG, "init SFTP");
|
||||||
|
|
||||||
String base_dir = getBaseDir();
|
String base_dir = getBaseDir();
|
||||||
jsch.setKnownHosts(base_dir + "/known_hosts");
|
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);
|
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);
|
sessionConnect(session, cInfo);
|
||||||
|
|
||||||
Channel channel = session.openChannel("sftp");
|
Channel channel = session.openChannel("sftp");
|
||||||
@@ -471,14 +463,34 @@ public class SftpStorage extends JavaFileStorageBase {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sessionConnect(Session session, ConnectionInfo ci) throws JSchException {
|
private void sessionConnect(Session session, ConnectionInfo cInfo) throws JSchException {
|
||||||
if (ci.connectTimeoutSec != UNSET_SFTP_CONNECT_TIMEOUT) {
|
if (cInfo.connectTimeoutSec != UNSET_SFTP_CONNECT_TIMEOUT) {
|
||||||
session.connect(ci.connectTimeoutSec * 1000);
|
session.connect(cInfo.connectTimeoutSec * 1000);
|
||||||
} else {
|
} else {
|
||||||
session.connect();
|
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() {
|
private String getBaseDir() {
|
||||||
return _appContext.getFilesDir().getAbsolutePath();
|
return _appContext.getFilesDir().getAbsolutePath();
|
||||||
}
|
}
|
||||||
@@ -529,6 +541,17 @@ public class SftpStorage extends JavaFileStorageBase {
|
|||||||
return _keyUtils.getValidatedCustomKeyContent(keyContent);
|
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)
|
public ConnectionInfo splitStringToConnectionInfo(String filename)
|
||||||
throws UnsupportedEncodingException {
|
throws UnsupportedEncodingException {
|
||||||
@@ -578,8 +601,7 @@ public class SftpStorage extends JavaFileStorageBase {
|
|||||||
ci.keyPassphrase = options.get(SFTP_KEYPASSPHRASE_OPTION_NAME);
|
ci.keyPassphrase = options.get(SFTP_KEYPASSPHRASE_OPTION_NAME);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Support for prepending/appending config values (instead of complete replacement)?
|
for (String cfgKey : SSH_CFG_CSV_EXPANDABLE) {
|
||||||
for (String cfgKey : SSH_CFG_KEYS) {
|
|
||||||
if (options.containsKey(cfgKey)) {
|
if (options.containsKey(cfgKey)) {
|
||||||
ci.configOpts.put(cfgKey, options.get(cfgKey));
|
ci.configOpts.put(cfgKey, options.get(cfgKey));
|
||||||
}
|
}
|
||||||
@@ -662,7 +684,7 @@ public class SftpStorage extends JavaFileStorageBase {
|
|||||||
String username, String password,
|
String username, String password,
|
||||||
int connectTimeoutSec,
|
int connectTimeoutSec,
|
||||||
String keyName, String keyPassphrase,
|
String keyName, String keyPassphrase,
|
||||||
String kexAlgorithms)
|
String kexAlgorithms, String shkAlgorithms)
|
||||||
throws UnsupportedEncodingException {
|
throws UnsupportedEncodingException {
|
||||||
|
|
||||||
StringBuilder uri = new StringBuilder(getProtocolPrefix()).append(encode(username));
|
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_KEYNAME_OPTION_NAME, keyName, nonBlankStringResolver)
|
||||||
.addOption(SFTP_KEYPASSPHRASE_OPTION_NAME, keyPassphrase, nonBlankStringResolver)
|
.addOption(SFTP_KEYPASSPHRASE_OPTION_NAME, keyPassphrase, nonBlankStringResolver)
|
||||||
.addOption(SSH_CFG_KEX, kexAlgorithms, nonBlankStringResolver)
|
.addOption(SSH_CFG_KEX, kexAlgorithms, nonBlankStringResolver)
|
||||||
|
.addOption(SSH_CFG_SERVER_HOST_KEY, shkAlgorithms, nonBlankStringResolver)
|
||||||
.build());
|
.build());
|
||||||
|
|
||||||
// FIXME: Remove this!
|
// FIXME: Remove this!
|
||||||
Log.d("KP2AJFS", "buildFullPath returns uri: " + uri.toString());
|
Log.d(TAG, "buildFullPath returns uri: " + uri);
|
||||||
// FIXME <end>
|
// FIXME <end>
|
||||||
|
|
||||||
return uri.toString();
|
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);
|
EditText etKeyPassphrase = view.findViewById(R.id.private_key_passphrase);
|
||||||
String keyPassphrase = etKeyPassphrase.getText().toString();
|
String keyPassphrase = etKeyPassphrase.getText().toString();
|
||||||
|
|
||||||
|
// TODO: Add kex and shk configurability to SFTP dialog
|
||||||
onReceivePathForFileSelect(requestCode, sftpStorage1.buildFullPath(
|
onReceivePathForFileSelect(requestCode, sftpStorage1.buildFullPath(
|
||||||
host, port, initialDir, user, pwd, connectTimeout,
|
host, port, initialDir, user, pwd, connectTimeout,
|
||||||
keyName, keyPassphrase, null));
|
keyName, keyPassphrase, null, null));
|
||||||
} catch (UnsupportedEncodingException e) {
|
} catch (UnsupportedEncodingException e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -204,8 +204,12 @@ namespace keepass2android
|
|||||||
{
|
{
|
||||||
dlgContents.FindViewById<EditText>(Resource.Id.sftp_kex).Text = ci.ConfigOpts[SftpStorage.SshCfgKex].ToString();
|
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);
|
authModeSpinner.SetSelection(SftpModeSpinnerPasswd);
|
||||||
} else if (!string.IsNullOrEmpty(ci.KeyName))
|
} else if (!string.IsNullOrEmpty(ci.KeyName))
|
||||||
@@ -254,10 +258,11 @@ namespace keepass2android
|
|||||||
int.TryParse(connectTimeoutText, out connectTimeout);
|
int.TryParse(connectTimeoutText, out connectTimeout);
|
||||||
}
|
}
|
||||||
string kexAlgorithms = dlgContents.FindViewById<EditText>(Resource.Id.sftp_kex).Text;
|
string kexAlgorithms = dlgContents.FindViewById<EditText>(Resource.Id.sftp_kex).Text;
|
||||||
|
string shkAlgorithms = dlgContents.FindViewById<EditText>(Resource.Id.sftp_shk).Text;
|
||||||
|
|
||||||
string sftpPath = fileStorage.BuildFullPath(
|
string sftpPath = fileStorage.BuildFullPath(
|
||||||
host, port, initialPath, user, password, connectTimeout, keyName, keyPassphrase,
|
host, port, initialPath, user, password, connectTimeout, keyName, keyPassphrase,
|
||||||
kexAlgorithms);
|
kexAlgorithms, shkAlgorithms);
|
||||||
|
|
||||||
onStartBrowse(sftpPath);
|
onStartBrowse(sftpPath);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -180,5 +180,20 @@
|
|||||||
android:singleLine="true"
|
android:singleLine="true"
|
||||||
android:text=""
|
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>
|
</LinearLayout>
|
||||||
|
|||||||
@@ -608,7 +608,9 @@
|
|||||||
<string name="private_key_create_new">[Add New...]</string>
|
<string name="private_key_create_new">[Add New...]</string>
|
||||||
<string name="hint_sftp_key_passphrase">Key passphrase (optional)</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="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>
|
<string name="enter_ftp_login_title">Enter FTP login data:</string>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user