OSSerializer.java

/*******************************************************************************
 * Copyright (c) 2018 RISE SICS and others.
 * 
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v2.0
 * and Eclipse Distribution License v1.0 which accompany this distribution.
 * 
 * The Eclipse Public License is available at
 *    http://www.eclipse.org/legal/epl-v20.html
 * and the Eclipse Distribution License is available at
 *    http://www.eclipse.org/org/documents/edl-v10.html.
 * 
 * Contributors:
 *    Joakim Brorsson
 *    Ludwig Seitz (RISE SICS)
 *    Tobias Andersson (RISE SICS)
 *    Rikard Höglund (RISE SICS)
 *    
 ******************************************************************************/
package org.eclipse.californium.oscore;

import java.nio.ByteBuffer;

import org.eclipse.californium.core.coap.CoAP;
import org.eclipse.californium.core.coap.Message;
import org.eclipse.californium.core.coap.OptionSet;
import org.eclipse.californium.core.coap.Request;
import org.eclipse.californium.core.network.serialization.DataSerializer;
import org.eclipse.californium.cose.AlgorithmID;
import org.eclipse.californium.elements.util.Bytes;
import org.eclipse.californium.elements.util.DatagramWriter;
import org.eclipse.californium.oscore.group.GroupRecipientCtx;
import org.eclipse.californium.oscore.group.GroupSenderCtx;
import org.eclipse.californium.oscore.group.OptionEncoder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.upokecenter.cbor.CBORObject;

import net.i2p.crypto.eddsa.Utils;

/**
 * 
 * Implements methods for serializing OSCORE data, creating AAD, reading data
 * and generating nonce.
 *
 */
public class OSSerializer {

	private static final byte[] ONE_ZERO = new byte[] { 0x00 };

	/**
	 * The logger
	 */
	private static final Logger LOGGER = LoggerFactory.getLogger(OSSerializer.class);

	/**
	 * Prepare options and payload for encrypting.
	 * 
	 * @param options the options
	 * 
	 * @param payload the payload
 	 * @param realCode the actual code of the message
	 * 
	 * @return the serialized plaintext for OSCore
	 */
	public static byte[] serializeConfidentialData(OptionSet options, byte[] payload, int realCode) {
		if (options != null) {
			DatagramWriter writer = new DatagramWriter();
			if (realCode > 0) {
				OptionSet filteredOptions = OptionJuggle.prepareEoptions(options);
				writer.write(realCode, CoAP.MessageFormat.CODE_BITS);
				DataSerializer.serializeOptionsAndPayload(writer, filteredOptions, payload);
				return writer.toByteArray();
			} else {
				LOGGER.error(ErrorDescriptions.COAP_CODE_INVALID);
				throw new IllegalArgumentException(ErrorDescriptions.COAP_CODE_INVALID);
			}
		} else {
			LOGGER.error(ErrorDescriptions.OPTIONSET_NULL);
			throw new NullPointerException(ErrorDescriptions.OPTIONSET_NULL);
		}
	}

	/**
	 * Prepare the additional authenticated data of a message.
	 * 
	 * Note that for the request* parameters they must contain the value of what was in
	 * a request. Either this actual request or the request associated to this response. 
	 * 
	 * external_aad = [ ver : uint, alg : int, request_kid : bstr, request_piv :
	 * bstr, options : bstr]
	 * 
	 * @param version the CoAP version number
	 * @param algorithm AEAD algorithm
	 * @param requestSeq the sequence number (request PIV)
	 * @param requestSenderId sender ID (request KID)
	 * @param options the option set
	 * @return byte array with AAD
	 */
	public static byte[] serializeAAD(int version, AlgorithmID algorithm, int requestSeq, byte[] requestSenderId, OptionSet options) {
		if (version == CoAP.VERSION) {
			if (requestSeq > -1) {
				if (algorithm != null) {
					if (options != null) {
						CBORObject algorithms = CBORObject.NewArray();
						algorithms.Add(algorithm.AsCBOR());

						CBORObject aad = CBORObject.NewArray();
						aad.Add(version);
						aad.Add(algorithms);
						aad.Add(requestSenderId);
						aad.Add(processPartialIV(requestSeq));
						
						//I-class options (currently none)
						aad.Add(CBORObject.FromObject(Bytes.EMPTY));
						
						return aad.EncodeToBytes();
					} else {
						LOGGER.error(ErrorDescriptions.OPTIONSET_NULL);
						throw new NullPointerException(ErrorDescriptions.OPTIONSET_NULL);
					}
				} else {
					LOGGER.error(ErrorDescriptions.ALGORITHM_NOT_DEFINED);
					throw new NullPointerException(ErrorDescriptions.ALGORITHM_NOT_DEFINED);
				}
			} else {
				LOGGER.error(ErrorDescriptions.SEQ_NBR_INVALID);
				throw new IllegalArgumentException(ErrorDescriptions.SEQ_NBR_INVALID);
			}
		} else {
			LOGGER.error(ErrorDescriptions.WRONG_VERSION_NBR);
			throw new IllegalArgumentException(ErrorDescriptions.WRONG_VERSION_NBR);
		}
	}

