All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.named_data.jndn.encrypt.EncryptorV2 Maven / Gradle / Ivy

/**
 * Copyright (C) 2018 Regents of the University of California.
 * @author: Jeff Thompson 
 * @author: From the NAC library https://github.com/named-data/name-based-access-control/blob/new/src/encryptor.cpp
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see .
 * A copy of the GNU Lesser General Public License is in the file COPYING.
 */

package net.named_data.jndn.encrypt;

import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import net.named_data.jndn.Data;
import net.named_data.jndn.Face;
import net.named_data.jndn.Interest;
import net.named_data.jndn.InterestFilter;
import net.named_data.jndn.Name;
import net.named_data.jndn.NetworkNack;
import net.named_data.jndn.OnData;
import net.named_data.jndn.OnInterestCallback;
import net.named_data.jndn.OnNetworkNack;
import net.named_data.jndn.OnRegisterFailed;
import net.named_data.jndn.OnTimeout;
import net.named_data.jndn.security.KeyChain;
import net.named_data.jndn.security.SigningInfo;
import net.named_data.jndn.security.v2.Validator;
import net.named_data.jndn.encrypt.EncryptError.OnError;
import net.named_data.jndn.encrypt.algo.EncryptAlgorithmType;
import net.named_data.jndn.in_memory_storage.InMemoryStorageRetaining;
import net.named_data.jndn.security.SecurityException;
import net.named_data.jndn.security.certificate.PublicKey;
import net.named_data.jndn.util.Blob;
import net.named_data.jndn.util.Common;

/**
 * EncryptorV2 encrypts the requested content for name-based access control (NAC)
 * using security v2. For the meaning of "KEK", etc. see:
 * https://github.com/named-data/name-based-access-control/blob/new/docs/spec.rst
 */
public class EncryptorV2 {
  /**
   * Create an EncryptorV2 with the given parameters. This uses the face to
   * register to receive Interests for the prefix {ckPrefix}/CK.
   * @param accessPrefix The NAC prefix to fetch the Key Encryption Key (KEK)
   * (e.g., /access/prefix/NAC/data/subset). This copies the Name.
   * @param ckPrefix The prefix under which Content Keys (CK) will be generated.
   * (Each will have a unique version appended.) This copies the Name.
   * @param ckDataSigningInfo The SigningInfo parameters to sign the Content Key
   * (CK) Data packet. This copies the SigningInfo.
   * @param onError On failure to create the CK data (failed to fetch the KEK,
   * failed to encrypt with the KEK, etc.), this calls
   * onError.onError(errorCode, message) where errorCode is from the
   * EncryptError.ErrorCode enum, and message is an error string. The encrypt
   * method will continue trying to retrieve the KEK until success (with each
   * attempt separated by RETRY_DELAY_KEK_RETRIEVAL) and onError may be called
   * multiple times.
   * NOTE: The library will log any exceptions thrown by this callback, but for
   * better error handling the callback should catch and properly handle any
   * exceptions.
   * @param validator The validation policy to ensure correctness of KEK.
   * @param keyChain The KeyChain used to sign Data packets.
   * @param face The Face that will be used to fetch the KEK and publish CK data.
   */
  public EncryptorV2
    (Name accessPrefix, Name ckPrefix, SigningInfo ckDataSigningInfo,
     OnError onError, Validator validator, KeyChain keyChain, Face face)
    throws IOException, SecurityException
  {
    // Copy the Name.
    accessPrefix_ = new Name(accessPrefix);
    ckPrefix_ = new Name(ckPrefix);
    ckBits_ = new byte[AES_KEY_SIZE];
    ckDataSigningInfo_ = new SigningInfo(ckDataSigningInfo);
    isKekRetrievalInProgress_ = false;
    onError_ = onError;
    keyChain_ = keyChain;
    face_ = face;

    regenerateCk();

    ckRegisteredPrefixId_ = face_.registerPrefix
      (new Name(ckPrefix).append(NAME_COMPONENT_CK),
       new OnInterestCallback() {
         public void onInterest(Name prefix, Interest interest, Face face, long interestFilterId, InterestFilter filter) {
           Data data = storage_.find(interest);
           if (data != null) {
             logger_.log(Level.INFO, "Serving {0} from InMemoryStorage",
               data.getName());
             try {
               face.putData(data);
             } catch (IOException ex) {
               logger_.log(Level.SEVERE, "Error in Face.putData: {0}", ex);
             }
           }
           else {
             logger_.log(Level.INFO, "Didn't find CK data for {0}",
               interest.getName());
             // TODO: Send NACK?
           }
         }
       },
       new OnRegisterFailed() {
         public void onRegisterFailed(Name prefix) {
           logger_.log(Level.SEVERE, "Failed to register prefix {0}", prefix);
         }
       });
  }

