mirror of
https://github.com/hierynomus/sshj.git
synced 2025-12-06 15:20:54 +03:00
Compare commits
78 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4be5228a4d | ||
|
|
1398b190ec | ||
|
|
acad163e50 | ||
|
|
8e9e644bb8 | ||
|
|
857d56a679 | ||
|
|
0e4a8f675f | ||
|
|
95aab0088e | ||
|
|
e390394e3b | ||
|
|
995de2da99 | ||
|
|
cea67fef73 | ||
|
|
b4bc69626e | ||
|
|
27bf52ec10 | ||
|
|
11921e2d3a | ||
|
|
4fe605289b | ||
|
|
0816bf95af | ||
|
|
b886085da5 | ||
|
|
c3236a7405 | ||
|
|
7f8f43c8ae | ||
|
|
5ff27ec597 | ||
|
|
bfa82b4e44 | ||
|
|
31ed35407c | ||
|
|
f4f8071020 | ||
|
|
f525ed0e5b | ||
|
|
93046f315e | ||
|
|
54376b7622 | ||
|
|
f0e92c920f | ||
|
|
09e2ca512e | ||
|
|
607e80591c | ||
|
|
079cb08fb0 | ||
|
|
cf340c2a09 | ||
|
|
586a66420e | ||
|
|
624fe839cb | ||
|
|
81d77d277c | ||
|
|
70af58d199 | ||
|
|
c0d1519ee2 | ||
|
|
03f8b2224d | ||
|
|
f94444bc53 | ||
|
|
dc6b20772b | ||
|
|
81e87a4d35 | ||
|
|
a262f51900 | ||
|
|
50c753dc58 | ||
|
|
1c547886c8 | ||
|
|
b7dc869a13 | ||
|
|
4774721b49 | ||
|
|
542bb35bda | ||
|
|
3b67d2b476 | ||
|
|
9b9b208434 | ||
|
|
a3cce0d2f9 | ||
|
|
5d040dd4bb | ||
|
|
461c0e46d4 | ||
|
|
f4d34d899d | ||
|
|
2bef99c875 | ||
|
|
a186dbf0bc | ||
|
|
a5fdb29fad | ||
|
|
3069138482 | ||
|
|
a3c9c61a09 | ||
|
|
31d156b19f | ||
|
|
ec69d109e8 | ||
|
|
f35c2bd4ce | ||
|
|
07837098eb | ||
|
|
39a7be9221 | ||
|
|
e7614db94a | ||
|
|
233c0dcaa6 | ||
|
|
0d16fbe146 | ||
|
|
154a202384 | ||
|
|
830a39dc24 | ||
|
|
dcfa1833d7 | ||
|
|
6e7fb96d07 | ||
|
|
d5d6096d5d | ||
|
|
2551f8e559 | ||
|
|
430cbfcf13 | ||
|
|
ec467a3875 | ||
|
|
1b258f0677 | ||
|
|
559384ac91 | ||
|
|
5674072666 | ||
|
|
f33bfecbf5 | ||
|
|
c0f6000ff5 | ||
|
|
3de0302c84 |
1
.gitattributes
vendored
1
.gitattributes
vendored
@@ -1 +1,2 @@
|
||||
*.bat text eol=crlf
|
||||
src/itest/docker-image/** eol=lf
|
||||
|
||||
22
.github/workflows/gradle.yml
vendored
22
.github/workflows/gradle.yml
vendored
@@ -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
|
||||
|
||||
14
.github/workflows/release.yml
vendored
14
.github/workflows/release.yml
vendored
@@ -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 }}
|
||||
|
||||
|
||||
|
||||
61
README.adoc
61
README.adoc
@@ -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,55 @@ Issue tracker: https://github.com/hierynomus/sshj/issues
|
||||
Fork away!
|
||||
|
||||
== Release history
|
||||
SSHJ 0.39.0 (2024-02-20)::
|
||||
* Upgraded dependencies
|
||||
* Remove hard dependencies on BouncyCastle, making it optional.
|
||||
* Merged https://github.com/hierynomus/sshj/pull/993[#993]: Remove EDDSA dependency
|
||||
* Merged https://github.com/hierynomus/sshj/pull/959[#959]: Improve Curve25519 public key handling
|
||||
* Merged https://github.com/hierynomus/sshj/pull/911[#911]: Fix for bad packet received with heartbeat enabled
|
||||
* Merged https://github.com/hierynomus/sshj/pull/926[#926]: Close session when closing SFTP client
|
||||
* Merged https://github.com/hierynomus/sshj/pull/928[#928]: Improve file-listing performance
|
||||
* Merged https://github.com/hierynomus/sshj/pull/934[#934]: Don't send keep-alive before KEX done
|
||||
* Merged https://github.com/hierynomus/sshj/pull/936[#936]: Improve Base64 decoding error handling
|
||||
* Merged https://github.com/hierynomus/sshj/pull/925[#925]: Allow passing connected sockets
|
||||
* Merged https://github.com/hierynomus/sshj/pull/922[#922]: Fix bug in known_hosts parsing
|
||||
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
|
||||
* Merged https://github.com/hierynomus/sshj/pull/793[#793]: Merge PKCS5 and PKCS8 classes
|
||||
* Upgraded dependencies SLF4J (1.7.36) and Logback (1.2.11)
|
||||
* Merged https://github.com/hierynomus/sshj/pull/791[#791]: Update KeepAlive examples
|
||||
* Merged https://github.com/hierynomus/sshj/pull/775[#775]: Add SFTP resume support
|
||||
SSHJ 0.33.0 (2022-04-22)::
|
||||
* Upgraded dependencies BouncyCastle (1.70)
|
||||
* Merged https://github.com/hierynomus/sshj/pull/687[#687]: Correctly close connection when remote closes connection.
|
||||
|
||||
@@ -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"
|
||||
|
||||
204
build.gradle
204
build.gradle
@@ -1,24 +1,31 @@
|
||||
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 '2.0.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}"
|
||||
github {
|
||||
user 'hierynomus'
|
||||
license 'Apache'
|
||||
}
|
||||
|
||||
scmVersion {
|
||||
tag {
|
||||
@@ -33,30 +40,20 @@ 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.80"
|
||||
def sshdVersion = "2.15.0"
|
||||
|
||||
dependencies {
|
||||
implementation "org.slf4j:slf4j-api:1.7.32"
|
||||
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.17"
|
||||
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.9"
|
||||
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 {
|
||||
@@ -65,7 +62,15 @@ license {
|
||||
mapping {
|
||||
java = 'SLASHSTAR_STYLE'
|
||||
}
|
||||
excludes(['**/djb/Curve25519.java', '**/sshj/common/Base64.java', '**/com/hierynomus/sshj/userauth/keyprovider/bcrypt/*.java'])
|
||||
excludes([
|
||||
'**/com/hierynomus/sshj/userauth/keyprovider/bcrypt/*.java',
|
||||
'**/files/test_file_*.txt',
|
||||
])
|
||||
}
|
||||
|
||||
java {
|
||||
withJavadocJar()
|
||||
withSourcesJar()
|
||||
}
|
||||
|
||||
if (!JavaVersion.current().isJava9Compatible()) {
|
||||
@@ -79,10 +84,81 @@ if (JavaVersion.current().isJava8Compatible()) {
|
||||
}
|
||||
}
|
||||
|
||||
compileJava {
|
||||
options.compilerArgs.addAll(['--release', '7'])
|
||||
testing {
|
||||
suites {
|
||||
configureEach {
|
||||
useJUnitJupiter()
|
||||
dependencies {
|
||||
implementation "org.slf4j:slf4j-api:2.0.17"
|
||||
implementation 'org.spockframework:spock-core:2.3-groovy-3.0'
|
||||
implementation "org.mockito:mockito-core:5.16.1"
|
||||
implementation "org.assertj:assertj-core:3.27.3"
|
||||
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.5.18"
|
||||
}
|
||||
|
||||
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 platform('org.testcontainers:testcontainers-bom:1.20.6')
|
||||
implementation 'org.testcontainers:junit-jupiter'
|
||||
}
|
||||
|
||||
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()
|
||||
@@ -104,8 +180,6 @@ jar {
|
||||
instruction "Import-Package", "!net.schmizz.*"
|
||||
instruction "Import-Package", "!com.hierynomus.sshj.*"
|
||||
instruction "Import-Package", "javax.crypto*"
|
||||
instruction "Import-Package", "!net.i2p.crypto.eddsa.math"
|
||||
instruction "Import-Package", "net.i2p*"
|
||||
instruction "Import-Package", "com.jcraft.jzlib*;version=\"[1.1,2)\";resolution:=optional"
|
||||
instruction "Import-Package", "org.slf4j*;version=\"[1.7,5)\""
|
||||
instruction "Import-Package", "org.bouncycastle*;resolution:=optional"
|
||||
@@ -116,12 +190,6 @@ jar {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
java {
|
||||
withJavadocJar()
|
||||
withSourcesJar()
|
||||
}
|
||||
|
||||
sourcesJar {
|
||||
manifest {
|
||||
attributes(
|
||||
@@ -135,58 +203,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'
|
||||
}
|
||||
|
||||
publishing {
|
||||
publications {
|
||||
maven(MavenPublication) {
|
||||
@@ -267,17 +283,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)
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
<groupId>com.hierynomus</groupId>
|
||||
<artifactId>sshj-examples</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
<version>0.19.1</version>
|
||||
<version>0.37.0</version>
|
||||
|
||||
<name>sshj-examples</name>
|
||||
<description>Examples for SSHv2 library for Java</description>
|
||||
@@ -55,7 +55,7 @@
|
||||
<dependency>
|
||||
<groupId>com.hierynomus</groupId>
|
||||
<artifactId>sshj</artifactId>
|
||||
<version>0.31.0</version>
|
||||
<version>0.33.0</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
|
||||
@@ -19,8 +19,9 @@ public class KeepAlive {
|
||||
final SSHClient ssh = new SSHClient(defaultConfig);
|
||||
try {
|
||||
ssh.addHostKeyVerifier(new PromiscuousVerifier());
|
||||
// Set interval to enable keep-alive before connecting
|
||||
ssh.getConnection().getKeepAlive().setKeepAliveInterval(5);
|
||||
ssh.connect(args[0]);
|
||||
ssh.getConnection().getKeepAlive().setKeepAliveInterval(5); //every 60sec
|
||||
ssh.authPassword(args[1], args[2]);
|
||||
Session session = ssh.startSession();
|
||||
session.allocateDefaultPTY();
|
||||
|
||||
@@ -19,6 +19,7 @@ public class RemotePF {
|
||||
client.loadKnownHosts();
|
||||
|
||||
client.connect("localhost");
|
||||
client.getConnection().getKeepAlive().setKeepAliveInterval(5);
|
||||
try {
|
||||
|
||||
client.authPublickey(System.getProperty("user.name"));
|
||||
@@ -33,8 +34,6 @@ public class RemotePF {
|
||||
// what we do with incoming connections that are forwarded to us
|
||||
new SocketForwardingConnectListener(new InetSocketAddress("google.com", 80)));
|
||||
|
||||
client.getTransport().setHeartbeatInterval(30);
|
||||
|
||||
// Something to hang on to so that the forwarding stays
|
||||
client.getTransport().join();
|
||||
|
||||
|
||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
4
gradle/wrapper/gradle-wrapper.properties
vendored
4
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,5 +1,7 @@
|
||||
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.13-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
296
gradlew
vendored
296
gradlew
vendored
@@ -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.
|
||||
@@ -15,80 +15,115 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
##
|
||||
## 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/HEAD/platforms/jvm/plugins-application/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_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$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"'
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||
|
||||
# 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,87 +132,120 @@ Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
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.
|
||||
JAVACMD=java
|
||||
if ! command -v java >/dev/null 2>&1
|
||||
then
|
||||
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
|
||||
location of your Java installation."
|
||||
fi
|
||||
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*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
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
|
||||
|
||||
|
||||
# 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"'
|
||||
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
if ! command -v xargs >/dev/null 2>&1
|
||||
then
|
||||
die "xargs is not available"
|
||||
fi
|
||||
|
||||
# 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" "$@"
|
||||
|
||||
57
gradlew.bat
vendored
57
gradlew.bat
vendored
@@ -13,8 +13,10 @@
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
@rem SPDX-License-Identifier: Apache-2.0
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%" == "" @echo off
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@@ -25,7 +27,8 @@
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@rem This is normally unused
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@@ -40,13 +43,13 @@ if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto init
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
@@ -54,48 +57,36 @@ 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%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
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
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
7
src/itest/docker-image/entrypoint.sh
Normal file
7
src/itest/docker-image/entrypoint.sh
Normal 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 "$@"
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
72
src/itest/java/com/hierynomus/sshj/HostKeyVerifierTest.java
Normal file
72
src/itest/java/com/hierynomus/sshj/HostKeyVerifierTest.java
Normal 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());
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
74
src/itest/java/com/hierynomus/sshj/ManyChannelsTest.java
Normal file
74
src/itest/java/com/hierynomus/sshj/ManyChannelsTest.java
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
86
src/itest/java/com/hierynomus/sshj/PublicKeyAuthTest.java
Normal file
86
src/itest/java/com/hierynomus/sshj/PublicKeyAuthTest.java
Normal 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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
100
src/itest/java/com/hierynomus/sshj/RsaShaKeySignatureTest.java
Normal file
100
src/itest/java/com/hierynomus/sshj/RsaShaKeySignatureTest.java
Normal 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();
|
||||
});
|
||||
}
|
||||
}
|
||||
246
src/itest/java/com/hierynomus/sshj/SshdContainer.java
Normal file
246
src/itest/java/com/hierynomus/sshj/SshdContainer.java
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
79
src/itest/java/com/hierynomus/sshj/sftp/FileWriteTest.java
Normal file
79
src/itest/java/com/hierynomus/sshj/sftp/FileWriteTest.java
Normal file
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* 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 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;
|
||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
|
||||
@Testcontainers
|
||||
public class FileWriteTest {
|
||||
@Container
|
||||
private static final 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);
|
||||
|
||||
final byte[] expectedInitialText = new byte[initialText.length];
|
||||
System.arraycopy(readBytes, 0, expectedInitialText, 0, expectedInitialText.length);
|
||||
assertArrayEquals(expectedInitialText, initialText);
|
||||
|
||||
final byte[] expectedAppendText = new byte[appendText.length];
|
||||
System.arraycopy(readBytes, initialText.length, expectedAppendText, 0, expectedAppendText.length);
|
||||
assertArrayEquals(expectedAppendText, appendText);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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/");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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).putString(""));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
34
src/itest/resources/logback-test.xml
Normal file
34
src/itest/resources/logback-test.xml
Normal 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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,8 +15,6 @@
|
||||
*/
|
||||
package com.hierynomus.sshj.common;
|
||||
|
||||
import org.bouncycastle.openssl.EncryptionException;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
@@ -28,11 +26,15 @@ public class KeyDecryptionFailedException extends IOException {
|
||||
|
||||
public static final String MESSAGE = "Decryption of the key failed. A supplied passphrase may be incorrect.";
|
||||
|
||||
public KeyDecryptionFailedException() {
|
||||
super(MESSAGE);
|
||||
public KeyDecryptionFailedException(final String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public KeyDecryptionFailedException(EncryptionException cause) {
|
||||
public KeyDecryptionFailedException(final String message, final Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public KeyDecryptionFailedException(IOException cause) {
|
||||
super(MESSAGE, cause);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -1,56 +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 net.i2p.crypto.eddsa.EdDSAPublicKey;
|
||||
import net.i2p.crypto.eddsa.spec.EdDSANamedCurveSpec;
|
||||
import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable;
|
||||
import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec;
|
||||
import net.schmizz.sshj.common.SSHRuntimeException;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Our own extension of the EdDSAPublicKey that comes from ECC-25519, as that class does not implement equality.
|
||||
* The code uses the equality of the keys as an indicator whether they're the same during host key verification.
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
public class Ed25519PublicKey extends EdDSAPublicKey {
|
||||
|
||||
public Ed25519PublicKey(EdDSAPublicKeySpec spec) {
|
||||
super(spec);
|
||||
|
||||
EdDSANamedCurveSpec ed25519 = EdDSANamedCurveTable.getByName("Ed25519");
|
||||
if (!spec.getParams().getCurve().equals(ed25519.getCurve())) {
|
||||
throw new SSHRuntimeException("Cannot create Ed25519 Public Key from wrong spec");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
if (!(other instanceof Ed25519PublicKey)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Ed25519PublicKey otherKey = (Ed25519PublicKey) other;
|
||||
return Arrays.equals(getAbyte(), otherKey.getAbyte());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return getA().hashCode();
|
||||
}
|
||||
}
|
||||
@@ -15,14 +15,14 @@
|
||||
*/
|
||||
package com.hierynomus.sshj.signature;
|
||||
|
||||
import net.i2p.crypto.eddsa.EdDSAEngine;
|
||||
import net.schmizz.sshj.common.KeyType;
|
||||
import net.schmizz.sshj.common.SSHRuntimeException;
|
||||
import net.schmizz.sshj.common.SecurityUtils;
|
||||
import net.schmizz.sshj.signature.AbstractSignature;
|
||||
import net.schmizz.sshj.signature.Signature;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.NoSuchProviderException;
|
||||
import java.security.SignatureException;
|
||||
|
||||
public class SignatureEdDSA extends AbstractSignature {
|
||||
@@ -43,11 +43,11 @@ public class SignatureEdDSA extends AbstractSignature {
|
||||
super(getEngine(), KeyType.ED25519.toString());
|
||||
}
|
||||
|
||||
private static EdDSAEngine getEngine() {
|
||||
private static java.security.Signature getEngine() {
|
||||
try {
|
||||
return new EdDSAEngine(MessageDigest.getInstance("SHA-512"));
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new SSHRuntimeException(e);
|
||||
return SecurityUtils.getSignature("Ed25519");
|
||||
} catch (NoSuchAlgorithmException | NoSuchProviderException e) {
|
||||
throw new SSHRuntimeException("Ed25519 Signatures not supported", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ public class IdentificationStringParser {
|
||||
private final Logger log;
|
||||
private final Buffer.PlainBuffer buffer;
|
||||
|
||||
private byte[] EXPECTED_START_BYTES = new byte[] {'S', 'S', 'H', '-'};
|
||||
private final byte[] EXPECTED_START_BYTES = new byte[] {'S', 'S', 'H', '-'};
|
||||
|
||||
public IdentificationStringParser(Buffer.PlainBuffer buffer) {
|
||||
this(buffer, LoggerFactory.DEFAULT);
|
||||
|
||||
@@ -121,11 +121,11 @@ public class BlockCiphers {
|
||||
public static class Factory
|
||||
implements net.schmizz.sshj.common.Factory.Named<Cipher> {
|
||||
|
||||
private int keysize;
|
||||
private String cipher;
|
||||
private String mode;
|
||||
private String name;
|
||||
private int ivsize;
|
||||
private final int keysize;
|
||||
private final String cipher;
|
||||
private final String mode;
|
||||
private final String name;
|
||||
private final int ivsize;
|
||||
|
||||
/**
|
||||
* @param ivsize
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -33,12 +33,12 @@ public class GcmCiphers {
|
||||
public static class Factory
|
||||
implements net.schmizz.sshj.common.Factory.Named<Cipher> {
|
||||
|
||||
private int keysize;
|
||||
private int authSize;
|
||||
private String cipher;
|
||||
private String mode;
|
||||
private String name;
|
||||
private int ivsize;
|
||||
private final int keysize;
|
||||
private final int authSize;
|
||||
private final String cipher;
|
||||
private final String mode;
|
||||
private final String name;
|
||||
private final int ivsize;
|
||||
|
||||
/**
|
||||
* @param ivsize
|
||||
|
||||
@@ -40,10 +40,10 @@ public class StreamCiphers {
|
||||
public static class Factory
|
||||
implements net.schmizz.sshj.common.Factory.Named<Cipher> {
|
||||
|
||||
private int keysize;
|
||||
private String cipher;
|
||||
private String mode;
|
||||
private String name;
|
||||
private final int keysize;
|
||||
private final String cipher;
|
||||
private final String mode;
|
||||
private final String name;
|
||||
|
||||
/**
|
||||
* @param keysize The keysize used in bits.
|
||||
|
||||
@@ -28,8 +28,8 @@ import java.security.GeneralSecurityException;
|
||||
*
|
||||
*/
|
||||
public class DHG extends AbstractDHG {
|
||||
private BigInteger group;
|
||||
private BigInteger generator;
|
||||
private final BigInteger group;
|
||||
private final BigInteger generator;
|
||||
|
||||
public DHG(BigInteger group, BigInteger generator, Digest digest) {
|
||||
super(new DH(), digest);
|
||||
|
||||
@@ -68,10 +68,10 @@ public class DHGroups {
|
||||
public static class Factory
|
||||
implements net.schmizz.sshj.common.Factory.Named<KeyExchange> {
|
||||
|
||||
private String name;
|
||||
private BigInteger group;
|
||||
private BigInteger generator;
|
||||
private Factory.Named<Digest> digestFactory;
|
||||
private final String name;
|
||||
private final BigInteger group;
|
||||
private final BigInteger generator;
|
||||
private final Factory.Named<Digest> digestFactory;
|
||||
|
||||
public Factory(String name, BigInteger group, BigInteger generator, Named<Digest> digestFactory) {
|
||||
this.name = name;
|
||||
@@ -89,6 +89,11 @@ public class DHGroups {
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -71,10 +71,10 @@ public class Macs {
|
||||
|
||||
public static class Factory implements net.schmizz.sshj.common.Factory.Named<MAC> {
|
||||
|
||||
private String name;
|
||||
private String algorithm;
|
||||
private int bSize;
|
||||
private int defBSize;
|
||||
private final String name;
|
||||
private final String algorithm;
|
||||
private final int bSize;
|
||||
private final int defBSize;
|
||||
private final boolean etm;
|
||||
|
||||
public Factory(String name, String algorithm, int bSize, int defBSize, boolean etm) {
|
||||
|
||||
@@ -15,20 +15,26 @@
|
||||
*/
|
||||
package com.hierynomus.sshj.transport.verification;
|
||||
|
||||
import net.schmizz.sshj.common.Base64;
|
||||
import net.schmizz.sshj.common.IOUtils;
|
||||
import net.schmizz.sshj.common.Base64DecodingException;
|
||||
import net.schmizz.sshj.common.Base64Decoder;
|
||||
import net.schmizz.sshj.common.SSHException;
|
||||
import net.schmizz.sshj.transport.mac.MAC;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
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);
|
||||
@@ -51,7 +57,7 @@ public class KnownHostMatchers {
|
||||
}
|
||||
|
||||
private static class EquiHostMatcher implements HostMatcher {
|
||||
private String host;
|
||||
private final String host;
|
||||
|
||||
public EquiHostMatcher(String host) {
|
||||
this.host = host;
|
||||
@@ -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(StandardCharsets.UTF_8)));
|
||||
}
|
||||
|
||||
private byte[] getSaltyBytes() throws IOException {
|
||||
private byte[] getSaltyBytes() throws IOException, Base64DecodingException {
|
||||
if (saltyBytes == null) {
|
||||
saltyBytes = Base64.decode(salt);
|
||||
saltyBytes = Base64Decoder.decode(salt);
|
||||
}
|
||||
return saltyBytes;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -46,17 +47,17 @@ public class OpenSSHKeyFileUtil {
|
||||
* @param publicKey Public key accessible through a {@code Reader}
|
||||
*/
|
||||
public static ParsedPubKey initPubKey(Reader publicKey) throws IOException {
|
||||
final BufferedReader br = new BufferedReader(publicKey);
|
||||
try {
|
||||
try (BufferedReader br = new BufferedReader(publicKey)) {
|
||||
String keydata;
|
||||
while ((keydata = br.readLine()) != null) {
|
||||
keydata = keydata.trim();
|
||||
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,8 +65,8 @@ public class OpenSSHKeyFileUtil {
|
||||
}
|
||||
}
|
||||
throw new IOException("Public key file is blank");
|
||||
} finally {
|
||||
br.close();
|
||||
} catch (Base64DecodingException err) {
|
||||
throw new IOException("Public key decoding failed", err);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,49 +18,69 @@ 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 net.i2p.crypto.eddsa.EdDSAPrivateKey;
|
||||
import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable;
|
||||
import net.i2p.crypto.eddsa.spec.EdDSAPrivateKeySpec;
|
||||
import com.hierynomus.sshj.transport.cipher.ChachaPolyCiphers;
|
||||
import com.hierynomus.sshj.transport.cipher.GcmCiphers;
|
||||
import com.hierynomus.sshj.userauth.keyprovider.bcrypt.BCrypt;
|
||||
import net.schmizz.sshj.common.*;
|
||||
import net.schmizz.sshj.common.Buffer.PlainBuffer;
|
||||
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 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 net.schmizz.sshj.userauth.password.PasswordFinder;
|
||||
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.security.spec.ECPrivateKeySpec;
|
||||
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.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
|
||||
* The format is described in the following document: <a href="https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.key">Key Protocol</a>
|
||||
*/
|
||||
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 +98,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 +176,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 IOException(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 +335,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 IOException("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:
|
||||
@@ -246,8 +348,14 @@ public class OpenSSHKeyV1KeyFile extends BaseFileKeyProvider {
|
||||
keyBuffer.readUInt32(); // length of privatekey+publickey
|
||||
byte[] privKey = new byte[32];
|
||||
keyBuffer.readRawBytes(privKey); // string privatekey
|
||||
keyBuffer.readRawBytes(new byte[32]); // string publickey (again...)
|
||||
kp = new KeyPair(publicKey, new EdDSAPrivateKey(new EdDSAPrivateKeySpec(privKey, EdDSANamedCurveTable.getByName("Ed25519"))));
|
||||
|
||||
final byte[] pubKey = new byte[32];
|
||||
keyBuffer.readRawBytes(pubKey); // string publickey (again...)
|
||||
|
||||
final PrivateKey edPrivateKey = Ed25519KeyFactory.getPrivateKey(privKey);
|
||||
final PublicKey edPublicKey = Ed25519KeyFactory.getPublicKey(pubKey);
|
||||
|
||||
kp = new KeyPair(edPublicKey, edPrivateKey);
|
||||
break;
|
||||
case RSA:
|
||||
final RSAPrivateCrtKeySpec rsaPrivateCrtKeySpec = readRsaPrivateKeySpec(keyBuffer);
|
||||
@@ -255,13 +363,13 @@ public class OpenSSHKeyV1KeyFile extends BaseFileKeyProvider {
|
||||
kp = new KeyPair(publicKey, privateKey);
|
||||
break;
|
||||
case ECDSA256:
|
||||
kp = new KeyPair(publicKey, createECDSAPrivateKey(kt, keyBuffer, "P-256"));
|
||||
kp = new KeyPair(publicKey, createECDSAPrivateKey(kt, keyBuffer, ECDSACurve.SECP256R1));
|
||||
break;
|
||||
case ECDSA384:
|
||||
kp = new KeyPair(publicKey, createECDSAPrivateKey(kt, keyBuffer, "P-384"));
|
||||
kp = new KeyPair(publicKey, createECDSAPrivateKey(kt, keyBuffer, ECDSACurve.SECP384R1));
|
||||
break;
|
||||
case ECDSA521:
|
||||
kp = new KeyPair(publicKey, createECDSAPrivateKey(kt, keyBuffer, "P-521"));
|
||||
kp = new KeyPair(publicKey, createECDSAPrivateKey(kt, keyBuffer, ECDSACurve.SECP521R1));
|
||||
break;
|
||||
|
||||
default:
|
||||
@@ -278,13 +386,10 @@ public class OpenSSHKeyV1KeyFile extends BaseFileKeyProvider {
|
||||
return kp;
|
||||
}
|
||||
|
||||
private PrivateKey createECDSAPrivateKey(KeyType kt, PlainBuffer buffer, String name) throws GeneralSecurityException, Buffer.BufferException {
|
||||
private PrivateKey createECDSAPrivateKey(KeyType kt, PlainBuffer buffer, ECDSACurve ecdsaCurve) throws GeneralSecurityException, Buffer.BufferException {
|
||||
kt.readPubKeyFromBuffer(buffer); // Public key
|
||||
BigInteger s = new BigInteger(1, buffer.readBytes());
|
||||
X9ECParameters ecParams = NISTNamedCurves.getByName(name);
|
||||
ECNamedCurveSpec ecCurveSpec = new ECNamedCurveSpec(name, ecParams.getCurve(), ecParams.getG(), ecParams.getN());
|
||||
ECPrivateKeySpec pks = new ECPrivateKeySpec(s, ecCurveSpec);
|
||||
return SecurityUtils.getKeyFactory(KeyAlgorithm.ECDSA).generatePrivate(pks);
|
||||
final BigInteger s = new BigInteger(1, buffer.readBytes());
|
||||
return ECDSAKeyFactory.getPrivateKey(s, ecdsaCurve);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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>
|
||||
* System.out.println("It matches");<br>
|
||||
* else<br>
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -176,6 +177,7 @@ public class Promise<V, T extends Throwable> {
|
||||
}
|
||||
return val;
|
||||
} catch (InterruptedException ie) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw chainer.chain(ie);
|
||||
} finally {
|
||||
lock.unlock();
|
||||
|
||||
@@ -29,6 +29,6 @@ final class Heartbeater
|
||||
|
||||
@Override
|
||||
protected void doKeepAlive() throws TransportException {
|
||||
conn.getTransport().write(new SSHPacket(Message.IGNORE));
|
||||
conn.getTransport().write(new SSHPacket(Message.IGNORE).putString(""));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,14 +35,29 @@ public abstract class KeepAlive extends Thread {
|
||||
setDaemon(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* KeepAlive enabled based on KeepAlive interval
|
||||
*
|
||||
* @return Enabled when KeepInterval is greater than 0
|
||||
*/
|
||||
public boolean isEnabled() {
|
||||
return keepAliveInterval > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get KeepAlive interval in seconds
|
||||
*
|
||||
* @return KeepAlive interval in seconds defaults to 0
|
||||
*/
|
||||
public synchronized int getKeepAliveInterval() {
|
||||
return keepAliveInterval;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set KeepAlive interval in seconds
|
||||
*
|
||||
* @param keepAliveInterval KeepAlive interval in seconds
|
||||
*/
|
||||
public synchronized void setKeepAliveInterval(int keepAliveInterval) {
|
||||
this.keepAliveInterval = keepAliveInterval;
|
||||
}
|
||||
@@ -60,6 +75,8 @@ public abstract class KeepAlive extends Thread {
|
||||
TimeUnit.SECONDS.sleep(interval);
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
// this is almost certainly a planned interruption, but even so, no harm in setting the interrupt flag
|
||||
Thread.currentThread().interrupt();
|
||||
log.trace("{} Interrupted while sleeping", getClass().getSimpleName());
|
||||
} catch (Exception e) {
|
||||
// If we weren't interrupted, kill the transport, then this exception was unexpected.
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -200,4 +200,8 @@ public interface Config {
|
||||
* See {@link #isVerifyHostKeyCertificates()}.
|
||||
*/
|
||||
void setVerifyHostKeyCertificates(boolean value);
|
||||
|
||||
int getMaxCircularBufferSize();
|
||||
|
||||
void setMaxCircularBufferSize(int maxCircularBufferSize);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -29,23 +29,24 @@ 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;
|
||||
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.random.BouncyCastleRandom;
|
||||
import net.schmizz.sshj.transport.random.JCERandom;
|
||||
import net.schmizz.sshj.transport.random.SingletonRandomFactory;
|
||||
import net.schmizz.sshj.userauth.keyprovider.OpenSSHKeyFile;
|
||||
import net.schmizz.sshj.userauth.keyprovider.PKCS5KeyFile;
|
||||
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
|
||||
@@ -58,14 +59,13 @@ import java.util.*;
|
||||
* net.schmizz.sshj.transport.mac.HMACMD596}</li>
|
||||
* <li>{@link net.schmizz.sshj.ConfigImpl#setCompressionFactories Compression}: {@link net.schmizz.sshj.transport.compression.NoneCompression}</li>
|
||||
* <li>{@link net.schmizz.sshj.ConfigImpl#setKeyAlgorithms KeyAlgorithm}: {@link net.schmizz.sshj.signature.SignatureRSA}, {@link net.schmizz.sshj.signature.SignatureDSA}</li>
|
||||
* <li>{@link net.schmizz.sshj.ConfigImpl#setRandomFactory PRNG}: {@link net.schmizz.sshj.transport.random.BouncyCastleRandom}* or {@link net.schmizz.sshj.transport.random.JCERandom}</li>
|
||||
* <li>{@link net.schmizz.sshj.ConfigImpl#setRandomFactory BC}: {@link net.schmizz.sshj.transport.random.BouncyCastleRandom}* or {@link net.schmizz.sshj.transport.random.JCERandom}</li>
|
||||
* <li>{@link net.schmizz.sshj.ConfigImpl#setRandomFactory BCFIPS}: {@link net.schmizz.sshj.transport.random.BouncyCastleFipsRandom}* or {@link net.schmizz.sshj.transport.random.JCERandom}</li>
|
||||
* <li>{@link net.schmizz.sshj.ConfigImpl#setFileKeyProviderFactories Key file support}: {@link net.schmizz.sshj.userauth.keyprovider.PKCS8KeyFile}*, {@link
|
||||
* net.schmizz.sshj.userauth.keyprovider.OpenSSHKeyFile}*</li>
|
||||
* <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 {
|
||||
@@ -75,11 +75,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();
|
||||
@@ -104,35 +103,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() {
|
||||
@@ -153,22 +149,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 PKCS5KeyFile.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(),
|
||||
@@ -206,27 +199,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() {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -60,6 +59,7 @@ import java.io.IOException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.ServerSocket;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.KeyPair;
|
||||
import java.util.*;
|
||||
|
||||
@@ -127,7 +127,7 @@ public class SSHClient
|
||||
private final List<LocalPortForwarder> forwarders = new ArrayList<LocalPortForwarder>();
|
||||
|
||||
/** character set of the remote machine */
|
||||
protected Charset remoteCharset = IOUtils.UTF8;
|
||||
protected Charset remoteCharset = StandardCharsets.UTF_8;
|
||||
|
||||
/** Default constructor. Initializes this object using {@link DefaultConfig}. */
|
||||
public SSHClient() {
|
||||
@@ -551,7 +551,7 @@ public class SSHClient
|
||||
* Creates a {@link KeyProvider} instance from given location on the file system. Currently the following private key files are supported:
|
||||
* <ul>
|
||||
* <li>PKCS8 (OpenSSH uses this format)</li>
|
||||
* <li>PKCS5</li>
|
||||
* <li>PEM-encoded PKCS1</li>
|
||||
* <li>Putty keyfile</li>
|
||||
* <li>openssh-key-v1 (New OpenSSH keyfile format)</li>
|
||||
* </ul>
|
||||
@@ -733,7 +733,7 @@ public class SSHClient
|
||||
throws IOException {
|
||||
checkConnected();
|
||||
checkAuthenticated();
|
||||
return new SFTPClient(new SFTPEngine(this).init());
|
||||
return new SFTPClient(this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -742,11 +742,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);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -766,7 +766,7 @@ public class SSHClient
|
||||
* remote character set or {@code null} for default
|
||||
*/
|
||||
public void setRemoteCharset(Charset remoteCharset) {
|
||||
this.remoteCharset = remoteCharset != null ? remoteCharset : IOUtils.UTF8;
|
||||
this.remoteCharset = remoteCharset != null ? remoteCharset : StandardCharsets.UTF_8;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -805,12 +805,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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
47
src/main/java/net/schmizz/sshj/common/Base64Decoder.java
Normal file
47
src/main/java/net/schmizz/sshj/common/Base64Decoder.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ package net.schmizz.sshj.common;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.PublicKey;
|
||||
import java.util.Arrays;
|
||||
@@ -428,7 +429,7 @@ public class Buffer<T extends Buffer<T>> {
|
||||
*/
|
||||
public String readString()
|
||||
throws BufferException {
|
||||
return readString(IOUtils.UTF8);
|
||||
return readString(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -454,7 +455,7 @@ public class Buffer<T extends Buffer<T>> {
|
||||
}
|
||||
|
||||
public T putString(String string) {
|
||||
return putString(string, IOUtils.UTF8);
|
||||
return putString(string, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -17,8 +17,8 @@ package net.schmizz.sshj.common;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.CharBuffer;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.CharsetEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
|
||||
/** Utility functions for byte arrays. */
|
||||
@@ -141,7 +141,7 @@ public class ByteArrayUtils {
|
||||
* @return UTF-8 bytes of the string
|
||||
*/
|
||||
public static byte[] encodeSensitiveStringToUtf8(char[] str) {
|
||||
CharsetEncoder charsetEncoder = Charset.forName("UTF-8").newEncoder();
|
||||
CharsetEncoder charsetEncoder = StandardCharsets.UTF_8.newEncoder();
|
||||
ByteBuffer utf8Buffer = ByteBuffer.allocate((int) (str.length * charsetEncoder.maxBytesPerChar()));
|
||||
assert utf8Buffer.hasArray();
|
||||
charsetEncoder.encode(CharBuffer.wrap(str), utf8Buffer, true);
|
||||
|
||||
194
src/main/java/net/schmizz/sshj/common/CircularBuffer.java
Normal file
194
src/main/java/net/schmizz/sshj/common/CircularBuffer.java
Normal 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 + "]";
|
||||
}
|
||||
|
||||
}
|
||||
45
src/main/java/net/schmizz/sshj/common/ECDSACurve.java
Normal file
45
src/main/java/net/schmizz/sshj/common/ECDSACurve.java
Normal file
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* Enumeration of supported ECDSA Curves with corresponding algorithm parameter names
|
||||
*/
|
||||
public enum ECDSACurve {
|
||||
/** NIST P-256 */
|
||||
SECP256R1("secp256r1"),
|
||||
|
||||
/** NIST P-384 */
|
||||
SECP384R1("secp384r1"),
|
||||
|
||||
/** NIST P-521 */
|
||||
SECP521R1("secp521r1");
|
||||
|
||||
private final String curveName;
|
||||
|
||||
ECDSACurve(final String curveName) {
|
||||
this.curveName = curveName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Curve Name for use with Java Cryptography Architecture components
|
||||
*
|
||||
* @return Curve Name
|
||||
*/
|
||||
public String getCurveName() {
|
||||
return curveName;
|
||||
}
|
||||
}
|
||||
86
src/main/java/net/schmizz/sshj/common/ECDSAKeyFactory.java
Normal file
86
src/main/java/net/schmizz/sshj/common/ECDSAKeyFactory.java
Normal 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 net.schmizz.sshj.common;
|
||||
|
||||
import com.hierynomus.sshj.common.KeyAlgorithm;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.security.AlgorithmParameters;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.security.spec.ECGenParameterSpec;
|
||||
import java.security.spec.ECParameterSpec;
|
||||
import java.security.spec.ECPoint;
|
||||
import java.security.spec.ECPrivateKeySpec;
|
||||
import java.security.spec.ECPublicKeySpec;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Factory for generating Elliptic Curve Keys using Java Security components for NIST Curves
|
||||
*/
|
||||
public class ECDSAKeyFactory {
|
||||
|
||||
private ECDSAKeyFactory() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Elliptic Curve Private Key for private key value and Curve Name
|
||||
*
|
||||
* @param privateKeyInteger Private Key
|
||||
* @param ecdsaCurve Elliptic Curve
|
||||
* @return Elliptic Curve Private Key
|
||||
* @throws GeneralSecurityException Thrown on failure to create parameter specification
|
||||
*/
|
||||
public static PrivateKey getPrivateKey(final BigInteger privateKeyInteger, final ECDSACurve ecdsaCurve) throws GeneralSecurityException {
|
||||
Objects.requireNonNull(privateKeyInteger, "Private Key integer required");
|
||||
Objects.requireNonNull(ecdsaCurve, "Curve required");
|
||||
|
||||
final ECParameterSpec parameterSpec = getParameterSpec(ecdsaCurve);
|
||||
final ECPrivateKeySpec privateKeySpec = new ECPrivateKeySpec(privateKeyInteger, parameterSpec);
|
||||
|
||||
final KeyFactory keyFactory = SecurityUtils.getKeyFactory(KeyAlgorithm.ECDSA);
|
||||
return keyFactory.generatePrivate(privateKeySpec);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Elliptic Curve Public Key for public key value and Curve Name
|
||||
*
|
||||
* @param point Public Key point
|
||||
* @param ecdsaCurve Elliptic Curve
|
||||
* @return Elliptic Curve Public Key
|
||||
* @throws GeneralSecurityException Thrown on failure to create parameter specification
|
||||
*/
|
||||
public static PublicKey getPublicKey(final ECPoint point, final ECDSACurve ecdsaCurve) throws GeneralSecurityException {
|
||||
Objects.requireNonNull(point, "Elliptic Curve Point required");
|
||||
Objects.requireNonNull(ecdsaCurve, "Curve required");
|
||||
|
||||
final ECParameterSpec parameterSpec = getParameterSpec(ecdsaCurve);
|
||||
final ECPublicKeySpec publicKeySpec = new ECPublicKeySpec(point, parameterSpec);
|
||||
|
||||
final KeyFactory keyFactory = SecurityUtils.getKeyFactory(KeyAlgorithm.ECDSA);
|
||||
return keyFactory.generatePublic(publicKeySpec);
|
||||
}
|
||||
|
||||
private static ECParameterSpec getParameterSpec(final ECDSACurve ecdsaCurve) throws GeneralSecurityException {
|
||||
final ECGenParameterSpec genParameterSpec = new ECGenParameterSpec(ecdsaCurve.getCurveName());
|
||||
final AlgorithmParameters algorithmParameters = AlgorithmParameters.getInstance(KeyAlgorithm.EC_KEYSTORE);
|
||||
algorithmParameters.init(genParameterSpec);
|
||||
return algorithmParameters.getParameterSpec(ECParameterSpec.class);
|
||||
}
|
||||
}
|
||||
@@ -17,21 +17,16 @@ package net.schmizz.sshj.common;
|
||||
|
||||
import com.hierynomus.sshj.common.KeyAlgorithm;
|
||||
import com.hierynomus.sshj.secg.SecgUtils;
|
||||
import org.bouncycastle.asn1.nist.NISTNamedCurves;
|
||||
import org.bouncycastle.asn1.x9.X9ECParameters;
|
||||
import org.bouncycastle.jce.spec.ECNamedCurveSpec;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.Key;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.PublicKey;
|
||||
import java.security.interfaces.ECKey;
|
||||
import java.security.interfaces.ECPublicKey;
|
||||
import java.security.spec.ECPoint;
|
||||
import java.security.spec.ECPublicKeySpec;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
@@ -42,13 +37,13 @@ class ECDSAVariationsAdapter {
|
||||
|
||||
private final static Logger log = LoggerFactory.getLogger(ECDSAVariationsAdapter.class);
|
||||
|
||||
public final static Map<String, String> SUPPORTED_CURVES = new HashMap<String, String>();
|
||||
public final static Map<String, String> NIST_CURVES_NAMES = new HashMap<String, String>();
|
||||
public final static Map<String, String> SUPPORTED_CURVES = new HashMap<>();
|
||||
public final static Map<String, ECDSACurve> NIST_CURVES = new HashMap<>();
|
||||
|
||||
static {
|
||||
NIST_CURVES_NAMES.put("256", "p-256");
|
||||
NIST_CURVES_NAMES.put("384", "p-384");
|
||||
NIST_CURVES_NAMES.put("521", "p-521");
|
||||
NIST_CURVES.put("256", ECDSACurve.SECP256R1);
|
||||
NIST_CURVES.put("384", ECDSACurve.SECP384R1);
|
||||
NIST_CURVES.put("521", ECDSACurve.SECP521R1);
|
||||
|
||||
SUPPORTED_CURVES.put("256", "nistp256");
|
||||
SUPPORTED_CURVES.put("384", "nistp384");
|
||||
@@ -57,9 +52,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();
|
||||
@@ -75,21 +67,15 @@ class ECDSAVariationsAdapter {
|
||||
algorithm, curveName, keyLen, x04, Arrays.toString(x), Arrays.toString(y)));
|
||||
}
|
||||
|
||||
if (!SUPPORTED_CURVES.values().contains(curveName)) {
|
||||
if (!SUPPORTED_CURVES.containsValue(curveName)) {
|
||||
throw new GeneralSecurityException(String.format("Unknown curve %s", curveName));
|
||||
}
|
||||
|
||||
BigInteger bigX = new BigInteger(1, x);
|
||||
BigInteger bigY = new BigInteger(1, y);
|
||||
|
||||
String name = NIST_CURVES_NAMES.get(variation);
|
||||
X9ECParameters ecParams = NISTNamedCurves.getByName(name);
|
||||
ECNamedCurveSpec ecCurveSpec = new ECNamedCurveSpec(name, ecParams.getCurve(), ecParams.getG(), ecParams.getN());
|
||||
ECPoint p = new ECPoint(bigX, bigY);
|
||||
ECPublicKeySpec publicKeySpec = new ECPublicKeySpec(p, ecCurveSpec);
|
||||
|
||||
KeyFactory keyFactory = KeyFactory.getInstance(KeyAlgorithm.ECDSA);
|
||||
return keyFactory.generatePublic(publicKeySpec);
|
||||
final BigInteger bigX = new BigInteger(1, x);
|
||||
final BigInteger bigY = new BigInteger(1, y);
|
||||
final ECPoint point = new ECPoint(bigX, bigY);
|
||||
final ECDSACurve ecdsaCurve = NIST_CURVES.get(variation);
|
||||
return ECDSAKeyFactory.getPublicKey(point, ecdsaCurve);
|
||||
} catch (Exception ex) {
|
||||
throw new GeneralSecurityException(ex);
|
||||
}
|
||||
@@ -99,7 +85,7 @@ class ECDSAVariationsAdapter {
|
||||
final ECPublicKey ecdsa = (ECPublicKey) pk;
|
||||
byte[] encoded = SecgUtils.getEncoded(ecdsa.getW(), ecdsa.getParams().getCurve());
|
||||
|
||||
buf.putString("nistp" + Integer.toString(fieldSizeFromKey(ecdsa)))
|
||||
buf.putString("nistp" + (fieldSizeFromKey(ecdsa)))
|
||||
.putBytes(encoded);
|
||||
}
|
||||
|
||||
|
||||
90
src/main/java/net/schmizz/sshj/common/Ed25519KeyFactory.java
Normal file
90
src/main/java/net/schmizz/sshj/common/Ed25519KeyFactory.java
Normal file
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* 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.security.GeneralSecurityException;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.security.spec.PKCS8EncodedKeySpec;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
import java.util.Base64;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Factory for generating Edwards-curve 25519 Public and Private Keys
|
||||
*/
|
||||
public class Ed25519KeyFactory {
|
||||
private static final int KEY_LENGTH = 32;
|
||||
|
||||
private static final String KEY_ALGORITHM = "Ed25519";
|
||||
|
||||
private static final byte[] ED25519_PKCS8_PRIVATE_KEY_HEADER = Base64.getDecoder().decode("MC4CAQEwBQYDK2VwBCIEIA");
|
||||
|
||||
private static final byte[] ED25519_PKCS8_PUBLIC_KEY_HEADER = Base64.getDecoder().decode("MCowBQYDK2VwAyEA");
|
||||
|
||||
private static final int PRIVATE_KEY_ENCODED_LENGTH = 48;
|
||||
|
||||
private static final int PUBLIC_KEY_ENCODED_LENGTH = 44;
|
||||
|
||||
private Ed25519KeyFactory() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Edwards-curve Private Key for private key binary
|
||||
*
|
||||
* @param privateKeyBinary Private Key byte array consisting of 32 bytes
|
||||
* @return Edwards-curve 25519 Private Key
|
||||
* @throws GeneralSecurityException Thrown on failure to generate Private Key
|
||||
*/
|
||||
public static PrivateKey getPrivateKey(final byte[] privateKeyBinary) throws GeneralSecurityException {
|
||||
Objects.requireNonNull(privateKeyBinary, "Private Key byte array required");
|
||||
if (privateKeyBinary.length == KEY_LENGTH) {
|
||||
final byte[] privateKeyEncoded = new byte[PRIVATE_KEY_ENCODED_LENGTH];
|
||||
System.arraycopy(ED25519_PKCS8_PRIVATE_KEY_HEADER, 0, privateKeyEncoded, 0, ED25519_PKCS8_PRIVATE_KEY_HEADER.length);
|
||||
System.arraycopy(privateKeyBinary, 0, privateKeyEncoded, ED25519_PKCS8_PRIVATE_KEY_HEADER.length, KEY_LENGTH);
|
||||
final PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKeyEncoded);
|
||||
|
||||
final KeyFactory keyFactory = SecurityUtils.getKeyFactory(KEY_ALGORITHM);
|
||||
return keyFactory.generatePrivate(keySpec);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Key length of 32 bytes required");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Edwards-curve Public Key for public key binary
|
||||
*
|
||||
* @param publicKeyBinary Public Key byte array consisting of 32 bytes
|
||||
* @return Edwards-curve 25519 Public Key
|
||||
* @throws GeneralSecurityException Thrown on failure to generate Public Key
|
||||
*/
|
||||
public static PublicKey getPublicKey(final byte[] publicKeyBinary) throws GeneralSecurityException {
|
||||
Objects.requireNonNull(publicKeyBinary, "Public Key byte array required");
|
||||
if (publicKeyBinary.length == KEY_LENGTH) {
|
||||
final byte[] publicKeyEncoded = new byte[PUBLIC_KEY_ENCODED_LENGTH];
|
||||
System.arraycopy(ED25519_PKCS8_PUBLIC_KEY_HEADER, 0, publicKeyEncoded, 0, ED25519_PKCS8_PUBLIC_KEY_HEADER.length);
|
||||
System.arraycopy(publicKeyBinary, 0, publicKeyEncoded, ED25519_PKCS8_PUBLIC_KEY_HEADER.length, KEY_LENGTH);
|
||||
final X509EncodedKeySpec keySpec = new X509EncodedKeySpec(publicKeyEncoded);
|
||||
|
||||
final KeyFactory keyFactory = SecurityUtils.getKeyFactory(KEY_ALGORITHM);
|
||||
return keyFactory.generatePublic(keySpec);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Key length of 32 bytes required");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,12 +19,9 @@ import java.io.ByteArrayOutputStream;
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.Charset;
|
||||
|
||||
public class IOUtils {
|
||||
|
||||
public static final Charset UTF8 = Charset.forName("UTF-8");
|
||||
|
||||
public static void closeQuietly(Closeable... closeables) {
|
||||
closeQuietly(LoggerFactory.DEFAULT, closeables);
|
||||
}
|
||||
|
||||
@@ -16,13 +16,8 @@
|
||||
package net.schmizz.sshj.common;
|
||||
|
||||
import com.hierynomus.sshj.common.KeyAlgorithm;
|
||||
import com.hierynomus.sshj.signature.Ed25519PublicKey;
|
||||
import com.hierynomus.sshj.signature.SignatureEdDSA;
|
||||
import com.hierynomus.sshj.userauth.certificate.Certificate;
|
||||
import net.i2p.crypto.eddsa.EdDSAPublicKey;
|
||||
import net.i2p.crypto.eddsa.spec.EdDSANamedCurveSpec;
|
||||
import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable;
|
||||
import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec;
|
||||
import net.schmizz.sshj.common.Buffer.BufferException;
|
||||
import net.schmizz.sshj.signature.Signature;
|
||||
import net.schmizz.sshj.signature.SignatureDSA;
|
||||
@@ -178,20 +173,16 @@ public enum KeyType {
|
||||
public PublicKey readPubKeyFromBuffer(Buffer<?> buf) throws GeneralSecurityException {
|
||||
try {
|
||||
final int keyLen = buf.readUInt32AsInt();
|
||||
final byte[] p = new byte[keyLen];
|
||||
buf.readRawBytes(p);
|
||||
final byte[] publicKeyBinary = new byte[keyLen];
|
||||
buf.readRawBytes(publicKeyBinary);
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug(String.format("Key algo: %s, Key curve: 25519, Key Len: %s\np: %s",
|
||||
sType,
|
||||
keyLen,
|
||||
Arrays.toString(p))
|
||||
Arrays.toString(publicKeyBinary))
|
||||
);
|
||||
}
|
||||
|
||||
EdDSANamedCurveSpec ed25519 = EdDSANamedCurveTable.getByName("Ed25519");
|
||||
EdDSAPublicKeySpec publicSpec = new EdDSAPublicKeySpec(p, ed25519);
|
||||
return new Ed25519PublicKey(publicSpec);
|
||||
|
||||
return Ed25519KeyFactory.getPublicKey(publicKeyBinary);
|
||||
} catch (Buffer.BufferException be) {
|
||||
throw new SSHRuntimeException(be);
|
||||
}
|
||||
@@ -199,13 +190,17 @@ public enum KeyType {
|
||||
|
||||
@Override
|
||||
protected void writePubKeyContentsIntoBuffer(PublicKey pk, Buffer<?> buf) {
|
||||
EdDSAPublicKey key = (EdDSAPublicKey) pk;
|
||||
buf.putBytes(key.getAbyte());
|
||||
final byte[] encoded = pk.getEncoded();
|
||||
final int keyLength = 32;
|
||||
final int headerLength = encoded.length - keyLength;
|
||||
final byte[] encodedPublicKey = new byte[keyLength];
|
||||
System.arraycopy(encoded, headerLength, encodedPublicKey, 0, keyLength);
|
||||
buf.putBytes(encodedPublicKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isMyType(Key key) {
|
||||
return "EdDSA".equals(key.getAlgorithm());
|
||||
return "EdDSA".equals(key.getAlgorithm()) || "Ed25519".equals(key.getAlgorithm());
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -271,6 +276,9 @@ public class SecurityUtils {
|
||||
*/
|
||||
public static synchronized void setSecurityProvider(String securityProvider) {
|
||||
SecurityUtils.securityProvider = securityProvider;
|
||||
if(null == securityProvider) {
|
||||
SecurityUtils.registerBouncyCastle = null;
|
||||
}
|
||||
registrationDone = false;
|
||||
}
|
||||
|
||||
@@ -281,8 +289,8 @@ public class SecurityUtils {
|
||||
if (securityProvider == null && registerBouncyCastle == null) {
|
||||
LOG.info("BouncyCastle not registered, using the default JCE provider");
|
||||
} else if (securityProvider == null) {
|
||||
LOG.error("Failed to register BouncyCastle as the defaut JCE provider");
|
||||
throw new SSHRuntimeException("Failed to register BouncyCastle as the defaut JCE provider");
|
||||
LOG.error("Failed to register BouncyCastle as the default JCE provider");
|
||||
throw new SSHRuntimeException("Failed to register BouncyCastle as the default JCE provider");
|
||||
}
|
||||
}
|
||||
registrationDone = true;
|
||||
|
||||
@@ -27,6 +27,7 @@ import org.slf4j.Logger;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.LinkedList;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
@@ -90,7 +91,7 @@ public abstract class AbstractChannel
|
||||
this.log = loggerFactory.getLogger(getClass());
|
||||
this.trans = conn.getTransport();
|
||||
|
||||
this.remoteCharset = remoteCharset != null ? remoteCharset : IOUtils.UTF8;
|
||||
this.remoteCharset = remoteCharset != null ? remoteCharset : StandardCharsets.UTF_8;
|
||||
id = conn.nextID();
|
||||
|
||||
lwin = new Window.Local(conn.getWindowSize(), conn.getMaxPacketSize(), loggerFactory);
|
||||
@@ -164,8 +165,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 +304,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 +354,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 +375,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 +446,8 @@ public abstract class AbstractChannel
|
||||
+ rwin + " >";
|
||||
}
|
||||
|
||||
public interface TransportRunnable {
|
||||
void run() throws TransportException, ConnectionException;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -105,6 +106,7 @@ public final class ChannelInputStream
|
||||
try {
|
||||
buf.wait();
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw (IOException) new InterruptedIOException().initCause(e);
|
||||
}
|
||||
}
|
||||
@@ -112,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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,14 +30,14 @@ 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;
|
||||
|
||||
private final DataBuffer buffer = new DataBuffer();
|
||||
private final byte[] b = new byte[1];
|
||||
|
||||
private AtomicBoolean closed;
|
||||
private final AtomicBoolean closed;
|
||||
private SSHException error;
|
||||
|
||||
private final class DataBuffer {
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -93,6 +93,7 @@ public abstract class Window {
|
||||
throw new ConnectionException("Timeout when trying to expand the window size");
|
||||
}
|
||||
} catch (InterruptedException ie) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new ConnectionException(ie);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -317,13 +317,10 @@ public class RandomAccessRemoteFile
|
||||
@Override
|
||||
public void writeUTF(String str)
|
||||
throws IOException {
|
||||
final DataOutputStream dos = new DataOutputStream(rf.new RemoteFileOutputStream(fp));
|
||||
try {
|
||||
try (DataOutputStream dos = new DataOutputStream(rf.new RemoteFileOutputStream(fp));) {
|
||||
dos.writeUTF(str);
|
||||
} finally {
|
||||
dos.close();
|
||||
fp += dos.size();
|
||||
}
|
||||
fp += dos.size();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -261,7 +261,16 @@ public class RemoteFile
|
||||
private boolean eof;
|
||||
|
||||
public ReadAheadRemoteFileInputStream(int maxUnconfirmedReads) {
|
||||
this(maxUnconfirmedReads, 0L, -1L);
|
||||
this(maxUnconfirmedReads, 0L);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param maxUnconfirmedReads Maximum number of unconfirmed requests to send
|
||||
* @param fileOffset Initial offset in file to read from
|
||||
*/
|
||||
public ReadAheadRemoteFileInputStream(int maxUnconfirmedReads, long fileOffset) {
|
||||
this(maxUnconfirmedReads, fileOffset, -1L);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,21 +246,41 @@ public class SFTPClient
|
||||
xfer.download(source, dest);
|
||||
}
|
||||
|
||||
public void get(String source, String dest, long byteOffset)
|
||||
throws IOException {
|
||||
xfer.download(source, dest, byteOffset);
|
||||
}
|
||||
|
||||
public void put(String source, String dest)
|
||||
throws IOException {
|
||||
xfer.upload(source, dest);
|
||||
}
|
||||
|
||||
public void put(String source, String dest, long byteOffset)
|
||||
throws IOException {
|
||||
xfer.upload(source, dest, byteOffset);
|
||||
}
|
||||
|
||||
public void get(String source, LocalDestFile dest)
|
||||
throws IOException {
|
||||
xfer.download(source, dest);
|
||||
}
|
||||
|
||||
public void get(String source, LocalDestFile dest, long byteOffset)
|
||||
throws IOException {
|
||||
xfer.download(source, dest, byteOffset);
|
||||
}
|
||||
|
||||
public void put(LocalSourceFile source, String dest)
|
||||
throws IOException {
|
||||
xfer.upload(source, dest);
|
||||
}
|
||||
|
||||
public void put(LocalSourceFile source, String dest, long byteOffset)
|
||||
throws IOException {
|
||||
xfer.upload(source, dest, byteOffset);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close()
|
||||
throws IOException {
|
||||
|
||||
@@ -17,7 +17,6 @@ package net.schmizz.sshj.sftp;
|
||||
|
||||
import com.hierynomus.sshj.common.ThreadNameProvider;
|
||||
import net.schmizz.concurrent.Promise;
|
||||
import net.schmizz.sshj.common.IOUtils;
|
||||
import net.schmizz.sshj.common.LoggerFactory;
|
||||
import net.schmizz.sshj.common.SSHException;
|
||||
import net.schmizz.sshj.connection.channel.direct.Session;
|
||||
@@ -28,6 +27,7 @@ import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.EnumSet;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
@@ -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() {
|
||||
@@ -296,7 +373,7 @@ public class SFTPEngine
|
||||
/** Using UTF-8 */
|
||||
protected static String readSingleName(Response res)
|
||||
throws IOException {
|
||||
return readSingleName(res, IOUtils.UTF8);
|
||||
return readSingleName(res, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
/** Using any character set */
|
||||
|
||||
@@ -50,25 +50,47 @@ public class SFTPFileTransfer
|
||||
@Override
|
||||
public void upload(String source, String dest)
|
||||
throws IOException {
|
||||
upload(new FileSystemFile(source), dest);
|
||||
upload(source, dest, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void upload(String source, String dest, long byteOffset)
|
||||
throws IOException {
|
||||
upload(new FileSystemFile(source), dest, byteOffset);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void download(String source, String dest)
|
||||
throws IOException {
|
||||
download(source, new FileSystemFile(dest));
|
||||
download(source, dest, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void download(String source, String dest, long byteOffset)
|
||||
throws IOException {
|
||||
download(source, new FileSystemFile(dest), byteOffset);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void upload(LocalSourceFile localFile, String remotePath) throws IOException {
|
||||
new Uploader(localFile, remotePath).upload(getTransferListener());
|
||||
upload(localFile, remotePath, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void upload(LocalSourceFile localFile, String remotePath, long byteOffset) throws IOException {
|
||||
new Uploader(localFile, remotePath).upload(getTransferListener(), byteOffset);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void download(String source, LocalDestFile dest) throws IOException {
|
||||
download(source, dest, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void download(String source, LocalDestFile dest, long byteOffset) throws IOException {
|
||||
final PathComponents pathComponents = engine.getPathHelper().getComponents(source);
|
||||
final FileAttributes attributes = engine.stat(source);
|
||||
new Downloader().download(getTransferListener(), new RemoteResourceInfo(pathComponents, attributes), dest);
|
||||
new Downloader().download(getTransferListener(), new RemoteResourceInfo(pathComponents, attributes), dest, byteOffset);
|
||||
}
|
||||
|
||||
public void setUploadFilter(LocalFileFilter uploadFilter) {
|
||||
@@ -92,7 +114,8 @@ public class SFTPFileTransfer
|
||||
@SuppressWarnings("PMD.MissingBreakInSwitch")
|
||||
private void download(final TransferListener listener,
|
||||
final RemoteResourceInfo remote,
|
||||
final LocalDestFile local) throws IOException {
|
||||
final LocalDestFile local,
|
||||
final long byteOffset) throws IOException {
|
||||
final LocalDestFile adjustedFile;
|
||||
switch (remote.getAttributes().getType()) {
|
||||
case DIRECTORY:
|
||||
@@ -101,8 +124,9 @@ public class SFTPFileTransfer
|
||||
case UNKNOWN:
|
||||
log.warn("Server did not supply information about the type of file at `{}` " +
|
||||
"-- assuming it is a regular file!", remote.getPath());
|
||||
// fall through
|
||||
case REGULAR:
|
||||
adjustedFile = downloadFile(listener.file(remote.getName(), remote.getAttributes().getSize()), remote, local);
|
||||
adjustedFile = downloadFile(listener.file(remote.getName(), remote.getAttributes().getSize()), remote, local, byteOffset);
|
||||
break;
|
||||
default:
|
||||
throw new IOException(remote + " is not a regular file or directory");
|
||||
@@ -116,37 +140,29 @@ public class SFTPFileTransfer
|
||||
final LocalDestFile local)
|
||||
throws IOException {
|
||||
final LocalDestFile adjusted = local.getTargetDirectory(remote.getName());
|
||||
final RemoteDirectory rd = engine.openDir(remote.getPath());
|
||||
try {
|
||||
try (RemoteDirectory rd = engine.openDir(remote.getPath())) {
|
||||
for (RemoteResourceInfo rri : rd.scan(getDownloadFilter()))
|
||||
download(listener, rri, adjusted.getChild(rri.getName()));
|
||||
} finally {
|
||||
rd.close();
|
||||
download(listener, rri, adjusted.getChild(rri.getName()), 0); // not supporting individual byte offsets for these files
|
||||
}
|
||||
return adjusted;
|
||||
}
|
||||
|
||||
private LocalDestFile downloadFile(final StreamCopier.Listener listener,
|
||||
final RemoteResourceInfo remote,
|
||||
final LocalDestFile local)
|
||||
final LocalDestFile local,
|
||||
final long byteOffset)
|
||||
throws IOException {
|
||||
final LocalDestFile adjusted = local.getTargetFile(remote.getName());
|
||||
final RemoteFile rf = engine.open(remote.getPath());
|
||||
try {
|
||||
final RemoteFile.ReadAheadRemoteFileInputStream rfis = rf.new ReadAheadRemoteFileInputStream(16);
|
||||
final OutputStream os = adjusted.getOutputStream();
|
||||
try {
|
||||
try (RemoteFile rf = engine.open(remote.getPath())) {
|
||||
log.debug("Attempting to download {} with offset={}", remote.getPath(), byteOffset);
|
||||
try (RemoteFile.ReadAheadRemoteFileInputStream rfis = rf.new ReadAheadRemoteFileInputStream(16, byteOffset);
|
||||
OutputStream os = adjusted.getOutputStream(byteOffset != 0)) {
|
||||
new StreamCopier(rfis, os, engine.getLoggerFactory())
|
||||
.bufSize(engine.getSubsystem().getLocalMaxPacketSize())
|
||||
.keepFlushing(false)
|
||||
.listener(listener)
|
||||
.copy();
|
||||
} finally {
|
||||
rfis.close();
|
||||
os.close();
|
||||
}
|
||||
} finally {
|
||||
rf.close();
|
||||
}
|
||||
return adjusted;
|
||||
}
|
||||
@@ -173,17 +189,17 @@ public class SFTPFileTransfer
|
||||
this.remote = remote;
|
||||
}
|
||||
|
||||
private void upload(final TransferListener listener) throws IOException {
|
||||
private void upload(final TransferListener listener, long byteOffset) throws IOException {
|
||||
if (source.isDirectory()) {
|
||||
makeDirIfNotExists(remote); // Ensure that the directory exists
|
||||
uploadDir(listener.directory(source.getName()), source, remote);
|
||||
setAttributes(source, remote);
|
||||
} else if (source.isFile() && isDirectory(remote)) {
|
||||
String adjustedRemote = engine.getPathHelper().adjustForParent(this.remote, source.getName());
|
||||
uploadFile(listener.file(source.getName(), source.getLength()), source, adjustedRemote);
|
||||
uploadFile(listener.file(source.getName(), source.getLength()), source, adjustedRemote, byteOffset);
|
||||
setAttributes(source, adjustedRemote);
|
||||
} else if (source.isFile()) {
|
||||
uploadFile(listener.file(source.getName(), source.getLength()), source, remote);
|
||||
uploadFile(listener.file(source.getName(), source.getLength()), source, remote, byteOffset);
|
||||
setAttributes(source, remote);
|
||||
} else {
|
||||
throw new IOException(source + " is not a file or directory");
|
||||
@@ -192,13 +208,14 @@ public class SFTPFileTransfer
|
||||
|
||||
private void upload(final TransferListener listener,
|
||||
final LocalSourceFile local,
|
||||
final String remote)
|
||||
final String remote,
|
||||
final long byteOffset)
|
||||
throws IOException {
|
||||
final String adjustedPath;
|
||||
if (local.isDirectory()) {
|
||||
adjustedPath = uploadDir(listener.directory(local.getName()), local, remote);
|
||||
} else if (local.isFile()) {
|
||||
adjustedPath = uploadFile(listener.file(local.getName(), local.getLength()), local, remote);
|
||||
adjustedPath = uploadFile(listener.file(local.getName(), local.getLength()), local, remote, byteOffset);
|
||||
} else {
|
||||
throw new IOException(local + " is not a file or directory");
|
||||
}
|
||||
@@ -217,22 +234,34 @@ public class SFTPFileTransfer
|
||||
throws IOException {
|
||||
makeDirIfNotExists(remote);
|
||||
for (LocalSourceFile f : local.getChildren(getUploadFilter()))
|
||||
upload(listener, f, engine.getPathHelper().adjustForParent(remote, f.getName()));
|
||||
upload(listener, f, engine.getPathHelper().adjustForParent(remote, f.getName()), 0); // not supporting individual byte offsets for these files
|
||||
return remote;
|
||||
}
|
||||
|
||||
private String uploadFile(final StreamCopier.Listener listener,
|
||||
final LocalSourceFile local,
|
||||
final String remote)
|
||||
final String remote,
|
||||
final long byteOffset)
|
||||
throws IOException {
|
||||
final String adjusted = prepareFile(local, remote);
|
||||
final String adjusted = prepareFile(local, remote, byteOffset);
|
||||
RemoteFile rf = null;
|
||||
InputStream fis = null;
|
||||
RemoteFile.RemoteFileOutputStream rfos = null;
|
||||
EnumSet<OpenMode> modes;
|
||||
try {
|
||||
rf = engine.open(adjusted, EnumSet.of(OpenMode.WRITE, OpenMode.CREAT, OpenMode.TRUNC));
|
||||
if (byteOffset == 0) {
|
||||
// Starting at the beginning, overwrite/create
|
||||
modes = EnumSet.of(OpenMode.WRITE, OpenMode.CREAT, OpenMode.TRUNC);
|
||||
} else {
|
||||
// Starting at some offset, append
|
||||
modes = EnumSet.of(OpenMode.WRITE, OpenMode.APPEND);
|
||||
}
|
||||
|
||||
log.debug("Attempting to upload {} with offset={}", local.getName(), byteOffset);
|
||||
rf = engine.open(adjusted, modes);
|
||||
fis = local.getInputStream();
|
||||
rfos = rf.new RemoteFileOutputStream(0, 16);
|
||||
fis.skip(byteOffset);
|
||||
rfos = rf.new RemoteFileOutputStream(byteOffset, 16);
|
||||
new StreamCopier(fis, rfos, engine.getLoggerFactory())
|
||||
.bufSize(engine.getSubsystem().getRemoteMaxPacketSize() - rf.getOutgoingPacketOverhead())
|
||||
.keepFlushing(false)
|
||||
@@ -294,7 +323,7 @@ public class SFTPFileTransfer
|
||||
}
|
||||
}
|
||||
|
||||
private String prepareFile(final LocalSourceFile local, final String remote)
|
||||
private String prepareFile(final LocalSourceFile local, final String remote, final long byteOffset)
|
||||
throws IOException {
|
||||
final FileAttributes attrs;
|
||||
try {
|
||||
@@ -309,7 +338,7 @@ public class SFTPFileTransfer
|
||||
if (attrs.getMode().getType() == FileMode.Type.DIRECTORY) {
|
||||
throw new IOException("Trying to upload file " + local.getName() + " to path " + remote + " but that is a directory");
|
||||
} else {
|
||||
log.debug("probeFile: {} is a {} file that will be replaced", remote, attrs.getMode().getType());
|
||||
log.debug("probeFile: {} is a {} file that will be {}", remote, attrs.getMode().getType(), byteOffset > 0 ? "resumed" : "replaced");
|
||||
return remote;
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user