ObjectSecurityLayer.java

/*******************************************************************************
 * Copyright (c) 2019 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
 *    Tobias Andersson (RISE SICS)
 *    Rikard Höglund (RISE SICS)
 *    
 ******************************************************************************/
package org.eclipse.californium.oscore;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.eclipse.californium.core.coap.EmptyMessage;
import org.eclipse.californium.core.coap.Message;
import org.eclipse.californium.core.coap.MessageObserverAdapter;
import org.eclipse.californium.core.coap.OptionNumberRegistry;
import org.eclipse.californium.core.coap.OptionSet;
import org.eclipse.californium.core.coap.Request;
import org.eclipse.californium.core.coap.Response;
import org.eclipse.californium.core.coap.Token;

import org.eclipse.californium.core.coap.CoAP.ResponseCode;
import org.eclipse.californium.core.network.Exchange;
import org.eclipse.californium.core.network.stack.AbstractLayer;
import org.eclipse.californium.elements.util.Bytes;
import org.eclipse.californium.oscore.ContextRederivation.PHASE;
import org.eclipse.californium.oscore.group.OptionEncoder;

/**
 * 
 * Applies OSCORE mechanics at stack layer.
 *
 */
public class ObjectSecurityLayer extends AbstractLayer {

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

	private final OSCoreCtxDB ctxDb;

	public ObjectSecurityLayer(OSCoreCtxDB ctxDb) {
		if (ctxDb == null) {
			throw new NullPointerException("OSCoreCtxDB must be provided!");
		}
		this.ctxDb = ctxDb;
	}

	/**
	 * Encrypt an outgoing request using the OSCore context.
	 * 
	 * @param message the message
	 * @param ctxDb the context database used
	 * 
	 * @return the encrypted message
	 * 
	 * @throws OSException error while encrypting request
	 */
	public static Request prepareSend(OSCoreCtxDB ctxDb, Request message) throws OSException {
		return RequestEncryptor.encrypt(ctxDb, message);
	}

	/**
	 * Encrypt an outgoing response using the OSCore context.
	 * 
	 * @param ctxDb the OSCore context DB
	 * @param message the message
	 * @param ctx the OSCore context
	 * @param newPartialIV boolean to indicate whether to use a new partial IV or not
	 * @param outerBlockwise boolean to indicate whether the block-wise options
	 *            should be encrypted or not
	 * @param requestOption the OSCORE option of the corresponding request
	 * 
	 * @return the encrypted message
	 * 
	 * @throws OSException error while encrypting response
	 */
	public static Response prepareSend(OSCoreCtxDB ctxDb, Response message, OSCoreCtx ctx, final boolean newPartialIV,
			boolean outerBlockwise, byte[] requestOption) throws OSException {
		return ResponseEncryptor.encrypt(ctxDb, message, ctx, newPartialIV, outerBlockwise, requestOption);
	}

	/**
	 * Decrypt an incoming request using the right OSCore context
	 *
	 * @param ctxDb the context database used
	 * @param request the incoming request
	 * @param ctx the OSCore context
	 * 
	 * @return the decrypted and verified request
	 * 
	 * @throws CoapOSException error while decrypting request
	 */
	public static Request prepareReceive(OSCoreCtxDB ctxDb, Request request, OSCoreCtx ctx) throws CoapOSException {
		return RequestDecryptor.decrypt(ctxDb, request, ctx);
	}

	/**
	 * Decrypt an incoming response using the right OSCore context
	 *
	 * @param ctxDb the context database used
	 * @param response the incoming request
	 * @return the decrypted and verified response
	 * 
	 * @throws OSException error while decrypting response
	 */
	public static Response prepareReceive(OSCoreCtxDB ctxDb, Response response) throws OSException {
		return ResponseDecryptor.decrypt(ctxDb, response);
	}

	@Override
	public void sendRequest(final Exchange exchange, final Request request) {
		Request req = request;
		if (shouldProtectRequest(request)) {
			try {
				// Handle outgoing requests for more data from a responder that
				// is responding with outer block-wise. These requests should
				// not be processed with OSCORE.
				boolean outerBlockwise = request.getOptions().hasBlock2() && exchange.getCurrentResponse() != null
						&& ctxDb.getContextByToken(exchange.getCurrentResponse().getToken()) != null;
				if (outerBlockwise) {
					super.sendRequest(exchange, req);
					return;
				}

				String uri = request.getURI();
				
				// Check if parameters in the option was set by the application
				if (request.getOptions().getOscore().length != 0) {
					// Use the URI from the option to find the correct context
					uri = OptionEncoder.getContextUri(request.getOptions().getOscore());
				}
				
				if (uri == null) {
					LOGGER.error(ErrorDescriptions.URI_NULL);
					throw new OSException(ErrorDescriptions.URI_NULL);
				}

				OSCoreCtx ctx = ctxDb.getContext(uri);

				if (ctx == null) {
					LOGGER.error(ErrorDescriptions.CTX_NULL);
					throw new OSException(ErrorDescriptions.CTX_NULL);
				}

				// Initiate context re-derivation procedure if flag is set
				if (ctx.getContextRederivationPhase() == PHASE.CLIENT_INITIATE) {
					throw new IllegalStateException("must be handled in ObjectSecurityContextLayer!");
				}

				/*
				 * Sets an operator on the exchange. This operator will in
				 * turn set information about the OSCORE context used in the
				 * endpoint context that will be created after the request is sent.
				 */
				OSCoreEndpointContextInfo.sendingRequest(ctx, exchange);

				final int seqByToken = ctx.getSenderSeq();

				final Request preparedRequest = prepareSend(ctxDb, request);
				final OSCoreCtx finalCtx = ctxDb.getContext(uri);

				exchange.setCryptographicContextID(preparedRequest.getOptions().getOscore());

				if (outgoingExceedsMaxUnfragSize(preparedRequest, outerBlockwise, ctx.getMaxUnfragmentedSize())) {
					throw new IllegalStateException("outgoing request is exceeding the MAX_UNFRAGMENTED_SIZE!");
				}

				preparedRequest.addMessageObserver(0, new MessageObserverAdapter() {

					// TODO: New observer for more Token handling?

					@Override
					public void onReadyToSend() {
						Token token = preparedRequest.getToken();

						// add at head of message observers to update
						// the token of the original request first,
						// before calling other message observers!
						if (request.getToken() == null) {
							request.setToken(token);
						}

						ctxDb.addContext(token, finalCtx);
						ctxDb.addSeqByToken(token, seqByToken);
					}
				});

				req = preparedRequest;

			} catch (OSException e) {
				LOGGER.error("Error sending request: " + e.getMessage());
				return;
			} catch (IllegalArgumentException e) {
				LOGGER.error("Unable to send request because of illegal argument: " + e.getMessage());
				return;
			}
		}
		LOGGER.info("Request: " + exchange.getRequest().toString());
		super.sendRequest(exchange, req);
	}

	@Override
	public void sendResponse(Exchange exchange, Response response) {
		/* If the request contained the Observe option always add a partial IV to the response.
		 * A partial IV will also be added if the responsesIncludePartialIV flag is set in the context. */
		boolean addPartialIV;
		
		/*
		 * If the original request used outer block-wise options so should the
		 * response. (They are not encrypted but external unprotected options.)
		 */
		boolean outerBlockwise;

		if (shouldProtectResponse(exchange)) {
			// If the current block-request still has a non-empty OSCORE option it
			// means it was not unprotected by OSCORE as and individual request.
			// Rather it was not processed by OSCORE until after being re-assembled
			// by the block-wise layer. Thus the response should use outer block options.
			outerBlockwise = exchange.getCurrentRequest().getOptions().hasOscore()
					&& exchange.getCurrentRequest().getOptions().getOscore().length != 0;

			try {
				// TODO: Don't just use Token as key, also use the addressing
				// information. Maybe consider using Token and RID + ID Context
				// instead. Need to change the map to take 3 things instead of
				// just Token.
				//
				// Or actually use the requestOption KID and KID Context
				// directly. (Indirectly using the Token).
				OSCoreCtx ctx = ctxDb.getContextByToken(exchange.getCurrentRequest().getToken());
				addPartialIV = ctx.getResponsesIncludePartialIV() || exchange.getRequest().getOptions().hasObserve();

				byte[] requestOption = exchange.getCryptographicContextID();
				Response preparedResponse = prepareSend(ctxDb, response, ctx, addPartialIV, outerBlockwise,
						requestOption);

				if (outgoingExceedsMaxUnfragSize(preparedResponse, outerBlockwise, ctx.getMaxUnfragmentedSize())) {
					super.sendResponse(exchange,
							Response.createResponse(exchange.getCurrentRequest(), ResponseCode.INTERNAL_SERVER_ERROR));
					throw new IllegalStateException("outgoing response is exceeding the MAX_UNFRAGMENTED_SIZE!");
				}

				response = preparedResponse;
				exchange.setResponse(response);
			} catch (OSException e) {
				LOGGER.error("Error sending response: " + e.getMessage());
				return;
			}
		}

		// Remove token after response is transmitted, unless ongoing Observe.
		// Takes token from corresponding request
		if (response.getOptions().hasObserve() == false || exchange.getRequest().isObserveCancel()) {
			ctxDb.removeToken(exchange.getCurrentRequest().getToken());
		}

		super.sendResponse(exchange, response);
	}

	@Override
	public void sendEmptyMessage(Exchange exchange, EmptyMessage message) {
		super.sendEmptyMessage(exchange, message);
	}

	@Override
	public void receiveRequest(Exchange exchange, Request request) {
		if (isProtected(request)) {

			// Retrieve the OSCORE context associated with this RID and ID Context
			byte[] rid = OptionJuggle.getRid(request.getOptions().getOscore());
			byte[] IDContext = OptionJuggle.getIDContext(request.getOptions().getOscore());
			
			OSCoreCtx ctx = null;
			try {
				ctx = ctxDb.getContext(rid, IDContext);
			} catch (CoapOSException e) {
				LOGGER.error("Error while receiving OSCore request: " + e.getMessage());
				Response error;
				error = CoapOSExceptionHandler.manageError(e, request);
				if (error != null) {
					super.sendResponse(exchange, error);
				}
				return;
			}

			// For OSCORE-protected requests with the outer block1-option let
			// them pass through to be re-assembled by the block-wise layer
			if (request.getOptions().hasBlock1()) {

				if (request.getMaxResourceBodySize() == 0) {
					int maxPayloadSize = getIncomingMaxUnfragSize(request, ctx);
					request.setMaxResourceBodySize(maxPayloadSize);
				}

				super.receiveRequest(exchange, request);
				return;
			}

			byte[] oscoreOption = null;
			try {
				request = prepareReceive(ctxDb, request, ctx);
				oscoreOption = request.getOptions().getOscore();
				request.getOptions().setOscore(Bytes.EMPTY);
				exchange.setRequest(request);
			} catch (CoapOSException e) {
				LOGGER.error("Error while receiving OSCore request: " + e.getMessage());
				Response error;
				error = CoapOSExceptionHandler.manageError(e, request);
				if (error != null) {
					super.sendResponse(exchange, error);
				}
				return;
			}
			exchange.setCryptographicContextID(oscoreOption);
		}
		super.receiveRequest(exchange, request);
	}

	//Always accepts unprotected responses, which is needed for reception of error messages
	@Override
	public void receiveResponse(Exchange exchange, Response response) {
		Request request = exchange.getCurrentRequest();
		if (request == null) {
			LOGGER.error("No request tied to this response");
			return;
		}

		try {
			//Printing of status information.
			//Warns when expecting OSCORE response but unprotected response is received
			if (!isProtected(response) && responseShouldBeProtected(exchange, response)) {
				LOGGER.warn("Incoming response is NOT OSCORE protected!");
			} else if (isProtected(response)) {
				LOGGER.info("Incoming response is OSCORE protected");
			}

			// For OSCORE-protected response with the outer block2-option let
			// them pass through to be re-assembled by the block-wise layer
			if (response.getOptions().hasBlock2()) {

				if (response.getMaxResourceBodySize() == 0) {
					int maxPayloadSize = getIncomingMaxUnfragSize(response, ctxDb);
					response.setMaxResourceBodySize(maxPayloadSize);
				}

				super.receiveResponse(exchange, response);
				return;
			}

			//If response is protected with OSCORE parse it first with prepareReceive
			if (isProtected(response)) {
				
				boolean groupModeResp = OptionJuggle.getGroupModeBit(response.getOptions().getOscore());

				if(groupModeResp) {
					System.out.println("Incoming Group Mode Response payload size: " + response.getPayloadSize());
				} else {
					System.out.println("Incoming Pairwise Mode Response payload size: " + response.getPayloadSize());
				}
				
				
				response = prepareReceive(ctxDb, response);
			}
		} catch (OSException e) {
			LOGGER.error("Error while receiving OSCore response: " + e.getMessage());
			EmptyMessage error = CoapOSExceptionHandler.manageError(e, response);
			if (error != null) {
				sendEmptyMessage(exchange, error);
			}
			return;
		}
		
		// Remove token if this is an incoming response to an Observe
		// cancellation request
		if (exchange.getRequest().isObserveCancel()) {
			ctxDb.removeToken(response.getToken());
		}
		
		super.receiveResponse(exchange, response);
	}