	/**
	 * Generates the nonce.
	 * 
	 * Note that that if a response does not include a partial IV the nonce will be 
	 * generated using parameters from the corresponding original request.
	 * 
	 * See https://tools.ietf.org/html/draft-ietf-core-object-security-16#section-5.2
	 * 
	 * @param partialIV partial IV to calculate nonce with (from original request or response)
	 * @param senderID sender ID of message (either original request or response)
	 * @param commonIV common IV shared between sender and recipient
	 * @param nonceLength the algorithm dependent length of nonce
	 * @return the generated nonce or null if either one of the input parameters
	 *         are null
	 * @throws OSException if any of the parameters are invalid
	 */
	public static byte[] nonceGeneration(byte[] partialIV, byte[] senderID, byte[] commonIV, int nonceLength)
			throws OSException {
		if (partialIV != null) {
			if (senderID != null) {
				if (commonIV != null) {
					if (nonceLength > 0) {
						int s = senderID.length;
						int zeroes = 5 - partialIV.length;

						if (zeroes > 0) {
							partialIV = leftPaddingZeroes(partialIV, zeroes);
						}

						zeroes = (nonceLength - 6) - senderID.length;

						if (zeroes > 0) {
							senderID = leftPaddingZeroes(senderID, zeroes);
						}

						zeroes = nonceLength - commonIV.length;

						if (zeroes > 0) {
							commonIV = leftPaddingZeroes(commonIV, zeroes);
						}

						byte[] tmp = new byte[1 + senderID.length + partialIV.length];
						tmp[0] = (byte) s;
						System.arraycopy(senderID, 0, tmp, 1, senderID.length);
						System.arraycopy(partialIV, 0, tmp, senderID.length + 1, partialIV.length);

						byte[] result = new byte[commonIV.length];

						int i = 0;
						for (byte b : tmp) {
							result[i] = (byte) (b ^ commonIV[i++]);
						}

						return result;
					} else {
						LOGGER.error(ErrorDescriptions.NONCE_LENGTH_INVALID);
						throw new IllegalArgumentException(ErrorDescriptions.NONCE_LENGTH_INVALID);
					}
				} else {
					LOGGER.error(ErrorDescriptions.COMMON_IV_NULL);
					throw new NullPointerException(ErrorDescriptions.COMMON_IV_NULL);
				}
			} else {
				LOGGER.error(ErrorDescriptions.SENDER_ID_NULL);
				throw new NullPointerException(ErrorDescriptions.SENDER_ID_NULL);
			}
		} else {
			LOGGER.error(ErrorDescriptions.PARTIAL_IV_NULL);
			throw new NullPointerException(ErrorDescriptions.PARTIAL_IV_NULL);
		}
	}

	/**
	 * Padds the left side of the byte array paddMe with zeros as the int zeros
	 * has
	 * 
	 * @param paddMe byte array to pad
	 * @param zeros number of zeroes to pad with
	 * @return the left-padded byte array
	 */
	public static byte[] leftPaddingZeroes(byte[] paddMe, int zeros) {
		byte[] tmp = new byte[zeros + paddMe.length];
		System.arraycopy(paddMe, 0, tmp, zeros, paddMe.length);
		return tmp;
	}

	/**
	 * Processes a partialIV correctly
	 * 
	 * @param value the partialIV
	 * @return the processed partialIV
	 */
	public static byte[] processPartialIV(int value) {
		byte[] partialIV = ByteBuffer.allocate(Decryptor.INTEGER_BYTES).putInt(value).array();
		return stripZeroes(partialIV);
	}

