# -*- Mode:python; c-file-style:"gnu"; indent-tabs-mode:nil -*- */
#
# Copyright (C) 2018-2019 Regents of the University of California.
# Author: Jeff Thompson <[email protected]>
# 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 <http://www.gnu.org/licenses/>.
# A copy of the GNU Lesser General Public License is in the file COPYING.
"""
This module defines the EncryptorV2 class which 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
"""
import logging
from random import SystemRandom
from pyndn.name import Name
from pyndn.interest import Interest
from pyndn.data import Data
from pyndn.util.common import Common
from pyndn.util.blob import Blob
from pyndn.in_memory_storage.in_memory_storage_retaining import InMemoryStorageRetaining
from pyndn.security.certificate.public_key import PublicKey
from pyndn.encrypt.algo.encrypt_params import EncryptParams, EncryptAlgorithmType
from pyndn.encrypt.algo.aes_algorithm import AesAlgorithm
from pyndn.encrypt.encrypt_error import EncryptError
from pyndn.encrypt.encrypted_content import EncryptedContent
# The Python documentation says "Use SystemRandom if you require a
# cryptographically secure pseudo-random number generator."
# http://docs.python.org/2/library/random.html
_systemRandom = SystemRandom()
[docs]class EncryptorV2(object):
"""
Create an EncryptorV2 with the given parameters. This uses the face to
register to receive Interests for the prefix {ckPrefix}/CK.
:param Name accessPrefix: The NAC prefix to fetch the Key Encryption Key
(KEK) (e.g., /access/prefix/NAC/data/subset). This copies the Name.
:param Name ckPrefix: The prefix under which Content Keys (CK) will be
generated. (Each will have a unique version appended.) This copies the Name.
:param SigningInfo 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(errorCode, message) where errorCode is from EncryptError.ErrorCode
and message is a str. The encrypt method will continue trying to retrieve
the KEK until success (with each attempt separated by
RETRY_DELAY_KEK_RETRIEVAL_MS) 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.
:type onError: function object
:param Validator validator: The validation policy to ensure correctness of
the KEK.
:param KeyChain keyChain: The KeyChain used to sign Data packets.
:param Face face: The Face that will be used to fetch the KEK and publish CK
data.
"""
def __init__(self, accessPrefix, ckPrefix, ckDataSigningInfo,
onError, validator, keyChain, face):
# Copy the Name.
self._accessPrefix = Name(accessPrefix)
self._ckPrefix = Name(ckPrefix)
self._ckBits = bytearray(EncryptorV2.AES_KEY_SIZE)
self._ckDataSigningInfo = SigningInfo(ckDataSigningInfo)
self._isKekRetrievalInProgress = False
self._onError = onError
self._keyChain = keyChain
self._face = face
self._kekData = None
# Storage for encrypted CKs.
self._storage = InMemoryStorageRetaining()
self._kekPendingInterestId = 0
self.regenerateCk()
def onInterest(prefix, interest, face, interestFilterId, filter):
data = self._storage.find(interest)
if data != None:
logging.getLogger(__name__).info("Serving " +
data.getName().toUri() + " from InMemoryStorage")
try:
face.putData(data)
except:
logging.exception("Error in Face.putData")
else:
logging.getLogger(__name__).info(
"Didn't find CK data for " + interest.getName().toUri())
# TODO: Send NACK?
def onRegisterFailed(prefix):
logging.getLogger(__name__).error(
"Failed to register prefix " + prefix.toUri())
self._ckRegisteredPrefixId = self._face.registerPrefix(
Name(ckPrefix).append(EncryptorV2.NAME_COMPONENT_CK),
onInterest, onRegisterFailed)
[docs] def shutdown(self):
self._face.unsetInterestFilter(self._ckRegisteredPrefixId)
if self._kekPendingInterestId > 0:
self._face.removePendingInterest(self._kekPendingInterestId)
[docs] def encrypt(self, plainData):
"""
Encrypt the plainData using the existing Content Key (CK) and return a
new EncryptedContent.
:param plainData: The data to encrypt.
:type plainData: Blob or an array which implements the buffer protocol
:return: The new EncryptedContent.
:rtype: EncryptedContent
"""
# Generate the initial vector.
initialVector = bytearray(EncryptorV2.AES_IV_SIZE)
for i in range(len(initialVector)):
initialVector[i] = _systemRandom.randint(0, 0xff)
params = EncryptParams(EncryptAlgorithmType.AesCbc)
params.setInitialVector(Blob(initialVector, False))
encryptedData = AesAlgorithm.encrypt(
Blob(self._ckBits, False), Blob(plainData, False), params)
content = EncryptedContent()
content.setInitialVector(params.getInitialVector())
content.setPayload(encryptedData)
content.setKeyLocatorName(self._ckName)
return content
[docs] def regenerateCk(self):
"""
Create a new Content Key (CK) and publish the corresponding CK Data
packet. This uses the onError given to the constructor to report errors.
"""
# TODO: Ensure that the CK Data packet for the old CK is published when
# the CK is updated before the KEK is fetched.
self._ckName = Name(self._ckPrefix)
self._ckName.append(EncryptorV2.NAME_COMPONENT_CK)
# The version is the ID of the CK.
self._ckName.appendVersion(int(Common.getNowMilliseconds()))
logging.getLogger(__name__).info("Generating new CK: " +
self._ckName.toUri())
for i in range(len(self._ckBits)):
self._ckBits[i] = _systemRandom.randint(0, 0xff)
# One implication: If the CK is updated before the KEK is fetched, then
# the KDK for the old CK will not be published.
if self._kekData == None:
self._retryFetchingKek()
else:
self._makeAndPublishCkData(self._onError)
[docs] def size(self):
"""
Get the number of packets stored in in-memory storage.
:return: The number of packets.
:rtype: int
"""
return self._storage.size()
def _retryFetchingKek(self):
if self._isKekRetrievalInProgress:
return
logging.getLogger(__name__).info("Retrying fetching of the KEK")
self._isKekRetrievalInProgress = True
def onReady():
logging.getLogger(__name__).info("The KEK was retrieved and published")
self._isKekRetrievalInProgress = False
def onError(errorCode, message):
logging.getLogger(__name__).info("Failed to retrieve KEK: " + message)
self._isKekRetrievalInProgress = False
self._onError(errorCode, message)
self._fetchKekAndPublishCkData(onReady, onError, EncryptorV2.N_RETRIES)
def _fetchKekAndPublishCkData(self, onReady, onError, nTriesLeft):
"""
Create an Interest for <access-prefix>/KEK to retrieve the
<access-prefix>/KEK/<key-id> KEK Data packet, and set _kekData.
:param onReady: When the KEK is retrieved and published, this calls
onReady().
:type onError: function object
:param onError: On failure, this calls onError(errorCode, message)
where errorCode is from EncryptError.ErrorCode, and message is an
error string.
:type onError: function object
:param int nTriesLeft: The number of retries for expressInterest timeouts.
"""
logging.getLogger(__name__).info("Fetching KEK: " +
Name(self._accessPrefix).append(EncryptorV2.NAME_COMPONENT_KEK).toUri())
if self._kekPendingInterestId > 0:
onError(EncryptError.ErrorCode.General,
"fetchKekAndPublishCkData: There is already a _kekPendingInterestId")
return
def onData(interest, kekData):
self._kekPendingInterestId = 0
# TODO: Verify if the key is legitimate.
self._kekData = kekData
if self._makeAndPublishCkData(onError):
onReady()
# Otherwise, failure has already been reported.
def onTimeout(interest):
self._kekPendingInterestId = 0
if nTriesLeft > 1:
self._fetchKekAndPublishCkData(onReady, onError, nTriesLeft - 1)
else:
onError(EncryptError.ErrorCode.KekRetrievalTimeout,
"Retrieval of KEK [" + interest.getName().toUri() + "] timed out")
logging.getLogger(__name__).info(
"Scheduling retry after all timeouts")
self._face.callLater(
EncryptorV2.RETRY_DELAY_KEK_RETRIEVAL_MS, self._retryFetchingKek)
def onNetworkNack(interest, networkNack):
self._kekPendingInterestId = 0
if nTriesLeft > 1:
def callback():
self._fetchKekAndPublishCkData(onReady, onError, nTriesLeft - 1)
self._face.callLater(EncryptorV2.RETRY_DELAY_AFTER_NACK_MS, callback)
else:
onError(EncryptError.ErrorCode.KekRetrievalFailure,
"Retrieval of KEK [" + interest.getName().toUri() +
"] failed. Got NACK (" + str(networkNack.getReason()) + ")")
logging.getLogger(__name__).info("Scheduling retry from NACK")
self._face.callLater(
EncryptorV2.RETRY_DELAY_KEK_RETRIEVAL_MS, self._retryFetchingKek)
try:
self._kekPendingInterestId = self._face.expressInterest(
Interest(Name(self._accessPrefix).append(EncryptorV2.NAME_COMPONENT_KEK))
.setMustBeFresh(True)
.setCanBePrefix(True),
onData, onTimeout, onNetworkNack)
except Exception as ex:
onError(EncryptError.ErrorCode.General,
"expressInterest error: " + repr(ex))
def _makeAndPublishCkData(self, onError):
"""
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(errorCode, message) where
errorCode is from EncryptError.ErrorCode, and message is an error
string.
:type onError: function object
:return: True on success, else False.
:rtype: bool
"""
try:
kek = PublicKey(self._kekData.getContent())
content = EncryptedContent()
content.setPayload(kek.encrypt
(Blob(self._ckBits, False), EncryptAlgorithmType.RsaOaep))
ckData = Data(
Name(self._ckName).append(EncryptorV2.NAME_COMPONENT_ENCRYPTED_BY)
.append(self._kekData.getName()))
ckData.setContent(content.wireEncodeV2())
# FreshnessPeriod can serve as a soft access control for revoking access.
ckData.getMetaInfo().setFreshnessPeriod(
EncryptorV2.DEFAULT_CK_FRESHNESS_PERIOD_MS)
self._keyChain.sign(ckData, self._ckDataSigningInfo)
self._storage.insert(ckData)
logging.getLogger(__name__).info("Publishing CK data: " +
ckData.getName().toUri())
return True
except Exception as ex:
onError(EncryptError.ErrorCode.EncryptionFailure,
"Failed to encrypt generated CK with KEK " +
self._kekData.getName().toUri())
return False
NAME_COMPONENT_ENCRYPTED_BY = Name.Component("ENCRYPTED-BY")
NAME_COMPONENT_NAC = Name.Component("NAC")
NAME_COMPONENT_KEK = Name.Component("KEK")
NAME_COMPONENT_KDK = Name.Component("KDK")
NAME_COMPONENT_CK = Name.Component("CK")
RETRY_DELAY_AFTER_NACK_MS = 1000.0
RETRY_DELAY_KEK_RETRIEVAL_MS = 60 * 1000.0
AES_KEY_SIZE = 32
AES_IV_SIZE = 16
N_RETRIES = 3
DEFAULT_CK_FRESHNESS_PERIOD_MS = 3600 * 1000.0
# Import this at the end of the file to avoid circular references.
from pyndn.security.signing_info import SigningInfo