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=/**\n * @return the ${bare_field_name}\n *//**\n * @param ${param} the ${bare_field_name} to set\n *//**\n * ${tags}\n *//**\n * \n *//**\n * @author ${user}\n *\n * ${tags}\n *//**\n * \n *//**\n * ${tags}\n *//* (non-Javadoc)\n * ${see_to_overridden}\n *//**\n * ${tags}\n * ${see_to_target}\n */${filecomment}\n${package_declaration}\n\n${typecomment}\n${type_declaration}\n\n\n\n// ${todo} Auto-generated catch block\n${exception_var}.printStackTrace();// ${todo} Auto-generated method stub\n${body_statement}${body_statement}\n// ${todo} Auto-generated constructor stubreturn ${field};${field} \= ${param};/**\n * @return the ${bare_field_name}\n *//**\n * @param ${param} the ${bare_field_name} to set\n *//**\n * ${tags}\n *//**\n * \n *//**\n * @author ${user}\n *\n * ${tags}\n *//**\n * \n *//**\n * ${tags}\n *//* (non-Jsdoc)\n * ${see_to_overridden}\n *//**\n * ${tags}\n * ${see_to_target}\n */${filecomment}\n${package_declaration}\n\n${typecomment}\n${type_declaration}\n// ${todo} Auto-generated catch block\n${exception_var}.printStackTrace();// ${todo} Auto-generated function stub\n${body_statement}${body_statement}\n// ${todo} Auto-generated constructor stubreturn ${field};${field} \= ${param};
-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());
+ }
}