Source code for pyndn.encrypt.consumer

# -*- Mode:python; c-file-style:"gnu"; indent-tabs-mode:nil -*- */
#
# Copyright (C) 2015-2019 Regents of the University of California.
# Author: Jeff Thompson <[email protected]>
# 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.network_nack import NetworkNack
from pyndn.link import Link
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. :param Link cKeyLink: (optional) The Link object to use in Interests for C-KEY retrieval. This makes a copy of the Link object. If the Link object's getDelegations().size() is zero, don't use it. If omitted, don't use a Link object. :param Link dKeyLink: (optional) The Link object to use in Interests for D-KEY retrieval. This makes a copy of the Link object. If the Link object's getDelegations().size() is zero, don't use it. If omitted, don't use a Link object. """ def __init__(self, face, keyChain, groupName, consumerName, database, cKeyLink = None, dKeyLink = None): self._database = database self._keyChain = keyChain self._face = face self._groupName = Name(groupName) self._consumerName = Name(consumerName) self._cKeyLink = (Consumer.NO_LINK if cKeyLink == None else Link(cKeyLink)) self._dKeyLink = (Consumer.NO_LINK if dKeyLink == None else Link(dKeyLink)) # 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, link = None): """ 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 :param Link link: (optional) The Link object to use in Interests for data retrieval. This makes a copy of the Link object. If the Link object's getDelegations().size() is zero, don't use it. If omitted, don't use a Link object. """ if link == None: link = Consumer.NO_LINK interest = Interest(contentName) def onVerified(validData): # Decrypt the content. def onPlainText(plainText): try: onConsumeComplete(validData, plainText) except: logging.exception("Error in onConsumeComplete") self._decryptContent(validData, onPlainText, onError) # Copy the Link object since the passed link may become invalid. self._sendInterest(interest, 1, Link(link), onVerified, 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) 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._sendInterest(interest, 1, self._cKeyLink, onVerified, 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) 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._sendInterest(interest, 1, self._dKeyLink, onVerified, 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") return # Decrypt the D-KEY. Consumer._decrypt( encryptedNonce, consumerKeyBlob, lambda nonceKeyBits: Consumer._decrypt( encryptedPayloadBlob, nonceKeyBits, onPlainText, onError), onError) def _sendInterest(self, interest, nRetrials, link, onVerified, onError): """ Express the interest, call verifyData for the fetched Data packet and call onVerified if verify succeeds. If verify fails, call onError(EncryptError.ErrorCode.Validation, "verifyData failed"). If the interest times out, re-express nRetrials times. If the interest times out nRetrials times, or for a network Nack, call onError(EncryptError.ErrorCode.DataRetrievalFailure, interest.getName().toUri()). :param Interest interest: The Interest to express. :param int nRetrials: The number of retrials left after a timeout. :param Link link: The Link object to use in the Interest. This does not make a copy of the Link object. If the Link object's getDelegations().size() is zero, don't use it. :param onVerified: When the fetched Data packet validation succeeds, this calls onVerified(data). :type onVerified: 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 """ # 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: self._keyChain.verifyData( contentData, onVerified, lambda d, reason: Consumer._callOnError( onError, EncryptError.ErrorCode.Validation, "verifyData failed. Reason: " + reason)) except Exception as ex: try: onError(EncryptError.ErrorCode.General, "verifyData error: " + repr(ex)) except: logging.exception("Error in onError") def onNetworkNack(interest, networkNack): # We have run out of options. Report a retrieval failure. try: onError(EncryptError.ErrorCode.DataRetrievalFailure, interest.getName().toUri()) except: logging.exception("Error in onError") def onTimeout(interest): if nRetrials > 0: self._sendInterest(interest, nRetrials - 1, link, onVerified, onError) else: onNetworkNack(interest, NetworkNack()) if link.getDelegations().size() == 0: # We can use the supplied interest without copying. request = interest else: # Copy the supplied interest and add the Link. request = Interest(interest) # This will use a cached encoding if available. request.setLinkWireEncoding(link.wireEncode()) try: self._face.expressInterest(request, onData, onTimeout, onNetworkNack) except Exception as ex: try: onError(EncryptError.ErrorCode.General, "expressInterest error: " + repr(ex)) except: logging.exception("Error in 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") NO_LINK = Link()