mirror of
https://github.com/hierynomus/sshj.git
synced 2025-12-06 15:20:54 +03:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
20e2161022 | ||
|
|
fb0f3afa17 | ||
|
|
114c2bb424 | ||
|
|
079bde5dbf | ||
|
|
eaee42b017 | ||
|
|
8b61d96808 | ||
|
|
73fcc81e83 | ||
|
|
0f7926d4fa | ||
|
|
ca6f15650a | ||
|
|
eb78dc499d | ||
|
|
a852f33a15 | ||
|
|
ccabc1a20c | ||
|
|
cb2986d32e | ||
|
|
dc70f08e45 | ||
|
|
bf68ec18b2 | ||
|
|
7e78260ca9 | ||
|
|
27c60cee60 | ||
|
|
551b8b4fcf | ||
|
|
fd591e70be |
@@ -1,3 +1,4 @@
|
||||
Shikhar Bhushan <shikhar@schmizz.net>
|
||||
Cyril Ledru <cledru@keynectis.net>
|
||||
Incendium <incendium@gmail.com>
|
||||
Incendium <incendium@gmail.com>
|
||||
Philip Langdale <philipl@cloudera.com>
|
||||
24
pom.xml
24
pom.xml
@@ -6,7 +6,7 @@
|
||||
<groupId>net.schmizz</groupId>
|
||||
<artifactId>sshj</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
<version>0.4.0</version>
|
||||
<version>0.4.1</version>
|
||||
|
||||
<name>sshj</name>
|
||||
<description>SSHv2 library for Java</description>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bcprov-jdk16</artifactId>
|
||||
<version>1.45</version>
|
||||
<version>1.46</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<version>4.8.1</version>
|
||||
<version>4.8.2</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
@@ -60,19 +60,19 @@
|
||||
<dependency>
|
||||
<groupId>org.apache.sshd</groupId>
|
||||
<artifactId>sshd-core</artifactId>
|
||||
<version>0.4.0</version>
|
||||
<version>0.5.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>ch.qos.logback</groupId>
|
||||
<artifactId>logback-core</artifactId>
|
||||
<version>0.9.24</version>
|
||||
<version>0.9.29</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>ch.qos.logback</groupId>
|
||||
<artifactId>logback-classic</artifactId>
|
||||
<version>0.9.24</version>
|
||||
<version>0.9.29</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
@@ -96,6 +96,7 @@
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>2.3.2</version>
|
||||
<configuration>
|
||||
<excludes>
|
||||
<exclude>examples/*.java</exclude>
|
||||
@@ -107,11 +108,14 @@
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-release-plugin</artifactId>
|
||||
<version>2.0</version>
|
||||
<version>2.1</version>
|
||||
<configuration>
|
||||
<mavenExecutorId>forked-path</mavenExecutorId>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-assembly-plugin</artifactId>
|
||||
<version>2.2-beta-5</version>
|
||||
<version>2.2.1</version>
|
||||
<configuration>
|
||||
<descriptors>
|
||||
<descriptor>src/assemble/examples.xml</descriptor>
|
||||
@@ -130,6 +134,7 @@
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-source-plugin</artifactId>
|
||||
<version>2.1.2</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>attach-sources</id>
|
||||
@@ -142,6 +147,7 @@
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-javadoc-plugin</artifactId>
|
||||
<version>2.8</version>
|
||||
<configuration>
|
||||
<encoding>${project.build.sourceEncoding}</encoding>
|
||||
</configuration>
|
||||
@@ -209,7 +215,7 @@
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-gpg-plugin</artifactId>
|
||||
<version>1.0</version>
|
||||
<version>1.3</version>
|
||||
<configuration>
|
||||
<passphrase>${gpg.passphrase}</passphrase>
|
||||
</configuration>
|
||||
|
||||
@@ -73,8 +73,8 @@ public class Event<T extends Throwable> {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return whether this event is in a 'set' state. An event is set by a call to {@link set()} or {@link
|
||||
* deliverError}
|
||||
* @return whether this event is in a 'set' state. An event is set by a call to {@link #set} or {@link
|
||||
* #deliverError}
|
||||
*/
|
||||
public boolean isSet() {
|
||||
return promise.isDelivered();
|
||||
|
||||
@@ -91,7 +91,7 @@ public class DefaultConfig
|
||||
|
||||
private final Logger log = LoggerFactory.getLogger(getClass());
|
||||
|
||||
private static final String VERSION = "SSHJ_0_3";
|
||||
private static final String VERSION = "SSHJ_0_4_1";
|
||||
|
||||
public DefaultConfig() {
|
||||
setVersion(VERSION);
|
||||
|
||||
@@ -50,6 +50,7 @@ import net.schmizz.sshj.userauth.keyprovider.FileKeyProvider;
|
||||
import net.schmizz.sshj.userauth.keyprovider.KeyPairWrapper;
|
||||
import net.schmizz.sshj.userauth.keyprovider.KeyProvider;
|
||||
import net.schmizz.sshj.userauth.keyprovider.KeyProviderUtil;
|
||||
import net.schmizz.sshj.userauth.keyprovider.OpenSSHKeyFile;
|
||||
import net.schmizz.sshj.userauth.method.AuthKeyboardInteractive;
|
||||
import net.schmizz.sshj.userauth.method.AuthMethod;
|
||||
import net.schmizz.sshj.userauth.method.AuthPassword;
|
||||
@@ -64,6 +65,8 @@ import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.Reader;
|
||||
import java.io.StringReader;
|
||||
import java.net.SocketAddress;
|
||||
import java.security.KeyPair;
|
||||
import java.security.PublicKey;
|
||||
@@ -501,6 +504,33 @@ public class SSHClient
|
||||
return loadKeys(location, passphrase.toCharArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link KeyProvider} instance from passed strings. Currently only PKCS8 format
|
||||
* private key files are supported (OpenSSH uses this format).
|
||||
* <p/>
|
||||
*
|
||||
* @param privateKey the private key as a string
|
||||
* @param publicKey the public key as a string if it's not included with the private key
|
||||
* @param passwordFinder the {@link PasswordFinder} that can supply the passphrase for decryption (may be {@code
|
||||
* null} in case keyfile is not encrypted)
|
||||
*
|
||||
* @return the key provider ready for use in authentication
|
||||
*
|
||||
* @throws SSHException if there was no suitable key provider available for the file format; typically because
|
||||
* BouncyCastle is not in the classpath
|
||||
* @throws IOException if the key file format is not known, etc.
|
||||
*/
|
||||
public KeyProvider loadKeys(String privateKey, String publicKey, PasswordFinder passwordFinder)
|
||||
throws IOException {
|
||||
final FileKeyProvider.Format format = KeyProviderUtil.detectKeyFileFormat(privateKey, publicKey != null);
|
||||
final FileKeyProvider fkp = Factory.Named.Util.create(trans.getConfig().getFileKeyProviderFactories(), format
|
||||
.toString());
|
||||
if (fkp == null)
|
||||
throw new SSHException("No provider available for " + format + " key file");
|
||||
fkp.init(privateKey, publicKey, passwordFinder);
|
||||
return fkp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts loading the user's {@code known_hosts} file from the default locations, i.e. {@code ~/.ssh/known_hosts}
|
||||
* and {@code ~/.ssh/known_hosts2} on most platforms. Adds the resulting {@link OpenSSHKnownHosts} object as a host
|
||||
|
||||
@@ -341,41 +341,13 @@ public class Buffer<T extends Buffer<T>> {
|
||||
*/
|
||||
public BigInteger readMPInt()
|
||||
throws BufferException {
|
||||
return new BigInteger(readMPIntAsBytes());
|
||||
return new BigInteger(readBytes());
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes an SSH multiple-precision integer from a {@code BigInteger}
|
||||
*
|
||||
* @param bi {@code BigInteger} to write
|
||||
*
|
||||
* @return this
|
||||
*/
|
||||
public T putMPInt(BigInteger bi) {
|
||||
return putMPInt(bi.toByteArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes an SSH multiple-precision integer from a Java byte-array
|
||||
*
|
||||
* @param foo byte-array
|
||||
*
|
||||
* @return this
|
||||
*/
|
||||
public T putMPInt(byte[] foo) {
|
||||
int i = foo.length;
|
||||
if ((foo[0] & 0x80) != 0) {
|
||||
i++;
|
||||
putUInt32(i);
|
||||
putByte((byte) 0);
|
||||
} else
|
||||
putUInt32(i);
|
||||
return putRawBytes(foo);
|
||||
}
|
||||
|
||||
public byte[] readMPIntAsBytes()
|
||||
throws BufferException {
|
||||
return readBytes();
|
||||
final byte[] asBytes = bi.toByteArray();
|
||||
putUInt32(asBytes.length);
|
||||
return putRawBytes(asBytes);
|
||||
}
|
||||
|
||||
public long readUInt64()
|
||||
|
||||
@@ -42,18 +42,6 @@ public class ByteArrayUtils {
|
||||
|
||||
final static char[] digits = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
|
||||
|
||||
/**
|
||||
* Check whether two byte arrays are the equal.
|
||||
*
|
||||
* @param a1
|
||||
* @param a2
|
||||
*
|
||||
* @return <code>true</code> or <code>false</code>
|
||||
*/
|
||||
public static boolean equals(byte[] a1, byte[] a2) {
|
||||
return (a1.length != a2.length && equals(a1, 0, a2, 0, a1.length));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether some part or whole of two byte arrays is equal, for <code>length</code> bytes starting at some
|
||||
* offset.
|
||||
@@ -75,17 +63,6 @@ public class ByteArrayUtils {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a hexadecimal representation of <code>array</code>, with each octet separated by a space.
|
||||
*
|
||||
* @param array
|
||||
*
|
||||
* @return hex string, each octet delimited by a space
|
||||
*/
|
||||
public static String printHex(byte[] array) {
|
||||
return printHex(array, 0, array.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a hexadecimal representation of a byte array starting at <code>offset</code> index for <code>len</code>
|
||||
* bytes, with each octet separated by a space.
|
||||
@@ -139,8 +116,4 @@ public class ByteArrayUtils {
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public static byte[] copyOf(byte[] array) {
|
||||
return Arrays.copyOf(array, array.length);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -90,9 +90,9 @@ public abstract class AbstractSignature
|
||||
| sig[i++] & 0x000000ff;
|
||||
i += j;
|
||||
j = sig[i++] << 24 & 0xff000000
|
||||
| sig[i++] << 16 & 0x00ff0000
|
||||
| sig[i++] << 8 & 0x0000ff00
|
||||
| sig[i++] & 0x000000ff;
|
||||
| sig[i++] << 16 & 0x00ff0000
|
||||
| sig[i++] << 8 & 0x0000ff00
|
||||
| sig[i++] & 0x000000ff;
|
||||
byte[] newSig = new byte[j];
|
||||
System.arraycopy(sig, i, newSig, 0, j);
|
||||
sig = newSig;
|
||||
|
||||
@@ -63,7 +63,7 @@ final class Encoder
|
||||
private SSHPacket checkHeaderSpace(SSHPacket buffer) {
|
||||
if (buffer.rpos() < 5) {
|
||||
log.warn("Performance cost: when sending a packet, ensure that "
|
||||
+ "5 bytes are available in front of the buffer");
|
||||
+ "5 bytes are available in front of the buffer");
|
||||
SSHPacket nb = new SSHPacket(buffer.available() + 5);
|
||||
nb.rpos(5);
|
||||
nb.wpos(5);
|
||||
@@ -96,8 +96,6 @@ final class Encoder
|
||||
long encode(SSHPacket buffer) {
|
||||
encodeLock.lock();
|
||||
try {
|
||||
buffer = checkHeaderSpace(buffer);
|
||||
|
||||
if (log.isTraceEnabled())
|
||||
log.trace("Encoding packet #{}: {}", seq, buffer.printHex());
|
||||
|
||||
|
||||
@@ -35,13 +35,12 @@
|
||||
*/
|
||||
package net.schmizz.sshj.transport;
|
||||
|
||||
import net.schmizz.concurrent.Event;
|
||||
import net.schmizz.concurrent.ErrorDeliveryUtil;
|
||||
import net.schmizz.concurrent.Event;
|
||||
import net.schmizz.sshj.common.Buffer;
|
||||
import net.schmizz.sshj.common.DisconnectReason;
|
||||
import net.schmizz.sshj.common.ErrorNotifiable;
|
||||
import net.schmizz.sshj.common.Factory;
|
||||
import net.schmizz.sshj.common.IOUtils;
|
||||
import net.schmizz.sshj.common.KeyType;
|
||||
import net.schmizz.sshj.common.Message;
|
||||
import net.schmizz.sshj.common.SSHException;
|
||||
@@ -57,6 +56,7 @@ import net.schmizz.sshj.transport.verification.HostKeyVerifier;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.PublicKey;
|
||||
import java.util.Arrays;
|
||||
@@ -234,17 +234,16 @@ final class KeyExchanger
|
||||
|
||||
private void gotKexInit(SSHPacket buf)
|
||||
throws TransportException {
|
||||
Proposal serverProposal = new Proposal(buf);
|
||||
buf.rpos(buf.rpos() - 1);
|
||||
final Proposal serverProposal = new Proposal(buf);
|
||||
negotiatedAlgs = clientProposal.negotiate(serverProposal);
|
||||
log.debug("Negotiated algorithms: {}", negotiatedAlgs);
|
||||
kex = Factory.Named.Util.create(transport.getConfig().getKeyExchangeFactories(), negotiatedAlgs
|
||||
.getKeyExchangeAlgorithm());
|
||||
kex = Factory.Named.Util.create(transport.getConfig().getKeyExchangeFactories(),
|
||||
negotiatedAlgs.getKeyExchangeAlgorithm());
|
||||
try {
|
||||
kex.init(transport,
|
||||
transport.getServerID().getBytes(IOUtils.UTF8),
|
||||
transport.getClientID().getBytes(IOUtils.UTF8),
|
||||
buf.getCompactData(),
|
||||
clientProposal.getPacket().getCompactData());
|
||||
transport.getServerID(), transport.getClientID(),
|
||||
serverProposal.getPacket().getCompactData(), clientProposal.getPacket().getCompactData());
|
||||
} catch (GeneralSecurityException e) {
|
||||
throw new TransportException(DisconnectReason.KEY_EXCHANGE_FAILED, e);
|
||||
}
|
||||
@@ -262,7 +261,7 @@ final class KeyExchanger
|
||||
*
|
||||
* @return the resized key
|
||||
*/
|
||||
private static byte[] resizedKey(byte[] E, int blockSize, Digest hash, byte[] K, byte[] H) {
|
||||
private static byte[] resizedKey(byte[] E, int blockSize, Digest hash, BigInteger K, byte[] H) {
|
||||
while (blockSize > E.length) {
|
||||
Buffer.PlainBuffer buffer = new Buffer.PlainBuffer().putMPInt(K).putRawBytes(H).putRawBytes(E);
|
||||
hash.update(buffer.array(), 0, buffer.available());
|
||||
@@ -280,13 +279,15 @@ final class KeyExchanger
|
||||
private void gotNewKeys() {
|
||||
final Digest hash = kex.getHash();
|
||||
|
||||
final byte[] H = kex.getH();
|
||||
|
||||
if (sessionID == null)
|
||||
// session id is 'H' from the first key exchange and does not change thereafter
|
||||
sessionID = Arrays.copyOf(kex.getH(), kex.getH().length);
|
||||
sessionID = H;
|
||||
|
||||
final Buffer.PlainBuffer hashInput = new Buffer.PlainBuffer()
|
||||
.putMPInt(kex.getK())
|
||||
.putRawBytes(kex.getH())
|
||||
.putRawBytes(H)
|
||||
.putByte((byte) 0) // <placeholder>
|
||||
.putRawBytes(sessionID);
|
||||
final int pos = hashInput.available() - sessionID.length - 1; // Position of <placeholder>
|
||||
@@ -360,7 +361,6 @@ final class KeyExchanger
|
||||
* having sent the packet ourselves (would cause gotKexInit() to fail)
|
||||
*/
|
||||
kexInitSent.await(transport.getTimeout(), TimeUnit.SECONDS);
|
||||
buf.rpos(buf.rpos() - 1);
|
||||
gotKexInit(buf);
|
||||
expected = Expected.FOLLOWUP;
|
||||
break;
|
||||
|
||||
@@ -86,7 +86,8 @@ class Proposal {
|
||||
packet.putUInt32(0); // "Reserved" for future by spec
|
||||
}
|
||||
|
||||
public Proposal(SSHPacket packet) throws TransportException {
|
||||
public Proposal(SSHPacket packet)
|
||||
throws TransportException {
|
||||
this.packet = packet;
|
||||
final int savedPos = packet.rpos();
|
||||
packet.rpos(packet.rpos() + 17); // Skip message ID & cookie
|
||||
@@ -144,14 +145,14 @@ class Proposal {
|
||||
public NegotiatedAlgorithms negotiate(Proposal other)
|
||||
throws TransportException {
|
||||
return new NegotiatedAlgorithms(
|
||||
firstMatch(this.getKeyExchangeAlgorithms(), other.getKeyExchangeAlgorithms()), //
|
||||
firstMatch(this.getSignatureAlgorithms(), other.getSignatureAlgorithms()), //
|
||||
firstMatch(this.getClient2ServerCipherAlgorithms(), other.getClient2ServerCipherAlgorithms()), //
|
||||
firstMatch(this.getServer2ClientCipherAlgorithms(), other.getServer2ClientCipherAlgorithms()), //
|
||||
firstMatch(this.getClient2ServerMACAlgorithms(), other.getClient2ServerMACAlgorithms()), //
|
||||
firstMatch(this.getServer2ClientMACAlgorithms(), other.getServer2ClientMACAlgorithms()), //
|
||||
firstMatch(this.getClient2ServerCompressionAlgorithms(), other.getClient2ServerCompressionAlgorithms()), //
|
||||
firstMatch(this.getServer2ClientCompressionAlgorithms(), other.getServer2ClientCompressionAlgorithms()) //
|
||||
firstMatch(this.getKeyExchangeAlgorithms(), other.getKeyExchangeAlgorithms()),
|
||||
firstMatch(this.getSignatureAlgorithms(), other.getSignatureAlgorithms()),
|
||||
firstMatch(this.getClient2ServerCipherAlgorithms(), other.getClient2ServerCipherAlgorithms()),
|
||||
firstMatch(this.getServer2ClientCipherAlgorithms(), other.getServer2ClientCipherAlgorithms()),
|
||||
firstMatch(this.getClient2ServerMACAlgorithms(), other.getClient2ServerMACAlgorithms()),
|
||||
firstMatch(this.getServer2ClientMACAlgorithms(), other.getServer2ClientMACAlgorithms()),
|
||||
firstMatch(this.getClient2ServerCompressionAlgorithms(), other.getClient2ServerCompressionAlgorithms()),
|
||||
firstMatch(this.getServer2ClientCompressionAlgorithms(), other.getServer2ClientCompressionAlgorithms())
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,6 @@
|
||||
package net.schmizz.sshj.transport.kex;
|
||||
|
||||
import net.schmizz.sshj.common.Buffer;
|
||||
import net.schmizz.sshj.common.ByteArrayUtils;
|
||||
import net.schmizz.sshj.common.DisconnectReason;
|
||||
import net.schmizz.sshj.common.Factory;
|
||||
import net.schmizz.sshj.common.KeyType;
|
||||
@@ -53,6 +52,7 @@ import org.slf4j.LoggerFactory;
|
||||
import java.math.BigInteger;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.PublicKey;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Base class for DHG key exchange algorithms. Implementations will only have to configure the required data on the
|
||||
@@ -65,32 +65,30 @@ public abstract class AbstractDHG
|
||||
|
||||
private Transport trans;
|
||||
|
||||
private final Digest sha = new SHA1();
|
||||
private final Digest sha1 = new SHA1();
|
||||
private final DH dh = new DH();
|
||||
|
||||
private byte[] V_S;
|
||||
private byte[] V_C;
|
||||
private String V_S;
|
||||
private String V_C;
|
||||
private byte[] I_S;
|
||||
private byte[] I_C;
|
||||
|
||||
private byte[] e;
|
||||
private byte[] K;
|
||||
private byte[] H;
|
||||
private PublicKey hostKey;
|
||||
|
||||
@Override
|
||||
public byte[] getH() {
|
||||
return ByteArrayUtils.copyOf(H);
|
||||
return Arrays.copyOf(H, H.length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getK() {
|
||||
return ByteArrayUtils.copyOf(K);
|
||||
public BigInteger getK() {
|
||||
return dh.getK();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Digest getHash() {
|
||||
return sha;
|
||||
return sha1;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -99,19 +97,18 @@ public abstract class AbstractDHG
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(Transport trans, byte[] V_S, byte[] V_C, byte[] I_S, byte[] I_C)
|
||||
public void init(Transport trans, String V_S, String V_C, byte[] I_S, byte[] I_C)
|
||||
throws GeneralSecurityException, TransportException {
|
||||
this.trans = trans;
|
||||
this.V_S = ByteArrayUtils.copyOf(V_S);
|
||||
this.V_C = ByteArrayUtils.copyOf(V_C);
|
||||
this.I_S = ByteArrayUtils.copyOf(I_S);
|
||||
this.I_C = ByteArrayUtils.copyOf(I_C);
|
||||
sha.init();
|
||||
this.V_S = V_S;
|
||||
this.V_C = V_C;
|
||||
this.I_S = Arrays.copyOf(I_S, I_S.length);
|
||||
this.I_C = Arrays.copyOf(I_C, I_C.length);
|
||||
sha1.init();
|
||||
initDH(dh);
|
||||
e = dh.getE();
|
||||
|
||||
log.info("Sending SSH_MSG_KEXDH_INIT");
|
||||
trans.write(new SSHPacket(Message.KEXDH_INIT).putMPInt(e));
|
||||
trans.write(new SSHPacket(Message.KEXDH_INIT).putMPInt(dh.getE()));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -122,19 +119,18 @@ public abstract class AbstractDHG
|
||||
|
||||
log.info("Received SSH_MSG_KEXDH_REPLY");
|
||||
final byte[] K_S;
|
||||
final byte[] f;
|
||||
final BigInteger f;
|
||||
final byte[] sig; // signature sent by server
|
||||
try {
|
||||
K_S = packet.readBytes();
|
||||
f = packet.readMPIntAsBytes();
|
||||
f = packet.readMPInt();
|
||||
sig = packet.readBytes();
|
||||
hostKey = new Buffer.PlainBuffer(K_S).readPublicKey();
|
||||
} catch (Buffer.BufferException be) {
|
||||
throw new TransportException(be);
|
||||
}
|
||||
|
||||
dh.setF(new BigInteger(f));
|
||||
K = dh.getK();
|
||||
dh.computeK(f);
|
||||
|
||||
final Buffer.PlainBuffer buf = new Buffer.PlainBuffer()
|
||||
.putString(V_C)
|
||||
@@ -142,11 +138,11 @@ public abstract class AbstractDHG
|
||||
.putString(I_C)
|
||||
.putString(I_S)
|
||||
.putString(K_S)
|
||||
.putMPInt(e)
|
||||
.putMPInt(dh.getE())
|
||||
.putMPInt(f)
|
||||
.putMPInt(K);
|
||||
sha.update(buf.array(), 0, buf.available());
|
||||
H = sha.digest();
|
||||
.putMPInt(dh.getK());
|
||||
sha1.update(buf.array(), buf.rpos(), buf.available());
|
||||
H = sha1.digest();
|
||||
|
||||
Signature signature = Factory.Named.Util.create(trans.getConfig().getSignatureFactories(),
|
||||
KeyType.fromKey(hostKey).toString());
|
||||
@@ -158,6 +154,7 @@ public abstract class AbstractDHG
|
||||
return true;
|
||||
}
|
||||
|
||||
protected abstract void initDH(DH dh);
|
||||
protected abstract void initDH(DH dh)
|
||||
throws GeneralSecurityException;
|
||||
|
||||
}
|
||||
|
||||
@@ -54,7 +54,6 @@ public class DH {
|
||||
private BigInteger p;
|
||||
private BigInteger g;
|
||||
private BigInteger e; // my public key
|
||||
private BigInteger f; // your public key
|
||||
private BigInteger K; // shared secret key
|
||||
private final KeyPairGenerator generator;
|
||||
private final KeyAgreement agreement;
|
||||
@@ -68,39 +67,30 @@ public class DH {
|
||||
}
|
||||
}
|
||||
|
||||
public void setF(BigInteger f) {
|
||||
this.f = f;
|
||||
}
|
||||
|
||||
public void setG(BigInteger g) {
|
||||
this.g = g;
|
||||
}
|
||||
|
||||
public void setP(BigInteger p) {
|
||||
public void init(BigInteger p, BigInteger g)
|
||||
throws GeneralSecurityException {
|
||||
this.p = p;
|
||||
this.g = g;
|
||||
generator.initialize(new DHParameterSpec(p, g));
|
||||
final KeyPair kp = generator.generateKeyPair();
|
||||
agreement.init(kp.getPrivate());
|
||||
e = ((javax.crypto.interfaces.DHPublicKey) kp.getPublic()).getY();
|
||||
}
|
||||
|
||||
public byte[] getE()
|
||||
public void computeK(BigInteger f)
|
||||
throws GeneralSecurityException {
|
||||
if (e == null) {
|
||||
generator.initialize(new DHParameterSpec(p, g));
|
||||
final KeyPair kp = generator.generateKeyPair();
|
||||
agreement.init(kp.getPrivate());
|
||||
e = ((javax.crypto.interfaces.DHPublicKey) kp.getPublic()).getY();
|
||||
}
|
||||
return e.toByteArray();
|
||||
final KeyFactory keyFactory = SecurityUtils.getKeyFactory("DH");
|
||||
final PublicKey yourPubKey = keyFactory.generatePublic(new DHPublicKeySpec(f, p, g));
|
||||
agreement.doPhase(yourPubKey, true);
|
||||
K = new BigInteger(1, agreement.generateSecret());
|
||||
}
|
||||
|
||||
public byte[] getK()
|
||||
throws GeneralSecurityException {
|
||||
if (K == null) {
|
||||
final KeyFactory keyFactory = SecurityUtils.getKeyFactory("DH");
|
||||
final DHPublicKeySpec keySpec = new DHPublicKeySpec(f, p, g);
|
||||
final PublicKey yourPubKey = keyFactory.generatePublic(keySpec);
|
||||
agreement.doPhase(yourPubKey, true);
|
||||
K = new BigInteger(agreement.generateSecret());
|
||||
}
|
||||
return K.toByteArray();
|
||||
public BigInteger getE() {
|
||||
return e;
|
||||
}
|
||||
|
||||
public BigInteger getK() {
|
||||
return K;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -35,6 +35,8 @@
|
||||
*/
|
||||
package net.schmizz.sshj.transport.kex;
|
||||
|
||||
import java.security.GeneralSecurityException;
|
||||
|
||||
/**
|
||||
* Diffie-Hellman key exchange with SHA-1 and Oakley Group 2 [RFC2409] (1024-bit MODP Group).
|
||||
*
|
||||
@@ -60,9 +62,9 @@ public class DHG1
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initDH(DH dh) {
|
||||
dh.setG(DHGroupData.G);
|
||||
dh.setP(DHGroupData.P1);
|
||||
protected void initDH(DH dh)
|
||||
throws GeneralSecurityException {
|
||||
dh.init(DHGroupData.P1, DHGroupData.G);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -35,6 +35,8 @@
|
||||
*/
|
||||
package net.schmizz.sshj.transport.kex;
|
||||
|
||||
import java.security.GeneralSecurityException;
|
||||
|
||||
/**
|
||||
* Diffie-Hellman key exchange with SHA-1 and Oakley Group 14 [RFC3526] (2048-bit MODP Group).
|
||||
* <p/>
|
||||
@@ -61,9 +63,8 @@ public class DHG14
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initDH(DH dh) {
|
||||
dh.setG(DHGroupData.G);
|
||||
dh.setP(DHGroupData.P14);
|
||||
protected void initDH(DH dh) throws GeneralSecurityException {
|
||||
dh.init(DHGroupData.P14, DHGroupData.G);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ import net.schmizz.sshj.transport.Transport;
|
||||
import net.schmizz.sshj.transport.TransportException;
|
||||
import net.schmizz.sshj.transport.digest.Digest;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.PublicKey;
|
||||
|
||||
@@ -59,14 +60,14 @@ public interface KeyExchange {
|
||||
* @throws GeneralSecurityException
|
||||
* @throws TransportException if there is an error sending a packet
|
||||
*/
|
||||
void init(Transport trans, byte[] V_S, byte[] V_C, byte[] I_S, byte[] I_C)
|
||||
void init(Transport trans, String V_S, String V_C, byte[] I_S, byte[] I_C)
|
||||
throws GeneralSecurityException, TransportException;
|
||||
|
||||
/** @return the computed H parameter */
|
||||
byte[] getH();
|
||||
|
||||
/** @return the computed K parameter */
|
||||
byte[] getK();
|
||||
BigInteger getK();
|
||||
|
||||
/**
|
||||
* The message digest used by this key exchange algorithm.
|
||||
|
||||
@@ -33,4 +33,7 @@ public interface FileKeyProvider
|
||||
|
||||
void init(File location, PasswordFinder pwdf);
|
||||
|
||||
void init(String privateKey, String publicKey);
|
||||
|
||||
void init(String privateKey, String publicKey, PasswordFinder pwdf);
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@ import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileReader;
|
||||
import java.io.IOException;
|
||||
import java.io.Reader;
|
||||
import java.io.StringReader;
|
||||
|
||||
public class KeyProviderUtil {
|
||||
|
||||
@@ -37,13 +39,50 @@ public class KeyProviderUtil {
|
||||
*/
|
||||
public static FileKeyProvider.Format detectKeyFileFormat(File location)
|
||||
throws IOException {
|
||||
BufferedReader br = new BufferedReader(new FileReader(location));
|
||||
return detectKeyFileFormat(new FileReader(location),
|
||||
new File(location + ".pub").exists());
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to detect how a key file is encoded.
|
||||
* <p/>
|
||||
* Return values are consistent with the {@code NamedFactory} implementations in the {@code keyprovider} package.
|
||||
*
|
||||
* @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
|
||||
*/
|
||||
public static FileKeyProvider.Format detectKeyFileFormat(String privateKey,
|
||||
boolean separatePubKey)
|
||||
throws IOException {
|
||||
return detectKeyFileFormat(new StringReader(privateKey), separatePubKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to detect how a key file is encoded.
|
||||
* <p/>
|
||||
* Return values are consistent with the {@code NamedFactory} implementations in the {@code keyprovider} package.
|
||||
*
|
||||
* @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
|
||||
*/
|
||||
private static FileKeyProvider.Format detectKeyFileFormat(Reader privateKey,
|
||||
boolean separatePubKey)
|
||||
throws IOException {
|
||||
BufferedReader br = new BufferedReader(privateKey);
|
||||
String firstLine = br.readLine();
|
||||
IOUtils.closeQuietly(br);
|
||||
if (firstLine == null)
|
||||
throw new IOException("Empty file");
|
||||
if (firstLine.startsWith("-----BEGIN") && firstLine.endsWith("PRIVATE KEY-----"))
|
||||
if (new File(location + ".pub").exists())
|
||||
if (separatePubKey)
|
||||
// Can delay asking for password since have unencrypted pubkey
|
||||
return FileKeyProvider.Format.OpenSSH;
|
||||
else
|
||||
@@ -54,5 +93,4 @@ public class KeyProviderUtil {
|
||||
*/
|
||||
return FileKeyProvider.Format.Unknown;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -23,8 +23,11 @@ import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileReader;
|
||||
import java.io.IOException;
|
||||
import java.io.Reader;
|
||||
import java.io.StringReader;
|
||||
import java.security.PublicKey;
|
||||
|
||||
|
||||
/**
|
||||
* Represents an OpenSSH identity that consists of a PKCS8-encoded private key file and an unencrypted public key file
|
||||
* of the same name with the {@code ".pub"} extension. This allows to delay requesting of the passphrase until the
|
||||
@@ -62,18 +65,7 @@ public class OpenSSHKeyFile
|
||||
final File f = new File(location + ".pub");
|
||||
if (f.exists())
|
||||
try {
|
||||
final BufferedReader br = new BufferedReader(new FileReader(f));
|
||||
try {
|
||||
final String keydata = br.readLine();
|
||||
if (keydata != null) {
|
||||
String[] parts = keydata.split(" ");
|
||||
assert parts.length >= 2;
|
||||
type = KeyType.fromString(parts[0]);
|
||||
pubKey = new Buffer.PlainBuffer(Base64.decode(parts[1])).readPublicKey();
|
||||
}
|
||||
} finally {
|
||||
br.close();
|
||||
}
|
||||
initPubKey(new FileReader(f));
|
||||
} catch (IOException e) {
|
||||
// let super provide both public & private key
|
||||
log.warn("Error reading public key file: {}", e.toString());
|
||||
@@ -81,4 +73,36 @@ public class OpenSSHKeyFile
|
||||
super.init(location);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(String privateKey, String publicKey) {
|
||||
if (publicKey != null) {
|
||||
initPubKey(new StringReader(publicKey));
|
||||
}
|
||||
super.init(privateKey, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read and store the separate public key provided alongside the private key
|
||||
*
|
||||
* @param publicKey Public key accessible through a {@code Reader}
|
||||
*/
|
||||
private void initPubKey(Reader publicKey) {
|
||||
try {
|
||||
final BufferedReader br = new BufferedReader(publicKey);
|
||||
try {
|
||||
final String keydata = br.readLine();
|
||||
if (keydata != null) {
|
||||
String[] parts = keydata.split(" ");
|
||||
assert parts.length >= 2;
|
||||
type = KeyType.fromString(parts[0]);
|
||||
pubKey = new Buffer.PlainBuffer(Base64.decode(parts[1])).readPublicKey();
|
||||
}
|
||||
} finally {
|
||||
br.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// let super provide both public & private key
|
||||
log.warn("Error reading public key: {}", e.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,9 @@ import net.schmizz.sshj.common.KeyType;
|
||||
import net.schmizz.sshj.userauth.password.PasswordFinder;
|
||||
import net.schmizz.sshj.userauth.password.PasswordUtils;
|
||||
import net.schmizz.sshj.userauth.password.PrivateKeyFileResource;
|
||||
import net.schmizz.sshj.userauth.password.PrivateKeyStringResource;
|
||||
import net.schmizz.sshj.userauth.password.Resource;
|
||||
|
||||
import org.bouncycastle.openssl.EncryptionException;
|
||||
import org.bouncycastle.openssl.PEMReader;
|
||||
import org.slf4j.Logger;
|
||||
@@ -29,6 +32,8 @@ import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.Reader;
|
||||
import java.io.StringReader;
|
||||
import java.security.KeyPair;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
@@ -52,7 +57,8 @@ public class PKCS8KeyFile
|
||||
|
||||
protected final Logger log = LoggerFactory.getLogger(getClass());
|
||||
protected PasswordFinder pwdf;
|
||||
protected PrivateKeyFileResource resource;
|
||||
@SuppressWarnings("unchecked")
|
||||
protected Resource resource;
|
||||
protected KeyPair kp;
|
||||
|
||||
protected KeyType type;
|
||||
@@ -89,6 +95,19 @@ public class PKCS8KeyFile
|
||||
this.pwdf = pwdf;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(String privateKey, String publicKey) {
|
||||
assert privateKey != null;
|
||||
assert publicKey == null;
|
||||
resource = new PrivateKeyStringResource(privateKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(String privateKey, String publicKey, PasswordFinder pwdf) {
|
||||
init(privateKey, publicKey);
|
||||
this.pwdf = pwdf;
|
||||
}
|
||||
|
||||
protected org.bouncycastle.openssl.PasswordFinder makeBouncyPasswordFinder() {
|
||||
if (pwdf == null)
|
||||
return null;
|
||||
@@ -111,7 +130,7 @@ public class PKCS8KeyFile
|
||||
for (; ;) {
|
||||
// while the PasswordFinder tells us we should retry
|
||||
try {
|
||||
r = new PEMReader(new InputStreamReader(new FileInputStream(resource.getDetail())), pFinder);
|
||||
r = new PEMReader(resource.getReader(), pFinder);
|
||||
o = r.readObject();
|
||||
} catch (EncryptionException e) {
|
||||
if (pwdf.shouldRetry(resource))
|
||||
|
||||
@@ -15,6 +15,10 @@
|
||||
*/
|
||||
package net.schmizz.sshj.userauth.password;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.Reader;
|
||||
import java.io.StringReader;
|
||||
|
||||
public class AccountResource
|
||||
extends Resource<String> {
|
||||
|
||||
@@ -22,4 +26,8 @@ public class AccountResource
|
||||
super(user + "@" + host);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Reader getReader() throws IOException {
|
||||
return new StringReader(getDetail());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,10 @@
|
||||
package net.schmizz.sshj.userauth.password;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.Reader;
|
||||
|
||||
public class PrivateKeyFileResource
|
||||
extends Resource<File> {
|
||||
@@ -24,4 +28,9 @@ public class PrivateKeyFileResource
|
||||
super(privateKeyFile);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Reader getReader()
|
||||
throws IOException {
|
||||
return new InputStreamReader(new FileInputStream(getDetail()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2010, 2011 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.password;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.Reader;
|
||||
import java.io.StringReader;
|
||||
|
||||
public class PrivateKeyStringResource extends Resource<String> {
|
||||
|
||||
public PrivateKeyStringResource(String string) {
|
||||
super(string);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Reader getReader() throws IOException {
|
||||
return new StringReader(getDetail());
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,9 @@
|
||||
*/
|
||||
package net.schmizz.sshj.userauth.password;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.Reader;
|
||||
|
||||
/** A password-protected resource */
|
||||
public abstract class Resource<H> {
|
||||
|
||||
@@ -28,6 +31,9 @@ public abstract class Resource<H> {
|
||||
return detail;
|
||||
}
|
||||
|
||||
public abstract Reader getReader()
|
||||
throws IOException;
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o)
|
||||
|
||||
@@ -45,7 +45,6 @@ public interface LocalDestFile {
|
||||
/**
|
||||
* Set the permissions for the underlying file.
|
||||
*
|
||||
* @param f the file
|
||||
* @param perms permissions e.g. 0644
|
||||
*
|
||||
* @throws IOException
|
||||
@@ -66,7 +65,6 @@ public interface LocalDestFile {
|
||||
/**
|
||||
* Set the last modified time for the underlying file.
|
||||
*
|
||||
* @param f the file
|
||||
* @param t time in seconds since Unix epoch
|
||||
*
|
||||
* @throws IOException
|
||||
|
||||
41
src/test/java/net/schmizz/sshj/LoadsOfConnects.java
Normal file
41
src/test/java/net/schmizz/sshj/LoadsOfConnects.java
Normal file
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright 2010, 2011 sshj contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package net.schmizz.sshj;
|
||||
|
||||
import net.schmizz.sshj.util.BasicFixture;
|
||||
import org.junit.Test;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class LoadsOfConnects {
|
||||
|
||||
protected final Logger log = LoggerFactory.getLogger(getClass());
|
||||
|
||||
private final BasicFixture fixture = new BasicFixture();
|
||||
|
||||
@Test
|
||||
public void loadsOfConnects()
|
||||
throws IOException, InterruptedException {
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
System.out.println("Try " + i);
|
||||
fixture.init(false);
|
||||
fixture.done();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -31,6 +31,7 @@ import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Scanner;
|
||||
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
@@ -113,6 +114,19 @@ public class OpenSSHKeyFileTest {
|
||||
assertEquals(KeyUtil.newDSAPrivateKey(x, p, q, g), dsa.getPrivate());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fromString()
|
||||
throws IOException, GeneralSecurityException {
|
||||
FileKeyProvider dsa = new OpenSSHKeyFile();
|
||||
String privateKey = readFile("src/test/resources/id_dsa");
|
||||
String publicKey = readFile("src/test/resources/id_dsa.pub");
|
||||
dsa.init(privateKey, publicKey,
|
||||
PasswordUtils.createOneOff(correctPassphrase));
|
||||
assertEquals(dsa.getType(), KeyType.DSA);
|
||||
assertEquals(KeyUtil.newDSAPublicKey(y, p, q, g), dsa.getPublic());
|
||||
assertEquals(KeyUtil.newDSAPrivateKey(x, p, q, g), dsa.getPrivate());
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setup()
|
||||
throws UnsupportedEncodingException, GeneralSecurityException {
|
||||
@@ -120,4 +134,19 @@ public class OpenSSHKeyFileTest {
|
||||
throw new AssertionError("bouncy castle needed");
|
||||
}
|
||||
|
||||
private String readFile(String pathname)
|
||||
throws IOException {
|
||||
|
||||
StringBuilder fileContents = new StringBuilder();
|
||||
Scanner scanner = new Scanner(new File(pathname));
|
||||
String lineSeparator = System.getProperty("line.separator");
|
||||
try {
|
||||
while(scanner.hasNextLine()) {
|
||||
fileContents.append(scanner.nextLine() + lineSeparator);
|
||||
}
|
||||
return fileContents.toString();
|
||||
} finally {
|
||||
scanner.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user