View Javadoc
1   package org.metricshub.ipmi.core.coding.commands.session;
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.commands.IpmiCommandCoder;
26  import org.metricshub.ipmi.core.coding.commands.IpmiVersion;
27  import org.metricshub.ipmi.core.coding.commands.PrivilegeLevel;
28  import org.metricshub.ipmi.core.coding.payload.IpmiPayload;
29  import org.metricshub.ipmi.core.coding.payload.PlainMessage;
30  import org.metricshub.ipmi.core.coding.payload.lan.IPMIException;
31  import org.metricshub.ipmi.core.coding.payload.lan.NetworkFunction;
32  import org.metricshub.ipmi.core.coding.protocol.AuthenticationType;
33  import org.metricshub.ipmi.core.coding.protocol.IpmiMessage;
34  import org.metricshub.ipmi.core.coding.protocol.Ipmiv20Message;
35  import org.metricshub.ipmi.core.coding.protocol.PayloadType;
36  import org.metricshub.ipmi.core.coding.security.CipherSuite;
37  import org.metricshub.ipmi.core.coding.security.ConfidentialityNone;
38  import org.metricshub.ipmi.core.common.Randomizer;
39  import org.metricshub.ipmi.core.common.TypeConverter;
40  
41  import java.nio.charset.StandardCharsets;
42  import java.security.InvalidKeyException;
43  import java.security.NoSuchAlgorithmException;
44  import java.util.Arrays;
45  
46  /**
47   * <p>
48   * A wrapper for RMCP+ RAKP1 message and it's response - RAKP2 message. The same
49   * instance of this class that was used to prepare RAKP Message 1 should be also
50   * used to decode matching RAKP Message 2 since the generated random number is
51   * used in encryption process.
52   * </p>
53   * <p>
54   * Capable of calculating SIK (Session Integrity Key) when RAKP Message 2 is
55   * given ({@link #calculateSik(Rakp1ResponseData)}).
56   * </p>
57   */
58  public class Rakp1 extends IpmiCommandCoder<Rakp1ResponseData> {
59  
60  	/**
61  	 * Maximum length of the password and of the BMC key (Kg) in IPMI v2.0, in bytes (IPMI 2.0 tables 22-30 and
62  	 * 22-35).
63  	 */
64  	private static final int MAX_KEY_LENGTH = 20;
65  
66  	/**
67  	 * Maximum length of the user name, in bytes (IPMI 2.0 table 13-11).
68  	 */
69  	private static final int MAX_USERNAME_LENGTH = 16;
70  
71  	/**
72  	 * The Managed System's Session ID for this session. Must be as returned by
73  	 * the Managed System in the Open Session Response message.
74  	 */
75  	private int managedSystemSessionId;
76  
77  	/**
78  	 * The random number generated by console.
79  	 */
80  	private byte[] consoleRandomNumber;
81  
82  	private PrivilegeLevel requestedMaximumPrivilegeLevel;
83  
84  	/**
85  	 * ASCII character Name that the user at the Remote Console wishes to assume
86  	 * for this session. It's length cannot exceed 16.
87  	 */
88  	private String username;
89  
90  	/**
91  	 * Password matching username, as sent to the BMC (at most 20 bytes).
92  	 */
93  	private byte[] password;
94  
95  	/**
96  	 * Kg key associated with the target BMC. Should be null if Get Channel
97  	 * Authentication Capabilities Response indicated that Kg is disabled which
98  	 * means that 'one-key' logins are being used (
99  	 * {@link GetChannelAuthenticationCapabilitiesResponseData#isKgEnabled()} ==
100 	 * false)
101 	 */
102 	private byte[] bmcKey;
103 
104 	public void setManagedSystemSessionId(int managedSystemSessionId) {
105 		this.managedSystemSessionId = managedSystemSessionId;
106 	}
107 
108 	public int getManagedSystemSessionId() {
109 		return managedSystemSessionId;
110 	}
111 
112 	public void setRequestedMaximumPrivilegeLevel(PrivilegeLevel requestedMaximumPrivilegeLevel) {
113 		this.requestedMaximumPrivilegeLevel = requestedMaximumPrivilegeLevel;
114 	}
115 
116 	public PrivilegeLevel getRequestedMaximumPrivilegeLevel() {
117 		return requestedMaximumPrivilegeLevel;
118 	}
119 
120 	public void setUsername(String username) {
121 		checkLength("Username", username.getBytes(StandardCharsets.UTF_8), MAX_USERNAME_LENGTH);
122 		this.username = username;
123 	}
124 
125 	/**
126 	 * Checks the credentials of a session against what a BMC stores: a user name of at most 16 bytes in UTF-8, a
127 	 * password and a BMC key (Kg) of at most 20 bytes.
128 	 *
129 	 * @param username the user name
130 	 * @param password the password as sent to the BMC, or null
131 	 * @param bmcKey the BMC key, or null
132 	 * @throws IllegalArgumentException when one of them is too long
133 	 */
134 	public static void checkCredentials(String username, byte[] password, byte[] bmcKey) {
135 		checkLength("Username", username.getBytes(StandardCharsets.UTF_8), MAX_USERNAME_LENGTH);
136 		checkLength("Password", password, MAX_KEY_LENGTH);
137 		checkLength("BMC key", bmcKey, MAX_KEY_LENGTH);
138 	}
139 
140 	private static void checkLength(String name, byte[] value, int maxLength) {
141 		if (value != null && value.length > maxLength) {
142 			throw new IllegalArgumentException(name + " is too long. Its length cannot exceed " + maxLength + " bytes");
143 		}
144 	}
145 
146 	public String getUsername() {
147 		return username;
148 	}
149 
150 	/**
151 	 * @return the user name as sent to the BMC, encoded in UTF-8
152 	 */
153 	byte[] getUsernameBytes() {
154 		return username.getBytes(StandardCharsets.UTF_8);
155 	}
156 
157 	private void setPassword(byte[] password) {
158 		checkLength("Password", password, MAX_KEY_LENGTH);
159 		this.password = password;
160 	}
161 
162 	/**
163 	 * @return the password as the key of the authentication algorithm (Kuid): the array of the caller, not a copy,
164 	 *         since the HMAC zero-pads its key as the BMC stores it (20 bytes); an empty array for no password
165 	 */
166 	byte[] getPasswordBytes() {
167 		return password == null ? new byte[0] : password;
168 	}
169 
170 	private void setConsoleRandomNumber(byte[] randomNumber) {
171 		this.consoleRandomNumber = randomNumber;
172 	}
173 
174 	public byte[] getConsoleRandomNumber() {
175 		return consoleRandomNumber;
176 	}
177 
178 	private void setBmcKey(byte[] bmcKey) {
179 		checkLength("BMC key", bmcKey, MAX_KEY_LENGTH);
180 		this.bmcKey = bmcKey;
181 	}
182 
183 	public byte[] getBmcKey() {
184 		return bmcKey;
185 	}
186 
187 	/**
188 	 * Initiates class for encoding and decoding. Sets IPMI version to
189 	 * {@link IpmiVersion#V20} since RAKP1 is a RMCP+ command. Sets
190 	 * Authentication Type to RMCP+.
191 	 *
192 	 * @param managedSystemSessionId
193 	 *        - The Managed System's Session ID for this session. Must be as
194 	 *        returned by the Managed System in the Open Session Response
195 	 *        message.
196 	 * @param privilegeLevel
197 	 *        - Requested Maximum {@link PrivilegeLevel}
198 	 * @param username
199 	 *        - ASCII character Name that the user at the Remote Console
200 	 *        wishes to assume for this session. It's length cannot exceed
201 	 *        16.
202 	 * @param password
203 	 *        - password matching username, as sent to the BMC (UTF-8 for a text password), at most 20 bytes
204 	 * @param bmcKey
205 	 *        - BMC specific key. Should be null if Get Channel
206 	 *        Authentication Capabilities Response indicated that Kg is
207 	 *        disabled which means that 'one-key' logins are being used (
208 	 *        {@link GetChannelAuthenticationCapabilitiesResponseData#isKgEnabled()}
209 	 *        == false)
210 	 * @param cipherSuite
211 	 *        - {@link CipherSuite} containing authentication,
212 	 *        confidentiality and integrity algorithms for this session.
213 	 */
214 	public Rakp1(int managedSystemSessionId, PrivilegeLevel privilegeLevel, String username, byte[] password,
215 			byte[] bmcKey, CipherSuite cipherSuite) {
216 		setSessionParameters(IpmiVersion.V20, cipherSuite, AuthenticationType.RMCPPlus);
217 		setManagedSystemSessionId(managedSystemSessionId);
218 		setRequestedMaximumPrivilegeLevel(privilegeLevel);
219 		setUsername(username);
220 		setPassword(password);
221 		this.setBmcKey(bmcKey);
222 
223 		setConsoleRandomNumber(Randomizer.getBytes(16));
224 	}
225 
226 	@Override
227 	public IpmiMessage encodePayload(int messageSequenceNumber, int sessionSequenceNumber, int sessionId) {
228 		if (sessionId != 0) {
229 			throw new IllegalArgumentException("Session ID must be 0");
230 		}
231 		Ipmiv20Message message = new Ipmiv20Message(new ConfidentialityNone());
232 
233 		message.setPayloadType(PayloadType.Rakp1);
234 		message.setSessionID(0);
235 		message.setSessionSequenceNumber(0);
236 		message.setAuthenticationType(getAuthenticationType());
237 		message.setPayloadAuthenticated(false);
238 		message.setPayloadEncrypted(false);
239 
240 		message.setPayload(preparePayload(messageSequenceNumber));
241 
242 		return message;
243 	}
244 
245 	@Override
246 	protected IpmiPayload preparePayload(int sequenceNumber) {
247 		byte[] usernameBytes = getUsernameBytes();
248 		byte[] payload = new byte[28 + usernameBytes.length];
249 
250 		// message tag
251 		payload[0] = TypeConverter.intToByte(sequenceNumber);
252 
253 		payload[1] = 0; // reserved
254 		payload[2] = 0; // reserved
255 		payload[3] = 0; // reserved
256 
257 		byte[] sId = TypeConverter.intToLittleEndianByteArray(getManagedSystemSessionId());
258 
259 		System.arraycopy(sId, 0, payload, 4, 4); // managed system session ID
260 
261 		System.arraycopy(consoleRandomNumber, 0, payload, 8, 16); // generated
262 																															// random
263 		// number
264 
265 		// requested privilege level; set name-only lookup
266 		payload[24] = TypeConverter.intToByte(encodePrivilegeLevel(requestedMaximumPrivilegeLevel) | 0x10);
267 
268 		payload[25] = 0; // reserved
269 		payload[26] = 0; // reserved
270 
271 		payload[27] = TypeConverter.intToByte(usernameBytes.length); // username length
272 		System.arraycopy(usernameBytes, 0, payload, 28, usernameBytes.length); // username
273 
274 		return new PlainMessage(payload);
275 	}
276 
277 	@Override
278 	public byte getCommandCode() {
279 		return 0;
280 	}
281 
282 	@Override
283 	public NetworkFunction getNetworkFunction() {
284 		return null;
285 	}
286 
287 	/**
288 	 * @throws IllegalArgumentException
289 	 *         when message is not a response for class-specific command,
290 	 *         response has invalid length or authentication check fails.
291 	 * @throws NoSuchAlgorithmException
292 	 *         - when authentication, confidentiality or integrity algorithm
293 	 *         fails.
294 	 * @throws InvalidKeyException
295 	 *         - when creating of the algorithm key fails
296 	 */
297 	@Override
298 	public Rakp1ResponseData getResponseData(IpmiMessage message)
299 			throws IPMIException,
300 			NoSuchAlgorithmException,
301 			InvalidKeyException {
302 		if (!isCommandResponse(message)) {
303 			throw new IllegalArgumentException("This is not RAKP 2 message!");
304 		}
305 
306 		byte[] payload = validateSessionSetupResponse(message);
307 
308 		Rakp1ResponseData data = new Rakp1ResponseData();
309 
310 		data.setMessageTag(payload[0]);
311 
312 		data.setStatusCode(payload[1]);
313 
314 		int length = getCipherSuite().getAuthenticationAlgorithm().getKeyLength();
315 
316 		if (payload.length < 40 + length) {
317 			throw new IllegalArgumentException("Invalid payload length");
318 		}
319 
320 		byte[] buffer = new byte[4];
321 
322 		System.arraycopy(payload, 4, buffer, 0, 4);
323 
324 		data.setRemoteConsoleSessionId(TypeConverter.littleEndianByteArrayToInt(buffer));
325 
326 		byte[] managedSystemGuid = new byte[16];
327 
328 		System.arraycopy(payload, 24, managedSystemGuid, 0, 16);
329 
330 		data.setManagedSystemGuid(managedSystemGuid);
331 
332 		byte[] managedSystemRandomNumber = new byte[16];
333 
334 		System.arraycopy(payload, 8, managedSystemRandomNumber, 0, 16);
335 
336 		data.setManagedSystemRandomNumber(managedSystemRandomNumber);
337 
338 		byte[] key = Arrays.copyOfRange(payload, 40, 40 + length);
339 
340 		if (!getCipherSuite().getAuthenticationAlgorithm()
341 				.checkKeyExchangeAuthenticationCode(prepareKeyExchangeAuthenticationCodeBase(data), key, getPasswordBytes())) {
342 			throw new AuthenticationException("Authentication check failed");
343 		}
344 
345 		return data;
346 	}
347 
348 	/**
349 	 * @return byte array holding prepared base for calculating
350 	 *         KeyExchangeAuthenticationCode for RAKP Message 2
351 	 */
352 	private byte[] prepareKeyExchangeAuthenticationCodeBase(Rakp1ResponseData responseData) {
353 		byte[] usernameBytes = getUsernameBytes();
354 		byte[] keac = new byte[58 + usernameBytes.length];
355 
356 		byte[] rSID = TypeConverter.intToLittleEndianByteArray(responseData.getRemoteConsoleSessionId());
357 
358 		System.arraycopy(rSID, 0, keac, 0, 4);
359 
360 		byte[] mSID = TypeConverter.intToLittleEndianByteArray(getManagedSystemSessionId());
361 
362 		System.arraycopy(mSID, 0, keac, 4, 4);
363 
364 		System.arraycopy(getConsoleRandomNumber(), 0, keac, 8, 16);
365 
366 		System.arraycopy(responseData.getManagedSystemRandomNumber(), 0, keac, 24, 16);
367 
368 		System.arraycopy(responseData.getManagedSystemGuid(), 0, keac, 40, 16);
369 
370 		keac[56] = TypeConverter.intToByte(encodePrivilegeLevel(requestedMaximumPrivilegeLevel) | 0x10);
371 
372 		keac[57] = TypeConverter.intToByte(usernameBytes.length);
373 		System.arraycopy(usernameBytes, 0, keac, 58, usernameBytes.length);
374 
375 		return keac;
376 	}
377 
378 	/**
379 	 * Calculates SIK (Session Integrity Key) based on RAKP Messages 1 and 2
380 	 *
381 	 * @param responseData
382 	 *        RAKP Message 2 data
383 	 * @return Session Integrity Key
384 	 * @throws NoSuchAlgorithmException
385 	 *         - when authentication, confidentiality or integrity algorithm
386 	 *         fails.
387 	 * @throws InvalidKeyException
388 	 *         - when creating of the algorithm key fails
389 	 */
390 	public byte[] calculateSik(Rakp1ResponseData responseData) throws InvalidKeyException, NoSuchAlgorithmException {
391 		byte[] key = null;
392 		// An all-zero Kg is the default value, which means one-key logins (IPMI 2.0 section 13.33)
393 		if (getBmcKey() == null || Arrays.equals(getBmcKey(), new byte[getBmcKey().length])) {
394 			key = getPasswordBytes();
395 		} else {
396 			key = getBmcKey();
397 		}
398 
399 		return getCipherSuite().getAuthenticationAlgorithm()
400 				.getKeyExchangeAuthenticationCode(prepareSikBase(responseData), key);
401 	}
402 
403 	/**
404 	 * @return byte array holding prepared base for calculating Session
405 	 *         Integrity Key
406 	 */
407 	private byte[] prepareSikBase(Rakp1ResponseData responseData) {
408 		byte[] usernameBytes = getUsernameBytes();
409 		byte[] sikBase = new byte[34 + usernameBytes.length];
410 
411 		System.arraycopy(getConsoleRandomNumber(), 0, sikBase, 0, 16);
412 
413 		System.arraycopy(responseData.getManagedSystemRandomNumber(), 0, sikBase, 16, 16);
414 
415 		sikBase[32] = TypeConverter.intToByte(encodePrivilegeLevel(requestedMaximumPrivilegeLevel) | 0x10);
416 
417 		sikBase[33] = TypeConverter.intToByte(usernameBytes.length);
418 		System.arraycopy(usernameBytes, 0, sikBase, 34, usernameBytes.length);
419 
420 		return sikBase;
421 	}
422 
423 	@Override
424 	public boolean isCommandResponse(IpmiMessage message) {
425 		return message instanceof Ipmiv20Message && ((Ipmiv20Message) message).getPayloadType() == PayloadType.Rakp2;
426 	}
427 }