/* * Copyright (c) 2016 Dell EMC Software * All Rights Reserved */ package com.iwave.ext.windows.winrm.ntlm; import static com.iwave.ext.windows.winrm.ntlm.NTLMConstants.BOUNDARY_PREFIX; import static com.iwave.ext.windows.winrm.ntlm.NTLMConstants.CONTENT_TYPE_FOR_ENCRYPTED_PART_AS_BYTES; import static com.iwave.ext.windows.winrm.ntlm.NTLMConstants.NEWLINE_AS_BYTES; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.Arrays; import java.util.List; import org.apache.commons.io.IOUtils; import org.apache.http.Header; import org.apache.http.HeaderElement; import org.apache.http.HttpEntity; import org.apache.http.HttpHeaders; import org.apache.http.NameValuePair; import org.apache.http.entity.HttpEntityWrapper; import org.apache.http.message.BasicHeader; import org.apache.http.message.BasicHeaderElement; import org.apache.http.message.BasicHeaderValueFormatter; import org.apache.http.message.BasicLineParser; import org.apache.http.message.BasicNameValuePair; /** * This class is responsible for taking an HttpEntity with an encrypted payload and presenting it unencrypted. The format of * the message should be * * <pre> * --Encrypted Boundary\r\n * \tContent-Type: application/HTTP-SPNEGO-session-encrypted\r\n * \tOriginalContent: type={type};charset={charset};Length={LENGTH}\r\n * --Encrypted Boundary\r\n * \tContent-Type: application/octet-stream\r\n * 20 bytes for a signature * {ENCRYPTED PAYLOAD OF SIZE {LENGTH}} * --Encrypted Boundary-- * </pre> * */ public class NTLMDecryptedEntity extends HttpEntityWrapper { /** The plaintext from the encrypted payload. */ private byte[] plaintext; /** The length of the plaintext, as indicated by the header in the message. */ private Integer length; /** The content type of the plaintext message. */ private Header contentType; /** * This constructor decrypts the wrapped entity. * * @param wrappedEntity * the entity to decrypt * @param crypt * the encryption object to use to decrypt the payload * @param boundary * the boundary for the multi-part message */ public NTLMDecryptedEntity(HttpEntity wrappedEntity, NTLMCrypt crypt, String boundary) { super(wrappedEntity); byte[] actualBoundary = (BOUNDARY_PREFIX + boundary).getBytes(NTLMUtils.DEFAULT_CHARSET); try { byte[] content = IOUtils.toByteArray(wrappedEntity.getContent()); process(content, crypt, actualBoundary); } catch (Exception e) { throw new RuntimeException(e); } } /** * Processes the multipart message. * * @param content * the message * @param crypt * the encryption mechanism * @param boundary * the boundary of the multipart message * @throws UnrecognizedNTLMMessageException * if something goes wrong */ private void process(byte[] content, NTLMCrypt crypt, byte[] boundary) throws UnrecognizedNTLMMessageException { List<byte[]> parts = NTLMUtils.split(boundary, content); if (parts.size() != 4) { throw new RuntimeException("The multipart message is not well formed."); } processHeaders(parts.get(1)); processPayload(parts.get(2), crypt); } /** * Processes the section of the multipart message that is headers. * * @param headerBytes * the header bytes */ private void processHeaders(byte[] headerBytes) { try { int start = NTLMUtils.indexOf(NTLMConstants.ORIGINAL_CONTENT_AS_BYTES, headerBytes); int end = NTLMUtils.indexOf(NTLMConstants.NEWLINE_AS_BYTES, headerBytes, start); String type = null; String encoding = null; Header header = BasicLineParser.parseHeader(new String(Arrays.copyOfRange(headerBytes, start, end), NTLMUtils.DEFAULT_CHARSET), null); for (HeaderElement element : header.getElements()) { if (element.getName().equals(NTLMConstants.TYPE)) { type = element.getValue(); } for (NameValuePair nvp : element.getParameters()) { if (nvp.getName().equals(NTLMConstants.CHARSET)) { encoding = nvp.getValue(); } else if (nvp.getName().equals(NTLMConstants.LENGTH)) { length = Integer.parseInt(nvp.getValue()); } } } if (type == null || encoding == null || length == null) { throw new Exception("OriginalContent does not contain the necessary values. " + header.getValue()); } contentType = buildContentTypeHeader(type, encoding); } catch (Exception e) { throw new RuntimeException("There was an error extracting values from the oringial content header.", e); } } /** * Builds the content type header that would have existed in a decrypted message. * * @param type * the original content type * @param encoding * the original charset * * @return the new content type header */ private Header buildContentTypeHeader(String type, String encoding) { NameValuePair[] nvps = new NameValuePair[] { new BasicNameValuePair(NTLMConstants.CHARSET, encoding), }; BasicHeaderElement elem = new BasicHeaderElement(type, null, nvps); return new BasicHeader(HttpHeaders.CONTENT_TYPE, BasicHeaderValueFormatter.formatHeaderElement(elem, false, null)); } /** * Process the payload from the multipart message. * * @param payloadBytes * the part of the message that is the payload * @param crypt * the encryption mechanism * @throws UnrecognizedNTLMMessageException * if something goes wrong */ private void processPayload(byte[] payloadBytes, NTLMCrypt crypt) throws UnrecognizedNTLMMessageException { int start = NTLMUtils.indexOf(CONTENT_TYPE_FOR_ENCRYPTED_PART_AS_BYTES, payloadBytes) + CONTENT_TYPE_FOR_ENCRYPTED_PART_AS_BYTES.length + NEWLINE_AS_BYTES.length; plaintext = crypt.decryptAndVerifyPayload(Arrays.copyOfRange(payloadBytes, start, payloadBytes.length), (int) getContentLength()); } @Override public InputStream getContent() { return new ByteArrayInputStream(plaintext); } @Override public long getContentLength() { return length; } @Override public void writeTo(OutputStream outstream) throws IOException { outstream.write(plaintext); } @Override public Header getContentType() { if (contentType == null) { return super.getContentType(); } else { return contentType; } } }