View Javadoc
1   package org.metricshub.ipmi.core.coding.protocol.decoder;
2   
3   /*-
4    * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲
5    * IPMI Java Client
6    * ჻჻჻჻჻჻
7    * Copyright 2023 Verax Systems, MetricsHub
8    * ჻჻჻჻჻჻
9    * This program is free software: you can redistribute it and/or modify
10   * it under the terms of the GNU Lesser General Public License as
11   * published by the Free Software Foundation, either version 3 of the
12   * License, or (at your option) any later version.
13   *
14   * This program is distributed in the hope that it will be useful,
15   * but WITHOUT ANY WARRANTY; without even the implied warranty of
16   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17   * GNU General Lesser Public License for more details.
18   *
19   * You should have received a copy of the GNU General Lesser Public
20   * License along with this program.  If not, see
21   * <http://www.gnu.org/licenses/lgpl-3.0.html>.
22   * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱
23   */
24  
25  import org.metricshub.ipmi.core.coding.protocol.IpmiMessage;
26  import org.metricshub.ipmi.core.coding.protocol.Ipmiv20Message;
27  import org.metricshub.ipmi.core.coding.protocol.PayloadType;
28  import org.metricshub.ipmi.core.coding.rmcp.RmcpMessage;
29  import org.metricshub.ipmi.core.coding.security.CipherSuite;
30  import org.metricshub.ipmi.core.coding.security.ConfidentialityNone;
31  import org.metricshub.ipmi.core.coding.security.SecurityConstants;
32  import org.metricshub.ipmi.core.common.TypeConverter;
33  
34  import java.security.InvalidKeyException;
35  import java.security.MessageDigest;
36  import java.util.Arrays;
37  
38  /**
39   * Decodes IPMI v2.0 session header and retrieves encrypted payload.
40   */
41  public class Protocolv20Decoder extends ProtocolDecoder {
42  
43  	/**
44  	 * Length of the session header without the OEM fields: AuthType/Format, Payload Type, Session ID, Session
45  	 * Sequence Number and Payload Length.
46  	 */
47  	private static final int HEADER_LENGTH = 12;
48  
49  	private CipherSuite cipherSuite;
50  
51  	/**
52  	 * Initiates IPMI v2.0 packet decoder.
53  	 *
54  	 * @param cipherSuite
55  	 *        - {@link CipherSuite} that will be used to decode the message
56  	 */
57  	public Protocolv20Decoder(CipherSuite cipherSuite) {
58  		super();
59  		this.cipherSuite = cipherSuite;
60  	}
61  
62  	/**
63  	 * Decodes IPMI v2.0 message fields.
64  	 *
65  	 * @param rmcpMessage
66  	 *        - RMCP message to decode.
67  	 * @return decoded message
68  	 * @see Ipmiv20Message
69  	 * @throws IllegalArgumentException
70  	 *         when delivered RMCP message does not contain encapsulated
71  	 *         IPMI message or when AuthCode field is incorrect (integrity
72  	 *         check fails).
73  	 * @throws InvalidKeyException
74  	 *         - when initiation of the integrity algorithm fails
75  	 */
76  	@Override
77  	public IpmiMessage decode(RmcpMessage rmcpMessage) throws InvalidKeyException {
78  		Ipmiv20Message message = new Ipmiv20Message(cipherSuite.getConfidentialityAlgorithm());
79  
80  		byte[] raw = rmcpMessage.getData();
81  
82  		checkLength(raw, HEADER_LENGTH);
83  
84  		message.setAuthenticationType(decodeAuthenticationType(raw[0]));
85  
86  		message.setPayloadEncrypted(decodeEncryption(raw[1]));
87  
88  		message.setPayloadAuthenticated(decodeAuthentication(raw[1]));
89  
90  		message.setPayloadType(decodePayloadType(raw[1]));
91  
92  		int offset = 2;
93  
94  		if (message.getPayloadType() == PayloadType.Oem) {
95  			checkLength(raw, HEADER_LENGTH + 6);
96  
97  			message.setOemIANA(decodeOEMIANA(raw));
98  			offset += 4;
99  
100 			message.setOemPayloadID(decodeOEMPayloadId(raw, offset));
101 			offset += 2;
102 		}
103 
104 		message.setSessionID(decodeSessionID(raw, offset));
105 		offset += 4;
106 
107 		message.setSessionSequenceNumber(decodeSessionSequenceNumber(raw, offset));
108 		offset += 4;
109 
110 		int payloadLength = decodePayloadLength(raw, offset);
111 		offset += 2;
112 
113 		checkLength(raw, offset + payloadLength);
114 
115 		// A packet of a session with integrity protection must carry a valid AuthCode, or it is discarded (IPMI 2.0
116 		// section 13.28.4). The AuthCode covers the encrypted payload: it is checked before decrypting it.
117 		if (message.getSessionID() != 0 && cipherSuite.getIntegrityAlgorithm().getCode() != SecurityConstants.IA_NONE) {
118 			if (!message.isPayloadAuthenticated()) {
119 				throw new IllegalArgumentException("Unauthenticated message in a session with integrity protection");
120 			}
121 			int authCodeOffset = skipIntegrityPAD(raw, offset + payloadLength);
122 			if (!validateAuthCode(raw, authCodeOffset)) {
123 				throw new IllegalArgumentException("Integrity check failed");
124 			}
125 			message.setAuthCode(decodeAuthCode(raw, authCodeOffset));
126 		}
127 
128 		if (message.isPayloadEncrypted()) {
129 			message.setPayload(
130 					decodePayload(raw, offset, payloadLength, message.getConfidentialityAlgorithm(), message.getPayloadType()));
131 		} else {
132 			message
133 					.setPayload(decodePayload(raw, offset, payloadLength, new ConfidentialityNone(), message.getPayloadType()));
134 		}
135 
136 		return message;
137 	}
138 
139 	/**
140 	 * Checks that the message holds at least the given number of bytes.
141 	 *
142 	 * @param rawMessage the message data
143 	 * @param length the minimum length
144 	 * @throws IllegalArgumentException when the message is shorter
145 	 */
146 	private static void checkLength(byte[] rawMessage, int length) {
147 		if (rawMessage.length < length) {
148 			throw new IllegalArgumentException("Message is truncated: " + rawMessage.length + " bytes, expected " + length);
149 		}
150 	}
151 
152 	/**
153 	 * Decodes first bit of Payload Type.
154 	 *
155 	 * @param payloadType
156 	 * @return True if payload is encrypted, false otherwise.
157 	 */
158 	private boolean decodeEncryption(byte payloadType) {
159 		return (payloadType & TypeConverter.intToByte(0x80)) != 0;
160 	}
161 
162 	/**
163 	 * Decodes second bit of Payload Type.
164 	 *
165 	 * @param payloadType
166 	 * @return True if payload is authenticated, false otherwise.
167 	 */
168 	public boolean decodeAuthentication(byte payloadType) {
169 		return (payloadType & TypeConverter.intToByte(0x40)) != 0;
170 	}
171 
172 	public static PayloadType decodePayloadType(byte payloadType) {
173 		return PayloadType.parseInt(TypeConverter.intToByte(payloadType & TypeConverter.intToByte(0x3f)));
174 	}
175 
176 	/**
177 	 * Decodes OEM IANA.
178 	 *
179 	 * @param rawMessage
180 	 *        - Byte array holding whole message data.
181 	 * @return OEM IANA number.
182 	 */
183 	private int decodeOEMIANA(byte[] rawMessage) {
184 		byte[] oemIANA = new byte[4];
185 
186 		System.arraycopy(rawMessage, 2, oemIANA, 0, 3);
187 		oemIANA[3] = 0;
188 
189 		return TypeConverter.littleEndianByteArrayToInt(oemIANA);
190 	}
191 
192 	/**
193 	 * Decodes OEM payload ID. To implement manufacturer-specific OEM Payload ID
194 	 * decoding, override this function.
195 	 *
196 	 * @param rawMessage
197 	 *        - Byte array holding whole message data.
198 	 * @param offset
199 	 *        - Offset to OEM payload ID in header.
200 	 * @return Decoded OEM payload ID.
201 	 */
202 	protected Object decodeOEMPayloadId(byte[] rawMessage, int offset) {
203 		byte[] oemPayload = new byte[2];
204 
205 		System.arraycopy(rawMessage, offset, oemPayload, 0, 2);
206 
207 		return oemPayload;
208 	}
209 
210 	@Override
211 	protected int decodePayloadLength(byte[] rawData, int offset) {
212 		byte[] payloadLength = new byte[4];
213 		System.arraycopy(rawData, offset, payloadLength, 0, 2);
214 		payloadLength[2] = 0;
215 		payloadLength[3] = 0;
216 
217 		return TypeConverter.littleEndianByteArrayToInt(payloadLength);
218 	}
219 
220 	/**
221 	 * Skips the integrity pad and pad length fields.
222 	 *
223 	 * @param rawMessage
224 	 *        - Byte array holding whole message data.
225 	 * @param offset
226 	 *        - Offset to integrity pad.
227 	 * @return Offset to Auth Code
228 	 * @throws IllegalArgumentException
229 	 *         when message is corrupted and pad length does not appear
230 	 *         after integrity pad or length is incorrect.
231 	 */
232 	private int skipIntegrityPAD(final byte[] rawMessage, final int offset) {
233 		// Integrity PAD: 0 to 3 FFh bytes, then the Pad Length and the Next Header (07h)
234 		int skip = 0;
235 		while (skip < 3
236 				&& offset + skip < rawMessage.length
237 				&& TypeConverter.byteToInt(rawMessage[offset + skip]) == 0xff) {
238 			++skip;
239 		}
240 
241 		int currentOffset = offset + skip + 2; // skip pad length and next header fields
242 		if (currentOffset >= rawMessage.length || TypeConverter.byteToInt(rawMessage[offset + skip]) != skip) {
243 			throw new IllegalArgumentException("Message is corrupted.");
244 		}
245 		return currentOffset;
246 	}
247 
248 	/**
249 	 * Decodes the Auth Code.
250 	 *
251 	 * @param rawMessage
252 	 *        - Byte array holding whole message data.
253 	 * @param offset
254 	 *        - Offset to auth code.
255 	 * @return Auth Code
256 	 */
257 	private byte[] decodeAuthCode(byte[] rawMessage, int offset) {
258 		byte[] authCode = new byte[rawMessage.length - offset];
259 		System.arraycopy(rawMessage, offset, authCode, 0, authCode.length);
260 		return authCode;
261 	}
262 
263 	/**
264 	 * Checks if Auth Code of the received message is valid
265 	 *
266 	 * @param rawMessage
267 	 *        - received message
268 	 * @param offset
269 	 *        - offset to the AuthCode field in the message
270 	 * @return True if AuthCode is correct, false otherwise.
271 	 * @throws InvalidKeyException
272 	 *         - when initiation of the integrity algorithm fails
273 	 */
274 	private boolean validateAuthCode(byte[] rawMessage, int offset) {
275 		return MessageDigest.isEqual(
276 				decodeAuthCode(rawMessage, offset),
277 				cipherSuite.getIntegrityAlgorithm().generateAuthCode(Arrays.copyOf(rawMessage, offset)));
278 	}
279 
280 	/**
281 	 * Decodes session ID.
282 	 *
283 	 * @param message
284 	 *        - message to get session ID from
285 	 * @return Session ID.
286 	 */
287 	public static int decodeSessionID(RmcpMessage message) {
288 		int offset = 2;
289 		if (decodePayloadType(message.getData()[1]) == PayloadType.Oem) {
290 			offset += 6;
291 		}
292 		return decodeSessionID(message.getData(), offset);
293 	}
294 }