View Javadoc
1   package org.metricshub.winrm.service.client.encryption;
2   
3   /*-
4    * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲
5    * WinRM Java Client
6    * ჻჻჻჻჻჻
7    * Copyright 2023 - 2024 Metricshub
8    * ჻჻჻჻჻჻
9    * Licensed under the Apache License, Version 2.0 (the "License");
10   * you may not use this file except in compliance with the License.
11   * You may obtain a copy of the License at
12   *
13   *      http://www.apache.org/licenses/LICENSE-2.0
14   *
15   * Unless required by applicable law or agreed to in writing, software
16   * distributed under the License is distributed on an "AS IS" BASIS,
17   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18   * See the License for the specific language governing permissions and
19   * limitations under the License.
20   * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱
21   */
22  
23  import java.io.ByteArrayOutputStream;
24  import java.io.IOException;
25  import java.util.Arrays;
26  import java.util.function.Function;
27  import java.util.function.UnaryOperator;
28  import java.util.zip.CRC32;
29  import org.apache.cxf.message.Message;
30  import org.apache.http.auth.Credentials;
31  import org.metricshub.winrm.service.client.auth.ntlm.NTCredentialsWithEncryption;
32  import org.metricshub.winrm.service.client.auth.ntlm.NTLMEngineUtils;
33  
34  /**
35   * Code from io.cloudsoft.winrm4j.client.encryption.NtlmEncryptionUtils release
36   * 0.12.3 @link https://github.com/cloudsoft/winrm4j
37   */
38  public class NtlmEncryptionUtils {
39  
40  	public static final String ENCRYPTED_BOUNDARY_PREFIX = "--Encrypted Boundary";
41  	public static final String ENCRYPTED_BOUNDARY_CR = ENCRYPTED_BOUNDARY_PREFIX + "\r\n";
42  	public static final String ENCRYPTED_BOUNDARY_END = ENCRYPTED_BOUNDARY_PREFIX + "--\r\n";
43  
44  	protected final NTCredentialsWithEncryption credentials;
45  
46  	private NtlmEncryptionUtils(final NTCredentialsWithEncryption credentials) {
47  		this.credentials = credentials;
48  	}
49  
50  	static NtlmEncryptionUtils of(final Credentials credentials) {
51  		return credentials instanceof NTCredentialsWithEncryption
52  			? new NtlmEncryptionUtils((NTCredentialsWithEncryption) credentials)
53  			: null;
54  	}
55  
56  	static NtlmEncryptionUtils of(final Message message) {
57  		final Credentials credentials = (Credentials) message.getExchange().get(Credentials.class.getName());
58  		return of(credentials);
59  	}
60  
61  	public byte[] encryptAndSign(final Message message, final byte[] messageBody) {
62  		try (final ByteArrayOutputStream out = new ByteArrayOutputStream()) {
63  			out.write(ENCRYPTED_BOUNDARY_CR.getBytes());
64  			out.write(("\tContent-Type: application/HTTP-SPNEGO-session-encrypted\r\n").getBytes());
65  
66  			// message.get(Message.CONTENT_TYPE); - if we need the action
67  			// Content-Type -> application/soap+xml;
68  			// action="http://schemas.xmlsoap.org/ws/2004/09/transfer/Create"
69  			out.write(
70  				String
71  					.format("\tOriginalContent: type=application/soap+xml;charset=UTF-8;Length=%d\r\n", messageBody.length)
72  					.getBytes()
73  			);
74  
75  			out.write(ENCRYPTED_BOUNDARY_CR.getBytes());
76  			out.write("\tContent-Type: application/octet-stream\r\n".getBytes());
77  
78  			// for credssh chunking might be needed, but not for ntlm
79  
80  			writeNtlmEncrypted(messageBody, out);
81  
82  			out.write(ENCRYPTED_BOUNDARY_END.getBytes());
83  
84  			message.put(
85  				Message.CONTENT_TYPE,
86  				"multipart/encrypted;protocol=\"application/HTTP-SPNEGO-session-encrypted\";" +
87  				"boundary=\"Encrypted Boundary\""
88  			);
89  			message.put(Message.ENCODING, null);
90  
91  			return out.toByteArray();
92  		} catch (final Exception e) {
93  			throw new IllegalStateException("Cannot encrypt WinRM message", e);
94  		}
95  	}
96  
97  	private byte[] seal(final byte[] in) {
98  		return credentials.getStatefulEncryptor().update(in);
99  	}
100 
101 	private void writeNtlmEncrypted(final byte[] messageBody, final ByteArrayOutputStream encrypted) throws IOException {
102 		long seqNum = credentials.getSequenceNumberOutgoing().incrementAndGet();
103 
104 		try (
105 			final ByteArrayOutputStream signatureOs = new ByteArrayOutputStream();
106 			final ByteArrayOutputStream sealedOs = new ByteArrayOutputStream()
107 		) {
108 			// seal first, even though appended afterwards, because encryptor is stateful
109 			sealedOs.write(seal(messageBody));
110 
111 			calculateSignature(
112 				messageBody,
113 				seqNum,
114 				signatureOs,
115 				credentials,
116 				NTCredentialsWithEncryption::getClientSigningKey,
117 				this::seal
118 			);
119 
120 			encrypted.write(ByteArrayUtils.getLittleEndianUnsignedInt(signatureOs.size()));
121 			encrypted.write(signatureOs.toByteArray());
122 			encrypted.write(sealedOs.toByteArray());
123 		}
124 	}
125 
126 	public void decrypt(final Message message) {
127 		new Decryptor(credentials).handle(message);
128 	}
129 
130 	static void calculateSignature(
131 		final byte[] messageBody,
132 		final long seqNum,
133 		final ByteArrayOutputStream signature,
134 		final NTCredentialsWithEncryption credentials,
135 		final Function<NTCredentialsWithEncryption, byte[]> signingKeyFunction,
136 		final UnaryOperator<byte[]> sealer
137 	) throws IOException {
138 		if (credentials.hasNegotiateFlag(NTLMEngineUtils.NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY)) {
139 			// also see HMACMD5 in NTLMEngineIpml
140 			byte[] checksum = EncryptionUtils.hmacMd5(
141 				signingKeyFunction.apply(credentials),
142 				ByteArrayUtils.concat(ByteArrayUtils.getLittleEndianUnsignedInt(seqNum), messageBody)
143 			);
144 
145 			checksum = Arrays.copyOfRange(checksum, 0, 8);
146 
147 			if (credentials.hasNegotiateFlag(NTLMEngineUtils.NTLMSSP_NEGOTIATE_KEY_EXCH)) {
148 				checksum = sealer.apply(checksum);
149 			}
150 			// version
151 			signature.write(new byte[] { 1, 0, 0, 0 });
152 			// checksum
153 			signature.write(checksum);
154 			// seq num
155 			signature.write(ByteArrayUtils.getLittleEndianUnsignedInt(seqNum));
156 		} else {
157 			final CRC32 crc = new CRC32();
158 			crc.update(messageBody);
159 			final long messageCrc = crc.getValue();
160 
161 			// version
162 			signature.write(new byte[] { 1, 0, 0, 0 });
163 			// random pad
164 			signature.write(sealer.apply(ByteArrayUtils.getLittleEndianUnsignedInt(0)));
165 			// checksum
166 			signature.write(sealer.apply(ByteArrayUtils.getLittleEndianUnsignedInt(messageCrc)));
167 			// seq num
168 			signature.write(sealer.apply(ByteArrayUtils.getLittleEndianUnsignedInt(seqNum)));
169 		}
170 	}
171 }