  public final void
  shutdown()
  {
    face_.unsetInterestFilter(ckRegisteredPrefixId_);
    if (kekPendingInterestId_ > 0)
      face_.removePendingInterest(kekPendingInterestId_);
  }

  public final EncryptedContent
  encrypt(byte[] plainData)
    throws NoSuchAlgorithmException, NoSuchPaddingException,
      InvalidAlgorithmParameterException, IllegalBlockSizeException,
      BadPaddingException
  {
    // Generate the initial vector.
    byte[] iv = new byte[AES_IV_SIZE];
    Common.getRandom().nextBytes(iv);

    Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
    try {
      cipher.init
        (Cipher.ENCRYPT_MODE, new SecretKeySpec(ckBits_, "AES"),
         new IvParameterSpec(iv));
    } catch (InvalidKeyException ex) {
      throw new Error
        ("If the error is 'Illegal key size', try installing the " +
         "Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files: " +
         ex);
    }
    byte[] encryptedData = cipher.doFinal(plainData);

    EncryptedContent content = new EncryptedContent();
    content.setInitialVector(new Blob(iv, false));
    content.setPayload(new Blob(encryptedData, false));
    content.setKeyLocatorName(ckName_);

    return content;
  }

  /**
   * Create a new Content Key (CK) and publish the corresponding CK Data packet.
   * This uses the onError given to the constructor to report errors.
   */
  public final void
  regenerateCk()
  {
    // TODO: Ensure that the CK Data packet for the old CK is published when the
    // CK is updated before the KEK is fetched.

    ckName_ = new Name(ckPrefix_);
    ckName_.append(NAME_COMPONENT_CK);
    // The version is the ID of the CK.
    ckName_.appendVersion((long)Common.getNowMilliseconds());

    logger_.log(Level.INFO, "Generating new CK: {0}", ckName_);
    Common.getRandom().nextBytes(ckBits_);

    // One implication: If the CK is updated before the KEK is fetched, then
    // the KDK for the old CK will not be published.
    if (kekData_ == null)
      retryFetchingKek();
    else
      makeAndPublishCkData(onError_);
  }

  /**
   * The number of packets stored in in-memory storage.
   * @return The number of packets.
   */
  public final int
  size() { return storage_.size(); }

  /**
   * Get the the storage cache, which should only be used for testing.
   * @return The storage cache.
   */
  public final HashMap
  getCache_() { return storage_.getCache_(); }

  /**
   * Get the isKekRetrievalInProgress_ flag. This is only for testing.
   * @return The isKekRetrievalInProgress_ flag;
   */
  public final boolean
  getIsKekRetrievalInProgress_() { return isKekRetrievalInProgress_; }

  /**
   * Set the internal kekData_ to null. This is only for testing.
   */
  public final void
  clearKekData_() { kekData_ = null; }

  private void
  retryFetchingKek()
  {
    if (isKekRetrievalInProgress_)
      return;

    logger_.log(Level.INFO, "Retrying fetching of the KEK");
    isKekRetrievalInProgress_ = true;
    fetchKekAndPublishCkData
      (new Runnable() {
         public void run() {
           logger_.log(Level.INFO, "The KEK was retrieved and published");
           isKekRetrievalInProgress_ = false;
         }
       },
       new OnError() {
         public void onError(EncryptError.ErrorCode errorCode, String message) {
           logger_.log(Level.INFO, "Failed to retrieve KEK: {0}", message);
           isKekRetrievalInProgress_ = false;
           onError_.onError(errorCode, message);
         }
       },
       N_RETRIES);
  }

