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:
Rick Brown
2023-07-20 19:48:49 -04:00
parent 9204c4ca8f
commit 83529dd3b5
6 changed files with 208 additions and 28 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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