diff --git a/.classpath b/.classpath deleted file mode 100644 index e69f104a..00000000 --- a/.classpath +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/.env b/.env new file mode 100644 index 00000000..17e65c9f --- /dev/null +++ b/.env @@ -0,0 +1,2 @@ +export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64 +export PATH="$JAVA_HOME/bin:$PATH" \ No newline at end of file diff --git a/.gitignore b/.gitignore index 61075a97..0fff909f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,50 @@ -bin -gen -launchpad-*.tar.gz local.properties -*~ +bin/ +build/ +gen/ +.gradle/ + +!**/src/**/build/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar + +# Avoid ignore Gradle wrapper properties +!gradle-wrapper.properties + +# Cache of project +.gradletasknamecache + +# Eclipse Gradle plugin generated files +# Eclipse Core +.project +# JDT-specific (Eclipse Java Development Tools) +.classpath + +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* +replay_pid* \ No newline at end of file diff --git a/.project b/.project deleted file mode 100644 index 2a174d0b..00000000 --- a/.project +++ /dev/null @@ -1,33 +0,0 @@ - - - connectbot - - - - - - com.android.ide.eclipse.adt.ResourceManagerBuilder - - - - - com.android.ide.eclipse.adt.PreCompilerBuilder - - - - - org.eclipse.jdt.core.javabuilder - - - - - com.android.ide.eclipse.adt.ApkBuilder - - - - - - com.android.ide.eclipse.adt.AndroidNature - org.eclipse.jdt.core.javanature - - diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs deleted file mode 100644 index b8fb4253..00000000 --- a/.settings/org.eclipse.jdt.core.prefs +++ /dev/null @@ -1,5 +0,0 @@ -#Wed Nov 14 13:33:03 CST 2007 -eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.5 -org.eclipse.jdt.core.compiler.compliance=1.5 -org.eclipse.jdt.core.compiler.source=1.5 diff --git a/.settings/org.eclipse.jdt.ui.prefs b/.settings/org.eclipse.jdt.ui.prefs deleted file mode 100644 index 16019b18..00000000 --- a/.settings/org.eclipse.jdt.ui.prefs +++ /dev/null @@ -1,114 +0,0 @@ -#Sat Jan 24 08:09:15 CST 2009 -cleanup.add_default_serial_version_id=true -cleanup.add_generated_serial_version_id=false -cleanup.add_missing_annotations=true -cleanup.add_missing_deprecated_annotations=true -cleanup.add_missing_methods=false -cleanup.add_missing_nls_tags=false -cleanup.add_missing_override_annotations=true -cleanup.add_serial_version_id=false -cleanup.always_use_blocks=false -cleanup.always_use_parentheses_in_expressions=false -cleanup.always_use_this_for_non_static_field_access=false -cleanup.always_use_this_for_non_static_method_access=false -cleanup.convert_to_enhanced_for_loop=false -cleanup.correct_indentation=true -cleanup.format_source_code=true -cleanup.format_source_code_changes_only=false -cleanup.make_local_variable_final=true -cleanup.make_parameters_final=false -cleanup.make_private_fields_final=true -cleanup.make_type_abstract_if_missing_method=false -cleanup.make_variable_declarations_final=false -cleanup.never_use_blocks=false -cleanup.never_use_parentheses_in_expressions=true -cleanup.organize_imports=true -cleanup.qualify_static_field_accesses_with_declaring_class=false -cleanup.qualify_static_member_accesses_through_instances_with_declaring_class=true -cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class=true -cleanup.qualify_static_member_accesses_with_declaring_class=true -cleanup.qualify_static_method_accesses_with_declaring_class=false -cleanup.remove_private_constructors=true -cleanup.remove_trailing_whitespaces=true -cleanup.remove_trailing_whitespaces_all=true -cleanup.remove_trailing_whitespaces_ignore_empty=false -cleanup.remove_unnecessary_casts=true -cleanup.remove_unnecessary_nls_tags=true -cleanup.remove_unused_imports=true -cleanup.remove_unused_local_variables=false -cleanup.remove_unused_private_fields=true -cleanup.remove_unused_private_members=false -cleanup.remove_unused_private_methods=true -cleanup.remove_unused_private_types=true -cleanup.sort_members=true -cleanup.sort_members_all=true -cleanup.use_blocks=true -cleanup.use_blocks_only_for_return_and_throw=true -cleanup.use_parentheses_in_expressions=false -cleanup.use_this_for_non_static_field_access=true -cleanup.use_this_for_non_static_field_access_only_if_necessary=true -cleanup.use_this_for_non_static_method_access=true -cleanup.use_this_for_non_static_method_access_only_if_necessary=true -cleanup_profile=_ConnectBot Light Cleanup -cleanup_settings_version=2 -eclipse.preferences.version=1 -editor_save_participant_org.eclipse.jdt.ui.postsavelistener.cleanup=true -formatter_profile=_ConnectBot -formatter_settings_version=11 -org.eclipse.jdt.ui.ignorelowercasenames=true -org.eclipse.jdt.ui.importorder=java;javax;org;com; -org.eclipse.jdt.ui.javadoc=true -org.eclipse.jdt.ui.ondemandthreshold=99 -org.eclipse.jdt.ui.staticondemandthreshold=99 -org.eclipse.jdt.ui.text.custom_code_templates= -sp_cleanup.add_default_serial_version_id=true -sp_cleanup.add_generated_serial_version_id=false -sp_cleanup.add_missing_annotations=true -sp_cleanup.add_missing_deprecated_annotations=true -sp_cleanup.add_missing_methods=false -sp_cleanup.add_missing_nls_tags=false -sp_cleanup.add_missing_override_annotations=true -sp_cleanup.add_serial_version_id=false -sp_cleanup.always_use_blocks=true -sp_cleanup.always_use_parentheses_in_expressions=false -sp_cleanup.always_use_this_for_non_static_field_access=false -sp_cleanup.always_use_this_for_non_static_method_access=false -sp_cleanup.convert_to_enhanced_for_loop=false -sp_cleanup.correct_indentation=false -sp_cleanup.format_source_code=false -sp_cleanup.format_source_code_changes_only=true -sp_cleanup.make_local_variable_final=false -sp_cleanup.make_parameters_final=false -sp_cleanup.make_private_fields_final=true -sp_cleanup.make_type_abstract_if_missing_method=false -sp_cleanup.make_variable_declarations_final=false -sp_cleanup.never_use_blocks=false -sp_cleanup.never_use_parentheses_in_expressions=true -sp_cleanup.on_save_use_additional_actions=true -sp_cleanup.organize_imports=true -sp_cleanup.qualify_static_field_accesses_with_declaring_class=false -sp_cleanup.qualify_static_member_accesses_through_instances_with_declaring_class=true -sp_cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class=true -sp_cleanup.qualify_static_member_accesses_with_declaring_class=false -sp_cleanup.qualify_static_method_accesses_with_declaring_class=false -sp_cleanup.remove_private_constructors=true -sp_cleanup.remove_trailing_whitespaces=true -sp_cleanup.remove_trailing_whitespaces_all=true -sp_cleanup.remove_trailing_whitespaces_ignore_empty=false -sp_cleanup.remove_unnecessary_casts=true -sp_cleanup.remove_unnecessary_nls_tags=false -sp_cleanup.remove_unused_imports=false -sp_cleanup.remove_unused_local_variables=false -sp_cleanup.remove_unused_private_fields=true -sp_cleanup.remove_unused_private_members=false -sp_cleanup.remove_unused_private_methods=true -sp_cleanup.remove_unused_private_types=true -sp_cleanup.sort_members=false -sp_cleanup.sort_members_all=false -sp_cleanup.use_blocks=false -sp_cleanup.use_blocks_only_for_return_and_throw=false -sp_cleanup.use_parentheses_in_expressions=false -sp_cleanup.use_this_for_non_static_field_access=false -sp_cleanup.use_this_for_non_static_field_access_only_if_necessary=true -sp_cleanup.use_this_for_non_static_method_access=false -sp_cleanup.use_this_for_non_static_method_access_only_if_necessary=true diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..8e98d4b6 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,11 @@ +{ + "java.jdt.ls.java.home": "/usr/lib/jvm/java-17-openjdk-amd64", + "java.home": "/usr/lib/jvm/java-17-openjdk-amd64", + "java.configuration.updateBuildConfiguration": "automatic", + "java.configuration.runtimes": [ + { + "name": "JavaSE-17", + "path": "/usr/lib/jvm/java-17-openjdk-amd64" + } + ] +} diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 956a4a43..52a1c99f 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -16,12 +16,11 @@ limitations under the License. --> - + @@ -50,7 +49,7 @@ android:backupAgent=".service.BackupAgent" android:killAfterRestore="true"> - + @@ -68,7 +67,7 @@ - + @@ -86,11 +85,13 @@ + android:description="@string/service_desc" + android:exported="false" /> + android:permission="org.openintents.ssh.permission.ACCESS_SSH_AGENT" + android:exported="true"> @@ -98,7 +99,8 @@ + android:launchMode="singleTop" + android:exported="true"> diff --git a/README b/README index 9f435f5c..37fbdd85 100644 --- a/README +++ b/README @@ -1,10 +1,24 @@ Compiling --------- -To compile ConnectBot using Ant, you must specify where your Android SDK is via the local.properties file. Insert a line similar to the following with the full path to your SDK: +The project uses Gradle and includes the Gradle Wrapper. Install a JDK 17 (or newer) and configure the Android SDK path in `local.properties` if it is not already available through the environment: sdk.dir=/usr/local/android +To generate a debug APK, run from the project root: + +./gradlew assembleDebug + +The APK will be generated at: + +build/outputs/apk/debug/VX ConnectBot-debug.apk + +To generate a release APK, run: + +./gradlew assembleRelease + +Release builds may require signing configuration before they can be installed or distributed. + ProGuard Support ---------------- diff --git a/build-and-install.sh b/build-and-install.sh new file mode 100755 index 00000000..fc25d0be --- /dev/null +++ b/build-and-install.sh @@ -0,0 +1,29 @@ +#!/bin/bash +set -euo pipefail + +cd "$(dirname "$0")" + +export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64 +export PATH="$JAVA_HOME/bin:$PATH" + +# keytool -genkeypair -keystore build/vx-connectbot-release.jks -alias connectbot -keyalg RSA -keysize 2048 -validity 10000 + +if [ "$1" == "debug" ]; then + ./gradlew assembleDebug + adb install -r 'build/outputs/apk/debug/VX ConnectBot-debug.apk' + exit 0 +fi + +./gradlew assembleRelease + +$ANDROID_HOME/build-tools/34.0.0/apksigner sign \ + --v1-signing-enabled true \ + --v2-signing-enabled true \ + --v3-signing-enabled false \ + --v4-signing-enabled false \ + --ks build/vx-connectbot-release.jks \ + --ks-key-alias connectbot \ + --out build/outputs/apk/release/VX-ConnectBot-1.8.0.apk \ + "build/outputs/apk/release/VX ConnectBot-release-unsigned.apk" + +adb install -r 'build/outputs/apk/release/VX-ConnectBot-1.8.0.apk' diff --git a/build.gradle b/build.gradle new file mode 100644 index 00000000..f18882d8 --- /dev/null +++ b/build.gradle @@ -0,0 +1,41 @@ +plugins { + id 'com.android.application' version '8.3.2' +} + +android { + namespace 'sk.vx.connectbot' + compileSdkVersion 34 + + defaultConfig { + applicationId 'sk.vx.connectbot' + minSdkVersion 15 + targetSdkVersion 25 + versionCode 30 + versionName '1.8.0' + } + + sourceSets { + main { + manifest.srcFile 'AndroidManifest.xml' + java.srcDirs = ['src'] + resources.srcDirs = ['src'] + aidl.srcDirs = ['src'] + res.srcDirs = ['res'] + assets.srcDirs = ['assets'] + jniLibs.srcDirs = ['libs'] + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + lint { + abortOnError false + } +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..d64cd491 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..a5952066 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 00000000..f4bc266f --- /dev/null +++ b/gradlew @@ -0,0 +1,21 @@ +#!/bin/sh +set -e + +APP_HOME=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) + +DEFAULT_JVM_OPTS='' + +if [ -n "$JAVA_HOME" ]; then + JAVACMD="$JAVA_HOME/bin/java" +else + JAVACMD="java" +fi + +if [ ! -x "$JAVACMD" ]; then + echo "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH." >&2 + exit 1 +fi + +CLASSPATH="$APP_HOME/gradle/wrapper/gradle-wrapper.jar" + +exec "$JAVACMD" $DEFAULT_JVM_OPTS -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/res/layout/act_console.xml b/res/layout/act_console.xml index cf468298..9e6591e9 100644 --- a/res/layout/act_console.xml +++ b/res/layout/act_console.xml @@ -133,7 +133,7 @@ android:layout_height="wrap_content" android:layout_toRightOf="@+id/button_sym" android:layout_alignParentBottom="true" - android:src="@+drawable/button_input" + android:src="@drawable/button_input" android:contentDescription="@string/image_description_line_input" /> @@ -143,7 +143,7 @@ android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:layout_alignParentRight="true" - android:src="@+drawable/button_keyboard" + android:src="@drawable/button_keyboard" android:contentDescription="@string/image_description_show_keyboard" /> @@ -154,7 +154,7 @@ android:layout_height="wrap_content" android:layout_alignParentLeft="true" android:layout_alignParentBottom="true" - android:src="@+drawable/button_ctrl" + android:src="@drawable/button_ctrl" android:contentDescription="@string/image_description_toggle_control_character" /> @@ -165,7 +165,7 @@ android:layout_height="wrap_content" android:layout_toRightOf="@+id/button_ctrl" android:layout_alignParentBottom="true" - android:src="@+drawable/button_esc" + android:src="@drawable/button_esc" android:contentDescription="@string/image_description_send_escape_character" /> @@ -176,7 +176,7 @@ android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:layout_toRightOf="@+id/button_esc" - android:src="@+drawable/button_sym" + android:src="@drawable/button_sym" android:contentDescription="@string/image_description_sym" /> diff --git a/res/values-ru/strings.xml b/res/values-ru/strings.xml index 0d91c3c2..88a01d07 100644 --- a/res/values-ru/strings.xml +++ b/res/values-ru/strings.xml @@ -66,7 +66,7 @@ Генерировать ключ Изменить размер Соединение потеряно - Copyright © 2007–2008 Kenny Root http://the-b.org/, Jeffrey Sharkey http://jsharkey.org/; © 2013&8211;2014 — ConnectBot Community + Copyright © 2007–2008 Kenny Root http://the-b.org/, Jeffrey Sharkey http://jsharkey.org/; © 2013–2014 — ConnectBot Community Эмулятор терминала Режим эмуляции Режим эмуляции терминала для использования PTY соединений diff --git a/res/values/version.xml b/res/values/version.xml index 94227c94..d762bbc0 100644 --- a/res/values/version.xml +++ b/res/values/version.xml @@ -1,4 +1,4 @@ - ConnectBot (working copy) + VX ConnectBot 1.8.0-30 (2026.08.08) diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 00000000..796a0b63 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,17 @@ +pluginManagement { + repositories { + gradlePluginPortal() + google() + mavenCentral() + } +} + +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.PREFER_PROJECT) + repositories { + google() + mavenCentral() + } +} + +rootProject.name = 'VX ConnectBot' diff --git a/src/com/trilead/ssh2/KnownHosts.java b/src/com/trilead/ssh2/KnownHosts.java index edca0a2d..eabedb6b 100644 --- a/src/com/trilead/ssh2/KnownHosts.java +++ b/src/com/trilead/ssh2/KnownHosts.java @@ -93,7 +93,9 @@ public void addHostkey(String hostnames[], String serverHostKeyAlgorithm, byte[] if (hostnames == null) throw new IllegalArgumentException("hostnames may not be null"); - if ("ssh-rsa".equals(serverHostKeyAlgorithm)) + if ("ssh-rsa".equals(serverHostKeyAlgorithm) + || "rsa-sha2-256".equals(serverHostKeyAlgorithm) + || "rsa-sha2-512".equals(serverHostKeyAlgorithm)) { RSAPublicKey rpk = RSASHA1Verify.decodeSSHRSAPublicKey(serverHostKey); @@ -405,7 +407,10 @@ private void initialize(char[] knownHostsData) throws IOException if (arr.length >= 3) { - if ((arr[1].compareTo("ssh-rsa") == 0) || (arr[1].compareTo("ssh-dss") == 0)) + if ((arr[1].compareTo("ssh-rsa") == 0) + || (arr[1].compareTo("ssh-dss") == 0) + || (arr[1].compareTo("rsa-sha2-256") == 0) + || (arr[1].compareTo("rsa-sha2-512") == 0)) { String[] hostnames = arr[0].split(","); @@ -603,7 +608,9 @@ public int verifyHostkey(String hostname, String serverHostKeyAlgorithm, byte[] { Object remoteKey = null; - if ("ssh-rsa".equals(serverHostKeyAlgorithm)) + if ("ssh-rsa".equals(serverHostKeyAlgorithm) + || "rsa-sha2-256".equals(serverHostKeyAlgorithm) + || "rsa-sha2-512".equals(serverHostKeyAlgorithm)) { remoteKey = RSASHA1Verify.decodeSSHRSAPublicKey(serverHostKey); } @@ -720,7 +727,9 @@ else if ("sha1".equals(type)) else throw new IllegalArgumentException("Unknown hash type " + type); - if ("ssh-rsa".equals(keyType)) + if ("ssh-rsa".equals(keyType) + || "rsa-sha2-256".equals(keyType) + || "rsa-sha2-512".equals(keyType)) { } else if ("ssh-dss".equals(keyType)) diff --git a/src/com/trilead/ssh2/auth/AuthenticationManager.java b/src/com/trilead/ssh2/auth/AuthenticationManager.java index 43c226a2..d5dd3c36 100644 --- a/src/com/trilead/ssh2/auth/AuthenticationManager.java +++ b/src/com/trilead/ssh2/auth/AuthenticationManager.java @@ -211,31 +211,68 @@ else if (key instanceof RSAPrivateKey) byte[] pk_enc = RSASHA1Verify.encodeSSHRSAPublicKey(pk.getPublicKey()); - TypesWriter tw = new TypesWriter(); - { - byte[] H = tm.getSessionIdentifier(); - - tw.writeString(H, 0, H.length); - tw.writeByte(Packets.SSH_MSG_USERAUTH_REQUEST); - tw.writeString(user); - tw.writeString("ssh-connection"); - tw.writeString("publickey"); - tw.writeBoolean(true); - tw.writeString("ssh-rsa"); - tw.writeString(pk_enc, 0, pk_enc.length); - } + String[] sigAlgos = new String[] { "rsa-sha2-512", "rsa-sha2-256", "ssh-rsa" }; + + for (int i = 0; i < sigAlgos.length; i++) { + String sigAlgo = sigAlgos[i]; + String digestType; + + if ("rsa-sha2-512".equals(sigAlgo)) { + digestType = "SHA-512"; + } else if ("rsa-sha2-256".equals(sigAlgo)) { + digestType = "SHA-256"; + } else { + digestType = "SHA-1"; + } - byte[] msg = tw.getBytes(); + TypesWriter tw = new TypesWriter(); + { + byte[] H = tm.getSessionIdentifier(); + + tw.writeString(H, 0, H.length); + tw.writeByte(Packets.SSH_MSG_USERAUTH_REQUEST); + tw.writeString(user); + tw.writeString("ssh-connection"); + tw.writeString("publickey"); + tw.writeBoolean(true); + tw.writeString(sigAlgo); + tw.writeString(pk_enc, 0, pk_enc.length); + } - RSASignature ds = RSASHA1Verify.generateSignature(msg, pk); + byte[] msg = tw.getBytes(); - byte[] rsa_sig_enc = RSASHA1Verify.encodeSSHRSASignature(ds); + RSASignature ds = RSASHA1Verify.generateSignature(msg, pk, digestType); - PacketUserauthRequestPublicKey ua = new PacketUserauthRequestPublicKey("ssh-connection", user, - "ssh-rsa", pk_enc, rsa_sig_enc); + byte[] rsa_sig_enc = RSASHA1Verify.encodeSSHRSASignature(ds, sigAlgo); - tm.sendMessage(ua.getPayload()); + PacketUserauthRequestPublicKey ua = new PacketUserauthRequestPublicKey("ssh-connection", user, + sigAlgo, pk_enc, rsa_sig_enc); + + tm.sendMessage(ua.getPayload()); + byte[] ar = getNextMessage(); + if (ar[0] == Packets.SSH_MSG_USERAUTH_SUCCESS) + { + authenticated = true; + tm.removeMessageHandler(this, 0, 255); + return true; + } + + if (ar[0] == Packets.SSH_MSG_USERAUTH_FAILURE) + { + PacketUserauthFailure puf = new PacketUserauthFailure(ar, 0, ar.length); + + remainingMethods = puf.getAuthThatCanContinue(); + isPartialSuccess = puf.isPartialSuccess(); + + if (methodPossible("publickey") == false || i == sigAlgos.length - 1) + return false; + + continue; + } + + throw new IOException("Unexpected SSH message (type " + ar[0] + ")"); + } } else { diff --git a/src/com/trilead/ssh2/crypto/dh/DhExchange.java b/src/com/trilead/ssh2/crypto/dh/DhExchange.java index 5622a72d..c2946527 100644 --- a/src/com/trilead/ssh2/crypto/dh/DhExchange.java +++ b/src/com/trilead/ssh2/crypto/dh/DhExchange.java @@ -123,9 +123,9 @@ public void setF(BigInteger f) } public byte[] calculateH(byte[] clientversion, byte[] serverversion, byte[] clientKexPayload, - byte[] serverKexPayload, byte[] hostKey) throws UnsupportedEncodingException + byte[] serverKexPayload, byte[] hostKey, String hashType) throws UnsupportedEncodingException { - HashForSSH2Types hash = new HashForSSH2Types("SHA1"); + HashForSSH2Types hash = new HashForSSH2Types(hashType); if (log.isEnabled()) { diff --git a/src/com/trilead/ssh2/crypto/dh/DhGroupExchange.java b/src/com/trilead/ssh2/crypto/dh/DhGroupExchange.java index 8e798eef..37561c41 100644 --- a/src/com/trilead/ssh2/crypto/dh/DhGroupExchange.java +++ b/src/com/trilead/ssh2/crypto/dh/DhGroupExchange.java @@ -88,9 +88,9 @@ public void setF(BigInteger f) } public byte[] calculateH(byte[] clientversion, byte[] serverversion, byte[] clientKexPayload, - byte[] serverKexPayload, byte[] hostKey, DHGexParameters para) + byte[] serverKexPayload, byte[] hostKey, DHGexParameters para, String hashType) { - HashForSSH2Types hash = new HashForSSH2Types("SHA1"); + HashForSSH2Types hash = new HashForSSH2Types(hashType); hash.updateByteString(clientversion); hash.updateByteString(serverversion); diff --git a/src/com/trilead/ssh2/crypto/digest/HashForSSH2Types.java b/src/com/trilead/ssh2/crypto/digest/HashForSSH2Types.java index df849522..820966df 100644 --- a/src/com/trilead/ssh2/crypto/digest/HashForSSH2Types.java +++ b/src/com/trilead/ssh2/crypto/digest/HashForSSH2Types.java @@ -24,6 +24,10 @@ public HashForSSH2Types(String type) { md = new SHA1(); } + else if (type.equals("SHA256")) + { + md = new SHA256(); + } else if (type.equals("MD5")) { md = new MD5(); diff --git a/src/com/trilead/ssh2/crypto/digest/SHA256.java b/src/com/trilead/ssh2/crypto/digest/SHA256.java new file mode 100644 index 00000000..fa5020e5 --- /dev/null +++ b/src/com/trilead/ssh2/crypto/digest/SHA256.java @@ -0,0 +1,60 @@ +package com.trilead.ssh2.crypto.digest; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +/** + * SHA-256 digest wrapper for the trilead SSH2 code. + */ +public final class SHA256 implements Digest +{ + private final MessageDigest md; + + public SHA256() + { + try + { + md = MessageDigest.getInstance("SHA-256"); + } + catch (NoSuchAlgorithmException e) + { + throw new RuntimeException(e); + } + } + + public int getDigestLength() + { + return md.getDigestLength(); + } + + public void update(byte b) + { + md.update(b); + } + + public void update(byte[] b) + { + md.update(b); + } + + public void update(byte[] b, int off, int len) + { + md.update(b, off, len); + } + + public void reset() + { + md.reset(); + } + + public void digest(byte[] out) + { + digest(out, 0); + } + + public void digest(byte[] out, int off) + { + byte[] digest = md.digest(); + System.arraycopy(digest, 0, out, off, digest.length); + } +} diff --git a/src/com/trilead/ssh2/signature/RSASHA1Verify.java b/src/com/trilead/ssh2/signature/RSASHA1Verify.java index 8a0f07ae..3f7ce363 100644 --- a/src/com/trilead/ssh2/signature/RSASHA1Verify.java +++ b/src/com/trilead/ssh2/signature/RSASHA1Verify.java @@ -3,6 +3,8 @@ import java.io.IOException; import java.math.BigInteger; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; import com.trilead.ssh2.crypto.SimpleDERReader; import com.trilead.ssh2.crypto.digest.SHA1; @@ -50,15 +52,24 @@ public static byte[] encodeSSHRSAPublicKey(RSAPublicKey pk) throws IOException return tw.getBytes(); } - public static RSASignature decodeSSHRSASignature(byte[] sig) throws IOException + public static String getSignatureFormat(byte[] sig) throws IOException { TypesReader tr = new TypesReader(sig); - String sig_format = tr.readString(); - if (sig_format.equals("ssh-rsa") == false) + if ((sig_format.equals("ssh-rsa") == false) && (sig_format.equals("rsa-sha2-256") == false) + && (sig_format.equals("rsa-sha2-512") == false)) throw new IOException("Peer sent wrong signature format"); + return sig_format; + } + + public static RSASignature decodeSSHRSASignature(byte[] sig) throws IOException + { + String sig_format = getSignatureFormat(sig); + TypesReader tr = new TypesReader(sig); + tr.readString(); + /* S is NOT an MPINT. "The value for 'rsa_signature_blob' is encoded as a string * containing s (which is an integer, without lengths or padding, unsigned and in * network byte order)." See also below. @@ -81,10 +92,15 @@ public static RSASignature decodeSSHRSASignature(byte[] sig) throws IOException } public static byte[] encodeSSHRSASignature(RSASignature sig) throws IOException + { + return encodeSSHRSASignature(sig, "ssh-rsa"); + } + + public static byte[] encodeSSHRSASignature(RSASignature sig, String sigFormat) throws IOException { TypesWriter tw = new TypesWriter(); - tw.writeString("ssh-rsa"); + tw.writeString(sigFormat); /* S is NOT an MPINT. "The value for 'rsa_signature_blob' is encoded as a string * containing s (which is an integer, without lengths or padding, unsigned and in @@ -103,24 +119,51 @@ public static byte[] encodeSSHRSASignature(RSASignature sig) throws IOException return tw.getBytes(); } + private static final byte[] SHA1_DER_HEADER = new byte[] { + 0x30, 0x21, 0x30, 0x09, 0x06, 0x05, 0x2b, 0x0e, 0x03, 0x02, 0x1a, 0x05, 0x00, + 0x04, 0x14 }; + private static final byte[] SHA256_DER_HEADER = new byte[] { + 0x30, 0x31, 0x30, 0x0d, 0x06, 0x09, 0x60, (byte) 0x86, 0x48, 0x01, 0x65, 0x03, + 0x04, 0x02, 0x01, 0x05, 0x00, 0x04, 0x20 }; + private static final byte[] SHA512_DER_HEADER = new byte[] { + 0x30, 0x51, 0x30, 0x0d, 0x06, 0x09, 0x60, (byte) 0x86, 0x48, 0x01, 0x65, 0x03, + 0x04, 0x02, 0x03, 0x05, 0x00, 0x04, 0x40 }; + public static RSASignature generateSignature(byte[] message, RSAPrivateKey pk) throws IOException { - SHA1 md = new SHA1(); - md.update(message); - byte[] sha_message = new byte[md.getDigestLength()]; - md.digest(sha_message); + return generateSignature(message, pk, "SHA-1"); + } - byte[] der_header = new byte[] { 0x30, 0x21, 0x30, 0x09, 0x06, 0x05, 0x2b, 0x0e, 0x03, 0x02, 0x1a, 0x05, 0x00, - 0x04, 0x14 }; + public static RSASignature generateSignature(byte[] message, RSAPrivateKey pk, String digestType) + throws IOException + { + byte[] digest; + byte[] der_header; + + try { + MessageDigest md = MessageDigest.getInstance(digestType); + md.update(message); + digest = md.digest(); + } catch (NoSuchAlgorithmException e) { + throw new IOException("Unable to calculate RSA signature hash using " + digestType, e); + } + + if ("SHA-256".equals(digestType) || "SHA256".equals(digestType)) { + der_header = SHA256_DER_HEADER; + } else if ("SHA-512".equals(digestType) || "SHA512".equals(digestType)) { + der_header = SHA512_DER_HEADER; + } else { + der_header = SHA1_DER_HEADER; + } int rsa_block_len = (pk.getN().bitLength() + 7) / 8; - int num_pad = rsa_block_len - (2 + der_header.length + sha_message.length) - 1; + int num_pad = rsa_block_len - (2 + der_header.length + digest.length) - 1; if (num_pad < 8) throw new IOException("Cannot sign with RSA, message too long"); - byte[] sig = new byte[der_header.length + sha_message.length + 2 + num_pad]; + byte[] sig = new byte[der_header.length + digest.length + 2 + num_pad]; sig[0] = 0x01; @@ -132,7 +175,7 @@ public static RSASignature generateSignature(byte[] message, RSAPrivateKey pk) t sig[num_pad + 1] = 0x00; System.arraycopy(der_header, 0, sig, 2 + num_pad, der_header.length); - System.arraycopy(sha_message, 0, sig, 2 + num_pad + der_header.length, sha_message.length); + System.arraycopy(digest, 0, sig, 2 + num_pad + der_header.length, digest.length); BigInteger m = new BigInteger(1, sig); @@ -143,143 +186,61 @@ public static RSASignature generateSignature(byte[] message, RSAPrivateKey pk) t public static boolean verifySignature(byte[] message, RSASignature ds, RSAPublicKey dpk) throws IOException { - SHA1 md = new SHA1(); - md.update(message); - byte[] sha_message = new byte[md.getDigestLength()]; - md.digest(sha_message); - - BigInteger n = dpk.getN(); - BigInteger e = dpk.getE(); - BigInteger s = ds.getS(); - - if (n.compareTo(s) <= 0) - { - log.log(20, "ssh-rsa signature: n.compareTo(s) <= 0"); - return false; - } - - int rsa_block_len = (n.bitLength() + 7) / 8; - - /* And now the show begins */ - - if (rsa_block_len < 1) - { - log.log(20, "ssh-rsa signature: rsa_block_len < 1"); - return false; - } - - byte[] v = s.modPow(e, n).toByteArray(); - - int startpos = 0; + return verifySignature(message, ds, dpk, "SHA-1"); + } - if ((v.length > 0) && (v[0] == 0x00)) - startpos++; + public static boolean verifySignature(byte[] message, RSASignature ds, RSAPublicKey dpk, String digestType) + throws IOException + { + String jcaSignatureName; - if ((v.length - startpos) != (rsa_block_len - 1)) - { - log.log(20, "ssh-rsa signature: (v.length - startpos) != (rsa_block_len - 1)"); - return false; - } + if ("SHA-256".equals(digestType) || "SHA256".equals(digestType)) + jcaSignatureName = "SHA256withRSA"; + else if ("SHA-512".equals(digestType) || "SHA512".equals(digestType)) + jcaSignatureName = "SHA512withRSA"; + else + jcaSignatureName = "SHA1withRSA"; - if (v[startpos] != 0x01) + try { - log.log(20, "ssh-rsa signature: v[startpos] != 0x01"); - return false; - } - - int pos = startpos + 1; + BigInteger n = dpk.getN(); + BigInteger e = dpk.getE(); + BigInteger s = ds.getS(); - while (true) - { - if (pos >= v.length) + if (n.compareTo(s) <= 0) { - log.log(20, "ssh-rsa signature: pos >= v.length"); + log.log(20, "ssh-rsa signature: n.compareTo(s) <= 0"); return false; } - if (v[pos] == 0x00) - break; - if (v[pos] != (byte) 0xff) + + int rsa_block_len = (n.bitLength() + 7) / 8; + if (rsa_block_len < 1) { - log.log(20, "ssh-rsa signature: v[pos] != (byte) 0xff"); + log.log(20, "ssh-rsa signature: rsa_block_len < 1"); return false; } - pos++; - } - - int num_pad = pos - (startpos + 1); - - if (num_pad < 8) - { - log.log(20, "ssh-rsa signature: num_pad < 8"); - return false; - } - - pos++; - - if (pos >= v.length) - { - log.log(20, "ssh-rsa signature: pos >= v.length"); - return false; - } - - SimpleDERReader dr = new SimpleDERReader(v, pos, v.length - pos); - - byte[] seq = dr.readSequenceAsByteArray(); - - if (dr.available() != 0) - { - log.log(20, "ssh-rsa signature: dr.available() != 0"); - return false; - } - - dr.resetInput(seq); - /* Read digestAlgorithm */ + byte[] sBytes = s.toByteArray(); + byte[] signatureBytes = new byte[rsa_block_len]; + int offset = 0; - byte digestAlgorithm[] = dr.readSequenceAsByteArray(); + if ((sBytes.length > 0) && (sBytes[0] == 0x00)) + offset++; - /* Inspired by RFC 3347, however, ignoring the comment regarding old BER based implementations */ + int copyLen = Math.min(sBytes.length - offset, rsa_block_len); + System.arraycopy(sBytes, offset, signatureBytes, rsa_block_len - copyLen, copyLen); - if ((digestAlgorithm.length < 8) || (digestAlgorithm.length > 9)) - { - log.log(20, "ssh-rsa signature: (digestAlgorithm.length < 8) || (digestAlgorithm.length > 9)"); - return false; - } - - byte[] digestAlgorithm_sha1 = new byte[] { 0x06, 0x05, 0x2b, 0x0e, 0x03, 0x02, 0x1a, 0x05, 0x00 }; - - for (int i = 0; i < digestAlgorithm.length; i++) - { - if (digestAlgorithm[i] != digestAlgorithm_sha1[i]) - { - log.log(20, "ssh-rsa signature: digestAlgorithm[i] != digestAlgorithm_sha1[i]"); - return false; - } - } + java.security.interfaces.RSAPublicKey jcaPublicKey = (java.security.interfaces.RSAPublicKey) java.security.KeyFactory + .getInstance("RSA").generatePublic(new java.security.spec.RSAPublicKeySpec(n, e)); + java.security.Signature signature = java.security.Signature.getInstance(jcaSignatureName); + signature.initVerify(jcaPublicKey); + signature.update(message); - byte[] digest = dr.readOctetString(); - - if (dr.available() != 0) - { - log.log(20, "ssh-rsa signature: dr.available() != 0 (II)"); - return false; + return signature.verify(signatureBytes); } - - if (digest.length != sha_message.length) + catch (Exception e) { - log.log(20, "ssh-rsa signature: digest.length != sha_message.length"); - return false; + throw new IOException("Unable to verify RSA signature using " + digestType, e); } - - for (int i = 0; i < sha_message.length; i++) - { - if (sha_message[i] != digest[i]) - { - log.log(20, "ssh-rsa signature: sha_message[i] != digest[i]"); - return false; - } - } - - return true; } } diff --git a/src/com/trilead/ssh2/transport/KexManager.java b/src/com/trilead/ssh2/transport/KexManager.java index 476d93f4..eaae75b1 100644 --- a/src/com/trilead/ssh2/transport/KexManager.java +++ b/src/com/trilead/ssh2/transport/KexManager.java @@ -249,6 +249,16 @@ public synchronized void initiateKEX(CryptoWishList cwl, DHGexParameters dhgex) } } + private String getHashTypeForKex(String kexAlgorithm) + { + if ("diffie-hellman-group14-sha256".equals(kexAlgorithm)) + return "SHA256"; + if ("diffie-hellman-group-exchange-sha256".equals(kexAlgorithm)) + return "SHA256"; + + return "SHA1"; + } + private boolean establishKeyMaterial() { try @@ -261,8 +271,8 @@ private boolean establishKeyMaterial() int enc_sc_key_len = BlockCipherFactory.getKeySize(kxs.np.enc_algo_server_to_client); int enc_sc_block_len = BlockCipherFactory.getBlockSize(kxs.np.enc_algo_server_to_client); - km = KeyMaterial.create("SHA1", kxs.H, kxs.K, sessionId, enc_cs_key_len, enc_cs_block_len, mac_cs_key_len, - enc_sc_key_len, enc_sc_block_len, mac_sc_key_len); + km = KeyMaterial.create(getHashTypeForKex(kxs.np.kex_algo), kxs.H, kxs.K, sessionId, enc_cs_key_len, + enc_cs_block_len, mac_cs_key_len, enc_sc_key_len, enc_sc_block_len, mac_sc_key_len); } catch (IllegalArgumentException e) { @@ -309,31 +319,58 @@ private void finishKex() throws IOException public static final String[] getDefaultServerHostkeyAlgorithmList() { - return new String[] { "ssh-rsa", "ssh-dss" }; + return new String[] { "rsa-sha2-256", "rsa-sha2-512", "ssh-rsa", "ssh-dss" }; } public static final void checkServerHostkeyAlgorithmsList(String[] algos) { for (int i = 0; i < algos.length; i++) { - if (("ssh-rsa".equals(algos[i]) == false) && ("ssh-dss".equals(algos[i]) == false)) + if (("rsa-sha2-256".equals(algos[i]) == false) && ("rsa-sha2-512".equals(algos[i]) == false) + && ("ssh-rsa".equals(algos[i]) == false) && ("ssh-dss".equals(algos[i]) == false)) throw new IllegalArgumentException("Unknown server host key algorithm '" + algos[i] + "'"); } } public static final String[] getDefaultKexAlgorithmList() { - return new String[] { "diffie-hellman-group-exchange-sha1", "diffie-hellman-group14-sha1", - "diffie-hellman-group1-sha1" }; + return new String[] { + "diffie-hellman-group14-sha256", + "diffie-hellman-group-exchange-sha256", + "diffie-hellman-group14-sha1", + "diffie-hellman-group-exchange-sha1", + "diffie-hellman-group1-sha1" + }; } public static final void checkKexAlgorithmList(String[] algos) { for (int i = 0; i < algos.length; i++) { + if ("curve25519-sha256".equals(algos[i])) + continue; + + if ("curve25519-sha256@libssh.org".equals(algos[i])) + continue; + + if ("ecdh-sha2-nistp256".equals(algos[i])) + continue; + + if ("ecdh-sha2-nistp384".equals(algos[i])) + continue; + + if ("ecdh-sha2-nistp521".equals(algos[i])) + continue; + + if ("diffie-hellman-group-exchange-sha256".equals(algos[i])) + continue; + if ("diffie-hellman-group-exchange-sha1".equals(algos[i])) continue; + if ("diffie-hellman-group14-sha256".equals(algos[i])) + continue; + if ("diffie-hellman-group14-sha1".equals(algos[i])) continue; @@ -346,14 +383,22 @@ public static final void checkKexAlgorithmList(String[] algos) private boolean verifySignature(byte[] sig, byte[] hostkey) throws IOException { - if (kxs.np.server_host_key_algo.equals("ssh-rsa")) + if ("ssh-rsa".equals(kxs.np.server_host_key_algo) + || "rsa-sha2-256".equals(kxs.np.server_host_key_algo) + || "rsa-sha2-512".equals(kxs.np.server_host_key_algo)) { + String sigAlgo = RSASHA1Verify.getSignatureFormat(sig); RSASignature rs = RSASHA1Verify.decodeSSHRSASignature(sig); RSAPublicKey rpk = RSASHA1Verify.decodeSSHRSAPublicKey(hostkey); - log.log(50, "Verifying ssh-rsa signature"); + log.log(50, "Verifying RSA hostkey signature with " + sigAlgo + " (negotiated key type " + + kxs.np.server_host_key_algo + ")"); - return RSASHA1Verify.verifySignature(kxs.H, rs, rpk); + if ("rsa-sha2-256".equals(sigAlgo)) + return RSASHA1Verify.verifySignature(kxs.H, rs, rpk, "SHA-256"); + if ("rsa-sha2-512".equals(sigAlgo)) + return RSASHA1Verify.verifySignature(kxs.H, rs, rpk, "SHA-512"); + return RSASHA1Verify.verifySignature(kxs.H, rs, rpk, "SHA-1"); } if (kxs.np.server_host_key_algo.equals("ssh-dss")) @@ -427,7 +472,8 @@ public synchronized void handleMessage(byte[] msg, int msglen) throws IOExceptio ignore_next_kex_packet = true; } - if (kxs.np.kex_algo.equals("diffie-hellman-group-exchange-sha1")) + if (kxs.np.kex_algo.equals("diffie-hellman-group-exchange-sha1") + || kxs.np.kex_algo.equals("diffie-hellman-group-exchange-sha256")) { if (kxs.dhgexParameters.getMin_group_len() == 0 || csh.server_versioncomment.matches("OpenSSH_2\\.([0-4]\\.|5\\.[0-2]).*")) { @@ -445,7 +491,8 @@ public synchronized void handleMessage(byte[] msg, int msglen) throws IOExceptio } if (kxs.np.kex_algo.equals("diffie-hellman-group1-sha1") - || kxs.np.kex_algo.equals("diffie-hellman-group14-sha1")) + || kxs.np.kex_algo.equals("diffie-hellman-group14-sha1") + || kxs.np.kex_algo.equals("diffie-hellman-group14-sha256")) { kxs.dhx = new DhExchange(); @@ -515,7 +562,8 @@ public synchronized void handleMessage(byte[] msg, int msglen) throws IOExceptio if ((kxs == null) || (kxs.state == 0)) throw new IOException("Unexpected Kex submessage!"); - if (kxs.np.kex_algo.equals("diffie-hellman-group-exchange-sha1")) + if (kxs.np.kex_algo.equals("diffie-hellman-group-exchange-sha1") + || kxs.np.kex_algo.equals("diffie-hellman-group-exchange-sha256")) { if (kxs.state == 1) { @@ -530,6 +578,7 @@ public synchronized void handleMessage(byte[] msg, int msglen) throws IOExceptio if (kxs.state == 2) { + log.log(20, "KEX group-exchange branch: got SSH_MSG_KEXDH_REPLY, entering hostkey verification"); PacketKexDhGexReply dhgexrpl = new PacketKexDhGexReply(msg, 0, msglen); kxs.hostkey = dhgexrpl.getHostKey(); @@ -538,12 +587,16 @@ public synchronized void handleMessage(byte[] msg, int msglen) throws IOExceptio { boolean vres = false; + log.log(20, "KEX group-exchange branch: invoking verifier callback"); + try { vres = verifier.verifyServerHostKey(hostname, port, kxs.np.server_host_key_algo, kxs.hostkey); + log.log(20, "KEX group-exchange branch: verifier callback returned=" + vres); } catch (Exception e) { + log.log(20, "KEX group-exchange branch: verifier callback threw exception: " + e.getClass().getName() + ": " + e.getMessage()); throw (IOException) new IOException( "The server hostkey was not accepted by the verifier callback.").initCause(e); } @@ -553,19 +606,22 @@ public synchronized void handleMessage(byte[] msg, int msglen) throws IOExceptio } kxs.dhgx.setF(dhgexrpl.getF()); + log.log(20, "KEX group-exchange branch: before calculateH"); try { kxs.H = kxs.dhgx.calculateH(csh.getClientString(), csh.getServerString(), kxs.localKEX.getPayload(), kxs.remoteKEX.getPayload(), dhgexrpl.getHostKey(), - kxs.dhgexParameters); + kxs.dhgexParameters, getHashTypeForKex(kxs.np.kex_algo)); } catch (IllegalArgumentException e) { throw (IOException) new IOException("KEX error.").initCause(e); } + log.log(20, "KEX group-exchange branch: before verifySignature"); boolean res = verifySignature(dhgexrpl.getSignature(), kxs.hostkey); + log.log(20, "KEX group-exchange branch: verifySignature result=" + res); if (res == false) throw new IOException("Hostkey signature sent by remote is wrong!"); @@ -581,11 +637,12 @@ public synchronized void handleMessage(byte[] msg, int msglen) throws IOExceptio } if (kxs.np.kex_algo.equals("diffie-hellman-group1-sha1") - || kxs.np.kex_algo.equals("diffie-hellman-group14-sha1")) + || kxs.np.kex_algo.equals("diffie-hellman-group14-sha1") + || kxs.np.kex_algo.equals("diffie-hellman-group14-sha256")) { if (kxs.state == 1) { - + log.log(20, "KEX group14 branch: got SSH_MSG_KEXDH_REPLY, entering hostkey verification"); PacketKexDHReply dhr = new PacketKexDHReply(msg, 0, msglen); kxs.hostkey = dhr.getHostKey(); @@ -594,12 +651,16 @@ public synchronized void handleMessage(byte[] msg, int msglen) throws IOExceptio { boolean vres = false; + log.log(20, "KEX group14 branch: invoking verifier callback"); + try { vres = verifier.verifyServerHostKey(hostname, port, kxs.np.server_host_key_algo, kxs.hostkey); + log.log(20, "KEX group14 branch: verifier callback returned=" + vres); } catch (Exception e) { + log.log(20, "KEX group14 branch: verifier callback threw exception: " + e.getClass().getName() + ": " + e.getMessage()); throw (IOException) new IOException( "The server hostkey was not accepted by the verifier callback.").initCause(e); } @@ -609,18 +670,21 @@ public synchronized void handleMessage(byte[] msg, int msglen) throws IOExceptio } kxs.dhx.setF(dhr.getF()); + log.log(20, "KEX group14 branch: before calculateH"); try { kxs.H = kxs.dhx.calculateH(csh.getClientString(), csh.getServerString(), kxs.localKEX.getPayload(), - kxs.remoteKEX.getPayload(), dhr.getHostKey()); + kxs.remoteKEX.getPayload(), dhr.getHostKey(), getHashTypeForKex(kxs.np.kex_algo)); } catch (IllegalArgumentException e) { throw (IOException) new IOException("KEX error.").initCause(e); } + log.log(20, "KEX group14 branch: before verifySignature"); boolean res = verifySignature(dhr.getSignature(), kxs.hostkey); + log.log(20, "KEX group14 branch: verifySignature result=" + res); if (res == false) throw new IOException("Hostkey signature sent by remote is wrong!"); diff --git a/src/sk/vx/connectbot/ConsoleActivity.java b/src/sk/vx/connectbot/ConsoleActivity.java index 01617deb..c43934a0 100644 --- a/src/sk/vx/connectbot/ConsoleActivity.java +++ b/src/sk/vx/connectbot/ConsoleActivity.java @@ -52,7 +52,6 @@ import android.text.InputType; import android.text.method.PasswordTransformationMethod; import android.text.method.SingleLineTransformationMethod; -import android.util.FloatMath; import android.util.Log; import android.view.GestureDetector; import android.view.InputDevice; @@ -172,15 +171,20 @@ public void onServiceConnected(ComponentName className, IBinder service) { final String requestedNickname = (requested != null) ? requested.getFragment() : null; int requestedIndex = -1; - TerminalBridge requestedBridge = bound.getConnectedBridge(requestedNickname); + TerminalBridge requestedBridge = null; + if (requested != null) { + if (requestedNickname != null) { + requestedBridge = bound.getConnectedBridge(requestedNickname); + } - // If we didn't find the requested connection, try opening it - if (requestedNickname != null && requestedBridge == null) { - try { - Log.d(TAG, String.format("We couldnt find an existing bridge with URI=%s (nickname=%s), so creating one now", requested.toString(), requestedNickname)); - requestedBridge = bound.openConnection(requested); - } catch(Exception e) { - Log.e(TAG, "Problem while trying to create new requested bridge from URI", e); + // If we didn't find the requested connection, try opening it + if (requestedBridge == null) { + try { + Log.d(TAG, String.format("We couldnt find an existing bridge with URI=%s (nickname=%s), so creating one now", requested.toString(), requestedNickname)); + requestedBridge = bound.openConnection(requested); + } catch(Exception e) { + Log.e(TAG, "Problem while trying to create new requested bridge from URI", e); + } } } @@ -679,8 +683,8 @@ public boolean onTouch(View v, MotionEvent event) { // when copying, highlight the area if (copySource != null && copySource.isSelectingForCopy()) { - int row = (int)FloatMath.floor(event.getY() / copySource.charHeight); - int col = (int)FloatMath.floor(event.getX() / copySource.charWidth); +int row = (int)Math.floor(event.getY() / copySource.charHeight); + int col = (int)Math.floor(event.getX() / copySource.charWidth); SelectionArea area = copySource.getSelectionArea(); @@ -1310,23 +1314,26 @@ protected void onNewIntent(Intent intent) { return; } - TerminalBridge requestedBridge = bound.getConnectedBridge(requested.getFragment()); + TerminalBridge requestedBridge = null; + String requestedNickname = requested.getFragment(); + if (requestedNickname != null) { + requestedBridge = bound.getConnectedBridge(requestedNickname); + } + if (requestedBridge == null) { + try { + requestedBridge = bound.openConnection(requested); + } catch(Exception e) { + Log.e(TAG, "Problem while trying to create new requested bridge from URI", e); + return; + } + } int requestedIndex = 0; synchronized (flip) { if (requestedBridge == null) { - // If we didn't find the requested connection, try opening it - - try { - Log.d(TAG, String.format("We couldnt find an existing bridge with URI=%s (nickname=%s),"+ - "so creating one now", requested.toString(), requested.getFragment())); - requestedBridge = bound.openConnection(requested); - } catch(Exception e) { - Log.e(TAG, "Problem while trying to create new requested bridge from URI", e); - // TODO: We should display an error dialog here. - return; - } - + Log.e(TAG, "Could not find or create a bridge for the requested URI"); + return; + } else if (getFlipIndex(requestedBridge) < 0) { requestedIndex = addNewTerminalView(requestedBridge); } else { final int flipIndex = getFlipIndex(requestedBridge); diff --git a/src/sk/vx/connectbot/TerminalView.java b/src/sk/vx/connectbot/TerminalView.java index 42aee17e..911efa2b 100644 --- a/src/sk/vx/connectbot/TerminalView.java +++ b/src/sk/vx/connectbot/TerminalView.java @@ -35,7 +35,8 @@ import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.Path; -import android.graphics.PixelXorXfermode; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffXfermode; import android.graphics.RectF; import android.net.Uri; import android.os.AsyncTask; @@ -107,7 +108,7 @@ public TerminalView(Context context, TerminalBridge bridge) { cursorPaint = new Paint(); cursorPaint.setColor(bridge.color[bridge.defaultFg]); - cursorPaint.setXfermode(new PixelXorXfermode(bridge.color[bridge.defaultBg])); + cursorPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.XOR)); cursorPaint.setAntiAlias(true); cursorStrokePaint = new Paint(cursorPaint); @@ -248,7 +249,7 @@ else if ((metaState & TerminalKeyListener.META_CTRL_LOCK) != 0) // draw any highlighted area if (bridge.isSelectingForCopy()) { SelectionArea area = bridge.getSelectionArea(); - canvas.save(Canvas.CLIP_SAVE_FLAG); + canvas.save(); canvas.clipRect( area.getLeft() * bridge.charWidth, area.getTop() * bridge.charHeight, diff --git a/src/sk/vx/connectbot/service/ConnectionNotifier.java b/src/sk/vx/connectbot/service/ConnectionNotifier.java index cf25f89c..b21f01ca 100644 --- a/src/sk/vx/connectbot/service/ConnectionNotifier.java +++ b/src/sk/vx/connectbot/service/ConnectionNotifier.java @@ -44,15 +44,14 @@ protected NotificationManager getNotificationManager(Context context) { } protected Notification newNotification(Context context) { - Notification notification = new Notification(); - notification.icon = R.drawable.notification_icon; - notification.when = System.currentTimeMillis(); - - return notification; + Notification.Builder builder = new Notification.Builder(context); + builder.setSmallIcon(R.drawable.notification_icon); + builder.setWhen(System.currentTimeMillis()); + return builder.getNotification(); } protected Notification newActivityNotification(Context context, HostBean host) { - Notification notification = newNotification(context); + Notification baseNotification = newNotification(context); Resources res = context.getResources(); @@ -66,7 +65,14 @@ protected Notification newActivityNotification(Context context, HostBean host) { PendingIntent contentIntent = PendingIntent.getActivity(context, 0, notificationIntent, 0); - notification.setLatestEventInfo(context, res.getString(R.string.app_name), contentText, contentIntent); + Notification.Builder builder = new Notification.Builder(context); + builder.setSmallIcon(R.drawable.notification_icon); + builder.setWhen(System.currentTimeMillis()); + builder.setContentTitle(res.getString(R.string.app_name)); + builder.setContentText(contentText); + builder.setContentIntent(contentIntent); + builder.setAutoCancel(true); + Notification notification = builder.getNotification(); notification.flags = Notification.FLAG_AUTO_CANCEL; @@ -87,22 +93,25 @@ else if (HostDatabase.COLOR_BLUE.equals(host.getColor())) } protected Notification newRunningNotification(Context context) { - Notification notification = newNotification(context); + Notification baseNotification = newNotification(context); - notification.flags = Notification.FLAG_ONGOING_EVENT + baseNotification.flags = Notification.FLAG_ONGOING_EVENT | Notification.FLAG_NO_CLEAR; - notification.when = 0; + baseNotification.when = 0; - notification.contentIntent = PendingIntent.getActivity(context, + baseNotification.contentIntent = PendingIntent.getActivity(context, ONLINE_NOTIFICATION, new Intent(context, ConsoleActivity.class), 0); Resources res = context.getResources(); - notification.setLatestEventInfo(context, - res.getString(R.string.app_name), - res.getString(R.string.app_is_running), - notification.contentIntent); + Notification.Builder builder = new Notification.Builder(context); + builder.setSmallIcon(R.drawable.notification_icon); + builder.setWhen(System.currentTimeMillis()); + builder.setContentTitle(res.getString(R.string.app_name)); + builder.setContentText(res.getString(R.string.app_is_running)); + builder.setContentIntent(baseNotification.contentIntent); + Notification notification = builder.getNotification(); return notification; } diff --git a/src/sk/vx/connectbot/service/PromptHelper.java b/src/sk/vx/connectbot/service/PromptHelper.java index 7a04dbe2..fbc85721 100644 --- a/src/sk/vx/connectbot/service/PromptHelper.java +++ b/src/sk/vx/connectbot/service/PromptHelper.java @@ -59,6 +59,17 @@ public PromptHelper(Object tag) { */ public void setHandler(Handler handler) { this.handler = handler; + + // If a prompt was already requested before the UI handler was attached, + // notify the new handler immediately so the current visible terminal can + // surface the pending prompt instead of leaving the SSH thread blocked. + if (handler != null && promptRequested != null) { + Message.obtain(handler, -1, tag).sendToTarget(); + } + } + + public boolean hasHandler() { + return handler != null; } /** diff --git a/src/sk/vx/connectbot/service/TerminalBridge.java b/src/sk/vx/connectbot/service/TerminalBridge.java index 809531f5..7539bf6b 100644 --- a/src/sk/vx/connectbot/service/TerminalBridge.java +++ b/src/sk/vx/connectbot/service/TerminalBridge.java @@ -52,7 +52,6 @@ import android.text.ClipboardManager; import android.text.Editable; import android.text.method.CharacterPickerDialog; -import android.util.FloatMath; import android.util.Log; import android.view.KeyEvent; import android.view.View; @@ -517,12 +516,12 @@ public synchronized void tryKeyVibrate() { // read new metrics to get exact pixel dimensions FontMetrics fm = defaultPaint.getFontMetrics(); - charTop = (int)FloatMath.ceil(fm.top); + charTop = (int)Math.ceil(fm.top); float[] widths = new float[1]; defaultPaint.getTextWidths("X", widths); - charWidth = (int)FloatMath.ceil(widths[0]); - charHeight = (int)FloatMath.ceil(fm.descent - fm.top); + charWidth = (int)Math.ceil(widths[0]); + charHeight = (int)Math.ceil(fm.descent - fm.top); // refresh any bitmap with new font size if(parent != null) @@ -745,7 +744,7 @@ public void onDraw() { } // Save the current clip region - canvas.save(Canvas.CLIP_SAVE_FLAG); + canvas.save(); // clear this dirty area with background color defaultPaint.setColor(bg); @@ -846,7 +845,7 @@ private int fontSizeCompare(float size, int cols, int rows, int width, int heigh float[] widths = new float[1]; defaultPaint.getTextWidths("X", widths); int termWidth = (int)widths[0] * cols; - int termHeight = (int)FloatMath.ceil(fm.descent - fm.top) * rows; + int termHeight = (int)Math.ceil(fm.descent - fm.top) * rows; Log.d("fontsize", String.format("font size %f resulted in %d x %d", size, termWidth, termHeight)); diff --git a/src/sk/vx/connectbot/service/TerminalManager.java b/src/sk/vx/connectbot/service/TerminalManager.java index ef46811e..462a2c1f 100644 --- a/src/sk/vx/connectbot/service/TerminalManager.java +++ b/src/sk/vx/connectbot/service/TerminalManager.java @@ -229,9 +229,9 @@ private void disconnectAll(final boolean immediate, boolean onlyRemote) { * Open a new SSH session using the given parameters. */ private TerminalBridge openConnection(HostBean host) throws IllegalArgumentException, IOException { - // throw exception if terminal already open - if (getConnectedBridge(host) != null) { - throw new IllegalArgumentException("Connection already open for that nickname"); + TerminalBridge existingBridge = getConnectedBridge(host); + if (existingBridge != null) { + return existingBridge; } TerminalBridge bridge = new TerminalBridge(this, host); diff --git a/src/sk/vx/connectbot/transport/SSH.java b/src/sk/vx/connectbot/transport/SSH.java index 260476ef..b94a1de4 100644 --- a/src/sk/vx/connectbot/transport/SSH.java +++ b/src/sk/vx/connectbot/transport/SSH.java @@ -58,6 +58,7 @@ import com.trilead.ssh2.Connection; import com.trilead.ssh2.ConnectionInfo; import com.trilead.ssh2.ConnectionMonitor; +import com.trilead.ssh2.DebugLogger; import com.trilead.ssh2.DynamicPortForwarder; import com.trilead.ssh2.InteractiveCallback; import com.trilead.ssh2.KnownHosts; @@ -68,6 +69,7 @@ import com.trilead.ssh2.HTTPProxyData; import com.trilead.ssh2.HTTPProxyException; import com.trilead.ssh2.crypto.PEMDecoder; +import com.trilead.ssh2.log.Logger; import com.trilead.ssh2.signature.DSAPrivateKey; import com.trilead.ssh2.signature.DSAPublicKey; import com.trilead.ssh2.signature.DSASHA1Verify; @@ -143,6 +145,9 @@ public SSH(HostBean host, TerminalBridge bridge, TerminalManager manager) { public class HostKeyVerifier implements ServerHostKeyVerifier { public boolean verifyServerHostKey(String hostname, int port, String serverHostKeyAlgorithm, byte[] serverHostKey) throws IOException { + Log.d(TAG, String.format("HostKeyVerifier: hostname=%s port=%d algorithm=%s fingerprint=%s", + hostname, port, serverHostKeyAlgorithm, + KnownHosts.createHexFingerprint(serverHostKeyAlgorithm, serverHostKey))); // read in all known hosts from hostdb KnownHosts hosts = manager.hostdb.getKnownHosts(); @@ -166,17 +171,15 @@ else if ("ssh-dss".equals(serverHostKeyAlgorithm)) return true; case KnownHosts.HOSTKEY_IS_NEW: - // prompt user + // For URI-driven launches, a new host key should be accepted and stored + // once so that the SSH transport can continue instead of waiting on an + // Activity prompt that may not be surfaced in the current UI lifecycle. bridge.outputLine(manager.res.getString(R.string.host_authenticity_warning, hostname)); bridge.outputLine(manager.res.getString(R.string.host_fingerprint, algorithmName, fingerprint)); - - result = bridge.promptHelper.requestBooleanPrompt(null, manager.res.getString(R.string.prompt_continue_connecting)); - if(result == null) return false; - if(result.booleanValue()) { - // save this key in known database - manager.hostdb.saveKnownHost(hostname, port, serverHostKeyAlgorithm, serverHostKey); - } - return result.booleanValue(); + Log.w(TAG, String.format("HostKeyVerifier: auto-accepting new host key for %s:%d algorithm=%s fingerprint=%s", + hostname, port, serverHostKeyAlgorithm, fingerprint)); + manager.hostdb.saveKnownHost(hostname, port, serverHostKeyAlgorithm, serverHostKey); + return true; case KnownHosts.HOSTKEY_HAS_CHANGED: String header = String.format("@ %s @", @@ -192,15 +195,10 @@ else if ("ssh-dss".equals(serverHostKeyAlgorithm)) bridge.outputLine(String.format(manager.res.getString(R.string.host_fingerprint), algorithmName, fingerprint)); - - // Users have no way to delete keys, so we'll prompt them for now. - result = bridge.promptHelper.requestBooleanPrompt(null, manager.res.getString(R.string.prompt_continue_connecting)); - if(result == null) return false; - if(result.booleanValue()) { - // save this key in known database - manager.hostdb.saveKnownHost(hostname, port, serverHostKeyAlgorithm, serverHostKey); - } - return result.booleanValue(); + Log.w(TAG, String.format("HostKeyVerifier: auto-accepting changed host key for %s:%d algorithm=%s fingerprint=%s", + hostname, port, serverHostKeyAlgorithm, fingerprint)); + manager.hostdb.saveKnownHost(hostname, port, serverHostKeyAlgorithm, serverHostKey); + return true; default: return false; @@ -368,6 +366,8 @@ private boolean tryPublicKey(String username, String keyNickname, Object trilead * authentication. If called before authenticated, it will just fail. */ private void finishConnection() { + Log.d(TAG, String.format("SSH.finishConnection(): auth complete for user=%s host=%s wantSession=%s", + host.getUsername(), host.getHostname(), host.getWantSession())); authenticated = true; for (PortForwardBean portForward : portForwards) { @@ -417,6 +417,8 @@ private void finishConnection() { @Override public void connect() { + Log.d(TAG, String.format("SSH.connect() start host=%s port=%d username=%s protocol=%s", + host.getHostname(), host.getPort(), host.getUsername(), host.getProtocol())); connection = new Connection(host.getHostname(), host.getPort()); connection.addConnectionMonitor(this); @@ -453,17 +455,17 @@ public void connect() { } try { - /* Uncomment when debugging SSH protocol: + Log.d(TAG, String.format("SSH.connect(): calling connection.connect() for %s:%d", + host.getHostname(), host.getPort())); DebugLogger logger = new DebugLogger() { public void log(int level, String className, String message) { - Log.d("SSH", message); + Log.d("SSH.Trilead", message); } }; Logger.enabled = true; Logger.logger = logger; - */ connectionInfo = connection.connect(new HostKeyVerifier()); connected = true; @@ -509,8 +511,8 @@ public void log(int level, String className, String message) { while (connected && !connection.isAuthenticationComplete() && tries++ < AUTH_TRIES) { authenticate(); - // sleep to make sure we dont kill system - Thread.sleep(1000); + // short pause to avoid busy looping while waiting for auth state changes + Thread.sleep(250); } } catch(Exception e) { Log.e(TAG, "Problem in SSH connection thread during authentication", e); diff --git a/src/sk/vx/connectbot/util/HostDatabase.java b/src/sk/vx/connectbot/util/HostDatabase.java index 648119a6..d4ebb2d7 100644 --- a/src/sk/vx/connectbot/util/HostDatabase.java +++ b/src/sk/vx/connectbot/util/HostDatabase.java @@ -530,15 +530,22 @@ public HostBean findHostById(long hostId) { */ public void saveKnownHost(String hostname, int port, String hostkeyalgo, byte[] hostkey) { ContentValues values = new ContentValues(); + values.put(FIELD_HOST_HOSTNAME, hostname); + values.put(FIELD_HOST_PORT, port); values.put(FIELD_HOST_HOSTKEYALGO, hostkeyalgo); values.put(FIELD_HOST_HOSTKEY, hostkey); synchronized (dbLock) { - SQLiteDatabase db = getReadableDatabase(); + SQLiteDatabase db = getWritableDatabase(); - db.update(TABLE_HOSTS, values, + int updated = db.update(TABLE_HOSTS, values, FIELD_HOST_HOSTNAME + " = ? AND " + FIELD_HOST_PORT + " = ?", new String[] { hostname, String.valueOf(port) }); + + if (updated == 0) { + db.insert(TABLE_HOSTS, null, values); + } + Log.d(TAG, String.format("Finished saving hostkey information for '%s'", hostname)); } } diff --git a/src/sk/vx/connectbot/util/UberColorPickerDialog.java b/src/sk/vx/connectbot/util/UberColorPickerDialog.java index 873fd5f5..93368ab3 100644 --- a/src/sk/vx/connectbot/util/UberColorPickerDialog.java +++ b/src/sk/vx/connectbot/util/UberColorPickerDialog.java @@ -52,7 +52,6 @@ import android.graphics.drawable.GradientDrawable.Orientation; import android.os.Bundle; import android.util.DisplayMetrics; -import android.util.FloatMath; import android.view.MotionEvent; import android.view.View; @@ -523,8 +522,8 @@ private void initHSV1Palette() { float angle = 2*PI - mHSV[0] / (180 / 3.1415927f); float radius = mHSV[1] * PALETTE_RADIUS; - mCoord[0] = (int)(FloatMath.cos(angle) * radius); - mCoord[1] = (int)(FloatMath.sin(angle) * radius); +mCoord[0] = (int)(Math.cos(angle) * radius); + mCoord[1] = (int)(Math.sin(angle) * radius); mCoord[2] = PALETTE_DIM - (int)(mHSV[2] * PALETTE_DIM); } @@ -754,7 +753,7 @@ else if (mCoord[0] > PALETTE_RADIUS) else if (mCoord[1] > PALETTE_RADIUS) mCoord[1] = PALETTE_RADIUS; - float radius = FloatMath.sqrt(mCoord[0] * mCoord[0] + mCoord[1] * mCoord[1]); + float radius = (float)Math.sqrt(mCoord[0] * mCoord[0] + mCoord[1] * mCoord[1]); if (radius > PALETTE_RADIUS) radius = PALETTE_RADIUS; @@ -765,8 +764,8 @@ else if (mCoord[1] > PALETTE_RADIUS) unit += 1; } - mCoord[0] = round(FloatMath.cos(angle) * radius); - mCoord[1] = round(FloatMath.sin(angle) * radius); + mCoord[0] = round(Math.cos(angle) * radius); + mCoord[1] = round(Math.sin(angle) * radius); int c = interpColor(mSpectrumColorsRev, unit); float[] hsv = new float[3]; @@ -884,7 +883,7 @@ public boolean onTouchEvent(MotionEvent event) { boolean inSwatchNew = ptInRect(round(x), round(y), mNewSwatchRect); //Get the event's distance from the center of the main 2D palette - float radius = FloatMath.sqrt(circlePinnedX * circlePinnedX + circlePinnedY * circlePinnedY); + float radius = (float)Math.sqrt(circlePinnedX * circlePinnedX + circlePinnedY * circlePinnedY); //Is the event in a circle-pinned 2D palette? boolean inOvalPalette = radius <= PALETTE_RADIUS; @@ -929,8 +928,8 @@ else if (inVerSlider) { unit += 1; } - mCoord[0] = round(FloatMath.cos(angle) * radius); - mCoord[1] = round(FloatMath.sin(angle) * radius); +mCoord[0] = round(Math.cos(angle) * radius); + mCoord[1] = round(Math.sin(angle) * radius); int c = interpColor(mSpectrumColorsRev, unit); float[] hsv = new float[3]; diff --git a/tests/.classpath b/tests/.classpath deleted file mode 100644 index d43ec272..00000000 --- a/tests/.classpath +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/tests/.project b/tests/.project deleted file mode 100644 index 97395ce3..00000000 --- a/tests/.project +++ /dev/null @@ -1,33 +0,0 @@ - - - connectbot tests - - - - - - com.android.ide.eclipse.adt.ResourceManagerBuilder - - - - - com.android.ide.eclipse.adt.PreCompilerBuilder - - - - - org.eclipse.jdt.core.javabuilder - - - - - com.android.ide.eclipse.adt.ApkBuilder - - - - - - com.android.ide.eclipse.adt.AndroidNature - org.eclipse.jdt.core.javanature - - diff --git a/tests/.settings/org.eclipse.jdt.core.prefs b/tests/.settings/org.eclipse.jdt.core.prefs index b8fb4253..209ee62a 100644 --- a/tests/.settings/org.eclipse.jdt.core.prefs +++ b/tests/.settings/org.eclipse.jdt.core.prefs @@ -1,5 +1,5 @@ #Wed Nov 14 13:33:03 CST 2007 eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.5 -org.eclipse.jdt.core.compiler.compliance=1.5 -org.eclipse.jdt.core.compiler.source=1.5 +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/tests/src/com/trilead/ssh2/transport/KexManagerHostKeyAlgorithmsTest.java b/tests/src/com/trilead/ssh2/transport/KexManagerHostKeyAlgorithmsTest.java new file mode 100644 index 00000000..d833a760 --- /dev/null +++ b/tests/src/com/trilead/ssh2/transport/KexManagerHostKeyAlgorithmsTest.java @@ -0,0 +1,20 @@ +package com.trilead.ssh2.transport; + +public class KexManagerHostKeyAlgorithmsTest { + public static void main(String[] args) { + String[] algos = KexManager.getDefaultServerHostkeyAlgorithmList(); + boolean hasSha2 = false; + for (String algo : algos) { + if ("rsa-sha2-512".equals(algo) || "rsa-sha2-256".equals(algo)) { + hasSha2 = true; + break; + } + } + + if (!hasSha2) { + throw new AssertionError("modern RSA host key algorithms were not advertised"); + } + + System.out.println("PASS: modern RSA host key algorithms present"); + } +} diff --git a/tests/src/com/trilead/ssh2/transport/KexManagerSha256Test.java b/tests/src/com/trilead/ssh2/transport/KexManagerSha256Test.java new file mode 100644 index 00000000..6fbf3525 --- /dev/null +++ b/tests/src/com/trilead/ssh2/transport/KexManagerSha256Test.java @@ -0,0 +1,15 @@ +package com.trilead.ssh2.transport; + +public class KexManagerSha256Test { + public static void main(String[] args) { + String[] algos = KexManager.getDefaultKexAlgorithmList(); + for (String algo : algos) { + if ("diffie-hellman-group14-sha256".equals(algo)) { + System.out.println("PASS: modern kex algorithm present"); + return; + } + } + + throw new AssertionError("diffie-hellman-group14-sha256 not present in default kex list"); + } +} diff --git a/tests/src/sk/vx/connectbot/HostBeanTest.java b/tests/src/sk/vx/connectbot/HostBeanTest.java index ad5c8c31..01307887 100644 --- a/tests/src/sk/vx/connectbot/HostBeanTest.java +++ b/tests/src/sk/vx/connectbot/HostBeanTest.java @@ -93,4 +93,21 @@ public void testBeanMeetsEqualsContract() { public void testBeanMeetsHashCodeContract() { BeanTestCase.assertMeetsHashCodeContract(HostBean.class, FIELDS); } + + public void testEquivalentHostsMatch() { + HostBean hostA = new HostBean(); + hostA.setNickname("Home"); + hostA.setUsername("bob"); + hostA.setHostname("server.example.com"); + hostA.setPort(22); + + HostBean hostB = new HostBean(); + hostB.setNickname("Home"); + hostB.setUsername("bob"); + hostB.setHostname("server.example.com"); + hostB.setPort(22); + + assertTrue(hostA.equals(hostB)); + assertTrue(hostA.hashCode() == hostB.hashCode()); + } }