# -*- Mode:python; c-file-style:"gnu"; indent-tabs-mode:nil -*- */
#
# Copyright (C) 2015-2016 Regents of the University of California.
# Author: Jeff Thompson <jefft0@remap.ucla.edu>
# Author: From ndn-group-encrypt src/consumer https://github.com/named-data/ndn-group-encrypt
#
# 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 Consumer class which manages fetched group keys used to
decrypt a data packet in the group-based encryption protocol.
Note: This class is an experimental feature. The API may change.
"""
import logging
from pyndn.name import Name
from pyndn.interest import Interest
from pyndn.util.blob import Blob
from pyndn.encrypt.encrypted_content import EncryptedContent
from pyndn.encrypt.encrypt_error import EncryptError
from pyndn.encrypt.algo.aes_algorithm import AesAlgorithm
from pyndn.encrypt.algo.rsa_algorithm import RsaAlgorithm
from pyndn.encrypt.algo.encrypt_params import EncryptParams, EncryptAlgorithmType
from pyndn.encrypt.algo.encryptor import Encryptor
[docs]class Consumer(object):
"""
Create a Consumer to use the given ConsumerDb, Face and other values.
:param Face face: The face used for data packet and key fetching.
:param KeyChain keyChain: The keyChain used to verify data packets.
:param Name groupName: The reading group name that the consumer belongs to.
This makes a copy of the Name.
:param Name consumerName: The identity of the consumer. This makes a copy of
the Name.
:param ConsumerDb database: The ConsumerDb database for storing decryption
keys.
"""
def __init__(self, face, keyChain, groupName, consumerName, database):
self._database = database
self._keyChain = keyChain
self._face = face
self._groupName = Name(groupName)
self._consumerName = Name(consumerName)
# The dictionary key is the C-KEY name. The value is the encoded key Blob.
self._cKeyMap = {}
# The dictionary key is the D-KEY name. The value is the encoded key Blob.
self._dKeyMap = {}
[docs] def consume(self, contentName, onConsumeComplete, onError):
"""
Express an Interest to fetch the content packet with contentName, and
decrypt it, fetching keys as needed.
:param Name contentName: The name of the content packet.
:param onConsumeComplete: When the content packet is fetched and
decrypted, this calls onConsumeComplete(contentData, result) where
contentData is the fetched Data packet and result is the decrypted
plain text Blob.
NOTE: The library will log any exceptions raised by this callback, but
for better error handling the callback should catch and properly
handle any exceptions.
:type onPlainText: function object
:param onError: This calls onError(errorCode, message) for an error,
where errorCode is from EncryptError.ErrorCode and message is a str.
NOTE: The library will log any exceptions raised by this callback, but
for better error handling the callback should catch and properly
handle any exceptions.
:type onError: function object
"""
interest = Interest(contentName)
# Prepare the callback functions.
def onData(contentInterest, contentData):
# The Interest has no selectors, so assume the library correctly
# matched with the Data name before calling onData.
try:
def onVerified(validData):
# Decrypt the content.
def onPlainText(plainText):
try:
onConsumeComplete(contentData, plainText)
except:
logging.exception("Error in onConsumeComplete")
self._decryptContent(validData, onPlainText, onError)
self._keyChain.verifyData(
contentData, onVerified,
lambda d: Consumer._callOnError(onError, EncryptError.ErrorCode.Validation,
"verifyData failed"))
except Exception as ex:
try:
onError(EncryptError.ErrorCode.General, "verifyData error: " + repr(ex))
except:
logging.exception("Error in onError")
def onTimeout(contentInterest):
# We should re-try at least once.
try:
self._face.expressInterest(
interest, onData,
lambda contentInterest:
Consumer._callOnError(onError,
EncryptError.ErrorCode.Timeout, interest.getName().toUri()))
except Exception as ex:
try:
onError(EncryptError.ErrorCode.General,
"expressInterest error: " + repr(ex))
except:
logging.exception("Error in onError")
# Express the Interest.
try:
self._face.expressInterest(interest, onData, onTimeout)
except Exception as ex:
try:
onError(EncryptError.ErrorCode.General,
"expressInterest error: " + repr(ex))
except:
logging.exception("Error in onError")
[docs] def setGroup(self, groupName):
"""
Set the group name.
:param Name groupName: The reading group name that the consumer belongs
to. This makes a copy of the Name.
"""
self._groupName = Name(groupName)
[docs] def addDecryptionKey(self, keyName, keyBlob):
"""
Add a new decryption key with keyName and keyBlob to the database.
:param Name keyName: The key name.
:param Blob keyBlob: The encoded key.
:raises ConsumerDb.Error: If a key with the same keyName already exists
in the database, or other database error.
:raises RuntimeError: if the consumer name is not a prefix of the key name.
"""
if not self._consumerName.match(keyName):
raise RuntimeError(
"addDecryptionKey: The consumer name must be a prefix of the key name")
self._database.addKey(keyName, keyBlob)
@staticmethod
def _decrypt(encryptedContent, keyBits, onPlainText, onError):
"""
Decrypt encryptedContent using keyBits.
:param encryptedContent: The EncryptedContent to decrypt, or a Blob
which is first decoded as an EncryptedContent.
:type encryptedContent: Blob or EncryptedContent
:param {Blob} keyBits The key value.
:param onPlainText: When encryptedBlob is decrypted, this calls
onPlainText(decryptedBlob) with the decrypted Blob.
:type onPlainText: function object
:param onError: This calls onError(errorCode, message) for an error,
where errorCode is from EncryptError.ErrorCode and message is a str.
:type onError: function object
"""
if isinstance(encryptedContent, Blob):
# Decode as EncryptedContent.
encryptedBlob = encryptedContent
encryptedContent = EncryptedContent()
encryptedContent.wireDecode(encryptedBlob)
payload = encryptedContent.getPayload()
if encryptedContent.getAlgorithmType() == EncryptAlgorithmType.AesCbc:
# Prepare the parameters.
decryptParams = EncryptParams(EncryptAlgorithmType.AesCbc)
decryptParams.setInitialVector(encryptedContent.getInitialVector())
# Decrypt the content.
try:
content = AesAlgorithm.decrypt(keyBits, payload, decryptParams)
except Exception as ex:
try:
onError(EncryptError.ErrorCode.InvalidEncryptedFormat, repr(ex))
except:
logging.exception("Error in onError")
return
onPlainText(content)
elif encryptedContent.getAlgorithmType() == EncryptAlgorithmType.RsaOaep:
# Prepare the parameters.
decryptParams = EncryptParams(EncryptAlgorithmType.RsaOaep)
# Decrypt the content.
try:
content = RsaAlgorithm.decrypt(keyBits, payload, decryptParams)
except Exception as ex:
Consumer._callOnError(onError,
EncryptError.ErrorCode.InvalidEncryptedFormat, repr(ex))
return
onPlainText(content)
else:
Consumer._callOnError(onError,
EncryptError.ErrorCode.UnsupportedEncryptionScheme,
repr(encryptedContent.getAlgorithmType()))
def _decryptContent(self, data, onPlainText, onError):
"""
Decrypt the data packet.
:param Data data: The data packet. This does not verify the packet.
:param onPlainText: When the data packet is decrypted, this calls
onPlainText(decryptedBlob) with the decrypted blob.
:type onPlainText: function object
:param onError: This calls onError(errorCode, message) for an error,
where errorCode is from EncryptError.ErrorCode and message is a str.
:type onError: function object
"""
# Get the encrypted content.
dataEncryptedContent = EncryptedContent()
try:
dataEncryptedContent.wireDecode(data.getContent())
except Exception as ex:
Consumer._callOnError(onError,
EncryptError.ErrorCode.InvalidEncryptedFormat, repr(ex))
return
cKeyName = dataEncryptedContent.getKeyLocator().getKeyName()
# Check if the content key is already in the store.
if cKeyName in self._cKeyMap:
cKey = self._cKeyMap[cKeyName]
self._decrypt(dataEncryptedContent, cKey, onPlainText, onError)
else:
# Retrieve the C-KEY Data from the network.
interestName = Name(cKeyName)
interestName.append(Encryptor.NAME_COMPONENT_FOR).append(self._groupName)
interest = Interest(interestName)
# Prepare the callback functions.
def onData(cKeyInterest, cKeyData):
# The Interest has no selectors, so assume the library correctly
# matched with the Data name before calling onData.
try:
def onVerified(validCKeyData):
def localOnPlainText(cKeyBits):
# cKeyName is already a copy inside the local
# dataEncryptedContent.
self._cKeyMap[cKeyName] = cKeyBits
Consumer._decrypt(
dataEncryptedContent, cKeyBits, onPlainText, onError)
self._decryptCKey(validCKeyData, localOnPlainText, onError)
self._keyChain.verifyData(
cKeyData, onVerified,
lambda d: Consumer._callOnError(onError, EncryptError.ErrorCode.Validation,
"verifyData failed"))
except Exception as ex:
try:
onError(EncryptError.ErrorCode.General,
"verifyData error: " + repr(ex))
except:
logging.exception("Error in onError")
def onTimeout(dKeyInterest):
# We should re-try at least once.
try:
self._face.expressInterest(
interest, onData,
lambda contentInterest:
Consumer._callOnError(onError,
EncryptError.ErrorCode.Timeout, interest.getName().toUri()))
except Exception as ex:
try:
onError(EncryptError.ErrorCode.General,
"expressInterest error: " + repr(ex))
except:
logging.exception("Error in onError")
# Express the Interest.
try:
self._face.expressInterest(interest, onData, onTimeout)
except Exception as ex:
try:
onError(EncryptError.ErrorCode.General,
"expressInterest error: " + repr(ex))
except:
logging.exception("Error in onError")
def _decryptCKey(self, cKeyData, onPlainText, onError):
"""
Decrypt cKeyData.
:param Data cKeyData: The C-KEY data packet.
:param onPlainText: When the data packet is decrypted, this calls
onPlainText(decryptedBlob) with the decrypted blob.
:type onPlainText: function object
:param onError: This calls onError(errorCode, message) for an error,
where errorCode is from EncryptError.ErrorCode and message is a str.
:type onError: function object
"""
# Get the encrypted content.
cKeyContent = cKeyData.getContent()
cKeyEncryptedContent = EncryptedContent()
try:
cKeyEncryptedContent.wireDecode(cKeyContent)
except Exception as ex:
try:
onError(EncryptError.ErrorCode.InvalidEncryptedFormat, repr(ex))
except:
logging.exception("Error in onError")
return
eKeyName = cKeyEncryptedContent.getKeyLocator().getKeyName()
dKeyName = eKeyName.getPrefix(-3)
dKeyName.append(Encryptor.NAME_COMPONENT_D_KEY).append(
eKeyName.getSubName(-2))
# Check if the decryption key is already in the store.
if dKeyName in self._dKeyMap:
dKey = self._dKeyMap[dKeyName]
Consumer._decrypt(cKeyEncryptedContent, dKey, onPlainText, onError)
else:
# Get the D-Key Data.
interestName = Name(dKeyName)
interestName.append(Encryptor.NAME_COMPONENT_FOR).append(
self._consumerName)
interest = Interest(interestName)
# Prepare the callback functions.
def onData(dKeyInterest, dKeyData):
# The Interest has no selectors, so assume the library correctly
# matched with the Data name before calling onData.
try:
def onVerified(validDKeyData):
def localOnPlainText(dKeyBits):
# dKeyName is already a local copy.
self._dKeyMap[dKeyName] = dKeyBits
Consumer._decrypt(
cKeyEncryptedContent, dKeyBits, onPlainText, onError)
self._decryptDKey(validDKeyData, localOnPlainText, onError)
self._keyChain.verifyData(
dKeyData, onVerified,
lambda d: Consumer._callOnError(onError, EncryptError.ErrorCode.Validation,
"verifyData failed"))
except Exception as ex:
try:
onError(EncryptError.ErrorCode.General,
"verifyData error: " + repr(ex))
except:
logging.exception("Error in onError")
def onTimeout(dKeyInterest):
# We should re-try at least once.
try:
self._face.expressInterest(
interest, onData,
lambda contentInterest:
Consumer._callOnError(onError,
EncryptError.ErrorCode.Timeout, interest.getName().toUri()))
except Exception as ex:
try:
onError(EncryptError.ErrorCode.General,
"expressInterest error: " + repr(ex))
except:
logging.exception("Error in onError")
# Express the Interest.
try:
self._face.expressInterest(interest, onData, onTimeout)
except Exception as ex:
try:
onError(EncryptError.ErrorCode.General,
"expressInterest error: " + repr(ex))
except:
logging.exception("Error in onError")
def _decryptDKey(self, dKeyData, onPlainText, onError):
"""
Decrypt dKeyData.
:param Data dKeyData: The D-KEY data packet.
:param onPlainText: When the data packet is decrypted, this calls
onPlainText(decryptedBlob) with the decrypted blob.
:type onPlainText: function object
:param onError: This calls onError(errorCode, message) for an error,
where errorCode is from EncryptError.ErrorCode and message is a str.
:type onError: function object
"""
# Get encrypted content.
dataContent = dKeyData.getContent()
# Process the nonce.
# dataContent is a sequence of the two EncryptedContent.
encryptedNonce = EncryptedContent()
try:
encryptedNonce.wireDecode(dataContent)
except Exception as ex:
try:
onError(EncryptError.ErrorCode.InvalidEncryptedFormat, repr(ex))
except:
logging.exception("Error in onError")
return
consumerKeyName = encryptedNonce.getKeyLocator().getKeyName()
# Get consumer decryption key.
try:
consumerKeyBlob = self._getDecryptionKey(consumerKeyName)
except Exception as ex:
Consumer._callOnError(onError,
EncryptError.ErrorCode.NoDecryptKey, "Database error: " + repr(ex))
return
if consumerKeyBlob.size() == 0:
try:
onError(EncryptError.ErrorCode.NoDecryptKey,
"The desired consumer decryption key in not in the database")
except:
logging.exception("Error in onError")
return
# Process the D-KEY.
# Use the size of encryptedNonce to find the start of encryptedPayload.
encryptedPayloadBlob = Blob(
dataContent.buf()[encryptedNonce.wireEncode().size():], False)
if encryptedPayloadBlob.size() == 0:
try:
onError(EncryptError.ErrorCode.InvalidEncryptedFormat,
"The data packet does not satisfy the D-KEY packet format")
except:
logging.exception("Error in onError")
# Decrypt the D-KEY.
Consumer._decrypt(
encryptedNonce, consumerKeyBlob,
lambda nonceKeyBits: Consumer._decrypt(
encryptedPayloadBlob, nonceKeyBits, onPlainText, onError),
onError)
def _getDecryptionKey(self, decryptionKeyName):
"""
Get the encoded blob of the decryption key with decryptionKeyName from
the database.
:param Name decryptionKeyName: The key name.
:return: A Blob with the encoded key, or an isNull Blob if cannot find
the key with keyName.
:rtype: Blob
:raises ConsumerDb.Error: For a database error.
"""
return self._database.getKey(decryptionKeyName)
@staticmethod
def _callOnError(onError, errorCode, message):
"""
Call onError(errorCode, message) within a try block to log exceptions
that it throws. We name this separate helper function to use inside
lambda expressions.
"""
try:
onError(errorCode, message)
except:
logging.exception("Error in onError")