Compare commits

...

51 Commits

Author SHA1 Message Date
Ramkumar
31ed35407c Check error stream is not null before notifying error (#961)
Co-authored-by: Jeroen van Erp <jeroen@hierynomus.com>
2024-09-13 12:35:37 -06:00
David Handermann
f4f8071020 Improved Curve25519 Public Key Handling (#959)
- Modified Curve25519 negotiation to determine algorithm identifier length based on PublicKey.getEncoded() length instead of hard-coded value of 44
- Runtime length determination avoids differences in X25519 implementations on Java 11

Co-authored-by: Jeroen van Erp <jeroen@hierynomus.com>
2024-09-13 10:17:02 -06:00
Thomas Canava
f525ed0e5b Allow custom scp download command line (#958)
Co-authored-by: Thomas Canava <thomas.canava@soprasteria.com>
2024-08-23 09:56:39 +02:00
Jeroen van Erp
93046f315e Provide public SCP command line builder (Fixes \#951)
Signed-off-by: Jeroen van Erp <jeroen@hierynomus.com>
2024-08-20 14:46:50 +02:00
Jeroen van Erp
54376b7622 Add SFTP integration test
Signed-off-by: Jeroen van Erp <jeroen@hierynomus.com>
2024-08-20 09:17:28 +02:00
David Handermann
f0e92c920f Upgraded Bouncy Castle and other dependencies (#945)
- Upgraded Bouncy Castle from 1.78 to 1.78.1
- Upgraded SLF4J from 2.0.7 to 2.0.13
- Upgraded SSHD from 2.10.0 to 2.12.1
- Upgraded Logback from 1.3.8 to 1.3.14
- Upgraded Testcontainers from 1.18.3 to 1.19.8
- Upgraded setup-java action to version 4
- Upgraded checkout action to version 4
2024-05-20 13:52:02 +02:00
David Kocher
09e2ca512e Add overloaded init methods that take the public key from a stream an… (#908)
* Add overloaded init methods that take the public key from a stream and properly initialize. Resolves #907.

* Override public key.
2024-04-29 16:46:38 +02:00
Raul Santelices
607e80591c Fix for issue #910: Bad packet received by server when hearbeat is enabled (#911)
* Fix for issue #910: Bad packet received by server when hearbeat is enabled

* Address re-keying case too

---------

Co-authored-by: Jeroen van Erp <jeroen@hierynomus.com>
2024-04-18 09:31:24 +02:00
Raul Santelices
079cb08fb0 Fix false-alarm timeout exception when waiting for key exchange to complete (#912)
Co-authored-by: Jeroen van Erp <jeroen@hierynomus.com>
2024-04-18 09:22:07 +02:00
eshaffer321
cf340c2a09 Update bouncyCastle to 1.78 to mitigate CVE-2024-29857 (#938)
Bouncy Caste version before 1.78 have 

CVE-2024-29857 - Importing an EC certificate with specially crafted F2m parameters can cause high CPU usage during parameter evaluation.

Is sshj impacted by this vulnerability?
2024-04-17 12:32:46 +02:00
Eric Vigeant
586a66420e Close Session when closing SCPEngine or SFTPEngine (#926)
Co-authored-by: Jeroen van Erp <jeroen@hierynomus.com>
2024-04-15 20:31:54 +02:00
Lucas
624fe839cb Support premature termination of listing (#928)
* Support premature termination of listing

* Added license header + small refactor

---------

Co-authored-by: Jeroen van Erp <jeroen@hierynomus.com>
2024-04-15 20:18:15 +02:00
Henning Pöttker
81d77d277c Don't send keep alive signals before kex is done (#934)
Otherwise, they could interfere with strict key exchange.

Co-authored-by: Jeroen van Erp <jeroen@hierynomus.com>
2024-04-15 09:29:06 +02:00
Vladimir Lagunov
70af58d199 Wrap IllegalArgumentException thrown by Base64 decoder (#936)
* Wrap IllegalArgumentException thrown by Base64 decoder

Some time ago, there had been `net.schmizz.sshj.common.Base64`. This class used to throw `IOException` in case of any problem. Although `IOException` isn't an appropriate class for indicating on parsing issues, a lot of code has been expecting `IOException` from Base64.

Once, the old Base64 decoder was replaced with the one, bundled into Java 14 (see f35c2bd4ce). Copy-paste elimination and switching to standard implementations is undoubtedly a good decision.

Unfortunately, `java.util.Base64.Decoder` brought a pesky issue. It throws `IllegalArgumentException` in case of any problem. Since it is an unchecked exception, it was quite challenging to notice it. It's especially challenging because the error appears during processing malformed base64 strings. So, a lot of places in the code kept expecting `IOException`. Sudden `IllegalArgumentException` led to authentication termination in cases where everything used to work perfectly.

One of such issues is already found and fixed: 03f8b2224d

This commit represents a work, based on revising every change made in f35c2bd4ce. It should fix all other similar issues.

* squash! Wrap IllegalArgumentException thrown by Base64 decoder

Rename Base64DecodeError -> Base64DecodingException

* squash! Wrap IllegalArgumentException thrown by Base64 decoder

A better warning message in KnownHostMatchers

* squash! Wrap IllegalArgumentException thrown by Base64 decoder

A better error message in OpenSSHKeyFileUtil

* squash! Wrap IllegalArgumentException thrown by Base64 decoder

A better error message in OpenSSHKeyV1KeyFile

* squash! Wrap IllegalArgumentException thrown by Base64 decoder

Get rid of unnecessary `throws IOException` in Base64Decoder

* squash! Wrap IllegalArgumentException thrown by Base64 decoder

Better error messages in OpenSSHKeyFileUtil and PuTTYKeyFile
2024-04-15 09:23:53 +02:00
Martin Volf
c0d1519ee2 connected sockets can be passed to the library (#925)
* connected sockets can be passed to the library

fixes hierynomus/sshj#924

Signed-off-by: Martin Volf <vlci.doupe@gmail.com>

* removed pointless socket check; test coverage improved

Signed-off-by: Martin Volf <vlci.doupe@gmail.com>

* better test coverage

Signed-off-by: Martin Volf <vlci.doupe@gmail.com>

---------

Signed-off-by: Martin Volf <vlci.doupe@gmail.com>
2024-01-29 11:49:43 +01:00
kegelh
03f8b2224d known_hosts parsing does not ignore malformed base64 strings since 0.36.0 (#922) 2024-01-26 13:36:29 +01:00
Pascal Schumacher
f94444bc53 Fix typo in README.adoc (#920) 2024-01-02 16:02:45 +01:00
Jeroen van Erp
dc6b20772b Prepare release 0.38.0 2024-01-02 09:26:09 +01:00
Henning Pöttker
81e87a4d35 Add unit tests of strict key exchange extension (#918) 2023-12-23 10:26:29 +01:00
Henning Pöttker
a262f51900 Implement OpenSSH strict key exchange extension (#917) 2023-12-21 22:33:54 +01:00
David Kocher
50c753dc58 Fixed writing known hosts key string (#903)
* Fix #902.

* Add test.
2023-11-21 21:24:28 +01:00
Raul Santelices
1c547886c8 Fix for Remote port forwarding buffers can grow without limits (issue #658) (#913)
* Fix for Remote port forwarding buffers can grow without limits (issue #658)

* Update test classes to use JUnit 5

* Fix MB computation
2023-11-21 21:21:35 +01:00
Jeroen van Erp
b7dc869a13 Moved host verification tests to JUnit5 (#906) 2023-10-23 12:24:29 +02:00
Florian Klemenz
4774721b49 adds fallback to posix-rename@openssh.com extension if possible and c… (#827)
* adds fallback to posix-rename@openssh.com extension if possible and communicates possible problems with flags to the developer

* Adds '{}' around if/else statements

* adds basic tests for file rename

* fix comments

* fixes indentation

* adds helper methods to make existing sftp rename tests more concise

* adds basic test for atomic rewrite

* adds possibility to request a specific client version (e.g. for testing purposes)

* adds testcases for SFTP rename flags fallback behaviour

* refactoring to make SFTPEngine.init(int requestedVersion) protected

---------

Co-authored-by: Florian Klemenz <florian.klemenz@fau.de>
Co-authored-by: Jeroen van Erp <jeroen@hierynomus.com>
2023-10-23 10:46:14 +02:00
exceptionfactory
542bb35bda Add ChaCha20-Poly1305 Support for OpenSSH Keys (#904)
* Add ChaCha20-Poly1305 Support for OpenSSH Keys

- Updated ChachaPolyCipher to support decryption without Additional Authenticated Data

* Added test for ChachaPolyCipher without AAD

* Streamlined ChachaPolyCipher.update() method
2023-10-16 22:54:22 +02:00
Jeroen van Erp
3b67d2b476 Prepare release 0.37.0 2023-10-11 22:56:18 +02:00
exceptionfactory
9b9b208434 Add AES-GCM Support for OpenSSH Private Keys (#899)
* Added AES-GCM Support for OpenSSH Private Keys

* Small simplification

Signed-off-by: Jeroen van Erp <jeroen@hierynomus.com>

---------

Signed-off-by: Jeroen van Erp <jeroen@hierynomus.com>
Co-authored-by: Jeroen van Erp <jeroen@hierynomus.com>
2023-10-11 22:48:02 +02:00
Jeroen van Erp
a3cce0d2f9 Fix bunch of warnings 2023-10-11 22:41:50 +02:00
Henning Pöttker
5d040dd4bb Fix zlib compression (#901)
* add failing integrationtest for https://github.com/hierynomus/sshj/issues/893

* Fix zlib compression

---------

Co-authored-by: Malte Otten <malte.otten@emsysvpp.de>
2023-10-11 20:57:20 +02:00
exceptionfactory
461c0e46d4 Improved malformed file handling for OpenSSH Private Keys (#898) 2023-10-09 09:17:01 +02:00
Jeroen van Erp
f4d34d899d Fix release build 2023-09-04 11:23:01 +02:00
Jeroen van Erp
2bef99c875 Prepare release 0.36.0 2023-09-04 08:58:52 +02:00
Raul Santelices
a186dbf0bc Fix race condition causing SSH_MSG_UNIMPLEMENTED occasionally during key exchange (#851)
* Fix race condition causing SSH_MSG_UNIMPLEMENTED occasionally during key exchange

* unit tests

* fix unit tests

---------

Co-authored-by: Jeroen van Erp <jeroen@hierynomus.com>
2023-09-02 00:54:22 +02:00
Jeroen van Erp
a5fdb29fad Fixed itests for missing docker container (#892)
Migrated all tests to junit5

Signed-off-by: Jeroen van Erp <jeroen@hierynomus.com>
2023-09-01 22:35:04 +02:00
exceptionfactory
3069138482 Add DefaultSecurityProviderConfig with Bouncy Castle disabled (#861)
* Added DefaultSecurityProviderConfig with Bouncy Castle disabled

* Upgrade test to junit jupiter

---------

Co-authored-by: Jeroen van Erp <jeroen@hierynomus.com>
2023-07-20 08:43:25 +02:00
Jeroen van Erp
a3c9c61a09 Prepare release notes 2023-07-18 23:49:49 +02:00
Jeroen van Erp
31d156b19f Rewriting testing utilities to use jupiter engine (#881)
* Rewriting testing utilities to use jupiter engine

Signed-off-by: Jeroen van Erp <jeroen@hierynomus.com>

* Fixed unit tests

* Fixed integration tests

---------

Signed-off-by: Jeroen van Erp <jeroen@hierynomus.com>
2023-07-18 23:45:41 +02:00
exceptionfactory
ec69d109e8 Removed Java 7 backport Socket utilities (#880) 2023-07-17 16:47:56 +02:00
exceptionfactory
f35c2bd4ce Replaced custom Base64 with java.util.Base64 (#879) 2023-07-15 00:07:03 +02:00
exceptionfactory
07837098eb Removed unused bcrypt password hashing methods (#852)
Co-authored-by: Jeroen van Erp <jeroen@hierynomus.com>
2023-07-14 15:39:18 +02:00
exceptionfactory
39a7be9221 Set Java 8 as minimum required version (#874)
- Upgraded Bouncy Castle from 1.70 to 1.75
- Upgraded SLF4J from 1.7.36 to 2.0.7
- Upgraded Logback from 1.2.11 to 1.3.8
- Upgraded Apache MINA SSHD from 2.8.0 to 2.10.0
- Upgraded Grizzly HTTP Server from 2.4.4 to 3.0.1
- Upgraded Testcontainers from 1.16.2 to 1.18.3
- Refactored references and removed HttpClient dependency
- Upgraded GitHub Actions setup-java from 1 to 3
- Updated GitHub Actions to use Temurin JDK 11
- Added OpenSSL upgrade to RSA Key Tests

Co-authored-by: Jeroen van Erp <jeroen@hierynomus.com>
2023-07-14 11:50:11 +02:00
cebaa
e7614db94a Have newStatefulSFTPClient return StatefulSFTPClient (#876) 2023-07-14 11:23:03 +02:00
Kaj J. Niemi
233c0dcaa6 Upgrade to gradle 7.6.1 to make JDK 17+ work (#860) 2023-05-22 13:19:33 +02:00
exceptionfactory
0d16fbe146 Replaced Curve25519 class with X25519 Key Agreement (#838)
Co-authored-by: Jeroen van Erp <jeroen@hierynomus.com>
2023-02-11 09:56:20 +01:00
Aaron Meriwether
154a202384 Remove jzlib dependency (#772)
* Remove jzlib dependency

* Document the Java 7 prerequisite
2023-02-11 09:17:00 +01:00
Jeroen van Erp
830a39dc24 Prepare release notes for 0.35.0 2023-01-30 14:27:03 +01:00
Jan S
dcfa1833d7 TimeoutException message improved (#835) 2023-01-06 10:05:33 +01:00
kegelh
6e7fb96d07 Support SSHClient.authPassword on FreeBSD (#815)
* Support SSHClient.authPassword on FreeBSD

FreeBSD "keyboard-interactive" prompt is "Password for user@host:"

* Add test for PasswordResponseProvider
2022-09-19 13:16:56 +02:00
kegelh
d5d6096d5d Fix #805: Prevent CHANNEL_CLOSE to be sent between Channel.isOpen and… (#813)
* Fix #805: Prevent CHANNEL_CLOSE to be sent between Channel.isOpen and a Transport.write call

Otherwise, a disconnect with a "packet referred to nonexistent channel" message can occur.

This particularly happens when the transport.Reader thread passes an eof from the server to the ChannelInputStream, the reading library-user thread returns, and closes the channel at the same time as the transport.Reader thread receives the subsequent CHANNEL_CLOSE from the server.

* Add integration test for #805
2022-09-17 07:11:11 +02:00
exceptionfactory
2551f8e559 Add Transport.isKeyExchangeRequired() to avoid unnecessary KEXINIT (#811)
* Added Transport.isKeyExchangeRequired() to avoid unnecessary KEXINIT

- Updated SSHClient.onConnect() to check isKeyExchangeRequired() before calling doKex()
- Added started timestamp in ThreadNameProvider for improved tracking

* Moved KeepAliveThread State check after authentication to avoid test timing issues
2022-09-16 15:04:26 +02:00
kegelh
430cbfcf13 Make all tests runnable on Windows (#814) 2022-09-16 12:25:28 +02:00
168 changed files with 5652 additions and 6122 deletions

1
.gitattributes vendored
View File

@@ -1 +1,2 @@
*.bat text eol=crlf
src/itest/docker-image/** eol=lf

View File

@@ -6,20 +6,20 @@ name: Build SSHJ
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
java12:
name: Build with Java 12
name: Build with Java 11
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up JDK 12
uses: actions/setup-java@v1
- uses: actions/checkout@v4
- name: Set up Java 11
uses: actions/setup-java@v4
with:
java-version: 12
distribution: 'temurin'
java-version: 11
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Build with Gradle
@@ -29,14 +29,14 @@ jobs:
integration:
name: Integration test
needs: [java12]
runs-on: [ubuntu-latest]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- run: git fetch --depth=1 origin +refs/tags/*:refs/tags/*
- uses: actions/setup-java@v1
- uses: actions/setup-java@v4
with:
java-version: 12
distribution: 'temurin'
java-version: 11
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Build with Gradle

View File

@@ -13,12 +13,13 @@ jobs:
name: Build with Java 12
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up JDK 12
uses: actions/setup-java@v1
uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: 12
- name: Grant execute permission for gradlew
run: chmod +x gradlew
@@ -29,21 +30,20 @@ jobs:
needs: [java12]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-java@v1
- uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: 12
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Release
run: ./gradlew release -Prelease.disableChecks -Prelease.pushTagsOnly -Prelease.customUsername=${{ github.actor }} -Prelease.customPassword=${{ github.token }}
run: ./gradlew clean publishToSonatype closeAndReleaseSonatypeStagingRepository
env:
ORG_GRADLE_PROJECT_signingKey: ${{ secrets.SIGNINGKEY }}
ORG_GRADLE_PROJECT_signingKeyId: ${{ secrets.SIGNINGKEYID }}
ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.SIGNINGPASSWORD }}
ORG_GRADLE_PROJECT_sonatypeUsername: ${{ secrets.OSSRH_USERNAME }}
ORG_GRADLE_PROJECT_sonatypePassword: ${{ secrets.OSSRH_PASSWORD }}

View File

@@ -1,7 +1,7 @@
= sshj - SSHv2 library for Java
Jeroen van Erp
:sshj_groupid: com.hierynomus
:sshj_version: 0.32.0
:sshj_version: 0.38.0
:source-highlighter: pygments
image:https://github.com/hierynomus/sshj/actions/workflows/gradle.yml/badge.svg[link="https://github.com/hierynomus/sshj/actions/workflows/gradle.yml"]
@@ -10,6 +10,8 @@ image:https://codecov.io/gh/hierynomus/sshj/branch/master/graph/badge.svg["codec
image:http://www.javadoc.io/badge/com.hierynomus/sshj.svg?color=blue["JavaDocs", link="http://www.javadoc.io/doc/com.hierynomus/sshj"]
image:https://maven-badges.herokuapp.com/maven-central/com.hierynomus/sshj/badge.svg["Maven Central",link="https://maven-badges.herokuapp.com/maven-central/com.hierynomus/sshj"]
WARNING: SSHJ versions up to and including 0.37.0 are vulnerable to https://nvd.nist.gov/vuln/detail/CVE-2023-48795[CVE-2023-48795 - Terrapin]. Please upgrade to 0.38.0 or higher.
To get started, have a look at one of the examples. Hopefully you will find the API pleasant to work with :)
== Getting SSHJ
@@ -46,7 +48,7 @@ If your project is built using another build tool that uses the Maven Central re
In the `examples` directory, there is a separate Maven project that shows how the library can be used in some sample cases. If you want to run them, follow these guidelines:
. Install http://maven.apache.org/[Maven 2.2.1] or up.
. Clone the Overthere repository.
. Clone the SSHJ repository.
. Go into the `examples` directory and run the command `mvn eclipse:eclipse`.
. Import the `examples` project into Eclipse.
. Change the login details in the example classes (address, username and password) and run them!
@@ -95,7 +97,11 @@ If you need something that is not included, it shouldn't be too hard to add (do
http://ssh-comparison.quendi.de/comparison.html[SSH Implementation Comparison]
== Dependencies
Java 6+. http://www.slf4j.org/download.html[slf4j] is required. http://www.bouncycastle.org/java.html[bouncycastle] is highly recommended and required for using some of the crypto algorithms. http://www.jcraft.com/jzlib/[jzlib] is required for using zlib compression.
- Java 8 or higher
- https://www.slf4j.org/[SLF4J 2.0.0]
- https://www.bouncycastle.org[Bouncy Castle]
== Reporting bugs
Issue tracker: https://github.com/hierynomus/sshj/issues
@@ -104,6 +110,36 @@ Issue tracker: https://github.com/hierynomus/sshj/issues
Fork away!
== Release history
SSHJ 0.38.0 (2024-01-02)::
* Mitigated CVE-2023-48795 - Terrapin
* Merged https://github.com/hierynomus/sshj/pull/917[#917]: Implement OpenSSH strict key exchange extension
* Merged https://github.com/hierynomus/sshj/pull/903[#903]: Fix for writing known hosts key string
* Merged https://github.com/hierynomus/sshj/pull/913[#913]: Prevent remote port forwarding buffers to grow without bounds
* Moved tests to JUnit5
* Merged https://github.com/hierynomus/sshj/pull/827[#827]: Fallback to posix-rename@openssh.com extension if available
* Merged https://github.com/hierynomus/sshj/pull/904[#904]: Add ChaCha20-Poly1305 support for OpenSSH keys
SSHJ 0.37.0 (2023-10-11)::
* Merged https://github.com/hierynomus/sshj/pull/899[#899]: Add support for AES-GCM OpenSSH private keys
* Merged https://github.com/hierynomus/sshj/pull/901[#901]: Fix ZLib compression bug
* Merged https://github.com/hierynomus/sshj/pull/898[#898]: Improved malformed file handling for OpenSSH private keys
SSHJ 0.36.0 (2023-09-04)::
* Rewrote Integration tests to JUnit5
* Merged https://github.com/hierynomus/sshj/pull/851[#851]: Fix race condition in key exchange causing intermittent SSH_MSG_UNIMPLEMENTED
* Merged https://github.com/hierynomus/sshj/pull/861[#861]: Add DefaultSecurityProviderConfig with has BouncyCastle disabled
* Merged https://github.com/hierynomus/sshj/pull/881[#881]: Rewrote test classes to JUnit Jupiter engine
* Merged https://github.com/hierynomus/sshj/pull/880[#880]: Removed Java 7 backport Socket utilities
* Merged https://github.com/hierynomus/sshj/pull/879[#879]: Replaced custom Base64 with java.util.Base64
* Merged https://github.com/hierynomus/sshj/pull/852[#852]: Removed unused bcrypt password hashing methods
* Merged https://github.com/hierynomus/sshj/pull/874[#874]: Java 8 minimum version + dependency upgrades
* Merged https://github.com/hierynomus/sshj/pull/876[#876]: Change `newStatefulSFTPClient` to return `StatefulSFTPClient`
* Merged https://github.com/hierynomus/sshj/pull/860[#860]: Upgrade to Gradle 7.6.1
* Merged https://github.com/hierynomus/sshj/pull/838[#838]: Replaced Curve25519 class with X25519 Key agreement
* Merged https://github.com/hierynomus/sshj/pull/772[#772]: Remove dependency on jzlib
SSHJ 0.35.0 (2023-01-30)::
* Merged https://github.com/hierynomus/sshj/pull/835[#835]: TimeoutException message improved
* Merged https://github.com/hierynomus/sshj/pull/815[#815]: Support authPassword on FreeBSD
* Merged https://github.com/hierynomus/sshj/pull/813[#813]: Prevent `CHANNEL_CLOSE` between isOpen and write call.
* Merged https://github.com/hierynomus/sshj/pull/811[#811]: Add `Transport.isKeyExchangeREquired` to prevent unnecessary KEXINIT
SSHJ 0.34.0 (2022-08-10)::
* Merged https://github.com/hierynomus/sshj/pull/743[#743]: Use default client credentials for AuthGssApiWithMic
* Merged https://github.com/hierynomus/sshj/pull/801[#801]: Restore thread interrupt status after catching InterruptedException

View File

@@ -23,7 +23,6 @@ dependencies {
compile "org.slf4j:slf4j-api:1.7.7"
compile "org.bouncycastle:bcprov-jdk15on:$bouncycastleVersion"
compile "org.bouncycastle:bcpkix-jdk15on:$bouncycastleVersion"
compile "com.jcraft:jzlib:1.1.3"
testCompile "junit:junit:4.11"
testCompile "org.mockito:mockito-core:1.9.5"

View File

@@ -1,25 +1,27 @@
plugins {
id "java"
id "jvm-test-suite"
id "groovy"
id "jacoco"
id "com.github.blindpirate.osgi" version '0.0.6'
id "maven-publish"
id "signing"
id 'pl.allegro.tech.build.axion-release' version '1.13.3'
id "com.bmuschko.docker-remote-api" version "7.1.0"
id 'pl.allegro.tech.build.axion-release' version '1.15.3'
id "com.github.hierynomus.license" version "0.16.1"
id 'ru.vyarus.github-info' version '1.2.0'
id "io.github.gradle-nexus.publish-plugin" version "1.0.0"
id "com.bmuschko.docker-remote-api" version "9.2.1"
id 'ru.vyarus.github-info' version '1.5.0'
id "io.github.gradle-nexus.publish-plugin" version "1.3.0"
}
group = "com.hierynomus"
ext.moduleName = "${project.group}.${project.name}"
defaultTasks "build"
repositories {
mavenCentral()
}
group = "com.hierynomus"
defaultTasks ["build"]
ext.moduleName = "${project.group}.${project.name}"
scmVersion {
tag {
prefix = 'v'
@@ -33,30 +35,21 @@ scmVersion {
project.version = scmVersion.version
compileJava {
options.release = 8
}
configurations.implementation.transitive = false
def bouncycastleVersion = "1.70"
def sshdVersion = "2.8.0"
def bouncycastleVersion = "1.78.1"
def sshdVersion = "2.12.1"
dependencies {
implementation "org.slf4j:slf4j-api:1.7.36"
implementation "org.bouncycastle:bcprov-jdk15on:$bouncycastleVersion"
implementation "org.bouncycastle:bcpkix-jdk15on:$bouncycastleVersion"
implementation "com.jcraft:jzlib:1.1.3"
implementation "org.slf4j:slf4j-api:2.0.13"
implementation "org.bouncycastle:bcprov-jdk18on:$bouncycastleVersion"
implementation "org.bouncycastle:bcpkix-jdk18on:$bouncycastleVersion"
implementation "com.hierynomus:asn-one:0.6.0"
implementation "net.i2p.crypto:eddsa:0.3.0"
testImplementation "junit:junit:4.13.2"
testImplementation 'org.spockframework:spock-core:1.3-groovy-2.4'
testImplementation "org.mockito:mockito-core:4.2.0"
testImplementation "org.apache.sshd:sshd-core:$sshdVersion"
testImplementation "org.apache.sshd:sshd-sftp:$sshdVersion"
testImplementation "org.apache.sshd:sshd-scp:$sshdVersion"
testImplementation "ch.qos.logback:logback-classic:1.2.11"
testImplementation 'org.glassfish.grizzly:grizzly-http-server:2.4.4'
testImplementation 'org.apache.httpcomponents:httpclient:4.5.13'
testImplementation 'org.testcontainers:testcontainers:1.16.2'
}
license {
@@ -66,13 +59,16 @@ license {
java = 'SLASHSTAR_STYLE'
}
excludes([
'**/djb/Curve25519.java',
'**/sshj/common/Base64.java',
'**/com/hierynomus/sshj/userauth/keyprovider/bcrypt/*.java',
'**/files/test_file_*.txt',
])
}
java {
withJavadocJar()
withSourcesJar()
}
if (!JavaVersion.current().isJava9Compatible()) {
throw new GradleScriptException("Minimum compilation version is Java 9")
}
@@ -84,10 +80,82 @@ if (JavaVersion.current().isJava8Compatible()) {
}
}
compileJava {
options.compilerArgs.addAll(['--release', '7'])
testing {
suites {
configureEach {
useJUnitJupiter()
dependencies {
implementation "org.slf4j:slf4j-api:2.0.13"
implementation 'org.spockframework:spock-core:2.3-groovy-3.0'
implementation "org.mockito:mockito-core:4.11.0"
implementation "org.assertj:assertj-core:3.24.2"
implementation "ru.vyarus:spock-junit5:1.2.0"
implementation "org.apache.sshd:sshd-core:$sshdVersion"
implementation "org.apache.sshd:sshd-sftp:$sshdVersion"
implementation "org.apache.sshd:sshd-scp:$sshdVersion"
implementation "ch.qos.logback:logback-classic:1.3.14"
implementation 'org.glassfish.grizzly:grizzly-http-server:3.0.1'
}
targets {
all {
testTask.configure {
testLogging {
showStandardStreams = false
exceptionFormat = 'full'
}
include "**/*Test.*"
include "**/*Spec.*"
afterSuite { descriptor, result ->
def indicator = "\u001B[32m✓\u001b[0m"
if (result.failedTestCount > 0) {
indicator = "\u001B[31m✘\u001b[0m"
}
logger.lifecycle("$indicator Test ${descriptor.name}; Executed: ${result.testCount}/\u001B[32m${result.successfulTestCount}\u001B[0m/\u001B[31m${result.failedTestCount}\u001B[0m")
}
}
}
}
}
test {
sources {
groovy {
srcDirs = ['src/test/groovy']
}
}
}
integrationTest(JvmTestSuite) {
dependencies {
implementation project()
implementation 'org.testcontainers:testcontainers:1.19.8'
implementation 'org.testcontainers:junit-jupiter:1.19.8'
}
sources {
java {
srcDirs = ['src/itest/java']
}
resources {
srcDirs = ['src/itest/resources']
}
}
targets {
all {
testTask.configure {
shouldRunAfter(test)
}
}
}
}
}
}
project.tasks.compileGroovy.onlyIf { false }
task writeSshjVersionProperties {
doLast {
project.file("${project.buildDir}/resources/main").mkdirs()
@@ -121,12 +189,6 @@ jar {
}
}
java {
withJavadocJar()
withSourcesJar()
}
sourcesJar {
manifest {
attributes(
@@ -140,53 +202,6 @@ sourcesJar {
}
}
configurations {
integrationTestImplementation.extendsFrom testImplementation
integrationTestRuntimeOnly.extendsFrom testRuntimeOnly
}
sourceSets {
integrationTest {
groovy {
compileClasspath += sourceSets.main.output + sourceSets.test.output
runtimeClasspath += sourceSets.main.output + sourceSets.test.output
srcDir file('src/itest/groovy')
}
resources.srcDir file('src/itest/resources')
}
}
task integrationTest(type: Test) {
testClassesDirs = sourceSets.integrationTest.output.classesDirs
classpath = sourceSets.integrationTest.runtimeClasspath
}
tasks.withType(Test) {
testLogging {
exceptionFormat = 'full'
}
include "**/*Test.*"
include "**/*Spec.*"
if (!project.hasProperty("allTests")) {
useJUnit {
excludeCategories 'com.hierynomus.sshj.test.SlowTests'
excludeCategories 'com.hierynomus.sshj.test.KnownFailingTests'
}
}
afterSuite { descriptor, result ->
if (descriptor.className != null) {
def indicator = "\u001B[32m✓\u001b[0m"
if (result.failedTestCount > 0) {
indicator = "\u001B[31m✘\u001b[0m"
}
logger.lifecycle("$indicator Test ${descriptor.name}; Executed: ${result.testCount}/\u001B[32m${result.successfulTestCount}\u001B[0m/\u001B[31m${result.failedTestCount}\u001B[0m")
}
}
}
project.tasks.compileGroovy.onlyIf { false }
github {
user 'hierynomus'
license 'Apache'
@@ -272,17 +287,11 @@ nexusPublishing {
jacocoTestReport {
reports {
xml.enabled true
html.enabled true
xml.required = true
html.required = true
}
}
task forkedUploadRelease(type: GradleBuild) {
buildFile = project.buildFile
tasks = ["clean", "publishToSonatype", "closeAndReleaseSonatypeStagingRepository"]
}
project.tasks.release.dependsOn([project.tasks.integrationTest, project.tasks.build])
project.tasks.release.finalizedBy(project.tasks.forkedUploadRelease)
project.tasks.jacocoTestReport.dependsOn(project.tasks.test)
project.tasks.check.dependsOn(project.tasks.jacocoTestReport)

View File

@@ -24,7 +24,7 @@
<groupId>com.hierynomus</groupId>
<artifactId>sshj-examples</artifactId>
<packaging>jar</packaging>
<version>0.33.0</version>
<version>0.37.0</version>
<name>sshj-examples</name>
<description>Examples for SSHv2 library for Java</description>

Binary file not shown.

View File

@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

269
gradlew vendored
View File

@@ -1,7 +1,7 @@
#!/usr/bin/env sh
#!/bin/sh
#
# Copyright 2015 the original author or authors.
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -17,78 +17,113 @@
#
##############################################################################
##
## Gradle start up script for UN*X
##
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
APP_BASE_NAME=${0##*/}
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
MAX_FD=maximum
warn () {
echo "$*"
}
} >&2
die () {
echo
echo "$*"
echo
exit 1
}
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD="$JAVA_HOME/bin/java"
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
@@ -97,7 +132,7 @@ Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
@@ -105,79 +140,95 @@ location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

22
gradlew.bat vendored
View File

@@ -40,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
@@ -54,7 +54,7 @@ goto fail
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
@@ -64,28 +64,14 @@ echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell

View File

@@ -0,0 +1,7 @@
#!/bin/ash
# generate host keys if not present
ssh-keygen -A
# do not detach (-D), log to stderr (-e), passthrough other arguments
exec /usr/sbin/sshd -D -e "$@"

View File

@@ -1,102 +0,0 @@
/*
* Copyright (C)2009 - SSHJ Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hierynomus.sshj
import com.hierynomus.sshj.key.KeyAlgorithms
import net.schmizz.sshj.DefaultConfig
import net.schmizz.sshj.SSHClient
import net.schmizz.sshj.transport.TransportException
import net.schmizz.sshj.userauth.UserAuthException
import org.junit.ClassRule
import spock.lang.Shared
import spock.lang.Specification
import spock.lang.Unroll
class IntegrationSpec extends Specification {
@Shared
@ClassRule
SshdContainer sshd
@Unroll
def "should accept correct key for #signatureName"() {
given:
def config = new DefaultConfig()
config.setKeyAlgorithms(Collections.singletonList(signatureFactory))
SSHClient sshClient = new SSHClient(config)
sshClient.addHostKeyVerifier(fingerprint) // test-containers/ssh_host_ecdsa_key's fingerprint
when:
sshClient.connect(sshd.containerIpAddress, sshd.firstMappedPort)
then:
sshClient.isConnected()
where:
signatureFactory << [KeyAlgorithms.ECDSASHANistp256(), KeyAlgorithms.EdDSA25519()]
fingerprint << ["d3:6a:a9:52:05:ab:b5:48:dd:73:60:18:0c:3a:f0:a3", "dc:68:38:ce:fc:6f:2c:d6:6d:6b:34:eb:5c:f0:41:6a"]
signatureName = signatureFactory.getName()
}
def "should decline wrong key"() throws IOException {
given:
SSHClient sshClient = new SSHClient(new DefaultConfig())
sshClient.addHostKeyVerifier("d4:6a:a9:52:05:ab:b5:48:dd:73:60:18:0c:3a:f0:a3")
when:
sshClient.connect(sshd.containerIpAddress, sshd.firstMappedPort)
then:
thrown(TransportException.class)
}
@Unroll
def "should authenticate with key #key"() {
given:
SSHClient client = sshd.getConnectedClient()
when:
def keyProvider = passphrase != null ? client.loadKeys("src/itest/resources/keyfiles/$key", passphrase) : client.loadKeys("src/itest/resources/keyfiles/$key")
client.authPublickey(IntegrationTestUtil.USERNAME, keyProvider)
then:
client.isAuthenticated()
where:
key | passphrase
// "id_ecdsa_nistp256" | null // TODO: Need to improve PKCS8 key support.
"id_ecdsa_opensshv1" | null
"id_ed25519_opensshv1" | null
"id_ed25519_opensshv1_aes256cbc.pem" | "foobar"
"id_ed25519_opensshv1_aes128cbc.pem" | "sshjtest"
"id_ed25519_opensshv1_protected" | "sshjtest"
"id_rsa" | null
"id_rsa_opensshv1" | null
"id_ecdsa_nistp384_opensshv1" | null
"id_ecdsa_nistp521_opensshv1" | null
}
def "should not authenticate with wrong key"() {
given:
SSHClient client = sshd.getConnectedClient()
when:
client.authPublickey("sshj", "src/itest/resources/keyfiles/id_unknown_key")
then:
thrown(UserAuthException.class)
!client.isAuthenticated()
}
}

View File

@@ -1,159 +0,0 @@
/*
* Copyright (C)2009 - SSHJ Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hierynomus.sshj
import com.hierynomus.sshj.key.KeyAlgorithms
import net.schmizz.sshj.Config
import net.schmizz.sshj.DefaultConfig
import org.testcontainers.images.builder.dockerfile.DockerfileBuilder
import spock.lang.Specification
import spock.lang.Unroll
import java.nio.file.Paths
/**
* Checks that SSHJ is able to work with OpenSSH 8.8, which removed ssh-rsa signature from the default setup.
*/
class RsaShaKeySignatureTest extends Specification {
private static final Map<String, KeyAlgorithms.Factory> SSH_HOST_KEYS_AND_FACTORIES = [
'ssh_host_ecdsa_256_key': KeyAlgorithms.ECDSASHANistp256(),
'ssh_host_ecdsa_384_key': KeyAlgorithms.ECDSASHANistp384(),
'ssh_host_ecdsa_521_key': KeyAlgorithms.ECDSASHANistp521(),
'ssh_host_ed25519_384_key': KeyAlgorithms.EdDSA25519(),
'ssh_host_rsa_2048_key': KeyAlgorithms.RSASHA512(),
]
private static void dockerfileBuilder(DockerfileBuilder it, String hostKey, String pubkeyAcceptedAlgorithms) {
it.from("archlinux:base")
it.run('yes | pacman -Sy core/openssh' +
' && (' +
' V=$(echo $(/usr/sbin/sshd -h 2>&1) | grep -o \'OpenSSH_[0-9][0-9]*[.][0-9][0-9]*p[0-9]\');' +
' if [[ "$V" < OpenSSH_8.8p1 ]]; then' +
' echo $V is too old 1>&2;' +
' exit 1;' +
' fi' +
')' +
' && set -o pipefail ' +
' && useradd --create-home sshj' +
' && echo \"sshj:ultrapassword\" | chpasswd')
it.add("authorized_keys", "/home/sshj/.ssh/")
it.add(hostKey, '/etc/ssh/')
it.run('chmod go-rwx /etc/ssh/ssh_host_*' +
' && chown -R sshj /home/sshj/.ssh' +
' && chmod -R go-rwx /home/sshj/.ssh')
it.expose(22)
def cmd = [
'/usr/sbin/sshd',
'-D',
'-e',
'-f', '/dev/null',
'-o', 'LogLevel=DEBUG2',
'-o', "HostKey=/etc/ssh/$hostKey",
]
if (pubkeyAcceptedAlgorithms != null) {
cmd += ['-o', "PubkeyAcceptedAlgorithms=$pubkeyAcceptedAlgorithms"]
}
it.cmd(cmd as String[])
}
private static SshdContainer makeSshdContainer(String hostKey, String pubkeyAcceptedAlgorithms) {
return new SshdContainer(new SshdContainer.DebugLoggingImageFromDockerfile()
.withFileFromPath("authorized_keys", Paths.get("src/itest/docker-image/authorized_keys"))
.withFileFromPath(hostKey, Paths.get("src/itest/docker-image/test-container/host_keys/$hostKey"))
.withDockerfileFromBuilder {
dockerfileBuilder(it, hostKey, pubkeyAcceptedAlgorithms)
})
}
@Unroll
def "connect to a server with host key #hostKey that does not support ssh-rsa"() {
given:
SshdContainer sshd = makeSshdContainer(hostKey, "rsa-sha2-512,rsa-sha2-256,ssh-ed25519")
sshd.start()
and:
Config config = new DefaultConfig()
config.keyAlgorithms = [
KeyAlgorithms.RSASHA512(),
KeyAlgorithms.RSASHA256(),
SSH_HOST_KEYS_AND_FACTORIES[hostKey],
]
when:
def sshClient = sshd.getConnectedClient(config)
sshClient.authPublickey("sshj", "src/itest/resources/keyfiles/id_rsa_opensshv1")
then:
sshClient.isAuthenticated()
cleanup:
sshClient?.disconnect()
sshd.stop()
where:
hostKey << SSH_HOST_KEYS_AND_FACTORIES.keySet()
}
@Unroll
def "connect to a default server with host key #hostKey using a default config"() {
given:
SshdContainer sshd = makeSshdContainer(hostKey, null)
sshd.start()
when:
def sshClient = sshd.getConnectedClient()
sshClient.authPublickey("sshj", "src/itest/resources/keyfiles/id_rsa_opensshv1")
then:
sshClient.isAuthenticated()
cleanup:
sshClient?.disconnect()
sshd.stop()
where:
hostKey << SSH_HOST_KEYS_AND_FACTORIES.keySet()
}
@Unroll
def "connect to a server with host key #hostkey that supports only ssh-rsa"() {
given:
SshdContainer sshd = makeSshdContainer(hostKey, "ssh-rsa,ssh-ed25519")
sshd.start()
and:
Config config = new DefaultConfig()
config.keyAlgorithms = [
KeyAlgorithms.SSHRSA(),
SSH_HOST_KEYS_AND_FACTORIES[hostKey],
]
when:
def sshClient = sshd.getConnectedClient(config)
sshClient.authPublickey("sshj", "src/itest/resources/keyfiles/id_rsa_opensshv1")
then:
sshClient.isAuthenticated()
cleanup:
sshClient.disconnect()
sshd.stop()
where:
hostKey << SSH_HOST_KEYS_AND_FACTORIES.keySet()
}
}

View File

@@ -1,155 +0,0 @@
/*
* Copyright (C)2009 - SSHJ Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hierynomus.sshj;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import net.schmizz.sshj.Config;
import net.schmizz.sshj.DefaultConfig;
import net.schmizz.sshj.SSHClient;
import net.schmizz.sshj.transport.verification.PromiscuousVerifier;
import org.jetbrains.annotations.NotNull;
import org.slf4j.LoggerFactory;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.images.builder.ImageFromDockerfile;
import org.testcontainers.images.builder.dockerfile.DockerfileBuilder;
import org.testcontainers.utility.DockerLoggerFactory;
import java.io.IOException;
import java.nio.file.Paths;
import java.util.concurrent.Future;
/**
* A JUnit4 rule for launching a generic SSH server container.
*/
public class SshdContainer extends GenericContainer<SshdContainer> {
/**
* A workaround for strange logger names of testcontainers. They contain no dots, but contain slashes,
* square brackets, and even emoji. It's uneasy to set the logging level via the XML file of logback, the
* result would be less readable than the code below.
*/
public static class DebugLoggingImageFromDockerfile extends ImageFromDockerfile {
public DebugLoggingImageFromDockerfile() {
super();
Logger logger = (Logger) LoggerFactory.getILoggerFactory()
.getLogger(DockerLoggerFactory.getLogger(getDockerImageName()).getName());
logger.setLevel(Level.DEBUG);
}
}
public static class Builder {
public static final String DEFAULT_SSHD_CONFIG = "" +
"PermitRootLogin yes\n" +
"AuthorizedKeysFile .ssh/authorized_keys\n" +
"Subsystem sftp /usr/lib/ssh/sftp-server\n" +
"KexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org,ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521,diffie-hellman-group-exchange-sha256,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512,diffie-hellman-group14-sha256,diffie-hellman-group14-sha1,diffie-hellman-group1-sha1,diffie-hellman-group-exchange-sha1\n" +
"macs umac-64-etm@openssh.com,umac-128-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,hmac-ripemd160-etm@openssh.com,umac-64@openssh.com,umac-128@openssh.com,hmac-sha2-256,hmac-sha2-512,hmac-ripemd160,hmac-ripemd160@openssh.com\n" +
"TrustedUserCAKeys /etc/ssh/trusted_ca_keys\n" +
"Ciphers 3des-cbc,blowfish-cbc,aes128-cbc,aes192-cbc,aes256-cbc,aes128-ctr,aes192-ctr,aes256-ctr,aes128-gcm@openssh.com,aes256-gcm@openssh.com,chacha20-poly1305@openssh.com\n" +
"HostKey /etc/ssh/ssh_host_rsa_key\n" +
"HostKey /etc/ssh/ssh_host_dsa_key\n" +
"HostKey /etc/ssh/ssh_host_ecdsa_key\n" +
"HostKey /etc/ssh/ssh_host_ed25519_key\n" +
"HostKey /etc/ssh/ssh_host_ecdsa_256_key\n" +
"HostCertificate /etc/ssh/ssh_host_ecdsa_256_key-cert.pub\n" +
"HostKey /etc/ssh/ssh_host_ecdsa_384_key\n" +
"HostCertificate /etc/ssh/ssh_host_ecdsa_384_key-cert.pub\n" +
"HostKey /etc/ssh/ssh_host_ecdsa_521_key\n" +
"HostCertificate /etc/ssh/ssh_host_ecdsa_521_key-cert.pub\n" +
"HostKey /etc/ssh/ssh_host_ed25519_384_key\n" +
"HostCertificate /etc/ssh/ssh_host_ed25519_384_key-cert.pub\n" +
"HostKey /etc/ssh/ssh_host_rsa_2048_key\n" +
"HostCertificate /etc/ssh/ssh_host_rsa_2048_key-cert.pub\n" +
"LogLevel DEBUG2\n";
public static void defaultDockerfileBuilder(@NotNull DockerfileBuilder builder) {
builder.from("sickp/alpine-sshd:7.5-r2");
builder.add("authorized_keys", "/home/sshj/.ssh/authorized_keys");
builder.add("test-container/ssh_host_ecdsa_key", "/etc/ssh/ssh_host_ecdsa_key");
builder.add("test-container/ssh_host_ecdsa_key.pub", "/etc/ssh/ssh_host_ecdsa_key.pub");
builder.add("test-container/ssh_host_ed25519_key", "/etc/ssh/ssh_host_ed25519_key");
builder.add("test-container/ssh_host_ed25519_key.pub", "/etc/ssh/ssh_host_ed25519_key.pub");
builder.copy("test-container/trusted_ca_keys", "/etc/ssh/trusted_ca_keys");
builder.copy("test-container/host_keys/*", "/etc/ssh/");
builder.run("apk add --no-cache tini"
+ " && echo \"root:smile\" | chpasswd"
+ " && adduser -D -s /bin/ash sshj"
+ " && passwd -u sshj"
+ " && echo \"sshj:ultrapassword\" | chpasswd"
+ " && chmod 600 /home/sshj/.ssh/authorized_keys"
+ " && chmod 600 /etc/ssh/ssh_host_*_key"
+ " && chmod 644 /etc/ssh/*.pub"
+ " && chown -R sshj:sshj /home/sshj");
builder.entryPoint("/sbin/tini", "/entrypoint.sh", "-o", "LogLevel=DEBUG2");
builder.add("sshd_config", "/etc/ssh/sshd_config");
}
private @NotNull String sshdConfig = DEFAULT_SSHD_CONFIG;
public @NotNull Builder withSshdConfig(@NotNull String sshdConfig) {
this.sshdConfig = sshdConfig;
return this;
}
public @NotNull SshdContainer build() {
return new SshdContainer(buildInner());
}
private @NotNull Future<String> buildInner() {
return new DebugLoggingImageFromDockerfile()
.withDockerfileFromBuilder(Builder::defaultDockerfileBuilder)
.withFileFromPath(".", Paths.get("src/itest/docker-image"))
.withFileFromString("sshd_config", sshdConfig);
}
}
@SuppressWarnings("unused") // Used dynamically by Spock
public SshdContainer() {
this(new SshdContainer.Builder().buildInner());
}
public SshdContainer(@NotNull Future<String> future) {
super(future);
withExposedPorts(22);
setWaitStrategy(new SshServerWaitStrategy());
withLogConsumer(outputFrame -> {
switch (outputFrame.getType()) {
case STDOUT:
logger().info("sshd stdout: {}", outputFrame.getUtf8String().stripTrailing());
break;
case STDERR:
logger().info("sshd stderr: {}", outputFrame.getUtf8String().stripTrailing());
break;
}
});
}
public SSHClient getConnectedClient(Config config) throws IOException {
SSHClient sshClient = new SSHClient(config);
sshClient.addHostKeyVerifier(new PromiscuousVerifier());
sshClient.connect("127.0.0.1", getFirstMappedPort());
return sshClient;
}
public SSHClient getConnectedClient() throws IOException {
return getConnectedClient(new DefaultConfig());
}
}

View File

@@ -1,75 +0,0 @@
/*
* Copyright (C)2009 - SSHJ Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hierynomus.sshj.sftp
import com.hierynomus.sshj.IntegrationTestUtil
import com.hierynomus.sshj.SshdContainer
import net.schmizz.sshj.SSHClient
import net.schmizz.sshj.sftp.OpenMode
import net.schmizz.sshj.sftp.RemoteFile
import net.schmizz.sshj.sftp.SFTPClient
import org.junit.ClassRule
import spock.lang.Shared
import spock.lang.Specification
import java.nio.charset.StandardCharsets
import static org.codehaus.groovy.runtime.IOGroovyMethods.withCloseable
class FileWriteSpec extends Specification {
@Shared
@ClassRule
SshdContainer sshd
def "should append to file (GH issue #390)"() {
given:
SSHClient client = sshd.getConnectedClient()
client.authPublickey("sshj", "src/test/resources/id_rsa")
SFTPClient sftp = client.newSFTPClient()
def file = "/home/sshj/test.txt"
def initialText = "This is the initial text.\n".getBytes(StandardCharsets.UTF_16)
def appendText = "And here's the appended text.\n".getBytes(StandardCharsets.UTF_16)
when:
withCloseable(sftp.open(file, EnumSet.of(OpenMode.WRITE, OpenMode.CREAT))) { RemoteFile initial ->
initial.write(0, initialText, 0, initialText.length)
}
then:
withCloseable(sftp.open(file, EnumSet.of(OpenMode.READ))) { RemoteFile read ->
def bytes = new byte[initialText.length]
read.read(0, bytes, 0, bytes.length)
bytes == initialText
}
when:
withCloseable(sftp.open(file, EnumSet.of(OpenMode.WRITE, OpenMode.APPEND))) { RemoteFile append ->
append.write(0, appendText, 0, appendText.length)
}
then:
withCloseable(sftp.open(file, EnumSet.of(OpenMode.READ))) { RemoteFile read ->
def bytes = new byte[initialText.length + appendText.length]
read.read(0, bytes, 0, bytes.length)
Arrays.copyOfRange(bytes, 0, initialText.length) == initialText
Arrays.copyOfRange(bytes, initialText.length, initialText.length + appendText.length) == appendText
}
cleanup:
sftp.close()
client.close()
}
}

View File

@@ -1,83 +0,0 @@
/*
* Copyright (C)2009 - SSHJ Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hierynomus.sshj.signature
import com.hierynomus.sshj.SshdContainer
import net.schmizz.sshj.DefaultConfig
import net.schmizz.sshj.SSHClient
import net.schmizz.sshj.transport.verification.OpenSSHKnownHosts
import spock.lang.Specification
import spock.lang.Unroll
import java.nio.file.Files
/**
* This is a brief test for verifying connection to a server using keys with certificates.
*
* Also, take a look at the unit test {@link net.schmizz.sshj.transport.verification.KeyWithCertificateUnitSpec}.
*/
class HostKeyWithCertificateSpec extends Specification {
@Unroll
def "accepting a signed host public key #hostKey"() {
given:
SshdContainer sshd = new SshdContainer.Builder()
.withSshdConfig("""
PasswordAuthentication yes
HostKey /etc/ssh/$hostKey
HostCertificate /etc/ssh/${hostKey}-cert.pub
""".stripMargin())
.build()
sshd.start()
and:
File knownHosts = Files.createTempFile("known_hosts", "").toFile()
knownHosts.deleteOnExit()
and:
File caPubKey = new File("src/itest/resources/keyfiles/certificates/CA_rsa.pem.pub")
def address = "127.0.0.1"
String knownHostsFileContents = "" +
"@cert-authority ${ address} ${caPubKey.text}" +
"\n@cert-authority [${address}]:${sshd.firstMappedPort} ${caPubKey.text}"
knownHosts.write(knownHostsFileContents)
and:
SSHClient sshClient = new SSHClient(new DefaultConfig())
sshClient.addHostKeyVerifier(new OpenSSHKnownHosts(knownHosts))
sshClient.connect(address, sshd.firstMappedPort)
when:
sshClient.authPassword("sshj", "ultrapassword")
then:
sshClient.authenticated
and:
knownHosts.getText() == knownHostsFileContents
cleanup:
sshd.stop()
where:
hostKey << [
"ssh_host_ecdsa_256_key",
"ssh_host_ecdsa_384_key",
"ssh_host_ecdsa_521_key",
"ssh_host_ed25519_384_key",
"ssh_host_rsa_2048_key",
]
}
}

View File

@@ -1,81 +0,0 @@
/*
* Copyright (C)2009 - SSHJ Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hierynomus.sshj.signature
import com.hierynomus.sshj.SshdContainer
import net.schmizz.sshj.DefaultConfig
import net.schmizz.sshj.SSHClient
import net.schmizz.sshj.transport.verification.PromiscuousVerifier
import org.junit.ClassRule
import spock.lang.Shared
import spock.lang.Specification
import spock.lang.Unroll
/**
* This is a brief test for verifying connection to a server using keys with certificates.
*
* Also, take a look at the unit test {@link net.schmizz.sshj.transport.verification.KeyWithCertificateUnitSpec}.
*/
class PublicKeyAuthWithCertificateSpec extends Specification {
@Shared
@ClassRule
SshdContainer sshd
@Unroll
def "authorising with a signed public key #keyName"() {
given:
SSHClient client = new SSHClient(new DefaultConfig())
client.addHostKeyVerifier(new PromiscuousVerifier())
client.connect("127.0.0.1", sshd.firstMappedPort)
when:
client.authPublickey("sshj", "src/itest/resources/keyfiles/certificates/$keyName")
then:
client.authenticated
where:
keyName << [
"id_ecdsa_256_pem_signed_by_ecdsa",
"id_ecdsa_256_rfc4716_signed_by_ecdsa",
"id_ecdsa_256_pem_signed_by_ed25519",
"id_ecdsa_256_rfc4716_signed_by_ed25519",
"id_ecdsa_256_pem_signed_by_rsa",
"id_ecdsa_256_rfc4716_signed_by_rsa",
"id_ecdsa_384_pem_signed_by_ecdsa",
"id_ecdsa_384_rfc4716_signed_by_ecdsa",
"id_ecdsa_384_pem_signed_by_ed25519",
"id_ecdsa_384_rfc4716_signed_by_ed25519",
"id_ecdsa_384_pem_signed_by_rsa",
"id_ecdsa_384_rfc4716_signed_by_rsa",
"id_ecdsa_521_pem_signed_by_ecdsa",
"id_ecdsa_521_rfc4716_signed_by_ecdsa",
"id_ecdsa_521_pem_signed_by_ed25519",
"id_ecdsa_521_rfc4716_signed_by_ed25519",
"id_ecdsa_521_pem_signed_by_rsa",
"id_ecdsa_521_rfc4716_signed_by_rsa",
"id_rsa_2048_pem_signed_by_ecdsa",
"id_rsa_2048_rfc4716_signed_by_ecdsa",
"id_rsa_2048_pem_signed_by_ed25519",
"id_rsa_2048_rfc4716_signed_by_ed25519",
"id_rsa_2048_pem_signed_by_rsa",
"id_rsa_2048_rfc4716_signed_by_rsa",
"id_ed25519_384_rfc4716_signed_by_ecdsa",
"id_ed25519_384_rfc4716_signed_by_ed25519",
"id_ed25519_384_rfc4716_signed_by_rsa",
]
}
}

View File

@@ -1,41 +0,0 @@
/*
* Copyright (C)2009 - SSHJ Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hierynomus.sshj.signature
import com.hierynomus.sshj.IntegrationTestUtil
import com.hierynomus.sshj.SshdContainer
import org.junit.ClassRule
import spock.lang.Shared
import spock.lang.Specification
import spock.lang.Unroll
class RsaSignatureClientKeySpec extends Specification {
@Shared
@ClassRule
SshdContainer sshd
@Unroll
def "should correctly connect using publickey auth with RSA key with signature"() {
given:
def client = sshd.getConnectedClient()
when:
client.authPublickey(IntegrationTestUtil.USERNAME, "src/itest/resources/keyfiles/id_rsa2")
then:
client.authenticated
}
}

View File

@@ -1,49 +0,0 @@
/*
* Copyright (C)2009 - SSHJ Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hierynomus.sshj.signature
import com.hierynomus.sshj.IntegrationTestUtil
import com.hierynomus.sshj.SshdContainer
import com.hierynomus.sshj.key.KeyAlgorithms
import net.schmizz.sshj.DefaultConfig
import org.junit.ClassRule
import spock.lang.Shared
import spock.lang.Specification
import spock.lang.Unroll
class SignatureSpec extends Specification {
@Shared
@ClassRule
SshdContainer sshd
@Unroll
def "should correctly connect with #sig Signature"() {
given:
def cfg = new DefaultConfig()
cfg.setKeyAlgorithms(Collections.singletonList(sigFactory))
def client = sshd.getConnectedClient(cfg)
when:
client.authPublickey(IntegrationTestUtil.USERNAME, IntegrationTestUtil.KEYFILE)
then:
client.authenticated
where:
sigFactory << [KeyAlgorithms.SSHRSA(), KeyAlgorithms.RSASHA256(), KeyAlgorithms.RSASHA512()]
sig = sigFactory.name
}
}

View File

@@ -1,62 +0,0 @@
/*
* Copyright (C)2009 - SSHJ Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hierynomus.sshj.transport.cipher
import com.hierynomus.sshj.IntegrationTestUtil
import com.hierynomus.sshj.SshdContainer
import net.schmizz.sshj.DefaultConfig
import org.junit.ClassRule
import spock.lang.Shared
import spock.lang.Specification
import spock.lang.Unroll
class CipherSpec extends Specification {
@Shared
@ClassRule
SshdContainer sshd
@Unroll
def "should correctly connect with #cipher Cipher"() {
given:
def cfg = new DefaultConfig()
cfg.setCipherFactories(cipherFactory)
def client = sshd.getConnectedClient(cfg)
when:
client.authPublickey(IntegrationTestUtil.USERNAME, IntegrationTestUtil.KEYFILE)
then:
client.authenticated
cleanup:
client.disconnect()
where:
cipherFactory << [BlockCiphers.TripleDESCBC(),
BlockCiphers.BlowfishCBC(),
BlockCiphers.AES128CBC(),
BlockCiphers.AES128CTR(),
BlockCiphers.AES192CBC(),
BlockCiphers.AES192CTR(),
BlockCiphers.AES256CBC(),
BlockCiphers.AES256CTR(),
GcmCiphers.AES128GCM(),
GcmCiphers.AES256GCM(),
ChachaPolyCiphers.CHACHA_POLY_OPENSSH()]
cipher = cipherFactory.name
}
}

View File

@@ -1,64 +0,0 @@
/*
* Copyright (C)2009 - SSHJ Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hierynomus.sshj.transport.kex
import com.hierynomus.sshj.IntegrationTestUtil
import com.hierynomus.sshj.SshdContainer
import net.schmizz.sshj.DefaultConfig
import net.schmizz.sshj.transport.kex.Curve25519SHA256
import net.schmizz.sshj.transport.kex.DHGexSHA1
import net.schmizz.sshj.transport.kex.DHGexSHA256
import net.schmizz.sshj.transport.kex.ECDHNistP
import org.junit.ClassRule
import spock.lang.Shared
import spock.lang.Specification
import spock.lang.Unroll
class KexSpec extends Specification {
@Shared
@ClassRule
SshdContainer sshd
@Unroll
def "should correctly connect with #kex Key Exchange"() {
given:
def cfg = new DefaultConfig()
cfg.setKeyExchangeFactories(kexFactory)
def client = sshd.getConnectedClient(cfg)
when:
client.authPublickey(IntegrationTestUtil.USERNAME, IntegrationTestUtil.KEYFILE)
then:
client.authenticated
where:
kexFactory << [DHGroups.Group1SHA1(),
DHGroups.Group14SHA1(),
DHGroups.Group14SHA256(),
DHGroups.Group16SHA512(),
DHGroups.Group18SHA512(),
new DHGexSHA1.Factory(),
new DHGexSHA256.Factory(),
new Curve25519SHA256.Factory(),
new Curve25519SHA256.FactoryLibSsh(),
new ECDHNistP.Factory256(),
new ECDHNistP.Factory384(),
new ECDHNistP.Factory521()]
kex = kexFactory.name
}
}

View File

@@ -1,72 +0,0 @@
/*
* Copyright (C)2009 - SSHJ Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hierynomus.sshj.transport.mac
import com.hierynomus.sshj.IntegrationTestUtil
import com.hierynomus.sshj.SshdContainer
import net.schmizz.sshj.DefaultConfig
import org.junit.ClassRule
import spock.lang.Shared
import spock.lang.Specification
import spock.lang.Unroll
class MacSpec extends Specification {
@Shared
@ClassRule
SshdContainer sshd
@Unroll
def "should correctly connect with #mac MAC"() {
given:
def cfg = new DefaultConfig()
cfg.setMACFactories(macFactory)
def client = sshd.getConnectedClient(cfg)
when:
client.authPublickey(IntegrationTestUtil.USERNAME, IntegrationTestUtil.KEYFILE)
then:
client.authenticated
cleanup:
client.disconnect()
where:
macFactory << [Macs.HMACRIPEMD160(), Macs.HMACRIPEMD160OpenSsh(), Macs.HMACSHA2256(), Macs.HMACSHA2512()]
mac = macFactory.name
}
@Unroll
def "should correctly connect with Encrypt-Then-Mac #mac MAC"() {
given:
def cfg = new DefaultConfig()
cfg.setMACFactories(macFactory)
def client = sshd.getConnectedClient(cfg)
when:
client.authPublickey(IntegrationTestUtil.USERNAME, IntegrationTestUtil.KEYFILE)
then:
client.authenticated
cleanup:
client.disconnect()
where:
macFactory << [Macs.HMACRIPEMD160Etm(), Macs.HMACSHA2256Etm(), Macs.HMACSHA2512Etm()]
mac = macFactory.name
}
}

View File

@@ -0,0 +1,72 @@
/*
* Copyright (C)2009 - SSHJ Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hierynomus.sshj;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import java.util.List;
import java.util.stream.Stream;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import com.hierynomus.sshj.key.KeyAlgorithms;
import net.schmizz.sshj.Config;
import net.schmizz.sshj.DefaultConfig;
import net.schmizz.sshj.SSHClient;
import net.schmizz.sshj.transport.TransportException;
@Testcontainers
public class HostKeyVerifierTest {
@Container
private static final SshdContainer sshd = new SshdContainer();
public static Stream<Arguments> signatureAlgos() {
return Stream.of(
Arguments.of(KeyAlgorithms.ECDSASHANistp256(), "d3:6a:a9:52:05:ab:b5:48:dd:73:60:18:0c:3a:f0:a3"),
Arguments.of(KeyAlgorithms.EdDSA25519(), "dc:68:38:ce:fc:6f:2c:d6:6d:6b:34:eb:5c:f0:41:6a"));
}
@ParameterizedTest(name = "Should connect with signature verified for Key Algorithm {0}")
@MethodSource("signatureAlgos")
public void shouldConnectWithSignatureVerified(KeyAlgorithms.Factory alg, String fingerprint) throws Throwable {
Config config = new DefaultConfig();
config.setKeyAlgorithms(List.of(alg));
try (SSHClient client = new SSHClient(config)) {
client.addHostKeyVerifier(fingerprint);
client.connect(sshd.getHost(), sshd.getFirstMappedPort());
assertTrue(client.isConnected());
}
}
@Test
public void shouldDeclineWrongKey() throws Throwable {
try (SSHClient client = new SSHClient()) {
assertThrows(TransportException.class, () -> {
client.addHostKeyVerifier("d4:6a:a9:52:05:ab:b5:48:dd:73:60:18:0c:3a:f0:a3");
client.connect(sshd.getHost(), sshd.getFirstMappedPort());
});
}
}
}

View File

@@ -0,0 +1,74 @@
/*
* Copyright (C)2009 - SSHJ Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hierynomus.sshj;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import org.junit.jupiter.api.Test;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import com.hierynomus.sshj.SshdContainer.SshdConfigBuilder;
import net.schmizz.sshj.SSHClient;
import net.schmizz.sshj.common.IOUtils;
import net.schmizz.sshj.connection.channel.direct.Session;
import static org.assertj.core.api.Assertions.*;
@Testcontainers
public class ManyChannelsTest {
@Container
private static final SshdContainer sshd = new SshdContainer(SshdContainer.Builder.defaultBuilder()
.withSshdConfig(SshdConfigBuilder.defaultBuilder().with("MaxSessions", "200")).withAllKeys());
@Test
public void shouldWorkWithManyChannelsWithoutNoExistentChannelError_GH805() throws Throwable {
try (SSHClient client = sshd.getConnectedClient()) {
client.authPublickey("sshj", "src/test/resources/id_rsa");
List<Future<Exception>> futures = new ArrayList<>();
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 20; i++) {
futures.add(executorService.submit(() -> {
try {
for (int j = 0; j < 10; j++) {
try (Session sshSession = client.startSession()) {
try (Session.Command sshCommand = sshSession.exec("ls -la")) {
IOUtils.readFully(sshCommand.getInputStream()).toString();
}
}
}
} catch (Exception e) {
return e;
}
return null;
}));
}
executorService.shutdown();
executorService.awaitTermination(1, TimeUnit.DAYS);
assertThat(futures).allSatisfy(future -> assertThat(future.get()).isNull());
}
}
}

View File

@@ -0,0 +1,86 @@
/*
* Copyright (C)2009 - SSHJ Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hierynomus.sshj;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.stream.Stream;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import com.hierynomus.sshj.SshdContainer.SshdConfigBuilder;
import net.schmizz.sshj.SSHClient;
import net.schmizz.sshj.userauth.UserAuthException;
import net.schmizz.sshj.userauth.keyprovider.KeyProvider;
@Testcontainers
public class PublicKeyAuthTest {
@Container
private static final SshdContainer sshd = new SshdContainer(SshdContainer.Builder.defaultBuilder().withSshdConfig(
SshdConfigBuilder.defaultBuilder().with("PubkeyAcceptedAlgorithms", "+ssh-rsa-cert-v01@openssh.com"))
.withAllKeys());
public static Stream<Arguments> keys() {
return Stream.of(
Arguments.of("id_rsa2", null),
// "id_ecdsa_nistp256" | null // TODO: Need to improve PKCS8 key support.
Arguments.of("id_ecdsa_opensshv1", null),
Arguments.of("id_ed25519_opensshv1", null),
Arguments.of("id_ed25519_opensshv1_aes256cbc.pem", "foobar"),
Arguments.of("id_ed25519_opensshv1_aes128cbc.pem", "sshjtest"),
Arguments.of("id_ed25519_opensshv1_protected", "sshjtest"),
Arguments.of("id_rsa", null),
Arguments.of("id_rsa_opensshv1", null),
Arguments.of("id_ecdsa_nistp384_opensshv1", null),
Arguments.of("id_ecdsa_nistp521_opensshv1", null));
}
@ParameterizedTest(name = "should authenticate with signed public key {0}")
@MethodSource("keys")
public void shouldAuthenticateWithSignedRsaKey(String key, String passphrase) throws Throwable {
try (SSHClient client = sshd.getConnectedClient()) {
KeyProvider p = null;
if (passphrase != null) {
p = client.loadKeys("src/itest/resources/keyfiles/" + key, passphrase);
} else {
p = client.loadKeys("src/itest/resources/keyfiles/" + key);
}
client.authPublickey("sshj", p);
assertTrue(client.isAuthenticated());
}
}
@Test
public void shouldNotAuthenticateWithUnknownKey() throws Throwable {
try (SSHClient client = sshd.getConnectedClient()) {
assertThrows(UserAuthException.class, () -> {
client.authPublickey("sshj", "src/itest/resources/keyfiles/id_unknown_key");
});
assertFalse(client.isAuthenticated());
}
}
}

View File

@@ -0,0 +1,100 @@
/*
* Copyright (C)2009 - SSHJ Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hierynomus.sshj;
import java.util.stream.Stream;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import com.hierynomus.sshj.SshdContainer.SshdConfigBuilder;
import com.hierynomus.sshj.key.KeyAlgorithms;
import net.schmizz.sshj.Config;
import net.schmizz.sshj.DefaultConfig;
import net.schmizz.sshj.SSHClient;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static com.hierynomus.sshj.SshdContainer.withSshdContainer;
public class RsaShaKeySignatureTest {
public static Stream<Arguments> hostKeysAndAlgorithms() {
return Stream.of(
Arguments.of("ssh_host_ecdsa_256_key", KeyAlgorithms.ECDSASHANistp256()),
Arguments.of("ssh_host_ecdsa_384_key", KeyAlgorithms.ECDSASHANistp384()),
Arguments.of("ssh_host_ecdsa_521_key", KeyAlgorithms.ECDSASHANistp521()),
Arguments.of("ssh_host_ed25519_384_key", KeyAlgorithms.EdDSA25519()),
Arguments.of("ssh_host_rsa_2048_key", KeyAlgorithms.RSASHA512()));
}
@ParameterizedTest(name = "Should connect to server that does not support ssh-rsa with host key {1}")
@MethodSource("hostKeysAndAlgorithms")
public void shouldConnectToServerThatDoesNotSupportSshRsaWithHostKey(String key, KeyAlgorithms.Factory algorithm)
throws Throwable {
SshdConfigBuilder configBuilder = SshdConfigBuilder
.defaultBuilder()
.with("PubkeyAcceptedAlgorithms", "rsa-sha2-512,rsa-sha2-256,ssh-ed25519");
withSshdContainer(SshdContainer.Builder.defaultBuilder()
.withSshdConfig(configBuilder).addHostKey("test-container/host_keys/" + key), sshd -> {
Config c = new DefaultConfig();
c.setKeyAlgorithms(List.of(KeyAlgorithms.RSASHA512(), KeyAlgorithms.RSASHA256(), algorithm));
SSHClient client = sshd.getConnectedClient(c);
client.authPublickey("sshj", "src/itest/resources/keyfiles/id_rsa_opensshv1");
assertTrue(client.isAuthenticated());
client.disconnect();
});
}
@ParameterizedTest(name = "Should connect to a default server with host key {1} with a default config")
@MethodSource("hostKeysAndAlgorithms")
public void shouldConnectToDefaultServer(String key, KeyAlgorithms.Factory algorithm) throws Throwable {
withSshdContainer(SshdContainer.Builder.defaultBuilder().addHostKey("test-container/host_keys/" + key),
sshd -> {
SSHClient client = sshd.getConnectedClient();
client.authPublickey("sshj", "src/itest/resources/keyfiles/id_rsa_opensshv1");
assertTrue(client.isAuthenticated());
client.disconnect();
});
}
@ParameterizedTest(name = "Should connect to a server that only supports ssh-rsa with host key {1}")
@MethodSource("hostKeysAndAlgorithms")
public void shouldConnectToSshRsaOnlyServer(String key, KeyAlgorithms.Factory algorithm) throws Throwable {
SshdConfigBuilder configBuilder = SshdConfigBuilder
.defaultBuilder()
.with("PubkeyAcceptedAlgorithms", "ssh-rsa,ssh-ed25519");
withSshdContainer(SshdContainer.Builder.defaultBuilder()
.withSshdConfig(configBuilder).addHostKey("test-container/host_keys/" + key), sshd -> {
Config c = new DefaultConfig();
c.setKeyAlgorithms(List.of(KeyAlgorithms.SSHRSA(), algorithm));
SSHClient client = sshd.getConnectedClient(c);
client.authPublickey("sshj", "src/itest/resources/keyfiles/id_rsa_opensshv1");
assertTrue(client.isAuthenticated());
client.disconnect();
});
}
}

View File

@@ -0,0 +1,246 @@
/*
* Copyright (C)2009 - SSHJ Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hierynomus.sshj;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import net.schmizz.sshj.Config;
import net.schmizz.sshj.DefaultConfig;
import net.schmizz.sshj.SSHClient;
import net.schmizz.sshj.transport.verification.PromiscuousVerifier;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.function.ThrowingConsumer;
import org.slf4j.LoggerFactory;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.images.builder.ImageFromDockerfile;
import org.testcontainers.images.builder.dockerfile.DockerfileBuilder;
import org.testcontainers.utility.DockerLoggerFactory;
import java.util.function.Consumer;
import java.io.IOException;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Future;
/**
* A JUnit4 rule for launching a generic SSH server container.
*/
public class SshdContainer extends GenericContainer<SshdContainer> {
/**
* A workaround for strange logger names of testcontainers. They contain no
* dots, but contain slashes,
* square brackets, and even emoji. It's uneasy to set the logging level via the
* XML file of logback, the
* result would be less readable than the code below.
*/
public static class DebugLoggingImageFromDockerfile extends ImageFromDockerfile {
public DebugLoggingImageFromDockerfile() {
super();
Logger logger = (Logger) LoggerFactory.getILoggerFactory()
.getLogger(DockerLoggerFactory.getLogger(getDockerImageName()).getName());
logger.setLevel(Level.DEBUG);
}
}
public static class SshdConfigBuilder {
public static final String DEFAULT_SSHD_CONFIG = "" +
"PermitRootLogin yes\n" +
"AuthorizedKeysFile .ssh/authorized_keys\n" +
"Subsystem sftp /usr/lib/ssh/sftp-server\n" +
"KexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org,ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521,diffie-hellman-group-exchange-sha256,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512,diffie-hellman-group14-sha256,diffie-hellman-group14-sha1,diffie-hellman-group1-sha1,diffie-hellman-group-exchange-sha1\n"
+
"macs umac-64-etm@openssh.com,umac-128-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,umac-64@openssh.com,umac-128@openssh.com,hmac-sha2-256,hmac-sha2-512\n"
+
"TrustedUserCAKeys /etc/ssh/trusted_ca_keys\n" +
"Ciphers 3des-cbc,aes128-cbc,aes192-cbc,aes256-cbc,aes128-ctr,aes192-ctr,aes256-ctr,aes128-gcm@openssh.com,aes256-gcm@openssh.com,chacha20-poly1305@openssh.com\n"
+
"LogLevel DEBUG2\n";
private String sshdConfig;
public SshdConfigBuilder(@NotNull String sshdConfig) {
this.sshdConfig = sshdConfig;
}
public static SshdConfigBuilder defaultBuilder() {
return new SshdConfigBuilder(DEFAULT_SSHD_CONFIG);
}
public @NotNull SshdConfigBuilder withHostKey(@NotNull String hostKey) {
sshdConfig += "HostKey /etc/ssh/" + Paths.get(hostKey).getFileName() + "\n";
return this;
}
public @NotNull SshdConfigBuilder withHostKeyCertificate(@NotNull String hostKeyCertificate) {
sshdConfig += "HostCertificate /etc/ssh/" + Paths.get(hostKeyCertificate).getFileName() + "\n";
return this;
}
public @NotNull SshdConfigBuilder with(String key, String value) {
sshdConfig += key + " " + value + "\n";
return this;
}
public @NotNull String build() {
return sshdConfig;
}
}
public static class Builder implements Consumer<DockerfileBuilder> {
private List<String> hostKeys = new ArrayList<>();
private List<String> certificates = new ArrayList<>();
private @NotNull SshdConfigBuilder sshdConfig = SshdConfigBuilder.defaultBuilder();
public static Builder defaultBuilder() {
Builder b = new Builder();
return b;
}
public @NotNull Builder withSshdConfig(@NotNull SshdConfigBuilder sshdConfig) {
this.sshdConfig = sshdConfig;
return this;
}
public @NotNull Builder withAllKeys() {
this.addHostKey("test-container/ssh_host_ecdsa_key");
this.addHostKey("test-container/ssh_host_ed25519_key");
this.addHostKey("test-container/host_keys/ssh_host_ecdsa_256_key");
this.addHostKey("test-container/host_keys/ssh_host_ecdsa_384_key");
this.addHostKey("test-container/host_keys/ssh_host_ecdsa_521_key");
this.addHostKey("test-container/host_keys/ssh_host_ed25519_384_key");
this.addHostKey("test-container/host_keys/ssh_host_rsa_2048_key");
this.addHostKeyCertificate("test-container/host_keys/ssh_host_ecdsa_256_key-cert.pub");
this.addHostKeyCertificate("test-container/host_keys/ssh_host_ecdsa_384_key-cert.pub");
this.addHostKeyCertificate("test-container/host_keys/ssh_host_ecdsa_521_key-cert.pub");
this.addHostKeyCertificate("test-container/host_keys/ssh_host_ed25519_384_key-cert.pub");
this.addHostKeyCertificate("test-container/host_keys/ssh_host_rsa_2048_key-cert.pub");
return this;
}
public @NotNull SshdContainer build() {
return new SshdContainer(buildInner());
}
@NotNull Future<String> buildInner() {
return new DebugLoggingImageFromDockerfile()
.withDockerfileFromBuilder(this)
.withFileFromPath(".", Paths.get("src/itest/docker-image"))
.withFileFromString("sshd_config", sshdConfig.build());
}
@Override
public void accept(@NotNull DockerfileBuilder builder) {
builder.from("alpine:3.19.0");
builder.run("apk add --no-cache openssh");
builder.expose(22);
builder.copy("entrypoint.sh", "/entrypoint.sh");
builder.add("authorized_keys", "/home/sshj/.ssh/authorized_keys");
builder.copy("test-container/trusted_ca_keys", "/etc/ssh/trusted_ca_keys");
for (String hostKey : hostKeys) {
builder.copy(hostKey, "/etc/ssh/" + Paths.get(hostKey).getFileName());
builder.copy(hostKey + ".pub", "/etc/ssh/" + Paths.get(hostKey).getFileName() + ".pub");
}
for (String certificate : certificates) {
builder.copy(certificate, "/etc/ssh/" + Paths.get(certificate).getFileName());
}
builder.run("apk add --no-cache tini"
+ " && echo \"root:smile\" | chpasswd"
+ " && adduser -D -s /bin/ash sshj"
+ " && passwd -u sshj"
+ " && echo \"sshj:ultrapassword\" | chpasswd"
+ " && chmod 600 /home/sshj/.ssh/authorized_keys"
+ " && chmod 600 /etc/ssh/ssh_host_*_key"
+ " && chmod 644 /etc/ssh/*.pub"
+ " && chmod 755 /entrypoint.sh"
+ " && chown -R sshj:sshj /home/sshj");
builder.entryPoint("/sbin/tini", "/entrypoint.sh", "-o", "LogLevel=DEBUG2");
builder.add("sshd_config", "/etc/ssh/sshd_config");
}
public @NotNull Builder addHostKey(@NotNull String hostKey) {
hostKeys.add(hostKey);
sshdConfig.withHostKey(hostKey);
return this;
}
public @NotNull Builder addHostKeyCertificate(@NotNull String hostKeyCertificate) {
certificates.add(hostKeyCertificate);
sshdConfig.withHostKeyCertificate(hostKeyCertificate);
return this;
}
}
@SuppressWarnings("unused") // Used dynamically by Spock
public SshdContainer() {
this(new SshdContainer.Builder().withAllKeys().buildInner());
}
public SshdContainer(SshdContainer.Builder builder) {
this(builder.buildInner());
}
public SshdContainer(@NotNull Future<String> future) {
super(future);
withExposedPorts(22);
setWaitStrategy(new SshServerWaitStrategy());
withLogConsumer(outputFrame -> {
switch (outputFrame.getType()) {
case STDOUT:
logger().info("sshd stdout: {}", outputFrame.getUtf8String().stripTrailing());
break;
case STDERR:
logger().info("sshd stderr: {}", outputFrame.getUtf8String().stripTrailing());
break;
case END:
break;
}
});
}
public SSHClient getConnectedClient(Config config) throws IOException {
SSHClient sshClient = new SSHClient(config);
sshClient.addHostKeyVerifier(new PromiscuousVerifier());
sshClient.connect("127.0.0.1", getFirstMappedPort());
return sshClient;
}
public SSHClient getConnectedClient() throws IOException {
return getConnectedClient(new DefaultConfig());
}
public static void withSshdContainer(SshdContainer.Builder builder, @NotNull ThrowingConsumer<SshdContainer> consumer) throws Throwable {
SshdContainer sshdContainer = new SshdContainer(builder.buildInner());
sshdContainer.start();
try {
consumer.accept(sshdContainer);
} finally {
sshdContainer.stop();
}
}
}

View File

@@ -0,0 +1,73 @@
/*
* Copyright (C)2009 - SSHJ Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hierynomus.sshj.sftp;
import java.nio.charset.StandardCharsets;
import java.util.EnumSet;
import org.junit.jupiter.api.Test;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.shaded.org.bouncycastle.util.Arrays;
import com.hierynomus.sshj.SshdContainer;
import net.schmizz.sshj.SSHClient;
import net.schmizz.sshj.sftp.OpenMode;
import net.schmizz.sshj.sftp.RemoteFile;
import net.schmizz.sshj.sftp.SFTPClient;
import static org.assertj.core.api.Assertions.assertThat;
@Testcontainers
public class FileWriteTest {
@Container
private static SshdContainer sshd = new SshdContainer();
@Test
public void shouldAppendToFile_GH390() throws Throwable {
try (SSHClient client = sshd.getConnectedClient()) {
client.authPublickey("sshj", "src/test/resources/id_rsa");
try (SFTPClient sftp = client.newSFTPClient()) {
String file = "/home/sshj/test.txt";
byte[] initialText = "This is the initial text.\n".getBytes(StandardCharsets.UTF_16);
byte[] appendText = "And here's the appended text.\n".getBytes(StandardCharsets.UTF_16);
try (RemoteFile initial = sftp.open(file, EnumSet.of(OpenMode.WRITE, OpenMode.CREAT))) {
initial.write(0, initialText, 0, initialText.length);
}
try (RemoteFile read = sftp.open(file, EnumSet.of(OpenMode.READ))) {
byte[] readBytes = new byte[initialText.length];
read.read(0, readBytes, 0, readBytes.length);
assertThat(readBytes).isEqualTo(initialText);
}
try (RemoteFile initial = sftp.open(file, EnumSet.of(OpenMode.WRITE, OpenMode.APPEND))) {
initial.write(0, appendText, 0, appendText.length);
}
try (RemoteFile read = sftp.open(file, EnumSet.of(OpenMode.READ))) {
byte[] readBytes = new byte[initialText.length + appendText.length];
read.read(0, readBytes, 0, readBytes.length);
assertThat(Arrays.copyOfRange(readBytes, 0, initialText.length)).isEqualTo(initialText);
assertThat(Arrays.copyOfRange(readBytes, initialText.length, initialText.length + appendText.length)).isEqualTo(appendText);
}
}
}
}
}

View File

@@ -0,0 +1,81 @@
/*
* Copyright (C)2009 - SSHJ Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hierynomus.sshj.sftp;
import com.hierynomus.sshj.SshdContainer;
import net.schmizz.sshj.SSHClient;
import net.schmizz.sshj.sftp.SFTPClient;
import net.schmizz.sshj.xfer.InMemorySourceFile;
import org.junit.jupiter.api.Test;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Random;
@Testcontainers
public class PutFileCompressedTest {
private static class TestInMemorySourceFile extends InMemorySourceFile {
private final String name;
private final byte[] data;
public TestInMemorySourceFile(String name, byte[] data) {
this.name = name;
this.data = data;
}
@Override
public String getName() {
return name;
}
@Override
public long getLength() {
return data.length;
}
@Override
public InputStream getInputStream() throws IOException {
return new ByteArrayInputStream(data);
}
}
@Container
private static SshdContainer sshd = new SshdContainer();
@Test
public void shouldPutCompressedFile_GH893() throws Throwable {
try (SSHClient client = sshd.getConnectedClient()) {
client.authPublickey("sshj", "src/test/resources/id_rsa");
client.useCompression();
try (SFTPClient sftp = client.newSFTPClient()) {
String filename = "test.txt";
// needs to be a larger file for bug taking effect
byte[] content = new byte[5000];
Random r = new Random(1);
r.nextBytes(content);
sftp.put(new TestInMemorySourceFile(filename,content), "/home/sshj/");
}
}
}
}

View File

@@ -0,0 +1,47 @@
/*
* Copyright (C)2009 - SSHJ Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hierynomus.sshj.sftp;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import org.junit.jupiter.api.Test;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import com.hierynomus.sshj.SshdContainer;
import net.schmizz.sshj.SSHClient;
import net.schmizz.sshj.sftp.FileAttributes;
import net.schmizz.sshj.sftp.SFTPClient;
@Testcontainers
public class SftpIntegrationTest {
@Container
private static SshdContainer sshd = new SshdContainer();
@Test
public void shouldCheckFileExistsForNonExistingFile_GH894() throws Throwable {
try (SSHClient client = sshd.getConnectedClient()) {
client.authPublickey("sshj", "src/test/resources/id_rsa");
try (SFTPClient sftp = client.newSFTPClient()) {
String file = "/home/sshj/i_do_not_exist.txt";
FileAttributes exists = sftp.statExistence(file);
assertNull(exists);
}
}
}
}

View File

@@ -0,0 +1,65 @@
/*
* Copyright (C)2009 - SSHJ Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hierynomus.sshj.signature;
import java.io.File;
import java.io.StringReader;
import java.nio.file.Files;
import java.util.List;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import com.hierynomus.sshj.SshdContainer;
import com.hierynomus.sshj.SshdContainer.SshdConfigBuilder;
import net.schmizz.sshj.DefaultConfig;
import net.schmizz.sshj.SSHClient;
import net.schmizz.sshj.transport.verification.OpenSSHKnownHosts;
import static com.hierynomus.sshj.SshdContainer.withSshdContainer;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class HostKeyWithCertificateTest {
@ParameterizedTest(name = "Should connect to server that has a signed host public key {0}")
@ValueSource(strings = { "ssh_host_ecdsa_256_key", "ssh_host_ecdsa_384_key", "ssh_host_ecdsa_521_key",
"ssh_host_ed25519_384_key" })
// TODO "ssh_host_rsa_2048_key" fails with "HOST_KEY_NOT_VERIFIABLE" after upgrade to new OpenSSH version
public void shouldConnectToServerWithSignedHostKey(String hostkey) throws Throwable {
File caPubKey = new File("src/itest/resources/keyfiles/certificates/CA_rsa.pem.pub");
String caPubKeyContents = Files.readString(caPubKey.toPath());
String address = "127.0.0.1";
SshdConfigBuilder b = SshdConfigBuilder.defaultBuilder().with("PasswordAuthentication", "yes");
withSshdContainer(SshdContainer.Builder.defaultBuilder().withSshdConfig(b).addHostKey("test-container/host_keys/" + hostkey).addHostKeyCertificate("test-container/host_keys/" + hostkey + "-cert.pub"), sshd -> {
String knownHosts = List.of("@cert-authority " + address + " " + caPubKeyContents,
"@cert-authority [" + address + "]:" + sshd.getFirstMappedPort() + " " + caPubKeyContents).stream()
.reduce("", (a, b1) -> a + "\n" + b1);
DefaultConfig cfg = new DefaultConfig();
try (SSHClient c = new SSHClient(cfg)) {
c.addHostKeyVerifier(new OpenSSHKnownHosts(new StringReader(knownHosts)));
c.connect(address, sshd.getFirstMappedPort());
c.authPassword("sshj", "ultrapassword");
assertTrue(c.isAuthenticated());
}
});
}
}

View File

@@ -0,0 +1,83 @@
/*
* Copyright (C)2009 - SSHJ Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hierynomus.sshj.signature;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.stream.Stream;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import com.hierynomus.sshj.SshdContainer;
import com.hierynomus.sshj.SshdContainer.SshdConfigBuilder;
import net.schmizz.sshj.Config;
import net.schmizz.sshj.DefaultConfig;
import net.schmizz.sshj.SSHClient;
@Testcontainers
public class PublicKeyAuthWithCertificateTest {
@Container
private static final SshdContainer sshd = new SshdContainer(SshdContainer.Builder.defaultBuilder().withSshdConfig(SshdConfigBuilder.defaultBuilder().with("PubkeyAcceptedAlgorithms", "+ssh-rsa-cert-v01@openssh.com")).withAllKeys());
public static Stream<String> keys() {
return Stream.of(
"id_ecdsa_256_pem_signed_by_ecdsa",
"id_ecdsa_256_rfc4716_signed_by_ecdsa",
"id_ecdsa_256_pem_signed_by_ed25519",
"id_ecdsa_256_rfc4716_signed_by_ed25519",
"id_ecdsa_256_pem_signed_by_rsa",
"id_ecdsa_256_rfc4716_signed_by_rsa",
"id_ecdsa_384_pem_signed_by_ecdsa",
"id_ecdsa_384_rfc4716_signed_by_ecdsa",
"id_ecdsa_384_pem_signed_by_ed25519",
"id_ecdsa_384_rfc4716_signed_by_ed25519",
"id_ecdsa_384_pem_signed_by_rsa",
"id_ecdsa_384_rfc4716_signed_by_rsa",
"id_ecdsa_521_pem_signed_by_ecdsa",
"id_ecdsa_521_rfc4716_signed_by_ecdsa",
"id_ecdsa_521_pem_signed_by_ed25519",
"id_ecdsa_521_rfc4716_signed_by_ed25519",
"id_ecdsa_521_pem_signed_by_rsa",
"id_ecdsa_521_rfc4716_signed_by_rsa",
"id_rsa_2048_pem_signed_by_ecdsa",
"id_rsa_2048_rfc4716_signed_by_ecdsa",
"id_rsa_2048_pem_signed_by_ed25519",
"id_rsa_2048_rfc4716_signed_by_ed25519",
"id_rsa_2048_pem_signed_by_rsa",
"id_rsa_2048_rfc4716_signed_by_rsa",
"id_ed25519_384_rfc4716_signed_by_ecdsa",
"id_ed25519_384_rfc4716_signed_by_ed25519",
"id_ed25519_384_rfc4716_signed_by_rsa");
}
@ParameterizedTest(name = "should authenticate with signed public key {0}")
@MethodSource("keys")
public void shouldAuthenticateWithSignedPublicKey(String key) throws Throwable {
Config c = new DefaultConfig();
SSHClient client = sshd.getConnectedClient(c);
client.authPublickey("sshj", "src/itest/resources/keyfiles/certificates/" + key);
assertTrue(client.isAuthenticated());
client.disconnect();
}
}

View File

@@ -0,0 +1,57 @@
/*
* Copyright (C)2009 - SSHJ Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hierynomus.sshj.signature;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.List;
import java.util.stream.Stream;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import com.hierynomus.sshj.SshdContainer;
import com.hierynomus.sshj.SshdContainer.SshdConfigBuilder;
import com.hierynomus.sshj.key.KeyAlgorithms;
import net.schmizz.sshj.Config;
import net.schmizz.sshj.DefaultConfig;
import net.schmizz.sshj.SSHClient;
@Testcontainers
public class SignatureTest {
@Container
private static final SshdContainer sshd = new SshdContainer(SshdContainer.Builder.defaultBuilder().withSshdConfig(SshdConfigBuilder.defaultBuilder().with("HostKeyAlgorithms", "+ssh-rsa").with("PubkeyAcceptedAlgorithms", "+ssh-rsa")).withAllKeys());
public static Stream<KeyAlgorithms.Factory> algs() {
return Stream.of(KeyAlgorithms.SSHRSA(), KeyAlgorithms.RSASHA256(), KeyAlgorithms.RSASHA512());
}
@ParameterizedTest(name = "should correctly connect with Signature {0}")
@MethodSource("algs")
public void shouldCorrectlyConnectWithMac(KeyAlgorithms.Factory alg) throws Throwable {
Config c = new DefaultConfig();
c.setKeyAlgorithms(List.of(alg));
try (SSHClient client = sshd.getConnectedClient(c)) {
client.authPublickey("sshj", "src/itest/resources/keyfiles/id_rsa");
assertTrue(client.isAuthenticated());
}
}
}

View File

@@ -0,0 +1,65 @@
/*
* Copyright (C)2009 - SSHJ Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hierynomus.sshj.transport.cipher;
import static org.junit.Assert.assertTrue;
import java.util.List;
import java.util.stream.Stream;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import com.hierynomus.sshj.SshdContainer;
import net.schmizz.sshj.Config;
import net.schmizz.sshj.DefaultConfig;
import net.schmizz.sshj.SSHClient;
import net.schmizz.sshj.common.Factory;
import net.schmizz.sshj.transport.cipher.Cipher;
@Testcontainers
public class CipherTest {
@Container
private static final SshdContainer sshd = new SshdContainer();
public static Stream<Factory.Named<Cipher>> ciphers() {
return Stream.of(BlockCiphers.TripleDESCBC(),
BlockCiphers.AES128CBC(),
BlockCiphers.AES128CTR(),
BlockCiphers.AES192CBC(),
BlockCiphers.AES192CTR(),
BlockCiphers.AES256CBC(),
BlockCiphers.AES256CTR(),
GcmCiphers.AES128GCM(),
GcmCiphers.AES256GCM(),
ChachaPolyCiphers.CHACHA_POLY_OPENSSH());
}
@ParameterizedTest(name = "should correctly connect with Cipher {0}")
@MethodSource("ciphers")
public void shouldCorrectlyConnectWithCipher(Factory.Named<Cipher> cipher) throws Throwable {
Config c = new DefaultConfig();
c.setCipherFactories(List.of(cipher));
try (SSHClient client = sshd.getConnectedClient(c)) {
client.authPublickey("sshj", "src/itest/resources/keyfiles/id_rsa_opensshv1");
assertTrue(client.isAuthenticated());
}
}
}

View File

@@ -0,0 +1,72 @@
/*
* Copyright (C)2009 - SSHJ Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hierynomus.sshj.transport.kex;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.List;
import java.util.stream.Stream;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import com.hierynomus.sshj.SshdContainer;
import net.schmizz.sshj.Config;
import net.schmizz.sshj.DefaultConfig;
import net.schmizz.sshj.SSHClient;
import net.schmizz.sshj.common.Factory;
import net.schmizz.sshj.transport.kex.Curve25519SHA256;
import net.schmizz.sshj.transport.kex.DHGexSHA1;
import net.schmizz.sshj.transport.kex.DHGexSHA256;
import net.schmizz.sshj.transport.kex.ECDHNistP;
import net.schmizz.sshj.transport.kex.KeyExchange;
@Testcontainers
public class KexTest {
@Container
private static final SshdContainer sshd = new SshdContainer();
public static Stream<Factory.Named<KeyExchange>> kex() {
return Stream.of(
DHGroups.Group1SHA1(),
DHGroups.Group14SHA1(),
DHGroups.Group14SHA256(),
DHGroups.Group16SHA512(),
DHGroups.Group18SHA512(),
new DHGexSHA1.Factory(),
new DHGexSHA256.Factory(),
new Curve25519SHA256.Factory(),
new Curve25519SHA256.FactoryLibSsh(),
new ECDHNistP.Factory256(),
new ECDHNistP.Factory384(),
new ECDHNistP.Factory521());
}
@ParameterizedTest(name = "should correctly connect with Key Exchange {0}")
@MethodSource("kex")
public void shouldCorrectlyConnectWithMac(Factory.Named<KeyExchange> kex) throws Throwable {
Config c = new DefaultConfig();
c.setKeyExchangeFactories(List.of(kex));
try (SSHClient client = sshd.getConnectedClient(c)) {
client.authPublickey("sshj", "src/itest/resources/keyfiles/id_rsa_opensshv1");
assertTrue(client.isAuthenticated());
}
}
}

View File

@@ -0,0 +1,153 @@
/*
* Copyright (C)2009 - SSHJ Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hierynomus.sshj.transport.kex;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.read.ListAppender;
import com.hierynomus.sshj.SshdContainer;
import net.schmizz.keepalive.KeepAlive;
import net.schmizz.keepalive.KeepAliveProvider;
import net.schmizz.sshj.Config;
import net.schmizz.sshj.DefaultConfig;
import net.schmizz.sshj.SSHClient;
import net.schmizz.sshj.common.Message;
import net.schmizz.sshj.common.SSHPacket;
import net.schmizz.sshj.connection.ConnectionImpl;
import net.schmizz.sshj.transport.TransportException;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.slf4j.LoggerFactory;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertTrue;
@Testcontainers
class StrictKeyExchangeTest {
@Container
private static final SshdContainer sshd = new SshdContainer();
private final List<Logger> watchedLoggers = new ArrayList<>();
private final ListAppender<ILoggingEvent> logWatcher = new ListAppender<>();
@BeforeEach
void setUpLogWatcher() {
logWatcher.start();
setUpLogger("net.schmizz.sshj.transport.Decoder");
setUpLogger("net.schmizz.sshj.transport.Encoder");
setUpLogger("net.schmizz.sshj.transport.KeyExchanger");
}
@AfterEach
void tearDown() {
watchedLoggers.forEach(Logger::detachAndStopAllAppenders);
}
private void setUpLogger(String className) {
Logger logger = ((Logger) LoggerFactory.getLogger(className));
logger.addAppender(logWatcher);
watchedLoggers.add(logger);
}
private static Stream<Arguments> strictKeyExchange() {
Config defaultConfig = new DefaultConfig();
Config heartbeaterConfig = new DefaultConfig();
heartbeaterConfig.setKeepAliveProvider(new KeepAliveProvider() {
@Override
public KeepAlive provide(ConnectionImpl connection) {
return new HotLoopHeartbeater(connection);
}
});
return Stream.of(defaultConfig, heartbeaterConfig).map(Arguments::of);
}
@MethodSource
@ParameterizedTest
void strictKeyExchange(Config config) throws Throwable {
try (SSHClient client = sshd.getConnectedClient(config)) {
client.authPublickey("sshj", "src/itest/resources/keyfiles/id_rsa_opensshv1");
assertTrue(client.isAuthenticated());
}
List<String> keyExchangerLogs = getLogs("KeyExchanger");
assertThat(keyExchangerLogs).contains(
"Initiating key exchange",
"Sending SSH_MSG_KEXINIT",
"Received SSH_MSG_KEXINIT",
"Enabling strict key exchange extension"
);
List<String> decoderLogs = getLogs("Decoder").stream()
.map(log -> log.split(":")[0])
.collect(Collectors.toList());
assertThat(decoderLogs).startsWith(
"Received packet #0",
"Received packet #1",
"Received packet #2",
"Received packet #0",
"Received packet #1",
"Received packet #2",
"Received packet #3"
);
List<String> encoderLogs = getLogs("Encoder").stream()
.map(log -> log.split(":")[0])
.collect(Collectors.toList());
assertThat(encoderLogs).startsWith(
"Encoding packet #0",
"Encoding packet #1",
"Encoding packet #2",
"Encoding packet #0",
"Encoding packet #1",
"Encoding packet #2",
"Encoding packet #3"
);
}
private List<String> getLogs(String className) {
return logWatcher.list.stream()
.filter(event -> event.getLoggerName().endsWith(className))
.map(ILoggingEvent::getFormattedMessage)
.collect(Collectors.toList());
}
private static class HotLoopHeartbeater extends KeepAlive {
HotLoopHeartbeater(ConnectionImpl conn) {
super(conn, "sshj-Heartbeater");
}
@Override
public boolean isEnabled() {
return true;
}
@Override
protected void doKeepAlive() throws TransportException {
conn.getTransport().write(new SSHPacket(Message.IGNORE));
}
}
}

View File

@@ -0,0 +1,54 @@
/*
* Copyright (C)2009 - SSHJ Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hierynomus.sshj.transport.mac;
import static org.junit.Assert.assertTrue;
import java.util.List;
import java.util.stream.Stream;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import com.hierynomus.sshj.SshdContainer;
import net.schmizz.sshj.Config;
import net.schmizz.sshj.DefaultConfig;
import net.schmizz.sshj.SSHClient;
@Testcontainers
public class MacTest {
@Container
private static final SshdContainer sshd = new SshdContainer();
public static Stream<Macs.Factory> macs() {
return Stream.of(Macs.HMACSHA2256(), Macs.HMACSHA2512(), Macs.HMACSHA2256Etm(), Macs.HMACSHA2512Etm());
}
@ParameterizedTest(name = "should correctly connect with MAC {0}")
@MethodSource("macs")
public void shouldCorrectlyConnectWithMac(Macs.Factory mac) throws Throwable {
Config c = new DefaultConfig();
c.setMACFactories(List.of(mac));
try (SSHClient client = sshd.getConnectedClient(c)) {
client.authPublickey("sshj", "src/itest/resources/keyfiles/id_rsa_opensshv1");
assertTrue(client.isAuthenticated());
}
}
}

View File

@@ -0,0 +1,34 @@
<!--
Copyright (C)2009 - SSHJ Contributors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%.-20thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="info">
<appender-ref ref="STDOUT"/>
</root>
<logger name="org.apache.sshd.server" level="debug" />
<logger name="net.schmizz.sshj" level="debug"/>
<logger name="net.schmizz.sshj.transport" level="trace" />
</configuration>

View File

@@ -1,78 +0,0 @@
/*
* Copyright (C)2009 - SSHJ Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hierynomus.sshj.backport;
import net.schmizz.sshj.common.IOUtils;
import java.io.IOException;
import java.io.InputStream;
import java.net.*;
public class Jdk7HttpProxySocket extends Socket {
private Proxy httpProxy = null;
public Jdk7HttpProxySocket(Proxy proxy) {
super(proxy.type() == Proxy.Type.HTTP ? Proxy.NO_PROXY : proxy);
if (proxy.type() == Proxy.Type.HTTP) {
this.httpProxy = proxy;
}
}
@Override
public void connect(SocketAddress endpoint, int timeout) throws IOException {
if (httpProxy != null) {
connectHttpProxy(endpoint, timeout);
} else {
super.connect(endpoint, timeout);
}
}
private void connectHttpProxy(SocketAddress endpoint, int timeout) throws IOException {
super.connect(httpProxy.address(), timeout);
if (!(endpoint instanceof InetSocketAddress)) {
throw new SocketException("Expected an InetSocketAddress to connect to, got: " + endpoint);
}
InetSocketAddress isa = (InetSocketAddress) endpoint;
String httpConnect = "CONNECT " + isa.getHostName() + ":" + isa.getPort() + " HTTP/1.0\n\n";
getOutputStream().write(httpConnect.getBytes(IOUtils.UTF8));
checkAndFlushProxyResponse();
}
private void checkAndFlushProxyResponse()throws IOException {
InputStream socketInput = getInputStream();
byte[] tmpBuffer = new byte[512];
int len = socketInput.read(tmpBuffer, 0, tmpBuffer.length);
if (len == 0) {
throw new SocketException("Empty response from proxy");
}
String proxyResponse = new String(tmpBuffer, 0, len, IOUtils.UTF8);
// Expecting HTTP/1.x 200 OK
if (proxyResponse.contains("200")) {
// Flush any outstanding message in buffer
if (socketInput.available() > 0) {
socketInput.skip(socketInput.available());
}
// Proxy Connect Successful
} else {
throw new SocketException("Fail to create Socket\nResponse was:" + proxyResponse);
}
}
}

View File

@@ -1,41 +0,0 @@
/*
* Copyright (C)2009 - SSHJ Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hierynomus.sshj.backport;
import java.io.Closeable;
import java.io.IOException;
import java.net.Socket;
public class Sockets {
/**
* Java 7 and up have Socket implemented as Closeable, whereas Java6 did not have this inheritance.
* @param socket The socket to wrap as Closeable
* @return The (potentially wrapped) Socket as a Closeable.
*/
public static Closeable asCloseable(final Socket socket) {
if (Closeable.class.isAssignableFrom(socket.getClass())) {
return Closeable.class.cast(socket);
} else {
return new Closeable() {
@Override
public void close() throws IOException {
socket.close();
}
};
}
}
}

View File

@@ -29,7 +29,8 @@ public class ThreadNameProvider {
public static void setThreadName(final Thread thread, final RemoteAddressProvider remoteAddressProvider) {
final InetSocketAddress remoteSocketAddress = remoteAddressProvider.getRemoteSocketAddress();
final String address = remoteSocketAddress == null ? DISCONNECTED : remoteSocketAddress.toString();
final String threadName = String.format("sshj-%s-%s", thread.getClass().getSimpleName(), address);
final long started = System.currentTimeMillis();
final String threadName = String.format("sshj-%s-%s-%d", thread.getClass().getSimpleName(), address, started);
thread.setName(threadName);
}
}

View File

@@ -22,9 +22,6 @@ import net.schmizz.sshj.signature.SignatureDSA;
import net.schmizz.sshj.signature.SignatureECDSA;
import net.schmizz.sshj.signature.SignatureRSA;
import java.util.Arrays;
import java.util.List;
public class KeyAlgorithms {
public static Factory SSHRSA() { return new Factory("ssh-rsa", new SignatureRSA.FactorySSHRSA(), KeyType.RSA); }
@@ -67,5 +64,10 @@ public class KeyAlgorithms {
public KeyAlgorithm create() {
return new BaseKeyAlgorithm(algorithmName, signatureFactory, keyType);
}
@Override
public String toString() {
return algorithmName;
}
}
}

View File

@@ -0,0 +1,30 @@
/*
* Copyright (C)2009 - SSHJ Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hierynomus.sshj.sftp;
import com.hierynomus.sshj.sftp.RemoteResourceSelector.Result;
import net.schmizz.sshj.sftp.RemoteResourceFilter;
public class RemoteResourceFilterConverter {
public static RemoteResourceSelector selectorFrom(RemoteResourceFilter filter) {
if (filter == null) {
return RemoteResourceSelector.ALL;
}
return resource -> filter.accept(resource) ? Result.ACCEPT : Result.CONTINUE;
}
}

View File

@@ -0,0 +1,49 @@
/*
* Copyright (C)2009 - SSHJ Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hierynomus.sshj.sftp;
import net.schmizz.sshj.sftp.RemoteResourceInfo;
public interface RemoteResourceSelector {
public static RemoteResourceSelector ALL = new RemoteResourceSelector() {
@Override
public Result select(RemoteResourceInfo resource) {
return Result.ACCEPT;
}
};
enum Result {
/**
* Accept the remote resource and add it to the result.
*/
ACCEPT,
/**
* Do not add the remote resource to the result and continue with the next.
*/
CONTINUE,
/**
* Do not add the remote resource to the result and stop further execution.
*/
BREAK;
}
/**
* Decide whether the remote resource should be included in the result and whether execution should continue.
*/
Result select(RemoteResourceInfo resource);
}

View File

@@ -16,9 +16,8 @@
package com.hierynomus.sshj.transport.cipher;
import java.security.GeneralSecurityException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.spec.AlgorithmParameterSpec;
import java.util.Arrays;
import javax.crypto.spec.IvParameterSpec;
@@ -82,8 +81,7 @@ public class ChachaPolyCipher extends BaseCipher {
}
@Override
protected void initCipher(javax.crypto.Cipher cipher, Mode mode, byte[] key, byte[] iv)
throws InvalidKeyException, InvalidAlgorithmParameterException {
protected void initCipher(javax.crypto.Cipher cipher, Mode mode, byte[] key, byte[] iv) {
this.mode = mode;
cipherKey = getKeySpec(Arrays.copyOfRange(key, 0, CHACHA_KEY_SIZE));
@@ -127,28 +125,34 @@ public class ChachaPolyCipher extends BaseCipher {
@Override
public void update(byte[] input, int inputOffset, int inputLen) {
if (inputOffset != AAD_LENGTH) {
if (inputOffset != 0 && inputOffset != AAD_LENGTH) {
throw new IllegalArgumentException("updateAAD called with inputOffset " + inputOffset);
}
final int macInputLength = AAD_LENGTH + inputLen;
final int macInputLength = inputOffset + inputLen;
if (mode == Mode.Decrypt) {
byte[] macInput = new byte[macInputLength];
System.arraycopy(encryptedAad, 0, macInput, 0, AAD_LENGTH);
System.arraycopy(input, AAD_LENGTH, macInput, AAD_LENGTH, inputLen);
final byte[] macInput = new byte[macInputLength];
byte[] expectedPolyTag = mac.doFinal(macInput);
byte[] actualPolyTag = Arrays.copyOfRange(input, macInputLength, macInputLength + POLY_TAG_LENGTH);
if (!Arrays.equals(actualPolyTag, expectedPolyTag)) {
if (inputOffset == 0) {
// Handle decryption without AAD
System.arraycopy(input, 0, macInput, 0, inputLen);
} else {
// Handle decryption with previous AAD from updateAAD()
System.arraycopy(encryptedAad, 0, macInput, 0, AAD_LENGTH);
System.arraycopy(input, AAD_LENGTH, macInput, AAD_LENGTH, inputLen);
}
final byte[] expectedPolyTag = mac.doFinal(macInput);
final byte[] actualPolyTag = Arrays.copyOfRange(input, macInputLength, macInputLength + POLY_TAG_LENGTH);
if (!MessageDigest.isEqual(actualPolyTag, expectedPolyTag)) {
throw new SSHRuntimeException("MAC Error");
}
}
try {
cipher.update(input, AAD_LENGTH, inputLen, input, AAD_LENGTH);
cipher.update(input, inputOffset, inputLen, input, inputOffset);
} catch (GeneralSecurityException e) {
throw new SSHRuntimeException("Error updating data through cipher", e);
throw new SSHRuntimeException("ChaCha20 cipher processing failed", e);
}
if (mode == Mode.Encrypt) {

View File

@@ -89,6 +89,11 @@ public class DHGroups {
public String getName() {
return name;
}
@Override
public String toString() {
return name;
}
}
}

View File

@@ -15,20 +15,26 @@
*/
package com.hierynomus.sshj.transport.verification;
import net.schmizz.sshj.common.Base64;
import net.schmizz.sshj.common.Base64DecodingException;
import net.schmizz.sshj.common.Base64Decoder;
import net.schmizz.sshj.common.IOUtils;
import net.schmizz.sshj.common.SSHException;
import net.schmizz.sshj.transport.mac.MAC;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.regex.Pattern;
import com.hierynomus.sshj.transport.mac.Macs;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class KnownHostMatchers {
private static final Logger log = LoggerFactory.getLogger(KnownHostMatchers.class);
public static HostMatcher createMatcher(String hostEntry) throws SSHException {
if (hostEntry.contains(",")) {
return new AnyHostMatcher(hostEntry);
@@ -80,17 +86,22 @@ public class KnownHostMatchers {
@Override
public boolean match(String hostname) throws IOException {
return hash.equals(hashHost(hostname));
try {
return hash.equals(hashHost(hostname));
} catch (Base64DecodingException err) {
log.warn("Hostname [{}] not matched: salt decoding failed", hostname, err);
return false;
}
}
private String hashHost(String host) throws IOException {
private String hashHost(String host) throws IOException, Base64DecodingException {
sha1.init(getSaltyBytes());
return "|1|" + salt + "|" + Base64.encodeBytes(sha1.doFinal(host.getBytes(IOUtils.UTF8)));
return "|1|" + salt + "|" + Base64.getEncoder().encodeToString(sha1.doFinal(host.getBytes(IOUtils.UTF8)));
}
private byte[] getSaltyBytes() throws IOException {
private byte[] getSaltyBytes() throws IOException, Base64DecodingException {
if (saltyBytes == null) {
saltyBytes = Base64.decode(salt);
saltyBytes = Base64Decoder.decode(salt);
}
return saltyBytes;
}

View File

@@ -15,7 +15,8 @@
*/
package com.hierynomus.sshj.userauth.keyprovider;
import net.schmizz.sshj.common.Base64;
import net.schmizz.sshj.common.Base64DecodingException;
import net.schmizz.sshj.common.Base64Decoder;
import net.schmizz.sshj.common.Buffer;
import net.schmizz.sshj.common.KeyType;
@@ -54,9 +55,10 @@ public class OpenSSHKeyFileUtil {
if (!keydata.isEmpty()) {
String[] parts = keydata.trim().split("\\s+");
if (parts.length >= 2) {
byte[] decodedPublicKey = Base64Decoder.decode(parts[1]);
return new ParsedPubKey(
KeyType.fromString(parts[0]),
new Buffer.PlainBuffer(Base64.decode(parts[1])).readPublicKey()
new Buffer.PlainBuffer(decodedPublicKey).readPublicKey()
);
} else {
throw new IOException("Got line with only one column");
@@ -64,6 +66,8 @@ public class OpenSSHKeyFileUtil {
}
}
throw new IOException("Public key file is blank");
} catch (Base64DecodingException err) {
throw new IOException("Public key decoding failed", err);
} finally {
br.close();
}

View File

@@ -18,6 +18,9 @@ package com.hierynomus.sshj.userauth.keyprovider;
import com.hierynomus.sshj.common.KeyAlgorithm;
import com.hierynomus.sshj.common.KeyDecryptionFailedException;
import com.hierynomus.sshj.transport.cipher.BlockCiphers;
import com.hierynomus.sshj.transport.cipher.ChachaPolyCiphers;
import com.hierynomus.sshj.transport.cipher.GcmCiphers;
import com.hierynomus.sshj.userauth.keyprovider.bcrypt.BCrypt;
import net.i2p.crypto.eddsa.EdDSAPrivateKey;
import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable;
import net.i2p.crypto.eddsa.spec.EdDSAPrivateKeySpec;
@@ -27,40 +30,65 @@ import net.schmizz.sshj.transport.cipher.Cipher;
import net.schmizz.sshj.userauth.keyprovider.BaseFileKeyProvider;
import net.schmizz.sshj.userauth.keyprovider.FileKeyProvider;
import net.schmizz.sshj.userauth.keyprovider.KeyFormat;
import net.schmizz.sshj.userauth.password.PasswordFinder;
import org.bouncycastle.asn1.nist.NISTNamedCurves;
import org.bouncycastle.asn1.x9.X9ECParameters;
import org.bouncycastle.jce.spec.ECNamedCurveSpec;
import com.hierynomus.sshj.userauth.keyprovider.bcrypt.BCrypt;
import org.bouncycastle.openssl.EncryptionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
import java.io.*;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.security.*;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.ECPrivateKeySpec;
import java.security.spec.RSAPrivateCrtKeySpec;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
/**
* Reads a key file in the new OpenSSH format.
* The format is described in the following document: https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.key
*/
public class OpenSSHKeyV1KeyFile extends BaseFileKeyProvider {
private static final Logger logger = LoggerFactory.getLogger(OpenSSHKeyV1KeyFile.class);
private static final String BEGIN = "-----BEGIN ";
private static final String END = "-----END ";
private static final byte[] AUTH_MAGIC = "openssh-key-v1\0".getBytes();
public static final String OPENSSH_PRIVATE_KEY = "OPENSSH PRIVATE KEY-----";
public static final String BCRYPT = "bcrypt";
private static final String NONE_CIPHER = "none";
private static final Map<String, Factory.Named<Cipher>> SUPPORTED_CIPHERS = new HashMap<>();
static {
SUPPORTED_CIPHERS.put(BlockCiphers.TripleDESCBC().getName(), BlockCiphers.TripleDESCBC());
SUPPORTED_CIPHERS.put(BlockCiphers.AES128CBC().getName(), BlockCiphers.AES128CBC());
SUPPORTED_CIPHERS.put(BlockCiphers.AES192CBC().getName(), BlockCiphers.AES192CBC());
SUPPORTED_CIPHERS.put(BlockCiphers.AES256CBC().getName(), BlockCiphers.AES256CBC());
SUPPORTED_CIPHERS.put(BlockCiphers.AES128CTR().getName(), BlockCiphers.AES128CTR());
SUPPORTED_CIPHERS.put(BlockCiphers.AES192CTR().getName(), BlockCiphers.AES192CTR());
SUPPORTED_CIPHERS.put(BlockCiphers.AES256CTR().getName(), BlockCiphers.AES256CTR());
SUPPORTED_CIPHERS.put(GcmCiphers.AES256GCM().getName(), GcmCiphers.AES256GCM());
SUPPORTED_CIPHERS.put(GcmCiphers.AES128GCM().getName(), GcmCiphers.AES128GCM());
SUPPORTED_CIPHERS.put(ChachaPolyCiphers.CHACHA_POLY_OPENSSH().getName(), ChachaPolyCiphers.CHACHA_POLY_OPENSSH());
}
private PublicKey pubKey;
@Override
public PublicKey getPublic()
throws IOException {
return pubKey != null ? pubKey : super.getPublic();
}
public static class Factory
implements net.schmizz.sshj.common.Factory.Named<FileKeyProvider> {
@@ -78,33 +106,60 @@ public class OpenSSHKeyV1KeyFile extends BaseFileKeyProvider {
protected final Logger log = LoggerFactory.getLogger(getClass());
@Override
public void init(File location) {
public void init(File location, PasswordFinder pwdf) {
File pubKey = OpenSSHKeyFileUtil.getPublicKeyFile(location);
if (pubKey != null)
if (pubKey != null) {
try {
initPubKey(new FileReader(pubKey));
} catch (IOException e) {
// let super provide both public & private key
log.warn("Error reading public key file: {}", e.toString());
}
super.init(location);
}
super.init(location, pwdf);
}
@Override
public void init(String privateKey, String publicKey, PasswordFinder pwdf) {
if (pubKey != null) {
try {
initPubKey(new StringReader(publicKey));
} catch (IOException e) {
log.warn("Error reading public key file: {}", e.toString());
}
}
super.init(privateKey, null, pwdf);
}
@Override
public void init(Reader privateKey, Reader publicKey, PasswordFinder pwdf) {
if (pubKey != null) {
try {
initPubKey(publicKey);
} catch (IOException e) {
log.warn("Error reading public key file: {}", e.toString());
}
}
super.init(privateKey, null, pwdf);
}
@Override
protected KeyPair readKeyPair() throws IOException {
BufferedReader reader = new BufferedReader(resource.getReader());
final BufferedReader reader = new BufferedReader(resource.getReader());
try {
if (!checkHeader(reader)) {
throw new IOException("This key is not in 'openssh-key-v1' format");
if (checkHeader(reader)) {
final String encodedPrivateKey = readEncodedKey(reader);
byte[] decodedPrivateKey = Base64Decoder.decode(encodedPrivateKey);
final PlainBuffer bufferedPrivateKey = new PlainBuffer(decodedPrivateKey);
return readDecodedKeyPair(bufferedPrivateKey);
} else {
final String message = String.format("File header not found [%s%s]", BEGIN, OPENSSH_PRIVATE_KEY);
throw new IOException(message);
}
String keyFile = readKeyFile(reader);
byte[] decode = Base64.decode(keyFile);
PlainBuffer keyBuffer = new PlainBuffer(decode);
return readDecodedKeyPair(keyBuffer);
} catch (GeneralSecurityException e) {
throw new SSHRuntimeException(e);
} catch (final GeneralSecurityException e) {
throw new SSHRuntimeException("Read OpenSSH Version 1 Key failed", e);
} catch (Base64DecodingException e) {
throw new SSHRuntimeException("Private Key decoding failed", e);
} finally {
IOUtils.closeQuietly(reader);
}
@@ -129,88 +184,143 @@ public class OpenSSHKeyV1KeyFile extends BaseFileKeyProvider {
int nrKeys = keyBuffer.readUInt32AsInt(); // int number of keys N; Should be 1
if (nrKeys != 1) {
throw new IOException("We don't support having more than 1 key in the file (yet).");
final String message = String.format("OpenSSH Private Key number of keys not supported [%d]", nrKeys);
throw new IOException(message);
}
PublicKey publicKey = pubKey;
if (publicKey == null) {
publicKey = readPublicKey(new PlainBuffer(keyBuffer.readBytes()));
}
else {
} else {
keyBuffer.readBytes();
}
PlainBuffer privateKeyBuffer = new PlainBuffer(keyBuffer.readBytes()); // string (possibly) encrypted, padded list of private keys
if ("none".equals(cipherName)) {
logger.debug("Reading unencrypted keypair");
final byte[] privateKeyEncoded = keyBuffer.readBytes();
final PlainBuffer privateKeyBuffer = new PlainBuffer(privateKeyEncoded);
if (NONE_CIPHER.equals(cipherName)) {
return readUnencrypted(privateKeyBuffer, publicKey);
} else {
logger.info("Keypair is encrypted with: " + cipherName + ", " + kdfName + ", " + Arrays.toString(kdfOptions));
final byte[] encryptedPrivateKey = readEncryptedPrivateKey(privateKeyEncoded, keyBuffer);
while (true) {
PlainBuffer decryptionBuffer = new PlainBuffer(privateKeyBuffer);
PlainBuffer decrypted = decryptBuffer(decryptionBuffer, cipherName, kdfName, kdfOptions);
final byte[] encrypted = encryptedPrivateKey.clone();
try {
final PlainBuffer decrypted = decryptPrivateKey(encrypted, privateKeyEncoded.length, cipherName, kdfName, kdfOptions);
return readUnencrypted(decrypted, publicKey);
} catch (KeyDecryptionFailedException e) {
if (pwdf == null || !pwdf.shouldRetry(resource))
throw e;
}
}
// throw new IOException("Cannot read encrypted keypair with " + cipherName + " yet.");
}
}
private PlainBuffer decryptBuffer(PlainBuffer privateKeyBuffer, String cipherName, String kdfName, byte[] kdfOptions) throws IOException {
Cipher cipher = createCipher(cipherName);
initializeCipher(kdfName, kdfOptions, cipher);
byte[] array = privateKeyBuffer.array();
cipher.update(array, 0, privateKeyBuffer.available());
return new PlainBuffer(array);
private byte[] readEncryptedPrivateKey(final byte[] privateKeyEncoded, final PlainBuffer inputBuffer) throws Buffer.BufferException {
final byte[] encryptedPrivateKey;
final int bufferRemaining = inputBuffer.available();
if (bufferRemaining == 0) {
encryptedPrivateKey = privateKeyEncoded;
} else {
// Read Authentication Tag for AES-GCM or ChaCha20-Poly1305
final byte[] authenticationTag = new byte[bufferRemaining];
inputBuffer.readRawBytes(authenticationTag);
final int encryptedBufferLength = privateKeyEncoded.length + authenticationTag.length;
final PlainBuffer encryptedBuffer = new PlainBuffer(encryptedBufferLength);
encryptedBuffer.putRawBytes(privateKeyEncoded);
encryptedBuffer.putRawBytes(authenticationTag);
encryptedPrivateKey = new byte[encryptedBufferLength];
encryptedBuffer.readRawBytes(encryptedPrivateKey);
}
return encryptedPrivateKey;
}
private void initializeCipher(String kdfName, byte[] kdfOptions, Cipher cipher) throws Buffer.BufferException {
private PlainBuffer decryptPrivateKey(final byte[] privateKey, final int privateKeyLength, final String cipherName, final String kdfName, final byte[] kdfOptions) throws IOException {
try {
final Cipher cipher = createCipher(cipherName);
initializeCipher(kdfName, kdfOptions, cipher);
cipher.update(privateKey, 0, privateKeyLength);
} catch (final SSHRuntimeException e) {
final String message = String.format("OpenSSH Private Key decryption failed with cipher [%s]", cipherName);
throw new KeyDecryptionFailedException(new EncryptionException(message, e));
}
final PlainBuffer decryptedPrivateKey = new PlainBuffer(privateKeyLength);
decryptedPrivateKey.putRawBytes(privateKey, 0, privateKeyLength);
return decryptedPrivateKey;
}
private void initializeCipher(final String kdfName, final byte[] kdfOptions, final Cipher cipher) throws Buffer.BufferException {
if (kdfName.equals(BCRYPT)) {
PlainBuffer opts = new PlainBuffer(kdfOptions);
final PlainBuffer bufferedOptions = new PlainBuffer(kdfOptions);
byte[] passphrase = new byte[0];
if (pwdf != null) {
CharBuffer charBuffer = CharBuffer.wrap(pwdf.reqPassword(null));
ByteBuffer byteBuffer = Charset.forName("UTF-8").encode(charBuffer);
final CharBuffer charBuffer = CharBuffer.wrap(pwdf.reqPassword(null));
final ByteBuffer byteBuffer = StandardCharsets.UTF_8.encode(charBuffer);
passphrase = Arrays.copyOfRange(byteBuffer.array(), byteBuffer.position(), byteBuffer.limit());
Arrays.fill(charBuffer.array(), '\u0000');
Arrays.fill(byteBuffer.array(), (byte) 0);
}
byte[] keyiv = new byte[cipher.getIVSize()+ cipher.getBlockSize()];
new BCrypt().pbkdf(passphrase, opts.readBytes(), opts.readUInt32AsInt(), keyiv);
final int ivSize = cipher.getIVSize();
final int blockSize = cipher.getBlockSize();
final int parameterSize = ivSize + blockSize;
final byte[] keyIvParameters = new byte[parameterSize];
final byte[] salt = bufferedOptions.readBytes();
final int iterations = bufferedOptions.readUInt32AsInt();
new BCrypt().pbkdf(passphrase, salt, iterations, keyIvParameters);
Arrays.fill(passphrase, (byte) 0);
byte[] key = Arrays.copyOfRange(keyiv, 0, cipher.getBlockSize());
byte[] iv = Arrays.copyOfRange(keyiv, cipher.getBlockSize(), cipher.getIVSize() + cipher.getBlockSize());
final byte[] key = Arrays.copyOfRange(keyIvParameters, 0, blockSize);
final byte[] iv = Arrays.copyOfRange(keyIvParameters, blockSize, parameterSize);
cipher.init(Cipher.Mode.Decrypt, key, iv);
} else {
throw new IllegalStateException("No support for KDF '" + kdfName + "'.");
final String message = String.format("OpenSSH Private Key encryption KDF not supported [%s]", kdfName);
throw new IllegalStateException(message);
}
}
private Cipher createCipher(String cipherName) {
if (cipherName.equals(BlockCiphers.AES256CTR().getName())) {
return BlockCiphers.AES256CTR().create();
} else if (cipherName.equals(BlockCiphers.AES256CBC().getName())) {
return BlockCiphers.AES256CBC().create();
} else if (cipherName.equals(BlockCiphers.AES128CBC().getName())) {
return BlockCiphers.AES128CBC().create();
private Cipher createCipher(final String cipherName) {
final Cipher cipher;
if (SUPPORTED_CIPHERS.containsKey(cipherName)) {
final Factory.Named<Cipher> cipherFactory = SUPPORTED_CIPHERS.get(cipherName);
cipher = cipherFactory.create();
} else {
final String message = String.format("OpenSSH Key encryption cipher not supported [%s]", cipherName);
throw new IllegalStateException(message);
}
throw new IllegalStateException("Cipher '" + cipherName + "' not currently implemented for openssh-key-v1 format");
return cipher;
}
private PublicKey readPublicKey(final PlainBuffer plainBuffer) throws Buffer.BufferException, GeneralSecurityException {
return KeyType.fromString(plainBuffer.readString()).readPubKeyFromBuffer(plainBuffer);
}
private String readKeyFile(final BufferedReader reader) throws IOException {
StringBuilder sb = new StringBuilder();
private String readEncodedKey(final BufferedReader reader) throws IOException {
final StringBuilder builder = new StringBuilder();
boolean footerFound = false;
String line = reader.readLine();
while (!line.startsWith(END)) {
sb.append(line);
while (line != null) {
if (line.startsWith(END)) {
footerFound = true;
break;
}
builder.append(line);
line = reader.readLine();
}
return sb.toString();
if (footerFound) {
return builder.toString();
} else {
final String message = String.format("File footer not found [%s%s]", END, OPENSSH_PRIVATE_KEY);
throw new IOException(message);
}
}
private boolean checkHeader(final BufferedReader reader) throws IOException {
@@ -233,12 +343,12 @@ public class OpenSSHKeyV1KeyFile extends BaseFileKeyProvider {
int checkInt1 = keyBuffer.readUInt32AsInt(); // uint32 checkint1
int checkInt2 = keyBuffer.readUInt32AsInt(); // uint32 checkint2
if (checkInt1 != checkInt2) {
throw new KeyDecryptionFailedException();
throw new KeyDecryptionFailedException(new EncryptionException("OpenSSH Private Key integer comparison failed"));
}
// The private key section contains both the public key and the private key
String keyType = keyBuffer.readString(); // string keytype
KeyType kt = KeyType.fromString(keyType);
logger.info("Read key type: {}", keyType, kt);
KeyPair kp;
switch (kt) {
case ED25519:

View File

@@ -14,11 +14,9 @@
package com.hierynomus.sshj.userauth.keyprovider.bcrypt;
import java.io.UnsupportedEncodingException;
import java.security.DigestException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
/**
* BCrypt implements OpenBSD-style Blowfish password hashing using
@@ -30,32 +28,6 @@ import java.security.SecureRandom;
* based on Bruce Schneier's Blowfish cipher. The work factor of
* the algorithm is parameterised, so it can be increased as
* computers get faster.
* <p>
* Usage is really simple. To hash a password for the first time,
* call the hashpw method with a random salt, like this:
* <p>
* <code>
* String pw_hash = BCrypt.hashpw(plain_password, BCrypt.gensalt()); <br>
* </code>
* <p>
* To check whether a plaintext password matches one that has been
* hashed previously, use the checkpw method:
* <p>
* <code>
* if (BCrypt.checkpw(candidate_password, stored_hash))<br>
* &nbsp;&nbsp;&nbsp;&nbsp;System.out.println("It matches");<br>
* else<br>
* &nbsp;&nbsp;&nbsp;&nbsp;System.out.println("It does not match");<br>
* </code>
* <p>
* The gensalt() method takes an optional parameter (log_rounds)
* that determines the computational complexity of the hashing:
* <p>
* <code>
* String strong_salt = BCrypt.gensalt(10)<br>
* String stronger_salt = BCrypt.gensalt(12)<br>
* </code>
* <p>
* The amount of work increases exponentially (2**log_rounds), so
* each increment is twice as much work. The default log_rounds is
* 10, and the valid range is 4 to 30.
@@ -64,22 +36,18 @@ import java.security.SecureRandom;
* @version 0.2
*/
public class BCrypt {
// BCrypt parameters
private static final int GENSALT_DEFAULT_LOG2_ROUNDS = 10;
private static final int BCRYPT_SALT_LEN = 16;
// Blowfish parameters
private static final int BLOWFISH_NUM_ROUNDS = 16;
// Initial contents of key schedule
private static final int P_orig[] = {
private static final int[] P_orig = {
0x243f6a88, 0x85a308d3, 0x13198a2e, 0x03707344,
0xa4093822, 0x299f31d0, 0x082efa98, 0xec4e6c89,
0x452821e6, 0x38d01377, 0xbe5466cf, 0x34e90c6c,
0xc0ac29b7, 0xc97c50dd, 0x3f84d5b5, 0xb5470917,
0x9216d5d9, 0x8979fb1b
};
private static final int S_orig[] = {
private static final int[] S_orig = {
0xd1310ba6, 0x98dfb5ac, 0x2ffd72db, 0xd01adfb7,
0xb8e1afed, 0x6a267e96, 0xba7c9045, 0xf12c7f99,
0x24a19947, 0xb3916cf7, 0x0801f2e2, 0x858efc16,
@@ -344,149 +312,9 @@ public class BCrypt {
0x66697368, 0x53776174, 0x44796e61, 0x6d697465,
};
// bcrypt IV: "OrpheanBeholderScryDoubt". The C implementation calls
// this "ciphertext", but it is really plaintext or an IV. We keep
// the name to make code comparison easier.
static private final int bf_crypt_ciphertext[] = {
0x4f727068, 0x65616e42, 0x65686f6c,
0x64657253, 0x63727944, 0x6f756274
};
// Table for Base64 encoding
static private final char base64_code[] = {
'.', '/', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J',
'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V',
'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h',
'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5',
'6', '7', '8', '9'
};
// Table for Base64 decoding
static private final byte index_64[] = {
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, 0, 1, 54, 55,
56, 57, 58, 59, 60, 61, 62, 63, -1, -1,
-1, -1, -1, -1, -1, 2, 3, 4, 5, 6,
7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27,
-1, -1, -1, -1, -1, -1, 28, 29, 30,
31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
41, 42, 43, 44, 45, 46, 47, 48, 49, 50,
51, 52, 53, -1, -1, -1, -1, -1
};
// Expanded Blowfish key
private int P[];
private int S[];
/**
* Encode a byte array using bcrypt's slightly-modified base64
* encoding scheme. Note that this is *not* compatible with
* the standard MIME-base64 encoding.
*
* @param d the byte array to encode
* @param len the number of bytes to encode
* @return base64-encoded string
* @exception IllegalArgumentException if the length is invalid
*/
private static String encode_base64(byte d[], int len)
throws IllegalArgumentException {
int off = 0;
StringBuffer rs = new StringBuffer();
int c1, c2;
if (len <= 0 || len > d.length)
throw new IllegalArgumentException ("Invalid len");
while (off < len) {
c1 = d[off++] & 0xff;
rs.append(base64_code[(c1 >> 2) & 0x3f]);
c1 = (c1 & 0x03) << 4;
if (off >= len) {
rs.append(base64_code[c1 & 0x3f]);
break;
}
c2 = d[off++] & 0xff;
c1 |= (c2 >> 4) & 0x0f;
rs.append(base64_code[c1 & 0x3f]);
c1 = (c2 & 0x0f) << 2;
if (off >= len) {
rs.append(base64_code[c1 & 0x3f]);
break;
}
c2 = d[off++] & 0xff;
c1 |= (c2 >> 6) & 0x03;
rs.append(base64_code[c1 & 0x3f]);
rs.append(base64_code[c2 & 0x3f]);
}
return rs.toString();
}
/**
* Look up the 3 bits base64-encoded by the specified character,
* range-checking againt conversion table
* @param x the base64-encoded value
* @return the decoded value of x
*/
private static byte char64(char x) {
if ((int)x < 0 || (int)x > index_64.length)
return -1;
return index_64[(int)x];
}
/**
* Decode a string encoded using bcrypt's base64 scheme to a
* byte array. Note that this is *not* compatible with
* the standard MIME-base64 encoding.
* @param s the string to decode
* @param maxolen the maximum number of bytes to decode
* @return an array containing the decoded bytes
* @throws IllegalArgumentException if maxolen is invalid
*/
private static byte[] decode_base64(String s, int maxolen)
throws IllegalArgumentException {
StringBuffer rs = new StringBuffer();
int off = 0, slen = s.length(), olen = 0;
byte ret[];
byte c1, c2, c3, c4, o;
if (maxolen <= 0)
throw new IllegalArgumentException ("Invalid maxolen");
while (off < slen - 1 && olen < maxolen) {
c1 = char64(s.charAt(off++));
c2 = char64(s.charAt(off++));
if (c1 == -1 || c2 == -1)
break;
o = (byte)(c1 << 2);
o |= (c2 & 0x30) >> 4;
rs.append((char)o);
if (++olen >= maxolen || off >= slen)
break;
c3 = char64(s.charAt(off++));
if (c3 == -1)
break;
o = (byte)((c2 & 0x0f) << 4);
o |= (c3 & 0x3c) >> 2;
rs.append((char)o);
if (++olen >= maxolen || off >= slen)
break;
c4 = char64(s.charAt(off++));
o = (byte)((c3 & 0x03) << 6);
o |= c4;
rs.append((char)o);
++olen;
}
ret = new byte[olen];
for (off = 0; off < olen; off++)
ret[off] = (byte)rs.charAt(off);
return ret;
}
private int[] P;
private int[] S;
/**
* Blowfish encipher a single 64-bit block encoded as
@@ -494,7 +322,7 @@ public class BCrypt {
* @param lr an array containing the two 32-bit half blocks
* @param off the position in the array of the blocks
*/
private final void encipher(int lr[], int off) {
private void encipher(int[] lr, int off) {
int i, n, l = lr[off], r = lr[off + 1];
l ^= P[0];
@@ -524,7 +352,7 @@ public class BCrypt {
* current offset into data
* @return the next word of material from data
*/
private static int streamtoword(byte data[], int offp[]) {
private static int streamtoword(byte[] data, int[] offp) {
int i;
int word = 0;
int off = offp[0];
@@ -542,18 +370,18 @@ public class BCrypt {
* Initialise the Blowfish key schedule
*/
private void init_key() {
P = (int[])P_orig.clone();
S = (int[])S_orig.clone();
P = P_orig.clone();
S = S_orig.clone();
}
/**
* Key the Blowfish cipher
* @param key an array containing the key
*/
private void key(byte key[]) {
private void key(byte[] key) {
int i;
int koffp[] = { 0 };
int lr[] = { 0, 0 };
int[] koffp = { 0 };
int[] lr = { 0, 0 };
int plen = P.length, slen = S.length;
for (i = 0; i < plen; i++)
@@ -579,10 +407,10 @@ public class BCrypt {
* @param data salt information
* @param key password information
*/
private void ekskey(byte data[], byte key[]) {
private void ekskey(byte[] data, byte[] key) {
int i;
int koffp[] = { 0 }, doffp[] = { 0 };
int lr[] = { 0, 0 };
int[] koffp = { 0 }, doffp = { 0 };
int[] lr = { 0, 0 };
int plen = P.length, slen = S.length;
for (i = 0; i < plen; i++)
@@ -687,183 +515,4 @@ public class BCrypt {
throw new RuntimeException(e);
}
}
/**
* Perform the central password hashing step in the
* bcrypt scheme
* @param password the password to hash
* @param salt the binary salt to hash with the password
* @param log_rounds the binary logarithm of the number
* of rounds of hashing to apply
* @param cdata the plaintext to encrypt
* @return an array containing the binary hashed password
*/
public byte[] crypt_raw(byte password[], byte salt[], int log_rounds,
int cdata[]) {
int rounds, i, j;
int clen = cdata.length;
byte ret[];
if (log_rounds < 4 || log_rounds > 30)
throw new IllegalArgumentException ("Bad number of rounds");
rounds = 1 << log_rounds;
if (salt.length != BCRYPT_SALT_LEN)
throw new IllegalArgumentException ("Bad salt length");
init_key();
ekskey(salt, password);
for (i = 0; i != rounds; i++) {
key(password);
key(salt);
}
for (i = 0; i < 64; i++) {
for (j = 0; j < (clen >> 1); j++)
encipher(cdata, j << 1);
}
ret = new byte[clen * 4];
for (i = 0, j = 0; i < clen; i++) {
ret[j++] = (byte)((cdata[i] >> 24) & 0xff);
ret[j++] = (byte)((cdata[i] >> 16) & 0xff);
ret[j++] = (byte)((cdata[i] >> 8) & 0xff);
ret[j++] = (byte)(cdata[i] & 0xff);
}
return ret;
}
/**
* Hash a password using the OpenBSD bcrypt scheme
* @param password the password to hash
* @param salt the salt to hash with (perhaps generated
* using BCrypt.gensalt)
* @return the hashed password
*/
public static String hashpw(String password, String salt) {
BCrypt B;
String real_salt;
byte passwordb[], saltb[], hashed[];
char minor = (char)0;
int rounds, off = 0;
StringBuffer rs = new StringBuffer();
if (salt.charAt(0) != '$' || salt.charAt(1) != '2')
throw new IllegalArgumentException ("Invalid salt version");
if (salt.charAt(2) == '$')
off = 3;
else {
minor = salt.charAt(2);
if (minor != 'a' || salt.charAt(3) != '$')
throw new IllegalArgumentException ("Invalid salt revision");
off = 4;
}
// Extract number of rounds
if (salt.charAt(off + 2) > '$')
throw new IllegalArgumentException ("Missing salt rounds");
rounds = Integer.parseInt(salt.substring(off, off + 2));
real_salt = salt.substring(off + 3, off + 25);
try {
passwordb = (password + (minor >= 'a' ? "\000" : "")).getBytes("UTF-8");
} catch (UnsupportedEncodingException uee) {
throw new AssertionError("UTF-8 is not supported");
}
saltb = decode_base64(real_salt, BCRYPT_SALT_LEN);
B = new BCrypt();
hashed = B.crypt_raw(passwordb, saltb, rounds,
(int[])bf_crypt_ciphertext.clone());
rs.append("$2");
if (minor >= 'a')
rs.append(minor);
rs.append("$");
if (rounds < 10)
rs.append("0");
if (rounds > 30) {
throw new IllegalArgumentException(
"rounds exceeds maximum (30)");
}
rs.append(Integer.toString(rounds));
rs.append("$");
rs.append(encode_base64(saltb, saltb.length));
rs.append(encode_base64(hashed,
bf_crypt_ciphertext.length * 4 - 1));
return rs.toString();
}
/**
* Generate a salt for use with the BCrypt.hashpw() method
* @param log_rounds the log2 of the number of rounds of
* hashing to apply - the work factor therefore increases as
* 2**log_rounds.
* @param random an instance of SecureRandom to use
* @return an encoded salt value
*/
public static String gensalt(int log_rounds, SecureRandom random) {
StringBuffer rs = new StringBuffer();
byte rnd[] = new byte[BCRYPT_SALT_LEN];
random.nextBytes(rnd);
rs.append("$2a$");
if (log_rounds < 10)
rs.append("0");
if (log_rounds > 30) {
throw new IllegalArgumentException(
"log_rounds exceeds maximum (30)");
}
rs.append(Integer.toString(log_rounds));
rs.append("$");
rs.append(encode_base64(rnd, rnd.length));
return rs.toString();
}
/**
* Generate a salt for use with the BCrypt.hashpw() method
* @param log_rounds the log2 of the number of rounds of
* hashing to apply - the work factor therefore increases as
* 2**log_rounds.
* @return an encoded salt value
*/
public static String gensalt(int log_rounds) {
return gensalt(log_rounds, new SecureRandom());
}
/**
* Generate a salt for use with the BCrypt.hashpw() method,
* selecting a reasonable default for the number of hashing
* rounds to apply
* @return an encoded salt value
*/
public static String gensalt() {
return gensalt(GENSALT_DEFAULT_LOG2_ROUNDS);
}
/**
* Check that a plaintext password matches a previously hashed
* one
* @param plaintext the plaintext password to verify
* @param hashed the previously-hashed password
* @return true if the passwords match, false otherwise
*/
public static boolean checkpw(String plaintext, String hashed) {
byte hashed_bytes[];
byte try_bytes[];
try {
String try_pw = hashpw(plaintext, hashed);
hashed_bytes = hashed.getBytes("UTF-8");
try_bytes = try_pw.getBytes("UTF-8");
} catch (UnsupportedEncodingException uee) {
return false;
}
if (hashed_bytes.length != try_bytes.length)
return false;
byte ret = 0;
for (int i = 0; i < try_bytes.length; i++)
ret |= hashed_bytes[i] ^ try_bytes[i];
return ret == 0;
}
}

View File

@@ -1,918 +0,0 @@
/* Ported from C to Java by Dmitry Skiba [sahn0], 23/02/08.
* Original: http://cds.xs4all.nl:8081/ecdh/
*/
/* Generic 64-bit integer implementation of Curve25519 ECDH
* Written by Matthijs van Duin, 200608242056
* Public domain.
*
* Based on work by Daniel J Bernstein, http://cr.yp.to/ecdh.html
*/
package djb;
public class Curve25519 {
/* key size */
public static final int KEY_SIZE = 32;
/* 0 */
public static final byte[] ZERO = {
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
};
/* the prime 2^255-19 */
public static final byte[] PRIME = {
(byte)237, (byte)255, (byte)255, (byte)255,
(byte)255, (byte)255, (byte)255, (byte)255,
(byte)255, (byte)255, (byte)255, (byte)255,
(byte)255, (byte)255, (byte)255, (byte)255,
(byte)255, (byte)255, (byte)255, (byte)255,
(byte)255, (byte)255, (byte)255, (byte)255,
(byte)255, (byte)255, (byte)255, (byte)255,
(byte)255, (byte)255, (byte)255, (byte)127
};
/* group order (a prime near 2^252+2^124) */
public static final byte[] ORDER = {
(byte)237, (byte)211, (byte)245, (byte)92,
(byte)26, (byte)99, (byte)18, (byte)88,
(byte)214, (byte)156, (byte)247, (byte)162,
(byte)222, (byte)249, (byte)222, (byte)20,
(byte)0, (byte)0, (byte)0, (byte)0,
(byte)0, (byte)0, (byte)0, (byte)0,
(byte)0, (byte)0, (byte)0, (byte)0,
(byte)0, (byte)0, (byte)0, (byte)16
};
/********* KEY AGREEMENT *********/
/* Private key clamping
* k [out] your private key for key agreement
* k [in] 32 random bytes
*/
public static final void clamp(byte[] k) {
k[31] &= 0x7F;
k[31] |= 0x40;
k[ 0] &= 0xF8;
}
/* Key-pair generation
* P [out] your public key
* s [out] your private key for signing
* k [out] your private key for key agreement
* k [in] 32 random bytes
* s may be NULL if you don't care
*
* WARNING: if s is not NULL, this function has data-dependent timing */
public static final void keygen(byte[] P, byte[] s, byte[] k) {
clamp(k);
core(P, s, k, null);
}
/* Key agreement
* Z [out] shared secret (needs hashing before use)
* k [in] your private key for key agreement
* P [in] peer's public key
*/
public static final void curve(byte[] Z, byte[] k, byte[] P) {
core(Z, null, k, P);
}
/********* DIGITAL SIGNATURES *********/
/* deterministic EC-KCDSA
*
* s is the private key for signing
* P is the corresponding public key
* Z is the context data (signer public key or certificate, etc)
*
* signing:
*
* m = hash(Z, message)
* x = hash(m, s)
* keygen25519(Y, NULL, x);
* r = hash(Y);
* h = m XOR r
* sign25519(v, h, x, s);
*
* output (v,r) as the signature
*
* verification:
*
* m = hash(Z, message);
* h = m XOR r
* verify25519(Y, v, h, P)
*
* confirm r == hash(Y)
*
* It would seem to me that it would be simpler to have the signer directly do
* h = hash(m, Y) and send that to the recipient instead of r, who can verify
* the signature by checking h == hash(m, Y). If there are any problems with
* such a scheme, please let me know.
*
* Also, EC-KCDSA (like most DS algorithms) picks x random, which is a waste of
* perfectly good entropy, but does allow Y to be calculated in advance of (or
* parallel to) hashing the message.
*/
/* Signature generation primitive, calculates (x-h)s mod q
* v [out] signature value
* h [in] signature hash (of message, signature pub key, and context data)
* x [in] signature private key
* s [in] private key for signing
* returns true on success, false on failure (use different x or h)
*/
public static final boolean sign(byte[] v, byte[] h, byte[] x, byte[] s) {
// v = (x - h) s mod q
int w, i;
byte[] h1 = new byte[32], x1 = new byte[32];
byte[] tmp1 = new byte[64];
byte[] tmp2 = new byte[64];
// Don't clobber the arguments, be nice!
cpy32(h1, h);
cpy32(x1, x);
// Reduce modulo group order
byte[] tmp3=new byte[32];
divmod(tmp3, h1, 32, ORDER, 32);
divmod(tmp3, x1, 32, ORDER, 32);
// v = x1 - h1
// If v is negative, add the group order to it to become positive.
// If v was already positive we don't have to worry about overflow
// when adding the order because v < ORDER and 2*ORDER < 2^256
mula_small(v, x1, 0, h1, 32, -1);
mula_small(v, v , 0, ORDER, 32, 1);
// tmp1 = (x-h)*s mod q
mula32(tmp1, v, s, 32, 1);
divmod(tmp2, tmp1, 64, ORDER, 32);
for (w = 0, i = 0; i < 32; i++)
w |= v[i] = tmp1[i];
return w != 0;
}
/* Signature verification primitive, calculates Y = vP + hG
* Y [out] signature public key
* v [in] signature value
* h [in] signature hash
* P [in] public key
*/
public static final void verify(byte[] Y, byte[] v, byte[] h, byte[] P) {
/* Y = v abs(P) + h G */
byte[] d=new byte[32];
long10[]
p=new long10[]{new long10(),new long10()},
s=new long10[]{new long10(),new long10()},
yx=new long10[]{new long10(),new long10(),new long10()},
yz=new long10[]{new long10(),new long10(),new long10()},
t1=new long10[]{new long10(),new long10(),new long10()},
t2=new long10[]{new long10(),new long10(),new long10()};
int vi = 0, hi = 0, di = 0, nvh=0, i, j, k;
/* set p[0] to G and p[1] to P */
set(p[0], 9);
unpack(p[1], P);
/* set s[0] to P+G and s[1] to P-G */
/* s[0] = (Py^2 + Gy^2 - 2 Py Gy)/(Px - Gx)^2 - Px - Gx - 486662 */
/* s[1] = (Py^2 + Gy^2 + 2 Py Gy)/(Px - Gx)^2 - Px - Gx - 486662 */
x_to_y2(t1[0], t2[0], p[1]); /* t2[0] = Py^2 */
sqrt(t1[0], t2[0]); /* t1[0] = Py or -Py */
j = is_negative(t1[0]); /* ... check which */
t2[0]._0 += 39420360; /* t2[0] = Py^2 + Gy^2 */
mul(t2[1], BASE_2Y, t1[0]);/* t2[1] = 2 Py Gy or -2 Py Gy */
sub(t1[j], t2[0], t2[1]); /* t1[0] = Py^2 + Gy^2 - 2 Py Gy */
add(t1[1-j], t2[0], t2[1]);/* t1[1] = Py^2 + Gy^2 + 2 Py Gy */
cpy(t2[0], p[1]); /* t2[0] = Px */
t2[0]._0 -= 9; /* t2[0] = Px - Gx */
sqr(t2[1], t2[0]); /* t2[1] = (Px - Gx)^2 */
recip(t2[0], t2[1], 0); /* t2[0] = 1/(Px - Gx)^2 */
mul(s[0], t1[0], t2[0]); /* s[0] = t1[0]/(Px - Gx)^2 */
sub(s[0], s[0], p[1]); /* s[0] = t1[0]/(Px - Gx)^2 - Px */
s[0]._0 -= 9 + 486662; /* s[0] = X(P+G) */
mul(s[1], t1[1], t2[0]); /* s[1] = t1[1]/(Px - Gx)^2 */
sub(s[1], s[1], p[1]); /* s[1] = t1[1]/(Px - Gx)^2 - Px */
s[1]._0 -= 9 + 486662; /* s[1] = X(P-G) */
mul_small(s[0], s[0], 1); /* reduce s[0] */
mul_small(s[1], s[1], 1); /* reduce s[1] */
/* prepare the chain */
for (i = 0; i < 32; i++) {
vi = (vi >> 8) ^ (v[i] & 0xFF) ^ ((v[i] & 0xFF) << 1);
hi = (hi >> 8) ^ (h[i] & 0xFF) ^ ((h[i] & 0xFF) << 1);
nvh = ~(vi ^ hi);
di = (nvh & (di & 0x80) >> 7) ^ vi;
di ^= nvh & (di & 0x01) << 1;
di ^= nvh & (di & 0x02) << 1;
di ^= nvh & (di & 0x04) << 1;
di ^= nvh & (di & 0x08) << 1;
di ^= nvh & (di & 0x10) << 1;
di ^= nvh & (di & 0x20) << 1;
di ^= nvh & (di & 0x40) << 1;
d[i] = (byte)di;
}
di = ((nvh & (di & 0x80) << 1) ^ vi) >> 8;
/* initialize state */
set(yx[0], 1);
cpy(yx[1], p[di]);
cpy(yx[2], s[0]);
set(yz[0], 0);
set(yz[1], 1);
set(yz[2], 1);
/* y[0] is (even)P + (even)G
* y[1] is (even)P + (odd)G if current d-bit is 0
* y[1] is (odd)P + (even)G if current d-bit is 1
* y[2] is (odd)P + (odd)G
*/
vi = 0;
hi = 0;
/* and go for it! */
for (i = 32; i--!=0; ) {
vi = (vi << 8) | (v[i] & 0xFF);
hi = (hi << 8) | (h[i] & 0xFF);
di = (di << 8) | (d[i] & 0xFF);
for (j = 8; j--!=0; ) {
mont_prep(t1[0], t2[0], yx[0], yz[0]);
mont_prep(t1[1], t2[1], yx[1], yz[1]);
mont_prep(t1[2], t2[2], yx[2], yz[2]);
k = ((vi ^ vi >> 1) >> j & 1)
+ ((hi ^ hi >> 1) >> j & 1);
mont_dbl(yx[2], yz[2], t1[k], t2[k], yx[0], yz[0]);
k = (di >> j & 2) ^ ((di >> j & 1) << 1);
mont_add(t1[1], t2[1], t1[k], t2[k], yx[1], yz[1],
p[di >> j & 1]);
mont_add(t1[2], t2[2], t1[0], t2[0], yx[2], yz[2],
s[((vi ^ hi) >> j & 2) >> 1]);
}
}
k = (vi & 1) + (hi & 1);
recip(t1[0], yz[k], 0);
mul(t1[1], yx[k], t1[0]);
pack(t1[1], Y);
}
///////////////////////////////////////////////////////////////////////////
/* sahn0:
* Using this class instead of long[10] to avoid bounds checks. */
private static final class long10 {
public long10() {}
public long10(
long _0, long _1, long _2, long _3, long _4,
long _5, long _6, long _7, long _8, long _9)
{
this._0=_0; this._1=_1; this._2=_2;
this._3=_3; this._4=_4; this._5=_5;
this._6=_6; this._7=_7; this._8=_8;
this._9=_9;
}
public long _0,_1,_2,_3,_4,_5,_6,_7,_8,_9;
}
/********************* radix 2^8 math *********************/
private static final void cpy32(byte[] d, byte[] s) {
int i;
for (i = 0; i < 32; i++)
d[i] = s[i];
}
/* p[m..n+m-1] = q[m..n+m-1] + z * x */
/* n is the size of x */
/* n+m is the size of p and q */
private static final int mula_small(byte[] p,byte[] q,int m,byte[] x,int n,int z) {
int v=0;
for (int i=0;i<n;++i) {
v+=(q[i+m] & 0xFF)+z*(x[i] & 0xFF);
p[i+m]=(byte)v;
v>>=8;
}
return v;
}
/* p += x * y * z where z is a small integer
* x is size 32, y is size t, p is size 32+t
* y is allowed to overlap with p+32 if you don't care about the upper half */
private static final int mula32(byte[] p, byte[] x, byte[] y, int t, int z) {
final int n = 31;
int w = 0;
int i = 0;
for (; i < t; i++) {
int zy = z * (y[i] & 0xFF);
w += mula_small(p, p, i, x, n, zy) +
(p[i+n] & 0xFF) + zy * (x[n] & 0xFF);
p[i+n] = (byte)w;
w >>= 8;
}
p[i+n] = (byte)(w + (p[i+n] & 0xFF));
return w >> 8;
}
/* divide r (size n) by d (size t), returning quotient q and remainder r
* quotient is size n-t+1, remainder is size t
* requires t > 0 && d[t-1] != 0
* requires that r[-1] and d[-1] are valid memory locations
* q may overlap with r+t */
private static final void divmod(byte[] q, byte[] r, int n, byte[] d, int t) {
int rn = 0;
int dt = ((d[t-1] & 0xFF) << 8);
if (t>1) {
dt |= (d[t-2] & 0xFF);
}
while (n-- >= t) {
int z = (rn << 16) | ((r[n] & 0xFF) << 8);
if (n>0) {
z |= (r[n-1] & 0xFF);
}
z/=dt;
rn += mula_small(r,r, n-t+1, d, t, -z);
q[n-t+1] = (byte)((z + rn) & 0xFF); /* rn is 0 or -1 (underflow) */
mula_small(r,r, n-t+1, d, t, -rn);
rn = (r[n] & 0xFF);
r[n] = 0;
}
r[t-1] = (byte)rn;
}
private static final int numsize(byte[] x,int n) {
while (n--!=0 && x[n]==0)
;
return n+1;
}
/* Returns x if a contains the gcd, y if b.
* Also, the returned buffer contains the inverse of a mod b,
* as 32-byte signed.
* x and y must have 64 bytes space for temporary use.
* requires that a[-1] and b[-1] are valid memory locations */
private static final byte[] egcd32(byte[] x,byte[] y,byte[] a,byte[] b) {
int an, bn = 32, qn, i;
for (i = 0; i < 32; i++)
x[i] = y[i] = 0;
x[0] = 1;
an = numsize(a, 32);
if (an==0)
return y; /* division by zero */
byte[] temp=new byte[32];
while (true) {
qn = bn - an + 1;
divmod(temp, b, bn, a, an);
bn = numsize(b, bn);
if (bn==0)
return x;
mula32(y, x, temp, qn, -1);
qn = an - bn + 1;
divmod(temp, a, an, b, bn);
an = numsize(a, an);
if (an==0)
return y;
mula32(x, y, temp, qn, -1);
}
}
/********************* radix 2^25.5 GF(2^255-19) math *********************/
private static final int P25=33554431; /* (1 << 25) - 1 */
private static final int P26=67108863; /* (1 << 26) - 1 */
/* Convert to internal format from little-endian byte format */
private static final void unpack(long10 x,byte[] m) {
x._0 = ((m[0] & 0xFF)) | ((m[1] & 0xFF))<<8 |
(m[2] & 0xFF)<<16 | ((m[3] & 0xFF)& 3)<<24;
x._1 = ((m[3] & 0xFF)&~ 3)>>2 | (m[4] & 0xFF)<<6 |
(m[5] & 0xFF)<<14 | ((m[6] & 0xFF)& 7)<<22;
x._2 = ((m[6] & 0xFF)&~ 7)>>3 | (m[7] & 0xFF)<<5 |
(m[8] & 0xFF)<<13 | ((m[9] & 0xFF)&31)<<21;
x._3 = ((m[9] & 0xFF)&~31)>>5 | (m[10] & 0xFF)<<3 |
(m[11] & 0xFF)<<11 | ((m[12] & 0xFF)&63)<<19;
x._4 = ((m[12] & 0xFF)&~63)>>6 | (m[13] & 0xFF)<<2 |
(m[14] & 0xFF)<<10 | (m[15] & 0xFF) <<18;
x._5 = (m[16] & 0xFF) | (m[17] & 0xFF)<<8 |
(m[18] & 0xFF)<<16 | ((m[19] & 0xFF)& 1)<<24;
x._6 = ((m[19] & 0xFF)&~ 1)>>1 | (m[20] & 0xFF)<<7 |
(m[21] & 0xFF)<<15 | ((m[22] & 0xFF)& 7)<<23;
x._7 = ((m[22] & 0xFF)&~ 7)>>3 | (m[23] & 0xFF)<<5 |
(m[24] & 0xFF)<<13 | ((m[25] & 0xFF)&15)<<21;
x._8 = ((m[25] & 0xFF)&~15)>>4 | (m[26] & 0xFF)<<4 |
(m[27] & 0xFF)<<12 | ((m[28] & 0xFF)&63)<<20;
x._9 = ((m[28] & 0xFF)&~63)>>6 | (m[29] & 0xFF)<<2 |
(m[30] & 0xFF)<<10 | (m[31] & 0xFF) <<18;
}
/* Check if reduced-form input >= 2^255-19 */
private static final boolean is_overflow(long10 x) {
return (
((x._0 > P26-19)) &&
((x._1 & x._3 & x._5 & x._7 & x._9) == P25) &&
((x._2 & x._4 & x._6 & x._8) == P26)
) || (x._9 > P25);
}
/* Convert from internal format to little-endian byte format. The
* number must be in a reduced form which is output by the following ops:
* unpack, mul, sqr
* set -- if input in range 0 .. P25
* If you're unsure if the number is reduced, first multiply it by 1. */
private static final void pack(long10 x,byte[] m) {
int ld = 0, ud = 0;
long t;
ld = (is_overflow(x)?1:0) - ((x._9 < 0)?1:0);
ud = ld * -(P25+1);
ld *= 19;
t = ld + x._0 + (x._1 << 26);
m[ 0] = (byte)t;
m[ 1] = (byte)(t >> 8);
m[ 2] = (byte)(t >> 16);
m[ 3] = (byte)(t >> 24);
t = (t >> 32) + (x._2 << 19);
m[ 4] = (byte)t;
m[ 5] = (byte)(t >> 8);
m[ 6] = (byte)(t >> 16);
m[ 7] = (byte)(t >> 24);
t = (t >> 32) + (x._3 << 13);
m[ 8] = (byte)t;
m[ 9] = (byte)(t >> 8);
m[10] = (byte)(t >> 16);
m[11] = (byte)(t >> 24);
t = (t >> 32) + (x._4 << 6);
m[12] = (byte)t;
m[13] = (byte)(t >> 8);
m[14] = (byte)(t >> 16);
m[15] = (byte)(t >> 24);
t = (t >> 32) + x._5 + (x._6 << 25);
m[16] = (byte)t;
m[17] = (byte)(t >> 8);
m[18] = (byte)(t >> 16);
m[19] = (byte)(t >> 24);
t = (t >> 32) + (x._7 << 19);
m[20] = (byte)t;
m[21] = (byte)(t >> 8);
m[22] = (byte)(t >> 16);
m[23] = (byte)(t >> 24);
t = (t >> 32) + (x._8 << 12);
m[24] = (byte)t;
m[25] = (byte)(t >> 8);
m[26] = (byte)(t >> 16);
m[27] = (byte)(t >> 24);
t = (t >> 32) + ((x._9 + ud) << 6);
m[28] = (byte)t;
m[29] = (byte)(t >> 8);
m[30] = (byte)(t >> 16);
m[31] = (byte)(t >> 24);
}
/* Copy a number */
private static final void cpy(long10 out, long10 in) {
out._0=in._0; out._1=in._1;
out._2=in._2; out._3=in._3;
out._4=in._4; out._5=in._5;
out._6=in._6; out._7=in._7;
out._8=in._8; out._9=in._9;
}
/* Set a number to value, which must be in range -185861411 .. 185861411 */
private static final void set(long10 out, int in) {
out._0=in; out._1=0;
out._2=0; out._3=0;
out._4=0; out._5=0;
out._6=0; out._7=0;
out._8=0; out._9=0;
}
/* Add/subtract two numbers. The inputs must be in reduced form, and the
* output isn't, so to do another addition or subtraction on the output,
* first multiply it by one to reduce it. */
private static final void add(long10 xy, long10 x, long10 y) {
xy._0 = x._0 + y._0; xy._1 = x._1 + y._1;
xy._2 = x._2 + y._2; xy._3 = x._3 + y._3;
xy._4 = x._4 + y._4; xy._5 = x._5 + y._5;
xy._6 = x._6 + y._6; xy._7 = x._7 + y._7;
xy._8 = x._8 + y._8; xy._9 = x._9 + y._9;
}
private static final void sub(long10 xy, long10 x, long10 y) {
xy._0 = x._0 - y._0; xy._1 = x._1 - y._1;
xy._2 = x._2 - y._2; xy._3 = x._3 - y._3;
xy._4 = x._4 - y._4; xy._5 = x._5 - y._5;
xy._6 = x._6 - y._6; xy._7 = x._7 - y._7;
xy._8 = x._8 - y._8; xy._9 = x._9 - y._9;
}
/* Multiply a number by a small integer in range -185861411 .. 185861411.
* The output is in reduced form, the input x need not be. x and xy may point
* to the same buffer. */
private static final long10 mul_small(long10 xy, long10 x, long y) {
long t;
t = (x._8*y);
xy._8 = (t & ((1 << 26) - 1));
t = (t >> 26) + (x._9*y);
xy._9 = (t & ((1 << 25) - 1));
t = 19 * (t >> 25) + (x._0*y);
xy._0 = (t & ((1 << 26) - 1));
t = (t >> 26) + (x._1*y);
xy._1 = (t & ((1 << 25) - 1));
t = (t >> 25) + (x._2*y);
xy._2 = (t & ((1 << 26) - 1));
t = (t >> 26) + (x._3*y);
xy._3 = (t & ((1 << 25) - 1));
t = (t >> 25) + (x._4*y);
xy._4 = (t & ((1 << 26) - 1));
t = (t >> 26) + (x._5*y);
xy._5 = (t & ((1 << 25) - 1));
t = (t >> 25) + (x._6*y);
xy._6 = (t & ((1 << 26) - 1));
t = (t >> 26) + (x._7*y);
xy._7 = (t & ((1 << 25) - 1));
t = (t >> 25) + xy._8;
xy._8 = (t & ((1 << 26) - 1));
xy._9 += (t >> 26);
return xy;
}
/* Multiply two numbers. The output is in reduced form, the inputs need not
* be. */
private static final long10 mul(long10 xy, long10 x, long10 y) {
/* sahn0:
* Using local variables to avoid class access.
* This seem to improve performance a bit...
*/
long
x_0=x._0,x_1=x._1,x_2=x._2,x_3=x._3,x_4=x._4,
x_5=x._5,x_6=x._6,x_7=x._7,x_8=x._8,x_9=x._9;
long
y_0=y._0,y_1=y._1,y_2=y._2,y_3=y._3,y_4=y._4,
y_5=y._5,y_6=y._6,y_7=y._7,y_8=y._8,y_9=y._9;
long t;
t = (x_0*y_8) + (x_2*y_6) + (x_4*y_4) + (x_6*y_2) +
(x_8*y_0) + 2 * ((x_1*y_7) + (x_3*y_5) +
(x_5*y_3) + (x_7*y_1)) + 38 *
(x_9*y_9);
xy._8 = (t & ((1 << 26) - 1));
t = (t >> 26) + (x_0*y_9) + (x_1*y_8) + (x_2*y_7) +
(x_3*y_6) + (x_4*y_5) + (x_5*y_4) +
(x_6*y_3) + (x_7*y_2) + (x_8*y_1) +
(x_9*y_0);
xy._9 = (t & ((1 << 25) - 1));
t = (x_0*y_0) + 19 * ((t >> 25) + (x_2*y_8) + (x_4*y_6)
+ (x_6*y_4) + (x_8*y_2)) + 38 *
((x_1*y_9) + (x_3*y_7) + (x_5*y_5) +
(x_7*y_3) + (x_9*y_1));
xy._0 = (t & ((1 << 26) - 1));
t = (t >> 26) + (x_0*y_1) + (x_1*y_0) + 19 * ((x_2*y_9)
+ (x_3*y_8) + (x_4*y_7) + (x_5*y_6) +
(x_6*y_5) + (x_7*y_4) + (x_8*y_3) +
(x_9*y_2));
xy._1 = (t & ((1 << 25) - 1));
t = (t >> 25) + (x_0*y_2) + (x_2*y_0) + 19 * ((x_4*y_8)
+ (x_6*y_6) + (x_8*y_4)) + 2 * (x_1*y_1)
+ 38 * ((x_3*y_9) + (x_5*y_7) +
(x_7*y_5) + (x_9*y_3));
xy._2 = (t & ((1 << 26) - 1));
t = (t >> 26) + (x_0*y_3) + (x_1*y_2) + (x_2*y_1) +
(x_3*y_0) + 19 * ((x_4*y_9) + (x_5*y_8) +
(x_6*y_7) + (x_7*y_6) +
(x_8*y_5) + (x_9*y_4));
xy._3 = (t & ((1 << 25) - 1));
t = (t >> 25) + (x_0*y_4) + (x_2*y_2) + (x_4*y_0) + 19 *
((x_6*y_8) + (x_8*y_6)) + 2 * ((x_1*y_3) +
(x_3*y_1)) + 38 *
((x_5*y_9) + (x_7*y_7) + (x_9*y_5));
xy._4 = (t & ((1 << 26) - 1));
t = (t >> 26) + (x_0*y_5) + (x_1*y_4) + (x_2*y_3) +
(x_3*y_2) + (x_4*y_1) + (x_5*y_0) + 19 *
((x_6*y_9) + (x_7*y_8) + (x_8*y_7) +
(x_9*y_6));
xy._5 = (t & ((1 << 25) - 1));
t = (t >> 25) + (x_0*y_6) + (x_2*y_4) + (x_4*y_2) +
(x_6*y_0) + 19 * (x_8*y_8) + 2 * ((x_1*y_5) +
(x_3*y_3) + (x_5*y_1)) + 38 *
((x_7*y_9) + (x_9*y_7));
xy._6 = (t & ((1 << 26) - 1));
t = (t >> 26) + (x_0*y_7) + (x_1*y_6) + (x_2*y_5) +
(x_3*y_4) + (x_4*y_3) + (x_5*y_2) +
(x_6*y_1) + (x_7*y_0) + 19 * ((x_8*y_9) +
(x_9*y_8));
xy._7 = (t & ((1 << 25) - 1));
t = (t >> 25) + xy._8;
xy._8 = (t & ((1 << 26) - 1));
xy._9 += (t >> 26);
return xy;
}
/* Square a number. Optimization of mul25519(x2, x, x) */
private static final long10 sqr(long10 x2, long10 x) {
long
x_0=x._0,x_1=x._1,x_2=x._2,x_3=x._3,x_4=x._4,
x_5=x._5,x_6=x._6,x_7=x._7,x_8=x._8,x_9=x._9;
long t;
t = (x_4*x_4) + 2 * ((x_0*x_8) + (x_2*x_6)) + 38 *
(x_9*x_9) + 4 * ((x_1*x_7) + (x_3*x_5));
x2._8 = (t & ((1 << 26) - 1));
t = (t >> 26) + 2 * ((x_0*x_9) + (x_1*x_8) + (x_2*x_7) +
(x_3*x_6) + (x_4*x_5));
x2._9 = (t & ((1 << 25) - 1));
t = 19 * (t >> 25) + (x_0*x_0) + 38 * ((x_2*x_8) +
(x_4*x_6) + (x_5*x_5)) + 76 * ((x_1*x_9)
+ (x_3*x_7));
x2._0 = (t & ((1 << 26) - 1));
t = (t >> 26) + 2 * (x_0*x_1) + 38 * ((x_2*x_9) +
(x_3*x_8) + (x_4*x_7) + (x_5*x_6));
x2._1 = (t & ((1 << 25) - 1));
t = (t >> 25) + 19 * (x_6*x_6) + 2 * ((x_0*x_2) +
(x_1*x_1)) + 38 * (x_4*x_8) + 76 *
((x_3*x_9) + (x_5*x_7));
x2._2 = (t & ((1 << 26) - 1));
t = (t >> 26) + 2 * ((x_0*x_3) + (x_1*x_2)) + 38 *
((x_4*x_9) + (x_5*x_8) + (x_6*x_7));
x2._3 = (t & ((1 << 25) - 1));
t = (t >> 25) + (x_2*x_2) + 2 * (x_0*x_4) + 38 *
((x_6*x_8) + (x_7*x_7)) + 4 * (x_1*x_3) + 76 *
(x_5*x_9);
x2._4 = (t & ((1 << 26) - 1));
t = (t >> 26) + 2 * ((x_0*x_5) + (x_1*x_4) + (x_2*x_3))
+ 38 * ((x_6*x_9) + (x_7*x_8));
x2._5 = (t & ((1 << 25) - 1));
t = (t >> 25) + 19 * (x_8*x_8) + 2 * ((x_0*x_6) +
(x_2*x_4) + (x_3*x_3)) + 4 * (x_1*x_5) +
76 * (x_7*x_9);
x2._6 = (t & ((1 << 26) - 1));
t = (t >> 26) + 2 * ((x_0*x_7) + (x_1*x_6) + (x_2*x_5) +
(x_3*x_4)) + 38 * (x_8*x_9);
x2._7 = (t & ((1 << 25) - 1));
t = (t >> 25) + x2._8;
x2._8 = (t & ((1 << 26) - 1));
x2._9 += (t >> 26);
return x2;
}
/* Calculates a reciprocal. The output is in reduced form, the inputs need not
* be. Simply calculates y = x^(p-2) so it's not too fast. */
/* When sqrtassist is true, it instead calculates y = x^((p-5)/8) */
private static final void recip(long10 y, long10 x, int sqrtassist) {
long10
t0=new long10(),
t1=new long10(),
t2=new long10(),
t3=new long10(),
t4=new long10();
int i;
/* the chain for x^(2^255-21) is straight from djb's implementation */
sqr(t1, x); /* 2 == 2 * 1 */
sqr(t2, t1); /* 4 == 2 * 2 */
sqr(t0, t2); /* 8 == 2 * 4 */
mul(t2, t0, x); /* 9 == 8 + 1 */
mul(t0, t2, t1); /* 11 == 9 + 2 */
sqr(t1, t0); /* 22 == 2 * 11 */
mul(t3, t1, t2); /* 31 == 22 + 9
== 2^5 - 2^0 */
sqr(t1, t3); /* 2^6 - 2^1 */
sqr(t2, t1); /* 2^7 - 2^2 */
sqr(t1, t2); /* 2^8 - 2^3 */
sqr(t2, t1); /* 2^9 - 2^4 */
sqr(t1, t2); /* 2^10 - 2^5 */
mul(t2, t1, t3); /* 2^10 - 2^0 */
sqr(t1, t2); /* 2^11 - 2^1 */
sqr(t3, t1); /* 2^12 - 2^2 */
for (i = 1; i < 5; i++) {
sqr(t1, t3);
sqr(t3, t1);
} /* t3 */ /* 2^20 - 2^10 */
mul(t1, t3, t2); /* 2^20 - 2^0 */
sqr(t3, t1); /* 2^21 - 2^1 */
sqr(t4, t3); /* 2^22 - 2^2 */
for (i = 1; i < 10; i++) {
sqr(t3, t4);
sqr(t4, t3);
} /* t4 */ /* 2^40 - 2^20 */
mul(t3, t4, t1); /* 2^40 - 2^0 */
for (i = 0; i < 5; i++) {
sqr(t1, t3);
sqr(t3, t1);
} /* t3 */ /* 2^50 - 2^10 */
mul(t1, t3, t2); /* 2^50 - 2^0 */
sqr(t2, t1); /* 2^51 - 2^1 */
sqr(t3, t2); /* 2^52 - 2^2 */
for (i = 1; i < 25; i++) {
sqr(t2, t3);
sqr(t3, t2);
} /* t3 */ /* 2^100 - 2^50 */
mul(t2, t3, t1); /* 2^100 - 2^0 */
sqr(t3, t2); /* 2^101 - 2^1 */
sqr(t4, t3); /* 2^102 - 2^2 */
for (i = 1; i < 50; i++) {
sqr(t3, t4);
sqr(t4, t3);
} /* t4 */ /* 2^200 - 2^100 */
mul(t3, t4, t2); /* 2^200 - 2^0 */
for (i = 0; i < 25; i++) {
sqr(t4, t3);
sqr(t3, t4);
} /* t3 */ /* 2^250 - 2^50 */
mul(t2, t3, t1); /* 2^250 - 2^0 */
sqr(t1, t2); /* 2^251 - 2^1 */
sqr(t2, t1); /* 2^252 - 2^2 */
if (sqrtassist!=0) {
mul(y, x, t2); /* 2^252 - 3 */
} else {
sqr(t1, t2); /* 2^253 - 2^3 */
sqr(t2, t1); /* 2^254 - 2^4 */
sqr(t1, t2); /* 2^255 - 2^5 */
mul(y, t1, t0); /* 2^255 - 21 */
}
}
/* checks if x is "negative", requires reduced input */
private static final int is_negative(long10 x) {
return (int)(((is_overflow(x) || (x._9 < 0))?1:0) ^ (x._0 & 1));
}
/* a square root */
private static final void sqrt(long10 x, long10 u) {
long10 v=new long10(), t1=new long10(), t2=new long10();
add(t1, u, u); /* t1 = 2u */
recip(v, t1, 1); /* v = (2u)^((p-5)/8) */
sqr(x, v); /* x = v^2 */
mul(t2, t1, x); /* t2 = 2uv^2 */
t2._0--; /* t2 = 2uv^2-1 */
mul(t1, v, t2); /* t1 = v(2uv^2-1) */
mul(x, u, t1); /* x = uv(2uv^2-1) */
}
/********************* Elliptic curve *********************/
/* y^2 = x^3 + 486662 x^2 + x over GF(2^255-19) */
/* t1 = ax + az
* t2 = ax - az */
private static final void mont_prep(long10 t1, long10 t2, long10 ax, long10 az) {
add(t1, ax, az);
sub(t2, ax, az);
}
/* A = P + Q where
* X(A) = ax/az
* X(P) = (t1+t2)/(t1-t2)
* X(Q) = (t3+t4)/(t3-t4)
* X(P-Q) = dx
* clobbers t1 and t2, preserves t3 and t4 */
private static final void mont_add(long10 t1, long10 t2, long10 t3, long10 t4,long10 ax, long10 az, long10 dx) {
mul(ax, t2, t3);
mul(az, t1, t4);
add(t1, ax, az);
sub(t2, ax, az);
sqr(ax, t1);
sqr(t1, t2);
mul(az, t1, dx);
}
/* B = 2 * Q where
* X(B) = bx/bz
* X(Q) = (t3+t4)/(t3-t4)
* clobbers t1 and t2, preserves t3 and t4 */
private static final void mont_dbl(long10 t1, long10 t2, long10 t3, long10 t4,long10 bx, long10 bz) {
sqr(t1, t3);
sqr(t2, t4);
mul(bx, t1, t2);
sub(t2, t1, t2);
mul_small(bz, t2, 121665);
add(t1, t1, bz);
mul(bz, t1, t2);
}
/* Y^2 = X^3 + 486662 X^2 + X
* t is a temporary */
private static final void x_to_y2(long10 t, long10 y2, long10 x) {
sqr(t, x);
mul_small(y2, x, 486662);
add(t, t, y2);
t._0++;
mul(y2, t, x);
}
/* P = kG and s = sign(P)/k */
private static final void core(byte[] Px, byte[] s, byte[] k, byte[] Gx) {
long10
dx=new long10(),
t1=new long10(),
t2=new long10(),
t3=new long10(),
t4=new long10();
long10[]
x=new long10[]{new long10(),new long10()},
z=new long10[]{new long10(),new long10()};
int i, j;
/* unpack the base */
if (Gx!=null)
unpack(dx, Gx);
else
set(dx, 9);
/* 0G = point-at-infinity */
set(x[0], 1);
set(z[0], 0);
/* 1G = G */
cpy(x[1], dx);
set(z[1], 1);
for (i = 32; i--!=0; ) {
if (i==0) {
i=0;
}
for (j = 8; j--!=0; ) {
/* swap arguments depending on bit */
int bit1 = (k[i] & 0xFF) >> j & 1;
int bit0 = ~(k[i] & 0xFF) >> j & 1;
long10 ax = x[bit0];
long10 az = z[bit0];
long10 bx = x[bit1];
long10 bz = z[bit1];
/* a' = a + b */
/* b' = 2 b */
mont_prep(t1, t2, ax, az);
mont_prep(t3, t4, bx, bz);
mont_add(t1, t2, t3, t4, ax, az, dx);
mont_dbl(t1, t2, t3, t4, bx, bz);
}
}
recip(t1, z[0], 0);
mul(dx, x[0], t1);
pack(dx, Px);
/* calculate s such that s abs(P) = G .. assumes G is std base point */
if (s!=null) {
x_to_y2(t2, t1, dx); /* t1 = Py^2 */
recip(t3, z[1], 0); /* where Q=P+G ... */
mul(t2, x[1], t3); /* t2 = Qx */
add(t2, t2, dx); /* t2 = Qx + Px */
t2._0 += 9 + 486662; /* t2 = Qx + Px + Gx + 486662 */
dx._0 -= 9; /* dx = Px - Gx */
sqr(t3, dx); /* t3 = (Px - Gx)^2 */
mul(dx, t2, t3); /* dx = t2 (Px - Gx)^2 */
sub(dx, dx, t1); /* dx = t2 (Px - Gx)^2 - Py^2 */
dx._0 -= 39420360; /* dx = t2 (Px - Gx)^2 - Py^2 - Gy^2 */
mul(t1, dx, BASE_R2Y); /* t1 = -Py */
if (is_negative(t1)!=0) /* sign is 1, so just copy */
cpy32(s, k);
else /* sign is -1, so negate */
mula_small(s, ORDER_TIMES_8, 0, k, 32, -1);
/* reduce s mod q
* (is this needed? do it just in case, it's fast anyway) */
//divmod((dstptr) t1, s, 32, order25519, 32);
/* take reciprocal of s mod q */
byte[] temp1=new byte[32];
byte[] temp2=new byte[64];
byte[] temp3=new byte[64];
cpy32(temp1, ORDER);
cpy32(s, egcd32(temp2, temp3, s, temp1));
if ((s[31] & 0x80)!=0)
mula_small(s, s, 0, ORDER, 32, 1);
}
}
/* smallest multiple of the order that's >= 2^255 */
private static final byte[] ORDER_TIMES_8 = {
(byte)104, (byte)159, (byte)174, (byte)231,
(byte)210, (byte)24, (byte)147, (byte)192,
(byte)178, (byte)230, (byte)188, (byte)23,
(byte)245, (byte)206, (byte)247, (byte)166,
(byte)0, (byte)0, (byte)0, (byte)0,
(byte)0, (byte)0, (byte)0, (byte)0,
(byte)0, (byte)0, (byte)0, (byte)0,
(byte)0, (byte)0, (byte)0, (byte)128
};
/* constants 2Gy and 1/(2Gy) */
private static final long10 BASE_2Y = new long10(
39999547, 18689728, 59995525, 1648697, 57546132,
24010086, 19059592, 5425144, 63499247, 16420658
);
private static final long10 BASE_R2Y = new long10(
5744, 8160848, 4790893, 13779497, 35730846,
12541209, 49101323, 30047407, 40071253, 6226132
);
}

View File

@@ -104,7 +104,8 @@ public class Promise<V, T extends Throwable> {
lock.lock();
try {
pendingEx = null;
deliver(null);
log.debug("Clearing <<{}>>", name);
val = null;
} finally {
lock.unlock();
}
@@ -136,7 +137,7 @@ public class Promise<V, T extends Throwable> {
throws T {
final V value = tryRetrieve(timeout, unit);
if (value == null)
throw chainer.chain(new TimeoutException("Timeout expired"));
throw chainer.chain(new TimeoutException("Timeout expired: " + timeout + " " + unit));
else
return value;
}

View File

@@ -19,8 +19,6 @@ import com.hierynomus.sshj.key.KeyAlgorithm;
import com.hierynomus.sshj.key.KeyAlgorithms;
import net.schmizz.sshj.common.Factory;
import net.schmizz.sshj.common.SecurityUtils;
import net.schmizz.sshj.transport.random.JCERandom;
import net.schmizz.sshj.transport.random.SingletonRandomFactory;
import java.util.Arrays;
@@ -34,7 +32,6 @@ public class AndroidConfig
SecurityUtils.registerSecurityProvider("org.spongycastle.jce.provider.BouncyCastleProvider");
}
@Override
protected void initKeyAlgorithms() {
setKeyAlgorithms(Arrays.<Factory.Named<KeyAlgorithm>>asList(
@@ -43,10 +40,4 @@ public class AndroidConfig
KeyAlgorithms.SSHDSA()
));
}
@Override
protected void initRandomFactory(boolean ignored) {
setRandomFactory(new SingletonRandomFactory(new JCERandom.Factory()));
}
}

View File

@@ -200,4 +200,8 @@ public interface Config {
* See {@link #isVerifyHostKeyCertificates()}.
*/
void setVerifyHostKeyCertificates(boolean value);
int getMaxCircularBufferSize();
void setMaxCircularBufferSize(int maxCircularBufferSize);
}

View File

@@ -49,6 +49,8 @@ public class ConfigImpl
private boolean waitForServerIdentBeforeSendingClientIdent = false;
private LoggerFactory loggerFactory;
private boolean verifyHostKeyCertificates = true;
// HF-982: default to 16MB buffers.
private int maxCircularBufferSize = 16 * 1024 * 1024;
@Override
public List<Factory.Named<Cipher>> getCipherFactories() {
@@ -175,6 +177,16 @@ public class ConfigImpl
return loggerFactory;
}
@Override
public int getMaxCircularBufferSize() {
return maxCircularBufferSize;
}
@Override
public void setMaxCircularBufferSize(int maxCircularBufferSize) {
this.maxCircularBufferSize = maxCircularBufferSize;
}
@Override
public void setLoggerFactory(LoggerFactory loggerFactory) {
this.loggerFactory = loggerFactory;

View File

@@ -29,7 +29,6 @@ import com.hierynomus.sshj.userauth.keyprovider.OpenSSHKeyV1KeyFile;
import net.schmizz.keepalive.KeepAliveProvider;
import net.schmizz.sshj.common.Factory;
import net.schmizz.sshj.common.LoggerFactory;
import net.schmizz.sshj.common.SecurityUtils;
import net.schmizz.sshj.transport.cipher.Cipher;
import net.schmizz.sshj.transport.compression.NoneCompression;
import net.schmizz.sshj.transport.kex.Curve25519SHA256;
@@ -43,7 +42,11 @@ import net.schmizz.sshj.userauth.keyprovider.PKCS8KeyFile;
import net.schmizz.sshj.userauth.keyprovider.PuTTYKeyFile;
import org.slf4j.Logger;
import java.util.*;
import java.util.Arrays;
import java.util.List;
import java.util.LinkedList;
import java.util.ListIterator;
import java.util.Properties;
/**
* A {@link net.schmizz.sshj.Config} that is initialized as follows. Items marked with an asterisk are added to the config only if
@@ -62,8 +65,6 @@ import java.util.*;
* <li>{@link net.schmizz.sshj.ConfigImpl#setVersion Client version}: {@code "NET_3_0"}</li>
* </ul>
* <p/>
* [1] It is worth noting that Sun's JRE does not have the unlimited cryptography extension enabled by default. This
* prevents using ciphers with strength greater than 128.
*/
public class DefaultConfig
extends ConfigImpl {
@@ -73,11 +74,10 @@ public class DefaultConfig
public DefaultConfig() {
setLoggerFactory(LoggerFactory.DEFAULT);
setVersion(readVersionFromProperties());
final boolean bouncyCastleRegistered = SecurityUtils.isBouncyCastleRegistered();
initKeyExchangeFactories(bouncyCastleRegistered);
initKeyExchangeFactories();
initKeyAlgorithms();
initRandomFactory(bouncyCastleRegistered);
initFileKeyProviderFactories(bouncyCastleRegistered);
initRandomFactory();
initFileKeyProviderFactories();
initCipherFactories();
initCompressionFactories();
initMACFactories();
@@ -102,35 +102,32 @@ public class DefaultConfig
log = loggerFactory.getLogger(getClass());
}
protected void initKeyExchangeFactories(boolean bouncyCastleRegistered) {
if (bouncyCastleRegistered) {
setKeyExchangeFactories(
new Curve25519SHA256.Factory(),
new Curve25519SHA256.FactoryLibSsh(),
new DHGexSHA256.Factory(),
new ECDHNistP.Factory521(),
new ECDHNistP.Factory384(),
new ECDHNistP.Factory256(),
new DHGexSHA1.Factory(),
DHGroups.Group1SHA1(),
DHGroups.Group14SHA1(),
DHGroups.Group14SHA256(),
DHGroups.Group15SHA512(),
DHGroups.Group16SHA512(),
DHGroups.Group17SHA512(),
DHGroups.Group18SHA512(),
ExtendedDHGroups.Group14SHA256AtSSH(),
ExtendedDHGroups.Group15SHA256(),
ExtendedDHGroups.Group15SHA256AtSSH(),
ExtendedDHGroups.Group15SHA384AtSSH(),
ExtendedDHGroups.Group16SHA256(),
ExtendedDHGroups.Group16SHA384AtSSH(),
ExtendedDHGroups.Group16SHA512AtSSH(),
ExtendedDHGroups.Group18SHA512AtSSH(),
new ExtInfoClientFactory());
} else {
setKeyExchangeFactories(DHGroups.Group1SHA1(), new DHGexSHA1.Factory());
}
protected void initKeyExchangeFactories() {
setKeyExchangeFactories(
new Curve25519SHA256.Factory(),
new Curve25519SHA256.FactoryLibSsh(),
new DHGexSHA256.Factory(),
new ECDHNistP.Factory521(),
new ECDHNistP.Factory384(),
new ECDHNistP.Factory256(),
new DHGexSHA1.Factory(),
DHGroups.Group1SHA1(),
DHGroups.Group14SHA1(),
DHGroups.Group14SHA256(),
DHGroups.Group15SHA512(),
DHGroups.Group16SHA512(),
DHGroups.Group17SHA512(),
DHGroups.Group18SHA512(),
ExtendedDHGroups.Group14SHA256AtSSH(),
ExtendedDHGroups.Group15SHA256(),
ExtendedDHGroups.Group15SHA256AtSSH(),
ExtendedDHGroups.Group15SHA384AtSSH(),
ExtendedDHGroups.Group16SHA256(),
ExtendedDHGroups.Group16SHA384AtSSH(),
ExtendedDHGroups.Group16SHA512AtSSH(),
ExtendedDHGroups.Group18SHA512AtSSH(),
new ExtInfoClientFactory()
);
}
protected void initKeyAlgorithms() {
@@ -151,21 +148,19 @@ public class DefaultConfig
KeyAlgorithms.SSHDSA()));
}
protected void initRandomFactory(boolean bouncyCastleRegistered) {
protected void initRandomFactory() {
setRandomFactory(new SingletonRandomFactory(new JCERandom.Factory()));
}
protected void initFileKeyProviderFactories(boolean bouncyCastleRegistered) {
if (bouncyCastleRegistered) {
setFileKeyProviderFactories(
new OpenSSHKeyV1KeyFile.Factory(),
new PKCS8KeyFile.Factory(),
new OpenSSHKeyFile.Factory(),
new PuTTYKeyFile.Factory());
}
protected void initFileKeyProviderFactories() {
setFileKeyProviderFactories(
new OpenSSHKeyV1KeyFile.Factory(),
new PKCS8KeyFile.Factory(),
new OpenSSHKeyFile.Factory(),
new PuTTYKeyFile.Factory()
);
}
protected void initCipherFactories() {
List<Factory.Named<Cipher>> avail = new LinkedList<Factory.Named<Cipher>>(Arrays.<Factory.Named<Cipher>>asList(
ChachaPolyCiphers.CHACHA_POLY_OPENSSH(),
@@ -203,27 +198,22 @@ public class DefaultConfig
StreamCiphers.Arcfour256())
);
boolean warn = false;
// Ref. https://issues.apache.org/jira/browse/SSHD-24
// "AES256 and AES192 requires unlimited cryptography extension"
for (Iterator<Factory.Named<Cipher>> i = avail.iterator(); i.hasNext(); ) {
final Factory.Named<Cipher> f = i.next();
final ListIterator<Factory.Named<Cipher>> factories = avail.listIterator();
while (factories.hasNext()) {
final Factory.Named<Cipher> factory = factories.next();
try {
final Cipher c = f.create();
final byte[] key = new byte[c.getBlockSize()];
final byte[] iv = new byte[c.getIVSize()];
c.init(Cipher.Mode.Encrypt, key, iv);
final Cipher cipher = factory.create();
final byte[] key = new byte[cipher.getBlockSize()];
final byte[] iv = new byte[cipher.getIVSize()];
cipher.init(Cipher.Mode.Encrypt, key, iv);
} catch (Exception e) {
warn = true;
log.warn(e.getCause().getMessage());
i.remove();
log.info("Cipher [{}] disabled: {}", factory.getName(), e.getCause().getMessage());
factories.remove();
}
}
if (warn)
log.warn("Disabling high-strength ciphers: cipher strengths apparently limited by JCE policy");
setCipherFactories(avail);
log.debug("Available cipher factories: {}", avail);
log.debug("Available Ciphers {}", avail);
}
protected void initMACFactories() {

View File

@@ -0,0 +1,28 @@
/*
* Copyright (C)2009 - SSHJ Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.schmizz.sshj;
import net.schmizz.sshj.common.SecurityUtils;
/**
* SSHJ Configuration that uses the default Security Provider configuration from java.security and disables Bouncy Castle registration
*/
public class DefaultSecurityProviderConfig extends DefaultConfig {
static {
// Disable Bouncy Castle Provider registration prior to invoking constructors
SecurityUtils.setRegisterBouncyCastle(false);
}
}

View File

@@ -28,7 +28,6 @@ import net.schmizz.sshj.connection.channel.forwarded.RemotePortForwarder.Forward
import net.schmizz.sshj.connection.channel.forwarded.X11Forwarder;
import net.schmizz.sshj.connection.channel.forwarded.X11Forwarder.X11Channel;
import net.schmizz.sshj.sftp.SFTPClient;
import net.schmizz.sshj.sftp.SFTPEngine;
import net.schmizz.sshj.sftp.StatefulSFTPClient;
import net.schmizz.sshj.transport.Transport;
import net.schmizz.sshj.transport.TransportException;
@@ -733,7 +732,7 @@ public class SSHClient
throws IOException {
checkConnected();
checkAuthenticated();
return new SFTPClient(new SFTPEngine(this).init());
return new SFTPClient(this);
}
/**
@@ -742,11 +741,11 @@ public class SSHClient
*
* @throws IOException if there is an error starting the {@code sftp} subsystem
*/
public SFTPClient newStatefulSFTPClient()
public StatefulSFTPClient newStatefulSFTPClient()
throws IOException {
checkConnected();
checkAuthenticated();
return new StatefulSFTPClient(new SFTPEngine(this).init());
return new StatefulSFTPClient(this);
}
/**
@@ -805,12 +804,12 @@ public class SSHClient
throws IOException {
super.onConnect();
trans.init(getRemoteHostname(), getRemotePort(), getInputStream(), getOutputStream());
doKex();
final KeepAlive keepAliveThread = conn.getKeepAlive();
if (keepAliveThread.isEnabled()) {
ThreadNameProvider.setThreadName(conn.getKeepAlive(), trans);
keepAliveThread.start();
}
doKex();
}
/**

View File

@@ -65,7 +65,9 @@ public abstract class SocketClient {
this.hostname = hostname;
this.port = port;
socket = socketFactory.createSocket();
socket.connect(makeInetSocketAddress(hostname, port), connectTimeout);
if (! socket.isConnected()) {
socket.connect(makeInetSocketAddress(hostname, port), connectTimeout);
}
onConnect();
}
}
@@ -104,7 +106,9 @@ public abstract class SocketClient {
public void connect(InetAddress host, int port) throws IOException {
this.port = port;
socket = socketFactory.createSocket();
socket.connect(new InetSocketAddress(host, port), connectTimeout);
if (! socket.isConnected()) {
socket.connect(new InetSocketAddress(host, port), connectTimeout);
}
onConnect();
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,47 @@
/*
* Copyright (C)2009 - SSHJ Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.schmizz.sshj.common;
import java.io.IOException;
import java.util.Base64;
/**
* <p>Wraps {@link java.util.Base64.Decoder} in order to wrap unchecked {@code IllegalArgumentException} thrown by
* the default Java Base64 decoder here and there.</p>
*
* <p>Please use this class instead of {@link java.util.Base64.Decoder}.</p>
*/
public class Base64Decoder {
private Base64Decoder() {
}
public static byte[] decode(byte[] source) throws Base64DecodingException {
try {
return Base64.getDecoder().decode(source);
} catch (IllegalArgumentException err) {
throw new Base64DecodingException(err);
}
}
public static byte[] decode(String src) throws Base64DecodingException {
try {
return Base64.getDecoder().decode(src);
} catch (IllegalArgumentException err) {
throw new Base64DecodingException(err);
}
}
}

View File

@@ -13,9 +13,16 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hierynomus.sshj
class IntegrationTestUtil {
static final String USERNAME = "sshj"
static final String KEYFILE = "src/itest/resources/keyfiles/id_rsa"
package net.schmizz.sshj.common;
/**
* A checked wrapper for all {@link IllegalArgumentException}, thrown by {@link java.util.Base64.Decoder}.
*
* @see Base64Decoder
*/
public class Base64DecodingException extends Exception {
public Base64DecodingException(IllegalArgumentException cause) {
super("Failed to decode base64: " + cause.getMessage(), cause);
}
}

View File

@@ -0,0 +1,194 @@
/*
* Copyright (C)2009 - SSHJ Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.schmizz.sshj.common;
public class CircularBuffer<T extends CircularBuffer<T>> {
public static class CircularBufferException
extends SSHException {
public CircularBufferException(String message) {
super(message);
}
}
public static final class PlainCircularBuffer
extends CircularBuffer<PlainCircularBuffer> {
public PlainCircularBuffer(int size, int maxSize) {
super(size, maxSize);
}
}
/**
* Maximum size of the internal array (one plus the maximum capacity of the buffer).
*/
private final int maxSize;
/**
* Internal array for the data. All bytes minus one can be used to avoid empty vs full ambiguity when rpos == wpos.
*/
private byte[] data;
/**
* Next read position. Wraps around the end of the internal array. When it reaches wpos, the buffer becomes empty.
* Can take the value data.length, which is equivalent to 0.
*/
private int rpos;
/**
* Next write position. Wraps around the end of the internal array. If it is equal to rpos, then the buffer is
* empty; the code does not allow wpos to reach rpos from the left. This implies that the buffer can store up to
* data.length - 1 bytes. Can take the value data.length, which is equivalent to 0.
*/
private int wpos;
/**
* Determines the size to which to grow the internal array.
*/
private int getNextSize(int currentSize) {
// Use next power of 2.
int nextSize = 1;
while (nextSize < currentSize) {
nextSize <<= 1;
if (nextSize <= 0) {
return maxSize;
}
}
return Math.min(nextSize, maxSize); // limit to max size
}
/**
* Creates a new circular buffer of the given size. The capacity of the buffer is one less than the size/
*/
public CircularBuffer(int size, int maxSize) {
this.maxSize = maxSize;
if (size > maxSize) {
throw new IllegalArgumentException(
String.format("Initial requested size %d larger than maximum size %d", size, maxSize));
}
int initialSize = getNextSize(size);
this.data = new byte[initialSize];
this.rpos = 0;
this.wpos = 0;
}
/**
* Data available in the buffer for reading.
*/
public int available() {
int available = wpos - rpos;
return available >= 0 ? available : available + data.length; // adjust if wpos is left of rpos
}
private void ensureAvailable(int a)
throws CircularBufferException {
if (available() < a) {
throw new CircularBufferException("Underflow");
}
}
/**
* Returns how many more bytes this buffer can receive.
*/
public int maxPossibleRemainingCapacity() {
// Remaining capacity is one less than remaining space to ensure that wpos does not reach rpos from the left.
int remaining = rpos - wpos - 1;
if (remaining < 0) {
remaining += data.length; // adjust if rpos is left of wpos
}
// Add the maximum amount the internal array can grow.
return remaining + maxSize - data.length;
}
/**
* If the internal array does not have room for "capacity" more bytes, resizes the array to make that room.
*/
void ensureCapacity(int capacity) throws CircularBufferException {
int available = available();
int remaining = data.length - available;
// If capacity fits exactly in the remaining space, expand it; otherwise, wpos would reach rpos from the left.
if (remaining <= capacity) {
int neededSize = available + capacity + 1;
int nextSize = getNextSize(neededSize);
if (nextSize < neededSize) {
throw new CircularBufferException("Attempted overflow");
}
byte[] tmp = new byte[nextSize];
// Copy data to the beginning of the new array.
if (wpos >= rpos) {
System.arraycopy(data, rpos, tmp, 0, available);
wpos -= rpos; // wpos must be relative to the new rpos, which will be 0
} else {
int tail = data.length - rpos;
System.arraycopy(data, rpos, tmp, 0, tail); // segment right of rpos
System.arraycopy(data, 0, tmp, tail, wpos); // segment left of wpos
wpos += tail; // wpos must be relative to the new rpos, which will be 0
}
rpos = 0;
data = tmp;
}
}
/**
* Reads data from this buffer into the provided array.
*/
public void readRawBytes(byte[] destination, int offset, int length) throws CircularBufferException {
ensureAvailable(length);
int rposNext = rpos + length;
if (rposNext <= data.length) {
System.arraycopy(data, rpos, destination, offset, length);
} else {
int tail = data.length - rpos;
System.arraycopy(data, rpos, destination, offset, tail); // segment right of rpos
rposNext = length - tail; // rpos wraps around the end of the buffer
System.arraycopy(data, 0, destination, offset + tail, rposNext); // remainder
}
// This can make rpos equal data.length, which has the same effect as wpos being 0.
rpos = rposNext;
}
/**
* Writes data to this buffer from the provided array.
*/
@SuppressWarnings("unchecked")
public T putRawBytes(byte[] source, int offset, int length) throws CircularBufferException {
ensureCapacity(length);
int wposNext = wpos + length;
if (wposNext <= data.length) {
System.arraycopy(source, offset, data, wpos, length);
} else {
int tail = data.length - wpos;
System.arraycopy(source, offset, data, wpos, tail); // segment right of wpos
wposNext = length - tail; // wpos wraps around the end of the buffer
System.arraycopy(source, offset + tail, data, 0, wposNext); // remainder
}
// This can make wpos equal data.length, which has the same effect as wpos being 0.
wpos = wposNext;
return (T) this;
}
// Used only for testing.
int length() {
return data.length;
}
@Override
public String toString() {
return "CircularBuffer [rpos=" + rpos + ", wpos=" + wpos + ", size=" + data.length + "]";
}
}

View File

@@ -57,9 +57,6 @@ class ECDSAVariationsAdapter {
static PublicKey readPubKeyFromBuffer(Buffer<?> buf, String variation) throws GeneralSecurityException {
String algorithm = BASE_ALGORITHM_NAME + variation;
if (!SecurityUtils.isBouncyCastleRegistered()) {
throw new GeneralSecurityException("BouncyCastle is required to read a key of type " + algorithm);
}
try {
// final String algo = buf.readString(); it has been already read
final String curveName = buf.readString();

View File

@@ -259,6 +259,11 @@ public class SecurityUtils {
return false;
}
/**
* Configure whether to register the Bouncy Castle Security Provider. Must be called prior to other methods
*
* @param registerBouncyCastle Enable or disable Bouncy Castle Provider registration on subsequent method invocation
*/
public static synchronized void setRegisterBouncyCastle(boolean registerBouncyCastle) {
SecurityUtils.registerBouncyCastle = registerBouncyCastle;
registrationDone = false;

View File

@@ -164,8 +164,7 @@ public abstract class AbstractChannel
}
@Override
public void handle(Message msg, SSHPacket buf)
throws ConnectionException, TransportException {
public void handle(Message msg, SSHPacket buf) throws SSHException {
switch (msg) {
case CHANNEL_DATA:
@@ -304,6 +303,25 @@ public abstract class AbstractChannel
}
}
// Prevent CHANNEL_CLOSE to be sent between isOpen and a Transport.write call in the runnable, otherwise
// a disconnect with a "packet referred to nonexistent channel" message can occur.
//
// This particularly happens when the transport.Reader thread passes an eof from the server to the
// ChannelInputStream, the reading library-user thread returns, and closes the channel at the same time as the
// transport.Reader thread receives the subsequent CHANNEL_CLOSE from the server.
boolean whileOpen(TransportRunnable runnable) throws TransportException, ConnectionException {
openCloseLock.lock();
try {
if (isOpen()) {
runnable.run();
return true;
}
} finally {
openCloseLock.unlock();
}
return false;
}
private void gotChannelRequest(SSHPacket buf)
throws ConnectionException, TransportException {
final String reqType;
@@ -335,7 +353,7 @@ public abstract class AbstractChannel
}
protected void gotExtendedData(SSHPacket buf)
throws ConnectionException, TransportException {
throws SSHException {
throw new ConnectionException(DisconnectReason.PROTOCOL_ERROR,
"Extended data not supported on " + type + " channel");
}
@@ -356,7 +374,7 @@ public abstract class AbstractChannel
}
protected void receiveInto(ChannelInputStream stream, SSHPacket buf)
throws ConnectionException, TransportException {
throws SSHException {
final int len;
try {
len = buf.readUInt32AsInt();
@@ -427,5 +445,8 @@ public abstract class AbstractChannel
+ rwin + " >";
}
public interface TransportRunnable {
void run() throws TransportException, ConnectionException;
}
}

View File

@@ -38,7 +38,7 @@ public final class ChannelInputStream
private final Channel chan;
private final Transport trans;
private final Window.Local win;
private final Buffer.PlainBuffer buf;
private final CircularBuffer.PlainCircularBuffer buf;
private final byte[] b = new byte[1];
private boolean eof;
@@ -46,10 +46,11 @@ public final class ChannelInputStream
public ChannelInputStream(Channel chan, Transport trans, Window.Local win) {
this.chan = chan;
log = chan.getLoggerFactory().getLogger(getClass());
this.log = chan.getLoggerFactory().getLogger(getClass());
this.trans = trans;
this.win = win;
buf = new Buffer.PlainBuffer(chan.getLocalMaxPacketSize());
this.buf = new CircularBuffer.PlainCircularBuffer(
chan.getLocalMaxPacketSize(), trans.getConfig().getMaxCircularBufferSize());
}
@Override
@@ -113,48 +114,44 @@ public final class ChannelInputStream
len = buf.available();
}
buf.readRawBytes(b, off, len);
if (buf.rpos() > win.getMaxPacketSize() && buf.available() == 0) {
buf.clear();
}
}
if (!chan.getAutoExpand()) {
checkWindow();
if (!chan.getAutoExpand()) {
checkWindow();
}
}
return len;
}
public void receive(byte[] data, int offset, int len)
throws ConnectionException, TransportException {
public void receive(byte[] data, int offset, int len) throws SSHException {
if (eof) {
throw new ConnectionException("Getting data on EOF'ed stream");
}
synchronized (buf) {
buf.putRawBytes(data, offset, len);
buf.notifyAll();
}
// Potential fix for #203 (window consumed below 0).
// This seems to be a race condition if we receive more data, while we're already sending a SSH_MSG_CHANNEL_WINDOW_ADJUST
// And the window has not expanded yet.
synchronized (win) {
// Potential fix for #203 (window consumed below 0).
// This seems to be a race condition if we receive more data, while we're already sending a SSH_MSG_CHANNEL_WINDOW_ADJUST
// And the window has not expanded yet.
win.consume(len);
}
if (chan.getAutoExpand()) {
checkWindow();
if (chan.getAutoExpand()) {
checkWindow();
}
}
}
private void checkWindow()
throws TransportException {
synchronized (win) {
final long adjustment = win.neededAdjustment();
if (adjustment > 0) {
log.debug("Sending SSH_MSG_CHANNEL_WINDOW_ADJUST to #{} for {} bytes", chan.getRecipient(), adjustment);
trans.write(new SSHPacket(Message.CHANNEL_WINDOW_ADJUST)
.putUInt32FromInt(chan.getRecipient()).putUInt32(adjustment));
win.expand(adjustment);
}
private void checkWindow() throws TransportException {
/*
* Window must fit in remaining buffer capacity. We already expect win.size() amount of data to arrive. The
* difference between that and the remaining capacity is the maximum adjustment we can make to the window.
*/
final long maxAdjustment = buf.maxPossibleRemainingCapacity() - win.getSize();
final long adjustment = Math.min(win.neededAdjustment(), maxAdjustment);
if (adjustment > 0) {
log.debug("Sending SSH_MSG_CHANNEL_WINDOW_ADJUST to #{} for {} bytes", chan.getRecipient(), adjustment);
trans.write(new SSHPacket(Message.CHANNEL_WINDOW_ADJUST)
.putUInt32FromInt(chan.getRecipient()).putUInt32(adjustment));
win.expand(adjustment);
}
}

View File

@@ -30,7 +30,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
*/
public final class ChannelOutputStream extends OutputStream implements ErrorNotifiable {
private final Channel chan;
private final AbstractChannel chan;
private final Transport trans;
private final Window.Remote win;
@@ -47,6 +47,12 @@ public final class ChannelOutputStream extends OutputStream implements ErrorNoti
private final SSHPacket packet = new SSHPacket(Message.CHANNEL_DATA);
private final Buffer.PlainBuffer leftOvers = new Buffer.PlainBuffer();
private final AbstractChannel.TransportRunnable packetWriteRunnable = new AbstractChannel.TransportRunnable() {
@Override
public void run() throws TransportException {
trans.write(packet);
}
};
DataBuffer() {
headerOffset = packet.rpos();
@@ -99,8 +105,9 @@ public final class ChannelOutputStream extends OutputStream implements ErrorNoti
if (leftOverBytes > 0) {
leftOvers.putRawBytes(packet.array(), packet.wpos(), leftOverBytes);
}
trans.write(packet);
if (!chan.whileOpen(packetWriteRunnable)) {
throwStreamClosed();
}
win.consume(writeNow);
packet.rpos(headerOffset);
@@ -119,7 +126,7 @@ public final class ChannelOutputStream extends OutputStream implements ErrorNoti
}
public ChannelOutputStream(Channel chan, Transport trans, Window.Remote win) {
public ChannelOutputStream(AbstractChannel chan, Transport trans, Window.Remote win) {
this.chan = chan;
this.trans = trans;
this.win = win;
@@ -157,7 +164,7 @@ public final class ChannelOutputStream extends OutputStream implements ErrorNoti
if (error != null) {
throw error;
} else {
throw new ConnectionException("Stream closed");
throwStreamClosed();
}
}
}
@@ -165,9 +172,14 @@ public final class ChannelOutputStream extends OutputStream implements ErrorNoti
@Override
public synchronized void close() throws IOException {
// Not closed yet, and underlying channel is open to flush the data to.
if (!closed.getAndSet(true) && chan.isOpen()) {
buffer.flush(false);
trans.write(new SSHPacket(Message.CHANNEL_EOF).putUInt32(chan.getRecipient()));
if (!closed.getAndSet(true)) {
chan.whileOpen(new AbstractChannel.TransportRunnable() {
@Override
public void run() throws TransportException, ConnectionException {
buffer.flush(false);
trans.write(new SSHPacket(Message.CHANNEL_EOF).putUInt32(chan.getRecipient()));
}
});
}
}
@@ -188,4 +200,7 @@ public final class ChannelOutputStream extends OutputStream implements ErrorNoti
return "< ChannelOutputStream for Channel #" + chan.getID() + " >";
}
private static void throwStreamClosed() throws ConnectionException {
throw new ConnectionException("Stream closed");
}
}

View File

@@ -22,8 +22,6 @@ import java.io.IOException;
import java.net.Socket;
import java.util.concurrent.TimeUnit;
import static com.hierynomus.sshj.backport.Sockets.asCloseable;
public class SocketStreamCopyMonitor
extends Thread {
@@ -43,7 +41,7 @@ public class SocketStreamCopyMonitor
await(y);
} catch (IOException ignored) {
} finally {
IOUtils.closeQuietly(channel, asCloseable(socket));
IOUtils.closeQuietly(channel, socket);
}
}

View File

@@ -29,8 +29,6 @@ import java.net.Socket;
import java.net.SocketException;
import java.util.concurrent.TimeUnit;
import static com.hierynomus.sshj.backport.Sockets.asCloseable;
public class LocalPortForwarder {
public static class ForwardedChannel
@@ -78,7 +76,7 @@ public class LocalPortForwarder {
chan.open();
chan.start();
} catch (IOException e) {
IOUtils.closeQuietly(chan, asCloseable(socket));
IOUtils.closeQuietly(chan, socket);
throw e;
}
}

View File

@@ -210,7 +210,7 @@ public class SessionChannel
@Override
protected void gotExtendedData(SSHPacket buf)
throws ConnectionException, TransportException {
throws SSHException {
try {
final int dataTypeCode = buf.readUInt32AsInt();
if (dataTypeCode == 1)
@@ -225,7 +225,9 @@ public class SessionChannel
@Override
public void notifyError(SSHException error) {
err.notifyError(error);
if (err != null) {
err.notifyError(error);
}
super.notifyError(error);
}

View File

@@ -15,6 +15,7 @@
*/
package net.schmizz.sshj.sftp;
import com.hierynomus.sshj.sftp.RemoteResourceSelector;
import net.schmizz.sshj.sftp.Response.StatusCode;
import java.io.IOException;
@@ -22,6 +23,8 @@ import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import static com.hierynomus.sshj.sftp.RemoteResourceFilterConverter.selectorFrom;
public class RemoteDirectory
extends RemoteResource {
@@ -31,37 +34,55 @@ public class RemoteDirectory
public List<RemoteResourceInfo> scan(RemoteResourceFilter filter)
throws IOException {
List<RemoteResourceInfo> rri = new LinkedList<RemoteResourceInfo>();
// TODO: Remove GOTO!
loop:
for (; ; ) {
final Response res = requester.request(newRequest(PacketType.READDIR))
.retrieve(requester.getTimeoutMs(), TimeUnit.MILLISECONDS);
switch (res.getType()) {
return scan(selectorFrom(filter));
}
public List<RemoteResourceInfo> scan(RemoteResourceSelector selector)
throws IOException {
if (selector == null) {
selector = RemoteResourceSelector.ALL;
}
List<RemoteResourceInfo> remoteResourceInfos = new LinkedList<>();
while (true) {
final Response response = requester.request(newRequest(PacketType.READDIR))
.retrieve(requester.getTimeoutMs(), TimeUnit.MILLISECONDS);
switch (response.getType()) {
case NAME:
final int count = res.readUInt32AsInt();
final int count = response.readUInt32AsInt();
for (int i = 0; i < count; i++) {
final String name = res.readString(requester.sub.getRemoteCharset());
res.readString(); // long name - IGNORED - shdve never been in the protocol
final FileAttributes attrs = res.readFileAttributes();
final String name = response.readString(requester.sub.getRemoteCharset());
response.readString(); // long name - IGNORED - shdve never been in the protocol
final FileAttributes attrs = response.readFileAttributes();
final PathComponents comps = requester.getPathHelper().getComponents(path, name);
final RemoteResourceInfo inf = new RemoteResourceInfo(comps, attrs);
if (!(".".equals(name) || "..".equals(name)) && (filter == null || filter.accept(inf))) {
rri.add(inf);
if (".".equals(name) || "..".equals(name)) {
continue;
}
final RemoteResourceSelector.Result selectionResult = selector.select(inf);
switch (selectionResult) {
case ACCEPT:
remoteResourceInfos.add(inf);
break;
case CONTINUE:
continue;
case BREAK:
return remoteResourceInfos;
}
}
break;
case STATUS:
res.ensureStatusIs(StatusCode.EOF);
break loop;
response.ensureStatusIs(StatusCode.EOF);
return remoteResourceInfos;
default:
throw new SFTPException("Unexpected packet: " + res.getType());
throw new SFTPException("Unexpected packet: " + response.getType());
}
}
return rri;
}
}

View File

@@ -15,6 +15,8 @@
*/
package net.schmizz.sshj.sftp;
import com.hierynomus.sshj.sftp.RemoteResourceSelector;
import net.schmizz.sshj.connection.channel.direct.SessionFactory;
import net.schmizz.sshj.xfer.FilePermission;
import net.schmizz.sshj.xfer.LocalDestFile;
import net.schmizz.sshj.xfer.LocalSourceFile;
@@ -24,6 +26,8 @@ import java.io.Closeable;
import java.io.IOException;
import java.util.*;
import static com.hierynomus.sshj.sftp.RemoteResourceFilterConverter.selectorFrom;
public class SFTPClient
implements Closeable {
@@ -39,6 +43,13 @@ public class SFTPClient
this.xfer = new SFTPFileTransfer(engine);
}
public SFTPClient(SessionFactory sessionFactory) throws IOException {
this.engine = new SFTPEngine(sessionFactory);
this.engine.init();
log = engine.getLoggerFactory().getLogger(getClass());
this.xfer = new SFTPFileTransfer(engine);
}
public SFTPEngine getSFTPEngine() {
return engine;
}
@@ -49,16 +60,18 @@ public class SFTPClient
public List<RemoteResourceInfo> ls(String path)
throws IOException {
return ls(path, null);
return ls(path, RemoteResourceSelector.ALL);
}
public List<RemoteResourceInfo> ls(String path, RemoteResourceFilter filter)
throws IOException {
final RemoteDirectory dir = engine.openDir(path);
try {
return dir.scan(filter);
} finally {
dir.close();
return ls(path, selectorFrom(filter));
}
public List<RemoteResourceInfo> ls(String path, RemoteResourceSelector selector)
throws IOException {
try (RemoteDirectory dir = engine.openDir(path)) {
return dir.scan(selector == null ? RemoteResourceSelector.ALL : selector);
}
}
@@ -232,7 +245,7 @@ public class SFTPClient
throws IOException {
xfer.download(source, dest);
}
public void get(String source, String dest, long byteOffset)
throws IOException {
xfer.download(source, dest, byteOffset);
@@ -252,7 +265,7 @@ public class SFTPClient
throws IOException {
xfer.download(source, dest);
}
public void get(String source, LocalDestFile dest, long byteOffset)
throws IOException {
xfer.download(source, dest, byteOffset);
@@ -262,7 +275,7 @@ public class SFTPClient
throws IOException {
xfer.upload(source, dest);
}
public void put(LocalSourceFile source, String dest, long byteOffset)
throws IOException {
xfer.upload(source, dest, byteOffset);

View File

@@ -48,6 +48,7 @@ public class SFTPEngine
protected final PathHelper pathHelper;
private final Session session;
protected final Session.Subsystem sub;
protected final PacketReader reader;
protected final OutputStream out;
@@ -63,7 +64,7 @@ public class SFTPEngine
public SFTPEngine(SessionFactory ssh, String pathSep)
throws SSHException {
Session session = ssh.startSession();
session = ssh.startSession();
loggerFactory = session.getLoggerFactory();
log = loggerFactory.getLogger(getClass());
sub = session.startSubsystem("sftp");
@@ -81,7 +82,23 @@ public class SFTPEngine
public SFTPEngine init()
throws IOException {
transmit(new SFTPPacket<Request>(PacketType.INIT).putUInt32(MAX_SUPPORTED_VERSION));
return init(MAX_SUPPORTED_VERSION);
}
/**
* Introduced for internal use by testcases.
* @param requestedVersion
* @throws IOException
*/
protected SFTPEngine init(int requestedVersion)
throws IOException {
if (requestedVersion > MAX_SUPPORTED_VERSION)
throw new SFTPException("You requested an unsupported protocol version: " + requestedVersion + " (requested) > " + MAX_SUPPORTED_VERSION + " (supported)");
if (requestedVersion < MAX_SUPPORTED_VERSION)
log.debug("Client version {} is smaller than MAX_SUPPORTED_VERSION {}", requestedVersion, MAX_SUPPORTED_VERSION);
transmit(new SFTPPacket<Request>(PacketType.INIT).putUInt32(requestedVersion));
final SFTPPacket<Response> response = reader.readPacket();
@@ -91,7 +108,7 @@ public class SFTPEngine
operativeVersion = response.readUInt32AsInt();
log.debug("Server version {}", operativeVersion);
if (MAX_SUPPORTED_VERSION < operativeVersion)
if (requestedVersion < operativeVersion)
throw new SFTPException("Server reported incompatible protocol version: " + operativeVersion);
while (response.available() > 0)
@@ -234,16 +251,75 @@ public class SFTPEngine
public void rename(String oldPath, String newPath, Set<RenameFlags> flags)
throws IOException {
if (operativeVersion < 1)
if (operativeVersion < 1) {
throw new SFTPException("RENAME is not supported in SFTPv" + operativeVersion);
}
final Request request = newRequest(PacketType.RENAME).putString(oldPath, sub.getRemoteCharset()).putString(newPath, sub.getRemoteCharset());
// SFTP Version 5 introduced rename flags according to Section 6.5 of the specification
if (operativeVersion >= 5) {
long renameFlagMask = 0L;
for (RenameFlags flag : flags) {
renameFlagMask = renameFlagMask | flag.longValue();
// request variables to be determined
PacketType type = PacketType.RENAME; // Default
long renameFlagMask = 0L;
String serverExtension = null;
if (!flags.isEmpty()) {
// SFTP Version 5 introduced rename flags according to Section 6.5 of the specification
if (operativeVersion >= 5) {
for (RenameFlags flag : flags) {
renameFlagMask = renameFlagMask | flag.longValue();
}
}
// Try to find a fallback solution if flags are not supported by the server.
// "posix-rename@openssh.com" provides ATOMIC and OVERWRITE behaviour.
// From the SFTP-spec, Section 6.5:
// "If SSH_FXP_RENAME_OVERWRITE is specified, the server MAY perform an atomic rename even if it is
// not requested."
// So, if overwrite is allowed we can always use the posix-rename as a fallback.
else if (flags.contains(RenameFlags.OVERWRITE) &&
supportsServerExtension("posix-rename","openssh.com")) {
type = PacketType.EXTENDED;
serverExtension = "posix-rename@openssh.com";
}
// Because the OVERWRITE flag changes the behaviour in a possibly unintended way, it has to be
// explicitly requested for the above fallback to be applicable.
// Tell this to the developer if ATOMIC is requested without OVERWRITE.
else if (flags.contains(RenameFlags.ATOMIC) &&
!flags.contains(RenameFlags.OVERWRITE) &&
!flags.contains(RenameFlags.NATIVE) && // see next case below
supportsServerExtension("posix-rename","openssh.com")) {
throw new SFTPException("RENAME-FLAGS are not supported in SFTPv" + operativeVersion + " but " +
"the \"posix-rename@openssh.com\" extension could be used as fallback if OVERWRITE " +
"behaviour is acceptable (needs to be activated via RenameFlags.OVERWRITE).");
}
// From the SFTP-spec, Section 6.5:
// "If flags includes SSH_FXP_RENAME_NATIVE, the server is free to do the rename operation in whatever
// fashion it deems appropriate. Other flag values are considered hints as to desired behavior, but not
// requirements."
else if (flags.contains(RenameFlags.NATIVE)) {
log.debug("Flags are not supported but NATIVE-flag allows to ignore other requested flags: " +
flags.toString());
}
// finally: let the user know that the server does not support what was asked
else {
throw new SFTPException("RENAME-FLAGS are not supported in SFTPv" + operativeVersion + " and no " +
"supported server extension could be found to achieve a similar result.");
}
}
// build and send request
final Request request = newRequest(type);
if (serverExtension != null) {
request.putString(serverExtension);
}
request.putString(oldPath, sub.getRemoteCharset())
.putString(newPath, sub.getRemoteCharset());
if (renameFlagMask != 0L) {
request.putUInt32(renameFlagMask);
}
@@ -271,6 +347,7 @@ public class SFTPEngine
throws IOException {
sub.close();
reader.interrupt();
session.close();
}
protected LoggerFactory getLoggerFactory() {

View File

@@ -15,6 +15,8 @@
*/
package net.schmizz.sshj.sftp;
import com.hierynomus.sshj.sftp.RemoteResourceSelector;
import net.schmizz.sshj.connection.channel.direct.SessionFactory;
import net.schmizz.sshj.xfer.LocalDestFile;
import net.schmizz.sshj.xfer.LocalSourceFile;
@@ -22,6 +24,8 @@ import java.io.IOException;
import java.util.List;
import java.util.Set;
import static com.hierynomus.sshj.sftp.RemoteResourceFilterConverter.selectorFrom;
public class StatefulSFTPClient
extends SFTPClient {
@@ -34,6 +38,12 @@ public class StatefulSFTPClient
log.debug("Start dir = {}", cwd);
}
public StatefulSFTPClient(SessionFactory sessionFactory) throws IOException {
super(sessionFactory);
this.cwd = getSFTPEngine().canonicalize(".");
log.debug("Start dir = {}", cwd);
}
private synchronized String cwdify(String path) {
return engine.getPathHelper().adjustForParent(cwd, path);
}
@@ -50,7 +60,7 @@ public class StatefulSFTPClient
public synchronized List<RemoteResourceInfo> ls()
throws IOException {
return ls(cwd, null);
return ls(cwd, RemoteResourceSelector.ALL);
}
public synchronized List<RemoteResourceInfo> ls(RemoteResourceFilter filter)
@@ -63,20 +73,21 @@ public class StatefulSFTPClient
return super.canonicalize(cwd);
}
@Override
public List<RemoteResourceInfo> ls(String path)
throws IOException {
return ls(path, null);
return ls(path, RemoteResourceSelector.ALL);
}
public List<RemoteResourceInfo> ls(String path, RemoteResourceFilter filter)
throws IOException {
return ls(path, selectorFrom(filter));
}
@Override
public List<RemoteResourceInfo> ls(String path, RemoteResourceFilter filter)
public List<RemoteResourceInfo> ls(String path, RemoteResourceSelector selector)
throws IOException {
final RemoteDirectory dir = getSFTPEngine().openDir(cwdify(path));
try {
return dir.scan(filter);
} finally {
dir.close();
try (RemoteDirectory dir = getSFTPEngine().openDir(cwdify(path))) {
return dir.scan(selector == null ? RemoteResourceSelector.ALL : selector);
}
}

View File

@@ -51,6 +51,14 @@ abstract class Converter {
return seq;
}
void resetSequenceNumber() {
seq = -1;
}
boolean isSequenceNumberAtMax() {
return seq == 0xffffffffL;
}
void setAlgorithms(Cipher cipher, MAC mac, Compression compression) {
this.cipher = cipher;
this.mac = mac;

View File

@@ -60,6 +60,10 @@ final class KeyExchanger
private final AtomicBoolean kexOngoing = new AtomicBoolean();
private final AtomicBoolean initialKex = new AtomicBoolean(true);
private final AtomicBoolean strictKex = new AtomicBoolean();
/** What we are expecting from the next packet */
private Expected expected = Expected.KEXINIT;
@@ -123,6 +127,14 @@ final class KeyExchanger
return kexOngoing.get();
}
boolean isStrictKex() {
return strictKex.get();
}
boolean isInitialKex() {
return initialKex.get();
}
/**
* Starts key exchange by sending a {@code SSH_MSG_KEXINIT} packet. Key exchange needs to be done once mandatorily
* after initializing the {@link Transport} for it to be usable and may be initiated at any later point e.g. if
@@ -136,13 +148,25 @@ final class KeyExchanger
void startKex(boolean waitForDone)
throws TransportException {
if (!kexOngoing.getAndSet(true)) {
done.clear();
sendKexInit();
if (isKeyExchangeAllowed()) {
log.debug("Initiating key exchange");
done.clear();
sendKexInit();
} else {
kexOngoing.set(false);
}
}
if (waitForDone)
waitForDone();
}
/**
* Key exchange can be initiated exactly once while connecting or later after authentication when re-keying.
*/
private boolean isKeyExchangeAllowed() {
return !isKexDone() || transport.isAuthenticated();
}
void waitForDone()
throws TransportException {
done.await(transport.getTimeoutMs(), TimeUnit.MILLISECONDS);
@@ -171,7 +195,7 @@ final class KeyExchanger
throws TransportException {
log.debug("Sending SSH_MSG_KEXINIT");
List<String> knownHostAlgs = findKnownHostAlgs(transport.getRemoteHost(), transport.getRemotePort());
clientProposal = new Proposal(transport.getConfig(), knownHostAlgs);
clientProposal = new Proposal(transport.getConfig(), knownHostAlgs, initialKex.get());
transport.write(clientProposal.getPacket());
kexInitSent.set();
}
@@ -190,6 +214,9 @@ final class KeyExchanger
throws TransportException {
log.debug("Sending SSH_MSG_NEWKEYS");
transport.write(new SSHPacket(Message.NEWKEYS));
if (strictKex.get()) {
transport.getEncoder().resetSequenceNumber();
}
}
/**
@@ -222,6 +249,10 @@ final class KeyExchanger
private void setKexDone() {
kexOngoing.set(false);
initialKex.set(false);
if (strictKex.get()) {
transport.getDecoder().resetSequenceNumber();
}
kexInitSent.clear();
done.set();
}
@@ -230,6 +261,7 @@ final class KeyExchanger
throws TransportException {
buf.rpos(buf.rpos() - 1);
final Proposal serverProposal = new Proposal(buf);
gotStrictKexInfo(serverProposal);
negotiatedAlgs = clientProposal.negotiate(serverProposal);
log.debug("Negotiated algorithms: {}", negotiatedAlgs);
for(AlgorithmsVerifier v: algorithmVerifiers) {
@@ -253,6 +285,18 @@ final class KeyExchanger
}
}
private void gotStrictKexInfo(Proposal serverProposal) throws TransportException {
if (initialKex.get() && serverProposal.isStrictKeyExchangeSupportedByServer()) {
strictKex.set(true);
log.debug("Enabling strict key exchange extension");
if (transport.getDecoder().getSequenceNumber() != 0) {
throw new TransportException(DisconnectReason.KEY_EXCHANGE_FAILED,
"SSH_MSG_KEXINIT was not first package during strict key exchange"
);
}
}
}
/**
* Private method used while putting new keys into use that will resize the key used to initialize the cipher to the
* needed length.

View File

@@ -37,8 +37,11 @@ class Proposal {
private final List<String> s2cComp;
private final SSHPacket packet;
public Proposal(Config config, List<String> knownHostAlgs) {
public Proposal(Config config, List<String> knownHostAlgs, boolean initialKex) {
kex = Factory.Named.Util.getNames(config.getKeyExchangeFactories());
if (initialKex) {
kex.add("kex-strict-c-v00@openssh.com");
}
sig = filterKnownHostKeyAlgorithms(Factory.Named.Util.getNames(config.getKeyAlgorithms()), knownHostAlgs);
c2sCipher = s2cCipher = Factory.Named.Util.getNames(config.getCipherFactories());
c2sMAC = s2cMAC = Factory.Named.Util.getNames(config.getMACFactories());
@@ -91,6 +94,10 @@ class Proposal {
return kex;
}
public boolean isStrictKeyExchangeSupportedByServer() {
return kex.contains("kex-strict-s-v00@openssh.com");
}
public List<String> getHostKeyAlgorithms() {
return sig;
}

View File

@@ -420,13 +420,13 @@ public final class TransportImpl
try {
if (kexer.isKexOngoing()) {
// Only transport layer packets (1 to 49) allowed except SERVICE_REQUEST
// Only transport layer packets (1 to 49) allowed except SERVICE_REQUEST and IGNORE
final Message m = Message.fromByte(payload.array()[payload.rpos()]);
if (!m.in(1, 49) || m == Message.SERVICE_REQUEST) {
if (!m.in(1, 49) || m == Message.SERVICE_REQUEST || m == Message.IGNORE) {
assert m != Message.KEXINIT;
kexer.waitForDone();
}
} else if (encoder.getSequenceNumber() == 0) // We get here every 2**32th packet
} else if (encoder.isSequenceNumberAtMax()) // We get here every 2**32th packet
kexer.startKex(true);
final long seq = encoder.encode(payload);
@@ -479,9 +479,20 @@ public final class TransportImpl
log.trace("Received packet {}", msg);
if (kexer.isInitialKex()) {
if (decoder.isSequenceNumberAtMax()) {
throw new TransportException(DisconnectReason.KEY_EXCHANGE_FAILED,
"Sequence number of decoder is about to wrap during initial key exchange");
}
if (kexer.isStrictKex() && !isKexerPacket(msg) && msg != Message.DISCONNECT) {
throw new TransportException(DisconnectReason.KEY_EXCHANGE_FAILED,
"Unexpected packet type during initial strict key exchange");
}
}
if (msg.geq(50)) { // not a transport layer packet
service.handle(msg, buf);
} else if (msg.in(20, 21) || msg.in(30, 49)) { // kex packet
} else if (isKexerPacket(msg)) {
kexer.handle(msg, buf);
} else {
switch (msg) {
@@ -513,6 +524,10 @@ public final class TransportImpl
}
}
private static boolean isKexerPacket(Message msg) {
return msg.in(20, 21) || msg.in(30, 49);
}
private void gotDebug(SSHPacket buf)
throws TransportException {
try {

View File

@@ -15,18 +15,15 @@
*/
package net.schmizz.sshj.transport.compression;
import com.jcraft.jzlib.Deflater;
import com.jcraft.jzlib.GZIPException;
import com.jcraft.jzlib.Inflater;
import com.jcraft.jzlib.JZlib;
import java.util.zip.DataFormatException;
import java.util.zip.Deflater;
import java.util.zip.Inflater;
import net.schmizz.sshj.common.Buffer;
import net.schmizz.sshj.common.DisconnectReason;
import net.schmizz.sshj.common.SSHRuntimeException;
import net.schmizz.sshj.transport.TransportException;
/** ZLib based Compression. */
public class ZlibCompression
implements Compression {
public class ZlibCompression implements Compression {
/** Named factory for the ZLib Compression. */
public static class Factory
@@ -52,19 +49,15 @@ public class ZlibCompression
@Override
public void init(Mode mode) {
try {
switch (mode) {
case DEFLATE:
deflater = new Deflater(JZlib.Z_DEFAULT_COMPRESSION);
break;
case INFLATE:
inflater = new Inflater();
break;
default:
assert false;
}
} catch (GZIPException gze) {
switch (mode) {
case DEFLATE:
deflater = new Deflater(Deflater.DEFAULT_COMPRESSION);
break;
case INFLATE:
inflater = new Inflater();
break;
default:
assert false;
}
}
@@ -75,43 +68,32 @@ public class ZlibCompression
@Override
public void compress(Buffer buffer) {
deflater.setNextIn(buffer.array());
deflater.setNextInIndex(buffer.rpos());
deflater.setAvailIn(buffer.available());
deflater.setInput(buffer.array(), buffer.rpos(), buffer.available());
buffer.wpos(buffer.rpos());
do {
deflater.setNextOut(tempBuf);
deflater.setNextOutIndex(0);
deflater.setAvailOut(BUF_SIZE);
final int status = deflater.deflate(JZlib.Z_PARTIAL_FLUSH);
if (status == JZlib.Z_OK) {
buffer.putRawBytes(tempBuf, 0, BUF_SIZE - deflater.getAvailOut());
while (true) {
final int len = deflater.deflate(tempBuf, 0, BUF_SIZE, Deflater.SYNC_FLUSH);
if(len > 0) {
buffer.putRawBytes(tempBuf, 0, len);
} else {
throw new SSHRuntimeException("compress: deflate returned " + status);
return;
}
} while (deflater.getAvailOut() == 0);
}
}
@Override
public void uncompress(Buffer from, Buffer to)
throws TransportException {
inflater.setNextIn(from.array());
inflater.setNextInIndex(from.rpos());
inflater.setAvailIn(from.available());
inflater.setInput(from.array(), from.rpos(), from.available());
while (true) {
inflater.setNextOut(tempBuf);
inflater.setNextOutIndex(0);
inflater.setAvailOut(BUF_SIZE);
final int status = inflater.inflate(JZlib.Z_PARTIAL_FLUSH);
switch (status) {
case JZlib.Z_OK:
to.putRawBytes(tempBuf, 0, BUF_SIZE - inflater.getAvailOut());
break;
case JZlib.Z_BUF_ERROR:
try {
int len = inflater.inflate(tempBuf, 0, BUF_SIZE);
if(len > 0) {
to.putRawBytes(tempBuf, 0, len);
} else {
return;
default:
throw new TransportException(DisconnectReason.COMPRESSION_ERROR, "uncompress: inflate returned " + status);
}
} catch (DataFormatException e) {
throw new TransportException(DisconnectReason.COMPRESSION_ERROR, "uncompress: inflate returned " + e.getMessage());
}
}
}

View File

@@ -15,50 +15,91 @@
*/
package net.schmizz.sshj.transport.kex;
import com.hierynomus.sshj.common.KeyAlgorithm;
import net.schmizz.sshj.common.Factory;
import net.schmizz.sshj.common.SecurityUtils;
import net.schmizz.sshj.transport.random.Random;
import org.bouncycastle.asn1.x9.X9ECParameters;
import org.bouncycastle.crypto.ec.CustomNamedCurves;
import org.bouncycastle.jce.spec.ECParameterSpec;
import java.math.BigInteger;
import java.security.GeneralSecurityException;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.PublicKey;
import java.security.spec.AlgorithmParameterSpec;
import java.util.Arrays;
import java.security.spec.KeySpec;
import java.security.spec.X509EncodedKeySpec;
/**
* Key Exchange Method using Curve25519 as defined in RFC 8731
*/
public class Curve25519DH extends DHBase {
private byte[] secretKey;
private static final String ALGORITHM = "X25519";
private static final int KEY_LENGTH = 32;
private int encodedKeyLength;
private int algorithmIdLength;
// Algorithm Identifier is set on Key Agreement Initialization
private byte[] algorithmId = new byte[KEY_LENGTH];
public Curve25519DH() {
super(KeyAlgorithm.ECDSA, "ECDH");
}
@Override
void computeK(byte[] f) throws GeneralSecurityException {
byte[] k = new byte[32];
djb.Curve25519.curve(k, secretKey, f);
setK(new BigInteger(1, k));
}
@Override
public void init(AlgorithmParameterSpec params, Factory<Random> randomFactory) throws GeneralSecurityException {
Random random = randomFactory.create();
byte[] secretBytes = new byte[32];
random.fill(secretBytes);
byte[] publicBytes = new byte[32];
djb.Curve25519.keygen(publicBytes, null, secretBytes);
this.secretKey = Arrays.copyOf(secretBytes, secretBytes.length);
setE(publicBytes);
super(ALGORITHM, ALGORITHM);
}
/**
* TODO want to figure out why BouncyCastle does not work.
* @return The initialized curve25519 parameter spec
* Compute Shared Secret Key using Diffie-Hellman Curve25519 known as X25519
*
* @param peerPublicKey Peer public key bytes
* @throws GeneralSecurityException Thrown on key agreement failures
*/
public static AlgorithmParameterSpec getCurve25519Params() {
X9ECParameters ecP = CustomNamedCurves.getByName("curve25519");
return new ECParameterSpec(ecP.getCurve(), ecP.getG(), ecP.getN(), ecP.getH(), ecP.getSeed());
@Override
void computeK(final byte[] peerPublicKey) throws GeneralSecurityException {
final KeyFactory keyFactory = SecurityUtils.getKeyFactory(ALGORITHM);
final KeySpec peerPublicKeySpec = getPeerPublicKeySpec(peerPublicKey);
final PublicKey generatedPeerPublicKey = keyFactory.generatePublic(peerPublicKeySpec);
agreement.doPhase(generatedPeerPublicKey, true);
final byte[] sharedSecretKey = agreement.generateSecret();
final BigInteger sharedSecretNumber = new BigInteger(BigInteger.ONE.signum(), sharedSecretKey);
setK(sharedSecretNumber);
}
/**
* Initialize Key Agreement with generated Public and Private Key Pair
*
* @param params Parameters not used
* @param randomFactory Random Factory not used
* @throws GeneralSecurityException Thrown on key agreement initialization failures
*/
@Override
public void init(final AlgorithmParameterSpec params, final Factory<Random> randomFactory) throws GeneralSecurityException {
final KeyPair keyPair = generator.generateKeyPair();
agreement.init(keyPair.getPrivate());
setPublicKey(keyPair.getPublic());
}
private void setPublicKey(final PublicKey publicKey) {
final byte[] encoded = publicKey.getEncoded();
// Set key and algorithm identifier lengths based on initialized Public Key
encodedKeyLength = encoded.length;
algorithmIdLength = encodedKeyLength - KEY_LENGTH;
algorithmId = new byte[algorithmIdLength];
// Encoded public key consists of the algorithm identifier and public key
final byte[] publicKeyEncoded = new byte[KEY_LENGTH];
System.arraycopy(encoded, algorithmIdLength, publicKeyEncoded, 0, KEY_LENGTH);
setE(publicKeyEncoded);
// Save Algorithm Identifier byte array
System.arraycopy(encoded, 0, algorithmId, 0, algorithmIdLength);
}
private KeySpec getPeerPublicKeySpec(final byte[] peerPublicKey) {
final byte[] encodedKeySpec = new byte[encodedKeyLength];
System.arraycopy(algorithmId, 0, encodedKeySpec, 0, algorithmIdLength);
System.arraycopy(peerPublicKey, 0, encodedKeySpec, algorithmIdLength, KEY_LENGTH);
return new X509EncodedKeySpec(encodedKeySpec);
}
}

View File

@@ -56,6 +56,6 @@ public class Curve25519SHA256 extends AbstractDHG {
@Override
protected void initDH(DHBase dh) throws GeneralSecurityException {
dh.init(Curve25519DH.getCurve25519Params(), trans.getConfig().getRandomFactory());
dh.init(null, trans.getConfig().getRandomFactory());
}
}

View File

@@ -15,16 +15,15 @@
*/
package net.schmizz.sshj.transport.verification;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.MessageDigest;
import java.security.PublicKey;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collections;
import java.util.List;
import java.util.regex.Pattern;
import net.schmizz.sshj.common.Base64;
import net.schmizz.sshj.common.Buffer;
import net.schmizz.sshj.common.SSHRuntimeException;
import net.schmizz.sshj.common.SecurityUtils;
@@ -46,48 +45,40 @@ public class FingerprintVerifier implements HostKeyVerifier {
*
* @param fingerprint of an SSH fingerprint in MD5 (hex), SHA-1 (base64) or SHA-256(base64) format
*
* @return
* @return Host Key Verifier
*/
public static HostKeyVerifier getInstance(String fingerprint) {
try {
if (fingerprint.startsWith("SHA1:")) {
return new FingerprintVerifier("SHA-1", fingerprint.substring(5));
}
if (fingerprint.startsWith("SHA256:")) {
return new FingerprintVerifier("SHA-256", fingerprint.substring(7));
}
final String md5;
if (fingerprint.startsWith("MD5:")) {
md5 = fingerprint.substring(4); // remove the MD5: prefix
} else {
md5 = fingerprint;
}
if (!MD5_FINGERPRINT_PATTERN.matcher(md5).matches()) {
throw new SSHRuntimeException("Invalid MD5 fingerprint: " + fingerprint);
}
// Use the old default fingerprint verifier for md5 fingerprints
return (new HostKeyVerifier() {
@Override
public boolean verify(String h, int p, PublicKey k) {
return SecurityUtils.getFingerprint(k).equals(md5);
}
@Override
public List<String> findExistingAlgorithms(String hostname, int port) {
return Collections.emptyList();
}
});
} catch (SSHRuntimeException e) {
throw e;
} catch (IOException e) {
throw new SSHRuntimeException(e);
if (fingerprint.startsWith("SHA1:")) {
return new FingerprintVerifier("SHA-1", fingerprint.substring(5));
}
if (fingerprint.startsWith("SHA256:")) {
return new FingerprintVerifier("SHA-256", fingerprint.substring(7));
}
final String md5;
if (fingerprint.startsWith("MD5:")) {
md5 = fingerprint.substring(4); // remove the MD5: prefix
} else {
md5 = fingerprint;
}
if (!MD5_FINGERPRINT_PATTERN.matcher(md5).matches()) {
throw new SSHRuntimeException("Invalid MD5 fingerprint: " + fingerprint);
}
// Use the old default fingerprint verifier for md5 fingerprints
return (new HostKeyVerifier() {
@Override
public boolean verify(String h, int p, PublicKey k) {
return SecurityUtils.getFingerprint(k).equals(md5);
}
@Override
public List<String> findExistingAlgorithms(String hostname, int port) {
return Collections.emptyList();
}
});
}
private final String digestAlgorithm;
@@ -99,10 +90,8 @@ public class FingerprintVerifier implements HostKeyVerifier {
* the used digest algorithm
* @param base64Fingerprint
* base64 encoded fingerprint data
*
* @throws IOException
*/
private FingerprintVerifier(String digestAlgorithm, String base64Fingerprint) throws IOException {
private FingerprintVerifier(String digestAlgorithm, String base64Fingerprint) {
this.digestAlgorithm = digestAlgorithm;
// if the length is not padded with "=" chars at the end so that it is divisible by 4 the SSHJ Base64 implementation does not work correctly
@@ -110,7 +99,7 @@ public class FingerprintVerifier implements HostKeyVerifier {
while (base64FingerprintBuilder.length() % 4 != 0) {
base64FingerprintBuilder.append("=");
}
fingerprintData = Base64.decode(base64FingerprintBuilder.toString());
fingerprintData = Base64.getDecoder().decode(base64FingerprintBuilder.toString());
}
@Override

View File

@@ -21,12 +21,22 @@ import com.hierynomus.sshj.userauth.certificate.Certificate;
import net.schmizz.sshj.common.*;
import org.slf4j.Logger;
import java.io.*;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Reader;
import java.math.BigInteger;
import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.spec.RSAPublicKeySpec;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.List;
/**
@@ -274,10 +284,10 @@ public class OpenSSHKnownHosts
if (type != KeyType.UNKNOWN) {
final String sKey = split[i++];
try {
byte[] keyBytes = Base64.decode(sKey);
byte[] keyBytes = Base64Decoder.decode(sKey);
key = new Buffer.PlainBuffer(keyBytes).readPublicKey();
} catch (IOException ioe) {
log.warn("Error decoding Base64 key bytes", ioe);
} catch (IOException | Base64DecodingException exception) {
log.warn("Error decoding Base64 key bytes", exception);
return new BadHostEntry(line);
}
} else if (isBits(sType)) {
@@ -454,7 +464,7 @@ public class OpenSSHKnownHosts
private String getKeyString(PublicKey pk) {
final Buffer.PlainBuffer buf = new Buffer.PlainBuffer().putPublicKey(pk);
return Base64.encodeBytes(buf.array(), buf.rpos(), buf.available());
return Base64.getEncoder().encodeToString(Arrays.copyOfRange(buf.array(), buf.rpos(), buf.available()));
}
protected String getHostPart() {

View File

@@ -34,38 +34,47 @@ public abstract class BaseFileKeyProvider implements FileKeyProvider {
@Override
public void init(Reader location) {
assert location != null;
resource = new PrivateKeyReaderResource(location);
this.init(location, (PasswordFinder) null);
}
@Override
public void init(Reader location, PasswordFinder pwdf) {
init(location);
this.init(location, null, pwdf);
}
@Override
public void init(Reader privateKey, Reader publicKey) {
this.init(privateKey, publicKey, null);
}
@Override
public void init(Reader privateKey, Reader publicKey, PasswordFinder pwdf) {
assert publicKey == null;
this.resource = new PrivateKeyReaderResource(privateKey);
this.pwdf = pwdf;
}
@Override
public void init(File location) {
assert location != null;
resource = new PrivateKeyFileResource(location.getAbsoluteFile());
this.init(location, null);
}
@Override
public void init(File location, PasswordFinder pwdf) {
init(location);
this.resource = new PrivateKeyFileResource(location.getAbsoluteFile());
this.pwdf = pwdf;
}
@Override
public void init(String privateKey, String publicKey) {
assert privateKey != null;
assert publicKey == null;
resource = new PrivateKeyStringResource(privateKey);
this.init(privateKey, publicKey, null);
}
@Override
public void init(String privateKey, String publicKey, PasswordFinder pwdf) {
init(privateKey, publicKey);
assert privateKey != null;
assert publicKey == null;
this.resource = new PrivateKeyStringResource(privateKey);
this.pwdf = pwdf;
}

View File

@@ -30,6 +30,10 @@ public interface FileKeyProvider
void init(Reader location);
void init(Reader privateKey, Reader publicKey);
void init(Reader privateKey, Reader publicKey, PasswordFinder pwdf);
void init(Reader location, PasswordFinder pwdf);
void init(String privateKey, String publicKey);

View File

@@ -16,6 +16,7 @@
package net.schmizz.sshj.userauth.keyprovider;
import com.hierynomus.sshj.userauth.keyprovider.OpenSSHKeyFileUtil;
import net.schmizz.sshj.userauth.password.PasswordFinder;
import java.io.*;
import java.security.PublicKey;
@@ -54,21 +55,22 @@ public class OpenSSHKeyFile
}
@Override
public void init(File location) {
public void init(File location, PasswordFinder pwdf) {
// try cert key location first
File pubKey = OpenSSHKeyFileUtil.getPublicKeyFile(location);
if (pubKey != null)
if (pubKey != null) {
try {
initPubKey(new FileReader(pubKey));
} catch (IOException e) {
// let super provide both public & private key
log.warn("Error reading public key file: {}", e.toString());
}
super.init(location);
}
super.init(location, pwdf);
}
@Override
public void init(String privateKey, String publicKey) {
public void init(String privateKey, String publicKey, PasswordFinder pwdf) {
if (publicKey != null) {
try {
initPubKey(new StringReader(publicKey));
@@ -77,7 +79,20 @@ public class OpenSSHKeyFile
log.warn("Error reading public key: {}", e.toString());
}
}
super.init(privateKey, null);
super.init(privateKey, null, pwdf);
}
@Override
public void init(Reader privateKey, Reader publicKey, PasswordFinder pwdf) {
if (publicKey != null) {
try {
initPubKey(publicKey);
} catch (IOException e) {
// let super provide both public & private key
log.warn("Error reading public key: {}", e.toString());
}
}
super.init(privateKey, null, pwdf);
}
/**

View File

@@ -22,10 +22,7 @@ import net.i2p.crypto.eddsa.spec.EdDSANamedCurveSpec;
import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable;
import net.i2p.crypto.eddsa.spec.EdDSAPrivateKeySpec;
import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec;
import net.schmizz.sshj.common.Base64;
import net.schmizz.sshj.common.Buffer;
import net.schmizz.sshj.common.KeyType;
import net.schmizz.sshj.common.SecurityUtils;
import net.schmizz.sshj.common.*;
import net.schmizz.sshj.userauth.password.PasswordUtils;
import org.bouncycastle.asn1.nist.NISTNamedCurves;
import org.bouncycastle.asn1.x9.X9ECParameters;
@@ -44,8 +41,6 @@ import java.security.*;
import java.security.spec.*;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
/**
@@ -242,29 +237,34 @@ public class PuTTYKeyFile extends BaseFileKeyProvider {
if (this.keyFileVersion == null) {
throw new IOException("Invalid key file format: missing \"PuTTY-User-Key-File-?\" entry");
}
// Retrieve keys from payload
publicKey = Base64.decode(payload.get("Public-Lines"));
if (this.isEncrypted()) {
final char[] passphrase;
if (pwdf != null) {
passphrase = pwdf.reqPassword(resource);
} else {
passphrase = "".toCharArray();
}
try {
privateKey = this.decrypt(Base64.decode(payload.get("Private-Lines")), passphrase);
Mac mac;
if (this.keyFileVersion <= 2) {
mac = this.prepareVerifyMacV2(passphrase);
try {
// Retrieve keys from payload
publicKey = Base64Decoder.decode(payload.get("Public-Lines"));
if (this.isEncrypted()) {
final char[] passphrase;
if (pwdf != null) {
passphrase = pwdf.reqPassword(resource);
} else {
mac = this.prepareVerifyMacV3();
passphrase = "".toCharArray();
}
this.verify(mac);
} finally {
PasswordUtils.blankOut(passphrase);
try {
privateKey = this.decrypt(Base64Decoder.decode(payload.get("Private-Lines")), passphrase);
Mac mac;
if (this.keyFileVersion <= 2) {
mac = this.prepareVerifyMacV2(passphrase);
} else {
mac = this.prepareVerifyMacV3();
}
this.verify(mac);
} finally {
PasswordUtils.blankOut(passphrase);
}
} else {
privateKey = Base64Decoder.decode(payload.get("Private-Lines"));
}
} else {
privateKey = Base64.decode(payload.get("Private-Lines"));
}
catch (Base64DecodingException e) {
throw new IOException("PuTTY key decoding failed", e);
}
}

View File

@@ -27,7 +27,8 @@ import java.util.regex.Pattern;
public class PasswordResponseProvider
implements ChallengeResponseProvider {
public static final Pattern DEFAULT_PROMPT_PATTERN = Pattern.compile(".*[pP]assword:\\s?\\z", Pattern.DOTALL);
// FreeBSD prompt is "Password for user@host:"
public static final Pattern DEFAULT_PROMPT_PATTERN = Pattern.compile(".*[pP]assword(?: for .*)?:\\s?\\z", Pattern.DOTALL);
private static final char[] EMPTY_RESPONSE = new char[0];

View File

@@ -45,9 +45,20 @@ public class SCPDownloadClient extends AbstractSCPClient {
public synchronized int copy(String sourcePath, LocalDestFile targetFile, ScpCommandLine.EscapeMode escapeMode)
throws IOException {
ScpCommandLine commandLine = ScpCommandLine.with(ScpCommandLine.Arg.SOURCE)
.and(ScpCommandLine.Arg.QUIET)
.and(ScpCommandLine.Arg.PRESERVE_TIMES)
.and(ScpCommandLine.Arg.RECURSIVE, recursiveMode)
.and(ScpCommandLine.Arg.LIMIT, String.valueOf(bandwidthLimit), (bandwidthLimit > 0));
return copy(sourcePath, targetFile, escapeMode, commandLine);
}
public synchronized int copy(String sourcePath, LocalDestFile targetFile, ScpCommandLine.EscapeMode escapeMode, ScpCommandLine commandLine)
throws IOException {
engine.cleanSlate();
try {
startCopy(sourcePath, targetFile, escapeMode);
commandLine.withPath(sourcePath, escapeMode);
startCopy(targetFile, commandLine);
} finally {
engine.exit();
}
@@ -62,14 +73,7 @@ public class SCPDownloadClient extends AbstractSCPClient {
this.recursiveMode = recursive;
}
private void startCopy(String sourcePath, LocalDestFile targetFile, ScpCommandLine.EscapeMode escapeMode)
throws IOException {
ScpCommandLine commandLine = ScpCommandLine.with(ScpCommandLine.Arg.SOURCE)
.and(ScpCommandLine.Arg.QUIET)
.and(ScpCommandLine.Arg.PRESERVE_TIMES)
.and(ScpCommandLine.Arg.RECURSIVE, recursiveMode)
.and(ScpCommandLine.Arg.LIMIT, String.valueOf(bandwidthLimit), (bandwidthLimit > 0));
commandLine.withPath(sourcePath, escapeMode);
private void startCopy(LocalDestFile targetFile, ScpCommandLine commandLine) throws IOException {
engine.execSCPWith(commandLine);
engine.signal("Start status OK");

View File

@@ -19,6 +19,7 @@ import net.schmizz.sshj.common.IOUtils;
import net.schmizz.sshj.common.LoggerFactory;
import net.schmizz.sshj.common.SSHException;
import net.schmizz.sshj.common.StreamCopier;
import net.schmizz.sshj.connection.channel.direct.Session;
import net.schmizz.sshj.connection.channel.direct.Session.Command;
import net.schmizz.sshj.connection.channel.direct.SessionFactory;
import net.schmizz.sshj.xfer.TransferListener;
@@ -41,6 +42,7 @@ class SCPEngine {
private final SessionFactory host;
private final TransferListener listener;
private Session session;
private Command scp;
private int exitStatus;
@@ -82,7 +84,8 @@ class SCPEngine {
void execSCPWith(ScpCommandLine commandLine)
throws SSHException {
scp = host.startSession().exec(commandLine.toCommandLine());
session = host.startSession();
scp = session.exec(commandLine.toCommandLine());
}
void exit() {
@@ -102,6 +105,10 @@ class SCPEngine {
log.warn("SCP exit signal: {}", scp.getExitSignal());
}
}
if(session != null) {
IOUtils.closeQuietly(session);
session = null;
}
scp = null;
}

View File

@@ -20,6 +20,7 @@ import net.schmizz.sshj.common.StreamCopier;
import net.schmizz.sshj.xfer.LocalFileFilter;
import net.schmizz.sshj.xfer.LocalSourceFile;
import net.schmizz.sshj.xfer.TransferListener;
import net.schmizz.sshj.xfer.scp.ScpCommandLine.Arg;
import java.io.IOException;
import java.io.InputStream;
@@ -43,37 +44,44 @@ public class SCPUploadClient extends AbstractSCPClient {
return copy(sourceFile, remotePath, ScpCommandLine.EscapeMode.SingleQuote);
}
public synchronized int copy (LocalSourceFile sourceFile, String remotePath, ScpCommandLine.EscapeMode escapeMode) throws IOException {
public synchronized int copy(LocalSourceFile sourceFile, String remotePath, ScpCommandLine.EscapeMode escapeMode)
throws IOException {
return copy(sourceFile, remotePath, escapeMode, true);
}
public synchronized int copy(LocalSourceFile sourceFile, String remotePath, ScpCommandLine.EscapeMode escapeMode, boolean preserveTimes)
throws IOException {
throws IOException {
ScpCommandLine commandLine = ScpCommandLine.with(ScpCommandLine.Arg.SINK)
.and(ScpCommandLine.Arg.RECURSIVE)
.and(ScpCommandLine.Arg.LIMIT, String.valueOf(bandwidthLimit), (bandwidthLimit > 0));
if (preserveTimes) {
commandLine.and(ScpCommandLine.Arg.PRESERVE_TIMES, sourceFile.providesAtimeMtime());
}
return copy(sourceFile, remotePath, escapeMode, commandLine);
}
public synchronized int copy(LocalSourceFile sourceFile, String remotePath, ScpCommandLine.EscapeMode escapeMode, ScpCommandLine commandLine)
throws IOException {
engine.cleanSlate();
try {
startCopy(sourceFile, remotePath, escapeMode, preserveTimes);
commandLine.withPath(remotePath, escapeMode);
startCopy(sourceFile, commandLine);
} finally {
engine.exit();
}
return engine.getExitStatus();
}
public void setUploadFilter(LocalFileFilter uploadFilter) {
this.uploadFilter = uploadFilter;
}
private void startCopy(LocalSourceFile sourceFile, String targetPath, ScpCommandLine.EscapeMode escapeMode, boolean preserveTimes)
private void startCopy(LocalSourceFile sourceFile, ScpCommandLine commandLine)
throws IOException {
ScpCommandLine commandLine = ScpCommandLine.with(ScpCommandLine.Arg.SINK)
.and(ScpCommandLine.Arg.RECURSIVE)
.and(ScpCommandLine.Arg.LIMIT, String.valueOf(bandwidthLimit), (bandwidthLimit > 0));
if (preserveTimes) {
commandLine.and(ScpCommandLine.Arg.PRESERVE_TIMES, sourceFile.providesAtimeMtime());
}
commandLine.withPath(targetPath, escapeMode);
engine.execSCPWith(commandLine);
engine.check("Start status OK");
process(engine.getTransferListener(), sourceFile, preserveTimes);
process(engine.getTransferListener(), sourceFile, commandLine.has(Arg.PRESERVE_TIMES));
}
private void process(TransferListener listener, LocalSourceFile f, boolean preserveTimes)

View File

@@ -24,7 +24,7 @@ public class ScpCommandLine {
private static final String SCP_COMMAND = "scp";
private EscapeMode mode;
enum Arg {
public enum Arg {
SOURCE('f'),
SINK('t'),
RECURSIVE('r'),
@@ -77,19 +77,19 @@ public class ScpCommandLine {
ScpCommandLine() {
}
static ScpCommandLine with(Arg name) {
public static ScpCommandLine with(Arg name) {
return with(name, null, true);
}
static ScpCommandLine with(Arg name, String value) {
public static ScpCommandLine with(Arg name, String value) {
return with(name, value, true);
}
static ScpCommandLine with(Arg name, boolean accept) {
public static ScpCommandLine with(Arg name, boolean accept) {
return with(name, null, accept);
}
static ScpCommandLine with(Arg name, String value, boolean accept) {
public static ScpCommandLine with(Arg name, String value, boolean accept) {
ScpCommandLine commandLine = new ScpCommandLine();
commandLine.addArgument(name, value, accept);
return commandLine;
@@ -101,22 +101,22 @@ public class ScpCommandLine {
}
}
ScpCommandLine and(Arg name) {
public ScpCommandLine and(Arg name) {
addArgument(name, null, true);
return this;
}
ScpCommandLine and(Arg name, String value) {
public ScpCommandLine and(Arg name, String value) {
addArgument(name, value, true);
return this;
}
ScpCommandLine and(Arg name, boolean accept) {
public ScpCommandLine and(Arg name, boolean accept) {
addArgument(name, null, accept);
return this;
}
ScpCommandLine and(Arg name, String value, boolean accept) {
public ScpCommandLine and(Arg name, String value, boolean accept) {
addArgument(name, value, accept);
return this;
}
@@ -127,6 +127,10 @@ public class ScpCommandLine {
return this;
}
boolean has(Arg arg) {
return arguments.containsKey(arg);
}
String toCommandLine() {
final StringBuilder cmd = new StringBuilder(SCP_COMMAND);
for (Arg arg : arguments.keySet()) {

View File

@@ -16,7 +16,6 @@
package com.hierynomus.sshj.common
import com.hierynomus.sshj.userauth.certificate.Certificate
import net.schmizz.sshj.common.Base64
import net.schmizz.sshj.common.Buffer
import net.schmizz.sshj.common.KeyType
import net.schmizz.sshj.userauth.keyprovider.OpenSSHKeyFile
@@ -110,7 +109,7 @@ f26VSnEypH3G3cmPYfpVcXL63bCb0E4sNJwENM4tQGZa5YGz3CxMdgIVAJUv4z9+
def keyType = KeyType.fromString(parts[0])
when:
def pubKey = new Buffer.PlainBuffer(Base64.decode(parts[1])).readPublicKey()
def pubKey = new Buffer.PlainBuffer(Base64.getDecoder().decode(parts[1])).readPublicKey()
then:
KeyType.fromKey(pubKey) == keyType

View File

@@ -15,18 +15,18 @@
*/
package com.hierynomus.sshj.connection.channel.direct
import com.hierynomus.sshj.test.SshFixture
import com.hierynomus.sshj.test.SshServerExtension
import net.schmizz.sshj.connection.channel.direct.Parameters
import org.junit.Rule
import org.junit.jupiter.api.extension.RegisterExtension
import spock.lang.Specification
import spock.util.concurrent.PollingConditions
class LocalPortForwarderSpec extends Specification {
@Rule
SshFixture tunnelFixture = new SshFixture()
@RegisterExtension
SshServerExtension tunnelFixture = new SshServerExtension()
@Rule
SshFixture realServer = new SshFixture()
@RegisterExtension
SshServerExtension realServer = new SshServerExtension()
def "should not hang when disconnect tunnel"() {
given:

View File

@@ -15,25 +15,30 @@
*/
package com.hierynomus.sshj.sftp
import com.hierynomus.sshj.test.SshFixture
import com.hierynomus.sshj.test.SshServerExtension
import com.hierynomus.sshj.test.util.FileUtil
import net.schmizz.sshj.SSHClient
import net.schmizz.sshj.sftp.FileMode
import net.schmizz.sshj.sftp.RemoteResourceInfo
import net.schmizz.sshj.sftp.SFTPClient
import org.junit.Rule
import org.junit.rules.TemporaryFolder
import org.junit.jupiter.api.extension.RegisterExtension
import spock.lang.Specification
import spock.lang.TempDir
import spock.lang.Unroll
import java.nio.file.Files
import java.nio.file.Path
import static org.codehaus.groovy.runtime.IOGroovyMethods.withCloseable
class SFTPClientSpec extends Specification {
@Rule
public SshFixture fixture = new SshFixture()
@RegisterExtension
public SshServerExtension fixture = new SshServerExtension()
@Rule
public TemporaryFolder temp = new TemporaryFolder()
@TempDir
public Path temp
@Unroll
def "should copy #sourceType->#targetType if #targetExists with #named name"() {
@@ -79,11 +84,11 @@ class SFTPClientSpec extends Specification {
def "should not throw exception on close before disconnect"() {
given:
File file = temp.newFile("source.txt")
File file = Files.createFile(temp.resolve("source.txt")).toFile()
FileUtil.writeToFile(file, "This is the source")
when:
doUpload(file, temp.newFile("dest.txt"))
doUpload(file, temp.resolve("dest.txt").toFile())
then:
noExceptionThrown()
@@ -147,10 +152,10 @@ class SFTPClientSpec extends Specification {
def "should not merge same name subdirs (GH #252)"() {
given:
File toto = temp.newFolder("toto")
File toto = Files.createDirectory(temp.resolve("toto")).toFile()
File tutu = mkdir(toto, "tutu")
File toto2 = mkdir(toto, "toto")
File dest = temp.newFolder("dest")
File dest = Files.createDirectory(temp.resolve("dest")).toFile()
FileUtil.writeToFile(new File(toto, "toto.txt"), "Toto file")
FileUtil.writeToFile(new File(tutu, "tototutu.txt"), "Toto/Tutu file")
FileUtil.writeToFile(new File(toto2, "totototo.txt"), "Toto/Toto file")
@@ -202,6 +207,60 @@ class SFTPClientSpec extends Specification {
attrs.type == FileMode.Type.DIRECTORY
}
def "should support premature termination of listing"() {
given:
SSHClient sshClient = fixture.setupConnectedDefaultClient()
sshClient.authPassword("test", "test")
SFTPClient sftpClient = sshClient.newSFTPClient()
final Path source = Files.createDirectory(temp.resolve("source")).toAbsolutePath()
final Path destination = Files.createDirectory(temp.resolve("destination")).toAbsolutePath()
final Path firstFile = Files.writeString(source.resolve("a_first.txt"), "first")
final Path secondFile = Files.writeString(source.resolve("b_second.txt"), "second")
final Path thirdFile = Files.writeString(source.resolve("c_third.txt"), "third")
final Path fourthFile = Files.writeString(source.resolve("d_fourth.txt"), "fourth")
sftpClient.put(firstFile.toString(), destination.resolve(firstFile.fileName).toString())
sftpClient.put(secondFile.toString(), destination.resolve(secondFile.fileName).toString())
sftpClient.put(thirdFile.toString(), destination.resolve(thirdFile.fileName).toString())
sftpClient.put(fourthFile.toString(), destination.resolve(fourthFile.fileName).toString())
def filesListed = 0
RemoteResourceInfo expectedFile = null
RemoteResourceSelector limitingSelector = new RemoteResourceSelector() {
@Override
RemoteResourceSelector.Result select(RemoteResourceInfo resource) {
filesListed += 1
switch(filesListed) {
case 1:
return RemoteResourceSelector.Result.CONTINUE
case 2:
expectedFile = resource
return RemoteResourceSelector.Result.ACCEPT
case 3:
return RemoteResourceSelector.Result.BREAK
default:
throw new AssertionError((Object) "Should NOT select any more resources")
}
}
}
when:
def listingResult = sftpClient
.ls(destination.toString(), limitingSelector);
then:
// first should be skipped by CONTINUE
listingResult.contains(expectedFile) // second should be included by ACCEPT
// third should be skipped by BREAK
// fourth should be skipped by preceding BREAK
listingResult.size() == 1
cleanup:
sftpClient.close()
sshClient.disconnect()
}
private void doUpload(File src, File dest) throws IOException {
SSHClient sshClient = fixture.setupConnectedDefaultClient()
sshClient.authPassword("test", "test")

View File

@@ -1,59 +0,0 @@
/*
* Copyright (C)2009 - SSHJ Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hierynomus.sshj.transport.verification
import spock.lang.Specification
import spock.lang.Unroll
class KnownHostMatchersSpec extends Specification {
@Unroll
def "should #yesno match host #host with pattern #pattern"() {
given:
def matcher = KnownHostMatchers.createMatcher(pattern)
expect:
match == matcher.match(host)
where:
pattern | host | match
"aaa.bbb.com" | "aaa.bbb.com" | true
"aaa.bbb.com" | "aaa.ccc.com" | false
"*.bbb.com" | "aaa.bbb.com" | true
"*.bbb.com" | "aaa.ccc.com" | false
"aaa.*.com" | "aaa.bbb.com" | true
"aaa.*.com" | "aaa.ccc.com" | true
"aaa.bbb.*" | "aaa.bbb.com" | true
"aaa.bbb.*" | "aaa.ccc.com" | false
"!*.bbb.com" | "aaa.bbb.com" | false
"!*.bbb.com" | "aaa.ccc.com" | true
"aaa.bbb.com,!*.ccc.com" | "xxx.yyy.com" | true
"aaa.bbb.com,!*.ccc.com" | "aaa.bbb.com" | true
"aaa.bbb.com,!*.ccc.com" | "aaa.ccc.com" | false
"aaa.b??.com" | "aaa.bbb.com" | true
"aaa.b??.com" | "aaa.bcd.com" | true
"aaa.b??.com" | "aaa.ccd.com" | false
"aaa.b??.com" | "aaa.bccd.com" | false
"|1|F1E1KeoE/eEWhi10WpGv4OdiO6Y=|3988QV0VE8wmZL7suNrYQLITLCg=" | "192.168.1.61" | true
"|1|F1E1KeoE/eEWhi10WpGv4OdiO6Y=|3988QV0VE8wmZL7suNrYQLITLCg=" | "192.168.2.61" | false
"[aaa.bbb.com]:2222" | "aaa.bbb.com" | false
"[aaa.bbb.com]:2222" | "[aaa.bbb.com]:2222" | true
"[aaa.?bb.com]:2222" | "[aaa.dbb.com]:2222" | true
"[aaa.?xb.com]:2222" | "[aaa.dbb.com]:2222" | false
"[*.bbb.com]:2222" | "[aaa.bbb.com]:2222" | true
yesno = match ? "" : "no"
}
}

View File

@@ -1,223 +0,0 @@
/*
* Copyright (C)2009 - SSHJ Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hierynomus.sshj.transport.verification
import net.schmizz.sshj.common.Base64
import net.schmizz.sshj.common.Buffer
import net.schmizz.sshj.transport.verification.OpenSSHKnownHosts
import net.schmizz.sshj.util.KeyUtil
import org.junit.Rule
import org.junit.rules.TemporaryFolder
import spock.lang.Specification
import spock.lang.Unroll
import java.security.PublicKey
class OpenSSHKnownHostsSpec extends Specification {
@Rule
public TemporaryFolder temp = new TemporaryFolder();
def "should parse and verify hashed host entry"() {
given:
def f = knownHosts("|1|F1E1KeoE/eEWhi10WpGv4OdiO6Y=|3988QV0VE8wmZL7suNrYQLITLCg= ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA6P9Hlwdahh250jGZYKg2snRq2j2lFJVdKSHyxqbJiVy9VX9gTkN3K2MD48qyrYLYOyGs3vTttyUk+cK++JMzURWsrP4piby7LpeOT+3Iq8CQNj4gXZdcH9w15Vuk2qS11at6IsQPVHpKD9HGg9//EFUccI/4w06k4XXLm/IxOGUwj6I2AeWmEOL3aDi+fe07TTosSdLUD6INtR0cyKsg0zC7Da24ixoShT8Oy3x2MpR7CY3PQ1pUVmvPkr79VeA+4qV9F1JM09WdboAMZgWQZ+XrbtuBlGsyhpUHSCQOya+kOJ+bYryS+U7A+6nmTW3C9FX4FgFqTF89UHOC7V0zZQ==");
final PublicKey key = KeyUtil
.newRSAPublicKey(
"e8ff4797075a861db9d2319960a836b2746ada3da514955d2921f2c6a6c9895cbd557f604e43772b6303e3cab2ad82d83b21acdef4edb72524f9c2bef893335115acacfe2989bcbb2e978e4fedc8abc090363e205d975c1fdc35e55ba4daa4b5d5ab7a22c40f547a4a0fd1c683dfff10551c708ff8c34ea4e175cb9bf2313865308fa23601e5a610e2f76838be7ded3b4d3a2c49d2d40fa20db51d1cc8ab20d330bb0dadb88b1a12853f0ecb7c7632947b098dcf435a54566bcf92befd55e03ee2a57d17524cd3d59d6e800c66059067e5eb6edb81946b3286950748240ec9afa4389f9b62bc92f94ec0fba9e64d6dc2f455f816016a4c5f3d507382ed5d3365",
"23");
when:
OpenSSHKnownHosts openSSHKnownHosts = new OpenSSHKnownHosts(f)
then:
openSSHKnownHosts.verify("192.168.1.61", 22, key)
!openSSHKnownHosts.verify("192.168.1.2", 22, key)
}
def "should parse and verify v1 host entry"() {
given:
def f = knownHosts("test.com,1.1.1.1 2048 35 22017496617994656680820635966392838863613340434802393112245951008866692373218840197754553998457793202561151141246686162285550121243768846314646395880632789308110750881198697743542374668273149584280424505890648953477691795864456749782348425425954366277600319096366690719901119774784695056100331902394094537054256611668966698242432417382422091372756244612839068092471592121759862971414741954991375710930168229171638843329213652899594987626853020377726482288618521941129157643483558764875338089684351824791983007780922947554898825663693324944982594850256042689880090306493029526546183035567296830604572253312294059766327")
def key = KeyUtil.newRSAPublicKey("ae6983ed63a33afc69fe0b88b4ba14393120a0b66e1460916a8390ff109139cd14f4e1701ab5c5feeb479441fe2091d04c0ba7d3fa1756b80ed103657ab53b5d7daa38af22f59f9cbfc16892d4ef1f8fd3ae49663c295be1f568a160d54328fbc2c0598f48d32296b1b9942336234952c440cda1bfac904e3391db98e52f9b1de229adc18fc34a9a569717aa9a5b1145e73b8a8394354028d02054ca760243fb8fc1575490607dd098e698e02b5d8bdf22d55ec958245222ef4c65b8836b9f13674a2d2895a587bfd4423b4eeb6d3ef98451640e3d63d2fc6a761ffd34446abab028494caf36d67ffd65298d69f19f2d90bae4c207b671db563a08f1bb9bf237",
"23")
when:
OpenSSHKnownHosts knownHosts = new OpenSSHKnownHosts(f)
then:
knownHosts.verify("test.com", 22, key)
}
def "should check all host entries for key"() {
given:
def f = knownHosts("""
host1 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBCiYp2IDgzDFhl8T4TRLIhEljvEixz1YN0XWh4dYh0REGK9T4QKiyb28EztPMdcOtz1uyX5rUGYXX9hj99S4SiU=
host1 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBLTjA7hduYGmvV9smEEsIdGLdghSPD7kL8QarIIOkeXmBh+LTtT/T1K+Ot/rmXCZsP8hoUXxbvN+Tks440Ci0ck=
""")
def pk = new Buffer.PlainBuffer(Base64.decode("AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBLTjA7hduYGmvV9smEEsIdGLdghSPD7kL8QarIIOkeXmBh+LTtT/T1K+Ot/rmXCZsP8hoUXxbvN+Tks440Ci0ck=")).readPublicKey()
when:
def knownhosts = new OpenSSHKnownHosts(f)
then:
knownhosts.verify("host1", 22, pk)
}
def "should not fail on bad base64 entry"() {
given:
def f = knownHosts("""
host1 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTIDgzDFhl8T4TRLIhEljvEixz1YN0XWh4dYh0REGK9T4QKiyb28EztPMdcOtz1uyX5rUGYXX9hj99S4SiU=
host1 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBLTjA7hduYGmvV9smEEsIdGLdghSPD7kL8QarIIOkeXmBh+LTtT/T1K+Ot/rmXCZsP8hoUXxbvN+Tks440Ci0ck=
""")
def pk = new Buffer.PlainBuffer(Base64.decode("AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBLTjA7hduYGmvV9smEEsIdGLdghSPD7kL8QarIIOkeXmBh+LTtT/T1K+Ot/rmXCZsP8hoUXxbvN+Tks440Ci0ck=")).readPublicKey()
when:
def knownhosts = new OpenSSHKnownHosts(f)
then:
knownhosts.verify("host1", 22, pk)
}
def "should mark bad line and not fail"() {
given:
def f = knownHosts("M36Lo+Ik5ukNugvvoNFlpnyiHMmtKxt3FpyEfYuryXjNqMNWHn/ARVnpUIl5jRLTB7WBzyLYMG7X5nuoFL9zYqKGtHxChbDunxMVbspw5WXI9VN+qxcLwmITmpEvI9ApyS/Ox2ZyN7zw==\n")
when:
def knownhosts = new OpenSSHKnownHosts(f)
then:
knownhosts.entries().size() == 1
knownhosts.entries().get(0) instanceof OpenSSHKnownHosts.BadHostEntry
}
@Unroll
def "should add comment for #type line"() {
given:
def f = knownHosts(s)
when:
def knownHosts = new OpenSSHKnownHosts(f)
then:
knownHosts.entries().size() == 1
knownHosts.entries().get(0) instanceof OpenSSHKnownHosts.CommentEntry
where:
type << ["newline", "comment"]
s << ["\n", "#comment\n"]
}
@Unroll
def "should contain comment at end of line"() {
given:
def f = knownHosts(host)
when:
OpenSSHKnownHosts knownHosts = new OpenSSHKnownHosts(f)
then:
knownHosts.entries().size() == 1
def entry = knownHosts.entries().get(0)
entry instanceof OpenSSHKnownHosts.HostEntry
entry.comment == comment
where:
host << [
"|1|F1E1KeoE/eEWhi10WpGv4OdiO6Y=|3988QV0VE8wmZL7suNrYQLITLCg= ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA6P9Hlwdahh250jGZYKg2snRq2j2lFJVdKSHyxqbJiVy9VX9gTkN3K2MD48qyrYLYOyGs3vTttyUk+cK++JMzURWsrP4piby7LpeOT+3Iq8CQNj4gXZdcH9w15Vuk2qS11at6IsQPVHpKD9HGg9//EFUccI/4w06k4XXLm/IxOGUwj6I2AeWmEOL3aDi+fe07TTosSdLUD6INtR0cyKsg0zC7Da24ixoShT8Oy3x2MpR7CY3PQ1pUVmvPkr79VeA+4qV9F1JM09WdboAMZgWQZ+XrbtuBlGsyhpUHSCQOya+kOJ+bYryS+U7A+6nmTW3C9FX4FgFqTF89UHOC7V0zZQ== this is a comment",
"test.com,1.1.1.1 2048 35 22017496617994656680820635966392838863613340434802393112245951008866692373218840197754553998457793202561151141246686162285550121243768846314646395880632789308110750881198697743542374668273149584280424505890648953477691795864456749782348425425954366277600319096366690719901119774784695056100331902394094537054256611668966698242432417382422091372756244612839068092471592121759862971414741954991375710930168229171638843329213652899594987626853020377726482288618521941129157643483558764875338089684351824791983007780922947554898825663693324944982594850256042689880090306493029526546183035567296830604572253312294059766327 single",
"schmizz.net,69.163.155.180 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA6P9Hlwdahh250jGZYKg2snRq2j2lFJVdKSHyxqbJiVy9VX9gTkN3K2MD48qyrYLYOyGs3vTttyUk+cK++JMzURWsrP4piby7LpeOT+3Iq8CQNj4gXZdcH9w15Vuk2qS11at6IsQPVHpKD9HGg9//EFUccI/4w06k4XXLm/IxOGUwj6I2AeWmEOL3aDi+fe07TTosSdLUD6INtR0cyKsg0zC7Da24ixoShT8Oy3x2MpR7CY3PQ1pUVmvPkr79VeA+4qV9F1JM09WdboAMZgWQZ+XrbtuBlGsyhpUHSCQOya+kOJ+bYryS+U7A+6nmTW3C9FX4FgFqTF89UHOC7V0zZQ==",
"schmizz.net,69.163.155.180 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA6P9Hlwdahh250jGZYKg2snRq2j2lFJVdKSHyxqbJiVy9VX9gTkN3K2MD48qyrYLYOyGs3vTttyUk+cK++JMzURWsrP4piby7LpeOT+3Iq8CQNj4gXZdcH9w15Vuk2qS11at6IsQPVHpKD9HGg9//EFUccI/4w06k4XXLm/IxOGUwj6I2AeWmEOL3aDi+fe07TTosSdLUD6INtR0cyKsg0zC7Da24ixoShT8Oy3x2MpR7CY3PQ1pUVmvPkr79VeA+4qV9F1JM09WdboAMZgWQZ+XrbtuBlGsyhpUHSCQOya+kOJ+bYryS+U7A+6nmTW3C9FX4FgFqTF89UHOC7V0zZQ== ",
"schmizz.net,69.163.155.180 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA6P9Hlwdahh250jGZYKg2snRq2j2lFJVdKSHyxqbJiVy9VX9gTkN3K2MD48qyrYLYOyGs3vTttyUk+cK++JMzURWsrP4piby7LpeOT+3Iq8CQNj4gXZdcH9w15Vuk2qS11at6IsQPVHpKD9HGg9//EFUccI/4w06k4XXLm/IxOGUwj6I2AeWmEOL3aDi+fe07TTosSdLUD6INtR0cyKsg0zC7Da24ixoShT8Oy3x2MpR7CY3PQ1pUVmvPkr79VeA+4qV9F1JM09WdboAMZgWQZ+XrbtuBlGsyhpUHSCQOya+kOJ+bYryS+U7A+6nmTW3C9FX4FgFqTF89UHOC7V0zZQ== extra space"
]
comment << [
"this is a comment",
"single",
null,
null,
"extra space"
]
}
@Unroll
def "should match any host name from multi-host line"() {
given:
def f = knownHosts("schmizz.net,69.163.155.180 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA6P9Hlwdahh250jGZYKg2snRq2j2lFJVdKSHyxqbJiVy9VX9gTkN3K2MD48qyrYLYOyGs3vTttyUk+cK++JMzURWsrP4piby7LpeOT+3Iq8CQNj4gXZdcH9w15Vuk2qS11at6IsQPVHpKD9HGg9//EFUccI/4w06k4XXLm/IxOGUwj6I2AeWmEOL3aDi+fe07TTosSdLUD6INtR0cyKsg0zC7Da24ixoShT8Oy3x2MpR7CY3PQ1pUVmvPkr79VeA+4qV9F1JM09WdboAMZgWQZ+XrbtuBlGsyhpUHSCQOya+kOJ+bYryS+U7A+6nmTW3C9FX4FgFqTF89UHOC7V0zZQ==")
def pk = new Buffer.PlainBuffer(Base64.decode("AAAAB3NzaC1yc2EAAAABIwAAAQEA6P9Hlwdahh250jGZYKg2snRq2j2lFJVdKSHyxqbJiVy9VX9gTkN3K2MD48qyrYLYOyGs3vTttyUk+cK++JMzURWsrP4piby7LpeOT+3Iq8CQNj4gXZdcH9w15Vuk2qS11at6IsQPVHpKD9HGg9//EFUccI/4w06k4XXLm/IxOGUwj6I2AeWmEOL3aDi+fe07TTosSdLUD6INtR0cyKsg0zC7Da24ixoShT8Oy3x2MpR7CY3PQ1pUVmvPkr79VeA+4qV9F1JM09WdboAMZgWQZ+XrbtuBlGsyhpUHSCQOya+kOJ+bYryS+U7A+6nmTW3C9FX4FgFqTF89UHOC7V0zZQ==")).readPublicKey()
when:
def knownHosts = new OpenSSHKnownHosts(f)
then:
knownHosts.verify(h, 22, pk)
where:
h << ["schmizz.net", "69.163.155.180"]
}
def "should produce meaningful toString()"() {
given:
def f = knownHosts("schmizz.net,69.163.155.180 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA6P9Hlwdahh250jGZYKg2snRq2j2lFJVdKSHyxqbJiVy9VX9gTkN3K2MD48qyrYLYOyGs3vTttyUk+cK++JMzURWsrP4piby7LpeOT+3Iq8CQNj4gXZdcH9w15Vuk2qS11at6IsQPVHpKD9HGg9//EFUccI/4w06k4XXLm/IxOGUwj6I2AeWmEOL3aDi+fe07TTosSdLUD6INtR0cyKsg0zC7Da24ixoShT8Oy3x2MpR7CY3PQ1pUVmvPkr79VeA+4qV9F1JM09WdboAMZgWQZ+XrbtuBlGsyhpUHSCQOya+kOJ+bYryS+U7A+6nmTW3C9FX4FgFqTF89UHOC7V0zZQ==")
when:
def knownhosts = new OpenSSHKnownHosts(f)
def toStringValue = knownhosts.toString()
then:
toStringValue == "OpenSSHKnownHosts{khFile='" + f + "'}"
}
def "should forgive redundant spaces like OpenSSH does"() {
given:
def key = "AAAAC3NzaC1lZDI1NTE5AAAAIIRsJi92NJJTQwXHZiRiARoEy4n1jYsNTQePHFTSl7tG"
def f = knownHosts("""
|host1 ssh-ed25519 $key
|
| host2 ssh-ed25519 $key ,./gargage\\.,
|\t\t\t\t\t
|\t@revoked host3\tssh-ed25519\t \t$key\t
""".stripMargin())
def pk = new Buffer.PlainBuffer(Base64.decode(key)).readPublicKey()
when:
def knownhosts = new OpenSSHKnownHosts(f)
then:
["host1", "host2", "host3"].forEach {
knownhosts.verify(it, 22, pk)
}
}
def "should not throw errors while parsing corrupted records"() {
given:
def key = "AAAAC3NzaC1lZDI1NTE5AAAAIIRsJi92NJJTQwXHZiRiARoEy4n1jYsNTQePHFTSl7tG"
def f = knownHosts(
"\n" // empty line
+ " \n" // blank line
+ "bad-host1\n" // absent key type and key contents
+ "bad-host2 ssh-ed25519\n" // absent key contents
+ " bad-host3 ssh-ed25519\n" // absent key contents, with leading spaces
+ "@revoked bad-host5 ssh-ed25519\n" // absent key contents, with marker
+ "good-host ssh-ed25519 $key" // the only good host at the end
)
when:
def knownhosts = new OpenSSHKnownHosts(f)
then:
knownhosts.verify("good-host", 22, new Buffer.PlainBuffer(Base64.decode(key)).readPublicKey())
}
def knownHosts(String s) {
def f = temp.newFile("known_hosts")
f.write(s)
return f
}
}

Some files were not shown because too many files have changed in this diff Show More