Package org.vngx.jsch.kex

Source Code of org.vngx.jsch.kex.KeyExchange

/*
* Copyright (c) 2010-2011 Michael Laudati, N1 Concepts LLC.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. The names of the authors may not be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
* FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL N1
* CONCEPTS LLC OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY DIRECT,
* INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

package org.vngx.jsch.kex;

import static org.vngx.jsch.constants.TransportLayerProtocol.SSH_MSG_KEXINIT;
import static org.vngx.jsch.constants.TransportLayerProtocol.SSH_MSG_NEWKEYS;

import java.util.concurrent.atomic.AtomicBoolean;

import org.vngx.jsch.Buffer;
import org.vngx.jsch.JSch;
import org.vngx.jsch.Packet;
import org.vngx.jsch.Session;
import org.vngx.jsch.UserInfo;
import org.vngx.jsch.Util;
import org.vngx.jsch.algorithm.AlgorithmManager;
import org.vngx.jsch.algorithm.Algorithms;
import org.vngx.jsch.algorithm.Random;
import org.vngx.jsch.cipher.Cipher;
import org.vngx.jsch.config.SessionConfig;
import org.vngx.jsch.constants.MessageConstants;
import org.vngx.jsch.constants.SSHConstants;
import org.vngx.jsch.exception.JSchException;
import org.vngx.jsch.util.HostKey;
import org.vngx.jsch.util.HostKeyRepository;
import org.vngx.jsch.util.HostKeyRepository.Check;
import org.vngx.jsch.util.Logger;
import org.vngx.jsch.util.Logger.Level;

/**
* <p>Key Exchange is any method in cryptography by which cryptographic keys are
* exchanged between users, allowing use of a cryptographic algorithm. If Alice
* and Bob wish to exchange encrypted messages, each must be equipped to decrypt
* received messages and to encrypt sent messages. The nature of the information
* they require to do so depends on the encryption technique they might use. If
* they use a code, both will require a copy of the same codebook. If they use a
* cipher, they will need appropriate keys. If the cipher is a symmetric key
* cipher, both will need a copy of the same key. If an asymmetric key cipher
* with the public/private key property, both will need the other's public
* key.</p>
*
* <p>Of course, if the DH private parameters for the client and server are
* revealed, then the session key is revealed, but these items can be thrown
* away after the key exchange completes.  It's worth pointing out that these
* items should not be allowed to end up on swap space and that they should be
* erased from memory as soon as the key exchange completes.</p>
*
* <p><a href="http://tools.ietf.org/html/rfc4253#section-7">RFC 4253 - The
* Secure Shell (SSH) Transport Layer Protocol: Key Exchange</a></p>
*
* @see org.vngx.jsch.kex.KexAlgorithm
* @see org.vngx.jsch.kex.KexProposal
*
* @author Michael Laudati
*/
public final class KeyExchange {

  /** Number of bytes in the KEX_INIT 'cookie'. */
  final static int KEX_COOKIE_LENGTH = 16;

  /** Session the key exchange belongs to. */
  final Session _session;
  /** Buffer for sending SSH packets. */
  final Buffer _buffer = new Buffer();
  /** True if session is currently in process of a key exchange. */
  final AtomicBoolean _inKeyExchange = new AtomicBoolean(false);

  /** Guessed algorithms during key exchange. */
  KexProposal _proposal;
  /** Key exchange algorithm which performs the actual exchange. */
  KexAlgorithm _kexAlg;
  /** Client's SSH_MSG_KEXINIT payload sent to server. */
  byte[] I_C;
  /** Server's SSH_MSG_KEXINIT payload received from server. */
  byte[] I_S;
  /** Host key generated after key exchange and validation. */
  HostKey _hostKey;
 

  /**
   * Creates a new instance of {@code KeyExchange} for the specified
   * {@code session} instance.  A key exchange should not be created
   * until after the session has established a socket connection to the remote
   * host and exchanged version information.
   *
   * @param session to create key exchange for
   */
  public KeyExchange(Session session) {
    if( session == null ) {
      throw new IllegalArgumentException("Session cannot be null");
    }
    _session = session;
  }

  public byte[] runFirstKex() throws Exception {
    // Initialize the key exchange for session and check expected response
    sendKexInit();
    if( _session.read(_buffer).getCommand() != SSH_MSG_KEXINIT ) {
      throw new KexException("Invalid kex protocol, expected SSH_MSG_KEXINIT(20): " + _buffer.getCommand());
    }
    JSch.getLogger().log(Logger.Level.INFO, "SSH_MSG_KEXINIT received");

    // Read server response and generate appropriate kex algorithm
    receiveKexInit(_buffer);

    // Check host key against known hosts before continuing...
    checkHost(_kexAlg);

    // Request new set of keys after initial key exchange
    sendNewKeys();
    // Read SSH_MSG_NEWKEYS response from server
    if( _session.read(_buffer).getCommand() != SSH_MSG_NEWKEYS ) {
      throw new KexException("Invalid kex protocol, expected SSH_MSG_NEWKEYS(21): " + _buffer.getCommand());
    }
    JSch.getLogger().log(Logger.Level.INFO, "SSH_MSG_NEWKEYS received");

    // Return the session ID (copy of exchange hash H) from first kex
    return Util.copyOf(_kexAlg.getH(), _kexAlg.getH().length);
  }

  public void rekey(Buffer rekeyBuffer) throws Exception {
    receiveKexInit(rekeyBuffer);
  }

  /**
   * Returns true if in the process of a key exchange.
   *
   * @return true if in process of a key exchange
   */
  public boolean inKex() {
    return _inKeyExchange.get();
  }

  public void kexCompleted() {
    _inKeyExchange.set(false);
  }
 
  /**
   * <p>Sends a key exchange init request message to the server using a local
   * packet specifying the client's proposals for key exchange.</p>
   *
   * <p><a href="http://tools.ietf.org/html/rfc4253#section-7.1">RFC 4253 -
   * 7.1. Algorithm Negotiation</a></p>
   *
   * @throws KexException if any errors occur
   */
  public void sendKexInit() throws KexException {
    // Check if already in middle of a kex
    if( _inKeyExchange.getAndSet(true) ) {  // Flip state flag entering kex
      return// Return if already in process of kex
    }
    Buffer kexBuffer = new Buffer();      // Use a separate packet and buffer since
    Packet kexPacket = new Packet(kexBuffer)// kex may be invoked by user thread
    try {
      // Random instance for generating the kex cookie.  The 'cookie' MUST
      // be a random value generated by the sender. Its purpose is to make
      // it impossible for either side to fully determine the keys and the
      // session identifier.
      final Random random = AlgorithmManager.getManager().createAlgorithm(Algorithms.RANDOM, _session);

      // Construct the KEX INIT message packet
      // byte    SSH_MSG_KEXINIT(20)
      // byte[16]  cookie (random bytes)
      // string  kex_algorithms
      // string  server_host_key_algorithms
      // string  encryption_algorithms_client_to_server
      // string  encryption_algorithms_server_to_client
      // string  mac_algorithms_client_to_server
      // string  mac_algorithms_server_to_client
      // string  compression_algorithms_client_to_server
      // string  compression_algorithms_server_to_client
      // string  languages_client_to_server
      // string  languages_server_to_client
      // byte    boolean first_kex_packet_follows
      // uint32  0 (reserved for future extension)
      kexPacket.reset();
      kexBuffer.putByte(SSH_MSG_KEXINIT);
      random.fill(kexBuffer.getArray(), kexBuffer.getIndex(), KEX_COOKIE_LENGTH);
      kexBuffer.skip(KEX_COOKIE_LENGTH)// Move index forward
      kexBuffer.putString(_session.getConfig().getString(SessionConfig.KEX_ALGORITHMS));
      kexBuffer.putString(_session.getConfig().getString(SessionConfig.KEX_SERVER_HOST_KEY));
      kexBuffer.putString(_session.getConfig().getCiphersC2S())// Checked list of client-to-server ciphers
      kexBuffer.putString(_session.getConfig().getCiphersS2C())// Checked list of server-to-client ciphers
      kexBuffer.putString(_session.getConfig().getString(SessionConfig.KEX_MAC_C2S));
      kexBuffer.putString(_session.getConfig().getString(SessionConfig.KEX_MAC_S2C));
      kexBuffer.putString(_session.getConfig().getString(SessionConfig.KEX_COMPRESSION_C2S));
      kexBuffer.putString(_session.getConfig().getString(SessionConfig.KEX_COMPRESSION_S2C));
      kexBuffer.putString(_session.getConfig().getString(SessionConfig.KEX_LANG_C2S));
      kexBuffer.putString(_session.getConfig().getString(SessionConfig.KEX_LANG_S2C));
      kexBuffer.putByte((byte) 0)// 0 is false, not sending guessed packet
      kexBuffer.putInt(0);

      // Set the client's kex algorithm initialization message
      I_C = new byte[kexBuffer.getIndex()-5];
      System.arraycopy(kexBuffer.getArray(), 5, I_C, 0, I_C.length);
      _session.write(kexPacket)// Send key exchange init message to server
      JSch.getLogger().log(Logger.Level.INFO, "SSH_MSG_KEXINIT sent");
    } catch(Exception e) {
      throw new KexException("Failed to send SSH_MSG_KEXINIT", e);
    } finally {
      kexBuffer.clear()// Clear buffer to ensure sensitive data is wiped
    }
  }

  /**
   * <p>Receives the server's key exchange KEXINIT from the specified
   * {@code buffer}.</p>
   *
   * @param buffer containing server's SSH_MSG_KEXINIT message
   * @throws KexException if any errors occur
   */
  private void receiveKexInit(final Buffer buffer) throws KexException {
    // Read the server's proposal for kex algorithms from buffer
    // Check packet length for compression and set size of data portion
    int packetLength = buffer.getInt();
    if( packetLength != buffer.getLength() ) {
      buffer.getByte(); /* padding length*/ 
      // Compressed: (uncompressed data length) - (packet length int + padding length byte)
      I_S = new byte[buffer.getIndex() - 5];
    } else {
      // Not compressed: (packet length) - (padding length byte )- (padding length)
      I_S = new byte[packetLength - 1 - buffer.getByte()];
    }
    buffer.getBytes(I_S)// Read in server proposal from buffer

    // If rekeying was activated by server, then send client's proposal for kex
    if( !_inKeyExchange.get() ) {
      sendKexInit();
    }

    // Guess algorithms to use from the client's and server's proposals
    _proposal = KexProposal.createProposal(I_S, I_C);
    if( JSch.getLogger().isEnabled(Logger.Level.DEBUG) ) {
      JSch.getLogger().log(Level.DEBUG, _proposal.toString());
    }

    // If not authorized yet, don't allow 'none' cipher to be used, throw
    // an exception to prevent auth data from being sent in the clear
    if( !_session.isAuthenticated() &&
        ( Cipher.CIPHER_NONE.equals(_proposal.getCipherAlgCtoS()) ||
          Cipher.CIPHER_NONE.equals(_proposal.getCipherAlgStoC()) ) ) {
      throw new KexException("Cipher 'none' cannot be used before authentication has succeeded");
    }

    // Attempt to create kex algorithm to perform kex
    try {
      JSch.getLogger().log(Logger.Level.INFO, "Kex method: " + _proposal.getKexAlg());
      _kexAlg = AlgorithmManager.getManager().createAlgorithm(_proposal.getKexAlg(), _session);
      _kexAlg.init(_session, I_C, I_S)// Initialize and return
    } catch(Exception e) {
      throw new KexException("Failed to load KexAlgorithm '"+_proposal.getKexAlg()+"'", e);
    }

    try {
      do {
        if( !_kexAlg.next(_session.read(_buffer)) ) {
          throw new KexException("Kex failure, host key could not be verified");
        }
      } while( _kexAlg.getState() != KexAlgorithm.STATE_END )// Do until kex is completed
    } catch(KexException ke) {
      throw ke;
    } catch(Exception e) {
      throw new KexException("Failed to run KexAlgorithm", e);
    }
  }

  /**
   * <p>Sends message to server to end key exchange and start using new keys.
   * Key exchange ends by each side sending an SSH_MSG_NEWKEYS message.  This
   * message is sent with the old keys and algorithms.  All messages sent
   * after this message MUST use the new keys and algorithms. When this
   * message is received, the new keys and algorithms MUST be used for
   * receiving.</p>
   *
   * <p>The purpose of this message is to ensure that a party is able to
   * respond with an SSH_MSG_DISCONNECT message that the other party can
   * understand if something goes wrong with the key exchange.</p>
   *
   * <p><a href="http://tools.ietf.org/html/rfc4253#section-7.3">RFC 4253 -
   * 7.3. Taking Keys Into Use</a></p>
   *
   * @throws KexException if any errors occur
   */
  public void sendNewKeys() throws KexException {
    try {
      // Send SSH_MSG_NEWKEYS request to server
      Buffer buffer = new Buffer(500);
      Packet packet = new Packet(buffer);
      packet.reset();
      buffer.putByte(SSH_MSG_NEWKEYS);
      _session.write(packet);
      JSch.getLogger().log(Logger.Level.INFO, "SSH_MSG_NEWKEYS sent");
    } catch(Exception e) {
      throw new KexException("Failed to send SSH_MSG_NEWKEYS request", e);
    }
  }

  /**
   * Checks if the host received during key exchange is a valid host as
   * determined by the user and known hosts.
   *
   * @param kex instance
   * @throws JSchException if any errors occur
   */
  private void checkHost(KexAlgorithm kex) throws JSchException {
    UserInfo _userinfo = _session.getUserInfo();

    // Check if host key alias exists and use it, or if it's not present and
    // not using default port, set the port in host to check
    String chost = _session.getHost();
    if( _session.getHostKeyAlias() != null ) {
      chost = _session.getHostKeyAlias();
    } else if( _session.getPort() != SSHConstants.DEFAULT_SSH_PORT ) {
      chost = "[" + chost + "]:" + _session.getPort();
    }

    // Check host against known hosts repository
    HostKeyRepository hkr = JSch.getInstance().getHostKeyRepository();
    Check keyCheck;
    synchronized( hkr ) {
      keyCheck = hkr.check(chost, kex.K_S);
    }

    boolean insert = false;
    String shkc = _session.getConfig().getString(SessionConfig.STRICT_HOST_KEY_CHECKING);
    if( ("ask".equals(shkc) || "yes".equals(shkc)) && keyCheck == Check.CHANGED ) {
      String file = hkr.getKnownHostsRepositoryID() != null ?
        hkr.getKnownHostsRepositoryID() : SSHConstants.KNOWN_HOSTS;

      // Notify user host key changed (ask if requested) and throw exception
      // if user doesn't accept the new key
      if( _userinfo != null ) {
        if( "ask".equals(shkc) ) {
          if( !_userinfo.promptYesNo(String.format(MessageConstants.PROMPT_REPLACE_KEY,
              kex._hostKeyType.DISPLAY_NAME, Util.getFingerPrint(kex.K_S), file)) ) {
            throw new JSchException("HostKey has changed (StrictHostKeyChecking:ask): "+chost);
          }
        } else // shkc.equals("yes")
          _userinfo.showMessage(String.format(MessageConstants.INVALID_SERVER_HOST,
              kex._hostKeyType.DISPLAY_NAME, Util.getFingerPrint(kex.K_S), file));
          throw new JSchException("HostKey has changed (StrictHostKeyChecking:yes): "+chost);
        }
      }

      // Remove the old key from the repository
      synchronized ( hkr ) {
        hkr.remove(chost, kex._hostKeyType, null);
        insert = true;
      }
    }

    if( ("ask".equals(shkc) || "yes".equals(shkc)) && keyCheck != Check.OK && !insert ) {
      if( "yes".equals(shkc) ) {
        throw new JSchException("HostKey does not match known hosts (StrictHostKeyChecking:yes): "+chost);
      }
      if( _userinfo != null ) {
        if( !_userinfo.promptYesNo(String.format(MessageConstants.PROMPT_UNKNOWN_KEY,
            chost, kex._hostKeyType.DISPLAY_NAME, Util.getFingerPrint(kex.K_S))) ) {
          throw new JSchException("HostKey does not match known hosts (StrictHostKeyChecking:ask): "+chost);
        }
        insert = true;
      } else {
        if( keyCheck == Check.NOT_INCLUDED ) {
          throw new JSchException("UnknownHostKey: "+chost+". "+kex._hostKeyType+" key fingerprint is "+Util.getFingerPrint(kex.K_S));
        } else {
          throw new JSchException("HostKey has been changed (StrictHostKeyChecking:ask): " + chost);
        }
      }
    }

    if( "no".equals(shkc) && keyCheck == Check.NOT_INCLUDED ) {
      insert = true;
    }
    if( keyCheck == Check.OK && JSch.getLogger().isEnabled(Logger.Level.INFO) ) {
      JSch.getLogger().log(Logger.Level.INFO, "Host '"+chost+"' is known and matches the "+kex._hostKeyType+" host key");
    }
    if( insert && JSch.getLogger().isEnabled(Logger.Level.WARN) ) {
      JSch.getLogger().log(Logger.Level.WARN, "Permanently added '"+chost+"' ("+kex._hostKeyType+") to the list of known hosts.");
    }

    // Create host key instance
    _hostKey = HostKey.createHostKey(chost, kex.K_S, _session.getConfig().getBoolean(SessionConfig.HASH_KNOWN_HOSTS));
    if( insert ) {
      synchronized( hkr ) {
        hkr.add(_hostKey, _userinfo);
      }
    }
  }

  /**
   * Returns the key exchange proposals agreed upon during the key exchange.
   *
   * @return key exchange proposals
   */
  public KexProposal getKexProposal() {
    return _proposal;
  }

  public KexAlgorithm getKexAlgorithm() {
    return _kexAlg;
  }

}
TOP

Related Classes of org.vngx.jsch.kex.KeyExchange

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.