PK!diderypy/__init__.pyPK!o33diderypy/app.pyimport ioflo.app.run def main(): print("MAIN") """ Main entry point for ioserve CLI""" # from didery import __version__ args = ioflo.app.run.parseArgs(version="0.0.1") # inject version here ioflo.app.run.run( name=args.name, period=float(args.period), real=args.realtime, retro=args.retrograde, filepath=args.filename, behaviors=args.behaviors, mode=args.parsemode, username=args.username, password=args.password, verbose=args.verbose, consolepath=args.console, statistics=args.statistics) if __name__ == '__main__': main() PK!A>E--diderypy/cli.py""" Module that contains the command line app. Why does this file exist, and why not put this in __main__? You might be tempted to import things from __main__ later, but that will cause problems: the code will get executed twice: - When you run `python -m py-dideryd` python will execute ``__main__.py`` as a script. That means there won't be any ``py-didery.__main__`` in ``sys.modules``. - When you import __main__ it will get executed again (as a module) because there's no ``py-didery.__main__`` in ``sys.modules``. Also see (1) from http://click.pocoo.org/5/setuptools/#setuptools-integration """ import os import click import ioflo.app.run from ioflo.aid import odict from diderypy.help import helping as h from diderypy.diderying import ValidationError from diderypy.lib import generating as gen try: import simplejson as json except ImportError: import json """ Command line interface for didery.py library. Path to config file containing server list required """ @click.command() @click.option( '--incept', '-i', multiple=False, is_flag=True, default=False, help="Send a key rotation history inception event." ) @click.option( '--upload', '-u', is_flag=True, default=False, help="Upload a new otp encrypted private key." ) @click.option( '--rotate', '-r', multiple=False, is_flag=True, default=False, help='Rotate public/private key pairs.' ) @click.option( '--update', '-U', multiple=False, is_flag=True, default=False, help='Update otp encrypted private key.' ) @click.option( '--retrieve', '-R', multiple=False, is_flag=True, default=False, help="Retrieve key rotation history." ) @click.option( '--download', '-d', multiple=False, is_flag=True, default=False, help="Download otp encrypted private key." ) @click.option( '--delete', '-D', multiple=False, is_flag=True, default=False, help="Delete rotation history." ) @click.option( '--remove', '-m', multiple=False, is_flag=True, default=False, help="Remove otp encrypted private key." ) @click.option( '--events', '-e', multiple=False, is_flag=True, default=False, help="Pull a record of all history rotation events for a specified did." ) @click.option( '-v', multiple=False, count=True, help="Verbosity of console output. There are 5 verbosity levels from '' to '-vvvv.'" ) @click.option( '--mute', '-M', multiple=False, is_flag=True, default=False, help="Mute all console output except prompts." ) @click.option( '--data', multiple=False, type=click.Path(exists=True, file_okay=True, dir_okay=False, readable=True, resolve_path=True), help="Path to the data file." ) @click.option( '--did', multiple=False, help="decentralized identifier(did)." ) @click.argument( 'config', type=click.Path(exists=True, file_okay=True, dir_okay=False, readable=True, resolve_path=True), ) def main(incept, upload, rotate, update, retrieve, download, delete, remove, events, v, mute, data, did, config): if mute: verbose = 0 else: v = 1 if v == 0 else v verbose = v if v <= 4 else 4 preloads = [ ('.main.incept.verbosity', odict(value=verbose)), ('.main.upload.verbosity', odict(value=verbose)), ('.main.rotate.verbosity', odict(value=verbose)), ('.main.update.verbosity', odict(value=verbose)), ('.main.retrieve.verbosity', odict(value=verbose)), ('.main.download.verbosity', odict(value=verbose)), ('.main.delete.verbosity', odict(value=verbose)), ('.main.remove.verbosity', odict(value=verbose)), ('.main.events.verbosity', odict(value=verbose)), ('.main.incept.start', odict(value=True if incept else False)), ('.main.upload.start', odict(value=True if upload else False)), ('.main.rotate.start', odict(value=True if rotate else False)), ('.main.update.start', odict(value=True if update else False)), ('.main.retrieve.start', odict(value=True if retrieve else False)), ('.main.download.start', odict(value=True if download else False)), ('.main.delete.start', odict(value=True if delete else False)), ('.main.remove.start', odict(value=True if remove else False)), ('.main.events.start', odict(value=True if events else False)) ] options = [incept, upload, rotate, update, retrieve, download, delete, remove, events] count = options.count(True) if count > 1: click.echo("Cannot combine --incept --upload, --rotate, --update, --retrieve, --download, --delete, --remove, or --events.") return if count == 0: click.echo("No options given. For help use --help. Exiting Didery.py") return try: configData = h.parseConfigFile(config) except ValidationError as err: click.echo("Error parsing the config file: {}.".format(err)) return try: if incept: preloads.extend(inceptSetup(configData, data)) if upload: preloads.extend(uploadSetup(configData, data)) if rotate: preloads.extend(rotateSetup(configData, data)) if update: preloads.extend(updateSetup(configData, data)) if retrieve: preloads.extend(retrieveSetup(configData, did)) if download: preloads.extend(downloadSetup(configData, did)) if delete: preloads.extend(deleteSetup(configData, did)) if remove: preloads.extend(removeSetup(configData, did)) if events: preloads.extend(eventsSetup(configData, did)) except (ValidationError, ValueError) as ex: click.echo("Error setting up didery.py: {}.".format(ex)) return projectDirpath = os.path.dirname( os.path.dirname( os.path.abspath( os.path.expanduser(__file__) ) ) ) floScriptpath = os.path.join(projectDirpath, "diderypy/flo/main.flo") """ Main entry point for ioserve CLI""" ioflo.app.run.run( name="didery.py", period=0.125, real=True, retro=True, filepath=floScriptpath, behaviors=['diderypy.core'], mode='', username='', password='', verbose=0, consolepath='', statistics=False, preloads=preloads) def inceptSetup(config, data): if data is None: history, sk = historyInit() data = history else: data = h.parseDataFile(data, "history") sk = click.prompt("Enter your signing/private key") preloads = [ ('.main.incept.servers', odict(value=config["servers"])), ('.main.incept.data', odict(value=data)), ('.main.incept.sk', odict(value=sk)) ] return preloads def uploadSetup(config, data): if data is None: raise ValueError("Data file required. Use --data=path/to/file") data = h.parseDataFile(data, "otp") sk = click.prompt("Enter your signing/private key") preloads = [ ('.main.upload.servers', odict(value=config["servers"])), ('.main.upload.data', odict(value=data)), ('.main.upload.sk', odict(value=sk)) ] return preloads def rotateSetup(config, data): if data is None: raise ValueError("Data file required. Use --data=path/to/file") data = h.parseDataFile(data, "history") csk = click.prompt("Enter your current signing/private key") rsk = click.prompt("Enter your pre-rotated signing/private key") if click.confirm("Generate a new pre-rotated key pair?"): pvk, psk = keyery() data["signers"].append(pvk) data["signer"] = int(data["signer"]) + 1 preloads = [ ('.main.rotate.servers', odict(value=config["servers"])), ('.main.rotate.data', odict(value=data)), ('.main.rotate.did', odict(value=data["id"])), ('.main.rotate.sk', odict(value=csk)), ('.main.rotate.psk', odict(value=rsk)) ] return preloads def updateSetup(config, data): if data is None: raise ValueError("Data file required. Use --data=path/to/file") data = h.parseDataFile(data, "otp") sk = click.prompt("Enter your signing/private key") preloads = [ ('.main.update.servers', odict(value=config["servers"])), ('.main.update.data', odict(value=data)), ('.main.update.did', odict(value=data["id"])), ('.main.update.sk', odict(value=sk)) ] return preloads def retrieveSetup(config, did): if did is None: raise ValueError("did required. Use --did") h.validateDid(did) preloads = [ ('.main.retrieve.servers', odict(value=config["servers"])), ('.main.retrieve.did', odict(value=did)) ] return preloads def downloadSetup(config, did): if did is None: raise ValueError("did required. Use --did") h.validateDid(did) preloads = [ ('.main.download.servers', odict(value=config["servers"])), ('.main.download.did', odict(value=did)) ] return preloads def deleteSetup(config, did): if did is None: raise ValueError("did required. Use --did") h.validateDid(did) sk = click.prompt("Enter your signing/private key") preloads = [ ('.main.delete.servers', odict(value=config["servers"])), ('.main.delete.did', odict(value=did)), ('.main.delete.sk', odict(value=sk)) ] return preloads def removeSetup(config, did): if did is None: raise ValueError("did required. Use --did") h.validateDid(did) sk = click.prompt("Enter your signing/private key") preloads = [ ('.main.remove.servers', odict(value=config["servers"])), ('.main.remove.did', odict(value=did)), ('.main.remove.sk', odict(value=sk)) ] return preloads def eventsSetup(config, did): if did is None: raise ValueError("did required. Use --did") h.validateDid(did) preloads = [ ('.main.events.servers', odict(value=config["servers"])), ('.main.events.did', odict(value=did)) ] return preloads def historyInit(): history, vk, sk, pvk, psk = gen.historyGen() with open('didery.keys.json', 'w') as keyFile: keys = { "current_sk": sk, "current_vk": vk, "pre_rotated_sk": psk, "pre_rotated_vk": pvk } keyFile.write(json.dumps(keys, encoding='utf-8')) click.prompt('\nKeys generated in ./didery.keys.json. \n' 'Make a copy and store them securely. \n\n' 'The file will be deleted after pressing any key+Enter') os.remove('didery.keys.json') click.echo('didery.keys.json deleted.') return history, sk def keyery(): vk, sk, did = gen.keyGen() with open('didery.keys.json', 'w') as keyFile: keys = { "signing_key": sk, "verification_key": vk } keyFile.write(json.dumps(keys, encoding='utf-8')) click.prompt('\nKeys generated in ./didery.keys.json. \n' 'Make a copy and store them securely. \n\n' 'The file will be deleted after pressing any key+Enter') os.remove('didery.keys.json') click.echo('didery.keys.json deleted.') return vk, sk PK!w6diderypy/core/__init__.py""" core package flo behaviors """ import importlib _modules = ['behaving', ] for m in _modules: importlib.import_module(".{0}".format(m), package='diderypy.core') PK!bf  diderypy/core/behaving.pytry: import simplejson as json except ImportError: import json from urllib.parse import urlparse from ioflo.aid import getConsole from ioflo.aid import odict from ioflo.base import doify from ..lib import historying as hist from ..lib import otping as otp from ..lib import history_eventing as event def outputSetupInfo(console, servers, data=None, did=None): console.terse("\n") console.concise("Servers:\n{}\n\n".format(servers)) if data: console.profuse("Data:\n{}\n\n".format(data)) if did: console.profuse("DID: {}\n\n".format(did)) def outputPushedResult(result, console, verbosity): successful = 0 profuse = "" concise = "" for url, data in result.items(): parsed_url = urlparse(url) if verbosity == console.Wordage.profuse: profuse += "{}://{}:\t{}\n".format(parsed_url.scheme, parsed_url.netloc, data) if verbosity == console.Wordage.concise: if data.status == 0: concise += "{}://{}:\tRequest Timed Out\n".format(parsed_url.scheme, parsed_url.netloc) else: concise += "{}://{}:\tHTTP_{}\n".format(parsed_url.scheme, parsed_url.netloc, data.status) if 300 > data.status >= 200: successful += 1 console.terse("\n{}/{} requests succeeded.\n\n".format(successful, len(result))) console.concise(concise) console.profuse(profuse) def outputPulledResult(data, results, console): if data: formatted_data = json.dumps(data, indent=4) console.terse("Data:\t{}\n".format(formatted_data)) for url, result in results.items(): console.verbose("{}\n".format(result)) else: console.terse("Consensus Failed.\n") for url, result in results.items(): console.concise("{}\n".format(result)) @doify('Incept', ioinits=odict( servers="", data="", sk="", test="", complete=odict(value=False), verbosity="", start="" )) def incept(self): if not self.start.value: self.complete.value = True return console = getConsole("didery.py", verbosity=self.verbosity.value) outputSetupInfo(console, self.servers.value, data=self.data.value) result = hist.postHistory(self.data.value, self.sk.value, self.servers.value) outputPushedResult(result, console, self.verbosity.value) self.complete.value = True @doify('Upload', ioinits=odict( servers="", data="", sk="", test="", complete=odict(value=False), verbosity="", start="" )) def upload(self): if not self.start.value: self.complete.value = True return console = getConsole("didery.py", verbosity=self.verbosity.value) outputSetupInfo(console, self.servers.value, data=self.data.value) result = otp.postOtpBlob(self.data.value, self.sk.value, self.servers.value) outputPushedResult(result, console, self.verbosity.value) self.complete.value = True @doify('Rotation', ioinits=odict( servers="", data="", did="", sk="", psk="", test="", complete=odict(value=False), verbosity="", start="" )) def rotation(self): if not self.start.value: self.complete.value = True return console = getConsole("didery.py", verbosity=self.verbosity.value) outputSetupInfo(console, self.servers.value, data=self.data.value) result = hist.putHistory(self.data.value, self.sk.value, self.psk.value, self.servers.value) outputPushedResult(result, console, self.verbosity.value) self.complete.value = True @doify('Update', ioinits=odict( servers="", data="", did="", sk="", test="", complete=odict(value=False), verbosity="", start="" )) def update(self): if not self.start.value: self.complete.value = True return console = getConsole("didery.py", verbosity=self.verbosity.value) outputSetupInfo(console, self.servers.value, data=self.data.value) result = otp.putOtpBlob(self.data.value, self.sk.value, self.servers.value) outputPushedResult(result, console, self.verbosity.value) self.complete.value = True @doify('Retrieval', ioinits=odict( servers="", did="", test="", complete=odict(value=False), verbosity="", start="" )) def retrieval(self): if not self.start.value: self.complete.value = True return console = getConsole("didery.py", verbosity=self.verbosity.value) outputSetupInfo(console, self.servers.value, did=self.did.value) data, results = hist.getHistory(self.did.value, self.servers.value) outputPulledResult(data, results, console) self.complete.value = True @doify('Download', ioinits=odict( servers="", did="", test="", complete=odict(value=False), verbosity="", start="" )) def download(self): if not self.start.value: self.complete.value = True return console = getConsole("didery.py", verbosity=self.verbosity.value) outputSetupInfo(console, self.servers.value, did=self.did.value) data, results = otp.getOtpBlob(self.did.value, self.servers.value) outputPulledResult(data, results, console) self.complete.value = True @doify('Delete', ioinits=odict( servers="", did="", sk="", test="", complete=odict(value=False), verbosity="", start="" )) def delete(self): if not self.start.value: self.complete.value = True return console = getConsole("didery.py", verbosity=self.verbosity.value) outputSetupInfo(console, self.servers.value, did=self.did.value) result = hist.deleteHistory(self.did.value, self.sk.value, self.servers.value) outputPushedResult(result, console, self.verbosity.value) self.complete.value = True @doify('Remove', ioinits=odict( servers="", did="", sk="", test="", complete=odict(value=False), verbosity="", start="" )) def remove(self): if not self.start.value: self.complete.value = True return console = getConsole("didery.py", verbosity=self.verbosity.value) outputSetupInfo(console, self.servers.value, did=self.did.value) result = otp.removeOtpBlob(self.did.value, self.sk.value, self.servers.value) outputPushedResult(result, console, self.verbosity.value) self.complete.value = True @doify('Events', ioinits=odict( servers="", did="", test="", complete=odict(value=False), verbosity="", start="" )) def events(self): if not self.start.value: self.complete.value = True return console = getConsole("didery.py", verbosity=self.verbosity.value) outputSetupInfo(console, self.servers.value, did=self.did.value) data, results = event.getHistoryEvents(self.did.value, self.servers.value) outputPulledResult(data, results, console) self.complete.value = True PK!oGRdiderypy/diderying.pyclass DideryPyError(Exception): """ Base Class for bluepea exceptions To use raise BluepeaError("Error: message") """ class ValidationError(DideryPyError): """ Validation related errors Usage: raise ValidationError("error message") """PK!s@@diderypy/flo/main.flo# Main Flow house main framer diderypy be active first diderypy frame diderypy print starting didery.py service bid start incept bid start upload bid start rotate bid start update bid start retrieve bid start download bid start delete bid start remove bid start events go next if .main.incept.complete == True and .main.upload.complete == True and .main.rotate.complete == True and .main.update.complete == True and .main.retrieve.complete == True and .main.download.complete == True and .main.delete.complete == True and .main.remove.complete == True and .main.events.complete == True frame stopper bid stop all framer incept be inactive via .main.incept frame incept print starting incept do incept at enter framer upload be inactive via .main.upload frame upload print starting upload do upload at enter framer rotate be inactive via .main.rotate frame rotate print starting rotation do rotation at enter framer update be inactive via .main.update frame update print starting update do update at enter framer retrieve be inactive via .main.retrieve frame retrieve print starting retrieval do retrieval at enter framer download be inactive via .main.download frame download print starting download do download at enter framer delete be inactive via .main.delete frame delete print starting delete do delete at enter framer remove be inactive via .main.remove frame remove print starting remove do remove at enter framer events be inactive via .main.events frame events print starting retrieval do events at enter PK!diderypy/help/__init__.pyPK!KQdiderypy/help/consensing.pyfrom hashlib import sha256 try: import simplejson as json except ImportError: import json from abc import ABC, abstractmethod from collections import OrderedDict as ODict from ..models.consensing import ConsensusResult MAJORITY = 2 / 3 class AbstractConsense(ABC): def __init__(self): self.valid_data = {} self.valid_match_counts = {} self.num_valid = 0 self.results = {} self.consensus = None def incrementValid(self): self.num_valid += 1 def addResult(self, url, req_status, response=None, http_status=None): self.results[url] = ConsensusResult(url, req_status, response, http_status) def addTimeOut(self, url): self.addResult(url, ConsensusResult.TIMEOUT) def addError(self, url, response, http_status): self.addResult(url, ConsensusResult.ERROR, response, http_status) def addFailure(self, url, response, http_status): self.addResult(url, ConsensusResult.FAILED, response, http_status) def addMatchCount(self, hashd): self.valid_match_counts[hashd] = self.valid_match_counts.get(hashd, 0) + 1 def addValidData(self, hashd, data): self.incrementValid() self.valid_data[hashd] = data def addSuccess(self, url, hashd, data, http_status): self.addResult(url, ConsensusResult.VALID, data, http_status) self.addMatchCount(hashd) self.addValidData(hashd, data) @abstractmethod def validateData(self, data): pass def consense(self, data): """ Validates signatures and then checks if a majority of the data items are equal. :param data: list of history dicts returned by the didery server :return: tuple - history dict and dict of result strings for each url. if consensus is not reached then None and the results dict are returned """ if not data: raise ValueError("data cannot be None.") self.validateData(data) for hashData, count in self.valid_match_counts.items(): if count >= len(data) * MAJORITY: self.consensus = self.valid_data[hashData] return self.consensus, self.results class Consense(AbstractConsense): def __init__(self, valid_data=None, match_counts=None, results=None, num_valid=None): AbstractConsense.__init__(self) if valid_data: self.valid_data = valid_data if match_counts: self.valid_match_counts = match_counts if results: self.results = results if num_valid: self.num_valid = num_valid def validateData(self, data): """ Checks for request errors and counts valid signatures :param data: dict of DideryResponse obj """ for url, response in data.items(): status = response.status data = response.response if status == 0: self.addTimeOut(url) # Request timed out continue elif status != 200: self.addError(url, data, status) # Error with request continue if data.valid: self.addSuccess(url, data.signature, data.data, status) # Signature validated else: self.addFailure(url, data.data, status) # Signature validation failed class CompositeConsense(AbstractConsense): def __init__(self): AbstractConsense.__init__(self) def _dataToDict(self, data): temp = {} for index, event in data.items(): temp[index] = event.data return temp def validateData(self, history_events): """ Checks for request errors and counts valid signatures :param history_events: dict of history rotation events returned by the didery server """ for url, response in history_events.items(): status = response.status data = response.response if status == 0: self.addTimeOut(url) # Request timed out continue elif status != 200: self.addError(url, data, status) # Error with request continue valid = True if len(data) > 0 else False for index, event in data.items(): if not event.valid: valid = False if valid: sha = sha256(str(ODict(data)).encode()).hexdigest() self.addSuccess(url, sha, self._dataToDict(data), status) # Signature validated else: self.addFailure(url, data, status) # Signature validation failed PK!8tdiderypy/help/helping.pyfrom ..diderying import ValidationError from collections import OrderedDict as ODict try: import simplejson as json except ImportError: import json from ioflo.aid import odict from ioflo.aio.http import Patron from ioflo.aio import WireLog from ioflo.base import Store from ioflo.aid import timing from ioflo.aid import getConsole from ..models.responding import responseFactory from ..lib.didering import validateDid console = getConsole() def parseJsonFile(file, requireds=()): """ Returns deserialized version of data string if data is correctly formed. Otherwise returns None :param file is json encoded unicode string :param requireds tuple of string keys required in json data """ data = None with open(file) as f: try: # now validate message data try: data = json.load(f, object_pairs_hook=ODict) except ValueError as ex: raise ValidationError("Invalid JSON") # invalid json if not data: # registration must not be empty raise ValidationError("Empty body") if not isinstance(data, dict): # must be dict subclass raise ValidationError("JSON not dict") for field in requireds: if field not in data: raise ValidationError("Missing required field {}".format(field)) except ValidationError: raise except Exception as ex: # unknown problem print(ex) raise ValidationError("Unexpected error") return data def parseConfigFile(file): """ Validate the data in the configuration file :param file: click.Path object :return: parsed configuration data """ data = parseJsonFile(file, ["servers"]) if not isinstance(data["servers"], list): raise ValidationError('"servers" field must be a list') return data def parseDataFile(file, dtype): data = {} if dtype == "history": data = parseJsonFile(file, ["history"])["history"] for field in ["id", "signer", "signers"]: if field not in data: raise ValidationError("Missing required field {}".format(field)) if dtype == "otp": data = parseJsonFile(file, ["otp"])["otp"] for field in ["id", "blob"]: if field not in data: raise ValidationError("Missing required field {}".format(field)) # Check for valid did validateDid(data["id"]) return data def httpRequest(method=u'GET', scheme=u'', # default if not in path host=u'localhost', # default if not in path port=None, # default if not in path path=u'/', qargs=None, headers=None, body=b'', data=None, store=None, timeout=1000.0, buffer=False,): """ Perform Async ReST request to Backend Server Parameters: Usage: (Inside a generator function) response = yield from backendRequest() response is the response if valid else None before response is completed the yield from yields up an empty string '' once completed then response has a value path can be full url with host port etc path takes precedence over others """ store = store if store is not None else Store(stamp=0.0) if buffer: wlog = WireLog(buffify=buffer, same=True) wlog.reopen() else: wlog = None if headers is None: headers = odict([('Accept', 'application/json'), ('Connection', 'close')]) client = Patron(bufsize=131072, wlog=wlog, store=store, scheme=scheme, hostname=host, port=port, method=method, path=path, qargs=qargs, headers=headers, body=body, data=data, reconnectable=False, ) console.concise("Making Request {0} {1} ...\n".format(method, path)) client.transmit() # assumes store clock is advanced elsewhere timer = timing.StoreTimer(store=store, duration=timeout) while ((client.requests or client.connector.txes or not client.responses) and not timer.expired): try: client.serviceAll() except Exception as ex: console.terse("Error: Servicing client. '{0}'\n".format(ex)) raise ex yield b'' # this is eventually yielded by wsgi app while waiting store.advanceStamp(0.125) response = None # in case timed out if client.responses: response = client.responses.popleft() client.close() if wlog: wlog.close() # response.get('status') # response.get('body').decode('utf-8') return response def awaitAsync(generators): values = {} while True: remove = [] for url, generator in generators.items(): try: next(generator) except StopIteration as si: if si.value: values[url] = responseFactory(url, si.value[1], json.loads(si.value[0])) else: values[url] = responseFactory(url, 0, None) remove.append(url) for val in remove: generators.pop(val) if len(generators) == 0: return values PK!3 **diderypy/help/signing.pyimport libnacl from ..lib import generating as gen def signResource(resource, sKey): """ signResource accepts a byte string and an EdDSA (Ed25519) key in the form of a byte string and returns a base64 url-file safe signature. :param resource: byte string to be signed :param sKey: signing/private key from EdDSA (Ed25519) key :return: url-file safe base64 signature string """ if resource is None: return None sig = libnacl.crypto_sign(resource, sKey) sig = sig[:libnacl.crypto_sign_BYTES] return gen.keyToKey64u(sig) def verify(sig, msg, vk): """ Returns True if signature sig of message msg is verified with verification key vk Otherwise False All of sig, msg, vk are bytes """ try: result = libnacl.crypto_sign_open(sig + msg, vk) except Exception as ex: return False return True if result else False def verify64u(signature, message, verkey): """ Returns True if signature is valid for message with respect to verification key verkey signature and verkey are encoded as unicode base64 url-file strings and message is unicode string as would be the case for a json object """ sig = gen.key64uToKey(signature) vk = gen.key64uToKey(verkey) return verify(sig, message, vk) PK!diderypy/lib/__init__.pyPK!R  diderypy/lib/didering.pyimport base64 def didGen(vk, method="dad"): """ didGen accepts an EdDSA (Ed25519) key in the form of a byte string and returns a DID. :param vk: 32 byte verifier/public key from EdDSA (Ed25519) key :param method: W3C did method string. Defaults to "dad". :return: W3C DID string """ if vk is None: return None # convert verkey to jsonable unicode string of base64 url-file safe vk64u = base64.urlsafe_b64encode(vk).decode("utf-8") return "did:{0}:{1}".format(method, vk64u) def didGen64(vk64u, method="dad"): """ didGen accepts a url-file safe base64 key in the form of a string and returns a DID. :param vk64u: base64 url-file safe verifier/public key from EdDSA (Ed25519) key :param method: W3C did method string. Defaults to "dad". :return: W3C DID string """ if vk64u is None: return None return "did:{0}:{1}".format(method, vk64u) def extractDidParts(did): """ Parses and returns a tuple containing the prefix method and key string contained in the supplied did string. If the supplied string does not fit the pattern pre:method:keystr a ValueError is raised. :param did: W3C DID string :return: (pre, method, key string) a tuple containing the did parts. """ try: # correct did format pre:method:keystr pre, meth, keystr = did.split(":") except ValueError as ex: raise ValueError("Malformed DID value") if not pre or not meth or not keystr: # check for empty values raise ValueError("Malformed DID value") return pre, meth, keystr def validateDid(did, method="dad"): """ Parses and returns did and key string as a tuple (did, keystr) raises ValueError if fails parsing :param did: W3C DID string :param method: W3C did method string. Defaults to "dad". :return: (string, string) W3C DID string, key string """ try: # correct did format pre:method:keystr pre, meth, keystr = did.split(":") except ValueError as ex: raise ValueError("Malformed DID value") if not pre or not meth or not keystr: # check for empty values raise ValueError("Malformed DID value") if pre != "did" or meth != method: raise ValueError("Invalid DID value") return did, keystr PK!> > diderypy/lib/generating.pyimport base64 import libnacl import diderypy.lib.didering as didering """ This module provides various key generation and manipulation functions for use with the didery server. Keys are generated using the python libnacl library. """ def keyToKey64u(key): """ keyToKey64u allows you to convert a key from a byte string to a base64 url-file safe string. :param key: 32 byte string :return: base64 url-file safe string """ if key is None: return None return base64.urlsafe_b64encode(key).decode("utf-8") def key64uToKey(key64u): """ key64uToKey allows you to convert a base64 url-file safe key string to a byte string :param key64u: base64 ulr-file safe string :return: byte string """ if key64u is None: return None return base64.urlsafe_b64decode(key64u.encode("utf-8")) def keyGen(seed=None): """ keyGen generates a url-file safe base64 public private key pair. If a seed is not provided libnacl's randombytes() function will be used to generate a seed. :param seed: The seed value used during key generation. :return: url-file safe base64 verifier/public key, signing/private key """ if seed is None: seed = libnacl.randombytes(libnacl.crypto_sign_SEEDBYTES) vk, sk = libnacl.crypto_sign_seed_keypair(seed) did = didering.didGen(vk) return keyToKey64u(vk), keyToKey64u(sk), did def historyGen(seed=None): """ historyGen generates a new key history dictionary and returns the history along with all generated keys. If a seed is not provided libnacl's randombytes() function will be used to generate a seed. :param seed: The seed value used during key generation. :return: a history dictionary with an "id", "signer" and "signers" field, url-file safe base64 verifier/public key string, url-file safe base64 signing/private key, url-file safe base64 pre-rotated verifier/public key, url-file safe base64 pre-rotated signing/private key """ vk, sk, did = keyGen(seed) pre_rotated_vk, pre_rotated_sk, did = keyGen(seed) history = { "id": didering.didGen64(vk), "signer": 0, "signers": [ vk, pre_rotated_vk ] } return history, vk, sk, pre_rotated_vk, pre_rotated_sk PK!ꋯ:: diderypy/lib/history_eventing.pyfrom ..help import helping as h from ..help import consensing def __patronHelper(method="GET", path="event", headers=None, data=None): result = yield from h.httpRequest(method, path=path, headers=headers, data=data) if result: return result['body'].decode(), result.get('status') else: return None def getHistoryEvents(did, urls): consense = consensing.CompositeConsense() if not urls: raise ValueError("At least one url required.") generators = {} for url in urls: endpoint = "{0}/{1}/{2}".format(url, "event", did) generators[endpoint] = __patronHelper(path=endpoint) data = h.awaitAsync(generators) print(data) events, results = consense.consense(data) events = {"events": events} if events else events return events, results PK!UF diderypy/lib/historying.pyimport arrow try: import simplejson as json except ImportError: import json from ..help import helping as h from ..help import consensing from ..help import signing as sign from ..lib import generating as gen def __patronHelper(method="GET", path="history", headers=None, data=None): result = yield from h.httpRequest(method, path=path, headers=headers, data=data) if result: return result['body'].decode(), result.get('status') else: return None # def getAllHistories(urls=None): # if urls is None: # urls = ["http://localhost:8080/history", "http://localhost:8000/history"] # # generators = [] # # for url in urls: # generators.append(__patronHelper(path=url)) # # return h.awaitAsync(generators) def getHistory(did, urls): consense = consensing.Consense() if not urls: raise ValueError("At least one url required.") generators = {} for url in urls: endpoint = "{0}/{1}/{2}".format(url, "history", did) generators[endpoint] = __patronHelper(path=endpoint) data = h.awaitAsync(generators) return consense.consense(data) def postHistory(data, sk, urls): if not urls: raise ValueError("At least one url required.") if not sk: raise ValueError("Signing key required.") generators = {} data['changed'] = str(arrow.utcnow()) bdata = json.dumps(data, ensure_ascii=False, separators=(',', ':')).encode() headers = { "Signature": 'signer="{0}"'.format(sign.signResource(bdata, gen.key64uToKey(sk))) } for url in urls: endpoint = "{0}/{1}".format(url, "history") generators[endpoint] = __patronHelper(method="POST", path=endpoint, data=data, headers=headers) return h.awaitAsync(generators) def putHistory(data, sk, psk, urls): if not urls: raise ValueError("At least one url required.") if not sk: raise ValueError("Signing key required.") if not psk: raise ValueError("Pre-rotated signing key required.") generators = {} did = data["id"] data['changed'] = str(arrow.utcnow()) bdata = json.dumps(data, ensure_ascii=False, separators=(',', ':')).encode() headers = { "Signature": 'signer="{0}"; rotation="{1}"'.format( sign.signResource(bdata, gen.key64uToKey(sk)), sign.signResource(bdata, gen.key64uToKey(psk)) ) } for url in urls: endpoint = "{0}/{1}/{2}".format(url, "history", did) generators[endpoint] = __patronHelper(method="PUT", path=endpoint, data=data, headers=headers) return h.awaitAsync(generators) def deleteHistory(did, sk, urls): if not urls: raise ValueError("At least one url required.") if not sk: raise ValueError("Signing key required.") generators = {} data = {"id": did} bdata = json.dumps(data, ensure_ascii=False, separators=(',', ':')).encode() headers = { "Signature": 'signer="{0}"'.format( sign.signResource(bdata, gen.key64uToKey(sk)) ) } for url in urls: endpoint = "{0}/{1}/{2}".format(url, "history", did) generators[endpoint] = __patronHelper(method="DELETE", path=endpoint, data=data, headers=headers) return h.awaitAsync(generators) PK!:ID% % diderypy/lib/otping.pyimport arrow try: import simplejson as json except ImportError: import json from ..help import helping as h from ..help import consensing from ..help import signing as sign from ..lib import generating as gen def __patronHelper(method="GET", path="blob", headers=None, data=None): result = yield from h.httpRequest(method, path=path, headers=headers, data=data) if result: return result['body'].decode(), result.get('status') else: return None # def getAllOtpBlobs(urls=None): # if urls is None: # urls = ["http://localhost:8080/history", "http://localhost:8000/history"] # # generators = [] # # for url in urls: # generators.append(__patronHelper(path=url)) # # return h.awaitAsync(generators) def getOtpBlob(did, urls): if not urls: raise ValueError("At least one url required.") consense = consensing.Consense() generators = {} for url in urls: endpoint = "{0}/{1}/{2}".format(url, "blob", did) generators[endpoint] = __patronHelper(path=endpoint) data = h.awaitAsync(generators) return consense.consense(data) def postOtpBlob(data, sk, urls): if not urls: raise ValueError("At least one url required.") if not sk: raise ValueError("Signing key required.") generators = {} data['changed'] = str(arrow.utcnow()) bdata = json.dumps(data, ensure_ascii=False, separators=(',', ':')).encode() headers = { "Signature": 'signer="{0}"'.format(sign.signResource(bdata, gen.key64uToKey(sk))) } for url in urls: endpoint = "{0}/{1}".format(url, "blob") generators[endpoint] = __patronHelper(method="POST", path=endpoint, data=data, headers=headers) return h.awaitAsync(generators) def putOtpBlob(data, sk, urls): if not urls: raise ValueError("At least one url required.") if not sk: raise ValueError("Signing key required.") generators = {} data['changed'] = str(arrow.utcnow()) did = data["id"] bdata = json.dumps(data, ensure_ascii=False, separators=(',', ':')).encode() headers = { "Signature": 'signer="{0}"'.format(sign.signResource(bdata, gen.key64uToKey(sk))) } for url in urls: endpoint = "{0}/{1}/{2}".format(url, "blob", did) generators[endpoint] = __patronHelper(method="PUT", path=endpoint, data=data, headers=headers) return h.awaitAsync(generators) def removeOtpBlob(did, sk, urls): if not urls: raise ValueError("At least one url required.") if not sk: raise ValueError("Signing key required.") generators = {} data = {"id": did} bdata = json.dumps(data, ensure_ascii=False, separators=(',', ':')).encode() headers = { "Signature": 'signer="{0}"'.format( sign.signResource(bdata, gen.key64uToKey(sk)) ) } for url in urls: endpoint = "{0}/{1}/{2}".format(url, "blob", did) generators[endpoint] = __patronHelper(method="DELETE", path=endpoint, data=data, headers=headers) return h.awaitAsync(generators) PK!diderypy/models/__init__.pyPK!cS S diderypy/models/consensing.pyfrom urllib.parse import urlparse class ConsensusResult: """ ConsensusResult object a container class for storing info about a request and the status of a requests validation during the consensing step. A list or dict of ConsensusResult objects will be returned by any function running didery.py's consensing algorithm. """ TIMEOUT = 0 VALID = 1 ERROR = 2 FAILED = 3 def __init__(self, url, validation_status, response=None, http_status=None): """ Initialize a ConsensusResult object Args: :param url: string, url that was queried :param validation_status: int, 0-3 :param response: dict or model, containing response data from the above url :param http_status: int, the http response status from the request """ self.url = url if 0 > validation_status > 3: raise ValueError("validation_status must be between 0 and 3") self.validation_status = validation_status self.response = response self.http_status = http_status def __str__(self): url = urlparse(self.url) str_rep = "{}://{}:\t".format(url.scheme, url.netloc) if self.validation_status == ConsensusResult.TIMEOUT: str_rep += "Request Timed Out" elif self.validation_status == ConsensusResult.VALID: str_rep += "Signature Validation Succeeded" elif self.validation_status == ConsensusResult.ERROR: str_rep += "Error Handling Request. HTTP_{}".format(self.http_status) elif self.validation_status == ConsensusResult.FAILED: str_rep += "Signature Validation Failed" return str_rep def __repr__(self): url = urlparse(self.url) str_rep = "{}://{}:\t".format(url.scheme, url.netloc) if self.validation_status == ConsensusResult.TIMEOUT: str_rep += "Request Timed Out.\t" elif self.validation_status == ConsensusResult.VALID: str_rep += "Signature Validation Succeeded.\t" elif self.validation_status == ConsensusResult.ERROR: str_rep += "Error Handling Request.\t" elif self.validation_status == ConsensusResult.FAILED: str_rep += "Signature Validation Failed.\t" str_rep += "HTTP_{}".format(self.http_status) return str_rep PK!diderypy/models/responding.pytry: import simplejson as json except ImportError: import json from collections import OrderedDict as ODict from ..lib.didering import validateDid from ..help.signing import verify64u def responseFactory(url, status, data): """ responseFactory() implements the factory pattern to build objects for history, otp, and events data based on the format of the data that is passed to it :param url: url string that was queried :param status: integer representing the http response status from the request :param data: dict containing response data from the above url :return: DideryResponse object containing in it's response field either a HistoryData object, OtpData object, or a dict of HistoryData objects depending on if you passed rotation history data, otp encrypted blob data, or events data. """ if "history" in data or ("deleted" in data and "history" in data["deleted"]): response = HistoryData(data) elif "otp_data" in data or ("deleted" in data and "otp_data" in data["deleted"]): response = OtpData(data) elif status == 200 and data and "event" in data[0]: response = {} for datum in data: key = str(datum["event"]["signer"]) event = { "history": datum["event"], "signatures": datum["signatures"] } response[key] = HistoryData(event) else: response = AbstractDideryData(data) return DideryResponse(url, status, response) class DideryResponse: """ DideryResponse object is a container class for storing info about a HTTP response. """ def __init__(self, url, status, response): """ :param url: url string that was queried :param status: integer representing the http response status from the request :param response: dict or model containing response data from the above url """ self.url = url self.status = status self.response = response def __str__(self): return str(ODict(self.__dict__)).replace("OrderedDict", "") def __repr__(self): return str(ODict(self.__dict__)).replace("OrderedDict", "DideryResponse") class AbstractDideryData: """ AbstractDideryData object is an abstract parent class for storing response data from didery servers. """ def __init__(self, data): self._data = data @property def data(self): return self._data @property def bdata(self): return json.dumps(self._data, ensure_ascii=False, separators=(",", ":")).encode() @property def body(self): return None @property def bbody(self): return json.dumps(self.body, ensure_ascii=False, separators=(",", ":")).encode() @property def did(self): return self.body["id"] @property def vk(self): return None @property def signature(self): return None @property def valid(self): return verify64u(self.signature, self.bbody, self.vk) def __eq__(self, other): return self.data == other.data class HistoryData(AbstractDideryData): """ HistoryData is a container class that implements the AbstractDideryData class. It adds three additional attributes to the base class. """ def __init__(self, data): """ :param data: dict returned from request to /history/ endpoint on didery servers """ AbstractDideryData.__init__(self, data) @property def body(self): if "deleted" in self._data: return self._data["deleted"]["history"] else: return self._data["history"] @property def vk(self): signer = int(self.body["signer"]) return self.body["signers"][signer] @property def previous_vk(self): signer = int(self.body["signer"]) if signer == 0: return None return self.body["signers"][signer-1] @property def signer_sig(self): return self._data["signatures"]["signer"] @property def rotation_sig(self): if "rotation" in self._data["signatures"]: return self._data["signatures"]["rotation"] else: return None @property def signature(self): if self.rotation_sig: return self.rotation_sig else: return self.signer_sig @property def valid(self): if self.rotation_sig: rotation = verify64u(self.rotation_sig, self.bbody, self.vk) signer = verify64u(self.signer_sig, self.bbody, self.previous_vk) else: rotation = True signer = verify64u(self.signer_sig, self.bbody, self.vk) return signer and rotation def __str__(self): return str(ODict(self.body)).replace("OrderedDict", "") def __repr__(self): return str(ODict(self.body)).replace("OrderedDict", "HistoryData") class OtpData(AbstractDideryData): """ OtpData is a container class that implements the AbstractDideryData class. It does not currently add any additional attributes or methods to the base class. """ def __init__(self, data): """ :param data: dict returned from request to /blob/ endpoint on didery servers """ AbstractDideryData.__init__(self, data) @property def body(self): if "deleted" in self._data: return self._data["deleted"]["otp_data"] else: return self._data["otp_data"] @property def vk(self): did, vk = validateDid(self.body["id"]) return vk @property def signature(self): return self._data["signatures"]["signer"] PK!]{],], diderypy-0.0.1.dist-info/LICENSE Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. PK!HW"TTdiderypy-0.0.1.dist-info/WHEEL A н#J."jm)Afb~ ڡ5 G7hiޅF4+-3ڦ/̖?XPK!H_n!?7!diderypy-0.0.1.dist-info/METADATAMo0 >2F"Eauuی钖~ 6q)k W1y1H!X@MZ))$5JCWt)au`Ӽ{%ѯYax{beYZ?&lRi 1OK%g9d\P: n 'TI ?[ޢbh8 Ի 9@)lۻꀙ{|@c/VfR97*Z;=uu<~̯i(0Uܮpfϐ7Tt︈L\(G1B^T:c~kx:q_PK!Hpdiderypy-0.0.1.dist-info/RECORDɶH}nb PfAeAF d&՛tS_I5 T aXG b%T]vqV_ՕVwLj/)j'd~?O~9LN;A4W^nT޵ܬlKz 0xb` EOMMv}9qhF 0166 F>AM{fP"qS:Mzb54{X{O`ѽ@Xߚ[Hwf ĩRu11E3s0?WILY|k`P7Ac[&,ZS/u3}Y}h.!YڤYbrCT+ #! >_jk<}cX@ӟ+ț|9)p8c/VX,b\! #ic"9}W`Ymzl&o cHh(`WRAvw.gzDd1ň0܅ NJE?4}IQD3bz@bRېxd\Si=S,*AX+ Y<V SM3 X14y)-g/SF#bYad'1s&`4IOE^ Slq\ bʏdd5%OX ÕN,Tm~-R<=VFzq}O5}PK!diderypy/__init__.pyPK!o332diderypy/app.pyPK!A>E--diderypy/cli.pyPK!w61diderypy/core/__init__.pyPK!bf  e2diderypy/core/behaving.pyPK!oGRMdiderypy/diderying.pyPK!s@@Odiderypy/flo/main.floPK!zVdiderypy/help/__init__.pyPK!KQVdiderypy/help/consensing.pyPK!8tididerypy/help/helping.pyPK!3 **diderypy/help/signing.pyPK!diderypy/lib/__init__.pyPK!R  Ldiderypy/lib/didering.pyPK!> > diderypy/lib/generating.pyPK!ꋯ:: diderypy/lib/history_eventing.pyPK!UF ~diderypy/lib/historying.pyPK!:ID% % diderypy/lib/otping.pyPK!diderypy/models/__init__.pyPK!cS S 3diderypy/models/consensing.pyPK!diderypy/models/responding.pyPK!]{],], diderypy-0.0.1.dist-info/LICENSEPK!HW"TTIdiderypy-0.0.1.dist-info/WHEELPK!H_n!?7!diderypy-0.0.1.dist-info/METADATAPK!HpWdiderypy-0.0.1.dist-info/RECORDPKu