	@Override
	public void receiveEmptyMessage(Exchange exchange, EmptyMessage message) {
		super.receiveEmptyMessage(exchange, message);
	}

	private static boolean shouldProtectResponse(Exchange exchange) {
		return exchange.getCryptographicContextID() != null;
	}

	//Method that checks if a response is expected to be protected with OSCORE
	private boolean responseShouldBeProtected(Exchange exchange, Response response) throws OSException {
		Request request = exchange.getCurrentRequest();
		OptionSet options = request.getOptions();
		if (exchange.getCryptographicContextID() == null) {
			if (response.getOptions().hasObserve() && request.getOptions().hasObserve()) {

				// Since the exchange object has been re-created the
				// cryptographic id doesn't exist
				if (options.hasOscore()) {
					String uri = request.getURI();
					try {
						OSCoreCtx ctx = ctxDb.getContext(uri);
						exchange.setCryptographicContextID(ctx.getRecipientId());
					} catch (OSException e) {
						LOGGER.error("Error when re-creating exchange at OSCORE level");
						throw new OSException("Error when re-creating exchange at OSCORE level");
					}
				}
			}
		}
		return exchange.getCryptographicContextID() != null;
	}

	private static boolean shouldProtectRequest(Request request) {
		OptionSet options = request.getOptions();
		return options.hasOption(OptionNumberRegistry.OSCORE);

	}

	private static boolean isProtected(Message message) {
		return message.getOptions().getOscore() != null;
	}

	/**
	 * Check if a message being sent exceeds the MAX_UNFRAGMENTED_SIZE and is
	 * not using inner block-wise. If so it should not be sent.
	 * 
	 * @param message the CoAP message
	 * @param outerBlockwise {@code true}, for outer, {@code false}, for inner blockwise
	 * @param maxUnfragmentedSize the MAX_UNFRAGMENTED_SIZE value
	 * 
	 * @return if the message exceeds the MAX_UNFRAGMENTED_SIZE
	 */
	private boolean outgoingExceedsMaxUnfragSize(Message message, boolean outerBlockwise,
			int maxUnfragmentedSize) {

		boolean usesInnerBlockwise = (message.getOptions().hasBlock1() == true
				|| message.getOptions().hasBlock2() == true) && outerBlockwise == false;

		if (message.getPayloadSize() > maxUnfragmentedSize && usesInnerBlockwise == false) {
			return true;
		} else {
			return false;
		}

	}

	/**
	 * Gets the MAX_UNFRAGMENTED_SIZE size for an incoming block-wise transfer.
	 * If outer block-wise is used this value will be set using
	 * setMaxResourceBodySize on the incoming request or response and enforced
	 * in the BlockwiseLayer. Reception of messages where the cumulative payload
	 * size exceeds this value will be aborted.
	 * 
	 * @param message the CoAP message
	 * @param ctx the context used
	 * 
	 * @return the MAX_UNFRAGMENTED_SIZE value to be used
	 */
	private int getIncomingMaxUnfragSize(Message message, OSCoreCtx ctx) {

		// No limit if no context is found. A null context will be handled later
		if (ctx == null) {
			return 0;
		} else {
			return ctx.getMaxUnfragmentedSize();
		}

	}

	/**
	 * Separate version of method for handling responses.
	 * 
	 * @param message the CoAP message
	 * @param ctxDb the context database used
	 * @return the MAX_UNFRAGMENTED_SIZE value to be used
	 */
	private int getIncomingMaxUnfragSize(Message message, OSCoreCtxDB ctxDb) {
		OSCoreCtx ctx = null;
		if (message instanceof Response) {
			ctx = ctxDb.getContextByToken(message.getToken());
		}

		return getIncomingMaxUnfragSize(message, ctx);
	}

}