  /**
   * Create an Interest for /KEK to retrieve the
   * /KEK/ KEK Data packet, and set kekData_.
   * @param onReady When the KEK is retrieved and published, this calls
   * onReady.run().
   * @param onError On failure, this calls onError.onError(errorCode, message)
   * where errorCode is from the EncryptError.ErrorCode enum, and message is an
   * error string.
   * @param nTriesLeft The number of retries for expressInterest timeouts.
   */
  private void
  fetchKekAndPublishCkData
    (final Runnable onReady, final OnError onError, final int nTriesLeft)
  {
    logger_.log(Level.INFO, "Fetching KEK: {0}",
      new Name(accessPrefix_).append(NAME_COMPONENT_KEK));

    if (kekPendingInterestId_ > 0) {
      onError.onError(EncryptError.ErrorCode.SecurityException,
        "fetchKekAndPublishCkData: There is already a kekPendingInterestId_");
      return;
    }
    try {
      kekPendingInterestId_ = face_.expressInterest
        (new Interest(new Name(accessPrefix_).append(NAME_COMPONENT_KEK))
               .setMustBeFresh(true)
               .setCanBePrefix(true),
         new OnData() {
           public void onData(Interest interest, Data kekData) {
             kekPendingInterestId_ = 0;
             // TODO: Verify if the key is legitimate.
             kekData_ = kekData;
             if (makeAndPublishCkData(onError))
               onReady.run();
             // Otherwise, failure has already been reported.
           }
         },
         new OnTimeout() {
           public void onTimeout(Interest interest) {
             kekPendingInterestId_ = 0;
             if (nTriesLeft > 1)
               fetchKekAndPublishCkData(onReady, onError, nTriesLeft - 1);
             else {
               onError.onError(EncryptError.ErrorCode.KekRetrievalTimeout,
                 "Retrieval of KEK [" + interest.getName().toUri() + "] timed out");
               logger_.log(Level.INFO, "Scheduling retry after all timeouts");
               face_.callLater
                 (RETRY_DELAY_KEK_RETRIEVAL_MS,
                  new Runnable() {
                    public void run() {
                      retryFetchingKek();
                    }
                  });
             }
           }
         },
         new OnNetworkNack() {
           public void onNetworkNack(Interest interest, NetworkNack networkNack) {
             kekPendingInterestId_ = 0;
             if (nTriesLeft > 1) {
               face_.callLater
                 (RETRY_DELAY_AFTER_NACK_MS,
                  new Runnable() {
                    public void run() {
                      fetchKekAndPublishCkData(onReady, onError, nTriesLeft - 1);
                    }
                  });
             }
             else {
               onError.onError(EncryptError.ErrorCode.KekRetrievalFailure,
                 "Retrieval of KEK [" + interest.getName().toUri() +
                 "] failed. Got NACK (" + networkNack.getReason() + ")");
               logger_.log(Level.INFO, "Scheduling retry from NACK");
               face_.callLater
                 (RETRY_DELAY_KEK_RETRIEVAL_MS,
                  new Runnable() {
                    public void run() {
                      retryFetchingKek();
                    }
                  });
             }
           }
         });
    } catch (Exception ex) {
      onError.onError(EncryptError.ErrorCode.General,
        "expressInterest error: " + ex);
    }
  }

  /**
   * Make a CK Data packet for ckName_ encrypted by the KEK in kekData_ and
   * insert it in the storage_.
   * @param onError On failure, this calls onError.onError(errorCode, message)
   * where errorCode is from the EncryptError.ErrorCode enum, and message is an
   * error string.
   * @return True on success, else false.
   */
  private boolean
  makeAndPublishCkData(OnError onError)
  {
    try {
      PublicKey kek = new PublicKey(kekData_.getContent());

      EncryptedContent content = new EncryptedContent();
      content.setPayload(kek.encrypt(ckBits_, EncryptAlgorithmType.RsaOaep));

      Data ckData = new Data
        (new Name(ckName_).append(NAME_COMPONENT_ENCRYPTED_BY)
         .append(kekData_.getName()));
      ckData.setContent(content.wireEncodeV2());
      // FreshnessPeriod can serve as a soft access control for revoking access
      ckData.getMetaInfo().setFreshnessPeriod(DEFAULT_CK_FRESHNESS_PERIOD_MS);
      keyChain_.sign(ckData, ckDataSigningInfo_);
      storage_.insert(ckData);

      logger_.log(Level.INFO, "Publishing CK data: {0}", ckData.getName());
      return true;
    }
    catch (Throwable ex) {
      onError.onError(EncryptError.ErrorCode.EncryptionFailure,
        "Failed to encrypt generated CK with KEK " + kekData_.getName().toUri());
      return false;
    }
  }

  public static final Name.Component NAME_COMPONENT_ENCRYPTED_BY =
    new Name.Component("ENCRYPTED-BY");
  public static final Name.Component NAME_COMPONENT_NAC = new Name.Component("NAC");
  public static final Name.Component NAME_COMPONENT_KEK = new Name.Component("KEK");
  public static final Name.Component NAME_COMPONENT_KDK = new Name.Component("KDK");
  public static final Name.Component NAME_COMPONENT_CK = new Name.Component("CK");

  public static final double RETRY_DELAY_AFTER_NACK_MS = 1000.0;
  public static final double RETRY_DELAY_KEK_RETRIEVAL_MS = 60 * 1000.0;

  private final Name accessPrefix_;
  private final Name ckPrefix_;
  private Name ckName_;
  private final byte[] ckBits_;
  private final SigningInfo ckDataSigningInfo_;

  private boolean isKekRetrievalInProgress_;
  private Data kekData_ = null;
  private final OnError onError_;

  // Storage for encrypted CKs.
  private final InMemoryStorageRetaining storage_ = new InMemoryStorageRetaining();
  private final long ckRegisteredPrefixId_;
  private long kekPendingInterestId_ = 0;

  private final KeyChain keyChain_;
  private final Face face_;
  private static final Logger logger_ = Logger.getLogger(EncryptorV2.class.getName());

  public static final int AES_KEY_SIZE = 32;
  public static final int AES_IV_SIZE = 16;
  public static final int N_RETRIES = 3;

  private static final double DEFAULT_CK_FRESHNESS_PERIOD_MS = 3600 * 1000.0;
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy