mirror of
https://github.com/hierynomus/sshj.git
synced 2025-12-07 07:40:55 +03:00
Add support for Encrypt-then-MAC MAC Algorithms (#450)
This commit is contained in:
@@ -20,6 +20,7 @@ import com.hierynomus.sshj.transport.cipher.BlockCiphers;
|
||||
import com.hierynomus.sshj.transport.cipher.StreamCiphers;
|
||||
import com.hierynomus.sshj.transport.kex.DHGroups;
|
||||
import com.hierynomus.sshj.transport.kex.ExtendedDHGroups;
|
||||
import com.hierynomus.sshj.transport.mac.Macs;
|
||||
import com.hierynomus.sshj.userauth.keyprovider.OpenSSHKeyV1KeyFile;
|
||||
import net.schmizz.keepalive.KeepAliveProvider;
|
||||
import net.schmizz.sshj.common.Factory;
|
||||
@@ -219,12 +220,22 @@ public class DefaultConfig
|
||||
|
||||
protected void initMACFactories() {
|
||||
setMACFactories(
|
||||
new HMACSHA1.Factory(),
|
||||
new HMACSHA196.Factory(),
|
||||
new HMACMD5.Factory(),
|
||||
new HMACMD596.Factory(),
|
||||
new HMACSHA2256.Factory(),
|
||||
new HMACSHA2512.Factory()
|
||||
Macs.HMACSHA1(),
|
||||
Macs.HMACSHA1Etm(),
|
||||
Macs.HMACSHA196(),
|
||||
Macs.HMACSHA196Etm(),
|
||||
Macs.HMACMD5(),
|
||||
Macs.HMACMD5Etm(),
|
||||
Macs.HMACMD596(),
|
||||
Macs.HMACMD596Etm(),
|
||||
Macs.HMACSHA2256(),
|
||||
Macs.HMACSHA2256Etm(),
|
||||
Macs.HMACSHA2512(),
|
||||
Macs.HMACSHA2512Etm(),
|
||||
Macs.HMACRIPEMD160(),
|
||||
Macs.HMACRIPEMD160Etm(),
|
||||
Macs.HMACRIPEMD16096(),
|
||||
Macs.HMACRIPEMD160OpenSsh()
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ abstract class Converter {
|
||||
protected int cipherSize = 8;
|
||||
protected long seq = -1;
|
||||
protected boolean authed;
|
||||
protected boolean etm;
|
||||
|
||||
long getSequenceNumber() {
|
||||
return seq;
|
||||
@@ -56,6 +57,7 @@ abstract class Converter {
|
||||
if (compression != null)
|
||||
compression.init(getCompressionType());
|
||||
this.cipherSize = cipher.getIVSize();
|
||||
this.etm = mac.isEtm();
|
||||
}
|
||||
|
||||
void setAuthenticated() {
|
||||
|
||||
@@ -21,7 +21,9 @@ import net.schmizz.sshj.transport.compression.Compression;
|
||||
import net.schmizz.sshj.transport.mac.MAC;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
/** Decodes packets from the SSH binary protocol per the current algorithms. */
|
||||
/**
|
||||
* Decodes packets from the SSH binary protocol per the current algorithms.
|
||||
*/
|
||||
final class Decoder
|
||||
extends Converter {
|
||||
|
||||
@@ -29,16 +31,26 @@ final class Decoder
|
||||
|
||||
private final Logger log;
|
||||
|
||||
/** What we pass decoded packets to */
|
||||
/**
|
||||
* What we pass decoded packets to
|
||||
*/
|
||||
private final SSHPacketHandler packetHandler;
|
||||
/** Buffer where as-yet undecoded data lives */
|
||||
/**
|
||||
* Buffer where as-yet undecoded data lives
|
||||
*/
|
||||
private final SSHPacket inputBuffer = new SSHPacket();
|
||||
/** Used in case compression is active to store the uncompressed data */
|
||||
/**
|
||||
* Used in case compression is active to store the uncompressed data
|
||||
*/
|
||||
private final SSHPacket uncompressBuffer = new SSHPacket();
|
||||
/** MAC result is stored here */
|
||||
/**
|
||||
* MAC result is stored here
|
||||
*/
|
||||
private byte[] macResult;
|
||||
|
||||
/** -1 if packet length not yet been decoded, else the packet length */
|
||||
/**
|
||||
* -1 if packet length not yet been decoded, else the packet length
|
||||
*/
|
||||
private int packetLength = -1;
|
||||
|
||||
/**
|
||||
@@ -60,53 +72,97 @@ final class Decoder
|
||||
*/
|
||||
private int decode()
|
||||
throws SSHException {
|
||||
|
||||
if (etm) {
|
||||
return decodeEtm();
|
||||
} else {
|
||||
return decodeMte();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode an Encrypt-Then-Mac packet.
|
||||
*/
|
||||
private int decodeEtm() throws SSHException {
|
||||
int bytesNeeded;
|
||||
while (true) {
|
||||
if (packetLength == -1) {
|
||||
assert inputBuffer.rpos() == 0 : "buffer cleared";
|
||||
bytesNeeded = 4 - inputBuffer.available();
|
||||
if (bytesNeeded <= 0) {
|
||||
// In Encrypt-Then-Mac, the packetlength is sent unencrypted.
|
||||
packetLength = inputBuffer.readUInt32AsInt();
|
||||
checkPacketLength(packetLength);
|
||||
} else {
|
||||
// Needs more data
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
assert inputBuffer.rpos() == 4 : "packet length read";
|
||||
bytesNeeded = packetLength + mac.getBlockSize() - inputBuffer.available();
|
||||
if (bytesNeeded <= 0) {
|
||||
seq = seq + 1 & 0xffffffffL;
|
||||
checkMAC(inputBuffer.array());
|
||||
decryptBuffer(4, packetLength);
|
||||
inputBuffer.wpos(packetLength + 4 - inputBuffer.readByte());
|
||||
final SSHPacket plain = usingCompression() ? decompressed() : inputBuffer;
|
||||
if (log.isTraceEnabled()) {
|
||||
log.trace("Received packet #{}: {}", seq, plain.printHex());
|
||||
}
|
||||
packetHandler.handle(plain.readMessageID(), plain); // Process the decoded packet
|
||||
inputBuffer.clear();
|
||||
packetLength = -1;
|
||||
} else {
|
||||
// Needs more data
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return bytesNeeded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode a Mac-Then-Encrypt packet
|
||||
* @return
|
||||
* @throws SSHException
|
||||
*/
|
||||
private int decodeMte() throws SSHException {
|
||||
int need;
|
||||
|
||||
/* Decoding loop */
|
||||
for (; ; )
|
||||
|
||||
if (packetLength == -1) // Waiting for beginning of packet
|
||||
{
|
||||
|
||||
if (packetLength == -1) { // Waiting for beginning of packet
|
||||
assert inputBuffer.rpos() == 0 : "buffer cleared";
|
||||
|
||||
need = cipherSize - inputBuffer.available();
|
||||
if (need <= 0)
|
||||
if (need <= 0) {
|
||||
packetLength = decryptLength();
|
||||
else
|
||||
} else {
|
||||
// Need more data
|
||||
break;
|
||||
|
||||
}
|
||||
} else {
|
||||
|
||||
assert inputBuffer.rpos() == 4 : "packet length read";
|
||||
|
||||
need = packetLength + (mac != null ? mac.getBlockSize() : 0) - inputBuffer.available();
|
||||
if (need <= 0) {
|
||||
|
||||
decryptPayload(inputBuffer.array());
|
||||
|
||||
decryptBuffer(cipherSize, packetLength + 4 - cipherSize); // Decrypt the rest of the payload
|
||||
seq = seq + 1 & 0xffffffffL;
|
||||
|
||||
if (mac != null)
|
||||
if (mac != null) {
|
||||
checkMAC(inputBuffer.array());
|
||||
|
||||
}
|
||||
// Exclude the padding & MAC
|
||||
inputBuffer.wpos(packetLength + 4 - inputBuffer.readByte());
|
||||
|
||||
final SSHPacket plain = usingCompression() ? decompressed() : inputBuffer;
|
||||
|
||||
if (log.isTraceEnabled())
|
||||
if (log.isTraceEnabled()) {
|
||||
log.trace("Received packet #{}: {}", seq, plain.printHex());
|
||||
|
||||
}
|
||||
packetHandler.handle(plain.readMessageID(), plain); // Process the decoded packet
|
||||
|
||||
inputBuffer.clear();
|
||||
packetLength = -1;
|
||||
|
||||
} else
|
||||
} else {
|
||||
// Need more data
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return need;
|
||||
@@ -118,8 +174,9 @@ final class Decoder
|
||||
mac.update(data, 0, packetLength + 4); // packetLength+4 = entire packet w/o mac
|
||||
mac.doFinal(macResult, 0); // compute
|
||||
// Check against the received MAC
|
||||
if (!ByteArrayUtils.equals(macResult, 0, data, packetLength + 4, mac.getBlockSize()))
|
||||
if (!ByteArrayUtils.equals(macResult, 0, data, packetLength + 4, mac.getBlockSize())) {
|
||||
throw new TransportException(DisconnectReason.MAC_ERROR, "MAC Error");
|
||||
}
|
||||
}
|
||||
|
||||
private SSHPacket decompressed()
|
||||
@@ -131,7 +188,7 @@ final class Decoder
|
||||
|
||||
private int decryptLength()
|
||||
throws TransportException {
|
||||
cipher.update(inputBuffer.array(), 0, cipherSize);
|
||||
decryptBuffer(0, cipherSize);
|
||||
|
||||
final int len; // Read packet length
|
||||
try {
|
||||
@@ -140,22 +197,26 @@ final class Decoder
|
||||
throw new TransportException(be);
|
||||
}
|
||||
|
||||
if (isInvalidPacketLength(len)) { // Check packet length validity
|
||||
log.error("Error decoding packet (invalid length) {}", inputBuffer.printHex());
|
||||
throw new TransportException(DisconnectReason.PROTOCOL_ERROR, "invalid packet length: " + len);
|
||||
}
|
||||
checkPacketLength(len);
|
||||
|
||||
return len;
|
||||
}
|
||||
|
||||
private static boolean isInvalidPacketLength(int len) {
|
||||
return len < 5 || len > MAX_PACKET_LEN;
|
||||
private void decryptBuffer(int offset, int length) {
|
||||
cipher.update(inputBuffer.array(), offset, length);
|
||||
}
|
||||
|
||||
private void decryptPayload(final byte[] data) {
|
||||
cipher.update(data, cipherSize, packetLength + 4 - cipherSize);
|
||||
private void checkPacketLength(int len) throws TransportException {
|
||||
if (len < 5 || len > MAX_PACKET_LEN) { // Check packet length validity
|
||||
log.error("Error decoding packet (invalid length) {}", inputBuffer.printHex());
|
||||
throw new TransportException(DisconnectReason.PROTOCOL_ERROR, "invalid packet length: " + len);
|
||||
}
|
||||
}
|
||||
|
||||
// private void decryptPayload(final byte[] data, int offset, int length) {
|
||||
// cipher.update(data, cipherSize, packetLength + 4 - cipherSize);
|
||||
// }
|
||||
|
||||
/**
|
||||
* Adds {@code len} bytes from {@code b} to the decoder buffer. When a packet has been successfully decoded, hooks
|
||||
* in to {@link SSHPacketHandler#handle} of the {@link SSHPacketHandler} this decoder was initialized with.
|
||||
|
||||
@@ -67,36 +67,58 @@ final class Encoder
|
||||
log.trace("Encoding packet #{}: {}", seq + 1, buffer.printHex());
|
||||
}
|
||||
|
||||
if (usingCompression())
|
||||
if (usingCompression()) {
|
||||
compress(buffer);
|
||||
}
|
||||
|
||||
final int payloadSize = buffer.available();
|
||||
int lengthWithoutPadding;
|
||||
if (etm) {
|
||||
// in Encrypt-Then-Mac mode, the length field is not encrypted, so we should keep it out of the
|
||||
// padding length calculation
|
||||
lengthWithoutPadding = 1 + payloadSize; // padLength (1 byte) + payload
|
||||
} else {
|
||||
lengthWithoutPadding = 4 + 1 + payloadSize; // packetLength (4 bytes) + padLength (1 byte) + payload
|
||||
}
|
||||
|
||||
// Compute padding length
|
||||
int padLen = -(payloadSize + 5) & cipherSize - 1;
|
||||
if (padLen < cipherSize)
|
||||
int padLen = cipherSize - (lengthWithoutPadding % cipherSize);
|
||||
if (padLen < 4) {
|
||||
padLen += cipherSize;
|
||||
}
|
||||
|
||||
final int startOfPacket = buffer.rpos() - 5;
|
||||
final int packetLen = payloadSize + 1 + padLen;
|
||||
int packetLen = 1 + payloadSize + padLen; // packetLength = padLen (1 byte) + payload + padding
|
||||
|
||||
if (packetLen < 16) {
|
||||
padLen += cipherSize;
|
||||
packetLen = 1 + payloadSize + padLen;
|
||||
}
|
||||
|
||||
final int endOfPadding = startOfPacket + 4 + packetLen;
|
||||
|
||||
// Put packet header
|
||||
buffer.wpos(startOfPacket);
|
||||
buffer.putUInt32(packetLen);
|
||||
buffer.putByte((byte) padLen);
|
||||
|
||||
// Now wpos will mark end of padding
|
||||
buffer.wpos(startOfPacket + 5 + payloadSize + padLen);
|
||||
buffer.wpos(endOfPadding);
|
||||
|
||||
// Fill padding
|
||||
prng.fill(buffer.array(), buffer.wpos() - padLen, padLen);
|
||||
prng.fill(buffer.array(), endOfPadding - padLen, padLen);
|
||||
|
||||
seq = seq + 1 & 0xffffffffL;
|
||||
|
||||
if (mac != null)
|
||||
putMAC(buffer, startOfPacket, buffer.wpos());
|
||||
|
||||
cipher.update(buffer.array(), startOfPacket, 4 + packetLen);
|
||||
if (etm) {
|
||||
cipher.update(buffer.array(), startOfPacket + 4, packetLen);
|
||||
putMAC(buffer, startOfPacket, endOfPadding);
|
||||
} else {
|
||||
if (mac != null) {
|
||||
putMAC(buffer, startOfPacket, endOfPadding);
|
||||
}
|
||||
|
||||
cipher.update(buffer.array(), startOfPacket, 4 + packetLen);
|
||||
}
|
||||
buffer.rpos(startOfPacket); // Make ready-to-read
|
||||
|
||||
return seq;
|
||||
|
||||
@@ -30,12 +30,18 @@ public class BaseMAC
|
||||
private final int defbsize;
|
||||
private final int bsize;
|
||||
private final byte[] tmp;
|
||||
private final boolean etm;
|
||||
private javax.crypto.Mac mac;
|
||||
|
||||
public BaseMAC(String algorithm, int bsize, int defbsize) {
|
||||
this(algorithm, bsize, defbsize, false);
|
||||
}
|
||||
|
||||
public BaseMAC(String algorithm, int bsize, int defbsize, boolean isEtm) {
|
||||
this.algorithm = algorithm;
|
||||
this.bsize = bsize;
|
||||
this.defbsize = defbsize;
|
||||
this.etm = isEtm;
|
||||
tmp = new byte[defbsize];
|
||||
}
|
||||
|
||||
@@ -112,4 +118,8 @@ public class BaseMAC
|
||||
update(tmp, 0, 4);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEtm() {
|
||||
return etm;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,9 @@
|
||||
*/
|
||||
package net.schmizz.sshj.transport.mac;
|
||||
|
||||
/** Message Authentication Code for use in SSH. It usually wraps a javax.crypto.Mac class. */
|
||||
/**
|
||||
* Message Authentication Code for use in SSH. It usually wraps a javax.crypto.Mac class.
|
||||
*/
|
||||
public interface MAC {
|
||||
|
||||
byte[] doFinal();
|
||||
@@ -33,4 +35,40 @@ public interface MAC {
|
||||
void update(byte[] foo, int start, int len);
|
||||
|
||||
void update(long foo);
|
||||
|
||||
/**
|
||||
* Indicates that an Encrypt-Then-Mac algorithm was selected.
|
||||
* <p>
|
||||
* This has the following implementation details.
|
||||
* 1.5 transport: Protocol 2 Encrypt-then-MAC MAC algorithms
|
||||
* <p>
|
||||
* OpenSSH supports MAC algorithms, whose names contain "-etm", that
|
||||
* perform the calculations in a different order to that defined in RFC
|
||||
* 4253. These variants use the so-called "encrypt then MAC" ordering,
|
||||
* calculating the MAC over the packet ciphertext rather than the
|
||||
* plaintext. This ordering closes a security flaw in the SSH transport
|
||||
* protocol, where decryption of unauthenticated ciphertext provided a
|
||||
* "decryption oracle" that could, in conjunction with cipher flaws, reveal
|
||||
* session plaintext.
|
||||
* <p>
|
||||
* Specifically, the "-etm" MAC algorithms modify the transport protocol
|
||||
* to calculate the MAC over the packet ciphertext and to send the packet
|
||||
* length unencrypted. This is necessary for the transport to obtain the
|
||||
* length of the packet and location of the MAC tag so that it may be
|
||||
* verified without decrypting unauthenticated data.
|
||||
* <p>
|
||||
* As such, the MAC covers:
|
||||
* <p>
|
||||
* mac = MAC(key, sequence_number || packet_length || encrypted_packet)
|
||||
* <p>
|
||||
* where "packet_length" is encoded as a uint32 and "encrypted_packet"
|
||||
* contains:
|
||||
* <p>
|
||||
* byte padding_length
|
||||
* byte[n1] payload; n1 = packet_length - padding_length - 1
|
||||
* byte[n2] random padding; n2 = padding_length
|
||||
*
|
||||
* @return Whether the MAC algorithm is an Encrypt-Then-Mac algorithm
|
||||
*/
|
||||
boolean isEtm();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user