	/**
	 * Remove trailing zeroes in a byte array
	 * 
	 * @param in the incoming array
	 * @return the array with trailing zeroes removed
	 */
	public static byte[] stripZeroes(byte[] in) {
		if (in != null) {
			if (in.length == 0) {
				return Bytes.EMPTY;
			}
			if (in.length == 1)
				return in;

			int firstValue = 0;

			while (firstValue < in.length && in[firstValue] == 0) {
				firstValue++;
			}

			int newLength = in.length - firstValue;

			if (newLength == 0) {
				return ONE_ZERO;
			}

			byte[] out = new byte[newLength];
			System.arraycopy(in, firstValue, out, 0, out.length);

			return out;
		} else {
			LOGGER.error(ErrorDescriptions.BYTE_ARRAY_NULL);
			throw new NullPointerException(ErrorDescriptions.BYTE_ARRAY_NULL);
		}
	}

	/**
	 * Update the external AAD for Group OSCORE by adding further parameters.
	 * TODO: Add also newPartialIV as input parameter.
	 * 
	 * @param ctx the context used
	 * @param aadBytes the current external AAD value
	 * @param message the CoAP message being processed
	 * @return the updated external AAD
	 */
	public static byte[] updateAADForGroup(OSCoreCtx ctx, byte[] aadBytes, Message message) {

		CBORObject algSign;
		CBORObject algSignEnc;
		CBORObject algKeyAgreement;

		byte[] senderPublicKey;
		byte[] gmPublicKey;

		if (ctx instanceof GroupRecipientCtx) {
			GroupRecipientCtx recipientCtx = (GroupRecipientCtx) ctx;
			algSign = recipientCtx.getAlgSign().AsCBOR();
			algSignEnc = recipientCtx.getAlgSignEnc().AsCBOR();
			algKeyAgreement = recipientCtx.getAlgKeyAgreement().AsCBOR();
			senderPublicKey = recipientCtx.getPublicKeyRaw();
			gmPublicKey = recipientCtx.getCommonCtx().getGmPublicKey();
		} else {
			GroupSenderCtx senderCtx = (GroupSenderCtx) ctx;
			algSign = senderCtx.getAlgSign().AsCBOR();
			algSignEnc = senderCtx.getAlgSignEnc().AsCBOR();
			algKeyAgreement = senderCtx.getAlgKeyAgreement().AsCBOR();
			senderPublicKey = senderCtx.getPublicKeyRaw();
			gmPublicKey = senderCtx.getCommonCtx().getGmPublicKey();
		}

		CBORObject groupAadEnc = CBORObject.DecodeFromBytes(aadBytes);

		// Build index 1 which holds the algorithms array
		CBORObject algorithms = groupAadEnc.get(1);
		algorithms.Add(algSignEnc);
		algorithms.Add(algSign);
		algorithms.Add(algKeyAgreement);

		// Add update algorithms array to external AAD (used for encryption)
		groupAadEnc.set(1, algorithms);

		// Add request_kid_context //FIXME
		if (ctx.getIdContext() == null || ctx.getIdContext().length == 0) {
			groupAadEnc.Add(CBORObject.FromObject(Bytes.EMPTY));
		} else {
			CBORObject requestKidContext = CBORObject.FromObject(ctx.getIdContext());
			groupAadEnc.Add(requestKidContext);
		}

		// Adding OSCORE option

		byte[] oscoreOption = message.getOptions().getOscore();

		// Check if this is an outgoing message //TODO: Check with option null?
		boolean outgoing = message.getSourceContext() == null;

		if (outgoing) {

			if (message instanceof Request) {
				boolean groupModeRequest = OptionEncoder.getPairwiseMode(oscoreOption) == false;
				oscoreOption = Encryptor.encodeOSCoreRequest(ctx, groupModeRequest);
			} else {
				boolean newPartialIV = ctx.getResponsesIncludePartialIV() || message.getOptions().hasObserve();
				oscoreOption = Encryptor.encodeOSCoreResponse(ctx, newPartialIV);
			}
		}

		// Actually add OSCORE option to external AAD
		groupAadEnc.Add(oscoreOption);

		// Add the sender public key
		// System.out.println("Sender public key: " +
		// Utils.bytesToHex(senderPublicKey));
		groupAadEnc.Add(CBORObject.FromObject(senderPublicKey));

		// Add the Group Manager's public key
		// System.out.println("gmPublicKey: " + Utils.bytesToHex(gmPublicKey));
		if (gmPublicKey == null || gmPublicKey.length == 0) {
			groupAadEnc.Add(CBORObject.Null);
		} else {
			groupAadEnc.Add(gmPublicKey);
		}

		return groupAadEnc.EncodeToBytes();
	}

}