mirror of
https://github.com/hierynomus/sshj.git
synced 2025-12-06 15:20:54 +03:00
Compare commits
55 Commits
issue-711
...
proxy-fact
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c3333d61c | ||
|
|
0d16fbe146 | ||
|
|
154a202384 | ||
|
|
830a39dc24 | ||
|
|
dcfa1833d7 | ||
|
|
6e7fb96d07 | ||
|
|
d5d6096d5d | ||
|
|
2551f8e559 | ||
|
|
430cbfcf13 | ||
|
|
ec467a3875 | ||
|
|
1b258f0677 | ||
|
|
559384ac91 | ||
|
|
5674072666 | ||
|
|
f33bfecbf5 | ||
|
|
c0f6000ff5 | ||
|
|
3de0302c84 | ||
|
|
d7e402c557 | ||
|
|
8ef996b406 | ||
|
|
e9cb90901c | ||
|
|
69812e9a81 | ||
|
|
9a939d029b | ||
|
|
50efeb6519 | ||
|
|
aabb1be52e | ||
|
|
32329e547e | ||
|
|
8cf63a96a9 | ||
|
|
cab7731928 | ||
|
|
50073db6c1 | ||
|
|
90099bbf5e | ||
|
|
ce0a7d5193 | ||
|
|
ced27fc898 | ||
|
|
624747c527 | ||
|
|
d8697c2228 | ||
|
|
7c14098f7d | ||
|
|
d5805a6c64 | ||
|
|
8a66dc5336 | ||
|
|
a5c10ab50f | ||
|
|
3256f5336d | ||
|
|
ad87db9196 | ||
|
|
781f2dc632 | ||
|
|
b2115dea6f | ||
|
|
d6d6f0dd33 | ||
|
|
93de1ecf47 | ||
|
|
46ca5375d0 | ||
|
|
771ac0e346 | ||
|
|
eb09a16aef | ||
|
|
53d241e4e3 | ||
|
|
03dd1aaf49 | ||
|
|
7742d9b661 | ||
|
|
14bf93e677 | ||
|
|
753e3a50e5 | ||
|
|
2e1ef9dbcd | ||
|
|
6f9873712f | ||
|
|
8e8e04ff1f | ||
|
|
b47e6fa012 | ||
|
|
f38fcbe57e |
1
.gitattributes
vendored
1
.gitattributes
vendored
@@ -1 +1,2 @@
|
||||
*.bat text eol=crlf
|
||||
src/itest/docker-image/** eol=lf
|
||||
|
||||
15
.github/workflows/gradle.yml
vendored
15
.github/workflows/gradle.yml
vendored
@@ -24,19 +24,8 @@ jobs:
|
||||
run: chmod +x gradlew
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew check
|
||||
# java10:
|
||||
# name: Build with Java 10
|
||||
# runs-on: ubuntu-latest
|
||||
# steps:
|
||||
# - uses: actions/checkout@v2
|
||||
# - name: Set up JDK 10
|
||||
# uses: actions/setup-java@v1
|
||||
# with:
|
||||
# java-version: 10
|
||||
# - name: Grant execute permission for gradlew
|
||||
# run: chmod +x gradlew
|
||||
# - name: Build with Gradle
|
||||
# run: ./gradlew check -xanimalsnifferMain -xanimalsnifferTest
|
||||
- name: Codecov
|
||||
uses: codecov/codecov-action@v2
|
||||
|
||||
integration:
|
||||
name: Integration test
|
||||
|
||||
49
.github/workflows/release.yml
vendored
Normal file
49
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,49 @@
|
||||
name: SSHJ Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
java12:
|
||||
name: Build with Java 12
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up JDK 12
|
||||
uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: 12
|
||||
- name: Grant execute permission for gradlew
|
||||
run: chmod +x gradlew
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew check
|
||||
release:
|
||||
name: Release
|
||||
needs: [java12]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-java@v1
|
||||
with:
|
||||
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 }}
|
||||
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 }}
|
||||
|
||||
|
||||
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -2,5 +2,6 @@
|
||||
"java.checkstyle.configuration": "${workspaceFolder}/gradle/config/checkstyle/checkstyle.xml",
|
||||
"files.watcherExclude": {
|
||||
"**/target": true
|
||||
}
|
||||
},
|
||||
"java.configuration.updateBuildConfiguration": "automatic"
|
||||
}
|
||||
|
||||
51
README.adoc
51
README.adoc
@@ -1,12 +1,11 @@
|
||||
= sshj - SSHv2 library for Java
|
||||
Jeroen van Erp
|
||||
:sshj_groupid: com.hierynomus
|
||||
:sshj_version: 0.31.0
|
||||
:sshj_version: 0.32.0
|
||||
:source-highlighter: pygments
|
||||
|
||||
image:https://api.bintray.com/packages/hierynomus/maven/sshj/images/download.svg[link="https://bintray.com/hierynomus/maven/sshj/_latestVersion"]
|
||||
image:https://travis-ci.org/hierynomus/sshj.svg?branch=master[link="https://travis-ci.org/hierynomus/sshj"]
|
||||
image:https://api.codacy.com/project/badge/Grade/14a0a316bb9149739b5ea26dbfa8da8a["Codacy code quality", link="https://www.codacy.com/app/jeroen_2/sshj?utm_source=github.com&utm_medium=referral&utm_content=hierynomus/sshj&utm_campaign=Badge_Grade"]
|
||||
image:https://github.com/hierynomus/sshj/actions/workflows/gradle.yml/badge.svg[link="https://github.com/hierynomus/sshj/actions/workflows/gradle.yml"]
|
||||
image:https://app.codacy.com/project/badge/Grade/2c8a5a67c6a54ed89c9a699fd6b27305["Codacy Grade", link="https://app.codacy.com/gh/hierynomus/sshj"]
|
||||
image:https://codecov.io/gh/hierynomus/sshj/branch/master/graph/badge.svg["codecov", link="https://codecov.io/gh/hierynomus/sshj"]
|
||||
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"]
|
||||
@@ -96,7 +95,7 @@ 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 7+. 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.
|
||||
|
||||
== Reporting bugs
|
||||
Issue tracker: https://github.com/hierynomus/sshj/issues
|
||||
@@ -105,6 +104,48 @@ Issue tracker: https://github.com/hierynomus/sshj/issues
|
||||
Fork away!
|
||||
|
||||
== Release history
|
||||
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.
|
||||
* Merged https://github.com/hierynomus/sshj/pull/741[#741]: Add support for testcontainers in test setup to test more scenarios
|
||||
* Merged https://github.com/hierynomus/sshj/pull/733[#733]: Send correct key proposal if client knows CA key
|
||||
* Merged https://github.com/hierynomus/sshj/pull/746[#746]: Fix bug in reading Putty private key file with passphrase
|
||||
* Merged https://github.com/hierynomus/sshj/pull/742[#742]: Use Config.keyAlgorithms to determine rsa-sha2 support
|
||||
* Merged https://github.com/hierynomus/sshj/pull/754[#754]: Use SFTP protocol version to set FXP rename flags conditionally
|
||||
* Merged https://github.com/hierynomus/sshj/pull/752[#752]: Correctly start and terminate KeepAlive thread
|
||||
* Merged https://github.com/hierynomus/sshj/pull/753[#753]: Provide better thread names
|
||||
* Merged https://github.com/hierynomus/sshj/pull/724[#724]: Add parameter to limit read ahead length
|
||||
* Merged https://github.com/hierynomus/sshj/pull/763[#763]: Try all public key algorithms for a specific key type
|
||||
* Merged https://github.com/hierynomus/sshj/pull/756[#756]: Remove deprecated proxy connect methods
|
||||
* Merged https://github.com/hierynomus/sshj/pull/770[#770]: Add support for `ed25519` `aes-128-cbc` keys
|
||||
* Merged https://github.com/hierynomus/sshj/pull/773[#773]: Fix NPE when reading empty OpenSSHKeyV1KeyFile
|
||||
* Merged https://github.com/hierynomus/sshj/pull/777[#777]: Don't request too many read-ahead packets
|
||||
|
||||
SSHJ 0.32.0 (2021-10-12)::
|
||||
* Send EOF on channel close (Fixes https://github.com/hierynomus/sshj/issues/143[#143], https://github.com/hierynomus/sshj/issues/496[#496], https://github.com/hierynomus/sshj/issues/553[#553], https://github.com/hierynomus/sshj/issues/554[#554])
|
||||
* Merged https://github.com/hierynomus/sshj/pull/726[#726]: Parse OpenSSH v1 keys with full CRT information present
|
||||
* Merged https://github.com/hierynomus/sshj/pull/721[#721]: Prefer known host key algorithm for host key verification
|
||||
* Merged https://github.com/hierynomus/sshj/pull/716[#716], https://github.com/hierynomus/sshj/pull/729[#729] and https://github.com/hierynomus/sshj/pull/730[#730]: Add full support for PuTTY v3 key files.
|
||||
* Merged https://github.com/hierynomus/sshj/pull/708[#708] and https://github.com/hierynomus/sshj/pull/713[#71]: Add support for PKCS#8 private keys
|
||||
* Merged https://github.com/hierynomus/sshj/pull/703[#703]: Support host certificate keys
|
||||
* Upgraded dependencies BouncyCastle (1.69), SLF4j (1.7.32), Logback (1.2.6), asn-one (0.6.0)
|
||||
* Merged https://github.com/hierynomus/sshj/pull/702[#702]: Support Public key authentication using certificates
|
||||
* Merged https://github.com/hierynomus/sshj/pull/691[#691]: Fix for writing negative unsigned integers to Buffer
|
||||
* Merged https://github.com/hierynomus/sshj/pull/682[#682]: Support for chacha20-poly1305@openssh.com cipher
|
||||
* Merged https://github.com/hierynomus/sshj/pull/680[#680]: Configurable preserve mtimes for SCP transfers
|
||||
|
||||
SSHJ 0.31.0 (2021-02-08)::
|
||||
* Bump dependencies (asn-one 0.5.0, BouncyCastle 1.68, slf4j-api 1.7.30)
|
||||
* Merged https://github.com/hierynomus/sshj/pull/660[#660]: Support ED25519 and ECDSA keys in PuTTY format
|
||||
|
||||
@@ -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"
|
||||
|
||||
236
build.gradle
236
build.gradle
@@ -1,23 +1,23 @@
|
||||
import java.text.SimpleDateFormat
|
||||
import com.bmuschko.gradle.docker.tasks.container.*
|
||||
import com.bmuschko.gradle.docker.tasks.image.*
|
||||
|
||||
plugins {
|
||||
id "java"
|
||||
id "groovy"
|
||||
id "jacoco"
|
||||
id "com.github.blindpirate.osgi" version '0.0.3'
|
||||
id "com.github.blindpirate.osgi" version '0.0.6'
|
||||
id "maven-publish"
|
||||
id 'pl.allegro.tech.build.axion-release' version '1.11.0'
|
||||
id "com.bmuschko.docker-remote-api" version "6.4.0"
|
||||
id "com.github.hierynomus.license" version "0.12.1"
|
||||
id "com.jfrog.bintray" version "1.8.5"
|
||||
id 'ru.vyarus.java-lib' version '1.0.5'
|
||||
// id 'ru.vyarus.pom' version '1.0.3'
|
||||
id 'ru.vyarus.github-info' version '1.1.0'
|
||||
id "signing"
|
||||
id 'pl.allegro.tech.build.axion-release' version '1.13.3'
|
||||
id "com.bmuschko.docker-remote-api" version "7.1.0"
|
||||
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"
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
group = "com.hierynomus"
|
||||
defaultTasks ["build"]
|
||||
ext.moduleName = "${project.group}.${project.name}"
|
||||
|
||||
scmVersion {
|
||||
@@ -33,36 +33,29 @@ scmVersion {
|
||||
|
||||
project.version = scmVersion.version
|
||||
|
||||
defaultTasks "build"
|
||||
configurations.implementation.transitive = false
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
configurations.compile.transitive = false
|
||||
|
||||
def bouncycastleVersion = "1.69"
|
||||
def sshdVersion = "2.1.0"
|
||||
def bouncycastleVersion = "1.70"
|
||||
def sshdVersion = "2.8.0"
|
||||
|
||||
dependencies {
|
||||
implementation "org.slf4j:slf4j-api:1.7.30"
|
||||
implementation "org.slf4j:slf4j-api:1.7.36"
|
||||
implementation "org.bouncycastle:bcprov-jdk15on:$bouncycastleVersion"
|
||||
implementation "org.bouncycastle:bcpkix-jdk15on:$bouncycastleVersion"
|
||||
implementation "com.jcraft:jzlib:1.1.3"
|
||||
implementation "com.hierynomus:asn-one:0.5.0"
|
||||
implementation "com.hierynomus:asn-one:0.6.0"
|
||||
|
||||
implementation "net.i2p.crypto:eddsa:0.3.0"
|
||||
|
||||
testImplementation "junit:junit:4.12"
|
||||
testImplementation "junit:junit:4.13.2"
|
||||
testImplementation 'org.spockframework:spock-core:1.3-groovy-2.4'
|
||||
testImplementation "org.mockito:mockito-core:2.28.2"
|
||||
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"
|
||||
testRuntimeOnly "ch.qos.logback:logback-classic:1.2.3"
|
||||
testImplementation "ch.qos.logback:logback-classic:1.2.11"
|
||||
testImplementation 'org.glassfish.grizzly:grizzly-http-server:2.4.4'
|
||||
testImplementation 'org.apache.httpcomponents:httpclient:4.5.9'
|
||||
|
||||
testImplementation 'org.apache.httpcomponents:httpclient:4.5.13'
|
||||
testImplementation 'org.testcontainers:testcontainers:1.16.2'
|
||||
}
|
||||
|
||||
license {
|
||||
@@ -71,7 +64,11 @@ license {
|
||||
mapping {
|
||||
java = 'SLASHSTAR_STYLE'
|
||||
}
|
||||
excludes(['**/djb/Curve25519.java', '**/sshj/common/Base64.java', '**/org/mindrot/jbcrypt/*.java'])
|
||||
excludes([
|
||||
'**/sshj/common/Base64.java',
|
||||
'**/com/hierynomus/sshj/userauth/keyprovider/bcrypt/*.java',
|
||||
'**/files/test_file_*.txt',
|
||||
])
|
||||
}
|
||||
|
||||
if (!JavaVersion.current().isJava9Compatible()) {
|
||||
@@ -85,7 +82,6 @@ if (JavaVersion.current().isJava8Compatible()) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
compileJava {
|
||||
options.compilerArgs.addAll(['--release', '7'])
|
||||
}
|
||||
@@ -123,6 +119,12 @@ jar {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
java {
|
||||
withJavadocJar()
|
||||
withSourcesJar()
|
||||
}
|
||||
|
||||
sourcesJar {
|
||||
manifest {
|
||||
attributes(
|
||||
@@ -188,71 +190,82 @@ github {
|
||||
license 'Apache'
|
||||
}
|
||||
|
||||
pom {
|
||||
description "SSHv2 library for Java"
|
||||
url "https://github.com/hierynomus/sshj"
|
||||
inceptionYear "2009"
|
||||
developers {
|
||||
developer {
|
||||
id "hierynomus"
|
||||
name "Jeroen van Erp"
|
||||
email "jeroen@javadude.nl"
|
||||
roles {
|
||||
role "Lead developer"
|
||||
}
|
||||
}
|
||||
developer {
|
||||
id "shikhar"
|
||||
name "Shikhar Bhushan"
|
||||
email "shikhar@schmizz.net"
|
||||
url "http://schmizz.net"
|
||||
roles {
|
||||
role "Previous lead developer"
|
||||
}
|
||||
}
|
||||
developer {
|
||||
id "iterate"
|
||||
name "David Kocher"
|
||||
email "dkocher@iterate.ch"
|
||||
organization "iterage GmbH"
|
||||
organizationUrl "https://iterate.ch"
|
||||
roles {
|
||||
role "Developer"
|
||||
}
|
||||
}
|
||||
}
|
||||
publishing {
|
||||
publications {
|
||||
maven(MavenPublication) {
|
||||
from(components.java)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (project.hasProperty("bintrayUsername") && project.hasProperty("bintrayApiKey")) {
|
||||
bintray {
|
||||
user = project.property("bintrayUsername")
|
||||
key = project.property("bintrayApiKey")
|
||||
publish = true
|
||||
publications = ["maven"]
|
||||
pkg {
|
||||
repo = "maven"
|
||||
name = "${project.name}"
|
||||
licenses = ["Apache-2.0"]
|
||||
vcsUrl = "https://github.com/hierynomus/sshj.git"
|
||||
labels = ["ssh", "sftp", "secure-shell", "network", "file-transfer"]
|
||||
githubRepo = "hierynomus/sshj"
|
||||
version {
|
||||
name = "${project.version}"
|
||||
vcsTag = "v${project.version}"
|
||||
released = new SimpleDateFormat('yyyy-MM-dd\'T\'HH:mm:ss.SSSZZ').format(new Date())
|
||||
gpg {
|
||||
sign = true
|
||||
passphrase = project.property("signing.password")
|
||||
}
|
||||
mavenCentralSync {
|
||||
sync = true
|
||||
user = project.property("sonatypeUsername")
|
||||
password = project.property("sonatypePassword")
|
||||
close = 1
|
||||
project.signing {
|
||||
required { project.gradle.taskGraph.hasTask("release") }
|
||||
sign publishing.publications.maven
|
||||
|
||||
if (project.hasProperty("signingKeyId") || project.hasProperty("signingKey")) {
|
||||
def signingKeyId = project.findProperty("signingKeyId")
|
||||
def signingKey = project.findProperty("signingKey")
|
||||
def signingPassword = project.findProperty("signingPassword")
|
||||
if (signingKeyId) {
|
||||
useInMemoryPgpKeys(signingKeyId, signingKey, signingPassword)
|
||||
} else if (signingKey) {
|
||||
useInMemoryPgpKeys(signingKey, signingPassword)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
project.plugins.withType(MavenPublishPlugin).all {
|
||||
PublishingExtension publishing = project.extensions.getByType(PublishingExtension)
|
||||
publishing.publications.withType(MavenPublication).all { mavenPublication ->
|
||||
mavenPublication.pom {
|
||||
name = "${project.name}"
|
||||
description = 'SSHv2 library for Java'
|
||||
inceptionYear = '2009'
|
||||
url = "https://github.com/hierynomus/${project.name}"
|
||||
licenses {
|
||||
license {
|
||||
name = "The Apache License, Version 2.0"
|
||||
url = "https://www.apache.org/licenses/LICENSE-2.0"
|
||||
}
|
||||
}
|
||||
developers {
|
||||
developer {
|
||||
id = "hierynomus"
|
||||
name = "Jeroen van Erp"
|
||||
email = "jeroen@hierynomus.com"
|
||||
}
|
||||
developer {
|
||||
id = "shikhar"
|
||||
name = "Shikhar Bhushan"
|
||||
email = "shikhar@schmizz.net"
|
||||
url = "http://schmizz.net"
|
||||
roles = ["Previous Lead developer"]
|
||||
}
|
||||
developer {
|
||||
id = "iterate"
|
||||
name = "David Kocher"
|
||||
email = "dkocher@iterate.ch"
|
||||
organization = "iterate GmbH"
|
||||
organizationUrl = "https://iterate.ch"
|
||||
roles = ["Developer"]
|
||||
}
|
||||
}
|
||||
scm {
|
||||
url = "https://github.com/hierynomus/${project.name}"
|
||||
connection = "scm:git@github.com:hierynomus/${project.name}.git"
|
||||
developerConnection = "scm:git@github.com:hierynomus/${project.name}.git"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nexusPublishing {
|
||||
repositories {
|
||||
sonatype() //sonatypeUsername and sonatypePassword properties are used automatically
|
||||
}
|
||||
|
||||
connectTimeout = Duration.ofMinutes(3)
|
||||
clientTimeout = Duration.ofMinutes(3)
|
||||
}
|
||||
|
||||
jacocoTestReport {
|
||||
@@ -262,48 +275,11 @@ jacocoTestReport {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
task buildItestImage(type: DockerBuildImage) {
|
||||
inputDir = file('src/itest/docker-image')
|
||||
images.add('sshj/sshd-itest:latest')
|
||||
}
|
||||
|
||||
task createItestContainer(type: DockerCreateContainer) {
|
||||
dependsOn buildItestImage
|
||||
targetImageId buildItestImage.getImageId()
|
||||
hostConfig.portBindings = ['2222:22']
|
||||
hostConfig.autoRemove = true
|
||||
}
|
||||
|
||||
task startItestContainer(type: DockerStartContainer) {
|
||||
dependsOn createItestContainer
|
||||
targetContainerId createItestContainer.getContainerId()
|
||||
}
|
||||
|
||||
task logItestContainer(type: DockerLogsContainer) {
|
||||
dependsOn createItestContainer
|
||||
targetContainerId createItestContainer.getContainerId()
|
||||
showTimestamps = true
|
||||
stdErr = true
|
||||
stdOut = true
|
||||
tailAll = true
|
||||
}
|
||||
|
||||
task stopItestContainer(type: DockerStopContainer) {
|
||||
targetContainerId createItestContainer.getContainerId()
|
||||
}
|
||||
|
||||
task forkedUploadRelease(type: GradleBuild) {
|
||||
buildFile = project.buildFile
|
||||
tasks = ["bintrayUpload"]
|
||||
tasks = ["clean", "publishToSonatype", "closeAndReleaseSonatypeStagingRepository"]
|
||||
}
|
||||
|
||||
project.tasks.integrationTest.dependsOn(startItestContainer)
|
||||
project.tasks.integrationTest.finalizedBy(stopItestContainer)
|
||||
|
||||
// Being enabled, it pollutes logs on CI. Uncomment when debugging some test to get sshd logs.
|
||||
// project.tasks.stopItestContainer.dependsOn(logItestContainer)
|
||||
|
||||
project.tasks.release.dependsOn([project.tasks.integrationTest, project.tasks.build])
|
||||
project.tasks.release.finalizedBy(project.tasks.forkedUploadRelease)
|
||||
project.tasks.jacocoTestReport.dependsOn(project.tasks.test)
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
<groupId>com.hierynomus</groupId>
|
||||
<artifactId>sshj-examples</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
<version>0.19.1</version>
|
||||
<version>0.33.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();
|
||||
|
||||
|
||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,5 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.4-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
FROM sickp/alpine-sshd:7.5-r2
|
||||
|
||||
ADD authorized_keys /home/sshj/.ssh/authorized_keys
|
||||
|
||||
ADD test-container/ssh_host_ecdsa_key /etc/ssh/ssh_host_ecdsa_key
|
||||
ADD test-container/ssh_host_ecdsa_key.pub /etc/ssh/ssh_host_ecdsa_key.pub
|
||||
ADD test-container/ssh_host_ed25519_key /etc/ssh/ssh_host_ed25519_key
|
||||
ADD test-container/ssh_host_ed25519_key.pub /etc/ssh/ssh_host_ed25519_key.pub
|
||||
ADD test-container/sshd_config /etc/ssh/sshd_config
|
||||
COPY test-container/trusted_ca_keys /etc/ssh/trusted_ca_keys
|
||||
COPY test-container/host_keys/* /etc/ssh/
|
||||
|
||||
RUN apk add --no-cache tini
|
||||
RUN \
|
||||
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
|
||||
|
||||
ENTRYPOINT ["/sbin/tini", "/entrypoint.sh", "-o", "LogLevel=DEBUG2"]
|
||||
@@ -3,6 +3,7 @@ ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBHQiZm0w
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDAdJiRkkBM8yC8seTEoAn2PfwbLKrkcahZ0xxPoWICJ root@sshj
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJ8ww4hJG/gHJYdkjTTBDF1GNz+228nuWprPV+NbQauA ajvanerp@Heimdall.local
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOaWrwt3drIOjeBq2LSHRavxAT7ja2f+5soOUJl/zKSI ajvanerp@Heimdall.xebialabs.com
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICYfPGSYFOHuSzTJ67H0ynvKJDfgDmwPOj7iJaLGbIBi sshjtest@TranceLove
|
||||
ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAoZ9l6Tkm2aL1tSBy2yw4xU5s8BE9MfqS/4J7DzvsYJxF6oQmTIjmStuhH/CT7UjuDtKXdXZUsIhKtafiizxGO8kHSzKDeitpth2RSr8ddMzZKyD6RNs7MfsgjA3UTtrrSrCXEY6O43S2cnuJrWzkPxtwxaQ3zOvDbS2tiulzyq0VzYmuhA/a4CyuQtJBuu+P2oqmu6pU/VB6IzONpvBvYbNPsH1WDmP7zko5wHPihXPCliztspKxS4DRtOZ7BGXyvg44UmIy0Kf4jOkaBV/eCCA4qH7ZHz71/5ceMOpszPcNOEmLGGYhwI+P3OuGMpkrSAv1f8IY6R8spZNncP6UaQ== no-passphrase
|
||||
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDKRyZAtOJJfAhPU6xE6ZXY564vwErAI3n3Yn4lTHL9bxev9Ily6eCqPLcV0WbSV04pztngFn9MjT7yb8mcXheHpIaWEH569sMpmpOtyfn4p68SceuXBGyyPGMIcfOTknkASd1JYSD4EPkd9rZmCzcx3vEnLu8ChnA/G221xSVQ5VC/jD/c/CgNUayhQ+xbn57qHKKtZwfTa21QmwIabGYJNwlVjlKTCdddeVnZfKqKrG7cxHQApsxd21rhM9IT/C/f4Y/Tx3WUUVeam0iZ265oiPHoPALqJIWSQIUheRYAxYAQqJwSQ0Or9MM8XXun2Iy3RUSGk6eIvrCsFbNURsHNs7Pu0UnpYv6FZ3vCkFep/1pAT6fQvY7pDOOWDHKXArD4watc9gIWaQBH73wDW/KgBcnMRSoGWgQjsYqIamP4oV1+HqUI3lRAsXZaX+eiBGt3+3A5KebP27UJ1YUwhwlzs7wzTKaCu0OaL+hOsP1F2AxAa995bgFksMd23645ux3YCJKXG4sGpJ1Z/Hs49K72gv+QjLZVxXqY623c8+3OUhlixqoEFd4iG7UMc5a552ch/VA+jaspmLZoFhPz99aBRVb1oCSPxSwLw+Q/wxv6pZmT+14rqTzY2farjU53hM+CsUPh7dnWXhGG7RuA5wCdeOXOYjuksfzAoHIZhPqTgQ== ajvanerp@Heimdall.local
|
||||
ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBMvfRYSe44VQGwxexOMibcM3+fWeUP1jrBofOxFDRRrzRF8dK/vll2svqTPXMRnITnT1UoemEcB5OHtvH4hzfh/HFeDxJ5S7UncYxoClTSa8MeMFG2Zj9CoUZs1SHbwSGg== root@sshj
|
||||
|
||||
@@ -1,158 +0,0 @@
|
||||
# $OpenBSD: sshd_config,v 1.101 2017/03/14 07:19:07 djm Exp $
|
||||
|
||||
# This is the sshd server system-wide configuration file. See
|
||||
# sshd_config(5) for more information.
|
||||
|
||||
# This sshd was compiled with PATH=/bin:/usr/bin:/sbin:/usr/sbin
|
||||
|
||||
# The strategy used for options in the default sshd_config shipped with
|
||||
# OpenSSH is to specify options with their default value where
|
||||
# possible, but leave them commented. Uncommented options override the
|
||||
# default value.
|
||||
|
||||
#Port 22
|
||||
#AddressFamily any
|
||||
#ListenAddress 0.0.0.0
|
||||
#ListenAddress ::
|
||||
|
||||
#HostKey /etc/ssh/ssh_host_rsa_key
|
||||
#HostKey /etc/ssh/ssh_host_dsa_key
|
||||
#HostKey /etc/ssh/ssh_host_ecdsa_key
|
||||
#HostKey /etc/ssh/ssh_host_ed25519_key
|
||||
|
||||
# Ciphers and keying
|
||||
#RekeyLimit default none
|
||||
|
||||
# Logging
|
||||
#SyslogFacility AUTH
|
||||
#LogLevel INFO
|
||||
|
||||
# Authentication:
|
||||
|
||||
#LoginGraceTime 2m
|
||||
PermitRootLogin yes
|
||||
#StrictModes yes
|
||||
#MaxAuthTries 6
|
||||
#MaxSessions 10
|
||||
|
||||
#PubkeyAuthentication yes
|
||||
|
||||
# The default is to check both .ssh/authorized_keys and .ssh/authorized_keys2
|
||||
# but this is overridden so installations will only check .ssh/authorized_keys
|
||||
AuthorizedKeysFile .ssh/authorized_keys
|
||||
|
||||
#AuthorizedPrincipalsFile none
|
||||
|
||||
#AuthorizedKeysCommand none
|
||||
#AuthorizedKeysCommandUser nobody
|
||||
|
||||
# For this to work you will also need host keys in /etc/ssh/ssh_known_hosts
|
||||
#HostbasedAuthentication no
|
||||
# Change to yes if you don't trust ~/.ssh/known_hosts for
|
||||
# HostbasedAuthentication
|
||||
#IgnoreUserKnownHosts no
|
||||
# Don't read the user's ~/.rhosts and ~/.shosts files
|
||||
#IgnoreRhosts yes
|
||||
|
||||
# To disable tunneled clear text passwords, change to no here!
|
||||
#PasswordAuthentication yes
|
||||
#PermitEmptyPasswords no
|
||||
|
||||
# Change to no to disable s/key passwords
|
||||
#ChallengeResponseAuthentication yes
|
||||
|
||||
# Kerberos options
|
||||
#KerberosAuthentication no
|
||||
#KerberosOrLocalPasswd yes
|
||||
#KerberosTicketCleanup yes
|
||||
#KerberosGetAFSToken no
|
||||
|
||||
# GSSAPI options
|
||||
#GSSAPIAuthentication no
|
||||
#GSSAPICleanupCredentials yes
|
||||
|
||||
# Set this to 'yes' to enable PAM authentication, account processing,
|
||||
# and session processing. If this is enabled, PAM authentication will
|
||||
# be allowed through the ChallengeResponseAuthentication and
|
||||
# PasswordAuthentication. Depending on your PAM configuration,
|
||||
# PAM authentication via ChallengeResponseAuthentication may bypass
|
||||
# the setting of "PermitRootLogin without-password".
|
||||
# If you just want the PAM account and session checks to run without
|
||||
# PAM authentication, then enable this but set PasswordAuthentication
|
||||
# and ChallengeResponseAuthentication to 'no'.
|
||||
#UsePAM no
|
||||
|
||||
#AllowAgentForwarding yes
|
||||
#AllowTcpForwarding yes
|
||||
#GatewayPorts no
|
||||
#X11Forwarding no
|
||||
#X11DisplayOffset 10
|
||||
#X11UseLocalhost yes
|
||||
#PermitTTY yes
|
||||
#PrintMotd yes
|
||||
#PrintLastLog yes
|
||||
#TCPKeepAlive yes
|
||||
#UseLogin no
|
||||
#PermitUserEnvironment no
|
||||
#Compression delayed
|
||||
#ClientAliveInterval 0
|
||||
#ClientAliveCountMax 3
|
||||
#UseDNS no
|
||||
#PidFile /run/sshd.pid
|
||||
#MaxStartups 10:30:100
|
||||
#PermitTunnel no
|
||||
#ChrootDirectory none
|
||||
#VersionAddendum none
|
||||
|
||||
# no default banner path
|
||||
#Banner none
|
||||
|
||||
# override default of no subsystems
|
||||
Subsystem sftp /usr/lib/ssh/sftp-server
|
||||
|
||||
# the following are HPN related configuration options
|
||||
# tcp receive buffer polling. disable in non autotuning kernels
|
||||
#TcpRcvBufPoll yes
|
||||
|
||||
# disable hpn performance boosts
|
||||
#HPNDisabled no
|
||||
|
||||
# buffer size for hpn to non-hpn connections
|
||||
#HPNBufferSize 2048
|
||||
|
||||
|
||||
# Example of overriding settings on a per-user basis
|
||||
#Match User anoncvs
|
||||
# X11Forwarding no
|
||||
# AllowTcpForwarding no
|
||||
# PermitTTY no
|
||||
# ForceCommand cvs server
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
TrustedUserCAKeys /etc/ssh/trusted_ca_keys
|
||||
|
||||
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
|
||||
|
||||
HostKey /etc/ssh/ssh_host_rsa_key
|
||||
HostKey /etc/ssh/ssh_host_dsa_key
|
||||
HostKey /etc/ssh/ssh_host_ecdsa_key
|
||||
HostKey /etc/ssh/ssh_host_ed25519_key
|
||||
|
||||
HostKey /etc/ssh/ssh_host_ecdsa_256_key
|
||||
HostCertificate /etc/ssh/ssh_host_ecdsa_256_key-cert.pub
|
||||
|
||||
HostKey /etc/ssh/ssh_host_ecdsa_384_key
|
||||
HostCertificate /etc/ssh/ssh_host_ecdsa_384_key-cert.pub
|
||||
|
||||
HostKey /etc/ssh/ssh_host_ecdsa_521_key
|
||||
HostCertificate /etc/ssh/ssh_host_ecdsa_521_key-cert.pub
|
||||
|
||||
HostKey /etc/ssh/ssh_host_ed25519_384_key
|
||||
HostCertificate /etc/ssh/ssh_host_ed25519_384_key-cert.pub
|
||||
|
||||
HostKey /etc/ssh/ssh_host_rsa_2048_key
|
||||
HostCertificate /etc/ssh/ssh_host_rsa_2048_key-cert.pub
|
||||
|
||||
LogLevel DEBUG2
|
||||
@@ -1,42 +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 net.schmizz.sshj.Config
|
||||
import net.schmizz.sshj.DefaultConfig
|
||||
import net.schmizz.sshj.SSHClient
|
||||
import net.schmizz.sshj.transport.verification.PromiscuousVerifier
|
||||
import spock.lang.Specification
|
||||
|
||||
class IntegrationBaseSpec extends Specification {
|
||||
protected static final int DOCKER_PORT = 2222
|
||||
protected static final String USERNAME = "sshj"
|
||||
protected static final String KEYFILE = "src/itest/resources/keyfiles/id_rsa"
|
||||
protected final static String SERVER_IP = System.getProperty("serverIP", "127.0.0.1")
|
||||
|
||||
protected static SSHClient getConnectedClient(Config config) {
|
||||
SSHClient sshClient = new SSHClient(config)
|
||||
sshClient.addHostKeyVerifier(new PromiscuousVerifier())
|
||||
sshClient.connect(SERVER_IP, DOCKER_PORT)
|
||||
|
||||
return sshClient
|
||||
}
|
||||
|
||||
protected static SSHClient getConnectedClient() throws IOException {
|
||||
return getConnectedClient(new DefaultConfig())
|
||||
}
|
||||
|
||||
}
|
||||
@@ -20,9 +20,15 @@ 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 IntegrationBaseSpec {
|
||||
class IntegrationSpec extends Specification {
|
||||
@Shared
|
||||
@ClassRule
|
||||
SshdContainer sshd
|
||||
|
||||
@Unroll
|
||||
def "should accept correct key for #signatureName"() {
|
||||
@@ -33,7 +39,7 @@ class IntegrationSpec extends IntegrationBaseSpec {
|
||||
sshClient.addHostKeyVerifier(fingerprint) // test-containers/ssh_host_ecdsa_key's fingerprint
|
||||
|
||||
when:
|
||||
sshClient.connect(SERVER_IP, DOCKER_PORT)
|
||||
sshClient.connect(sshd.containerIpAddress, sshd.firstMappedPort)
|
||||
|
||||
then:
|
||||
sshClient.isConnected()
|
||||
@@ -50,7 +56,7 @@ class IntegrationSpec extends IntegrationBaseSpec {
|
||||
sshClient.addHostKeyVerifier("d4:6a:a9:52:05:ab:b5:48:dd:73:60:18:0c:3a:f0:a3")
|
||||
|
||||
when:
|
||||
sshClient.connect(SERVER_IP, DOCKER_PORT)
|
||||
sshClient.connect(sshd.containerIpAddress, sshd.firstMappedPort)
|
||||
|
||||
then:
|
||||
thrown(TransportException.class)
|
||||
@@ -59,11 +65,11 @@ class IntegrationSpec extends IntegrationBaseSpec {
|
||||
@Unroll
|
||||
def "should authenticate with key #key"() {
|
||||
given:
|
||||
SSHClient client = getConnectedClient()
|
||||
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(USERNAME, keyProvider)
|
||||
client.authPublickey(IntegrationTestUtil.USERNAME, keyProvider)
|
||||
|
||||
then:
|
||||
client.isAuthenticated()
|
||||
@@ -74,6 +80,7 @@ class IntegrationSpec extends IntegrationBaseSpec {
|
||||
"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
|
||||
@@ -83,7 +90,7 @@ class IntegrationSpec extends IntegrationBaseSpec {
|
||||
|
||||
def "should not authenticate with wrong key"() {
|
||||
given:
|
||||
SSHClient client = getConnectedClient()
|
||||
SSHClient client = sshd.getConnectedClient()
|
||||
|
||||
when:
|
||||
client.authPublickey("sshj", "src/itest/resources/keyfiles/id_unknown_key")
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
class IntegrationTestUtil {
|
||||
static final String USERNAME = "sshj"
|
||||
static final String KEYFILE = "src/itest/resources/keyfiles/id_rsa"
|
||||
}
|
||||
74
src/itest/groovy/com/hierynomus/sshj/ManyChannelsSpec.groovy
Normal file
74
src/itest/groovy/com/hierynomus/sshj/ManyChannelsSpec.groovy
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 net.schmizz.sshj.SSHClient
|
||||
import net.schmizz.sshj.common.IOUtils
|
||||
import net.schmizz.sshj.connection.channel.direct.Session
|
||||
import spock.lang.Specification
|
||||
|
||||
import java.util.concurrent.*
|
||||
|
||||
import static org.codehaus.groovy.runtime.IOGroovyMethods.withCloseable
|
||||
|
||||
class ManyChannelsSpec extends Specification {
|
||||
|
||||
def "should work with many channels without nonexistent channel error (GH issue #805)"() {
|
||||
given:
|
||||
SshdContainer sshd = new SshdContainer.Builder()
|
||||
.withSshdConfig("""${SshdContainer.Builder.DEFAULT_SSHD_CONFIG}
|
||||
MaxSessions 200
|
||||
""".stripMargin())
|
||||
.build()
|
||||
sshd.start()
|
||||
SSHClient client = sshd.getConnectedClient()
|
||||
client.authPublickey("sshj", "src/test/resources/id_rsa")
|
||||
|
||||
when:
|
||||
List<Future<Exception>> futures = []
|
||||
ExecutorService executorService = Executors.newCachedThreadPool()
|
||||
|
||||
for (int i in 0..20) {
|
||||
futures.add(executorService.submit((Callable<Exception>) {
|
||||
return execute(client)
|
||||
}))
|
||||
}
|
||||
executorService.shutdown()
|
||||
executorService.awaitTermination(1, TimeUnit.DAYS)
|
||||
|
||||
then:
|
||||
futures*.get().findAll { it != null }.empty
|
||||
|
||||
cleanup:
|
||||
client.close()
|
||||
}
|
||||
|
||||
|
||||
private static Exception execute(SSHClient sshClient) {
|
||||
try {
|
||||
for (def i in 0..100) {
|
||||
withCloseable (sshClient.startSession()) {sshSession ->
|
||||
Session.Command sshCommand = sshSession.exec("ls -la")
|
||||
IOUtils.readFully(sshCommand.getInputStream()).toString()
|
||||
sshCommand.close()
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
return e
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
/*
|
||||
* 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()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* 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 org.testcontainers.containers.wait.strategy.WaitStrategy;
|
||||
import org.testcontainers.containers.wait.strategy.WaitStrategyTarget;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Socket;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* A wait strategy designed for {@link SshdContainer} to wait until the SSH server is ready, to avoid races when a test
|
||||
* tries to connect to a server before the server has started.
|
||||
*/
|
||||
public class SshServerWaitStrategy implements WaitStrategy {
|
||||
private Duration startupTimeout = Duration.ofMinutes(1);
|
||||
|
||||
@Override
|
||||
public void waitUntilReady(WaitStrategyTarget waitStrategyTarget) {
|
||||
long expectedEnd = System.nanoTime() + startupTimeout.toNanos();
|
||||
while (waitStrategyTarget.isRunning()) {
|
||||
long attemptStart = System.nanoTime();
|
||||
IOException error = null;
|
||||
byte[] buffer = new byte[7];
|
||||
try (Socket socket = new Socket()) {
|
||||
socket.setSoTimeout(500);
|
||||
socket.connect(new InetSocketAddress(
|
||||
waitStrategyTarget.getHost(), waitStrategyTarget.getFirstMappedPort()));
|
||||
// Haven't seen any SSH server that sends the version in two or more packets.
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
socket.getInputStream().read(buffer);
|
||||
if (!Arrays.equals(buffer, "SSH-2.0".getBytes(StandardCharsets.UTF_8))) {
|
||||
error = new IOException("The version message doesn't look like an SSH server version");
|
||||
}
|
||||
} catch (IOException err) {
|
||||
error = err;
|
||||
}
|
||||
|
||||
if (error == null) {
|
||||
break;
|
||||
} else if (System.nanoTime() >= expectedEnd) {
|
||||
throw new RuntimeException(error);
|
||||
}
|
||||
|
||||
try {
|
||||
//noinspection BusyWait
|
||||
Thread.sleep(Math.max(0L, 500L - (System.nanoTime() - attemptStart) / 1_000_000));
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public WaitStrategy withStartupTimeout(Duration startupTimeout) {
|
||||
this.startupTimeout = startupTimeout;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
155
src/itest/groovy/com/hierynomus/sshj/SshdContainer.java
Normal file
155
src/itest/groovy/com/hierynomus/sshj/SshdContainer.java
Normal file
@@ -0,0 +1,155 @@
|
||||
/*
|
||||
* 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());
|
||||
}
|
||||
}
|
||||
@@ -15,21 +15,28 @@
|
||||
*/
|
||||
package com.hierynomus.sshj.sftp
|
||||
|
||||
import com.hierynomus.sshj.IntegrationBaseSpec
|
||||
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 IntegrationBaseSpec {
|
||||
class FileWriteSpec extends Specification {
|
||||
@Shared
|
||||
@ClassRule
|
||||
SshdContainer sshd
|
||||
|
||||
def "should append to file (GH issue #390)"() {
|
||||
given:
|
||||
SSHClient client = getConnectedClient()
|
||||
SSHClient client = sshd.getConnectedClient()
|
||||
client.authPublickey("sshj", "src/test/resources/id_rsa")
|
||||
SFTPClient sftp = client.newSFTPClient()
|
||||
def file = "/home/sshj/test.txt"
|
||||
|
||||
@@ -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 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",
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -15,29 +15,34 @@
|
||||
*/
|
||||
package com.hierynomus.sshj.signature
|
||||
|
||||
import com.hierynomus.sshj.IntegrationBaseSpec
|
||||
import com.hierynomus.sshj.SshdContainer
|
||||
import net.schmizz.sshj.DefaultConfig
|
||||
import net.schmizz.sshj.SSHClient
|
||||
import net.schmizz.sshj.transport.verification.OpenSSHKnownHosts
|
||||
import net.schmizz.sshj.transport.verification.PromiscuousVerifier
|
||||
import org.junit.ClassRule
|
||||
import spock.lang.Shared
|
||||
import spock.lang.Specification
|
||||
import spock.lang.Unroll
|
||||
|
||||
import java.nio.file.Files
|
||||
import java.util.stream.Collectors
|
||||
|
||||
/**
|
||||
* 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 KeyWithCertificateSpec extends IntegrationBaseSpec {
|
||||
class PublicKeyAuthWithCertificateSpec extends Specification {
|
||||
@Shared
|
||||
@ClassRule
|
||||
SshdContainer sshd
|
||||
|
||||
@Unroll
|
||||
def "authorising with a signed public key #keyName"() {
|
||||
given:
|
||||
def client = getConnectedClient()
|
||||
SSHClient client = new SSHClient(new DefaultConfig())
|
||||
client.addHostKeyVerifier(new PromiscuousVerifier())
|
||||
client.connect("127.0.0.1", sshd.firstMappedPort)
|
||||
|
||||
when:
|
||||
client.authPublickey(USERNAME, "src/itest/resources/keyfiles/certificates/$keyName")
|
||||
client.authPublickey("sshj", "src/itest/resources/keyfiles/certificates/$keyName")
|
||||
|
||||
then:
|
||||
client.authenticated
|
||||
@@ -73,43 +78,4 @@ class KeyWithCertificateSpec extends IntegrationBaseSpec {
|
||||
"id_ed25519_384_rfc4716_signed_by_rsa",
|
||||
]
|
||||
}
|
||||
|
||||
@Unroll
|
||||
def "accepting a signed host public key with type #hostKeyAlgo"() {
|
||||
given:
|
||||
File knownHosts = Files.createTempFile("known_hosts", "").toFile()
|
||||
knownHosts.deleteOnExit()
|
||||
|
||||
and:
|
||||
File caPubKey = new File("src/itest/resources/keyfiles/certificates/CA_rsa.pem.pub")
|
||||
String knownHostsFileContents = "" +
|
||||
"@cert-authority $SERVER_IP ${caPubKey.text}" +
|
||||
"\n@cert-authority [$SERVER_IP]:$DOCKER_PORT ${caPubKey.text}"
|
||||
knownHosts.write(knownHostsFileContents)
|
||||
|
||||
and:
|
||||
def config = new DefaultConfig()
|
||||
config.keyAlgorithms = config.keyAlgorithms.stream()
|
||||
.filter { it.name == hostKeyAlgo }
|
||||
.collect(Collectors.toList())
|
||||
SSHClient sshClient = new SSHClient(config)
|
||||
sshClient.addHostKeyVerifier(new OpenSSHKnownHosts(knownHosts))
|
||||
sshClient.connect(SERVER_IP, DOCKER_PORT)
|
||||
|
||||
when:
|
||||
sshClient.authPassword("sshj", "ultrapassword")
|
||||
|
||||
then:
|
||||
sshClient.authenticated
|
||||
|
||||
and:
|
||||
knownHosts.getText() == knownHostsFileContents
|
||||
|
||||
where:
|
||||
hostKeyAlgo << [
|
||||
"ecdsa-sha2-nistp256-cert-v01@openssh.com",
|
||||
"ssh-ed25519-cert-v01@openssh.com",
|
||||
"ssh-rsa-cert-v01@openssh.com",
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -15,18 +15,25 @@
|
||||
*/
|
||||
package com.hierynomus.sshj.signature
|
||||
|
||||
import com.hierynomus.sshj.IntegrationBaseSpec
|
||||
import net.schmizz.sshj.DefaultConfig
|
||||
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 IntegrationBaseSpec {
|
||||
class RsaSignatureClientKeySpec extends Specification {
|
||||
@Shared
|
||||
@ClassRule
|
||||
SshdContainer sshd
|
||||
|
||||
@Unroll
|
||||
def "should correctly connect using publickey auth with RSA key with signature"() {
|
||||
given:
|
||||
def client = getConnectedClient(new DefaultConfig())
|
||||
def client = sshd.getConnectedClient()
|
||||
|
||||
when:
|
||||
client.authPublickey(USERNAME, "src/itest/resources/keyfiles/id_rsa2")
|
||||
client.authPublickey(IntegrationTestUtil.USERNAME, "src/itest/resources/keyfiles/id_rsa2")
|
||||
|
||||
then:
|
||||
client.authenticated
|
||||
|
||||
@@ -15,22 +15,29 @@
|
||||
*/
|
||||
package com.hierynomus.sshj.signature
|
||||
|
||||
import com.hierynomus.sshj.IntegrationBaseSpec
|
||||
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 IntegrationBaseSpec {
|
||||
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 = getConnectedClient(cfg)
|
||||
def client = sshd.getConnectedClient(cfg)
|
||||
|
||||
when:
|
||||
client.authPublickey(USERNAME, KEYFILE)
|
||||
client.authPublickey(IntegrationTestUtil.USERNAME, IntegrationTestUtil.KEYFILE)
|
||||
|
||||
then:
|
||||
client.authenticated
|
||||
|
||||
@@ -15,21 +15,28 @@
|
||||
*/
|
||||
package com.hierynomus.sshj.transport.cipher
|
||||
|
||||
import com.hierynomus.sshj.IntegrationBaseSpec
|
||||
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 IntegrationBaseSpec {
|
||||
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 = getConnectedClient(cfg)
|
||||
def client = sshd.getConnectedClient(cfg)
|
||||
|
||||
when:
|
||||
client.authPublickey(USERNAME, KEYFILE)
|
||||
client.authPublickey(IntegrationTestUtil.USERNAME, IntegrationTestUtil.KEYFILE)
|
||||
|
||||
then:
|
||||
client.authenticated
|
||||
|
||||
@@ -15,29 +15,32 @@
|
||||
*/
|
||||
package com.hierynomus.sshj.transport.kex
|
||||
|
||||
import com.hierynomus.sshj.IntegrationBaseSpec
|
||||
import com.hierynomus.sshj.transport.mac.Macs
|
||||
import com.hierynomus.sshj.IntegrationTestUtil
|
||||
import com.hierynomus.sshj.SshdContainer
|
||||
import net.schmizz.sshj.DefaultConfig
|
||||
import net.schmizz.sshj.transport.kex.Curve25519DH
|
||||
import net.schmizz.sshj.transport.kex.Curve25519SHA256
|
||||
import net.schmizz.sshj.transport.kex.DH
|
||||
import net.schmizz.sshj.transport.kex.DHGexSHA1
|
||||
import net.schmizz.sshj.transport.kex.DHGexSHA256
|
||||
import net.schmizz.sshj.transport.kex.ECDH
|
||||
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 IntegrationBaseSpec {
|
||||
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 = getConnectedClient(cfg)
|
||||
def client = sshd.getConnectedClient(cfg)
|
||||
|
||||
when:
|
||||
client.authPublickey(USERNAME, KEYFILE)
|
||||
client.authPublickey(IntegrationTestUtil.USERNAME, IntegrationTestUtil.KEYFILE)
|
||||
|
||||
then:
|
||||
client.authenticated
|
||||
|
||||
@@ -15,21 +15,28 @@
|
||||
*/
|
||||
package com.hierynomus.sshj.transport.mac
|
||||
|
||||
import com.hierynomus.sshj.IntegrationBaseSpec
|
||||
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 IntegrationBaseSpec {
|
||||
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 = getConnectedClient(cfg)
|
||||
def client = sshd.getConnectedClient(cfg)
|
||||
|
||||
when:
|
||||
client.authPublickey(USERNAME, KEYFILE)
|
||||
client.authPublickey(IntegrationTestUtil.USERNAME, IntegrationTestUtil.KEYFILE)
|
||||
|
||||
then:
|
||||
client.authenticated
|
||||
@@ -47,10 +54,10 @@ class MacSpec extends IntegrationBaseSpec {
|
||||
given:
|
||||
def cfg = new DefaultConfig()
|
||||
cfg.setMACFactories(macFactory)
|
||||
def client = getConnectedClient(cfg)
|
||||
def client = sshd.getConnectedClient(cfg)
|
||||
|
||||
when:
|
||||
client.authPublickey(USERNAME, KEYFILE)
|
||||
client.authPublickey(IntegrationTestUtil.USERNAME, IntegrationTestUtil.KEYFILE)
|
||||
|
||||
then:
|
||||
client.authenticated
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAACmFlczEyOC1jYmMAAAAGYmNyeXB0AAAAGAAAABDfrL8SxDyrkNlsJdAmc7Z0AAAAEAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAICYfPGSYFOHuSzTJ67H0ynvKJDfgDmwPOj7iJaLGbIBiAAAAkLVqaDIfs+sPBNyy7ytdLnP/xH7Nt5FIXx3Upw6wKuMGdBzbFQLcvu60Le+SFP3uUfXE8TcHramXbH0n+UBMW6raCAKOkHUU1BtrKxPG1eKU/LBx3Bk5FxyKm7fo0XsCUmqSVK25EHOJfYq1QwIbWICkvQUNu+2Hg8/MQKoFJMentI+GqjdaG76f6Wf+aj9UwA==
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* 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.common;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Proxy;
|
||||
import java.net.Socket;
|
||||
import java.net.UnknownHostException;
|
||||
|
||||
import javax.net.SocketFactory;
|
||||
|
||||
/**
|
||||
* A {@link SocketFactory} that creates sockets using a {@link Proxy}.
|
||||
*/
|
||||
class ProxySocketFactory extends SocketFactory {
|
||||
|
||||
private Proxy proxy;
|
||||
|
||||
public ProxySocketFactory(Proxy proxy) {
|
||||
this.proxy = proxy;
|
||||
}
|
||||
|
||||
public ProxySocketFactory(Proxy.Type proxyType, InetSocketAddress proxyAddress) {
|
||||
this(new Proxy(proxyType, proxyAddress));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Socket createSocket() throws IOException {
|
||||
return new Socket(proxy);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort)
|
||||
throws IOException {
|
||||
Socket s = createSocket();
|
||||
s.bind(new InetSocketAddress(localAddress, localPort));
|
||||
s.connect(new InetSocketAddress(address, port));
|
||||
return s;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Socket createSocket(InetAddress host, int port) throws IOException {
|
||||
Socket s = createSocket();
|
||||
s.connect(new InetSocketAddress(host, port));
|
||||
return s;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Socket createSocket(String host, int port) throws IOException, UnknownHostException {
|
||||
Socket s = createSocket();
|
||||
s.connect(new InetSocketAddress(host, port));
|
||||
return s;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Socket createSocket(String host, int port, InetAddress localHost, int localPort)
|
||||
throws IOException, UnknownHostException {
|
||||
Socket s = createSocket();
|
||||
s.bind(new InetSocketAddress(localHost, localPort));
|
||||
s.connect(new InetSocketAddress(host, port));
|
||||
return s;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* 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.common;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
|
||||
public interface RemoteAddressProvider {
|
||||
/**
|
||||
* Get Remote Socket Address associated with transport connection
|
||||
*
|
||||
* @return Remote Socket Address or null when not connected
|
||||
*/
|
||||
InetSocketAddress getRemoteSocketAddress();
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* 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.common;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
|
||||
public class ThreadNameProvider {
|
||||
private static final String DISCONNECTED = "DISCONNECTED";
|
||||
|
||||
/**
|
||||
* Set Thread Name prefixed with sshj followed by class and remote address when connected
|
||||
*
|
||||
* @param thread Class of Thread being named
|
||||
* @param remoteAddressProvider Remote Address Provider associated with Thread
|
||||
*/
|
||||
public static void setThreadName(final Thread thread, final RemoteAddressProvider remoteAddressProvider) {
|
||||
final InetSocketAddress remoteSocketAddress = remoteAddressProvider.getRemoteSocketAddress();
|
||||
final String address = remoteSocketAddress == null ? DISCONNECTED : remoteSocketAddress.toString();
|
||||
final long started = System.currentTimeMillis();
|
||||
final String threadName = String.format("sshj-%s-%s-%d", thread.getClass().getSimpleName(), address, started);
|
||||
thread.setName(threadName);
|
||||
}
|
||||
}
|
||||
@@ -27,8 +27,6 @@ import java.util.List;
|
||||
|
||||
public class KeyAlgorithms {
|
||||
|
||||
public static List<String> SSH_RSA_SHA2_ALGORITHMS = Arrays.asList("rsa-sha2-512", "rsa-sha2-256");
|
||||
|
||||
public static Factory SSHRSA() { return new Factory("ssh-rsa", new SignatureRSA.FactorySSHRSA(), KeyType.RSA); }
|
||||
public static Factory SSHRSACertV01() { return new Factory("ssh-rsa-cert-v01@openssh.com", new SignatureRSA.FactoryCERT(), KeyType.RSA_CERT); }
|
||||
public static Factory RSASHA256() { return new Factory("rsa-sha2-256", new SignatureRSA.FactoryRSASHA256(), KeyType.RSA); }
|
||||
@@ -61,6 +59,10 @@ public class KeyAlgorithms {
|
||||
return algorithmName;
|
||||
}
|
||||
|
||||
public KeyType getKeyType() {
|
||||
return keyType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public KeyAlgorithm create() {
|
||||
return new BaseKeyAlgorithm(algorithmName, signatureFactory, keyType);
|
||||
|
||||
@@ -30,7 +30,7 @@ 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 org.mindrot.jbcrypt.BCrypt;
|
||||
import com.hierynomus.sshj.userauth.keyprovider.bcrypt.BCrypt;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -45,7 +45,7 @@ import java.nio.CharBuffer;
|
||||
import java.nio.charset.Charset;
|
||||
import java.security.*;
|
||||
import java.security.spec.ECPrivateKeySpec;
|
||||
import java.security.spec.RSAPrivateKeySpec;
|
||||
import java.security.spec.RSAPrivateCrtKeySpec;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
@@ -177,11 +177,11 @@ public class OpenSSHKeyV1KeyFile extends BaseFileKeyProvider {
|
||||
Arrays.fill(charBuffer.array(), '\u0000');
|
||||
Arrays.fill(byteBuffer.array(), (byte) 0);
|
||||
}
|
||||
byte[] keyiv = new byte[48];
|
||||
byte[] keyiv = new byte[cipher.getIVSize()+ cipher.getBlockSize()];
|
||||
new BCrypt().pbkdf(passphrase, opts.readBytes(), opts.readUInt32AsInt(), keyiv);
|
||||
Arrays.fill(passphrase, (byte) 0);
|
||||
byte[] key = Arrays.copyOfRange(keyiv, 0, 32);
|
||||
byte[] iv = Arrays.copyOfRange(keyiv, 32, 48);
|
||||
byte[] key = Arrays.copyOfRange(keyiv, 0, cipher.getBlockSize());
|
||||
byte[] iv = Arrays.copyOfRange(keyiv, cipher.getBlockSize(), cipher.getIVSize() + cipher.getBlockSize());
|
||||
cipher.init(Cipher.Mode.Decrypt, key, iv);
|
||||
} else {
|
||||
throw new IllegalStateException("No support for KDF '" + kdfName + "'.");
|
||||
@@ -193,6 +193,8 @@ public class OpenSSHKeyV1KeyFile extends BaseFileKeyProvider {
|
||||
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();
|
||||
}
|
||||
throw new IllegalStateException("Cipher '" + cipherName + "' not currently implemented for openssh-key-v1 format");
|
||||
}
|
||||
@@ -216,6 +218,9 @@ public class OpenSSHKeyV1KeyFile extends BaseFileKeyProvider {
|
||||
while (line != null && !line.startsWith(BEGIN)) {
|
||||
line = reader.readLine();
|
||||
}
|
||||
if (line == null) {
|
||||
return false;
|
||||
}
|
||||
line = line.substring(BEGIN.length());
|
||||
return line.startsWith(OPENSSH_PRIVATE_KEY);
|
||||
}
|
||||
@@ -245,13 +250,9 @@ public class OpenSSHKeyV1KeyFile extends BaseFileKeyProvider {
|
||||
kp = new KeyPair(publicKey, new EdDSAPrivateKey(new EdDSAPrivateKeySpec(privKey, EdDSANamedCurveTable.getByName("Ed25519"))));
|
||||
break;
|
||||
case RSA:
|
||||
BigInteger n = keyBuffer.readMPInt(); // Modulus
|
||||
keyBuffer.readMPInt(); // Public Exponent
|
||||
BigInteger d = keyBuffer.readMPInt(); // Private Exponent
|
||||
keyBuffer.readMPInt(); // iqmp (q^-1 mod p)
|
||||
keyBuffer.readMPInt(); // p (Prime 1)
|
||||
keyBuffer.readMPInt(); // q (Prime 2)
|
||||
kp = new KeyPair(publicKey, SecurityUtils.getKeyFactory(KeyAlgorithm.RSA).generatePrivate(new RSAPrivateKeySpec(n, d)));
|
||||
final RSAPrivateCrtKeySpec rsaPrivateCrtKeySpec = readRsaPrivateKeySpec(keyBuffer);
|
||||
final PrivateKey privateKey = SecurityUtils.getKeyFactory(KeyAlgorithm.RSA).generatePrivate(rsaPrivateCrtKeySpec);
|
||||
kp = new KeyPair(publicKey, privateKey);
|
||||
break;
|
||||
case ECDSA256:
|
||||
kp = new KeyPair(publicKey, createECDSAPrivateKey(kt, keyBuffer, "P-256"));
|
||||
@@ -284,6 +285,35 @@ public class OpenSSHKeyV1KeyFile extends BaseFileKeyProvider {
|
||||
ECNamedCurveSpec ecCurveSpec = new ECNamedCurveSpec(name, ecParams.getCurve(), ecParams.getG(), ecParams.getN());
|
||||
ECPrivateKeySpec pks = new ECPrivateKeySpec(s, ecCurveSpec);
|
||||
return SecurityUtils.getKeyFactory(KeyAlgorithm.ECDSA).generatePrivate(pks);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read RSA Private CRT Key Spec according to OpenSSH sshkey_private_deserialize in sshkey.c
|
||||
*
|
||||
* @param buffer Buffer
|
||||
* @return RSA Private CRT Key Specification
|
||||
* @throws Buffer.BufferException Thrown on failure to read from buffer
|
||||
*/
|
||||
private RSAPrivateCrtKeySpec readRsaPrivateKeySpec(final PlainBuffer buffer) throws Buffer.BufferException {
|
||||
final BigInteger modulus = buffer.readMPInt();
|
||||
final BigInteger publicExponent = buffer.readMPInt();
|
||||
final BigInteger privateExponent = buffer.readMPInt();
|
||||
final BigInteger crtCoefficient = buffer.readMPInt(); // iqmp (q^-1 mod p)
|
||||
final BigInteger primeP = buffer.readMPInt();
|
||||
final BigInteger primeQ = buffer.readMPInt();
|
||||
|
||||
// Calculate Prime Exponent P and Prime Exponent Q according to RFC 8017 Section 3.2
|
||||
final BigInteger primeExponentP = privateExponent.remainder(primeP.subtract(BigInteger.ONE));
|
||||
final BigInteger primeExponentQ = privateExponent.remainder(primeQ.subtract(BigInteger.ONE));
|
||||
return new RSAPrivateCrtKeySpec(
|
||||
modulus,
|
||||
publicExponent,
|
||||
privateExponent,
|
||||
primeP,
|
||||
primeQ,
|
||||
primeExponentP,
|
||||
primeExponentQ,
|
||||
crtCoefficient
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
package org.mindrot.jbcrypt;
|
||||
package com.hierynomus.sshj.userauth.keyprovider.bcrypt;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.security.DigestException;
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
@@ -136,7 +136,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 +176,7 @@ public class Promise<V, T extends Throwable> {
|
||||
}
|
||||
return val;
|
||||
} catch (InterruptedException ie) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw chainer.chain(ie);
|
||||
} finally {
|
||||
lock.unlock();
|
||||
|
||||
@@ -24,7 +24,7 @@ final class Heartbeater
|
||||
extends KeepAlive {
|
||||
|
||||
Heartbeater(ConnectionImpl conn) {
|
||||
super(conn, "heartbeater");
|
||||
super(conn, "sshj-Heartbeater");
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -20,6 +20,8 @@ import net.schmizz.sshj.connection.ConnectionImpl;
|
||||
import net.schmizz.sshj.transport.TransportException;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public abstract class KeepAlive extends Thread {
|
||||
protected final Logger log;
|
||||
protected final ConnectionImpl conn;
|
||||
@@ -33,40 +35,49 @@ 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;
|
||||
if (keepAliveInterval > 0 && getState() == State.NEW) {
|
||||
start();
|
||||
}
|
||||
notify();
|
||||
}
|
||||
|
||||
synchronized protected int getPositiveInterval()
|
||||
throws InterruptedException {
|
||||
while (keepAliveInterval <= 0) {
|
||||
wait();
|
||||
}
|
||||
return keepAliveInterval;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
log.debug("Starting {}, sending keep-alive every {} seconds", getClass().getSimpleName(), keepAliveInterval);
|
||||
log.debug("{} Started with interval [{} seconds]", getClass().getSimpleName(), keepAliveInterval);
|
||||
try {
|
||||
while (!isInterrupted()) {
|
||||
final int hi = getPositiveInterval();
|
||||
final int interval = getKeepAliveInterval();
|
||||
if (conn.getTransport().isRunning()) {
|
||||
log.debug("Sending keep-alive since {} seconds elapsed", hi);
|
||||
log.debug("{} Sending after interval [{} seconds]", getClass().getSimpleName(), interval);
|
||||
doKeepAlive();
|
||||
}
|
||||
Thread.sleep(hi * 1000);
|
||||
TimeUnit.SECONDS.sleep(interval);
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
// Interrupt signal may be catched when sleeping.
|
||||
// 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.
|
||||
// Else we're in shutdown-mode already, so don't forcibly kill the transport.
|
||||
@@ -74,9 +85,7 @@ public abstract class KeepAlive extends Thread {
|
||||
conn.getTransport().die(e);
|
||||
}
|
||||
}
|
||||
|
||||
log.debug("Stopping {}", getClass().getSimpleName());
|
||||
|
||||
log.debug("{} Stopped", getClass().getSimpleName());
|
||||
}
|
||||
|
||||
protected abstract void doKeepAlive() throws TransportException, ConnectionException;
|
||||
|
||||
@@ -37,7 +37,7 @@ public class KeepAliveRunner extends KeepAlive {
|
||||
new LinkedList<Promise<SSHPacket, ConnectionException>>();
|
||||
|
||||
KeepAliveRunner(ConnectionImpl conn) {
|
||||
super(conn, "keep-alive");
|
||||
super(conn, "sshj-KeepAliveRunner");
|
||||
}
|
||||
|
||||
synchronized public int getMaxAliveCount() {
|
||||
|
||||
@@ -26,6 +26,7 @@ import net.schmizz.sshj.transport.mac.MAC;
|
||||
import net.schmizz.sshj.transport.random.Random;
|
||||
import net.schmizz.sshj.userauth.keyprovider.FileKeyProvider;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
@@ -188,4 +189,30 @@ public class ConfigImpl
|
||||
public void setVerifyHostKeyCertificates(boolean value) {
|
||||
verifyHostKeyCertificates = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modern servers neglect the key algorithm ssh-rsa. OpenSSH 8.8 even dropped its support by default in favour
|
||||
* of rsa-sha2-*. However, there are legacy servers like Apache SSHD that don't support the newer replacements
|
||||
* for ssh-rsa.
|
||||
*
|
||||
* If ssh-rsa factory is in {@link #getKeyAlgorithms()}, this methods makes ssh-rsa key algorithm more preferred
|
||||
* than any of rsa-sha2-*. Otherwise, nothing happens.
|
||||
*/
|
||||
public void prioritizeSshRsaKeyAlgorithm() {
|
||||
List<Factory.Named<KeyAlgorithm>> keyAlgorithms = getKeyAlgorithms();
|
||||
for (int sshRsaIndex = 0; sshRsaIndex < keyAlgorithms.size(); ++ sshRsaIndex) {
|
||||
if ("ssh-rsa".equals(keyAlgorithms.get(sshRsaIndex).getName())) {
|
||||
for (int i = 0; i < sshRsaIndex; ++i) {
|
||||
final String algo = keyAlgorithms.get(i).getName();
|
||||
if ("rsa-sha2-256".equals(algo) || "rsa-sha2-512".equals(algo)) {
|
||||
keyAlgorithms = new ArrayList<>(keyAlgorithms);
|
||||
keyAlgorithms.add(i, keyAlgorithms.remove(sshRsaIndex));
|
||||
setKeyAlgorithms(keyAlgorithms);
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,11 +36,9 @@ 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;
|
||||
@@ -162,7 +160,6 @@ public class DefaultConfig
|
||||
setFileKeyProviderFactories(
|
||||
new OpenSSHKeyV1KeyFile.Factory(),
|
||||
new PKCS8KeyFile.Factory(),
|
||||
new PKCS5KeyFile.Factory(),
|
||||
new OpenSSHKeyFile.Factory(),
|
||||
new PuTTYKeyFile.Factory());
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
*/
|
||||
package net.schmizz.sshj;
|
||||
|
||||
import net.schmizz.keepalive.KeepAlive;
|
||||
import com.hierynomus.sshj.common.ThreadNameProvider;
|
||||
import net.schmizz.sshj.common.*;
|
||||
import net.schmizz.sshj.connection.Connection;
|
||||
import net.schmizz.sshj.connection.ConnectionException;
|
||||
@@ -55,6 +57,7 @@ import javax.security.auth.login.LoginContext;
|
||||
import java.io.Closeable;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.ServerSocket;
|
||||
import java.nio.charset.Charset;
|
||||
import java.security.KeyPair;
|
||||
@@ -140,7 +143,7 @@ public class SSHClient
|
||||
super(DEFAULT_PORT);
|
||||
loggerFactory = config.getLoggerFactory();
|
||||
log = loggerFactory.getLogger(getClass());
|
||||
this.trans = new TransportImpl(config, this);
|
||||
this.trans = new TransportImpl(config);
|
||||
this.auth = new UserAuthImpl(trans);
|
||||
this.conn = new ConnectionImpl(trans, config.getKeepAliveProvider());
|
||||
}
|
||||
@@ -424,6 +427,7 @@ public class SSHClient
|
||||
@Override
|
||||
public void disconnect()
|
||||
throws IOException {
|
||||
conn.getKeepAlive().interrupt();
|
||||
for (LocalPortForwarder forwarder : forwarders) {
|
||||
try {
|
||||
forwarder.close();
|
||||
@@ -441,6 +445,16 @@ public class SSHClient
|
||||
return conn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Remote Socket Address from Transport
|
||||
*
|
||||
* @return Remote Socket Address or null when not connected
|
||||
*/
|
||||
@Override
|
||||
public InetSocketAddress getRemoteSocketAddress() {
|
||||
return trans.getRemoteSocketAddress();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the character set used to communicate with the remote machine for certain strings (like paths).
|
||||
*
|
||||
@@ -537,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>
|
||||
@@ -791,7 +805,17 @@ 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();
|
||||
}
|
||||
if (trans.isKeyExchangeRequired()) {
|
||||
log.debug("Initiating Key Exchange for new connection");
|
||||
doKex();
|
||||
} else {
|
||||
log.debug("Key Exchange already completed for new connection");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -15,8 +15,6 @@
|
||||
*/
|
||||
package net.schmizz.sshj;
|
||||
|
||||
import com.hierynomus.sshj.backport.JavaVersion;
|
||||
import com.hierynomus.sshj.backport.Jdk7HttpProxySocket;
|
||||
import net.schmizz.sshj.connection.channel.Channel;
|
||||
import net.schmizz.sshj.connection.channel.direct.DirectConnection;
|
||||
|
||||
@@ -26,7 +24,6 @@ import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Proxy;
|
||||
import java.net.Socket;
|
||||
|
||||
public abstract class SocketClient {
|
||||
@@ -57,73 +54,6 @@ public abstract class SocketClient {
|
||||
return new InetSocketAddress(hostname, port);
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to a host via a proxy.
|
||||
* @param hostname The host name to connect to.
|
||||
* @param proxy The proxy to connect via.
|
||||
* @deprecated This method will be removed after v0.12.0. If you want to connect via a proxy, you can do this by injecting a {@link javax.net.SocketFactory}
|
||||
* into the SocketClient. The SocketFactory should create sockets using the {@link java.net.Socket#Socket(java.net.Proxy)} constructor.
|
||||
*/
|
||||
@Deprecated
|
||||
public void connect(String hostname, Proxy proxy) throws IOException {
|
||||
connect(hostname, defaultPort, proxy);
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to a host via a proxy.
|
||||
* @param hostname The host name to connect to.
|
||||
* @param port The port to connect to.
|
||||
* @param proxy The proxy to connect via.
|
||||
* @deprecated This method will be removed after v0.12.0. If you want to connect via a proxy, you can do this by injecting a {@link javax.net.SocketFactory}
|
||||
* into the SocketClient. The SocketFactory should create sockets using the {@link java.net.Socket#Socket(java.net.Proxy)} constructor.
|
||||
*/
|
||||
@Deprecated
|
||||
public void connect(String hostname, int port, Proxy proxy) throws IOException {
|
||||
this.hostname = hostname;
|
||||
this.port = port;
|
||||
if (JavaVersion.isJava7OrEarlier() && proxy.type() == Proxy.Type.HTTP) {
|
||||
// Java7 and earlier have no support for HTTP Connect proxies, return our custom socket.
|
||||
socket = new Jdk7HttpProxySocket(proxy);
|
||||
} else {
|
||||
socket = new Socket(proxy);
|
||||
}
|
||||
socket.connect(makeInetSocketAddress(hostname, port), connectTimeout);
|
||||
onConnect();
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to a host via a proxy.
|
||||
* @param host The host address to connect to.
|
||||
* @param proxy The proxy to connect via.
|
||||
* @deprecated This method will be removed after v0.12.0. If you want to connect via a proxy, you can do this by injecting a {@link javax.net.SocketFactory}
|
||||
* into the SocketClient. The SocketFactory should create sockets using the {@link java.net.Socket#Socket(java.net.Proxy)} constructor.
|
||||
*/
|
||||
@Deprecated
|
||||
public void connect(InetAddress host, Proxy proxy) throws IOException {
|
||||
connect(host, defaultPort, proxy);
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to a host via a proxy.
|
||||
* @param host The host address to connect to.
|
||||
* @param port The port to connect to.
|
||||
* @param proxy The proxy to connect via.
|
||||
* @deprecated This method will be removed after v0.12.0. If you want to connect via a proxy, you can do this by injecting a {@link javax.net.SocketFactory}
|
||||
* into the SocketClient. The SocketFactory should create sockets using the {@link java.net.Socket#Socket(java.net.Proxy)} constructor.
|
||||
*/
|
||||
@Deprecated
|
||||
public void connect(InetAddress host, int port, Proxy proxy) throws IOException {
|
||||
this.port = port;
|
||||
if (JavaVersion.isJava7OrEarlier() && proxy.type() == Proxy.Type.HTTP) {
|
||||
// Java7 and earlier have no support for HTTP Connect proxies, return our custom socket.
|
||||
socket = new Jdk7HttpProxySocket(proxy);
|
||||
} else {
|
||||
socket = new Socket(proxy);
|
||||
}
|
||||
socket.connect(new InetSocketAddress(host, port), connectTimeout);
|
||||
onConnect();
|
||||
}
|
||||
|
||||
public void connect(String hostname) throws IOException {
|
||||
connect(hostname, defaultPort);
|
||||
}
|
||||
|
||||
@@ -145,8 +145,14 @@ public class StreamCopier {
|
||||
final double sizeKiB = count / 1024.0;
|
||||
log.debug(String.format("%1$,.1f KiB transferred in %2$,.1f seconds (%3$,.2f KiB/s)", sizeKiB, timeSeconds, (sizeKiB / timeSeconds)));
|
||||
|
||||
if (length != -1 && read == -1)
|
||||
throw new IOException("Encountered EOF, could not transfer " + length + " bytes");
|
||||
// Did we encounter EOF?
|
||||
if (read == -1) {
|
||||
// If InputStream was closed we should also close OutputStream
|
||||
out.close();
|
||||
|
||||
if (length != -1)
|
||||
throw new IOException("Encountered EOF, could not transfer " + length + " bytes");
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -427,5 +446,8 @@ public abstract class AbstractChannel
|
||||
+ rwin + " >";
|
||||
}
|
||||
|
||||
public interface TransportRunnable {
|
||||
void run() throws TransportException, ConnectionException;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -105,6 +105,7 @@ public final class ChannelInputStream
|
||||
try {
|
||||
buf.wait();
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw (IOException) new InterruptedIOException().initCause(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import net.schmizz.sshj.transport.TransportException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
* {@link OutputStream} for channels. Buffers data upto the remote window's maximum packet size. Data can also be
|
||||
@@ -29,14 +30,14 @@ import java.io.OutputStream;
|
||||
*/
|
||||
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 boolean closed;
|
||||
private AtomicBoolean closed;
|
||||
private SSHException error;
|
||||
|
||||
private final class DataBuffer {
|
||||
@@ -46,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();
|
||||
@@ -98,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);
|
||||
@@ -118,10 +126,11 @@ 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;
|
||||
this.closed = new AtomicBoolean(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -151,24 +160,26 @@ public final class ChannelOutputStream extends OutputStream implements ErrorNoti
|
||||
|
||||
private void checkClose() throws SSHException {
|
||||
// Check whether either the Stream is closed, or the underlying channel is closed
|
||||
if (closed || !chan.isOpen()) {
|
||||
if (error != null)
|
||||
if (closed.get() || !chan.isOpen()) {
|
||||
if (error != null) {
|
||||
throw error;
|
||||
else
|
||||
throw new ConnectionException("Stream closed");
|
||||
} else {
|
||||
throwStreamClosed();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void close() throws IOException {
|
||||
// Not closed yet, and underlying channel is open to flush the data to.
|
||||
if (!closed && chan.isOpen()) {
|
||||
try {
|
||||
buffer.flush(false);
|
||||
// trans.write(new SSHPacket(Message.CHANNEL_EOF).putUInt32(chan.getRecipient()));
|
||||
} finally {
|
||||
closed = true;
|
||||
}
|
||||
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()));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,10 +15,11 @@
|
||||
*/
|
||||
package net.schmizz.sshj.connection.channel.direct;
|
||||
|
||||
import com.hierynomus.sshj.common.RemoteAddressProvider;
|
||||
import net.schmizz.sshj.common.SSHException;
|
||||
|
||||
/** A factory interface for creating SSH {@link Session session channels}. */
|
||||
public interface SessionFactory {
|
||||
public interface SessionFactory extends RemoteAddressProvider {
|
||||
|
||||
/**
|
||||
* Opens a {@code session} channel. The returned {@link Session} instance allows {@link Session#exec(String)
|
||||
@@ -27,7 +28,7 @@ public interface SessionFactory {
|
||||
*
|
||||
* @return the opened {@code session} channel
|
||||
*
|
||||
* @throws SSHException
|
||||
* @throws SSHException Thrown on session initialization failures
|
||||
* @see Session
|
||||
*/
|
||||
Session startSession()
|
||||
|
||||
@@ -142,6 +142,10 @@ public class RemotePortForwarder
|
||||
// Listen on all IPv4
|
||||
return true;
|
||||
}
|
||||
if ("0.0.0.0".equals(address) && "0:0:0:0:0:0:0:0".equals(channelForward.address)) {
|
||||
// Handle IPv4 requests on IPv6 channel forward
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ public class PacketReader extends Thread {
|
||||
this.engine = engine;
|
||||
log = engine.getLoggerFactory().getLogger(getClass());
|
||||
this.in = engine.getSubsystem().getInputStream();
|
||||
setName("sftp reader");
|
||||
setName("sshj-PacketReader");
|
||||
setDaemon(true);
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,8 @@ import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Deque;
|
||||
import java.util.LinkedList;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
@@ -220,49 +222,104 @@ public class RemoteFile
|
||||
|
||||
public class ReadAheadRemoteFileInputStream
|
||||
extends InputStream {
|
||||
private class UnconfirmedRead {
|
||||
private final long offset;
|
||||
private final Promise<Response, SFTPException> promise;
|
||||
private final int length;
|
||||
|
||||
private UnconfirmedRead(long offset, int length, Promise<Response, SFTPException> promise) {
|
||||
this.offset = offset;
|
||||
this.length = length;
|
||||
this.promise = promise;
|
||||
}
|
||||
|
||||
UnconfirmedRead(long offset, int length) throws IOException {
|
||||
this(offset, length, RemoteFile.this.asyncRead(offset, length));
|
||||
}
|
||||
|
||||
public long getOffset() {
|
||||
return offset;
|
||||
}
|
||||
|
||||
public Promise<Response, SFTPException> getPromise() {
|
||||
return promise;
|
||||
}
|
||||
|
||||
public int getLength() {
|
||||
return length;
|
||||
}
|
||||
}
|
||||
|
||||
private final byte[] b = new byte[1];
|
||||
|
||||
private final int maxUnconfirmedReads;
|
||||
private final Queue<Promise<Response, SFTPException>> unconfirmedReads = new LinkedList<Promise<Response, SFTPException>>();
|
||||
private final Queue<Long> unconfirmedReadOffsets = new LinkedList<Long>();
|
||||
private final long readAheadLimit;
|
||||
private final Deque<UnconfirmedRead> unconfirmedReads = new ArrayDeque<>();
|
||||
|
||||
private long requestOffset;
|
||||
private long responseOffset;
|
||||
private long currentOffset;
|
||||
private int maxReadLength = Integer.MAX_VALUE;
|
||||
private boolean eof;
|
||||
|
||||
public ReadAheadRemoteFileInputStream(int maxUnconfirmedReads) {
|
||||
assert 0 <= maxUnconfirmedReads;
|
||||
|
||||
this.maxUnconfirmedReads = maxUnconfirmedReads;
|
||||
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);
|
||||
}
|
||||
|
||||
public ReadAheadRemoteFileInputStream(int maxUnconfirmedReads, long fileOffset) {
|
||||
/**
|
||||
*
|
||||
* @param maxUnconfirmedReads Maximum number of unconfirmed requests to send
|
||||
* @param fileOffset Initial offset in file to read from
|
||||
* @param readAheadLimit Read ahead is disabled after this limit has been reached
|
||||
*/
|
||||
public ReadAheadRemoteFileInputStream(int maxUnconfirmedReads, long fileOffset, long readAheadLimit) {
|
||||
assert 0 <= maxUnconfirmedReads;
|
||||
assert 0 <= fileOffset;
|
||||
|
||||
this.maxUnconfirmedReads = maxUnconfirmedReads;
|
||||
this.requestOffset = this.responseOffset = fileOffset;
|
||||
this.currentOffset = fileOffset;
|
||||
this.readAheadLimit = readAheadLimit > 0 ? fileOffset + readAheadLimit : Long.MAX_VALUE;
|
||||
}
|
||||
|
||||
private ByteArrayInputStream pending = new ByteArrayInputStream(new byte[0]);
|
||||
|
||||
private boolean retrieveUnconfirmedRead(boolean blocking) throws IOException {
|
||||
if (unconfirmedReads.size() <= 0) {
|
||||
final UnconfirmedRead unconfirmedRead = unconfirmedReads.peek();
|
||||
if (unconfirmedRead == null || !blocking && !unconfirmedRead.getPromise().isDelivered()) {
|
||||
return false;
|
||||
}
|
||||
unconfirmedReads.remove(unconfirmedRead);
|
||||
|
||||
if (!blocking && !unconfirmedReads.peek().isDelivered()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
unconfirmedReadOffsets.remove();
|
||||
final Response res = unconfirmedReads.remove().retrieve(requester.getTimeoutMs(), TimeUnit.MILLISECONDS);
|
||||
final Response res = unconfirmedRead.promise.retrieve(requester.getTimeoutMs(), TimeUnit.MILLISECONDS);
|
||||
switch (res.getType()) {
|
||||
case DATA:
|
||||
int recvLen = res.readUInt32AsInt();
|
||||
responseOffset += recvLen;
|
||||
pending = new ByteArrayInputStream(res.array(), res.rpos(), recvLen);
|
||||
if (unconfirmedRead.offset == currentOffset) {
|
||||
currentOffset += recvLen;
|
||||
pending = new ByteArrayInputStream(res.array(), res.rpos(), recvLen);
|
||||
|
||||
if (recvLen < unconfirmedRead.length) {
|
||||
// The server returned a packet smaller than the client had requested.
|
||||
// It can be caused by at least one of the following:
|
||||
// * The file has been read fully. Then, few futile read requests can be sent during
|
||||
// the next read(), but the file will be downloaded correctly anyway.
|
||||
// * The server shapes the request length. Then, the read window will be adjusted,
|
||||
// and all further read-ahead requests won't be shaped.
|
||||
// * The file on the server is not a regular file, it is something like fifo.
|
||||
// Then, the window will shrink, and the client will start reading the file slower than it
|
||||
// hypothetically can. It must be a rare case, and it is not worth implementing a sort of
|
||||
// congestion control algorithm here.
|
||||
maxReadLength = recvLen;
|
||||
unconfirmedReads.clear();
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case STATUS:
|
||||
@@ -290,40 +347,31 @@ public class RemoteFile
|
||||
// we also need to go here for len <= 0, because pending may be at
|
||||
// EOF in which case it would return -1 instead of 0
|
||||
|
||||
long requestOffset;
|
||||
if (unconfirmedReads.isEmpty()) {
|
||||
requestOffset = currentOffset;
|
||||
}
|
||||
else {
|
||||
final UnconfirmedRead lastRequest = unconfirmedReads.getLast();
|
||||
requestOffset = lastRequest.offset + lastRequest.length;
|
||||
}
|
||||
while (unconfirmedReads.size() <= maxUnconfirmedReads) {
|
||||
// Send read requests as long as there is no EOF and we have not reached the maximum parallelism
|
||||
int reqLen = Math.max(1024, len); // don't be shy!
|
||||
unconfirmedReads.add(RemoteFile.this.asyncRead(requestOffset, reqLen));
|
||||
unconfirmedReadOffsets.add(requestOffset);
|
||||
int reqLen = Math.min(Math.max(1024, len), maxReadLength);
|
||||
if (readAheadLimit > requestOffset) {
|
||||
long remaining = readAheadLimit - requestOffset;
|
||||
if (reqLen > remaining) {
|
||||
reqLen = (int) remaining;
|
||||
}
|
||||
}
|
||||
unconfirmedReads.add(new UnconfirmedRead(requestOffset, reqLen));
|
||||
requestOffset += reqLen;
|
||||
if (requestOffset >= readAheadLimit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
long nextOffset = unconfirmedReadOffsets.peek();
|
||||
if (responseOffset != nextOffset) {
|
||||
|
||||
// the server could not give us all the data we needed, so
|
||||
// we try to fill the gap synchronously
|
||||
|
||||
assert responseOffset < nextOffset;
|
||||
assert 0 < (nextOffset - responseOffset);
|
||||
assert (nextOffset - responseOffset) <= Integer.MAX_VALUE;
|
||||
|
||||
byte[] buf = new byte[(int) (nextOffset - responseOffset)];
|
||||
int recvLen = RemoteFile.this.read(responseOffset, buf, 0, buf.length);
|
||||
|
||||
if (recvLen < 0) {
|
||||
eof = true;
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (0 == recvLen) {
|
||||
// avoid infinite loops
|
||||
throw new SFTPException("Unexpected response size (0), bailing out");
|
||||
}
|
||||
|
||||
responseOffset += recvLen;
|
||||
pending = new ByteArrayInputStream(buf, 0, recvLen);
|
||||
} else if (!retrieveUnconfirmedRead(true /*blocking*/)) {
|
||||
if (!retrieveUnconfirmedRead(true /*blocking*/)) {
|
||||
|
||||
// this may happen if we change prefetch strategy
|
||||
// currently, we should never get here...
|
||||
|
||||
@@ -13,14 +13,20 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.hierynomus.sshj.backport;
|
||||
package net.schmizz.sshj.sftp;
|
||||
|
||||
public class JavaVersion {
|
||||
public static boolean isJava7OrEarlier() {
|
||||
String property = System.getProperty("java.specification.version");
|
||||
float diff = Float.parseFloat(property) - 1.7f;
|
||||
public enum RenameFlags {
|
||||
OVERWRITE(1),
|
||||
ATOMIC(2),
|
||||
NATIVE(4);
|
||||
|
||||
return diff < 0.01;
|
||||
private final long flag;
|
||||
|
||||
RenameFlags(long flag) {
|
||||
this.flag = flag;
|
||||
}
|
||||
|
||||
public long longValue() {
|
||||
return flag;
|
||||
}
|
||||
}
|
||||
@@ -115,9 +115,13 @@ public class SFTPClient
|
||||
}
|
||||
}
|
||||
|
||||
public void rename(String oldpath, String newpath)
|
||||
public void rename(String oldpath, String newpath) throws IOException {
|
||||
rename(oldpath, newpath, EnumSet.noneOf(RenameFlags.class));
|
||||
}
|
||||
|
||||
public void rename(String oldpath, String newpath, Set<RenameFlags> renameFlags)
|
||||
throws IOException {
|
||||
engine.rename(oldpath, newpath);
|
||||
engine.rename(oldpath, newpath, renameFlags);
|
||||
}
|
||||
|
||||
public void rm(String filename)
|
||||
@@ -228,21 +232,41 @@ public class SFTPClient
|
||||
throws IOException {
|
||||
xfer.download(source, dest);
|
||||
}
|
||||
|
||||
public void get(String source, String dest, long byteOffset)
|
||||
throws IOException {
|
||||
xfer.download(source, dest, byteOffset);
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
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;
|
||||
@@ -68,6 +69,7 @@ public class SFTPEngine
|
||||
sub = session.startSubsystem("sftp");
|
||||
out = sub.getOutputStream();
|
||||
reader = new PacketReader(this);
|
||||
ThreadNameProvider.setThreadName(reader, ssh);
|
||||
pathHelper = new PathHelper(new PathHelper.Canonicalizer() {
|
||||
@Override
|
||||
public String canonicalize(String path)
|
||||
@@ -230,13 +232,22 @@ public class SFTPEngine
|
||||
return stat(PacketType.LSTAT, path);
|
||||
}
|
||||
|
||||
public void rename(String oldPath, String newPath)
|
||||
public void rename(String oldPath, String newPath, Set<RenameFlags> flags)
|
||||
throws IOException {
|
||||
if (operativeVersion < 1)
|
||||
throw new SFTPException("RENAME is not supported in SFTPv" + operativeVersion);
|
||||
doRequest(
|
||||
newRequest(PacketType.RENAME).putString(oldPath, sub.getRemoteCharset()).putString(newPath, sub.getRemoteCharset())
|
||||
).ensureStatusPacketIsOK();
|
||||
|
||||
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.putUInt32(renameFlagMask);
|
||||
}
|
||||
|
||||
doRequest(request).ensureStatusPacketIsOK();
|
||||
}
|
||||
|
||||
public String canonicalize(String path)
|
||||
|
||||
@@ -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");
|
||||
@@ -119,7 +143,7 @@ public class SFTPFileTransfer
|
||||
final RemoteDirectory rd = engine.openDir(remote.getPath());
|
||||
try {
|
||||
for (RemoteResourceInfo rri : rd.scan(getDownloadFilter()))
|
||||
download(listener, rri, adjusted.getChild(rri.getName()));
|
||||
download(listener, rri, adjusted.getChild(rri.getName()), 0); // not supporting individual byte offsets for these files
|
||||
} finally {
|
||||
rd.close();
|
||||
}
|
||||
@@ -128,13 +152,15 @@ public class SFTPFileTransfer
|
||||
|
||||
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();
|
||||
log.debug("Attempting to download {} with offset={}", remote.getPath(), byteOffset);
|
||||
final RemoteFile.ReadAheadRemoteFileInputStream rfis = rf.new ReadAheadRemoteFileInputStream(16, byteOffset);
|
||||
final OutputStream os = adjusted.getOutputStream(byteOffset != 0);
|
||||
try {
|
||||
new StreamCopier(rfis, os, engine.getLoggerFactory())
|
||||
.bufSize(engine.getSubsystem().getLocalMaxPacketSize())
|
||||
@@ -173,17 +199,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 +218,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 +244,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 +333,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 +348,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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,9 +117,9 @@ public class StatefulSFTPClient
|
||||
}
|
||||
|
||||
@Override
|
||||
public void rename(String oldpath, String newpath)
|
||||
public void rename(String oldpath, String newpath, Set<RenameFlags> renameFlags)
|
||||
throws IOException {
|
||||
super.rename(cwdify(oldpath), cwdify(newpath));
|
||||
super.rename(cwdify(oldpath), cwdify(newpath), renameFlags);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* 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.signature;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.math.BigInteger;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import com.hierynomus.asn1.ASN1OutputStream;
|
||||
import com.hierynomus.asn1.encodingrules.der.DEREncoder;
|
||||
import com.hierynomus.asn1.types.ASN1Object;
|
||||
import com.hierynomus.asn1.types.constructed.ASN1Sequence;
|
||||
import com.hierynomus.asn1.types.primitive.ASN1Integer;
|
||||
|
||||
import net.schmizz.sshj.common.IOUtils;
|
||||
|
||||
public abstract class AbstractSignatureDSA extends AbstractSignature {
|
||||
protected AbstractSignatureDSA(String algorithm, String signatureName) {
|
||||
super(algorithm, signatureName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get ASN.1 Signature encoded using DER Sequence of integers
|
||||
*
|
||||
* @param r DSA Signature R
|
||||
* @param s DSA Signature S
|
||||
* @return ASN.1 Encoded Signature
|
||||
* @throws IOException Thrown when failing to write signature integers
|
||||
*/
|
||||
@SuppressWarnings("rawtypes")
|
||||
protected byte[] encodeAsnSignature(final BigInteger r, final BigInteger s) throws IOException {
|
||||
List<ASN1Object> vector = new ArrayList<ASN1Object>();
|
||||
vector.add(new com.hierynomus.asn1.types.primitive.ASN1Integer(r));
|
||||
vector.add(new ASN1Integer(s));
|
||||
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
ASN1OutputStream asn1OutputStream = new ASN1OutputStream(new DEREncoder(), baos);
|
||||
try {
|
||||
asn1OutputStream.writeObject(new ASN1Sequence(vector));
|
||||
asn1OutputStream.flush();
|
||||
} finally {
|
||||
IOUtils.closeQuietly(asn1OutputStream);
|
||||
}
|
||||
|
||||
return baos.toByteArray();
|
||||
|
||||
}
|
||||
}
|
||||
@@ -15,26 +15,19 @@
|
||||
*/
|
||||
package net.schmizz.sshj.signature;
|
||||
|
||||
import com.hierynomus.asn1.encodingrules.der.DEREncoder;
|
||||
import com.hierynomus.asn1.types.ASN1Object;
|
||||
import com.hierynomus.asn1.types.constructed.ASN1Sequence;
|
||||
import com.hierynomus.asn1.types.primitive.ASN1Integer;
|
||||
import net.schmizz.sshj.common.KeyType;
|
||||
import net.schmizz.sshj.common.SSHRuntimeException;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.math.BigInteger;
|
||||
import java.security.SignatureException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* DSA {@link Signature}
|
||||
*/
|
||||
public class SignatureDSA
|
||||
extends AbstractSignature {
|
||||
extends AbstractSignatureDSA {
|
||||
|
||||
/**
|
||||
* A named factory for DSA signature
|
||||
@@ -90,32 +83,14 @@ public class SignatureDSA
|
||||
public boolean verify(byte[] sig) {
|
||||
try {
|
||||
byte[] sigBlob = extractSig(sig, "ssh-dss");
|
||||
return signature.verify(asnEncode(sigBlob));
|
||||
BigInteger r = new BigInteger(1, Arrays.copyOfRange(sigBlob, 0, 20));
|
||||
BigInteger s = new BigInteger(1, Arrays.copyOfRange(sigBlob, 20, 40));
|
||||
|
||||
return signature.verify(encodeAsnSignature(r, s));
|
||||
} catch (SignatureException e) {
|
||||
throw new SSHRuntimeException(e);
|
||||
} catch (IOException e) {
|
||||
throw new SSHRuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes the signature as a DER sequence (ASN.1 format).
|
||||
*/
|
||||
private byte[] asnEncode(byte[] sigBlob) throws IOException {
|
||||
BigInteger r = new BigInteger(1, Arrays.copyOfRange(sigBlob, 0, 20));
|
||||
BigInteger s = new BigInteger(1, Arrays.copyOfRange(sigBlob, 20, 40));
|
||||
|
||||
List<ASN1Object> vector = new ArrayList<ASN1Object>();
|
||||
vector.add(new com.hierynomus.asn1.types.primitive.ASN1Integer(r));
|
||||
vector.add(new ASN1Integer(s));
|
||||
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
com.hierynomus.asn1.ASN1OutputStream asn1OutputStream = new com.hierynomus.asn1.ASN1OutputStream(new DEREncoder(), baos);
|
||||
|
||||
asn1OutputStream.writeObject(new ASN1Sequence(vector));
|
||||
asn1OutputStream.flush();
|
||||
|
||||
|
||||
return baos.toByteArray();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,9 +15,8 @@
|
||||
*/
|
||||
package net.schmizz.sshj.signature;
|
||||
|
||||
import com.hierynomus.asn1.ASN1InputStream;
|
||||
import com.hierynomus.asn1.encodingrules.der.DERDecoder;
|
||||
import com.hierynomus.asn1.encodingrules.der.DEREncoder;
|
||||
import com.hierynomus.asn1.types.ASN1Object;
|
||||
import com.hierynomus.asn1.types.constructed.ASN1Sequence;
|
||||
import com.hierynomus.asn1.types.primitive.ASN1Integer;
|
||||
import net.schmizz.sshj.common.Buffer;
|
||||
@@ -26,15 +25,12 @@ import net.schmizz.sshj.common.KeyType;
|
||||
import net.schmizz.sshj.common.SSHRuntimeException;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.math.BigInteger;
|
||||
import java.security.SignatureException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/** ECDSA {@link Signature} */
|
||||
public class SignatureECDSA extends AbstractSignature {
|
||||
public class SignatureECDSA extends AbstractSignatureDSA {
|
||||
|
||||
/** A named factory for ECDSA-256 signature */
|
||||
public static class Factory256 implements net.schmizz.sshj.common.Factory.Named<Signature> {
|
||||
@@ -91,7 +87,7 @@ public class SignatureECDSA extends AbstractSignature {
|
||||
@Override
|
||||
public byte[] encode(byte[] sig) {
|
||||
ByteArrayInputStream bais = new ByteArrayInputStream(sig);
|
||||
com.hierynomus.asn1.ASN1InputStream asn1InputStream = new com.hierynomus.asn1.ASN1InputStream(new DERDecoder(), bais);
|
||||
ASN1InputStream asn1InputStream = new ASN1InputStream(new DERDecoder(), bais);
|
||||
try {
|
||||
ASN1Sequence sequence = asn1InputStream.readObject();
|
||||
ASN1Integer r = (ASN1Integer) sequence.get(0);
|
||||
@@ -110,35 +106,14 @@ public class SignatureECDSA extends AbstractSignature {
|
||||
public boolean verify(byte[] sig) {
|
||||
try {
|
||||
byte[] sigBlob = extractSig(sig, keyTypeName);
|
||||
return signature.verify(asnEncode(sigBlob));
|
||||
Buffer.PlainBuffer sigbuf = new Buffer.PlainBuffer(sigBlob);
|
||||
BigInteger r = sigbuf.readMPInt();
|
||||
BigInteger s = sigbuf.readMPInt();
|
||||
return signature.verify(encodeAsnSignature(r, s));
|
||||
} catch (SignatureException e) {
|
||||
throw new SSHRuntimeException(e);
|
||||
} catch (IOException e) {
|
||||
throw new SSHRuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes the signature as a DER sequence (ASN.1 format).
|
||||
*/
|
||||
private byte[] asnEncode(byte[] sigBlob) throws IOException {
|
||||
Buffer.PlainBuffer sigbuf = new Buffer.PlainBuffer(sigBlob);
|
||||
BigInteger r = sigbuf.readMPInt();
|
||||
BigInteger s = sigbuf.readMPInt();
|
||||
|
||||
|
||||
List<ASN1Object> vector = new ArrayList<ASN1Object>();
|
||||
vector.add(new ASN1Integer(r));
|
||||
vector.add(new ASN1Integer(s));
|
||||
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
com.hierynomus.asn1.ASN1OutputStream asn1OutputStream = new com.hierynomus.asn1.ASN1OutputStream(new DEREncoder(), baos);
|
||||
try {
|
||||
asn1OutputStream.writeObject(new ASN1Sequence(vector));
|
||||
asn1OutputStream.flush();
|
||||
} finally {
|
||||
IOUtils.closeQuietly(asn1OutputStream);
|
||||
}
|
||||
return baos.toByteArray();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,11 +170,22 @@ final class KeyExchanger
|
||||
private void sendKexInit()
|
||||
throws TransportException {
|
||||
log.debug("Sending SSH_MSG_KEXINIT");
|
||||
clientProposal = new Proposal(transport.getConfig());
|
||||
List<String> knownHostAlgs = findKnownHostAlgs(transport.getRemoteHost(), transport.getRemotePort());
|
||||
clientProposal = new Proposal(transport.getConfig(), knownHostAlgs);
|
||||
transport.write(clientProposal.getPacket());
|
||||
kexInitSent.set();
|
||||
}
|
||||
|
||||
private List<String> findKnownHostAlgs(String hostname, int port) {
|
||||
for (HostKeyVerifier hkv : hostVerifiers) {
|
||||
List<String> keyTypes = hkv.findExistingAlgorithms(hostname, port);
|
||||
if (keyTypes != null && !keyTypes.isEmpty()) {
|
||||
return keyTypes;
|
||||
}
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
private void sendNewKeys()
|
||||
throws TransportException {
|
||||
log.debug("Sending SSH_MSG_NEWKEYS");
|
||||
@@ -232,7 +243,6 @@ final class KeyExchanger
|
||||
negotiatedAlgs.getKeyExchangeAlgorithm());
|
||||
transport.setHostKeyAlgorithm(Factory.Named.Util.create(transport.getConfig().getKeyAlgorithms(),
|
||||
negotiatedAlgs.getSignatureAlgorithm()));
|
||||
transport.setRSASHA2Support(negotiatedAlgs.getRSASHA2Support());
|
||||
|
||||
try {
|
||||
kex.init(transport,
|
||||
|
||||
@@ -26,10 +26,8 @@ public final class NegotiatedAlgorithms {
|
||||
private final String c2sComp;
|
||||
private final String s2cComp;
|
||||
|
||||
private final boolean rsaSHA2Support;
|
||||
|
||||
NegotiatedAlgorithms(String kex, String sig, String c2sCipher, String s2cCipher, String c2sMAC, String s2cMAC,
|
||||
String c2sComp, String s2cComp, boolean rsaSHA2Support) {
|
||||
String c2sComp, String s2cComp) {
|
||||
this.kex = kex;
|
||||
this.sig = sig;
|
||||
this.c2sCipher = c2sCipher;
|
||||
@@ -38,7 +36,6 @@ public final class NegotiatedAlgorithms {
|
||||
this.s2cMAC = s2cMAC;
|
||||
this.c2sComp = c2sComp;
|
||||
this.s2cComp = s2cComp;
|
||||
this.rsaSHA2Support = rsaSHA2Support;
|
||||
}
|
||||
|
||||
public String getKeyExchangeAlgorithm() {
|
||||
@@ -73,10 +70,6 @@ public final class NegotiatedAlgorithms {
|
||||
return s2cComp;
|
||||
}
|
||||
|
||||
public boolean getRSASHA2Support() {
|
||||
return rsaSHA2Support;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return ("[ " +
|
||||
@@ -88,7 +81,6 @@ public final class NegotiatedAlgorithms {
|
||||
"s2cMAC=" + s2cMAC + "; " +
|
||||
"c2sComp=" + c2sComp + "; " +
|
||||
"s2cComp=" + s2cComp + "; " +
|
||||
"rsaSHA2Support=" + rsaSHA2Support +
|
||||
" ]");
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
*/
|
||||
package net.schmizz.sshj.transport;
|
||||
|
||||
import com.hierynomus.sshj.key.KeyAlgorithms;
|
||||
import net.schmizz.sshj.Config;
|
||||
import net.schmizz.sshj.common.Buffer;
|
||||
import net.schmizz.sshj.common.Factory;
|
||||
@@ -38,9 +37,9 @@ class Proposal {
|
||||
private final List<String> s2cComp;
|
||||
private final SSHPacket packet;
|
||||
|
||||
public Proposal(Config config) {
|
||||
public Proposal(Config config, List<String> knownHostAlgs) {
|
||||
kex = Factory.Named.Util.getNames(config.getKeyExchangeFactories());
|
||||
sig = Factory.Named.Util.getNames(config.getKeyAlgorithms());
|
||||
sig = filterKnownHostKeyAlgorithms(Factory.Named.Util.getNames(config.getKeyAlgorithms()), knownHostAlgs);
|
||||
c2sCipher = s2cCipher = Factory.Named.Util.getNames(config.getCipherFactories());
|
||||
c2sMAC = s2cMAC = Factory.Named.Util.getNames(config.getMACFactories());
|
||||
c2sComp = s2cComp = Factory.Named.Util.getNames(config.getCompressionFactories());
|
||||
@@ -127,34 +126,45 @@ class Proposal {
|
||||
public NegotiatedAlgorithms negotiate(Proposal other)
|
||||
throws TransportException {
|
||||
return new NegotiatedAlgorithms(
|
||||
firstMatch("KeyExchangeAlgorithms",
|
||||
this.getKeyExchangeAlgorithms(),
|
||||
other.getKeyExchangeAlgorithms()),
|
||||
firstMatch("HostKeyAlgorithms",
|
||||
this.getHostKeyAlgorithms(),
|
||||
other.getHostKeyAlgorithms()),
|
||||
firstMatch("Client2ServerCipherAlgorithms",
|
||||
this.getClient2ServerCipherAlgorithms(),
|
||||
other.getClient2ServerCipherAlgorithms()),
|
||||
firstMatch("Server2ClientCipherAlgorithms",
|
||||
this.getServer2ClientCipherAlgorithms(),
|
||||
other.getServer2ClientCipherAlgorithms()),
|
||||
firstMatch("Client2ServerMACAlgorithms",
|
||||
this.getClient2ServerMACAlgorithms(),
|
||||
other.getClient2ServerMACAlgorithms()),
|
||||
firstMatch("Server2ClientMACAlgorithms",
|
||||
this.getServer2ClientMACAlgorithms(),
|
||||
other.getServer2ClientMACAlgorithms()),
|
||||
firstMatch("Client2ServerCompressionAlgorithms",
|
||||
this.getClient2ServerCompressionAlgorithms(),
|
||||
other.getClient2ServerCompressionAlgorithms()),
|
||||
firstMatch("Server2ClientCompressionAlgorithms",
|
||||
this.getServer2ClientCompressionAlgorithms(),
|
||||
other.getServer2ClientCompressionAlgorithms()),
|
||||
other.getHostKeyAlgorithms().containsAll(KeyAlgorithms.SSH_RSA_SHA2_ALGORITHMS)
|
||||
firstMatch("KeyExchangeAlgorithms", this.getKeyExchangeAlgorithms(), other.getKeyExchangeAlgorithms()),
|
||||
firstMatch("HostKeyAlgorithms", this.getHostKeyAlgorithms(), other.getHostKeyAlgorithms()),
|
||||
firstMatch("Client2ServerCipherAlgorithms", this.getClient2ServerCipherAlgorithms(),
|
||||
other.getClient2ServerCipherAlgorithms()),
|
||||
firstMatch("Server2ClientCipherAlgorithms", this.getServer2ClientCipherAlgorithms(),
|
||||
other.getServer2ClientCipherAlgorithms()),
|
||||
firstMatch("Client2ServerMACAlgorithms", this.getClient2ServerMACAlgorithms(),
|
||||
other.getClient2ServerMACAlgorithms()),
|
||||
firstMatch("Server2ClientMACAlgorithms", this.getServer2ClientMACAlgorithms(),
|
||||
other.getServer2ClientMACAlgorithms()),
|
||||
firstMatch("Client2ServerCompressionAlgorithms", this.getClient2ServerCompressionAlgorithms(),
|
||||
other.getClient2ServerCompressionAlgorithms()),
|
||||
firstMatch("Server2ClientCompressionAlgorithms", this.getServer2ClientCompressionAlgorithms(),
|
||||
other.getServer2ClientCompressionAlgorithms())
|
||||
);
|
||||
}
|
||||
|
||||
private List<String> filterKnownHostKeyAlgorithms(List<String> configuredKeyAlgorithms, List<String> knownHostKeyAlgorithms) {
|
||||
if (knownHostKeyAlgorithms != null && !knownHostKeyAlgorithms.isEmpty()) {
|
||||
List<String> preferredAlgorithms = new ArrayList<String>();
|
||||
List<String> otherAlgorithms = new ArrayList<String>();
|
||||
|
||||
for (String configuredKeyAlgorithm : configuredKeyAlgorithms) {
|
||||
if (knownHostKeyAlgorithms.contains(configuredKeyAlgorithm)) {
|
||||
preferredAlgorithms.add(configuredKeyAlgorithm);
|
||||
} else {
|
||||
otherAlgorithms.add(configuredKeyAlgorithm);
|
||||
}
|
||||
}
|
||||
|
||||
preferredAlgorithms.addAll(otherAlgorithms);
|
||||
|
||||
return preferredAlgorithms;
|
||||
} else {
|
||||
return configuredKeyAlgorithms;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static String firstMatch(String ofWhat, List<String> a, List<String> b)
|
||||
throws TransportException {
|
||||
for (String aa : a) {
|
||||
|
||||
@@ -29,7 +29,7 @@ public final class Reader
|
||||
public Reader(TransportImpl trans) {
|
||||
this.trans = trans;
|
||||
log = trans.getConfig().getLoggerFactory().getLogger(getClass());
|
||||
setName("reader");
|
||||
setName("sshj-Reader");
|
||||
setDaemon(true);
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
package net.schmizz.sshj.transport;
|
||||
|
||||
import com.hierynomus.sshj.common.RemoteAddressProvider;
|
||||
import com.hierynomus.sshj.key.KeyAlgorithm;
|
||||
import net.schmizz.sshj.Config;
|
||||
import net.schmizz.sshj.Service;
|
||||
@@ -27,11 +28,12 @@ import net.schmizz.sshj.transport.verification.HostKeyVerifier;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/** Transport layer of the SSH protocol. */
|
||||
public interface Transport
|
||||
extends SSHPacketHandler {
|
||||
extends SSHPacketHandler, RemoteAddressProvider {
|
||||
|
||||
/**
|
||||
* Sets the host information and the streams to be used by this transport. Identification information is exchanged
|
||||
@@ -69,6 +71,13 @@ public interface Transport
|
||||
void doKex()
|
||||
throws TransportException;
|
||||
|
||||
/**
|
||||
* Is Key Exchange required based on current transport status
|
||||
*
|
||||
* @return Key Exchange required status
|
||||
*/
|
||||
boolean isKeyExchangeRequired();
|
||||
|
||||
/** @return the version string used by this client to identify itself to an SSH server, e.g. "SSHJ_3_0" */
|
||||
String getClientVersion();
|
||||
|
||||
@@ -85,22 +94,6 @@ public interface Transport
|
||||
*/
|
||||
void setTimeoutMs(int timeout);
|
||||
|
||||
/**
|
||||
* @return the interval in seconds at which a heartbeat message is sent to the server
|
||||
* @deprecated Moved to {@link net.schmizz.keepalive.KeepAlive#getKeepAliveInterval()}. This is accessible through the {@link net.schmizz.sshj.connection.Connection}.
|
||||
* Scheduled to be removed in 0.12.0
|
||||
*/
|
||||
@Deprecated
|
||||
int getHeartbeatInterval();
|
||||
|
||||
/**
|
||||
* @param interval the interval in seconds, {@code 0} means no heartbeat
|
||||
* @deprecated Moved to {@link net.schmizz.keepalive.KeepAlive#getKeepAliveInterval()}. This is accessible through the {@link net.schmizz.sshj.connection.Connection}.
|
||||
* Scheduled to be removed in 0.12.0
|
||||
*/
|
||||
@Deprecated
|
||||
void setHeartbeatInterval(int interval);
|
||||
|
||||
/** @return the hostname to which this transport is connected. */
|
||||
String getRemoteHost();
|
||||
|
||||
@@ -224,7 +217,7 @@ public interface Transport
|
||||
/**
|
||||
* Specify a {@code listener} that will be notified upon disconnection.
|
||||
*
|
||||
* @param listener
|
||||
* @param listener Disconnect Listener to be configured
|
||||
*/
|
||||
void setDisconnectListener(DisconnectListener listener);
|
||||
|
||||
@@ -239,5 +232,5 @@ public interface Transport
|
||||
void die(Exception e);
|
||||
|
||||
KeyAlgorithm getHostKeyAlgorithm();
|
||||
KeyAlgorithm getClientKeyAlgorithm(KeyType keyType) throws TransportException;
|
||||
List<KeyAlgorithm> getClientKeyAlgorithms(KeyType keyType) throws TransportException;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
package net.schmizz.sshj.transport;
|
||||
|
||||
import com.hierynomus.sshj.common.ThreadNameProvider;
|
||||
import com.hierynomus.sshj.key.KeyAlgorithm;
|
||||
import com.hierynomus.sshj.key.KeyAlgorithms;
|
||||
import com.hierynomus.sshj.transport.IdentificationStringParser;
|
||||
@@ -22,7 +23,6 @@ import net.schmizz.concurrent.ErrorDeliveryUtil;
|
||||
import net.schmizz.concurrent.Event;
|
||||
import net.schmizz.sshj.AbstractService;
|
||||
import net.schmizz.sshj.Config;
|
||||
import net.schmizz.sshj.SSHClient;
|
||||
import net.schmizz.sshj.Service;
|
||||
import net.schmizz.sshj.common.*;
|
||||
import net.schmizz.sshj.transport.verification.AlgorithmsVerifier;
|
||||
@@ -32,6 +32,8 @@ import org.slf4j.Logger;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
@@ -80,20 +82,12 @@ public final class TransportImpl
|
||||
|
||||
private final Reader reader;
|
||||
|
||||
/**
|
||||
* @deprecated Moved to {@link net.schmizz.sshj.SSHClient}
|
||||
*/
|
||||
@Deprecated
|
||||
private final SSHClient sshClient;
|
||||
|
||||
private final Encoder encoder;
|
||||
|
||||
private final Decoder decoder;
|
||||
|
||||
private KeyAlgorithm hostKeyAlgorithm;
|
||||
|
||||
private boolean rsaSHA2Support;
|
||||
|
||||
private final Event<TransportException> serviceAccept;
|
||||
|
||||
private final Event<TransportException> close;
|
||||
@@ -136,8 +130,8 @@ public final class TransportImpl
|
||||
public TransportImpl(Config config) {
|
||||
this.config = config;
|
||||
this.loggerFactory = config.getLoggerFactory();
|
||||
this.serviceAccept = new Event<TransportException>("service accept", TransportException.chainer, loggerFactory);
|
||||
this.close = new Event<TransportException>("transport close", TransportException.chainer, loggerFactory);
|
||||
this.serviceAccept = new Event<>("service accept", TransportException.chainer, loggerFactory);
|
||||
this.close = new Event<>("transport close", TransportException.chainer, loggerFactory);
|
||||
this.nullService = new NullService(this);
|
||||
this.service = nullService;
|
||||
this.log = loggerFactory.getLogger(getClass());
|
||||
@@ -147,29 +141,6 @@ public final class TransportImpl
|
||||
this.decoder = new Decoder(this);
|
||||
this.kexer = new KeyExchanger(this);
|
||||
this.clientID = String.format("SSH-2.0-%s", config.getVersion());
|
||||
this.sshClient = null;
|
||||
}
|
||||
|
||||
/*
|
||||
* Temporary constructor until we remove support for the set/get Heartbeat interval from transport.
|
||||
* @deprecated To be removed in 0.12.0
|
||||
*/
|
||||
@Deprecated
|
||||
public TransportImpl(Config config, SSHClient sshClient) {
|
||||
this.config = config;
|
||||
this.loggerFactory = config.getLoggerFactory();
|
||||
this.serviceAccept = new Event<TransportException>("service accept", TransportException.chainer, loggerFactory);
|
||||
this.close = new Event<TransportException>("transport close", TransportException.chainer, loggerFactory);
|
||||
this.log = loggerFactory.getLogger(getClass());
|
||||
this.nullService = new NullService(this);
|
||||
this.service = nullService;
|
||||
this.disconnectListener = this;
|
||||
this.reader = new Reader(this);
|
||||
this.encoder = new Encoder(config.getRandomFactory().create(), writeLock, loggerFactory);
|
||||
this.decoder = new Decoder(this);
|
||||
this.kexer = new KeyExchanger(this);
|
||||
this.clientID = String.format("SSH-2.0-%s", config.getVersion());
|
||||
this.sshClient = sshClient;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -194,9 +165,20 @@ public final class TransportImpl
|
||||
throw new TransportException(e);
|
||||
}
|
||||
|
||||
ThreadNameProvider.setThreadName(reader, this);
|
||||
reader.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Remote Socket Address using Connection Information
|
||||
*
|
||||
* @return Remote Socket Address or null when not connected
|
||||
*/
|
||||
@Override
|
||||
public InetSocketAddress getRemoteSocketAddress() {
|
||||
return connInfo == null ? null : new InetSocketAddress(getRemoteHost(), getRemotePort());
|
||||
}
|
||||
|
||||
/**
|
||||
* TransportImpl implements its own default DisconnectListener.
|
||||
*/
|
||||
@@ -240,7 +222,7 @@ public final class TransportImpl
|
||||
*
|
||||
* @param buffer The buffer to read from.
|
||||
* @return empty string if full ident string has not yet been received
|
||||
* @throws IOException
|
||||
* @throws IOException Thrown when protocol version is not supported
|
||||
*/
|
||||
private String readIdentification(Buffer.PlainBuffer buffer)
|
||||
throws IOException {
|
||||
@@ -272,6 +254,16 @@ public final class TransportImpl
|
||||
kexer.startKex(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Is Key Exchange required returns true when Key Exchange is not done and when Key Exchange is not ongoing
|
||||
*
|
||||
* @return Key Exchange required status
|
||||
*/
|
||||
@Override
|
||||
public boolean isKeyExchangeRequired() {
|
||||
return !kexer.isKexDone() && !kexer.isKexOngoing();
|
||||
}
|
||||
|
||||
public boolean isKexDone() {
|
||||
return kexer.isKexDone();
|
||||
}
|
||||
@@ -286,20 +278,6 @@ public final class TransportImpl
|
||||
this.timeoutMs = timeoutMs;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
public int getHeartbeatInterval() {
|
||||
log.warn("**Deprecated**: Please use: sshClient.getConnection().getKeepAlive().getKeepAliveInterval()");
|
||||
return sshClient.getConnection().getKeepAlive().getKeepAliveInterval();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
public void setHeartbeatInterval(int interval) {
|
||||
log.warn("**Deprecated**: Please use: sshClient.getConnection().getKeepAlive().setKeepAliveInterval()");
|
||||
sshClient.getConnection().getKeepAlive().setKeepAliveInterval(interval);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRemoteHost() {
|
||||
return connInfo.host;
|
||||
@@ -587,7 +565,7 @@ public final class TransportImpl
|
||||
* Got an SSH_MSG_UNIMPLEMENTED, so lets see where we're at and act accordingly.
|
||||
*
|
||||
* @param packet The 'unimplemented' packet received
|
||||
* @throws TransportException
|
||||
* @throws TransportException Thrown when key exchange is ongoing
|
||||
*/
|
||||
private void gotUnimplemented(SSHPacket packet)
|
||||
throws SSHException {
|
||||
@@ -669,21 +647,19 @@ public final class TransportImpl
|
||||
return this.hostKeyAlgorithm;
|
||||
}
|
||||
|
||||
public void setRSASHA2Support(boolean rsaSHA2Support) {
|
||||
this.rsaSHA2Support = rsaSHA2Support;
|
||||
}
|
||||
|
||||
@Override
|
||||
public KeyAlgorithm getClientKeyAlgorithm(KeyType keyType) throws TransportException {
|
||||
if (keyType != KeyType.RSA || !rsaSHA2Support) {
|
||||
return Factory.Named.Util.create(getConfig().getKeyAlgorithms(), keyType.toString());
|
||||
}
|
||||
|
||||
public List<KeyAlgorithm> getClientKeyAlgorithms(KeyType keyType) throws TransportException {
|
||||
List<Factory.Named<KeyAlgorithm>> factories = getConfig().getKeyAlgorithms();
|
||||
List<KeyAlgorithm> available = new ArrayList<>();
|
||||
if (factories != null)
|
||||
for (Factory.Named<KeyAlgorithm> f : factories)
|
||||
if (f.getName().equals("ssh-rsa") || KeyAlgorithms.SSH_RSA_SHA2_ALGORITHMS.contains(f.getName()))
|
||||
return f.create();
|
||||
throw new TransportException("Cannot find an available KeyAlgorithm for type " + keyType);
|
||||
if (
|
||||
f instanceof KeyAlgorithms.Factory && ((KeyAlgorithms.Factory) f).getKeyType().equals(keyType)
|
||||
|| !(f instanceof KeyAlgorithms.Factory) && f.getName().equals(keyType.toString())
|
||||
)
|
||||
available.add(f.create());
|
||||
if (available.isEmpty())
|
||||
throw new TransportException("Cannot find an available KeyAlgorithm for type " + keyType);
|
||||
return available;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,18 +15,16 @@
|
||||
*/
|
||||
package net.schmizz.sshj.transport.compression;
|
||||
|
||||
import com.jcraft.jzlib.Deflater;
|
||||
import com.jcraft.jzlib.GZIPException;
|
||||
import com.jcraft.jzlib.Inflater;
|
||||
import com.jcraft.jzlib.JZlib;
|
||||
import java.util.zip.DataFormatException;
|
||||
import java.util.zip.Deflater;
|
||||
import java.util.zip.Inflater;
|
||||
|
||||
import net.schmizz.sshj.common.Buffer;
|
||||
import net.schmizz.sshj.common.DisconnectReason;
|
||||
import net.schmizz.sshj.common.SSHRuntimeException;
|
||||
import net.schmizz.sshj.transport.TransportException;
|
||||
import net.schmizz.sshj.transport.compression.Compression;
|
||||
|
||||
/** ZLib based Compression. */
|
||||
public class ZlibCompression
|
||||
implements Compression {
|
||||
public class ZlibCompression implements Compression {
|
||||
|
||||
/** Named factory for the ZLib Compression. */
|
||||
public static class Factory
|
||||
@@ -52,19 +50,15 @@ public class ZlibCompression
|
||||
|
||||
@Override
|
||||
public void init(Mode mode) {
|
||||
try {
|
||||
switch (mode) {
|
||||
case DEFLATE:
|
||||
deflater = new Deflater(JZlib.Z_DEFAULT_COMPRESSION);
|
||||
break;
|
||||
case INFLATE:
|
||||
inflater = new Inflater();
|
||||
break;
|
||||
default:
|
||||
assert false;
|
||||
}
|
||||
} catch (GZIPException gze) {
|
||||
|
||||
switch (mode) {
|
||||
case DEFLATE:
|
||||
deflater = new Deflater(Deflater.DEFAULT_COMPRESSION);
|
||||
break;
|
||||
case INFLATE:
|
||||
inflater = new Inflater();
|
||||
break;
|
||||
default:
|
||||
assert false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,43 +69,28 @@ public class ZlibCompression
|
||||
|
||||
@Override
|
||||
public void compress(Buffer buffer) {
|
||||
deflater.setNextIn(buffer.array());
|
||||
deflater.setNextInIndex(buffer.rpos());
|
||||
deflater.setAvailIn(buffer.available());
|
||||
deflater.setInput(buffer.array(), buffer.rpos(), buffer.available());
|
||||
buffer.wpos(buffer.rpos());
|
||||
do {
|
||||
deflater.setNextOut(tempBuf);
|
||||
deflater.setNextOutIndex(0);
|
||||
deflater.setAvailOut(BUF_SIZE);
|
||||
final int status = deflater.deflate(JZlib.Z_PARTIAL_FLUSH);
|
||||
if (status == JZlib.Z_OK) {
|
||||
buffer.putRawBytes(tempBuf, 0, BUF_SIZE - deflater.getAvailOut());
|
||||
} else {
|
||||
throw new SSHRuntimeException("compress: deflate returned " + status);
|
||||
}
|
||||
} while (deflater.getAvailOut() == 0);
|
||||
final int len = deflater.deflate(tempBuf, 0, BUF_SIZE, Deflater.SYNC_FLUSH);
|
||||
buffer.putRawBytes(tempBuf, 0, len);
|
||||
} while (!deflater.needsInput());
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void uncompress(Buffer from, Buffer to)
|
||||
throws TransportException {
|
||||
inflater.setNextIn(from.array());
|
||||
inflater.setNextInIndex(from.rpos());
|
||||
inflater.setAvailIn(from.available());
|
||||
inflater.setInput(from.array(), from.rpos(), from.available());
|
||||
while (true) {
|
||||
inflater.setNextOut(tempBuf);
|
||||
inflater.setNextOutIndex(0);
|
||||
inflater.setAvailOut(BUF_SIZE);
|
||||
final int status = inflater.inflate(JZlib.Z_PARTIAL_FLUSH);
|
||||
switch (status) {
|
||||
case JZlib.Z_OK:
|
||||
to.putRawBytes(tempBuf, 0, BUF_SIZE - inflater.getAvailOut());
|
||||
break;
|
||||
case JZlib.Z_BUF_ERROR:
|
||||
try {
|
||||
int len = inflater.inflate(tempBuf, 0, BUF_SIZE);
|
||||
if(len > 0) {
|
||||
to.putRawBytes(tempBuf, 0, len);
|
||||
} else {
|
||||
return;
|
||||
default:
|
||||
throw new TransportException(DisconnectReason.COMPRESSION_ERROR, "uncompress: inflate returned " + status);
|
||||
}
|
||||
} catch (DataFormatException e) {
|
||||
throw new TransportException(DisconnectReason.COMPRESSION_ERROR, "uncompress: inflate returned " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,50 +15,89 @@
|
||||
*/
|
||||
package net.schmizz.sshj.transport.kex;
|
||||
|
||||
import com.hierynomus.sshj.common.KeyAlgorithm;
|
||||
import net.schmizz.sshj.common.Factory;
|
||||
import net.schmizz.sshj.common.SecurityUtils;
|
||||
import net.schmizz.sshj.transport.random.Random;
|
||||
import org.bouncycastle.asn1.x9.X9ECParameters;
|
||||
import org.bouncycastle.crypto.ec.CustomNamedCurves;
|
||||
import org.bouncycastle.jce.spec.ECParameterSpec;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.KeyPair;
|
||||
import java.security.PublicKey;
|
||||
import java.security.spec.AlgorithmParameterSpec;
|
||||
import java.util.Arrays;
|
||||
import java.security.spec.KeySpec;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
|
||||
/**
|
||||
* Key Exchange Method using Curve25519 as defined in RFC 8731
|
||||
*/
|
||||
public class Curve25519DH extends DHBase {
|
||||
|
||||
private byte[] secretKey;
|
||||
private static final String ALGORITHM = "X25519";
|
||||
|
||||
private static final int ENCODED_ALGORITHM_ID_KEY_LENGTH = 44;
|
||||
|
||||
private static final int ALGORITHM_ID_LENGTH = 12;
|
||||
|
||||
private static final int KEY_LENGTH = 32;
|
||||
|
||||
private final byte[] algorithmId = new byte[ALGORITHM_ID_LENGTH];
|
||||
|
||||
public Curve25519DH() {
|
||||
super(KeyAlgorithm.ECDSA, "ECDH");
|
||||
}
|
||||
|
||||
@Override
|
||||
void computeK(byte[] f) throws GeneralSecurityException {
|
||||
byte[] k = new byte[32];
|
||||
djb.Curve25519.curve(k, secretKey, f);
|
||||
setK(new BigInteger(1, k));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(AlgorithmParameterSpec params, Factory<Random> randomFactory) throws GeneralSecurityException {
|
||||
Random random = randomFactory.create();
|
||||
byte[] secretBytes = new byte[32];
|
||||
random.fill(secretBytes);
|
||||
byte[] publicBytes = new byte[32];
|
||||
djb.Curve25519.keygen(publicBytes, null, secretBytes);
|
||||
this.secretKey = Arrays.copyOf(secretBytes, secretBytes.length);
|
||||
setE(publicBytes);
|
||||
super(ALGORITHM, ALGORITHM);
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO want to figure out why BouncyCastle does not work.
|
||||
* @return The initialized curve25519 parameter spec
|
||||
* Compute Shared Secret Key using Diffie-Hellman Curve25519 known as X25519
|
||||
*
|
||||
* @param peerPublicKey Peer public key bytes
|
||||
* @throws GeneralSecurityException Thrown on key agreement failures
|
||||
*/
|
||||
public static AlgorithmParameterSpec getCurve25519Params() {
|
||||
X9ECParameters ecP = CustomNamedCurves.getByName("curve25519");
|
||||
return new ECParameterSpec(ecP.getCurve(), ecP.getG(), ecP.getN(), ecP.getH(), ecP.getSeed());
|
||||
@Override
|
||||
void computeK(final byte[] peerPublicKey) throws GeneralSecurityException {
|
||||
final KeyFactory keyFactory = SecurityUtils.getKeyFactory(ALGORITHM);
|
||||
final KeySpec peerPublicKeySpec = getPeerPublicKeySpec(peerPublicKey);
|
||||
final PublicKey generatedPeerPublicKey = keyFactory.generatePublic(peerPublicKeySpec);
|
||||
|
||||
agreement.doPhase(generatedPeerPublicKey, true);
|
||||
final byte[] sharedSecretKey = agreement.generateSecret();
|
||||
final BigInteger sharedSecretNumber = new BigInteger(BigInteger.ONE.signum(), sharedSecretKey);
|
||||
setK(sharedSecretNumber);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize Key Agreement with generated Public and Private Key Pair
|
||||
*
|
||||
* @param params Parameters not used
|
||||
* @param randomFactory Random Factory not used
|
||||
* @throws GeneralSecurityException Thrown on key agreement initialization failures
|
||||
*/
|
||||
@Override
|
||||
public void init(final AlgorithmParameterSpec params, final Factory<Random> randomFactory) throws GeneralSecurityException {
|
||||
final KeyPair keyPair = generator.generateKeyPair();
|
||||
agreement.init(keyPair.getPrivate());
|
||||
setPublicKey(keyPair.getPublic());
|
||||
}
|
||||
|
||||
private void setPublicKey(final PublicKey publicKey) {
|
||||
final byte[] encoded = publicKey.getEncoded();
|
||||
|
||||
// Encoded public key consists of the algorithm identifier and public key
|
||||
if (encoded.length == ENCODED_ALGORITHM_ID_KEY_LENGTH) {
|
||||
final byte[] publicKeyEncoded = new byte[KEY_LENGTH];
|
||||
System.arraycopy(encoded, ALGORITHM_ID_LENGTH, publicKeyEncoded, 0, KEY_LENGTH);
|
||||
setE(publicKeyEncoded);
|
||||
|
||||
// Save Algorithm Identifier byte array
|
||||
System.arraycopy(encoded, 0, algorithmId, 0, ALGORITHM_ID_LENGTH);
|
||||
} else {
|
||||
throw new IllegalArgumentException(String.format("X25519 unsupported public key length [%d]", encoded.length));
|
||||
}
|
||||
}
|
||||
|
||||
private KeySpec getPeerPublicKeySpec(final byte[] peerPublicKey) {
|
||||
final byte[] encodedKeySpec = new byte[ENCODED_ALGORITHM_ID_KEY_LENGTH];
|
||||
System.arraycopy(algorithmId, 0, encodedKeySpec, 0, ALGORITHM_ID_LENGTH);
|
||||
System.arraycopy(peerPublicKey, 0, encodedKeySpec, ALGORITHM_ID_LENGTH, KEY_LENGTH);
|
||||
return new X509EncodedKeySpec(encodedKeySpec);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,6 +56,6 @@ public class Curve25519SHA256 extends AbstractDHG {
|
||||
|
||||
@Override
|
||||
protected void initDH(DHBase dh) throws GeneralSecurityException {
|
||||
dh.init(Curve25519DH.getCurve25519Params(), trans.getConfig().getRandomFactory());
|
||||
dh.init(null, trans.getConfig().getRandomFactory());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@ import java.security.GeneralSecurityException;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.PublicKey;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import net.schmizz.sshj.common.Base64;
|
||||
@@ -74,6 +76,11 @@ public class FingerprintVerifier implements HostKeyVerifier {
|
||||
public boolean verify(String h, int p, PublicKey k) {
|
||||
return SecurityUtils.getFingerprint(k).equals(md5);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> findExistingAlgorithms(String hostname, int port) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
});
|
||||
} catch (SSHRuntimeException e) {
|
||||
throw e;
|
||||
@@ -120,8 +127,13 @@ public class FingerprintVerifier implements HostKeyVerifier {
|
||||
return Arrays.equals(fingerprintData, digestData);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> findExistingAlgorithms(String hostname, int port) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "FingerprintVerifier{digestAlgorithm='" + digestAlgorithm + "'}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
package net.schmizz.sshj.transport.verification;
|
||||
|
||||
import java.security.PublicKey;
|
||||
import java.util.List;
|
||||
|
||||
/** Host key verification interface. */
|
||||
public interface HostKeyVerifier {
|
||||
@@ -35,4 +36,12 @@ public interface HostKeyVerifier {
|
||||
*/
|
||||
boolean verify(String hostname, int port, PublicKey key);
|
||||
|
||||
/**
|
||||
* It is necessary to connect with the type of algorithm that matches an existing know_host entry.
|
||||
* This will allow a match when we later verify with the negotiated key {@code HostKeyVerifier.verify}
|
||||
* @param hostname remote hostname
|
||||
* @param port remote port
|
||||
* @return existing key types or empty list if no keys known for hostname
|
||||
*/
|
||||
List<String> findExistingAlgorithms(String hostname, int port);
|
||||
}
|
||||
|
||||
@@ -90,6 +90,10 @@ public class OpenSSHKnownHosts
|
||||
}
|
||||
}
|
||||
|
||||
private String adjustHostname(final String hostname, final int port) {
|
||||
String lowerHN = hostname.toLowerCase();
|
||||
return (port != 22) ? "[" + lowerHN + "]:" + port : lowerHN;
|
||||
}
|
||||
|
||||
public File getFile() {
|
||||
return khFile;
|
||||
@@ -103,7 +107,7 @@ public class OpenSSHKnownHosts
|
||||
return false;
|
||||
}
|
||||
|
||||
final String adjustedHostname = (port != 22) ? "[" + hostname + "]:" + port : hostname;
|
||||
final String adjustedHostname = adjustHostname(hostname, port);
|
||||
|
||||
boolean foundApplicableHostEntry = false;
|
||||
for (KnownHostEntry e : entries) {
|
||||
@@ -127,6 +131,34 @@ public class OpenSSHKnownHosts
|
||||
return hostKeyUnverifiableAction(adjustedHostname, key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> findExistingAlgorithms(String hostname, int port) {
|
||||
final String adjustedHostname = adjustHostname(hostname, port);
|
||||
List<String> knownHostAlgorithms = new ArrayList<String>();
|
||||
for (KnownHostEntry e : entries) {
|
||||
try {
|
||||
if (e.appliesTo(adjustedHostname)) {
|
||||
final KeyType type = e.getType();
|
||||
if (e instanceof HostEntry && ((HostEntry) e).marker == Marker.CA_CERT) {
|
||||
// Only the CA key type is known, but the type of the host key is not.
|
||||
// Adding all supported types for keys with certificates.
|
||||
for (final KeyType candidate : KeyType.values()) {
|
||||
if (candidate.getParent() != null) {
|
||||
knownHostAlgorithms.add(candidate.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
knownHostAlgorithms.add(type.toString());
|
||||
}
|
||||
}
|
||||
} catch (IOException ioe) {
|
||||
}
|
||||
}
|
||||
|
||||
return knownHostAlgorithms;
|
||||
}
|
||||
|
||||
protected boolean hostKeyUnverifiableAction(String hostname, PublicKey key) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
package net.schmizz.sshj.transport.verification;
|
||||
|
||||
import java.security.PublicKey;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public final class PromiscuousVerifier
|
||||
implements HostKeyVerifier {
|
||||
@@ -25,4 +27,9 @@ public final class PromiscuousVerifier
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> findExistingAlgorithms(String hostname, int port) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -16,10 +16,9 @@
|
||||
package net.schmizz.sshj.userauth.keyprovider;
|
||||
|
||||
/**
|
||||
* @version $Id:$
|
||||
* Key File Formats
|
||||
*/
|
||||
public enum KeyFormat {
|
||||
PKCS5,
|
||||
PKCS8,
|
||||
OpenSSH,
|
||||
OpenSSHv1,
|
||||
|
||||
@@ -27,9 +27,9 @@ public class KeyProviderUtil {
|
||||
* <p/>
|
||||
* Return values are consistent with the {@code NamedFactory} implementations in the {@code keyprovider} package.
|
||||
*
|
||||
* @param location
|
||||
* @param location File Path to key
|
||||
* @return name of the key file format
|
||||
* @throws java.io.IOException
|
||||
* @throws java.io.IOException Thrown on file processing failures
|
||||
*/
|
||||
public static KeyFormat detectKeyFileFormat(File location)
|
||||
throws IOException {
|
||||
@@ -45,7 +45,7 @@ public class KeyProviderUtil {
|
||||
* @param privateKey Private key stored in a string
|
||||
* @param separatePubKey Is the public key stored separately from the private key
|
||||
* @return name of the key file format
|
||||
* @throws java.io.IOException
|
||||
* @throws java.io.IOException Thrown on file processing failures
|
||||
*/
|
||||
public static KeyFormat detectKeyFileFormat(String privateKey, boolean separatePubKey)
|
||||
throws IOException {
|
||||
@@ -60,7 +60,7 @@ public class KeyProviderUtil {
|
||||
* @param privateKey Private key accessible through a {@code Reader}
|
||||
* @param separatePubKey Is the public key stored separately from the private key
|
||||
* @return name of the key file format
|
||||
* @throws java.io.IOException
|
||||
* @throws java.io.IOException Thrown on file processing failures
|
||||
*/
|
||||
public static KeyFormat detectKeyFileFormat(Reader privateKey, boolean separatePubKey)
|
||||
throws IOException {
|
||||
@@ -94,10 +94,8 @@ public class KeyProviderUtil {
|
||||
} else if (separatePubKey) {
|
||||
// Can delay asking for password since have unencrypted pubkey
|
||||
return KeyFormat.OpenSSH;
|
||||
} else if (header.contains("BEGIN PRIVATE KEY") || header.contains("BEGIN ENCRYPTED PRIVATE KEY")) {
|
||||
return KeyFormat.PKCS8;
|
||||
} else {
|
||||
return KeyFormat.PKCS5;
|
||||
return KeyFormat.PKCS8;
|
||||
}
|
||||
} else if (header.startsWith("PuTTY-User-Key-File-")) {
|
||||
return KeyFormat.PuTTY;
|
||||
|
||||
@@ -1,272 +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 net.schmizz.sshj.userauth.keyprovider;
|
||||
|
||||
import com.hierynomus.sshj.common.KeyAlgorithm;
|
||||
import com.hierynomus.sshj.transport.cipher.BlockCiphers;
|
||||
import net.schmizz.sshj.common.Base64;
|
||||
import net.schmizz.sshj.common.ByteArrayUtils;
|
||||
import net.schmizz.sshj.common.IOUtils;
|
||||
import net.schmizz.sshj.common.KeyType;
|
||||
import net.schmizz.sshj.transport.cipher.*;
|
||||
import net.schmizz.sshj.transport.digest.Digest;
|
||||
import net.schmizz.sshj.transport.digest.MD5;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.math.BigInteger;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.CharBuffer;
|
||||
import java.security.*;
|
||||
import java.security.spec.*;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Represents a PKCS5-encoded key file. This is the format typically used by OpenSSH, OpenSSL, Amazon, etc.
|
||||
*/
|
||||
public class PKCS5KeyFile extends BaseFileKeyProvider {
|
||||
|
||||
public static class Factory
|
||||
implements net.schmizz.sshj.common.Factory.Named<FileKeyProvider> {
|
||||
|
||||
@Override
|
||||
public FileKeyProvider create() {
|
||||
return new PKCS5KeyFile();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "PKCS5";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates a format issue with PKCS5 data
|
||||
*/
|
||||
public static class FormatException
|
||||
extends IOException {
|
||||
|
||||
FormatException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates a problem decrypting the data
|
||||
*/
|
||||
public static class DecryptException
|
||||
extends IOException {
|
||||
|
||||
DecryptException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
}
|
||||
|
||||
protected byte[] data;
|
||||
|
||||
protected KeyPair readKeyPair()
|
||||
throws IOException {
|
||||
|
||||
BufferedReader reader = new BufferedReader(resource.getReader());
|
||||
try {
|
||||
String line = null;
|
||||
Cipher cipher = new NoneCipher();
|
||||
StringBuffer sb = new StringBuffer();
|
||||
byte[] iv = new byte[0]; // salt
|
||||
while ((line = reader.readLine()) != null) {
|
||||
if (line.startsWith("-----BEGIN ") && line.endsWith(" PRIVATE KEY-----")) {
|
||||
int end = line.length() - 17;
|
||||
if (end > 11) {
|
||||
String s = line.substring(11, line.length() - 17);
|
||||
if ("RSA".equals(s)) {
|
||||
type = KeyType.RSA;
|
||||
} else if ("DSA".equals(s)) {
|
||||
type = KeyType.DSA;
|
||||
} else if ("DSS".equals(s)) {
|
||||
type = KeyType.DSA;
|
||||
} else {
|
||||
throw new FormatException("Unrecognized PKCS5 key type");
|
||||
}
|
||||
} else {
|
||||
throw new FormatException("Bad header; possibly PKCS8 format?");
|
||||
}
|
||||
} else if (line.startsWith("-----END")) {
|
||||
break;
|
||||
} else if (type != null) {
|
||||
if (line.startsWith("Proc-Type: ")) {
|
||||
if (!"4,ENCRYPTED".equals(line.substring(11))) {
|
||||
throw new FormatException("Unrecognized Proc-Type");
|
||||
}
|
||||
} else if (line.startsWith("DEK-Info: ")) {
|
||||
int ptr = line.indexOf(",");
|
||||
if (ptr == -1) {
|
||||
throw new FormatException("Unrecognized DEK-Info");
|
||||
} else {
|
||||
String algorithm = line.substring(10, ptr);
|
||||
if ("DES-EDE3-CBC".equals(algorithm)) {
|
||||
cipher = BlockCiphers.TripleDESCBC().create();
|
||||
} else if ("AES-128-CBC".equals(algorithm)) {
|
||||
cipher = BlockCiphers.AES128CBC().create();
|
||||
} else if ("AES-192-CBC".equals(algorithm)) {
|
||||
cipher = BlockCiphers.AES192CBC().create();
|
||||
} else if ("AES-256-CBC".equals(algorithm)) {
|
||||
cipher = BlockCiphers.AES256CBC().create();
|
||||
} else {
|
||||
throw new FormatException("Not a supported algorithm: " + algorithm);
|
||||
}
|
||||
iv = Arrays.copyOfRange(ByteArrayUtils.parseHex(line.substring(ptr + 1)), 0, cipher.getIVSize());
|
||||
}
|
||||
} else if (line.length() > 0) {
|
||||
sb.append(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (type == null) {
|
||||
throw new FormatException("PKCS5 header not found");
|
||||
}
|
||||
ASN1Data asn = new ASN1Data(data = decrypt(Base64.decode(sb.toString()), cipher, iv));
|
||||
switch (type) {
|
||||
case RSA: {
|
||||
KeyFactory factory = KeyFactory.getInstance(KeyAlgorithm.RSA);
|
||||
asn.readNext();
|
||||
BigInteger modulus = asn.readNext();
|
||||
BigInteger pubExp = asn.readNext();
|
||||
BigInteger prvExp = asn.readNext();
|
||||
PublicKey pubKey = factory.generatePublic(new RSAPublicKeySpec(modulus, pubExp));
|
||||
PrivateKey prvKey = factory.generatePrivate(new RSAPrivateKeySpec(modulus, prvExp));
|
||||
return new KeyPair(pubKey, prvKey);
|
||||
}
|
||||
case DSA: {
|
||||
KeyFactory factory = KeyFactory.getInstance(KeyAlgorithm.DSA);
|
||||
asn.readNext();
|
||||
BigInteger p = asn.readNext();
|
||||
BigInteger q = asn.readNext();
|
||||
BigInteger g = asn.readNext();
|
||||
BigInteger pub = asn.readNext();
|
||||
BigInteger prv = asn.readNext();
|
||||
PublicKey pubKey = factory.generatePublic(new DSAPublicKeySpec(pub, p, q, g));
|
||||
PrivateKey prvKey = factory.generatePrivate(new DSAPrivateKeySpec(prv, p, q, g));
|
||||
return new KeyPair(pubKey, prvKey);
|
||||
}
|
||||
default:
|
||||
throw new IOException("Unrecognized PKCS5 key type: " + type);
|
||||
}
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new IOException(e);
|
||||
} catch (InvalidKeySpecException e) {
|
||||
throw new IOException(e);
|
||||
} finally {
|
||||
reader.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "PKCS5KeyFile{resource=" + resource + "}";
|
||||
}
|
||||
|
||||
private byte[] getPassphraseBytes() {
|
||||
CharBuffer cb = CharBuffer.wrap(pwdf.reqPassword(resource));
|
||||
ByteBuffer bb = IOUtils.UTF8.encode(cb);
|
||||
byte[] result = Arrays.copyOfRange(bb.array(), bb.position(), bb.limit());
|
||||
Arrays.fill(cb.array(), '\u0000');
|
||||
Arrays.fill(bb.array(), (byte) 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
private byte[] decrypt(byte[] raw, Cipher cipher, byte[] iv) throws DecryptException {
|
||||
if (pwdf == null) {
|
||||
return raw;
|
||||
}
|
||||
Digest md5 = new MD5();
|
||||
int bsize = cipher.getBlockSize();
|
||||
int hsize = md5.getBlockSize();
|
||||
int hnlen = bsize / hsize * hsize + (bsize % hsize == 0 ? 0 : hsize);
|
||||
do {
|
||||
md5.init();
|
||||
byte[] hn = new byte[hnlen];
|
||||
byte[] tmp = null;
|
||||
byte[] passphrase = getPassphraseBytes();
|
||||
for (int i = 0; i + hsize <= hn.length; ) {
|
||||
if (tmp != null) {
|
||||
md5.update(tmp, 0, tmp.length);
|
||||
}
|
||||
md5.update(passphrase, 0, passphrase.length);
|
||||
md5.update(iv, 0, iv.length > 8 ? 8 : iv.length);
|
||||
tmp = md5.digest();
|
||||
System.arraycopy(tmp, 0, hn, i, tmp.length);
|
||||
i += tmp.length;
|
||||
}
|
||||
Arrays.fill(passphrase, (byte) 0);
|
||||
byte[] key = Arrays.copyOfRange(hn, 0, bsize);
|
||||
cipher.init(Cipher.Mode.Decrypt, key, iv);
|
||||
Arrays.fill(key, (byte) 0);
|
||||
byte[] decrypted = Arrays.copyOf(raw, raw.length);
|
||||
cipher.update(decrypted, 0, decrypted.length);
|
||||
if (ASN1Data.MAGIC == decrypted[0]) {
|
||||
return decrypted;
|
||||
}
|
||||
} while (pwdf.shouldRetry(resource));
|
||||
throw new DecryptException("Decryption failed");
|
||||
}
|
||||
|
||||
class ASN1Data {
|
||||
static final byte MAGIC = (byte) 0x30;
|
||||
|
||||
private byte[] buff;
|
||||
private int index, length;
|
||||
|
||||
ASN1Data(byte[] buff) throws FormatException {
|
||||
this.buff = buff;
|
||||
index = 0;
|
||||
if (buff[index++] != MAGIC) {
|
||||
throw new FormatException("Not ASN.1 data");
|
||||
}
|
||||
length = buff[index++] & 0xff;
|
||||
if ((length & 0x80) != 0) {
|
||||
int counter = length & 0x7f;
|
||||
length = 0;
|
||||
while (counter-- > 0) {
|
||||
length = (length << 8) + (buff[index++] & 0xff);
|
||||
}
|
||||
}
|
||||
if ((index + length) > buff.length) {
|
||||
throw new FormatException("Length mismatch: " + buff.length + " != " + (index + length));
|
||||
}
|
||||
}
|
||||
|
||||
BigInteger readNext() throws IOException {
|
||||
if (index >= length) {
|
||||
throw new EOFException();
|
||||
} else if (buff[index++] != 0x02) {
|
||||
throw new IOException("Not an int code: " + Integer.toHexString(0xff & buff[index]));
|
||||
}
|
||||
int length = buff[index++] & 0xff;
|
||||
if ((length & 0x80) != 0) {
|
||||
int counter = length & 0x7f;
|
||||
length = 0;
|
||||
while (counter-- > 0) {
|
||||
length = (length << 8) + (buff[index++] & 0xff);
|
||||
}
|
||||
}
|
||||
byte[] sequence = new byte[length];
|
||||
System.arraycopy(buff, index, sequence, 0, length);
|
||||
index += length;
|
||||
return new BigInteger(sequence);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,14 +27,21 @@ import org.bouncycastle.openssl.PEMEncryptedKeyPair;
|
||||
import org.bouncycastle.openssl.PEMKeyPair;
|
||||
import org.bouncycastle.openssl.PEMParser;
|
||||
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
|
||||
import org.bouncycastle.openssl.jcajce.JceOpenSSLPKCS8DecryptorProviderBuilder;
|
||||
import org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder;
|
||||
import org.bouncycastle.operator.InputDecryptorProvider;
|
||||
import org.bouncycastle.operator.OperatorCreationException;
|
||||
import org.bouncycastle.pkcs.PKCS8EncryptedPrivateKeyInfo;
|
||||
import org.bouncycastle.pkcs.PKCSException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.KeyPair;
|
||||
|
||||
/** Represents a PKCS8-encoded key file. This is the format used by (old-style) OpenSSH and OpenSSL. */
|
||||
/**
|
||||
* Key File implementation supporting PEM-encoded PKCS8 and PKCS1 formats with or without password-based encryption
|
||||
*/
|
||||
public class PKCS8KeyFile extends BaseFileKeyProvider {
|
||||
|
||||
public static class Factory
|
||||
@@ -53,8 +60,6 @@ public class PKCS8KeyFile extends BaseFileKeyProvider {
|
||||
|
||||
protected final Logger log = LoggerFactory.getLogger(getClass());
|
||||
|
||||
protected char[] passphrase; // for blanking out
|
||||
|
||||
protected KeyPairConverter<PrivateKeyInfo> privateKeyInfoKeyPairConverter = new PrivateKeyInfoKeyPairConverter();
|
||||
|
||||
protected KeyPair readKeyPair()
|
||||
@@ -74,22 +79,19 @@ public class PKCS8KeyFile extends BaseFileKeyProvider {
|
||||
|
||||
if (o instanceof PEMEncryptedKeyPair) {
|
||||
final PEMEncryptedKeyPair encryptedKeyPair = (PEMEncryptedKeyPair) o;
|
||||
JcePEMDecryptorProviderBuilder decryptorBuilder = new JcePEMDecryptorProviderBuilder();
|
||||
if (SecurityUtils.getSecurityProvider() != null) {
|
||||
decryptorBuilder.setProvider(SecurityUtils.getSecurityProvider());
|
||||
}
|
||||
try {
|
||||
passphrase = pwdf == null ? null : pwdf.reqPassword(resource);
|
||||
kp = pemConverter.getKeyPair(encryptedKeyPair.decryptKeyPair(decryptorBuilder.build(passphrase)));
|
||||
} finally {
|
||||
PasswordUtils.blankOut(passphrase);
|
||||
}
|
||||
final PEMKeyPair pemKeyPair = readEncryptedKeyPair(encryptedKeyPair);
|
||||
kp = pemConverter.getKeyPair(pemKeyPair);
|
||||
} else if (o instanceof PEMKeyPair) {
|
||||
kp = pemConverter.getKeyPair((PEMKeyPair) o);
|
||||
} else if (o instanceof PrivateKeyInfo) {
|
||||
final PrivateKeyInfo privateKeyInfo = (PrivateKeyInfo) o;
|
||||
final PEMKeyPair pemKeyPair = privateKeyInfoKeyPairConverter.getKeyPair(privateKeyInfo);
|
||||
kp = pemConverter.getKeyPair(pemKeyPair);
|
||||
} else if (o instanceof PKCS8EncryptedPrivateKeyInfo) {
|
||||
final PKCS8EncryptedPrivateKeyInfo encryptedPrivateKeyInfo = (PKCS8EncryptedPrivateKeyInfo) o;
|
||||
final PrivateKeyInfo privateKeyInfo = readEncryptedPrivateKeyInfo(encryptedPrivateKeyInfo);
|
||||
final PEMKeyPair pemKeyPair = privateKeyInfoKeyPairConverter.getKeyPair(privateKeyInfo);
|
||||
kp = pemConverter.getKeyPair(pemKeyPair);
|
||||
} else {
|
||||
log.warn("Unexpected PKCS8 PEM Object [{}]", o);
|
||||
}
|
||||
@@ -114,4 +116,37 @@ public class PKCS8KeyFile extends BaseFileKeyProvider {
|
||||
public String toString() {
|
||||
return "PKCS8KeyFile{resource=" + resource + "}";
|
||||
}
|
||||
|
||||
private PEMKeyPair readEncryptedKeyPair(final PEMEncryptedKeyPair encryptedKeyPair) throws IOException {
|
||||
final JcePEMDecryptorProviderBuilder builder = new JcePEMDecryptorProviderBuilder();
|
||||
if (SecurityUtils.getSecurityProvider() != null) {
|
||||
builder.setProvider(SecurityUtils.getSecurityProvider());
|
||||
}
|
||||
char[] passphrase = null;
|
||||
try {
|
||||
passphrase = pwdf == null ? null : pwdf.reqPassword(resource);
|
||||
return encryptedKeyPair.decryptKeyPair(builder.build(passphrase));
|
||||
} finally {
|
||||
PasswordUtils.blankOut(passphrase);
|
||||
}
|
||||
}
|
||||
|
||||
private PrivateKeyInfo readEncryptedPrivateKeyInfo(final PKCS8EncryptedPrivateKeyInfo encryptedPrivateKeyInfo) throws EncryptionException {
|
||||
final JceOpenSSLPKCS8DecryptorProviderBuilder builder = new JceOpenSSLPKCS8DecryptorProviderBuilder();
|
||||
if (SecurityUtils.getSecurityProvider() != null) {
|
||||
builder.setProvider(SecurityUtils.getSecurityProvider());
|
||||
}
|
||||
char[] passphrase = null;
|
||||
try {
|
||||
passphrase = pwdf == null ? null : pwdf.reqPassword(resource);
|
||||
final InputDecryptorProvider inputDecryptorProvider = builder.build(passphrase);
|
||||
return encryptedPrivateKeyInfo.decryptPrivateKeyInfo(inputDecryptorProvider);
|
||||
} catch (final OperatorCreationException e) {
|
||||
throw new EncryptionException("Loading Password for Encrypted Private Key Failed", e);
|
||||
} catch (final PKCSException e) {
|
||||
throw new EncryptionException("Reading Encrypted Private Key Failed", e);
|
||||
} finally {
|
||||
PasswordUtils.blankOut(passphrase);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,8 @@ import net.schmizz.sshj.common.SecurityUtils;
|
||||
import net.schmizz.sshj.userauth.password.PasswordUtils;
|
||||
import org.bouncycastle.asn1.nist.NISTNamedCurves;
|
||||
import org.bouncycastle.asn1.x9.X9ECParameters;
|
||||
import org.bouncycastle.crypto.generators.Argon2BytesGenerator;
|
||||
import org.bouncycastle.crypto.params.Argon2Parameters;
|
||||
import org.bouncycastle.jce.spec.ECNamedCurveSpec;
|
||||
import org.bouncycastle.util.encoders.Hex;
|
||||
|
||||
@@ -40,11 +42,15 @@ import java.io.*;
|
||||
import java.math.BigInteger;
|
||||
import java.security.*;
|
||||
import java.security.spec.*;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* <h2>Sample PuTTY file format</h2>
|
||||
*
|
||||
* <pre>
|
||||
* PuTTY-User-Key-File-2: ssh-rsa
|
||||
* Encryption: none
|
||||
@@ -70,8 +76,7 @@ import java.util.Map;
|
||||
*/
|
||||
public class PuTTYKeyFile extends BaseFileKeyProvider {
|
||||
|
||||
public static class Factory
|
||||
implements net.schmizz.sshj.common.Factory.Named<FileKeyProvider> {
|
||||
public static class Factory implements net.schmizz.sshj.common.Factory.Named<FileKeyProvider> {
|
||||
|
||||
@Override
|
||||
public FileKeyProvider create() {
|
||||
@@ -84,38 +89,46 @@ public class PuTTYKeyFile extends BaseFileKeyProvider {
|
||||
}
|
||||
}
|
||||
|
||||
private Integer keyFileVersion;
|
||||
private byte[] privateKey;
|
||||
private byte[] publicKey;
|
||||
private byte[] verifyHmac; // only used by v3 keys
|
||||
|
||||
/**
|
||||
* Key type. Either "ssh-rsa" for RSA key, or "ssh-dss" for DSA key.
|
||||
* Key type
|
||||
*/
|
||||
@Override
|
||||
public KeyType getType() throws IOException {
|
||||
return KeyType.fromString(headers.get("PuTTY-User-Key-File-2"));
|
||||
String headerName = String.format("PuTTY-User-Key-File-%d", this.keyFileVersion);
|
||||
return KeyType.fromString(headers.get(headerName));
|
||||
}
|
||||
|
||||
public boolean isEncrypted() {
|
||||
// Currently the only supported encryption types are "aes256-cbc" and "none".
|
||||
return "aes256-cbc".equals(headers.get("Encryption"));
|
||||
public boolean isEncrypted() throws IOException {
|
||||
// Currently, the only supported encryption types are "aes256-cbc" and "none".
|
||||
String encryption = headers.get("Encryption");
|
||||
if ("none".equals(encryption)) {
|
||||
return false;
|
||||
}
|
||||
if ("aes256-cbc".equals(encryption)) {
|
||||
return true;
|
||||
}
|
||||
throw new IOException(String.format("Unsupported encryption: %s", encryption));
|
||||
}
|
||||
|
||||
private Map<String, String> payload
|
||||
= new HashMap<String, String>();
|
||||
private Map<String, String> payload = new HashMap<String, String>();
|
||||
|
||||
/**
|
||||
* For each line that looks like "Xyz: vvv", it will be stored in this map.
|
||||
*/
|
||||
private final Map<String, String> headers
|
||||
= new HashMap<String, String>();
|
||||
|
||||
private final Map<String, String> headers = new HashMap<String, String>();
|
||||
|
||||
protected KeyPair readKeyPair() throws IOException {
|
||||
this.parseKeyPair();
|
||||
final Buffer.PlainBuffer publicKeyReader = new Buffer.PlainBuffer(publicKey);
|
||||
final Buffer.PlainBuffer privateKeyReader = new Buffer.PlainBuffer(privateKey);
|
||||
publicKeyReader.readBytes(); // The first part of the payload is a human-readable key format name.
|
||||
if (KeyType.RSA.equals(this.getType())) {
|
||||
final KeyType keyType = this.getType();
|
||||
publicKeyReader.readBytes(); // The first part of the payload is a human-readable key format name.
|
||||
if (KeyType.RSA.equals(keyType)) {
|
||||
// public key exponent
|
||||
BigInteger e = publicKeyReader.readMPInt();
|
||||
// modulus
|
||||
@@ -131,15 +144,13 @@ public class PuTTYKeyFile extends BaseFileKeyProvider {
|
||||
throw new IOException(s.getMessage(), s);
|
||||
}
|
||||
try {
|
||||
return new KeyPair(
|
||||
factory.generatePublic(new RSAPublicKeySpec(n, e)),
|
||||
factory.generatePrivate(new RSAPrivateKeySpec(n, d))
|
||||
);
|
||||
return new KeyPair(factory.generatePublic(new RSAPublicKeySpec(n, e)),
|
||||
factory.generatePrivate(new RSAPrivateKeySpec(n, d)));
|
||||
} catch (InvalidKeySpecException i) {
|
||||
throw new IOException(i.getMessage(), i);
|
||||
}
|
||||
}
|
||||
if (KeyType.DSA.equals(this.getType())) {
|
||||
if (KeyType.DSA.equals(keyType)) {
|
||||
BigInteger p = publicKeyReader.readMPInt();
|
||||
BigInteger q = publicKeyReader.readMPInt();
|
||||
BigInteger g = publicKeyReader.readMPInt();
|
||||
@@ -155,22 +166,20 @@ public class PuTTYKeyFile extends BaseFileKeyProvider {
|
||||
throw new IOException(s.getMessage(), s);
|
||||
}
|
||||
try {
|
||||
return new KeyPair(
|
||||
factory.generatePublic(new DSAPublicKeySpec(y, p, q, g)),
|
||||
factory.generatePrivate(new DSAPrivateKeySpec(x, p, q, g))
|
||||
);
|
||||
return new KeyPair(factory.generatePublic(new DSAPublicKeySpec(y, p, q, g)),
|
||||
factory.generatePrivate(new DSAPrivateKeySpec(x, p, q, g)));
|
||||
} catch (InvalidKeySpecException e) {
|
||||
throw new IOException(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
if (KeyType.ED25519.equals(this.getType())) {
|
||||
if (KeyType.ED25519.equals(keyType)) {
|
||||
EdDSANamedCurveSpec ed25519 = EdDSANamedCurveTable.getByName("Ed25519");
|
||||
EdDSAPublicKeySpec publicSpec = new EdDSAPublicKeySpec(publicKeyReader.readBytes(), ed25519);
|
||||
EdDSAPrivateKeySpec privateSpec = new EdDSAPrivateKeySpec(privateKeyReader.readBytes(), ed25519);
|
||||
return new KeyPair(new EdDSAPublicKey(publicSpec), new EdDSAPrivateKey(privateSpec));
|
||||
}
|
||||
final String ecdsaCurve;
|
||||
switch (this.getType()) {
|
||||
switch (keyType) {
|
||||
case ECDSA256:
|
||||
ecdsaCurve = "P-256";
|
||||
break;
|
||||
@@ -187,12 +196,12 @@ public class PuTTYKeyFile extends BaseFileKeyProvider {
|
||||
if (ecdsaCurve != null) {
|
||||
BigInteger s = new BigInteger(1, privateKeyReader.readBytes());
|
||||
X9ECParameters ecParams = NISTNamedCurves.getByName(ecdsaCurve);
|
||||
ECNamedCurveSpec ecCurveSpec =
|
||||
new ECNamedCurveSpec(ecdsaCurve, ecParams.getCurve(), ecParams.getG(), ecParams.getN());
|
||||
ECNamedCurveSpec ecCurveSpec = new ECNamedCurveSpec(ecdsaCurve, ecParams.getCurve(), ecParams.getG(),
|
||||
ecParams.getN());
|
||||
ECPrivateKeySpec pks = new ECPrivateKeySpec(s, ecCurveSpec);
|
||||
try {
|
||||
PrivateKey privateKey = SecurityUtils.getKeyFactory(KeyAlgorithm.ECDSA).generatePrivate(pks);
|
||||
return new KeyPair(getType().readPubKeyFromBuffer(publicKeyReader), privateKey);
|
||||
return new KeyPair(keyType.readPubKeyFromBuffer(publicKeyReader), privateKey);
|
||||
} catch (GeneralSecurityException e) {
|
||||
throw new IOException(e.getMessage(), e);
|
||||
}
|
||||
@@ -201,6 +210,7 @@ public class PuTTYKeyFile extends BaseFileKeyProvider {
|
||||
}
|
||||
|
||||
protected void parseKeyPair() throws IOException {
|
||||
this.keyFileVersion = null;
|
||||
BufferedReader r = new BufferedReader(resource.getReader());
|
||||
// Parse the text into headers and payloads
|
||||
try {
|
||||
@@ -211,6 +221,9 @@ public class PuTTYKeyFile extends BaseFileKeyProvider {
|
||||
if (idx > 0) {
|
||||
headerName = line.substring(0, idx);
|
||||
headers.put(headerName, line.substring(idx + 2));
|
||||
if (headerName.startsWith("PuTTY-User-Key-File-")) {
|
||||
this.keyFileVersion = Integer.parseInt(headerName.substring(20));
|
||||
}
|
||||
} else {
|
||||
String s = payload.get(headerName);
|
||||
if (s == null) {
|
||||
@@ -226,6 +239,9 @@ public class PuTTYKeyFile extends BaseFileKeyProvider {
|
||||
} finally {
|
||||
r.close();
|
||||
}
|
||||
if (this.keyFileVersion == null) {
|
||||
throw new IOException("Invalid key file format: missing \"PuTTY-User-Key-File-?\" entry");
|
||||
}
|
||||
// Retrieve keys from payload
|
||||
publicKey = Base64.decode(payload.get("Public-Lines"));
|
||||
if (this.isEncrypted()) {
|
||||
@@ -236,8 +252,14 @@ public class PuTTYKeyFile extends BaseFileKeyProvider {
|
||||
passphrase = "".toCharArray();
|
||||
}
|
||||
try {
|
||||
privateKey = this.decrypt(Base64.decode(payload.get("Private-Lines")), new String(passphrase));
|
||||
this.verify(new String(passphrase));
|
||||
privateKey = this.decrypt(Base64.decode(payload.get("Private-Lines")), passphrase);
|
||||
Mac mac;
|
||||
if (this.keyFileVersion <= 2) {
|
||||
mac = this.prepareVerifyMacV2(passphrase);
|
||||
} else {
|
||||
mac = this.prepareVerifyMacV3();
|
||||
}
|
||||
this.verify(mac);
|
||||
} finally {
|
||||
PasswordUtils.blankOut(passphrase);
|
||||
}
|
||||
@@ -247,77 +269,158 @@ public class PuTTYKeyFile extends BaseFileKeyProvider {
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a passphrase into a key, by following the convention that PuTTY uses.
|
||||
* <p/>
|
||||
* <p/>
|
||||
* Converts a passphrase into a key, by following the convention that PuTTY
|
||||
* uses. Only PuTTY v1/v2 key files
|
||||
* <p><p/>
|
||||
* This is used to decrypt the private key when it's encrypted.
|
||||
*/
|
||||
private byte[] toKey(final String passphrase) throws IOException {
|
||||
private void initCipher(final char[] passphrase, Cipher cipher) throws IOException, InvalidAlgorithmParameterException, InvalidKeyException {
|
||||
// The field Key-Derivation has been introduced with Putty v3 key file format
|
||||
// For v3 the algorithms are "Argon2i" "Argon2d" and "Argon2id"
|
||||
String kdfAlgorithm = headers.get("Key-Derivation");
|
||||
if (kdfAlgorithm != null) {
|
||||
kdfAlgorithm = kdfAlgorithm.toLowerCase();
|
||||
byte[] keyData = this.argon2(kdfAlgorithm, passphrase);
|
||||
if (keyData == null) {
|
||||
throw new IOException(String.format("Unsupported key derivation function: %s", kdfAlgorithm));
|
||||
}
|
||||
byte[] key = new byte[32];
|
||||
byte[] iv = new byte[16];
|
||||
byte[] tag = new byte[32]; // Hmac key
|
||||
System.arraycopy(keyData, 0, key, 0, 32);
|
||||
System.arraycopy(keyData, 32, iv, 0, 16);
|
||||
System.arraycopy(keyData, 48, tag, 0, 32);
|
||||
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"),
|
||||
new IvParameterSpec(iv));
|
||||
verifyHmac = tag;
|
||||
return;
|
||||
}
|
||||
|
||||
// Key file format v1 + v2
|
||||
try {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-1");
|
||||
|
||||
// The encryption key is derived from the passphrase by means of a succession of SHA-1 hashes.
|
||||
// The encryption key is derived from the passphrase by means of a succession of
|
||||
// SHA-1 hashes.
|
||||
byte[] encodedPassphrase = PasswordUtils.toByteArray(passphrase);
|
||||
|
||||
// Sequence number 0
|
||||
digest.update(new byte[]{0, 0, 0, 0});
|
||||
digest.update(passphrase.getBytes());
|
||||
digest.update(encodedPassphrase);
|
||||
byte[] key1 = digest.digest();
|
||||
|
||||
// Sequence number 1
|
||||
digest.update(new byte[]{0, 0, 0, 1});
|
||||
digest.update(passphrase.getBytes());
|
||||
digest.update(encodedPassphrase);
|
||||
byte[] key2 = digest.digest();
|
||||
|
||||
byte[] r = new byte[32];
|
||||
System.arraycopy(key1, 0, r, 0, 20);
|
||||
System.arraycopy(key2, 0, r, 20, 12);
|
||||
Arrays.fill(encodedPassphrase, (byte) 0);
|
||||
|
||||
byte[] expanded = new byte[32];
|
||||
System.arraycopy(key1, 0, expanded, 0, 20);
|
||||
System.arraycopy(key2, 0, expanded, 20, 12);
|
||||
|
||||
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(expanded, 0, 32, "AES"),
|
||||
new IvParameterSpec(new byte[16])); // initial vector=0
|
||||
|
||||
return r;
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new IOException(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the MAC.
|
||||
* Uses BouncyCastle Argon2 implementation
|
||||
*/
|
||||
private void verify(final String passphrase) throws IOException {
|
||||
private byte[] argon2(String algorithm, final char[] passphrase) throws IOException {
|
||||
int type;
|
||||
if ("argon2i".equals(algorithm)) {
|
||||
type = Argon2Parameters.ARGON2_i;
|
||||
} else if ("argon2d".equals(algorithm)) {
|
||||
type = Argon2Parameters.ARGON2_d;
|
||||
} else if ("argon2id".equals(algorithm)) {
|
||||
type = Argon2Parameters.ARGON2_id;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
byte[] salt = Hex.decode(headers.get("Argon2-Salt"));
|
||||
int iterations = Integer.parseInt(headers.get("Argon2-Passes"));
|
||||
int memory = Integer.parseInt(headers.get("Argon2-Memory"));
|
||||
int parallelism = Integer.parseInt(headers.get("Argon2-Parallelism"));
|
||||
|
||||
Argon2Parameters a2p = new Argon2Parameters.Builder(type)
|
||||
.withVersion(Argon2Parameters.ARGON2_VERSION_13)
|
||||
.withIterations(iterations)
|
||||
.withMemoryAsKB(memory)
|
||||
.withParallelism(parallelism)
|
||||
.withSalt(salt).build();
|
||||
|
||||
Argon2BytesGenerator generator = new Argon2BytesGenerator();
|
||||
generator.init(a2p);
|
||||
byte[] output = new byte[80];
|
||||
int bytes = generator.generateBytes(passphrase, output);
|
||||
if (bytes != output.length) {
|
||||
throw new IOException("Failed to generate key via Argon2");
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the MAC (only required for v1/v2 keys. v3 keys are automatically
|
||||
* verified as part of the decryption process.
|
||||
*/
|
||||
private void verify(final Mac mac) throws IOException {
|
||||
final ByteArrayOutputStream out = new ByteArrayOutputStream(256);
|
||||
final DataOutputStream data = new DataOutputStream(out);
|
||||
// name of algorithm
|
||||
String keyType = this.getType().toString();
|
||||
data.writeInt(keyType.length());
|
||||
data.writeBytes(keyType);
|
||||
|
||||
data.writeInt(headers.get("Encryption").length());
|
||||
data.writeBytes(headers.get("Encryption"));
|
||||
|
||||
data.writeInt(headers.get("Comment").length());
|
||||
data.writeBytes(headers.get("Comment"));
|
||||
|
||||
data.writeInt(publicKey.length);
|
||||
data.write(publicKey);
|
||||
|
||||
data.writeInt(privateKey.length);
|
||||
data.write(privateKey);
|
||||
|
||||
final String encoded = Hex.toHexString(mac.doFinal(out.toByteArray()));
|
||||
final String reference = headers.get("Private-MAC");
|
||||
if (!encoded.equals(reference)) {
|
||||
throw new IOException("Invalid passphrase");
|
||||
}
|
||||
}
|
||||
|
||||
private Mac prepareVerifyMacV2(final char[] passphrase) throws IOException {
|
||||
// The key to the MAC is itself a SHA-1 hash of (v1/v2 key only):
|
||||
try {
|
||||
// The key to the MAC is itself a SHA-1 hash of:
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-1");
|
||||
digest.update("putty-private-key-file-mac-key".getBytes());
|
||||
if (passphrase != null) {
|
||||
digest.update(passphrase.getBytes());
|
||||
byte[] encodedPassphrase = PasswordUtils.toByteArray(passphrase);
|
||||
digest.update(encodedPassphrase);
|
||||
Arrays.fill(encodedPassphrase, (byte) 0);
|
||||
}
|
||||
final byte[] key = digest.digest();
|
||||
|
||||
final Mac mac = Mac.getInstance("HmacSHA1");
|
||||
Mac mac = Mac.getInstance("HmacSHA1");
|
||||
mac.init(new SecretKeySpec(key, 0, 20, mac.getAlgorithm()));
|
||||
return mac;
|
||||
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
|
||||
throw new IOException(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
final ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
final DataOutputStream data = new DataOutputStream(out);
|
||||
// name of algorithm
|
||||
data.writeInt(this.getType().toString().length());
|
||||
data.writeBytes(this.getType().toString());
|
||||
|
||||
data.writeInt(headers.get("Encryption").length());
|
||||
data.writeBytes(headers.get("Encryption"));
|
||||
|
||||
data.writeInt(headers.get("Comment").length());
|
||||
data.writeBytes(headers.get("Comment"));
|
||||
|
||||
data.writeInt(publicKey.length);
|
||||
data.write(publicKey);
|
||||
|
||||
data.writeInt(privateKey.length);
|
||||
data.write(privateKey);
|
||||
|
||||
final String encoded = Hex.toHexString(mac.doFinal(out.toByteArray()));
|
||||
final String reference = headers.get("Private-MAC");
|
||||
if (!encoded.equals(reference)) {
|
||||
throw new IOException("Invalid passphrase");
|
||||
}
|
||||
} catch (GeneralSecurityException e) {
|
||||
private Mac prepareVerifyMacV3() throws IOException {
|
||||
// for v3 keys the hMac key is included in the Argon output
|
||||
try {
|
||||
Mac mac = Mac.getInstance("HmacSHA256");
|
||||
mac.init(new SecretKeySpec(this.verifyHmac, 0, 32, mac.getAlgorithm()));
|
||||
return mac;
|
||||
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
|
||||
throw new IOException(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
@@ -325,17 +428,21 @@ public class PuTTYKeyFile extends BaseFileKeyProvider {
|
||||
/**
|
||||
* Decrypt private key
|
||||
*
|
||||
* @param privateKey the SSH private key to be decrypted
|
||||
* @param passphrase To decrypt
|
||||
*/
|
||||
private byte[] decrypt(final byte[] key, final String passphrase) throws IOException {
|
||||
private byte[] decrypt(final byte[] privateKey, final char[] passphrase) throws IOException {
|
||||
try {
|
||||
final Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
|
||||
final byte[] expanded = this.toKey(passphrase);
|
||||
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(expanded, 0, 32, "AES"),
|
||||
new IvParameterSpec(new byte[16])); // initial vector=0
|
||||
return cipher.doFinal(key);
|
||||
this.initCipher(passphrase, cipher);
|
||||
return cipher.doFinal(privateKey);
|
||||
} catch (GeneralSecurityException e) {
|
||||
throw new IOException(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
public int getKeyFileVersion() {
|
||||
return keyFileVersion;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -84,8 +84,7 @@ public class AuthGssApiWithMic
|
||||
|
||||
@Override
|
||||
public GSSContext run() throws GSSException {
|
||||
GSSName clientName = manager.createName(params.getUsername(), GSSName.NT_USER_NAME);
|
||||
GSSCredential clientCreds = manager.createCredential(clientName, GSSContext.DEFAULT_LIFETIME, selectedOid, GSSCredential.INITIATE_ONLY);
|
||||
GSSCredential clientCreds = manager.createCredential(GSSCredential.INITIATE_ONLY);
|
||||
GSSName peerName = manager.createName("host@" + params.getTransport().getRemoteHost(), GSSName.NT_HOSTBASED_SERVICE);
|
||||
|
||||
GSSContext context = manager.createContext(peerName, selectedOid, clientCreds, GSSContext.DEFAULT_LIFETIME);
|
||||
|
||||
@@ -27,17 +27,36 @@ import net.schmizz.sshj.userauth.keyprovider.KeyProvider;
|
||||
import java.io.IOException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.util.LinkedList;
|
||||
import java.util.Queue;
|
||||
|
||||
public abstract class KeyedAuthMethod
|
||||
extends AbstractAuthMethod {
|
||||
|
||||
protected final KeyProvider kProv;
|
||||
private Queue<KeyAlgorithm> available;
|
||||
|
||||
public KeyedAuthMethod(String name, KeyProvider kProv) {
|
||||
super(name);
|
||||
this.kProv = kProv;
|
||||
}
|
||||
|
||||
private KeyAlgorithm getPublicKeyAlgorithm(KeyType keyType) throws TransportException {
|
||||
if (available == null) {
|
||||
available = new LinkedList<>(params.getTransport().getClientKeyAlgorithms(keyType));
|
||||
}
|
||||
return available.peek();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldRetry() {
|
||||
if (available != null) {
|
||||
available.poll();
|
||||
return !available.isEmpty();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
protected SSHPacket putPubKey(SSHPacket reqBuf)
|
||||
throws UserAuthException {
|
||||
PublicKey key;
|
||||
@@ -50,13 +69,16 @@ public abstract class KeyedAuthMethod
|
||||
// public key as 2 strings: [ key type | key blob ]
|
||||
KeyType keyType = KeyType.fromKey(key);
|
||||
try {
|
||||
KeyAlgorithm ka = params.getTransport().getClientKeyAlgorithm(keyType);
|
||||
reqBuf.putString(ka.getKeyAlgorithm())
|
||||
.putString(new Buffer.PlainBuffer().putPublicKey(key).getCompactData());
|
||||
return reqBuf;
|
||||
KeyAlgorithm ka = getPublicKeyAlgorithm(keyType);
|
||||
if (ka != null) {
|
||||
reqBuf.putString(ka.getKeyAlgorithm())
|
||||
.putString(new Buffer.PlainBuffer().putPublicKey(key).getCompactData());
|
||||
return reqBuf;
|
||||
}
|
||||
} catch (IOException ioe) {
|
||||
throw new UserAuthException("No KeyAlgorithm configured for key " + keyType);
|
||||
throw new UserAuthException("No KeyAlgorithm configured for key " + keyType, ioe);
|
||||
}
|
||||
throw new UserAuthException("No KeyAlgorithm configured for key " + keyType);
|
||||
}
|
||||
|
||||
protected SSHPacket putSig(SSHPacket reqBuf)
|
||||
@@ -71,7 +93,7 @@ public abstract class KeyedAuthMethod
|
||||
final KeyType kt = KeyType.fromKey(key);
|
||||
Signature signature;
|
||||
try {
|
||||
signature = params.getTransport().getClientKeyAlgorithm(kt).newSignature();
|
||||
signature = getPublicKeyAlgorithm(kt).newSignature();
|
||||
} catch (TransportException e) {
|
||||
throw new UserAuthException("No KeyAlgorithm configured for key " + kt);
|
||||
}
|
||||
|
||||
@@ -27,7 +27,8 @@ import java.util.regex.Pattern;
|
||||
public class PasswordResponseProvider
|
||||
implements ChallengeResponseProvider {
|
||||
|
||||
public static final Pattern DEFAULT_PROMPT_PATTERN = Pattern.compile(".*[pP]assword:\\s?\\z", Pattern.DOTALL);
|
||||
// FreeBSD prompt is "Password for user@host:"
|
||||
public static final Pattern DEFAULT_PROMPT_PATTERN = Pattern.compile(".*[pP]assword(?: for .*)?:\\s?\\z", Pattern.DOTALL);
|
||||
|
||||
private static final char[] EMPTY_RESPONSE = new char[0];
|
||||
|
||||
|
||||
@@ -15,6 +15,9 @@
|
||||
*/
|
||||
package net.schmizz.sshj.userauth.password;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.CharBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
|
||||
/** Static utility method and factories */
|
||||
@@ -54,4 +57,17 @@ public class PasswordUtils {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a password to a UTF-8 encoded byte array
|
||||
*
|
||||
* @param password
|
||||
* @return
|
||||
*/
|
||||
public static byte[] toByteArray(char[] password) {
|
||||
CharBuffer charBuffer = CharBuffer.wrap(password);
|
||||
final ByteBuffer byteBuffer = StandardCharsets.UTF_8.encode(charBuffer);
|
||||
byte[] bytes = new byte[byteBuffer.remaining()];
|
||||
byteBuffer.get(bytes, 0, bytes.length);
|
||||
return bytes;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,7 +71,13 @@ public class FileSystemFile
|
||||
@Override
|
||||
public OutputStream getOutputStream()
|
||||
throws IOException {
|
||||
return new FileOutputStream(file);
|
||||
return getOutputStream(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OutputStream getOutputStream(boolean append)
|
||||
throws IOException {
|
||||
return new FileOutputStream(file, append);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -31,6 +31,19 @@ public interface FileTransfer {
|
||||
void upload(String localPath, String remotePath)
|
||||
throws IOException;
|
||||
|
||||
/**
|
||||
* This is meant to delegate to {@link #upload(LocalSourceFile, String)} with the {@code localPath} wrapped as e.g.
|
||||
* a {@link FileSystemFile}. Appends to existing if {@code byteOffset} > 0.
|
||||
*
|
||||
* @param localPath
|
||||
* @param remotePath
|
||||
* @param byteOffset
|
||||
*
|
||||
* @throws IOException
|
||||
*/
|
||||
void upload(String localPath, String remotePath, long byteOffset)
|
||||
throws IOException;
|
||||
|
||||
/**
|
||||
* This is meant to delegate to {@link #download(String, LocalDestFile)} with the {@code localPath} wrapped as e.g.
|
||||
* a {@link FileSystemFile}.
|
||||
@@ -43,6 +56,19 @@ public interface FileTransfer {
|
||||
void download(String remotePath, String localPath)
|
||||
throws IOException;
|
||||
|
||||
/**
|
||||
* This is meant to delegate to {@link #download(String, LocalDestFile)} with the {@code localPath} wrapped as e.g.
|
||||
* a {@link FileSystemFile}. Appends to existing if {@code byteOffset} > 0.
|
||||
*
|
||||
* @param localPath
|
||||
* @param remotePath
|
||||
* @param byteOffset
|
||||
*
|
||||
* @throws IOException
|
||||
*/
|
||||
void download(String remotePath, String localPath, long byteOffset)
|
||||
throws IOException;
|
||||
|
||||
/**
|
||||
* Upload {@code localFile} to {@code remotePath}.
|
||||
*
|
||||
@@ -54,6 +80,18 @@ public interface FileTransfer {
|
||||
void upload(LocalSourceFile localFile, String remotePath)
|
||||
throws IOException;
|
||||
|
||||
/**
|
||||
* Upload {@code localFile} to {@code remotePath}. Appends to existing if {@code byteOffset} > 0.
|
||||
*
|
||||
* @param localFile
|
||||
* @param remotePath
|
||||
* @param byteOffset
|
||||
*
|
||||
* @throws IOException
|
||||
*/
|
||||
void upload(LocalSourceFile localFile, String remotePath, long byteOffset)
|
||||
throws IOException;
|
||||
|
||||
/**
|
||||
* Download {@code remotePath} to {@code localFile}.
|
||||
*
|
||||
@@ -65,6 +103,18 @@ public interface FileTransfer {
|
||||
void download(String remotePath, LocalDestFile localFile)
|
||||
throws IOException;
|
||||
|
||||
/**
|
||||
* Download {@code remotePath} to {@code localFile}. Appends to existing if {@code byteOffset} > 0.
|
||||
*
|
||||
* @param localFile
|
||||
* @param remotePath
|
||||
* @param byteOffset
|
||||
*
|
||||
* @throws IOException
|
||||
*/
|
||||
void download(String remotePath, LocalDestFile localFile, long byteOffset)
|
||||
throws IOException;
|
||||
|
||||
TransferListener getTransferListener();
|
||||
|
||||
void setTransferListener(TransferListener listener);
|
||||
|
||||
@@ -20,8 +20,13 @@ import java.io.OutputStream;
|
||||
|
||||
public interface LocalDestFile {
|
||||
|
||||
long getLength();
|
||||
|
||||
OutputStream getOutputStream()
|
||||
throws IOException;
|
||||
|
||||
OutputStream getOutputStream(boolean append)
|
||||
throws IOException;
|
||||
|
||||
/** @return A child file/directory of this directory with given {@code name}. */
|
||||
LocalDestFile getChild(String name);
|
||||
|
||||
@@ -52,24 +52,49 @@ public class SCPFileTransfer
|
||||
@Override
|
||||
public void upload(String localPath, String remotePath)
|
||||
throws IOException {
|
||||
newSCPUploadClient().copy(new FileSystemFile(localPath), remotePath);
|
||||
upload(localPath, remotePath, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void upload(String localFile, String remotePath, long byteOffset)
|
||||
throws IOException {
|
||||
upload(new FileSystemFile(localFile), remotePath, byteOffset);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void download(String remotePath, String localPath)
|
||||
throws IOException {
|
||||
download(remotePath, new FileSystemFile(localPath));
|
||||
download(remotePath, localPath, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void download(String remotePath, String localPath, long byteOffset) throws IOException {
|
||||
download(remotePath, new FileSystemFile(localPath), byteOffset);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void download(String remotePath, LocalDestFile localFile)
|
||||
throws IOException {
|
||||
download(remotePath, localFile, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void download(String remotePath, LocalDestFile localFile, long byteOffset)
|
||||
throws IOException {
|
||||
checkByteOffsetSupport(byteOffset);
|
||||
newSCPDownloadClient().copy(remotePath, localFile);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void upload(LocalSourceFile localFile, String remotePath)
|
||||
throws IOException {
|
||||
upload(localFile, remotePath, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void upload(LocalSourceFile localFile, String remotePath, long byteOffset)
|
||||
throws IOException {
|
||||
checkByteOffsetSupport(byteOffset);
|
||||
newSCPUploadClient().copy(localFile, remotePath);
|
||||
}
|
||||
|
||||
@@ -79,4 +104,11 @@ public class SCPFileTransfer
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
private void checkByteOffsetSupport(long byteOffset) throws IOException {
|
||||
// TODO - implement byte offsets on SCP, if possible.
|
||||
if (byteOffset > 0) {
|
||||
throw new SCPException("Byte offset on SCP file transfers is not supported.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
package com.hierynomus.sshj.userauth.keyprovider
|
||||
|
||||
import com.hierynomus.sshj.test.SshFixture
|
||||
import net.schmizz.sshj.DefaultConfig
|
||||
import net.schmizz.sshj.SSHClient
|
||||
import net.schmizz.sshj.userauth.keyprovider.KeyFormat
|
||||
import org.apache.sshd.server.auth.pubkey.AcceptAllPublickeyAuthenticator
|
||||
@@ -39,7 +40,14 @@ class FileKeyProviderSpec extends Specification {
|
||||
@Unroll
|
||||
def "should have #format FileKeyProvider enabled by default"() {
|
||||
given:
|
||||
SSHClient client = fixture.setupConnectedDefaultClient()
|
||||
// `fixture` is backed by Apache SSHD server. Looks like it doesn't support rsa-sha2-512 public key signature.
|
||||
// Changing the default config to prioritize the former default implementation of RSA signature.
|
||||
def config = new DefaultConfig()
|
||||
config.prioritizeSshRsaKeyAlgorithm()
|
||||
|
||||
and:
|
||||
SSHClient client = fixture.setupClient(config)
|
||||
fixture.connectClient(client)
|
||||
|
||||
when:
|
||||
client.authPublickey("jeroen", keyfile)
|
||||
@@ -48,11 +56,11 @@ class FileKeyProviderSpec extends Specification {
|
||||
client.isAuthenticated()
|
||||
|
||||
cleanup:
|
||||
client.disconnect()
|
||||
client?.disconnect()
|
||||
|
||||
where:
|
||||
format | keyfile
|
||||
KeyFormat.PKCS5 | "src/test/resources/keyformats/pkcs5"
|
||||
KeyFormat.PKCS8 | "src/test/resources/keyformats/pkcs8"
|
||||
KeyFormat.OpenSSH | "src/test/resources/keyformats/openssh"
|
||||
}
|
||||
}
|
||||
|
||||
87
src/test/groovy/net/schmizz/sshj/ConfigImplSpec.groovy
Normal file
87
src/test/groovy/net/schmizz/sshj/ConfigImplSpec.groovy
Normal file
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* 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 com.hierynomus.sshj.key.KeyAlgorithms
|
||||
import spock.lang.Specification
|
||||
|
||||
class ConfigImplSpec extends Specification {
|
||||
static def ECDSA = KeyAlgorithms.ECDSASHANistp521()
|
||||
static def ED25519 = KeyAlgorithms.EdDSA25519()
|
||||
static def RSA_SHA_256 = KeyAlgorithms.RSASHA256()
|
||||
static def RSA_SHA_512 = KeyAlgorithms.RSASHA512()
|
||||
static def SSH_RSA = KeyAlgorithms.SSHRSA()
|
||||
|
||||
def "prioritizeSshRsaKeyAlgorithm does nothing if there is no ssh-rsa"() {
|
||||
given:
|
||||
def config = new DefaultConfig()
|
||||
config.keyAlgorithms = [RSA_SHA_512, ED25519]
|
||||
|
||||
when:
|
||||
config.prioritizeSshRsaKeyAlgorithm()
|
||||
|
||||
then:
|
||||
config.keyAlgorithms == [RSA_SHA_512, ED25519]
|
||||
}
|
||||
|
||||
def "prioritizeSshRsaKeyAlgorithm does nothing if there is no rsa-sha2-any"() {
|
||||
given:
|
||||
def config = new DefaultConfig()
|
||||
config.keyAlgorithms = [ED25519, SSH_RSA, ECDSA]
|
||||
|
||||
when:
|
||||
config.prioritizeSshRsaKeyAlgorithm()
|
||||
|
||||
then:
|
||||
config.keyAlgorithms == [ED25519, SSH_RSA, ECDSA]
|
||||
}
|
||||
|
||||
def "prioritizeSshRsaKeyAlgorithm does nothing if ssh-rsa already has higher priority"() {
|
||||
given:
|
||||
def config = new DefaultConfig()
|
||||
config.keyAlgorithms = [ED25519, SSH_RSA, RSA_SHA_512, ECDSA]
|
||||
|
||||
when:
|
||||
config.prioritizeSshRsaKeyAlgorithm()
|
||||
|
||||
then:
|
||||
config.keyAlgorithms == [ED25519, SSH_RSA, RSA_SHA_512, ECDSA]
|
||||
}
|
||||
|
||||
def "prioritizeSshRsaKeyAlgorithm prioritizes ssh-rsa if there is one rsa-sha2-any is prioritized"() {
|
||||
given:
|
||||
def config = new DefaultConfig()
|
||||
config.keyAlgorithms = [ED25519, RSA_SHA_512, ECDSA, SSH_RSA]
|
||||
|
||||
when:
|
||||
config.prioritizeSshRsaKeyAlgorithm()
|
||||
|
||||
then:
|
||||
config.keyAlgorithms == [ED25519, SSH_RSA, RSA_SHA_512, ECDSA]
|
||||
}
|
||||
|
||||
def "prioritizeSshRsaKeyAlgorithm prioritizes ssh-rsa if there are two rsa-sha2-any is prioritized"() {
|
||||
given:
|
||||
def config = new DefaultConfig()
|
||||
config.keyAlgorithms = [ED25519, RSA_SHA_512, ECDSA, RSA_SHA_256, SSH_RSA]
|
||||
|
||||
when:
|
||||
config.prioritizeSshRsaKeyAlgorithm()
|
||||
|
||||
then:
|
||||
config.keyAlgorithms == [ED25519, SSH_RSA, RSA_SHA_512, ECDSA, RSA_SHA_256]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
/*
|
||||
* 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.connection.channel.forwarded;
|
||||
|
||||
import com.hierynomus.sshj.test.HttpServer;
|
||||
import com.hierynomus.sshj.test.SshFixture;
|
||||
import com.hierynomus.sshj.test.util.FileUtil;
|
||||
import net.schmizz.sshj.SSHClient;
|
||||
import net.schmizz.sshj.connection.channel.direct.LocalPortForwarder;
|
||||
import net.schmizz.sshj.connection.channel.direct.Parameters;
|
||||
import org.apache.http.HttpResponse;
|
||||
import org.apache.http.client.HttpClient;
|
||||
import org.apache.http.client.methods.HttpGet;
|
||||
import org.apache.http.impl.client.HttpClientBuilder;
|
||||
import org.apache.sshd.server.forward.AcceptAllForwardingFilter;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.equalTo;
|
||||
import static org.junit.Assert.assertThat;
|
||||
|
||||
public class LocalPortForwarderTest {
|
||||
private static final Logger log = LoggerFactory.getLogger(LocalPortForwarderTest.class);
|
||||
|
||||
@Rule
|
||||
public SshFixture fixture = new SshFixture();
|
||||
|
||||
@Rule
|
||||
public HttpServer httpServer = new HttpServer();
|
||||
|
||||
@Before
|
||||
public void setUp() throws IOException {
|
||||
fixture.getServer().setForwardingFilter(new AcceptAllForwardingFilter());
|
||||
File file = httpServer.getDocRoot().newFile("index.html");
|
||||
FileUtil.writeToFile(file, "<html><head/><body><h1>Hi!</h1></body></html>");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldHaveWorkingHttpServer() throws IOException {
|
||||
// Just to check that we have a working http server...
|
||||
assertThat(httpGet("127.0.0.1", 8080), equalTo(200));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldHaveHttpServerThatClosesConnectionAfterResponse() throws IOException {
|
||||
// Just to check that the test server does close connections before we try through the forwarder...
|
||||
httpGetAndAssertConnectionClosedByServer(8080);
|
||||
}
|
||||
|
||||
@Test(timeout = 10_000)
|
||||
public void shouldCloseConnectionWhenRemoteServerClosesConnection() throws IOException {
|
||||
SSHClient sshClient = getFixtureClient();
|
||||
|
||||
ServerSocket serverSocket = new ServerSocket();
|
||||
serverSocket.setReuseAddress(true);
|
||||
serverSocket.bind(new InetSocketAddress("0.0.0.0", 12345));
|
||||
LocalPortForwarder localPortForwarder = sshClient.newLocalPortForwarder(new Parameters("0.0.0.0", 12345, "localhost", 8080), serverSocket);
|
||||
new Thread(() -> {
|
||||
try {
|
||||
localPortForwarder.listen();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}, "local port listener").start();
|
||||
|
||||
// Test once to prove that the local HTTP connection is closed when the remote HTTP connection is closed.
|
||||
httpGetAndAssertConnectionClosedByServer(12345);
|
||||
|
||||
// Test again to prove that the tunnel is still open, even after HTTP connection was closed.
|
||||
httpGetAndAssertConnectionClosedByServer(12345);
|
||||
}
|
||||
|
||||
public static void httpGetAndAssertConnectionClosedByServer(int port) throws IOException {
|
||||
System.out.println("HTTP GET to port: " + port);
|
||||
try (Socket socket = new Socket("localhost", port)) {
|
||||
// Send a basic HTTP GET
|
||||
// It returns 400 Bad Request because it's missing a bunch of info, but the HTTP response doesn't matter, we just want to test the connection closing.
|
||||
OutputStream outputStream = socket.getOutputStream();
|
||||
PrintWriter writer = new PrintWriter(outputStream);
|
||||
writer.println("GET / HTTP/1.1");
|
||||
writer.println("");
|
||||
writer.flush();
|
||||
|
||||
// Read the HTTP response
|
||||
InputStream inputStream = socket.getInputStream();
|
||||
InputStreamReader reader = new InputStreamReader(inputStream);
|
||||
int buf = -2;
|
||||
while (true) {
|
||||
buf = reader.read();
|
||||
System.out.print((char)buf);
|
||||
if (buf == -1) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Attempt to read more. If the server has closed the connection this will return -1
|
||||
int read = inputStream.read();
|
||||
|
||||
// Assert input stream was closed by server.
|
||||
Assert.assertEquals(-1, read);
|
||||
}
|
||||
}
|
||||
|
||||
private int httpGet(String server, int port) throws IOException {
|
||||
HttpClient client = HttpClientBuilder.create().build();
|
||||
String urlString = "http://" + server + ":" + port;
|
||||
log.info("Trying: GET " + urlString);
|
||||
HttpResponse execute = client.execute(new HttpGet(urlString));
|
||||
return execute.getStatusLine().getStatusCode();
|
||||
}
|
||||
|
||||
private SSHClient getFixtureClient() throws IOException {
|
||||
SSHClient sshClient = fixture.setupConnectedDefaultClient();
|
||||
sshClient.authPassword("jeroen", "jeroen");
|
||||
return sshClient;
|
||||
}
|
||||
}
|
||||
@@ -37,14 +37,14 @@ import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.InetSocketAddress;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.equalTo;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
public class RemotePortForwarderTest {
|
||||
private static final Logger log = LoggerFactory.getLogger(RemotePortForwarderTest.class);
|
||||
|
||||
private static final PortRange RANGE = new PortRange(9000, 9999);
|
||||
private static final InetSocketAddress HTTP_SERVER_SOCKET_ADDR = new InetSocketAddress("127.0.0.1", 8080);
|
||||
private static final String LOCALHOST = "127.0.0.1";
|
||||
private static final InetSocketAddress HTTP_SERVER_SOCKET_ADDR = new InetSocketAddress(LOCALHOST, 8080);
|
||||
|
||||
@Rule
|
||||
public SshFixture fixture = new SshFixture();
|
||||
@@ -62,61 +62,63 @@ public class RemotePortForwarderTest {
|
||||
@Test
|
||||
public void shouldHaveWorkingHttpServer() throws IOException {
|
||||
// Just to check that we have a working http server...
|
||||
assertThat(httpGet("127.0.0.1", 8080), equalTo(200));
|
||||
assertEquals(200, httpGet( 8080));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldDynamicallyForwardPortForLocalhost() throws IOException {
|
||||
SSHClient sshClient = getFixtureClient();
|
||||
RemotePortForwarder.Forward bind = forwardPort(sshClient, "127.0.0.1", new SinglePort(0));
|
||||
assertThat(httpGet("127.0.0.1", bind.getPort()), equalTo(200));
|
||||
assertHttpGetSuccess(bind);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldDynamicallyForwardPortForAllIPv4() throws IOException {
|
||||
SSHClient sshClient = getFixtureClient();
|
||||
RemotePortForwarder.Forward bind = forwardPort(sshClient, "0.0.0.0", new SinglePort(0));
|
||||
assertThat(httpGet("127.0.0.1", bind.getPort()), equalTo(200));
|
||||
assertHttpGetSuccess(bind);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldDynamicallyForwardPortForAllProtocols() throws IOException {
|
||||
SSHClient sshClient = getFixtureClient();
|
||||
RemotePortForwarder.Forward bind = forwardPort(sshClient, "", new SinglePort(0));
|
||||
assertThat(httpGet("127.0.0.1", bind.getPort()), equalTo(200));
|
||||
assertHttpGetSuccess(bind);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldForwardPortForLocalhost() throws IOException {
|
||||
SSHClient sshClient = getFixtureClient();
|
||||
RemotePortForwarder.Forward bind = forwardPort(sshClient, "127.0.0.1", RANGE);
|
||||
assertThat(httpGet("127.0.0.1", bind.getPort()), equalTo(200));
|
||||
assertHttpGetSuccess(bind);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldForwardPortForAllIPv4() throws IOException {
|
||||
SSHClient sshClient = getFixtureClient();
|
||||
RemotePortForwarder.Forward bind = forwardPort(sshClient, "0.0.0.0", RANGE);
|
||||
assertThat(httpGet("127.0.0.1", bind.getPort()), equalTo(200));
|
||||
assertHttpGetSuccess(bind);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldForwardPortForAllProtocols() throws IOException {
|
||||
SSHClient sshClient = getFixtureClient();
|
||||
RemotePortForwarder.Forward bind = forwardPort(sshClient, "", RANGE);
|
||||
assertThat(httpGet("127.0.0.1", bind.getPort()), equalTo(200));
|
||||
assertHttpGetSuccess(bind);
|
||||
}
|
||||
|
||||
private void assertHttpGetSuccess(final RemotePortForwarder.Forward bind) throws IOException {
|
||||
assertEquals(200, httpGet(bind.getPort()));
|
||||
}
|
||||
|
||||
private RemotePortForwarder.Forward forwardPort(SSHClient sshClient, String address, PortRange portRange) throws IOException {
|
||||
while (true) {
|
||||
try {
|
||||
RemotePortForwarder.Forward forward = sshClient.getRemotePortForwarder().bind(
|
||||
return sshClient.getRemotePortForwarder().bind(
|
||||
// where the server should listen
|
||||
new RemotePortForwarder.Forward(address, portRange.nextPort()),
|
||||
// what we do with incoming connections that are forwarded to us
|
||||
new SocketForwardingConnectListener(HTTP_SERVER_SOCKET_ADDR));
|
||||
|
||||
return forward;
|
||||
} catch (ConnectionException ce) {
|
||||
if (!portRange.hasNext()) {
|
||||
throw ce;
|
||||
@@ -125,9 +127,9 @@ public class RemotePortForwarderTest {
|
||||
}
|
||||
}
|
||||
|
||||
private int httpGet(String server, int port) throws IOException {
|
||||
private int httpGet(int port) throws IOException {
|
||||
HttpClient client = HttpClientBuilder.create().build();
|
||||
String urlString = "http://" + server + ":" + port;
|
||||
String urlString = "http://" + LOCALHOST + ":" + port;
|
||||
log.info("Trying: GET " + urlString);
|
||||
HttpResponse execute = client.execute(new HttpGet(urlString));
|
||||
return execute.getStatusLine().getStatusCode();
|
||||
@@ -140,7 +142,7 @@ public class RemotePortForwarderTest {
|
||||
}
|
||||
|
||||
private static class PortRange {
|
||||
private int upper;
|
||||
private final int upper;
|
||||
private int current;
|
||||
|
||||
public PortRange(int lower, int upper) {
|
||||
|
||||
@@ -15,54 +15,67 @@
|
||||
*/
|
||||
package com.hierynomus.sshj.keepalive;
|
||||
|
||||
import com.hierynomus.sshj.test.KnownFailingTests;
|
||||
import com.hierynomus.sshj.test.SlowTests;
|
||||
import com.hierynomus.sshj.test.SshFixture;
|
||||
import net.schmizz.keepalive.KeepAlive;
|
||||
import net.schmizz.keepalive.KeepAliveProvider;
|
||||
import net.schmizz.sshj.DefaultConfig;
|
||||
import net.schmizz.sshj.SSHClient;
|
||||
import net.schmizz.sshj.userauth.UserAuthException;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.experimental.categories.Category;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.management.ManagementFactory;
|
||||
import java.lang.management.ThreadInfo;
|
||||
import java.lang.management.ThreadMXBean;
|
||||
|
||||
import static org.junit.Assert.fail;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class KeepAliveThreadTerminationTest {
|
||||
|
||||
private static final int KEEP_ALIVE_SECONDS = 1;
|
||||
|
||||
private static final long STOP_SLEEP = 1500;
|
||||
|
||||
@Rule
|
||||
public SshFixture fixture = new SshFixture();
|
||||
|
||||
@Test
|
||||
@Category({SlowTests.class, KnownFailingTests.class})
|
||||
public void shouldCorrectlyTerminateThreadOnDisconnect() throws IOException, InterruptedException {
|
||||
DefaultConfig defaultConfig = new DefaultConfig();
|
||||
defaultConfig.setKeepAliveProvider(KeepAliveProvider.KEEP_ALIVE);
|
||||
for (int i = 0; i < 10; i++) {
|
||||
SSHClient sshClient = fixture.setupClient(defaultConfig);
|
||||
fixture.connectClient(sshClient);
|
||||
sshClient.getConnection().getKeepAlive().setKeepAliveInterval(1);
|
||||
try {
|
||||
sshClient.authPassword("bad", "credentials");
|
||||
fail("Should not auth.");
|
||||
} catch (UserAuthException e) {
|
||||
// OK
|
||||
}
|
||||
fixture.stopClient();
|
||||
Thread.sleep(2000);
|
||||
}
|
||||
public void shouldNotStartThreadOnSetKeepAliveInterval() {
|
||||
final SSHClient sshClient = setupClient();
|
||||
|
||||
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
|
||||
for (long l : threadMXBean.getAllThreadIds()) {
|
||||
ThreadInfo threadInfo = threadMXBean.getThreadInfo(l);
|
||||
if (threadInfo.getThreadName().equals("keep-alive") && threadInfo.getThreadState() != Thread.State.TERMINATED) {
|
||||
fail("Found alive keep-alive thread in state " + threadInfo.getThreadState());
|
||||
}
|
||||
}
|
||||
final KeepAlive keepAlive = sshClient.getConnection().getKeepAlive();
|
||||
assertTrue(keepAlive.isDaemon());
|
||||
assertFalse(keepAlive.isAlive());
|
||||
assertEquals(Thread.State.NEW, keepAlive.getState());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldStartThreadOnConnectAndInterruptOnDisconnect() throws IOException, InterruptedException {
|
||||
final SSHClient sshClient = setupClient();
|
||||
|
||||
final KeepAlive keepAlive = sshClient.getConnection().getKeepAlive();
|
||||
assertTrue(keepAlive.isDaemon());
|
||||
assertEquals(Thread.State.NEW, keepAlive.getState());
|
||||
|
||||
fixture.connectClient(sshClient);
|
||||
|
||||
assertThrows(UserAuthException.class, () -> sshClient.authPassword("bad", "credentials"));
|
||||
|
||||
assertEquals(Thread.State.TIMED_WAITING, keepAlive.getState());
|
||||
|
||||
fixture.stopClient();
|
||||
Thread.sleep(STOP_SLEEP);
|
||||
|
||||
assertFalse(keepAlive.isAlive());
|
||||
assertEquals(Thread.State.TERMINATED, keepAlive.getState());
|
||||
}
|
||||
|
||||
private SSHClient setupClient() {
|
||||
final DefaultConfig defaultConfig = new DefaultConfig();
|
||||
defaultConfig.setKeepAliveProvider(KeepAliveProvider.KEEP_ALIVE);
|
||||
final SSHClient sshClient = fixture.setupClient(defaultConfig);
|
||||
sshClient.getConnection().getKeepAlive().setKeepAliveInterval(KEEP_ALIVE_SECONDS);
|
||||
return sshClient;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,21 +17,25 @@ package com.hierynomus.sshj.sftp;
|
||||
|
||||
import com.hierynomus.sshj.test.SshFixture;
|
||||
import net.schmizz.sshj.SSHClient;
|
||||
import net.schmizz.sshj.common.ByteArrayUtils;
|
||||
import net.schmizz.sshj.sftp.OpenMode;
|
||||
import net.schmizz.sshj.sftp.RemoteFile;
|
||||
import net.schmizz.sshj.sftp.SFTPEngine;
|
||||
import net.schmizz.sshj.sftp.SFTPException;
|
||||
import org.apache.sshd.common.util.io.IoUtils;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.*;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.EnumSet;
|
||||
import java.util.Random;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.equalTo;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
public class RemoteFileTest {
|
||||
@Rule
|
||||
@@ -84,4 +88,141 @@ public class RemoteFileTest {
|
||||
|
||||
assertThat("The written and received data should match", data, equalTo(test2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldNotReadAheadAfterLimitInputStream() throws IOException {
|
||||
SSHClient ssh = fixture.setupConnectedDefaultClient();
|
||||
ssh.authPassword("test", "test");
|
||||
SFTPEngine sftp = new SFTPEngine(ssh).init();
|
||||
|
||||
RemoteFile rf;
|
||||
File file = temp.newFile("SftpReadAheadLimitTest.bin");
|
||||
rf = sftp.open(file.getPath(), EnumSet.of(OpenMode.WRITE, OpenMode.CREAT));
|
||||
byte[] data = new byte[8192];
|
||||
new Random(53).nextBytes(data);
|
||||
data[3072] = 1;
|
||||
rf.write(0, data, 0, data.length);
|
||||
rf.close();
|
||||
|
||||
assertThat("The file should exist", file.exists());
|
||||
|
||||
rf = sftp.open(file.getPath());
|
||||
InputStream rs = rf.new ReadAheadRemoteFileInputStream(16 /*maxUnconfirmedReads*/,0, 3072);
|
||||
|
||||
byte[] test = new byte[4097];
|
||||
int n = 0;
|
||||
|
||||
while (n < 2048) {
|
||||
n += rs.read(test, n, 2048 - n);
|
||||
}
|
||||
|
||||
rf.close();
|
||||
|
||||
while (n < 3072) {
|
||||
n += rs.read(test, n, 3072 - n);
|
||||
}
|
||||
|
||||
assertThat("buffer overrun", test[3072] == 0);
|
||||
|
||||
try {
|
||||
rs.read(test, n, test.length - n);
|
||||
fail("Content must not be buffered");
|
||||
} catch (SFTPException e){
|
||||
// expected
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void limitedReadAheadInputStream() throws IOException {
|
||||
SSHClient ssh = fixture.setupConnectedDefaultClient();
|
||||
ssh.authPassword("test", "test");
|
||||
SFTPEngine sftp = new SFTPEngine(ssh).init();
|
||||
|
||||
RemoteFile rf;
|
||||
File file = temp.newFile("SftpReadAheadLimitedTest.bin");
|
||||
rf = sftp.open(file.getPath(), EnumSet.of(OpenMode.WRITE, OpenMode.CREAT));
|
||||
byte[] data = new byte[8192];
|
||||
new Random(53).nextBytes(data);
|
||||
data[3072] = 1;
|
||||
rf.write(0, data, 0, data.length);
|
||||
rf.close();
|
||||
|
||||
assertThat("The file should exist", file.exists());
|
||||
|
||||
rf = sftp.open(file.getPath());
|
||||
InputStream rs = rf.new ReadAheadRemoteFileInputStream(16 /*maxUnconfirmedReads*/,0, 3072);
|
||||
|
||||
byte[] test = new byte[4097];
|
||||
int n = 0;
|
||||
|
||||
while (n < 2048) {
|
||||
n += rs.read(test, n, 2048 - n);
|
||||
}
|
||||
|
||||
while (n < 3072) {
|
||||
n += rs.read(test, n, 3072 - n);
|
||||
}
|
||||
|
||||
assertThat("buffer overrun", test[3072] == 0);
|
||||
|
||||
n += rs.read(test, n, test.length - n); // --> ArrayIndexOutOfBoundsException
|
||||
|
||||
byte[] test2 = new byte[data.length];
|
||||
System.arraycopy(test, 0, test2, 0, test.length);
|
||||
|
||||
while (n < data.length) {
|
||||
n += rs.read(test2, n, data.length - n);
|
||||
}
|
||||
|
||||
assertThat("The written and received data should match", data, equalTo(test2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldReadCorrectlyWhenWrappedInBufferedStream_FullSizeBuffer() throws IOException {
|
||||
doTestShouldReadCorrectlyWhenWrappedInBufferedStream(1024 * 1024, 1024 * 1024);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldReadCorrectlyWhenWrappedInBufferedStream_HalfSizeBuffer() throws IOException {
|
||||
doTestShouldReadCorrectlyWhenWrappedInBufferedStream(1024 * 1024, 512 * 1024);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldReadCorrectlyWhenWrappedInBufferedStream_QuarterSizeBuffer() throws IOException {
|
||||
doTestShouldReadCorrectlyWhenWrappedInBufferedStream(1024 * 1024, 256 * 1024);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldReadCorrectlyWhenWrappedInBufferedStream_SmallSizeBuffer() throws IOException {
|
||||
doTestShouldReadCorrectlyWhenWrappedInBufferedStream(1024 * 1024, 1024);
|
||||
}
|
||||
|
||||
private void doTestShouldReadCorrectlyWhenWrappedInBufferedStream(int fileSize, int bufferSize) throws IOException {
|
||||
SSHClient ssh = fixture.setupConnectedDefaultClient();
|
||||
ssh.authPassword("test", "test");
|
||||
SFTPEngine sftp = new SFTPEngine(ssh).init();
|
||||
|
||||
final byte[] expected = new byte[fileSize];
|
||||
new SecureRandom(new byte[] { 31 }).nextBytes(expected);
|
||||
|
||||
File file = temp.newFile("shouldReadCorrectlyWhenWrappedInBufferedStream.bin");
|
||||
try (OutputStream fStream = new FileOutputStream(file)) {
|
||||
IoUtils.copy(new ByteArrayInputStream(expected), fStream);
|
||||
}
|
||||
|
||||
RemoteFile rf = sftp.open(file.getPath());
|
||||
final byte[] actual;
|
||||
try (InputStream inputStream = new BufferedInputStream(
|
||||
rf.new ReadAheadRemoteFileInputStream(10),
|
||||
bufferSize)
|
||||
) {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
IoUtils.copy(inputStream, baos, expected.length);
|
||||
actual = baos.toByteArray();
|
||||
}
|
||||
|
||||
assertEquals("The file should be fully read", expected.length, actual.length);
|
||||
assertThat("The file should be read correctly",
|
||||
ByteArrayUtils.equals(expected, 0, actual, 0, expected.length));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,21 +19,18 @@ import net.schmizz.sshj.Config;
|
||||
import net.schmizz.sshj.DefaultConfig;
|
||||
import net.schmizz.sshj.SSHClient;
|
||||
import net.schmizz.sshj.util.gss.BogusGSSAuthenticator;
|
||||
import org.apache.sshd.common.NamedFactory;
|
||||
import org.apache.sshd.common.keyprovider.ClassLoadableResourceKeyPairProvider;
|
||||
import org.apache.sshd.common.util.OsUtils;
|
||||
import org.apache.sshd.scp.server.ScpCommandFactory;
|
||||
import org.apache.sshd.server.SshServer;
|
||||
import org.apache.sshd.server.auth.password.PasswordAuthenticator;
|
||||
import org.apache.sshd.server.command.Command;
|
||||
import org.apache.sshd.server.command.CommandFactory;
|
||||
import org.apache.sshd.server.scp.ScpCommandFactory;
|
||||
import org.apache.sshd.server.session.ServerSession;
|
||||
import org.apache.sshd.server.shell.ProcessShellFactory;
|
||||
import org.apache.sshd.server.subsystem.sftp.SftpSubsystemFactory;
|
||||
import org.apache.sshd.sftp.server.SftpSubsystemFactory;
|
||||
import org.junit.rules.ExternalResource;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.net.ServerSocket;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
@@ -42,10 +39,11 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
public class SshFixture extends ExternalResource {
|
||||
public static final String hostkey = "hostkey.pem";
|
||||
public static final String fingerprint = "ce:a7:c1:cf:17:3f:96:49:6a:53:1a:05:0b:ba:90:db";
|
||||
public static final String listCommand = OsUtils.isWin32() ? "cmd.exe /C dir" : "ls";
|
||||
|
||||
private SshServer server = defaultSshServer();
|
||||
private final SshServer server = defaultSshServer();
|
||||
private SSHClient client = null;
|
||||
private AtomicBoolean started = new AtomicBoolean(false);
|
||||
private final AtomicBoolean started = new AtomicBoolean(false);
|
||||
private boolean autoStart = true;
|
||||
|
||||
public SshFixture(boolean autoStart) {
|
||||
@@ -108,38 +106,23 @@ public class SshFixture extends ExternalResource {
|
||||
sshServer.setPort(randomPort());
|
||||
ClassLoadableResourceKeyPairProvider fileKeyPairProvider = new ClassLoadableResourceKeyPairProvider(hostkey);
|
||||
sshServer.setKeyPairProvider(fileKeyPairProvider);
|
||||
sshServer.setPasswordAuthenticator(new PasswordAuthenticator() {
|
||||
@Override
|
||||
public boolean authenticate(String username, String password, ServerSession session) {
|
||||
return username.equals(password);
|
||||
}
|
||||
});
|
||||
sshServer.setPasswordAuthenticator((username, password, session) -> username.equals(password));
|
||||
sshServer.setGSSAuthenticator(new BogusGSSAuthenticator());
|
||||
sshServer.setSubsystemFactories(Arrays.<NamedFactory<Command>>asList(new SftpSubsystemFactory()));
|
||||
sshServer.setSubsystemFactories(Collections.singletonList(new SftpSubsystemFactory()));
|
||||
ScpCommandFactory commandFactory = new ScpCommandFactory();
|
||||
commandFactory.setDelegateCommandFactory(new CommandFactory() {
|
||||
@Override
|
||||
public Command createCommand(String command) {
|
||||
return new ProcessShellFactory(command.split(" ")).create();
|
||||
}
|
||||
});
|
||||
commandFactory.setDelegateCommandFactory((session, command) -> new ProcessShellFactory(command, command.split(" ")).createShell(session));
|
||||
sshServer.setCommandFactory(commandFactory);
|
||||
sshServer.setShellFactory(new ProcessShellFactory("ls"));
|
||||
sshServer.setShellFactory(new ProcessShellFactory(listCommand, listCommand.split(" ")));
|
||||
return sshServer;
|
||||
}
|
||||
|
||||
private int randomPort() {
|
||||
try {
|
||||
ServerSocket s = null;
|
||||
try {
|
||||
s = new ServerSocket(0);
|
||||
try (final ServerSocket s = new ServerSocket(0)) {
|
||||
return s.getLocalPort();
|
||||
} finally {
|
||||
if (s != null)
|
||||
s.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -39,4 +39,8 @@ public class FileUtil {
|
||||
IOUtils.closeQuietly(fileInputStream);
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean compareFileContents(File f1, File f2) throws IOException {
|
||||
return readFromFile(f1).equals(readFromFile(f2));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,9 +22,8 @@ import net.schmizz.sshj.common.Factory;
|
||||
import net.schmizz.sshj.transport.kex.DHGexSHA1;
|
||||
import net.schmizz.sshj.transport.kex.DHGexSHA256;
|
||||
import net.schmizz.sshj.transport.kex.ECDHNistP;
|
||||
import org.apache.sshd.common.NamedFactory;
|
||||
import org.apache.sshd.common.kex.BuiltinDHFactories;
|
||||
import org.apache.sshd.common.kex.KeyExchange;
|
||||
import org.apache.sshd.common.kex.KeyExchangeFactory;
|
||||
import org.apache.sshd.server.SshServer;
|
||||
import org.apache.sshd.server.kex.DHGEXServer;
|
||||
import org.apache.sshd.server.kex.DHGServer;
|
||||
@@ -38,6 +37,7 @@ import java.util.Collections;
|
||||
@RunWith(Parameterized.class)
|
||||
public class KeyExchangeTest extends BaseAlgorithmTest {
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@Parameterized.Parameters(name = "algorithm={0}")
|
||||
public static Collection<Object[]> getParameters() {
|
||||
return Arrays.asList(new Object[][]{
|
||||
@@ -56,10 +56,10 @@ public class KeyExchangeTest extends BaseAlgorithmTest {
|
||||
});
|
||||
}
|
||||
|
||||
private Factory.Named<net.schmizz.sshj.transport.kex.KeyExchange> clientFactory;
|
||||
private NamedFactory<KeyExchange> serverFactory;
|
||||
private final Factory.Named<net.schmizz.sshj.transport.kex.KeyExchange> clientFactory;
|
||||
private final KeyExchangeFactory serverFactory;
|
||||
|
||||
public KeyExchangeTest(NamedFactory<KeyExchange> serverFactory, Factory.Named<net.schmizz.sshj.transport.kex.KeyExchange> clientFactory) {
|
||||
public KeyExchangeTest(KeyExchangeFactory serverFactory, Factory.Named<net.schmizz.sshj.transport.kex.KeyExchange> clientFactory) {
|
||||
this.clientFactory = clientFactory;
|
||||
this.serverFactory = serverFactory;
|
||||
}
|
||||
|
||||
@@ -12,19 +12,23 @@
|
||||
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
package org.mindrot.jbcrypt;
|
||||
package com.hierynomus.sshj.userauth.keyprovider.bcrypt;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
/**
|
||||
* JUnit unit tests for BCrypt routines
|
||||
* @author Damien Miller
|
||||
* @version 0.2
|
||||
*/
|
||||
public class TestBCrypt extends TestCase {
|
||||
String test_vectors[][] = {
|
||||
public class BCryptTest {
|
||||
String[][] test_vectors = {
|
||||
{ "",
|
||||
"$2a$06$DCq7YPn5Rq63x1Lad4cll.",
|
||||
"$2a$06$DCq7YPn5Rq63x1Lad4cll.TV4S6ytwfsfvkgY8jIucDrjc8deX1s." },
|
||||
@@ -87,17 +91,10 @@ public class TestBCrypt extends TestCase {
|
||||
"$2a$12$WApznUOJfkEGSmYRfnkrPOr466oFDCaj4b6HY3EXGvfxm43seyhgC" },
|
||||
};
|
||||
|
||||
/**
|
||||
* Entry point for unit tests
|
||||
* @param args unused
|
||||
*/
|
||||
public static void main(String[] args) {
|
||||
junit.textui.TestRunner.run(TestBCrypt.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test method for 'BCrypt.hashpw(String, String)'
|
||||
*/
|
||||
@Test
|
||||
public void testHashpw() {
|
||||
System.out.print("BCrypt.hashpw(): ");
|
||||
for (int i = 0; i < test_vectors.length; i++) {
|
||||
@@ -108,16 +105,16 @@ public class TestBCrypt extends TestCase {
|
||||
assertEquals(hashed, expected);
|
||||
System.out.print(".");
|
||||
}
|
||||
System.out.println("");
|
||||
}
|
||||
|
||||
/**
|
||||
* Test method for 'BCrypt.gensalt(int)'
|
||||
*/
|
||||
@Test
|
||||
public void testGensaltInt() {
|
||||
System.out.print("BCrypt.gensalt(log_rounds):");
|
||||
for (int i = 4; i <= 12; i++) {
|
||||
System.out.print(" " + Integer.toString(i) + ":");
|
||||
System.out.print(" " + i + ":");
|
||||
for (int j = 0; j < test_vectors.length; j += 4) {
|
||||
String plain = test_vectors[j][0];
|
||||
String salt = BCrypt.gensalt(i);
|
||||
@@ -127,12 +124,12 @@ public class TestBCrypt extends TestCase {
|
||||
System.out.print(".");
|
||||
}
|
||||
}
|
||||
System.out.println("");
|
||||
}
|
||||
|
||||
/**
|
||||
* Test method for 'BCrypt.gensalt()'
|
||||
*/
|
||||
@Test
|
||||
public void testGensalt() {
|
||||
System.out.print("BCrypt.gensalt(): ");
|
||||
for (int i = 0; i < test_vectors.length; i += 4) {
|
||||
@@ -143,13 +140,13 @@ public class TestBCrypt extends TestCase {
|
||||
assertEquals(hashed1, hashed2);
|
||||
System.out.print(".");
|
||||
}
|
||||
System.out.println("");
|
||||
}
|
||||
|
||||
/**
|
||||
* Test method for 'BCrypt.checkpw(String, String)'
|
||||
* expecting success
|
||||
*/
|
||||
@Test
|
||||
public void testCheckpw_success() {
|
||||
System.out.print("BCrypt.checkpw w/ good passwords: ");
|
||||
for (int i = 0; i < test_vectors.length; i++) {
|
||||
@@ -158,13 +155,13 @@ public class TestBCrypt extends TestCase {
|
||||
assertTrue(BCrypt.checkpw(plain, expected));
|
||||
System.out.print(".");
|
||||
}
|
||||
System.out.println("");
|
||||
}
|
||||
|
||||
/**
|
||||
* Test method for 'BCrypt.checkpw(String, String)'
|
||||
* expecting failure
|
||||
*/
|
||||
@Test
|
||||
public void testCheckpw_failure() {
|
||||
System.out.print("BCrypt.checkpw w/ bad passwords: ");
|
||||
for (int i = 0; i < test_vectors.length; i++) {
|
||||
@@ -174,12 +171,12 @@ public class TestBCrypt extends TestCase {
|
||||
assertFalse(BCrypt.checkpw(plain, expected));
|
||||
System.out.print(".");
|
||||
}
|
||||
System.out.println("");
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for correct hashing of non-US-ASCII passwords
|
||||
*/
|
||||
@Test
|
||||
public void testInternationalChars() {
|
||||
System.out.print("BCrypt.hashpw w/ international chars: ");
|
||||
String pw1 = "\u2605\u2605\u2605\u2605\u2605\u2605\u2605\u2605";
|
||||
@@ -192,7 +189,6 @@ public class TestBCrypt extends TestCase {
|
||||
String h2 = BCrypt.hashpw(pw2, BCrypt.gensalt());
|
||||
assertFalse(BCrypt.checkpw(pw1, h2));
|
||||
System.out.print(".");
|
||||
System.out.println("");
|
||||
}
|
||||
|
||||
private static class BCryptHashTV {
|
||||
@@ -242,7 +238,8 @@ public class TestBCrypt extends TestCase {
|
||||
}),
|
||||
};
|
||||
|
||||
public void testBCryptHashTestVectors() throws Exception {
|
||||
@Test
|
||||
public void testBCryptHashTestVectors() {
|
||||
System.out.print("BCrypt.hash w/ known vectors: ");
|
||||
for (BCryptHashTV tv : bcrypt_hash_test_vectors) {
|
||||
byte[] output = new byte[tv.out.length];
|
||||
@@ -250,7 +247,6 @@ public class TestBCrypt extends TestCase {
|
||||
assertEquals(Arrays.toString(tv.out), Arrays.toString(output));
|
||||
System.out.print(".");
|
||||
}
|
||||
System.out.println("");
|
||||
}
|
||||
|
||||
private static class BCryptPbkdfTV {
|
||||
@@ -281,7 +277,8 @@ public class TestBCrypt extends TestCase {
|
||||
(byte) 0x83, (byte) 0x3c, (byte) 0xf0, (byte) 0xdc, (byte) 0xf5, (byte) 0x6d, (byte) 0xb6, (byte) 0x56, (byte) 0x08, (byte) 0xe8, (byte) 0xf0, (byte) 0xdc, (byte) 0x0c, (byte) 0xe8, (byte) 0x82, (byte) 0xbd}),
|
||||
};
|
||||
|
||||
public void testBCryptPbkdfTestVectors() throws Exception {
|
||||
@Test
|
||||
public void testBCryptPbkdfTestVectors() {
|
||||
System.out.print("BCrypt.pbkdf w/ known vectors: ");
|
||||
for (BCryptPbkdfTV tv : bcrypt_pbkdf_test_vectors) {
|
||||
byte[] output = new byte[tv.out.length];
|
||||
@@ -289,6 +286,5 @@ public class TestBCrypt extends TestCase {
|
||||
assertEquals(Arrays.toString(tv.out), Arrays.toString(output));
|
||||
System.out.print(".");
|
||||
}
|
||||
System.out.println("");
|
||||
}
|
||||
}
|
||||
@@ -20,12 +20,8 @@ import net.schmizz.sshj.SSHClient;
|
||||
import net.schmizz.sshj.userauth.method.AuthKeyboardInteractive;
|
||||
import net.schmizz.sshj.userauth.method.ChallengeResponseProvider;
|
||||
import net.schmizz.sshj.userauth.password.Resource;
|
||||
import org.apache.sshd.client.auth.keyboard.UserAuthKeyboardInteractiveFactory;
|
||||
import org.apache.sshd.common.NamedFactory;
|
||||
import org.apache.sshd.server.auth.UserAuth;
|
||||
import org.apache.sshd.server.auth.keyboard.UserAuthKeyboardInteractive;
|
||||
import org.apache.sshd.server.auth.password.PasswordAuthenticator;
|
||||
import org.apache.sshd.server.session.ServerSession;
|
||||
import org.apache.sshd.server.auth.keyboard.UserAuthKeyboardInteractiveFactory;
|
||||
import org.apache.sshd.server.auth.password.AcceptAllPasswordAuthenticator;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
@@ -43,28 +39,8 @@ public class AuthKeyboardInteractiveTest {
|
||||
|
||||
@Before
|
||||
public void setKeyboardInteractiveAuthenticator() throws IOException {
|
||||
fixture.getServer().setUserAuthFactories(Collections.<NamedFactory<UserAuth>>singletonList(new NamedFactory<UserAuth>() {
|
||||
@Override
|
||||
public String getName() {
|
||||
return UserAuthKeyboardInteractiveFactory.NAME;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserAuth get() {
|
||||
return new UserAuthKeyboardInteractive();
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserAuth create() {
|
||||
return get();
|
||||
}
|
||||
}));
|
||||
fixture.getServer().setPasswordAuthenticator(new PasswordAuthenticator() {
|
||||
@Override
|
||||
public boolean authenticate(String username, String password, ServerSession session) {
|
||||
return password.equals(username);
|
||||
}
|
||||
});
|
||||
fixture.getServer().setUserAuthFactories(Collections.singletonList(new UserAuthKeyboardInteractiveFactory()));
|
||||
fixture.getServer().setPasswordAuthenticator(AcceptAllPasswordAuthenticator.INSTANCE);
|
||||
fixture.getServer().start();
|
||||
}
|
||||
|
||||
@@ -75,7 +51,7 @@ public class AuthKeyboardInteractiveTest {
|
||||
sshClient.auth(userAndPassword, new AuthKeyboardInteractive(new ChallengeResponseProvider() {
|
||||
@Override
|
||||
public List<String> getSubmethods() {
|
||||
return new ArrayList<String>();
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -21,56 +21,29 @@ import net.schmizz.sshj.userauth.UserAuthException;
|
||||
import net.schmizz.sshj.userauth.password.PasswordFinder;
|
||||
import net.schmizz.sshj.userauth.password.PasswordUpdateProvider;
|
||||
import net.schmizz.sshj.userauth.password.Resource;
|
||||
import org.apache.sshd.common.NamedFactory;
|
||||
import org.apache.sshd.common.util.buffer.Buffer;
|
||||
import org.apache.sshd.server.auth.UserAuth;
|
||||
import org.apache.sshd.server.auth.password.PasswordAuthenticator;
|
||||
import org.apache.sshd.server.auth.password.PasswordChangeRequiredException;
|
||||
import org.apache.sshd.server.auth.password.UserAuthPassword;
|
||||
import org.apache.sshd.server.auth.password.UserAuthPasswordFactory;
|
||||
import org.apache.sshd.server.session.ServerSession;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.ExpectedException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.Stack;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
|
||||
public class AuthPasswordTest {
|
||||
|
||||
@Rule
|
||||
public SshFixture fixture = new SshFixture(false);
|
||||
|
||||
@Rule
|
||||
public ExpectedException expectedException = ExpectedException.none();
|
||||
|
||||
@Before
|
||||
public void setPasswordAuthenticator() throws IOException {
|
||||
fixture.getServer().setUserAuthFactories(Collections.<NamedFactory<UserAuth>>singletonList(new NamedFactory<UserAuth>() {
|
||||
@Override
|
||||
public String getName() {
|
||||
return UserAuthPasswordFactory.NAME;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserAuth get() {
|
||||
return new UserAuthPassword() {
|
||||
@Override
|
||||
protected Boolean handleClientPasswordChangeRequest(Buffer buffer, ServerSession session, String username, String oldPassword, String newPassword) throws Exception {
|
||||
return session.getPasswordAuthenticator().authenticate(username, newPassword, session);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserAuth create() {
|
||||
return get();
|
||||
}
|
||||
}));
|
||||
fixture.getServer().setUserAuthFactories(Collections.singletonList(new UserAuthPasswordFactory()));
|
||||
fixture.getServer().setPasswordAuthenticator(new PasswordAuthenticator() {
|
||||
@Override
|
||||
public boolean authenticate(String username, String password, ServerSession session) {
|
||||
@@ -80,6 +53,12 @@ public class AuthPasswordTest {
|
||||
return password.equals(username);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handleClientPasswordChangeRequest(
|
||||
ServerSession session, String username, String oldPassword, String newPassword) {
|
||||
return username.equals(newPassword);
|
||||
}
|
||||
});
|
||||
fixture.getServer().start();
|
||||
}
|
||||
@@ -87,8 +66,7 @@ public class AuthPasswordTest {
|
||||
@Test
|
||||
public void shouldNotHandlePasswordChangeIfNoPasswordUpdateProviderSet() throws IOException {
|
||||
SSHClient sshClient = fixture.setupConnectedDefaultClient();
|
||||
expectedException.expect(UserAuthException.class);
|
||||
sshClient.authPassword("jeroen", "changeme");
|
||||
assertThrows(UserAuthException.class, () -> sshClient.authPassword("jeroen", "changeme"));
|
||||
assertThat("Should not have authenticated", !sshClient.isAuthenticated());
|
||||
}
|
||||
|
||||
@@ -112,8 +90,7 @@ public class AuthPasswordTest {
|
||||
@Test
|
||||
public void shouldHandlePasswordChangeWithWrongPassword() throws IOException {
|
||||
SSHClient sshClient = fixture.setupConnectedDefaultClient();
|
||||
expectedException.expect(UserAuthException.class);
|
||||
sshClient.authPassword("jeroen", new PasswordFinder() {
|
||||
assertThrows(UserAuthException.class, () -> sshClient.authPassword("jeroen", new PasswordFinder() {
|
||||
@Override
|
||||
public char[] reqPassword(Resource<?> resource) {
|
||||
return "changeme".toCharArray();
|
||||
@@ -123,7 +100,7 @@ public class AuthPasswordTest {
|
||||
public boolean shouldRetry(Resource<?> resource) {
|
||||
return false;
|
||||
}
|
||||
}, new StaticPasswordUpdateProvider("bad"));
|
||||
}, new StaticPasswordUpdateProvider("bad")));
|
||||
assertThat("Should not have authenticated", !sshClient.isAuthenticated());
|
||||
}
|
||||
|
||||
@@ -153,7 +130,7 @@ public class AuthPasswordTest {
|
||||
}
|
||||
|
||||
private static class StaticPasswordUpdateProvider implements PasswordUpdateProvider {
|
||||
private Stack<String> newPasswords = new Stack<String>();
|
||||
private final Stack<String> newPasswords = new Stack<>();
|
||||
|
||||
public StaticPasswordUpdateProvider(String... newPasswords) {
|
||||
for (int i = newPasswords.length - 1; i >= 0; i--) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user