Decryptor

Change-Id: I542c0e5b937f9b31bfbe2dee2e5b589cf0f6cec9
diff --git a/docs/_static/decryptor.png b/docs/_static/decryptor.png
new file mode 100644
index 0000000..30791b7
--- /dev/null
+++ b/docs/_static/decryptor.png
Binary files differ
diff --git a/docs/figures/decryptor.pptx b/docs/figures/decryptor.pptx
new file mode 100644
index 0000000..e0f9545
--- /dev/null
+++ b/docs/figures/decryptor.pptx
Binary files differ
diff --git a/docs/spec.rst b/docs/spec.rst
index 2dfc33f..2de5aa1 100644
--- a/docs/spec.rst
+++ b/docs/spec.rst
@@ -120,3 +120,12 @@
                       .EncryptedPayload = ContentKey encrypted by public key /[access-namespace]/NAC/[dataset]/KEK/[key-id]
                       // .InitializationVector, .EncryptedPayloadKey, and .Name are not set
                     )
+
+Decryptor
+---------
+
+.. figure:: _static/decryptor.png
+   :alt: Decryptor
+   :align: center
+
+Encryptor decrypts (asynchronous operation, contingent on successful retrieval of CK data, KDK, and decryption of both) the supplied ``EncryptedContent`` element.
diff --git a/src/decryptor.cpp b/src/decryptor.cpp
new file mode 100644
index 0000000..25043d5
--- /dev/null
+++ b/src/decryptor.cpp
@@ -0,0 +1,269 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/**
+ * Copyright (c) 2014-2018, Regents of the University of California
+ *
+ * NAC library 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.
+ *
+ * NAC library 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 copies of the GNU General Public License and GNU Lesser
+ * General Public License along with ndn-cxx, e.g., in COPYING.md file.  If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * See AUTHORS.md for complete list of NAC library authors and contributors.
+ */
+
+#include "decryptor.hpp"
+
+#include <ndn-cxx/util/logger.hpp>
+
+namespace ndn {
+namespace nac {
+
+NDN_LOG_INIT(nac.Decryptor);
+
+const size_t N_RETRIES = 3;
+
+Decryptor::Decryptor(const Key& credentialsKey, Validator& validator, KeyChain& keyChain, Face& face)
+  : m_credentialsKey(credentialsKey)
+  // , m_validator(validator)
+  , m_face(face)
+  , m_keyChain(keyChain)
+  , m_internalKeyChain("pib-memory:", "tpm-memory:")
+{
+}
+
+Decryptor::~Decryptor()
+{
+  for (auto& i : m_cks) {
+    if (i.second.pendingInterest != nullptr) {
+      m_face.removePendingInterest(i.second.pendingInterest);
+      i.second.pendingInterest = nullptr;
+
+      for (const auto& p : i.second.pendingDecrypts) {
+        p.onFailure(ErrorCode::CkRetrievalFailure, "Cancel pending decrypt as ContentKey is being destroyed");
+      }
+      i.second.pendingDecrypts.clear(); // not really necessary, but just in case
+    }
+  }
+}
+
+void
+Decryptor::decrypt(const Block& encryptedContent, const DecryptSuccessCallback& onSuccess, const ErrorCallback& onFailure)
+{
+  EncryptedContent ec(encryptedContent);
+  if (!ec.hasKeyLocator()) {
+    NDN_LOG_INFO("Missing required KeyLocator in the supplied EncryptedContent block");
+    return onFailure(ErrorCode::MissingRequiredKeyLocator,
+                     "Missing required KeyLocator in the supplied EncryptedContent block");
+  }
+
+  if (!ec.hasIv()) {
+    NDN_LOG_INFO("Missing required InitialVector in the supplied EncryptedContent block");
+    return onFailure(ErrorCode::MissingRequiredKeyLocator,
+                     "Missing required InitialVector in the supplied EncryptedContent block");
+  }
+
+  ContentKeys::iterator ck;
+  bool isNew = false;
+  std::tie(ck, isNew) = m_cks.emplace(ec.getKeyLocator(), ContentKey{});
+
+  if (ck->second.isRetrieved) {
+    doDecrypt(ec, ck->second.bits, onSuccess, onFailure);
+  }
+  else {
+    NDN_LOG_DEBUG("CK " << ec.getKeyLocator() << " not yet available, adding decrypt to the pending queue");
+    ck->second.pendingDecrypts.push_back({ec, onSuccess, onFailure});
+  }
+
+  if (isNew) {
+    fetchCk(ck, onFailure, N_RETRIES);
+  }
+}
+
+void
+Decryptor::fetchCk(ContentKeys::iterator ck, const ErrorCallback& onFailure, size_t nTriesLeft)
+{
+  // full name of CK is
+
+  // <whatever-prefix>/CK/<ck-id>  /ENCRYPTED-BY /<kek-prefix>/KEK/<key-id>
+  // \                          /                \                        /
+  //  -----------  -------------                  -----------  -----------
+  //             \/                                          \/
+  //   from the encrypted data          unknown (name in retrieved CK is used to determine KDK)
+
+  const Name& ckName = ck->first;
+
+  NDN_LOG_DEBUG("Fetching CK " << ckName);
+
+  ck->second.pendingInterest = m_face.expressInterest(Interest(ckName)
+                                                       .setMustBeFresh(false) // ?
+                                                       .setCanBePrefix(true),
+    [=] (const Interest& ckInterest, const Data& ckData) {
+      ck->second.pendingInterest = nullptr;
+      // @TODO verify if the key is legit
+      Name kdkPrefix, kdkIdentity, kdkKeyName;
+      std::tie(kdkPrefix, kdkIdentity, kdkKeyName) =
+        extractKdkInfoFromCkName(ckData.getName(), ckInterest.getName(), onFailure);
+      if (kdkPrefix.empty()) {
+        return; // error has been already reported
+      }
+
+      // check if KDK already exists (there is a corresponding
+      auto kdkIdentityIt = m_internalKeyChain.getPib().getIdentities().find(kdkIdentity);
+      if (kdkIdentityIt != m_internalKeyChain.getPib().getIdentities().end()) {
+        auto kdkKeyIt = (*kdkIdentityIt).getKeys().find(kdkKeyName);
+        if (kdkKeyIt != (*kdkIdentityIt).getKeys().end()) {
+          // KDK was already fetched and imported
+          NDN_LOG_DEBUG("KDK " << kdkKeyName << " already exists, directly using it to decrypt CK");
+          return decryptCkAndProcessPendingDecrypts(ck, ckData, kdkKeyName, onFailure);
+        }
+      }
+
+      fetchKdk(ck, kdkPrefix, ckData, onFailure, N_RETRIES);
+    },
+    [=] (const Interest& i, const lp::Nack& nack) {
+      ck->second.pendingInterest = nullptr;
+      onFailure(ErrorCode::CkRetrievalFailure,
+                "Retrieval of CK [" + i.getName().toUri() + "] failed. "
+                "Got NACK (" + boost::lexical_cast<std::string>(nack.getReason()) + ")");
+    },
+    [=] (const Interest& i) {
+      ck->second.pendingInterest = nullptr;
+      if (nTriesLeft > 1) {
+        fetchCk(ck, onFailure, nTriesLeft - 1);
+      }
+      else {
+        onFailure(ErrorCode::CkRetrievalTimeout,
+                  "Retrieval of CK [" + i.getName().toUri() + "] timed out");
+      }
+    });
+}
+
+void
+Decryptor::fetchKdk(ContentKeys::iterator ck, const Name& kdkPrefix, const Data& ckData,
+                    const ErrorCallback& onFailure, size_t nTriesLeft)
+{
+  // <kdk-prefix>/KDK/<kdk-id>    /ENCRYPTED-BY  /<credential-identity>/KEY/<key-id>
+  // \                          /                \                                /
+  //  -----------  -------------                  ---------------  ---------------
+  //             \/                                              \/
+  //     from the CK data                                from configuration
+
+  Name kdkName = kdkPrefix;
+  kdkName
+    .append(ENCRYPTED_BY)
+    .append(m_credentialsKey.getName());
+
+  NDN_LOG_DEBUG("Fetching KDK " << kdkName);
+
+  ck->second.pendingInterest = m_face.expressInterest(Interest(kdkName)
+                                                     .setMustBeFresh(true)
+                                                     .setCanBePrefix(false),
+    [=] (const Interest& ckInterest, const Data& kdkData) {
+      ck->second.pendingInterest = nullptr;
+      // @TODO verify if the key is legit
+
+      bool isOk = decryptAndImportKdk(kdkData, onFailure);
+      if (!isOk)
+        return;
+      decryptCkAndProcessPendingDecrypts(ck, ckData,
+                                         kdkPrefix.getPrefix(-2).append("KEY").append(kdkPrefix.get(-1)), // a bit hacky
+                                         onFailure);
+    },
+    [=] (const Interest& i, const lp::Nack& nack) {
+      ck->second.pendingInterest = nullptr;
+      onFailure(ErrorCode::KdkRetrievalFailure,
+                "Retrieval of KDK [" + i.getName().toUri() + "] failed. "
+                "Got NACK (" + boost::lexical_cast<std::string>(nack.getReason()) + ")");
+    },
+    [=] (const Interest& i) {
+      ck->second.pendingInterest = nullptr;
+      if (nTriesLeft > 1) {
+        fetchKdk(ck, kdkPrefix, ckData, onFailure, nTriesLeft - 1);
+      }
+      else {
+        onFailure(ErrorCode::KdkRetrievalTimeout,
+                  "Retrieval of KDK [" + i.getName().toUri() + "] timed out");
+      }
+    });
+}
+
+bool
+Decryptor::decryptAndImportKdk(const Data& kdkData, const ErrorCallback& onFailure)
+{
+  try {
+    NDN_LOG_DEBUG("Decrypting and importing KDK " << kdkData.getName());
+    EncryptedContent content(kdkData.getContent().blockFromValue());
+
+    SafeBag safeBag(content.getPayload().blockFromValue());
+    auto secret = m_keyChain.getTpm().decrypt(content.getPayloadKey().value(),
+                                              content.getPayloadKey().value_size(),
+                                              m_credentialsKey.getName());
+    if (secret == nullptr) {
+      onFailure(ErrorCode::TpmKeyNotFound,
+                "Could not decrypt secret, " + m_credentialsKey.getName().toUri() + " not found in TPM");
+      return false;
+    }
+
+    m_internalKeyChain.importSafeBag(safeBag, reinterpret_cast<const char*>(secret->data()), secret->size());
+    return true;
+  }
+  catch (const std::runtime_error& e) {
+    // can be tlv::Error, pib::Error, tpm::Error, and bunch of other runtime-derived errors
+    onFailure(ErrorCode::KdkDecryptionFailure,
+              "Failed to decrypt KDK [" + kdkData.getName().toUri() + "]: " + e.what());
+    return false;
+  }
+}
+
+void
+Decryptor::decryptCkAndProcessPendingDecrypts(ContentKeys::iterator ck, const Data& ckData, const Name& kdkKeyName,
+                                              const ErrorCallback& onFailure)
+{
+  NDN_LOG_DEBUG("Decrypting CK data " << ckData.getName());
+
+  EncryptedContent content(ckData.getContent().blockFromValue());
+
+  auto ckBits = m_internalKeyChain.getTpm().decrypt(content.getPayload().value(), content.getPayload().value_size(),
+                                                    kdkKeyName);
+  if (ckBits == nullptr) {
+    onFailure(ErrorCode::TpmKeyNotFound, "Could not decrypt secret, " + kdkKeyName.toUri() + " not found in TPM");
+    return;
+  }
+
+  ck->second.bits = *ckBits;
+  ck->second.isRetrieved = true;
+
+  for (const auto& item : ck->second.pendingDecrypts) {
+    doDecrypt(item.encryptedContent, ck->second.bits, item.onSuccess, item.onFailure);
+  }
+  ck->second.pendingDecrypts.clear();
+}
+
+void
+Decryptor::doDecrypt(const EncryptedContent& content, const Buffer& ckBits,
+                     const DecryptSuccessCallback& onSuccess,
+                     const ErrorCallback& onFailure)
+{
+  if (!content.hasIv()) {
+    BOOST_THROW_EXCEPTION(Error("Expecting Initial Vector in the encrypted content, but it is not present"));
+  }
+
+  OBufferStream os;
+  security::transform::bufferSource(content.getPayload().value(), content.getPayload().value_size())
+    >> security::transform::blockCipher(BlockCipherAlgorithm::AES_CBC,
+                                        CipherOperator::DECRYPT,
+                                        ckBits.data(), ckBits.size(),
+                                        content.getIv().value(), content.getIv().value_size())
+    >> security::transform::streamSink(os);
+
+  onSuccess(os.buf());
+}
+
+} // namespace nac
+} // namespace ndn
diff --git a/src/decryptor.hpp b/src/decryptor.hpp
new file mode 100644
index 0000000..d2d6406
--- /dev/null
+++ b/src/decryptor.hpp
@@ -0,0 +1,117 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/**
+ * Copyright (c) 2014-2018, Regents of the University of California
+ *
+ * NAC library 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.
+ *
+ * NAC library 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 copies of the GNU General Public License and GNU Lesser
+ * General Public License along with ndn-cxx, e.g., in COPYING.md file.  If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * See AUTHORS.md for complete list of NAC library authors and contributors.
+ */
+
+#ifndef NDN_NAC_DECRYPTOR_HPP
+#define NDN_NAC_DECRYPTOR_HPP
+
+#include "common.hpp"
+#include "encrypted-content.hpp"
+
+#include <ndn-cxx/face.hpp>
+
+namespace ndn {
+namespace nac {
+
+/**
+ * @brief NAC Decryptor
+ *
+ * Encryptor decrypts (asynchronous operation, contingent on successful retrieval of CK data,
+ * KDK, and decryption of both) the supplied ``EncryptedContent`` element.
+ */
+class Decryptor
+{
+public:
+  using DecryptSuccessCallback = std::function<void(ConstBufferPtr)>;
+
+private:
+  struct ContentKey
+  {
+    bool isRetrieved = false;
+    Buffer bits;
+    const PendingInterestId* pendingInterest = nullptr;
+
+    struct PendingDecrypt
+    {
+      EncryptedContent encryptedContent;
+      DecryptSuccessCallback onSuccess;
+      ErrorCallback onFailure;
+    };
+    std::list<PendingDecrypt> pendingDecrypts;
+  };
+
+  using ContentKeys = std::map<Name, ContentKey>;
+
+public:
+  /**
+   * @brief Constructor
+   * @param credentialsKey Credentials key to be used to retrieve and decrypt KDK
+   * @param validator Validation policy to ensure validity of KDK and CK
+   * @param keyChain  KeyChain
+   * @param face      Face that will be used to fetch CK and KDK
+   */
+  Decryptor(const Key& credentialsKey, Validator& validator, KeyChain& keyChain, Face& face);
+
+  ~Decryptor();
+
+  /**
+   * @brief Asynchronously decrypt @p encryptedContent
+   */
+  void
+  decrypt(const Block& encryptedContent, const DecryptSuccessCallback& onSuccess, const ErrorCallback& onFailure);
+
+private:
+  void
+  fetchCk(ContentKeys::iterator ck, const ErrorCallback& onFailure, size_t nTriesLeft);
+
+  void
+  fetchKdk(ContentKeys::iterator ck, const Name& kdkPrefix, const Data& ckData,
+           const ErrorCallback& onFailure, size_t nTriesLeft);
+
+  bool
+  decryptAndImportKdk(const Data& kdkData, const ErrorCallback& onFailure);
+
+  void
+  decryptCkAndProcessPendingDecrypts(ContentKeys::iterator ck, const Data& ckData,
+                                     const Name& kdkKeyName/* local keyChain name for KDK key*/,
+                                     const ErrorCallback& onFailure);
+
+  /**
+   * @brief Synchronously decrypt (assume CK exists)
+   */
+  void
+  doDecrypt(const EncryptedContent& encryptedContent, const Buffer& ckBits,
+            const DecryptSuccessCallback& onSuccess,
+            const ErrorCallback& onFailure);
+
+private:
+  Key m_credentialsKey;
+  // Validator& m_validator;
+  Face& m_face;
+  KeyChain& m_keyChain; // external keychain with access credentials
+  KeyChain m_internalKeyChain; // internal in-memory keychain for temporarily storing KDKs
+
+  // a set of Content Keys
+  // @TODO add some expiration, so they are not stored forever
+  ContentKeys m_cks;
+};
+
+} // namespace nac
+} // namespace ndn
+
+#endif // NDN_NAC_DECRYPTOR_HPP
diff --git a/tests/tests/decryptor.t.cpp b/tests/tests/decryptor.t.cpp
new file mode 100644
index 0000000..258d135
--- /dev/null
+++ b/tests/tests/decryptor.t.cpp
@@ -0,0 +1,137 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/**
+ * Copyright (c) 2014-2018, Regents of the University of California
+ *
+ * NAC library 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.
+ *
+ * NAC library 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 copies of the GNU General Public License and GNU Lesser
+ * General Public License along with ndn-cxx, e.g., in COPYING.md file.  If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * See AUTHORS.md for complete list of NAC library authors and contributors.
+ */
+
+#include "decryptor.hpp"
+
+#include "encryptor.hpp"
+#include "encrypted-content.hpp"
+#include "access-manager.hpp"
+
+#include "tests-common.hpp"
+#include "dummy-forwarder.hpp"
+#include "static-data.hpp"
+
+#include <iostream>
+#include <boost/mpl/vector.hpp>
+
+namespace ndn {
+namespace nac {
+namespace tests {
+
+class StaticDataEnvironment : public UnitTestTimeFixture
+{
+public:
+  StaticDataEnvironment()
+    : fw(m_io, m_keyChain)
+    , imsFace(static_cast<util::DummyClientFace&>(fw.addFace()))
+  {
+    StaticData data;
+    for (const auto& block : data.managerPackets) {
+      auto data = make_shared<Data>(block);
+      m_ims.insert(*data);
+    }
+
+    for (const auto& block : data.encryptorPackets) {
+      auto data = make_shared<Data>(block);
+      m_ims.insert(*data);
+    }
+
+    auto serveFromIms = [this] (const Name& prefix, const Interest& interest) {
+      auto data = m_ims.find(interest);
+      if (data != nullptr) {
+        imsFace.put(*data);
+      }
+    };
+    imsFace.setInterestFilter("/", serveFromIms, [] (auto...) {});
+    advanceClocks(1_ms, 10);
+
+    // import "/first/user" identity
+    m_keyChain.importSafeBag(SafeBag(data.userIdentities.at(0)), "password", strlen("password"));
+    // credentialIdentity = m_keyChain.getPib().getIdentity("/first/user");
+
+    addIdentity("/not/authorized");
+  }
+
+public:
+  DummyForwarder fw;
+  util::DummyClientFace& imsFace;
+  InMemoryStoragePersistent m_ims;
+};
+
+template<class T>
+class DecryptorFixture : public StaticDataEnvironment
+{
+public:
+  DecryptorFixture()
+    : face(static_cast<util::DummyClientFace&>(fw.addFace()))
+    , decryptor(m_keyChain.getPib().getIdentity(T().identity).getDefaultKey(), validator, m_keyChain, face)
+  {
+    advanceClocks(1_ms, 10);
+  }
+
+public:
+  util::DummyClientFace& face;
+  ValidatorNull validator;
+  Decryptor decryptor;
+};
+
+BOOST_AUTO_TEST_SUITE(TestDecryptor)
+
+struct Valid
+{
+  std::string identity = "/first/user";
+  bool expectToSucceed = true;
+};
+
+struct Invalid
+{
+  std::string identity = "/not/authorized";
+  bool expectToSucceed = false;
+};
+
+using Identities = boost::mpl::vector<Valid, Invalid>;
+
+BOOST_FIXTURE_TEST_CASE_TEMPLATE(DecryptSuccess, T, Identities, DecryptorFixture<T>)
+{
+  StaticData data;
+
+  size_t nSuccesses = 0;
+  size_t nFailures = 0;
+  this->decryptor.decrypt(data.encryptedBlobs.at(0),
+                    [&] (ConstBufferPtr buffer) {
+                      ++nSuccesses;
+                      BOOST_CHECK_EQUAL(buffer->size(), 15);
+                      std::string content(reinterpret_cast<const char*>(buffer->data()), buffer->size());
+                      BOOST_CHECK_EQUAL(content, "Data to encrypt");
+                    },
+                    [&] (const ErrorCode& code, const std::string& msg) {
+                      BOOST_TEST_MESSAGE(msg);
+                      ++nFailures;
+                    });
+  this->advanceClocks(2_s, 10);
+
+  BOOST_CHECK_EQUAL(nSuccesses, T().expectToSucceed ? 1 : 0);
+  BOOST_CHECK_EQUAL(nFailures, T().expectToSucceed ? 0 : 1);
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+} // namespace tests
+} // namespace nac
+} // namespace ndn