/*
* Copyright (c) 2016 Dell EMC Software
* All Rights Reserved
*/
package com.iwave.ext.windows.winrm.ntlm;
import java.util.Arrays;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
/**
* This class handles the specifics of encrypting/decrypting NTLM payloads. The protocol requires a stateful RC4 cipher. The
* details can be found at https://msdn.microsoft.com/en-us/library/cc236621.aspx or if you're not insane, a more readable
* (although less official) version can be found at http://davenport.sourceforge.net/ntlm.html.
*
* The NTLM encrypted payload has the following structure (also it's always in LITTLE_ENDIAN)
*
* <pre>
* Byte What it is Value
* ------------------------------
* 0 SIGNATURE_SIZE 16
* 4 VERSION_NUMBER 1
* 8 SIGNATURE RC4(HMAC(signingkey, sequence + plaintext)[0-7])
* 16 SEQUENCE sequence
* 20 Encrypted payload
* </pre>
*
*/
public class NTLMCrypt {
/** The computed client signing key. */
private byte[] clientSigningKey;
/** The computed client sealing key. */
private byte[] clientSealingKey;
/** The computed server signing key. */
private byte[] serverSigningKey;
/** The computed server sealing key. */
private byte[] serverSealingKey;
/** The RC4 stream for sending stuff. */
private Cipher clientRc4;
/** The RC4 stream for receiving stuff. */
private Cipher serverRc4;
/** The number of messages we have sent. */
private int clientSequence;
/** The number of messages we have received. */
private int serverSequence;
/**
* Initialized the encryption/decryption mechanism with the necessary values.
*
* @param key
* the key to use when signing and sealing
* @throws Exception
* if something goes wrong
*/
public NTLMCrypt(byte[] key) throws Exception {
// http://davenport.sourceforge.net/ntlm.html#ntlm2SessionSecurity mentions the specifics of what we're doing here
// http://davenport.sourceforge.net/ntlm.html#appendixC7 is an example of values that work if someone feels like
// refactoring
this.clientSigningKey = NTLMUtils.md5(key, NTLMConstants.CLIENTSIGNINGCONSTANT);
this.clientSealingKey = NTLMUtils.md5(key, NTLMConstants.CLIENTSEALINGCONSTANT);
this.serverSigningKey = NTLMUtils.md5(key, NTLMConstants.SERVERSIGNINGCONSTANT);
this.serverSealingKey = NTLMUtils.md5(key, NTLMConstants.SERVERSEALINGCONSTANT);
clientRc4 = Cipher.getInstance("RC4");
clientRc4.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(this.clientSealingKey, "RC4"));
serverRc4 = Cipher.getInstance("RC4");
serverRc4.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(this.serverSealingKey, "RC4"));
clientSequence = 0;
serverSequence = 0;
}
/**
* Encrypts and signs the payload. These operations must be performed atomically.
*
* @param plaintext
* the message to encrypt.
* @return the encrypted message
*/
public byte[] encryptAndSignPayload(byte[] plaintext) {
byte[] crypted = clientRc4.update(plaintext);
byte[] signature = getSignature(plaintext, clientSequence, clientRc4, clientSigningKey);
clientSequence++;
return NTLMUtils.concat(NTLMConstants.SIGNATURE_SIZE, signature, crypted);
}
/**
* Decrypts the message and verifies that it matches the signature. These operations must be performed atomically.
*
* @param crypted
* the encrypted payload (including signature)
* @param length
* the length of the message (not including signature)
* @return the decrypted message
* @throws UnrecognizedNTLMMessageException
* if the message is not able to be decrypted
*/
public byte[] decryptAndVerifyPayload(byte[] crypted, int length) throws UnrecognizedNTLMMessageException {
if (crypted.length != length + NTLMConstants.ENCRYPTED_HEADER_LENGTH) {
throw new UnrecognizedNTLMMessageException("Message length is incorrect");
}
boolean cryptOperationPerformed = false;
byte[] plaintext = null;
byte[] signature = null;
try {
byte[] size = Arrays.copyOfRange(crypted, 0, NTLMConstants.SIGNATURE_SIZE.length);
if (!Arrays.equals(size, NTLMConstants.SIGNATURE_SIZE)) {
throw new UnrecognizedNTLMMessageException(
"Signature is not the right size. The message is probably malformed.");
}
byte[] cryptedPayload = Arrays.copyOfRange(crypted, NTLMConstants.ENCRYPTED_HEADER_LENGTH,
NTLMConstants.ENCRYPTED_HEADER_LENGTH + length);
plaintext = serverRc4.update(cryptedPayload);
signature = getSignature(plaintext, serverSequence, serverRc4, serverSigningKey);
cryptOperationPerformed = true;
if (!Arrays.equals(signature, Arrays.copyOfRange(crypted, NTLMConstants.SIGNATURE_SIZE.length,
NTLMConstants.ENCRYPTED_HEADER_LENGTH))) {
throw new UnrecognizedNTLMMessageException("Signature does not match");
}
serverSequence++;
} catch (UnrecognizedNTLMMessageException e) {
// RC4 is a fun cipher, in that if you encrypt something, and then decrypt it, the key stream goes back to it's
// initial position. If we hit this block of code, it's because the message that we decrypted isn't the one we
// were supposed to decrypt. In this case, we reset the key stream to the position it was in before we started.
if (cryptOperationPerformed) {
serverRc4.update(signature);
serverRc4.update(plaintext);
}
throw e;
}
return plaintext;
}
/**
* Computes the signature operation on the provided plaintext.
*
* @param plaintext
* the plaintext
* @param sequence
* the sequence number
* @param rc4
* the cipher to use
* @param key
* the signingkey to use
* @return the computed signature
*/
private byte[] getSignature(byte[] plaintext, int sequence, Cipher rc4, byte[] key) {
byte[] sequenceBytes = NTLMUtils.convertInt(sequence);
byte[] hmac = NTLMUtils.calculateHmacMD5(key, NTLMUtils.concat(sequenceBytes, plaintext));
byte[] sig = rc4.update(Arrays.copyOfRange(hmac, 0, 8));
byte[] signature = NTLMUtils.concat(NTLMConstants.VERSION_NUMBER, sig, sequenceBytes);
return signature;
}
}