/* * 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.CHARSET; import static com.iwave.ext.windows.winrm.ntlm.NTLMConstants.CONTENT_TYPE_FOR_ENCRYPTED_PART; import static com.iwave.ext.windows.winrm.ntlm.NTLMConstants.CONTENT_TYPE_FOR_SOAP; import static com.iwave.ext.windows.winrm.ntlm.NTLMConstants.CONTENT_TYPE_FOR_SPNEGO; import static com.iwave.ext.windows.winrm.ntlm.NTLMConstants.EQUALS; import static com.iwave.ext.windows.winrm.ntlm.NTLMConstants.HORIZONTAL_TAB; import static com.iwave.ext.windows.winrm.ntlm.NTLMConstants.LENGTH; import static com.iwave.ext.windows.winrm.ntlm.NTLMConstants.NEWLINE; import static com.iwave.ext.windows.winrm.ntlm.NTLMConstants.ORIGINAL_CONTENT; import static com.iwave.ext.windows.winrm.ntlm.NTLMConstants.SEMICOLON; import static com.iwave.ext.windows.winrm.ntlm.NTLMConstants.TYPE; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.CharEncoding; 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.BasicLineFormatter; import org.apache.http.message.BasicNameValuePair; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * This class is responsible for taking an HttpEntity, encrypting it, and then presenting a new HttpEntity which follows the * NTLM specification. https://msdn.microsoft.com/en-us/library/cc251578.aspx * * <pre> * --Encrypted Boundary\r\n * \tContent-Type: application/HTTP-SPNEGO-session-encrypted\r\n * \tOriginalContent: type=application/soap+xml;charset={ENCODING};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 NTLMEncryptedEntity extends HttpEntityWrapper { /** * The logger for this class. */ private static final Logger LOG = LoggerFactory.getLogger(NTLMEncryptedEntity.class); /** The encrypted content to send. */ private byte[] encryptedContent; /** Constant for content type header. */ private static final String CONTENT_TYPE = BasicLineFormatter.INSTANCE.formatHeader(null, new BasicHeader(HttpHeaders.CONTENT_TYPE, CONTENT_TYPE_FOR_SPNEGO)).toString(); /** Constant for encrypted content type header. */ private static final String ENCRYPTED_CONTENT_TYPE = BasicLineFormatter.INSTANCE.formatHeader(null, new BasicHeader(HttpHeaders.CONTENT_TYPE, CONTENT_TYPE_FOR_ENCRYPTED_PART)).toString(); /** * Wraps and encrypts an http entity for transmission. * * @param wrappedEntity * the entity to wrap * @param crypt * the encryption to use * @param boundary * the boundary to use for the multipart message */ public NTLMEncryptedEntity(HttpEntity wrappedEntity, NTLMCrypt crypt, String boundary) { super(wrappedEntity); try { byte[] content = IOUtils.toByteArray(wrappedEntity.getContent()); StringBuilder builder = new StringBuilder(); // Add a boundary builder.append(BOUNDARY_PREFIX); builder.append(boundary); builder.append(NEWLINE); // Add the content-type header builder.append(HORIZONTAL_TAB); builder.append(CONTENT_TYPE); builder.append(NEWLINE); // Add the original content header builder.append(HORIZONTAL_TAB); builder.append(BasicLineFormatter.INSTANCE.formatHeader( null, buildOriginalContentHeader(CONTENT_TYPE_FOR_SOAP, getEncoding(wrappedEntity), Integer.toString(content.length))) .toString()); builder.append(NEWLINE); // Add another boundary builder.append(BOUNDARY_PREFIX); builder.append(boundary); builder.append(NEWLINE); // Add the encrypted header builder.append(HORIZONTAL_TAB); builder.append(ENCRYPTED_CONTENT_TYPE); builder.append(NEWLINE); // Concatenate everything together and add a boundary at the end encryptedContent = NTLMUtils.concat(builder.toString().getBytes(NTLMUtils.DEFAULT_CHARSET), crypt.encryptAndSignPayload(content), (BOUNDARY_PREFIX + boundary + BOUNDARY_PREFIX + NEWLINE).getBytes(NTLMUtils.DEFAULT_CHARSET)); } catch (Exception e) { throw new RuntimeException(e); } } /** * Builds OriginalContent: type={type};charset={charset};Length=length. Explicitly with no spaces after semi colons or it * breaks. * * @param type * the type * @param charset * the charset * @param length * the length * @return OriginalContent: type={type};charset={charset};Length=length */ private Header buildOriginalContentHeader(String type, String charset, String length) { NameValuePair[] nvps = new NameValuePair[] { new BasicNameValuePair(TYPE, type), new BasicNameValuePair(CHARSET, charset), new BasicNameValuePair(LENGTH, length) }; // We can't use the apache header formatters because they add spaces after the semi-colons, and the NTLM // specification forbids it (it blows up when attempting communication). As a result, we need to handcraft the header StringBuilder value = new StringBuilder(); for (int i = 0; i < nvps.length; i++) { if (i > 0) { value.append(SEMICOLON); } value.append(nvps[i].getName()).append(EQUALS).append(nvps[i].getValue()); } return new BasicHeader(ORIGINAL_CONTENT, value.toString()); } /** * Retrieves the character encoding from the entity. * * @param entity * the entity * @return the encoding */ private String getEncoding(HttpEntity entity) { String encoding = null; Header header = wrappedEntity.getContentType(); if (header != null) { for (HeaderElement he : header.getElements()) { for (NameValuePair nvp : he.getParameters()) { if (nvp.getName().equals(CHARSET)) { encoding = nvp.getValue(); } } } } // The encoding must be UTF-16 or UTF-8 as specified by the NTLM spec. if (CharEncoding.UTF_16.equals(encoding) || CharEncoding.UTF_8.equals(encoding)) { return encoding; } else { LOG.warn(encoding + " is not a valid character set for NTLM communcation."); return NTLMUtils.DEFAULT_CHARSET.displayName(); } } @Override public InputStream getContent() { return new ByteArrayInputStream(encryptedContent); } @Override public void writeTo(OutputStream outstream) throws IOException { outstream.write(encryptedContent); } @Override public long getContentLength() { return encryptedContent.length; } }