[
  {
    "path": ".github/workflows/python-publish.yml",
    "content": "# This workflow will upload a Python Package using Twine when a release is created\n# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries\n\n# This workflow uses actions that are not certified by GitHub.\n# They are provided by a third-party and are governed by\n# separate terms of service, privacy policy, and support\n# documentation.\n\nname: Upload Python Package\n\non:\n  workflow_dispatch:   \n  release:\n    types: [published]\n\njobs:\n  deploy:\n\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/checkout@v2\n    - name: Set up Python\n      uses: actions/setup-python@v2\n      with:\n        python-version: '3.x'\n    - name: Install dependencies\n      run: |\n        python -m pip install --upgrade pip\n        pip install build\n    - name: Build package\n      run: python -m build\n    - name: Publish package\n      uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29\n      with:\n        user: __token__\n        password: ${{ secrets.PYPI_API_TOKEN }}\n"
  },
  {
    "path": ".gitignore",
    "content": "certs/\n\n# Created by https://www.gitignore.io/api/node,macos,python,virtualenv\n\n### macOS ###\n*.DS_Store\n.AppleDouble\n.LSOverride\n\n# Icon must end with two \\r\nIcon\r\n\n# Thumbnails\n._*\n\n# Files that might appear in the root of a volume\n.DocumentRevisions-V100\n.fseventsd\n.Spotlight-V100\n.TemporaryItems\n.Trashes\n.VolumeIcon.icns\n.com.apple.timemachine.donotpresent\n\n# Directories potentially created on remote AFP share\n.AppleDB\n.AppleDesktop\nNetwork Trash Folder\nTemporary Items\n.apdisk\n\n### Node ###\n# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# Runtime data\npids\n*.pid\n*.seed\n*.pid.lock\n\n# Directory for instrumented libs generated by jscoverage/JSCover\nlib-cov\n\n# Coverage directory used by tools like istanbul\ncoverage\n\n# nyc test coverage\n.nyc_output\n\n# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)\n.grunt\n\n# Bower dependency directory (https://bower.io/)\nbower_components\n\n# node-waf configuration\n.lock-wscript\n\n# Compiled binary addons (http://nodejs.org/api/addons.html)\nbuild/Release\n\n# Dependency directories\nnode_modules/\njspm_packages/\n\n# Typescript v1 declaration files\ntypings/\n\n# Optional npm cache directory\n.npm\n\n# Optional eslint cache\n.eslintcache\n\n# Optional REPL history\n.node_repl_history\n\n# Output of 'npm pack'\n*.tgz\n\n# Yarn Integrity file\n.yarn-integrity\n\n# dotenv environment variables file\n.env\n\n\n### Python ###\n# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\n*.egg-info/\n.installed.cfg\n*.egg\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*.cover\n.hypothesis/\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\nlocal_settings.py\n\n# Flask stuff:\ninstance/\n.webassets-cache\n\n# Scrapy stuff:\n.scrapy\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\ntarget/\n\n# Jupyter Notebook\n.ipynb_checkpoints\n\n# pyenv\n.python-version\n\n# celery beat schedule file\ncelerybeat-schedule\n\n# SageMath parsed files\n*.sage.py\n\n# Environments\n.venv\nenv/\nvenv/\nENV/\nenv.bak/\nvenv.bak/\n\n# Spyder project settings\n.spyderproject\n.spyproject\n\n# Rope project settings\n.ropeproject\n\n# mkdocs documentation\n/site\n\n# mypy\n.mypy_cache/\n\n### VirtualEnv ###\n# Virtualenv\n# http://iamzed.com/2009/05/07/a-primer-on-virtualenv/\n# [Bb]in\n[Ii]nclude\n[Ll]ib\n[Ll]ib64\n[Ll]ocal\n[Mm]an\n[Ss]cripts\n[Tt]cl\npyvenv.cfg\npip-selfcheck.json\n\n# End of https://www.gitignore.io/api/node,macos,python,virtualenv\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2018 Jan-Gerd Tenberge\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "MANIFEST.in",
    "content": "include setup.json"
  },
  {
    "path": "README.md",
    "content": "# dpt-rp1-py\nPython script to manage electronic paper devices made by Sony (Digital Paper, DPT-RP1, DPT-CP1) or Fujitsu (Quaderno) without the Digital Paper App. This repository includes a Python library and a command line utility to manage documents on the reader. Tested on Windows, Linux, and macOS. Should also work for Sony's other digital paper readers.\n\nThroughout this document, _reader_ or _device_ refers to your Digital Paper device.\n\n## Installation\nWe now have a proper Python package, so you may just run:\n\n```\npip3 install dpt-rp1-py\n```\n\nInstalling the package also installs the command line utilities `dptrp1` and `dptmount`. To install the library from the sources, clone this repository, then run `python3 setup.py install` or `pip3 install .` from the root directory. To install as a developer use `python3 setup.py develop` (see [the setuptools docs](http://setuptools.readthedocs.io/en/latest/setuptools.html#development-mode)) and work on the source as usual.\n\n## Using the command line utility\nThe command line utility requires a connection to the reader via WiFi, Bluetooth, or USB. The USB connection works on Windows and MacOS but may not work on a Linux machine.\n\nTo see if you can successfully connect to the reader, try the command `dptrp1 list-documents`. If you have Sony's Digital Paper App installed, this should work without any further configuration. If this fails, register your reader with the app using `dptrp1 register`.\n\n### Basic usage\nHere you see some basic usage examples for the utility. Text following a dollar sign is the command as entered on the command line on MacOS or Linux. Your paths may look slightly different on Windows.\n\n#### Registering the device\nThis command pairs the command line utility to your reader. You only need to run this once. Keep the device nearby, you will need to read a code from the display and enter it.\n\n```\n$ dptrp1 register                                                                                 \nDiscovering Digital Paper for 30 seconds…\nFound Digital Paper with serial number 500XXXX\nCleaning up...\n<Response [204]>\nRequesting PIN...\nEncoding nonce...\nPlease enter the PIN shown on the DPT-RP1: \n```\n\n\n#### Listing all documents on the device\n```\n$ dptrp1 list-documents                                                                                 \nDocument/Note/Graph_20171022.pdf\nDocument/Work/Scans/Contract.pdf\nDocument/Papers/svetachov2010.pdf\nDocument/Papers/sporns2012.pdf\n```\n\n#### Getting general usage instructions\n```\n$ dptrp1 -h\nusage: dptrp1 [-h] [--client-id CLIENT_ID] [--key KEY] [--addr ADDR]\n              [--serial SERIAL] [--yes] [--quiet]\n              {copy-document,[...],wifi-scan}\n              [command_args [command_args ...]]\n\nRemote control for Sony DPT-RP1\n\npositional arguments:\n  {copy-document,[...],wifi-scan}\n                        Command to run\n  command_args          Arguments for the command\n\noptional arguments:\n  -h, --help            show this help message and exit\n  --client-id CLIENT_ID\n                        File containing the device's client id\n  --key KEY             File containing the device's private key\n  --addr ADDR           Hostname or IP address of the device. Disables auto\n                        discovery.\n  --serial SERIAL       Device serial number for auto discovery. Auto\n                        discovery only works for some minutes after the\n                        Digital Paper's Wi-Fi setting is switched on.\n  --yes, -y             Automatically answer yes to confirmation prompts, for\n                        running non-interactively.\n  --quiet, -q           Suppress informative messages.\n\n```\n\n#### Getting help for the upload command\n```\n$ dptrp1 help upload\n\n    Usage: dptrp1 upload <local_path> [<remote_path>]\n\n    Upload a local document to the reader.\n    Will upload to Document/ if only the local path is specified.\n```\n    \n#### Uploading a document to the reader\n```\n$ dptrp1 upload ~/Desktop/scan.pdf\n```\n\n#### Opening the second page of a document on the reader\n```\n$ dptrp1 display-document Document/scan.pdf 2\n```\n\n#### Connecting to a WiFi network\nThis command requires the path to a WiFi configuration file as a parameter. Look at the [sample configuration](https://github.com/janten/dpt-rp1-py/blob/master/samples/wifi_2.5G.json) file and put your network name in the _ssid_ field and your password into the _passwd_ field. You can generally leave the other fields unchanged.\n\n```\n$ dptrp1 wifi-add config.json\n```\n\n### Supported commands\nYou can get a list of the implemented commands by running `dptrp1` with no additional arguments. The most important commands for everyday use are _register_, _help_, _upload_, _download_, and _sync_.\n\nYou can get additional information about a specific command by calling `dptrp1 help <command>`, e.g. `dptrp1 help sync`.\n\nNote that the root path for DPT-RP1 is always `Document/`, which is misleadingly displayed as \"System Storage\" on the device. To download a document called _file.pdf_ from a folder called _Articles_ of the DPT-RP1, the correct command is `dptrp1 download Document/Articles/file.pdf`.\n\n### Registering the DPT-RP1\nThe DPT-RP1 uses SSL encryption to communicate with the computer.  This requires registering the DPT-RP1 with the computer, which results in two pieces of information, the client ID and the private key. If you have used Sony's Digital Paper App on the same computer, the utility will automatically try to use the existing credentials. If you do not have the Digital Paper App, use the _register_ command.\n\n#### Registering without the Digital Paper App\nIf you want to use a WiFi connection, make sure that the reader and your computer are connected to the same WiFi network. Some versions of the DPT-RP1 do not allow you to connect to a WiFi network from the device itself. In this case, use Bluetooth or USB first to configure the WiFi network (using the _wifi-add_ command) or update the firmware (using _update-firmware_).\n\nThe tool can generally figure out the correct IP address of the device automatically, but you may also specify it with the `--addr <address>` option. If you're on WiFi, go to _Wi-Fi Settings_ on the device and tap the connected network to see the device's address. If you use a Bluetooth connection, it's likely _172.25.47.1_. You can also try the hostname _digitalpaper.local_. Use the _register_ command like seen below, substituting the IP address of the device.\n\n```\ndptrp1 --addr 10.0.0.1 register\n```\n\nIf you get an error, wait a few seconds and try again. Sometimes it takes two or three tries to work.\n\n## Mounting as a file system\nThis Repository contains a `dptmount` script to mount the Digital Paper as a userspace mount. This tool has additional requirements.\n\n- On macOS, install osxfuse (e.g. with `brew cask install osxfuse`). \n- On Linux, you may need to install libfuse.\n\n### How to use \nCreate a yaml file with configuration details at _~/.config/dpt-rp1.conf_. You must specify either an address (with `addr`) or a Device ID (with `serial`). All entries must be strings, the serial number must be wrapped in quotation marks.\n\n```\ndptrp1:\n  addr: 192.168.0.200\n  serial: \"50040222\"\n  client-id: ~/.config/dpt/deviceid.dat\n  key: ~/.config/dpt/privatekey.dat\n```\n\nIf you register with `dptrp1 register` command, the client-id shall be $HOME/.config/dpt/deviceid.dat, and key shall be $HOME/.config/dpt/privatekey.dat. Mount the Digital Paper to a directory with `dptmount --config ~/.config/dpt-rp1.conf /mnt/mountpoint`\n\n#### Finding the private key and client ID on Windows\n\nIf you have already registered on Windows, the Digital Paper app stores the files in _Users/{username}/AppData/Roaming/Sony Corporation/Digital Paper App/_. You'll need the files _deviceid.dat_ and _privatekey.dat_.\n\n#### Finding the private key and client ID on macOS\n\nIf you have already registered on macOS, the Digital Paper app stores the files in _$HOME/Library/Application Support/Sony Corporation/Digital Paper App/_. You'll need the files _deviceid.dat_ and _privatekey.dat_.\n\n#### What works\n* Reading files\n* Moving files (both rename and move to different folder)\n* Uploading new files\n* Deleting files and folders \n\n#### What does not work\n* Currently there is no caching, therefore operations can be slow as they require uploading or downloading from the \ndevice. However, this avoids having to resolve conflicts if a document has been changed both on the Digital Paper and\nthe caching directory.\n"
  },
  {
    "path": "docs/linux-ethernet-over-usb.md",
    "content": "# Accessing the DPT-RP1 over USB in Linux\n\nTo use the DPT-RP1 through the USB cable, you need to perform two steps:\n\n  1. Switch the USB mode for DPT-RP1 to Ethernet-over-USB.\n  2. Determine the IPv6 link-local address for `digitalpaper.local` using mDNS.\n\n## Switching the USB mode the Ethernet-over-USB.\n\nWhen the DPT-RP1 is plugged into a USB port, it appears as a USB CDC ACM device (i.e. a serial port), usually at `/dev/ttyACM0`.\n\nBy sending a sequence of bytes to this serial port, the DPT-RP1 mode can be switched to Ethernet-over-USB.\n\nThe DPT-RP1 supports two protocols for Ethernet-over-USB : remote NDIS (RNDIS) for Windows machines, and USB CDC/ECM for Macs. Linux supports both these modes. \n\nYou only need to enable one of these modes.\n\n### Activating RNDIS mode\n\nTo activate RNDIS mode, send the following Python byte sequence to `/dev/ttyACM0` using [pyserial](https://pythonhosted.org/pyserial/) for example.\n\n    b\"\\x01\\x00\\x00\\x01\\x00\\x00\\x00\\x01\\x00\\x04\"\n\nCheck the output of `dmesg` to verify this worked:\n    \n    rndis_host 2-1:1.0 usb0: register 'rndis_host' at usb-0000:00:14.0-1, RNDIS device, xx:xx:xx:xx:xx:xx\n    \nwhere `xx:xx:xx:xx:xx:xx` is the Ethernet address for the DPT-RP1.\n\n### Activating CDC/ECM mode\n\nTo activate CDC/ECM mode, send the following alternative Python byte sequence:\n\n    b\"\\x01\\x00\\x00\\x01\\x00\\x00\\x00\\x01\\x01\\x04\"\n\nThe `dmesg` command will show:\n\n    cdc_ether 2-1:1.0 usb0: register 'cdc_ether' at usb-0000:00:14.0-1, CDC Ethernet Device, xx:xx:xx:xx:xx:xx\n\n## De-activate DHCP on the new Ethernet device\n\nIf you're using DHCP to obtain addresses, you should disable it for the DPT-RP1, since the DPT-RP1 does not run a DHCP server. \n\nFor example, if you're using Network Manager, change the IPv4 settings on the DPT-RP1 Ethernet device to 'Link-Local Only' instead of 'Automatic'. This will assign your end of the Ethernet link an IPv4 link-local address in the 169.254.0.0/16 range. \n\nWhen using Network Manager, also make sure that in the 'Ethernet' tab, 'device' is set to the interface name, not the MAC address. This will help Network Manager to restore the settings when connecting next time.\n\n## Determining the address for DPT-RP1\n\nThe DPT-RP1 uses an IPv6 link-local address when in Ethernet-over-USB. You can determine this address by using an mDNS resolver such as `avahi`.\n\n    $ avahi-resolve -n digitalpaper.local\n    digitalpaper.local\tfe80::xxxx:xxxx:xxxx:xxxx\n\nAlthough this returns the IPv6 link-local address, at least on my system, this address is incomplete. IPv6 link-local addresses need a scope identifier which identifies the network interface (i.e. link). On my system, the DPT-RP1 Ethernet device appears as `usb0` (from the output of `ifconfig`), and therefore the full address is:\n\n    fe80::xxxx:xxxx:xxxx:xxxx%usb0\n\nThe full URI for the DPT-RP1 would be:\n\n    https://[fe80::xxxx:xxxx:xxxx:xxxx%usb0]:8443/...\n\nThis syntax is accepted by urllib3 v1.22 and above.\n\n# Accessing the Fujitsu Quaderno Gen 2 over USB in Linux\n\nThe instructions in this guide will work, at the exception of the last part, trying to find the IPv6 local address:\nInstead of `digitalpaper.local`, the Quaderno name is `Android.local`.\n\n    $ avahi-resolve -n Android.local\n    Android.local\tfe80::xxxx:xxxx:xxxx:xxxx\n\nAnother way to find the device IP address if you don't know the name is to run `avahi-browse`:\n\n\t $ avahi-browse -avr\n\t = usb0 IPv6 Digital Paper FMVDP41                         _dp_fujitsu._tcp     local\n    hostname = [Android.local]\n    address = [fe80::xxxx:xxxx:xxxx:xxxx]\n    port = [8080]\n    txt = []\n\n"
  },
  {
    "path": "dptrp1/__init__.py",
    "content": ""
  },
  {
    "path": "dptrp1/cli/__init__.py",
    "content": ""
  },
  {
    "path": "dptrp1/cli/dptmount.py",
    "content": "#!/usr/bin/env python3\n\"\"\"\nUsage\n-----\n\n> dptmount /mnt/mymountpoint\n\n\nConfig file\n------------\n\nA simple yaml such as\n\n> dptrp1:\n>   client-id: ~/.config/dpt/deviceid.dat\n>   key: ~/.config/dpt/privatekey.dat\n>   addr: 192.168.0.200\n\nTodo\n----\n\n* Main thing is to allow for writing/uploading\n* Also, a reasonable and robust caching is needed\n* Rename/Move should be possible in the near future\n\nAuthor\n------\n\nJuan Grigera <juan@grigera.com.ar>\n\nupload functionality by Jochen Schroeder <cycomanic@gmail.com>\n\"\"\"\n\n# debian-dependency: python3-fusepy\n# pip3 install fusepy\n\nimport os\nimport sys\nimport errno\nimport time\nimport calendar\nimport yaml\nimport io\nfrom errno import ENOENT, EACCES\nfrom stat import S_IFDIR, S_IFLNK, S_IFREG\n\nimport logging\n\nlogger = logging.getLogger(\"dptmount\")\n\ntry:\n    from fuse import FUSE, FuseOSError, Operations, LoggingMixIn\nexcept ModuleNotFoundError:\n    from fusepy import FUSE, FuseOSError, Operations, LoggingMixIn\nfrom dptrp1.dptrp1 import DigitalPaper, find_auth_files\n\nimport anytree\n\nclass FileHandle(object):\n\n    def __init__(self, fs, local_path, new=False):\n        self.fs = fs\n        dpath, fname = os.path.split(local_path)\n        self.parent = self.fs._map_local_remote(dpath)\n        self.remote_path = os.path.join(self.parent.remote_path, fname)\n        if new:\n            self.status = \"clean\"\n        else:\n            node = self.fs._map_local_remote(local_path)\n            assert self.remote_path == node.item['entry_path']\n            self.status = \"unread\"\n        self.data = bytearray()\n\n    def read(self, length, offset):\n        if self.status == \"unread\":\n            logger.info('Downloading %s', self.remote_path)\n            self.status = \"clean\"\n            self.data = self.fs.dpt.download(self.remote_path)\n        return self.data[offset:offset + length]\n\n    def write(self, buf, offset):\n        self.status = \"dirty\"\n        self.data[offset:] = buf\n        return len(buf)\n\n    def flush(self):\n        if self.status != \"dirty\":\n            return\n        stream = io.BytesIO(self.data)\n        self.fs.dpt.upload(stream, self.remote_path)\n        # XXX do we sometimes need to remove an old node?\n        self.fs._add_remote_path_to_tree(self.parent, self.remote_path) # TBI\n        self.status = \"clean\"\n\nclass DptTablet(LoggingMixIn, Operations):\n    def __init__(\n        self,\n        dpt_ip_address=None,\n        dpt_serial_number=None,\n        dpt_key=None,\n        dpt_client_id=None,\n        uid=None,\n        gid=None,\n    ):\n        self.dpt_ip_address = dpt_ip_address\n        self.dpt_serial_number = dpt_serial_number\n        self.dpt_key = os.path.expanduser(dpt_key)\n        self.dpt_client_id = os.path.expanduser(dpt_client_id)\n        self.uid = uid\n        self.gid = gid\n        self.__authenticate__()\n\n        # Create root node\n        self.__init_empty_tree()\n\n        # Cache this for the session\n        logger.info(\"Loading initial document list\")\n        self._load_document_list()\n        logger.debug(anytree.RenderTree(self.root))\n\n        self.handle = {}\n        self.files = {}\n        self.fd = 0\n\n    def __init_empty_tree(self):\n        # Create root node\n        self.now = time.time()\n        self.root = anytree.Node('Document', item = None, localpath='/',\n                         remote_path=\"Document\",\n                         lstat=dict(st_mode=(S_IFDIR | 0o755),\n                                    st_ctime=self.now,\n                                    st_mtime=self.now,\n                                    st_atime=self.now,\n                                    st_nlink=2), )\n\n    def __authenticate__(self):\n        self.dpt = DigitalPaper(self.dpt_ip_address, self.dpt_serial_number)\n\n        with open(self.dpt_client_id) as fh:\n            client_id = fh.readline().strip()\n\n        with open(self.dpt_key, \"rb\") as fh:\n            key = fh.read()\n\n        self.dpt.authenticate(client_id, key)\n\n    def _remove_node(self, node):\n        node.parent = None\n        del node\n\n    def _add_node_to_tree(self, parent, item):\n        return anytree.Node(\n            item[\"entry_name\"],\n            parent=parent,\n            item=item,\n            remote_path=item[\"entry_path\"],\n            lstat=self._get_lstat(item),\n            localpath=os.path.join(parent.localpath, item[\"entry_name\"]),\n        )\n\n    def _add_remote_path_to_tree(self, parent, remote_path):\n        item = self.dpt._resolve_object_by_path(remote_path)\n        return self._add_node_to_tree(parent, item)\n\n    def _load_document_list(self):\n        # TODO maybe some smarter caching?\n        self._recurse_load_document_list(self.root)\n\n    def _recurse_load_document_list(self, parent):\n        parentnodepath = \"/\".join([str(node.name) for node in parent.path])\n\n        for item in self.dpt.list_objects_in_folder(parentnodepath):\n            node = self._add_node_to_tree(parent, item)\n            if item[\"entry_type\"] == \"folder\":\n                self._recurse_load_document_list(node)\n\n    def _get_lstat(self, item):\n        if \"reading_date\" in item:\n            atime = calendar.timegm(\n                time.strptime(item[\"reading_date\"], \"%Y-%m-%dT%H:%M:%SZ\")\n            )\n        else:\n            # access time = now if never read...\n            atime = self.now\n\n        lstat = dict(\n            st_atime=atime,\n            st_gid=self.gid,\n            st_uid=self.uid,\n            st_ctime=calendar.timegm(\n                time.strptime(item[\"created_date\"], \"%Y-%m-%dT%H:%M:%SZ\")\n            ),\n        )\n\n        # usual thing for directories is st_link keeps number of subdirectories\n        if item[\"entry_type\"] == \"folder\":\n            lstat[\"st_nlink\"] = 2\n            # todo: increment nlink in parent dir\n            lstat[\"st_mode\"] = S_IFDIR | 0o755\n            lstat[\"st_mtime\"] = self.now\n        else:\n            lstat[\"st_mode\"] = S_IFREG | 0o644\n            lstat[\"st_mtime\"] = calendar.timegm(\n                time.strptime(item[\"modified_date\"], \"%Y-%m-%dT%H:%M:%SZ\")\n            )\n            lstat[\"st_nlink\"] = 1\n            lstat[\"st_size\"] = int(item[\"file_size\"])\n\n            #'st_inot': item['entry_id'], 'entry_id': 'fe13e1df-1cfe-4fe3-9e83-3e12e78b8a47',\n\n        # 'entry_name': '10.1017.pdf', 'entry_path': 'Document/10.1017.pdf', 'entry_type': 'document',\n        # 'file_revision': 'a21ea4b1c368.2.0',\n        # 'is_new': 'false', 'mime_type': 'application/pdf',\n        # 'title': 'untitled', 'total_page': '4'}\n        return lstat\n\n    def _map_local_remote(self, full_local):\n        return anytree.search.find(\n            self.root, filter_=lambda node: node.localpath == full_local\n        )\n\n    def _is_read_only_flags(self, flags):\n        # from pcachefs\n        access_flags = os.O_RDONLY | os.O_WRONLY | os.O_RDWR\n        return flags & access_flags == os.O_RDONLY\n\n    # Filesystem methods\n    # ==================\n    def chmod(self, path, mode):\n        # TODO: should support chown/chmod\n        return 0\n\n    def chown(self, path, uid, gid):\n        # TODO: should support chown/chmod\n        return 0\n\n    def getattr(self, path, fh=None):\n        if path in self.files:\n            return self.files[path]\n        node = self._map_local_remote(path)\n        if node is None:\n            raise FuseOSError(ENOENT)\n        return node.lstat\n\n    def readdir(self, path, fh):\n        node = self._map_local_remote(path)\n        entries = node.children\n\n        dirents = [\".\", \"..\"]\n        dirents.extend([e.name for e in entries])\n        logger.debug(dirents)\n        return dirents\n\n    def unlink(self, path):\n        node = self._map_local_remote(path)\n        remote_path = node.remote_path\n        data = self.dpt.delete_document(node.remote_path)\n        self._remove_node(node)\n        return 0\n\n    # Directory creation\n    # ============\n    def rmdir(self, path):\n        node = self._map_local_remote(path)\n        self.dpt.delete_folder(node.remote_path)\n        self._remove_node(node)\n        return 0\n\n    def mkdir(self, path, mode):\n        ppath, dirname = os.path.split(path)\n        parent = self._map_local_remote(ppath)\n        remote_path = os.path.join(parent.remote_path, dirname)\n        self.dpt.new_folder(remote_path)\n        node = self._add_remote_path_to_tree(parent, remote_path)\n        return 0\n\n    # File methods\n    # ============\n    def open(self, path, flags):\n        if not self._is_read_only_flags(flags):\n            return FuseOSError(EACCES)\n        self.fd += 1\n        self.handle[self.fd] = FileHandle(self, path, new=False)\n        logger.info('file handle %d opened' % self.fd)\n        return self.fd\n\n    def release(self, path, fh):\n        # TODO: something is going wrong with releasing the file handles for new created docs\n        logger.info(\"file handle %d closed\" % fh)\n        node = self._map_local_remote(path)\n        del self.handle[fh]\n        return 0\n\n    def read(self, path, length, offset, fh):\n        return self.handle[fh].read(length, offset)\n\n    def rename(self, oldpath, newpath):\n        old_node = self._map_local_remote(oldpath)\n        new_folder, fname = os.path.split(newpath)\n        new_folder_node = self._map_local_remote(new_folder)\n        newpath = os.path.join(new_folder_node.remote_path, fname)\n        self.dpt.rename_document(old_node.remote_path, newpath)\n        self._remove_node(old_node)\n        self._add_remote_path_to_tree(new_folder_node, newpath)\n\n    def create(self, path, mode, fi=None):\n        #TODO: check if files is necessary\n        logger.debug(\"create path {}\".format(path))\n        self.files[path] = dict(\n            st_mode=(S_IFREG | mode),\n            st_nlink=1,\n            st_size=0,\n            st_ctime=time.time(),\n            st_mtime=time.time(),\n            st_atime=time.time(),\n        )\n\n        self.fd += 1\n        self.handle[self.fd] = FileHandle(self, path, new=True)\n        return self.fd\n\n    def write(self, path, buf, offset, fh):\n        return self.handle[fh].write(buf, offset)\n\n    def flush(self, path, fh):\n        self.handle[fh].flush()\n        self.files.pop(path, None)\n\nYAML_CONFIG_PATH = os.path.expanduser(\"~/.config/dpt-rp1.conf\")\n\n\ndef main():\n    import argparse\n\n    parser = argparse.ArgumentParser()\n    parser.add_argument(\"mountpoint\")\n\n    parser.add_argument(\n        \"--config\",\n        default=YAML_CONFIG_PATH,\n        help=\"config file, default is %s\" % YAML_CONFIG_PATH,\n    )\n    parser.add_argument(\"--verbose\", action=\"store_true\", help=\"Enable verbose logging\")\n    parser.add_argument(\n        \"--logfile\",\n        default=False,\n        help=\"Log to a file (default: log to standard output)\",\n    )\n    parser.add_argument(\"--big_writes\", default=True, help=\"Enable writes of big\")\n    args = parser.parse_args()\n    kwarg = [\"big_writes\"]\n    kwargs = {}\n    for k in kwarg:\n        kwargs[k] = getattr(args, k)\n\n    # Set up logging\n    if args.logfile is False:\n        logging.basicConfig()\n    else:\n        logging.basicConfig(filename=args.logfile)\n\n    if args.verbose:\n        logging.getLogger().setLevel(logging.DEBUG)\n    else:\n        logging.getLogger().setLevel(logging.INFO)\n\n    # Read YAML config if found\n    if os.path.isfile(args.config):\n        config = yaml.safe_load(open(args.config, \"r\"))\n    else:\n        print(\"Config file not found\")\n        sys.exit(-1)\n\n    # config\n    dpt_client_id, dpt_key = find_auth_files()\n    cfgargs = config[\"dptrp1\"]\n    params = dict(\n        dpt_ip_address=cfgargs.get(\"addr\", None),\n        dpt_serial_number=cfgargs.get(\"serial\", None),\n        dpt_client_id=cfgargs.get(\"client-id\", dpt_client_id),\n        dpt_key=cfgargs.get(\"key\", dpt_key),\n        uid=os.getuid(),\n        gid=os.getgid(),\n    )\n\n    tablet = DptTablet(**params)\n    fuse = FUSE(\n        tablet,\n        args.mountpoint,\n        foreground=True,\n        allow_other=False,\n        nothreads=True,\n        **kwargs\n    )\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "dptrp1/cli/dptrp1.py",
    "content": "#!/usr/bin/env python3\n# coding=utf-8\n\nimport argparse\nimport inspect\nimport json\nimport sys\nimport os\nimport re\n\nfrom pathlib import Path\nfrom dptrp1.dptrp1 import DigitalPaper, find_auth_files, get_default_auth_files\n\nROOT_FOLDER = 'Document'\n\ndef do_screenshot(d, filename):\n    \"\"\"\n    Take a screenshot of the device's screen and save it to the given local path.\n    \"\"\"\n    pic = d.take_screenshot()\n    with open(filename, \"wb\") as f:\n        f.write(pic)\n\ndef do_list_templates(d):\n    data = d.list_templates()\n    for d in data:\n        print(d[\"template_name\"])\n\ndef do_list_documents(d):\n    data = d.list_documents()\n    for d in data:\n        print(d[\"entry_path\"])\n\n\ndef do_list_folders(d, *remote_paths):\n    data = d.list_all()\n    for d in data:\n        if d[\"entry_type\"] == \"folder\":\n            print(d[\"entry_path\"] + \"/\")\n\n\ndef do_move_document(d, old_path, new_path):\n    d.move_file(old_path, new_path)\n\n\ndef do_copy_document(d, old_path, new_path):\n    d.copy_file(old_path, new_path)\n\n\ndef do_upload(d, local_path, remote_path=\"\"):\n    \"\"\"\n    Upload a local document to the reader.\n    Will upload to Document/ if only the local path is specified.\n    \"\"\"\n    if not remote_path:\n        remote_path = ROOT_FOLDER + \"/\" + os.path.basename(local_path)\n    d.upload_file(local_path, add_prefix(remote_path))\n\ndef do_upload_template(d, local_path, template_name=''):\n    \"\"\"\n    Upload a local document as a template for the reader.\n    The template name will be set as the file name if\n    only the local path is specified.\n    \"\"\"\n    if not template_name:\n        template_name = os.path.basename(local_path)\n    with open(local_path, 'rb') as f:\n        d.upload_template(f, template_name)\n\ndef do_download(d, remote_path, local_path):\n    \"\"\"\n    Download a document from the reader to your computer.\n    \"\"\"\n    data = d.download(remote_path)\n\n    if os.path.isdir(local_path):\n        re.sub(\"/?$\", \"/\", local_path)\n        local_path += os.path.basename(remote_path)\n\n    with open(local_path, \"wb\") as f:\n        f.write(data)\n\n\ndef do_list_document_info(d, remote_path=''):\n    \"\"\"\n    Print metadata about a document on the device.\n    If no path is given, information is printed for every document on the device.\n    \"\"\"\n    if not remote_path:\n        infos = d.list_all()\n        for info in infos:\n            print(info[\"entry_path\"])\n            for key in info:\n                print(\"    - \" + key + \": \" + info[key])\n    else:\n        info = d.list_document_info(add_prefix(remote_path))\n        print(info[\"entry_path\"])\n        for key in info:\n            print(\"    - \" + key + \": \" + info[key])\n\n\ndef do_display_document(d, remote_path, page=1):\n    \"\"\"\n    Displays the given document on the reader.\n    The path must be a valid path on the device.\n    To display a local document, upload it first.\n    Optionally pass a page number to open a specific page, number 1 being the front page.\n    Will show the first page if the page parameter is omitted.\n    \n    Example: dptrp1 display-document Document/Magazines/Comic.pdf 5\n    \"\"\"\n    info = d.list_document_info(add_prefix(remote_path))\n    d.display_document(info[\"entry_id\"], page)\n\n\ndef do_update_firmware(d, local_path):\n    with open(local_path, \"rb\") as fwfh:\n        d.update_firmware(fwfh)\n\ndef add_prefix(remote_path: str) -> str:\n    return remote_path if remote_path.startswith(ROOT_FOLDER) else f'{ROOT_FOLDER}/{remote_path}'\n\ndef do_delete_document(d, remote_path):\n    d.delete_document(add_prefix(remote_path))\n\ndef do_delete_template(d,remote_path):\n    d.delete_template(remote_path)\n\ndef do_delete_folder(d, remote_path):\n    d.delete_folder(add_prefix(remote_path))\n\n\ndef do_sync(d, local_path, remote_path=\"Document\"):\n    \"\"\"\n    Synchronize all PDF documents between a local path (on your PC) and a\n    remote path (on the DPT). Older documents will be overwritten by newer ones\n    without any additional warning. Also synchronizes the time and date on the\n    reader to the computer's time and date.\n\n    Example: dptrp1 sync ~/Dropbox/Papers Document/Papers\n    \"\"\"\n    d.sync(local_path, remote_path)\n\n\ndef do_new_folder(d, remote_path):\n    d.new_folder(add_prefix(remote_path))\n\n\ndef do_wifi_list(d):\n    data = d.wifi_list()\n    print(json.dumps(data, indent=2))\n\n\ndef do_wifi_scan(d):\n    data = d.wifi_scan()\n    print(json.dumps(data, indent=2))\n\n\ndef do_wifi(d):\n    print(d.wifi_enabled()[\"value\"])\n\n\ndef do_wifi_enable(d):\n    print(d.enable_wifi())\n\n\ndef do_wifi_disable(d):\n    print(d.disable_wifi())\n\n\ndef do_add_wifi(d, cfg_file=\"\"):\n    try:\n        cfg = json.load(open(cfg_file))\n    except JSONDecodeError:\n        quit(\"JSONDecodeError: Check the contents of %s\" % cfg_file)\n    except FileNotFoundError:\n        quit(\"File Not Found: %s\" % cfg_file)\n    if not cfg:\n        print(\n            d.configure_wifi(\n                ssid=\"vecna2\",\n                security=\"psk\",\n                passwd=\"elijah is a cat\",\n                dhcp=\"true\",\n                static_address=\"\",\n                gateway=\"\",\n                network_mask=\"\",\n                dns1=\"\",\n                dns2=\"\",\n                proxy=\"false\",\n            )\n        )\n    else:\n        print(d.configure_wifi(**cfg))\n\n\ndef do_delete_wifi(d, cfg_file=\"\"):\n    try:\n        cfg = json.load(open(cfg_file))\n    except ValueError:\n        quit(\"JSONDecodeError: Check the contents of %s\" % cfg_file)\n    except FileNotFoundError:\n        quit(\"File Not Found: %s\" % cfg_file)\n    if not cfg:\n        print(d.delete_wifi(ssid=\"vecna2\", security=\"psk\"))\n    else:\n        print(d.delete_wifi(**cfg))\n\n\ndef do_register(d, key_file, id_file):\n    _, key, device_id = d.register()\n\n    with open(key_file, \"w\") as f:\n        f.write(key)\n\n    with open(id_file, \"w\") as f:\n        f.write(device_id)\n\n\ndef format_parameter(parameter):\n    desc = \"\"\n    if parameter.default != inspect.Parameter.empty:\n        desc += \"[\"\n    desc += \"<{}>\".format(parameter.name)\n    if parameter.default != inspect.Parameter.empty:\n        desc += \" = \" + str(parameter.default) + \"]\"\n    return desc\n\n\ndef do_help(command):\n    \"\"\"\n    Print additional information about a command, if available.\n    \"\"\"\n    try:\n        args = list(inspect.signature(commands[command]).parameters.values())\n        args = [format_parameter(x) for x in args[1:]]\n        print()\n        print(\"    Usage:\", sys.argv[0], command, *args)\n    except:\n        pass\n    print(commands[command].__doc__)\n\n\ndef do_get_config(d, path):\n    \"\"\"\n    Saves the current device configuration to the given path.\n    The configuration will be saved as a JSON file compatible with the set-configuration command.\n    \"\"\"\n    config = d.get_config()\n    with open(path, \"w\") as file:\n        json.dump(config, file, indent=4, sort_keys=True)\n\n\ndef do_set_config(d, path):\n    \"\"\"\n    Reads the JSON-encoded configuration file and applies the configuration to the device.\n    Use get-configuration first to read the current configuration.\n    \"\"\"\n    with open(path) as file:\n        config = json.load(file)\n    d.set_config(config)\n\n\ncommands = {\n    \"screenshot\": do_screenshot,\n    \"list-documents\": do_list_documents,\n    \"list-templates\" : do_list_templates,\n    \"document-info\": do_list_document_info,\n    \"upload\": do_upload,\n    \"upload-template\" : do_upload_template,\n    \"download\": do_download,\n    \"delete\": do_delete_document,\n    \"delete-folder\": do_delete_folder,\n    \"delete-template\": do_delete_template,\n    \"new-folder\": do_new_folder,\n    \"move-document\": do_move_document,\n    \"copy-document\": do_copy_document,\n    \"list-folders\": do_list_folders,\n    \"wifi-list\": do_wifi_list,\n    \"wifi-scan\": do_wifi_scan,\n    \"wifi-add\": do_add_wifi,\n    \"wifi-del\": do_delete_wifi,\n    \"wifi\": do_wifi,\n    \"wifi-enable\": do_wifi_enable,\n    \"wifi-disable\": do_wifi_disable,\n    \"register\": do_register,\n    \"update-firmware\": do_update_firmware,\n    \"sync\": do_sync,\n    \"help\": do_help,\n    \"display-document\": do_display_document,\n    \"get-configuration\": do_get_config,\n    \"set-configuration\": do_set_config,\n}\n\n\ndef build_parser():\n    p = argparse.ArgumentParser(description=\"Remote control for Sony DPT-RP1\")\n    p.add_argument(\n        \"--client-id\", help=\"File containing the device's client id\", default=None\n    )\n    p.add_argument(\n        \"--key\", help=\"File containing the device's private key\", default=None\n    )\n    p.add_argument(\n        \"--addr\",\n        help=\"Hostname or IP address of the device. Disables auto discovery.\",\n        default=None,\n    )\n    p.add_argument(\n        \"--serial\",\n        help=\"Device serial number for auto discovery. Auto discovery only works for some minutes after the Digital Paper's Wi-Fi setting is switched on.\",\n        default=None,\n    )\n    p.add_argument(\n        \"--yes\",\n        \"-y\",\n        help=\"Automatically answer yes to confirmation prompts, for running non-interactively.\",\n        action=\"store_true\",\n        dest=\"assume_yes\",\n        default=False,\n    )\n    p.add_argument(\n        \"--quiet\",\n        \"-q\",\n        help=\"Suppress informative messages.\",\n        action=\"store_true\",\n        dest=\"quiet\",\n        default=False,\n    )\n    p.add_argument(\"command\", help=\"Command to run\", choices=sorted(commands.keys()))\n    p.add_argument(\"command_args\", help=\"Arguments for the command\", nargs=\"*\")\n    return p\n\n\ndef main():\n    args = build_parser().parse_args()\n    if args.command in [\"help\", \"command-help\"]:\n        # Help is available without a device\n        commands[args.command](*args.command_args)\n        return\n\n    dp = DigitalPaper(\n        addr=args.addr, id=args.serial, assume_yes=args.assume_yes, quiet=args.quiet\n    )\n    if args.command == \"register\":\n        # When registering the device, we default to storing auth files in our own configuration directory\n        default_deviceid, default_privatekey = get_default_auth_files()\n        do_register(\n            dp, args.key or default_privatekey, args.client_id or default_deviceid\n        )\n        return\n\n    # When connecting to a device, we default to looking for auth files in\n    # both our own configuration directory and in Sony's paths\n    found_deviceid, found_privatekey = find_auth_files()\n    if not args.key:\n        args.key = found_privatekey\n    if not args.client_id:\n        args.client_id = found_deviceid\n\n    if not os.path.exists(args.key) or not os.path.exists(args.client_id):\n        print(\"Could not read device identifier and private key.\")\n        print(\"Please use command 'register' first:\")\n        print()\n        print(\"    {} register\".format(sys.argv[0]))\n        print()\n        exit(1)\n    with open(args.client_id) as fh:\n        client_id = fh.readline().strip()\n    with open(args.key, \"rb\") as fh:\n        key = fh.read()\n    dp.authenticate(client_id, key)\n\n    try:\n        commands[args.command](dp, *args.command_args)\n    except Exception as e:\n        print(\"An error occured:\", e, file=sys.stderr)\n        print(\"For help, call:\", sys.argv[0], \"help\", args.command)\n        sys.exit(1)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "dptrp1/dptrp1.py",
    "content": "#!/usr/bin/env python3\nimport os\nimport sys\nimport uuid\nimport time\nimport base64\nimport httpsig\nimport urllib3\nimport requests\nimport functools\nimport unicodedata\nimport pickle\nimport shutil\nfrom tqdm import tqdm\nfrom glob import glob\nfrom urllib.parse import quote_plus\nfrom dptrp1.pyDH import DiffieHellman\nfrom datetime import datetime, timezone\nfrom pbkdf2 import PBKDF2\nfrom Crypto.Hash import SHA256\nfrom Crypto.Hash.HMAC import HMAC\nfrom Crypto.Cipher import AES\nfrom Crypto.PublicKey import RSA\nfrom pathlib import Path\nfrom collections import defaultdict\n\nurllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)\n\n\ndef get_default_auth_files():\n    \"\"\"Get the default path where the authentication files for connecting to DPT-RP1 are stored\"\"\"\n    config_path = os.path.join(os.path.expanduser(\"~\"), \".config\", \"dpt\")\n    os.makedirs(config_path, exist_ok=True)\n    deviceid = os.path.join(config_path, \"deviceid.dat\")\n    privatekey = os.path.join(config_path, \"privatekey.dat\")\n\n    return deviceid, privatekey\n\n\ndef find_auth_files():\n    \"\"\"Search for authentication files for connecting to DPT-RP1, both in default path and in paths from Sony's Digital Paper App\"\"\"\n    deviceid, privatekey = get_default_auth_files()\n\n    if not os.path.exists(deviceid) or not os.path.exists(privatekey):\n        # Could not find our own auth-files. Let's see if we can find any auth files created by Sony's Digital Paper App\n        search_paths = [\n            os.path.join(\n                os.path.expanduser(\"~\"),\n                \"Library/Application Support/Sony Corporation/Digital Paper App\",\n            ),  # Mac\n            os.path.join(\n                os.path.expanduser(\"~\"),\n                \"AppData/Roaming/Sony Corporation/Digital Paper App\",\n            ),  # Windows\n        ]\n\n        for path in search_paths:\n            # Recursively look for deviceid.dat and privatekey.dat in any sub-folders of the search paths\n            deviceid_matches = glob(\n                os.path.join(path, \"**/deviceid.dat\"), recursive=True\n            )\n            privatekey_matches = glob(\n                os.path.join(path, \"**/privatekey.dat\"), recursive=True\n            )\n\n            if deviceid_matches and privatekey_matches:\n                # Found a match. Selecting the first file from each for now.\n                # This might not be correct if the user has several devices with their own keys. Should ideally be configurable\n                deviceid = deviceid_matches[0]\n                privatekey = privatekey_matches[0]\n                break\n\n    return deviceid, privatekey\n\n\nclass DigitalPaperException(Exception):\n    pass\n\n\nclass ResolveObjectFailed(DigitalPaperException):\n    pass\n\n\nclass LookUpDPT:\n    def __init__(self, quiet=False):\n        import threading\n\n        self.addr = None\n        self.id = None\n        self.lock = threading.Lock()\n        self.quiet = quiet\n\n    def update_service(self, zeroconf, service_type, name):\n        pass\n\n    def add_service(self, zeroconf, type, name):\n        info = zeroconf.get_service_info(type, name)\n        import ipaddress\n\n        addr = ipaddress.IPv4Address(info.addresses[0])\n        info = requests.get(\n            \"http://{}:{}/register/information\".format(addr, info.port)\n        ).json()\n        if not self.id:\n            self.id = info[\"serial_number\"]\n            if not self.quiet:\n                print(\"Found Digital Paper with serial number {}\".format(self.id))\n                print(\"To discover only this specific device, call:\")\n                print()\n                print(\n                    \"    {} --serial {} {}\".format(\n                        sys.argv[0], self.id, \" \".join(sys.argv[1:])\n                    )\n                )\n                print()\n        if info[\"serial_number\"] == self.id:\n            self.addr = str(addr)\n            self.lock.release()\n\n    def find(self, id, timeout=30):\n        from zeroconf import ServiceBrowser, Zeroconf\n\n        if not self.quiet:\n            print(\"Discovering Digital Paper for {} seconds…\".format(timeout))\n        sys.stdout.flush()\n        self.id = id\n        zc = Zeroconf()\n        self.lock.acquire()\n        ServiceBrowser(zc, [\"_digitalpaper._tcp.local.\", \"_dp_fujitsu._tcp.local.\"], self)\n        wait = self.lock.acquire(timeout=timeout) or (self.addr is not None)\n        zc.close()\n        if not wait:\n            print(\"Failed\".format(timeout))\n            return None\n        else:\n            if not self.quiet:\n                print(\"Found digital paper at\", self.addr)\n                print(\"To skip the discovery process (and this message), call:\")\n                print()\n                print(\n                    \"    {} --addr {} {}\".format(\n                        sys.argv[0], self.addr, \" \".join(sys.argv[1:])\n                    )\n                )\n                print()\n            return self.addr\n\n\nclass DigitalPaper:\n    def __init__(self, addr=None, id=None, assume_yes=False, quiet=False):\n        if addr:\n            self.addr = addr\n            if id:\n                print(\n                    \"Ignoring serial number since address is set. Remove --serial {} from call to silence this message.\".format(\n                        id\n                    )\n                )\n        else:\n            lookup = LookUpDPT(quiet=quiet)\n            self.addr = lookup.find(id)\n\n        self.session = requests.Session()\n        self.session.verify = False  # disable ssl certificate verification\n        self.assume_yes = assume_yes  # Whether to disable interactive prompts (currently only in sync())\n\n    @property\n    def base_url(self):\n        if self.addr and \":\" in self.addr and self.addr[0] != \"[\":\n            port = \"\"\n        else:\n            port = \":8443\"\n\n        return \"https://\" + self.addr + port\n\n    ### Authentication\n\n    def register(self):\n        \"\"\"\n        Gets authentication info from a DPT-RP1.  You can call this BEFORE\n        DigitalPaper.authenticate()\n\n        Returns (ca, priv_key, client_id):\n            - ca: a PEM-encoded X.509 server certificate, issued by the CA\n                  on the device\n            - priv_key: a PEM-encoded 2048-bit RSA private key\n            - client_id: the client id\n        \"\"\"\n\n        register_pin_url = \"/register/pin\"\n        register_hash_url = \"/register/hash\"\n        register_ca_url = \"/register/ca\"\n        register_url = \"/register\"\n        register_cleanup_url = \"/register/cleanup\"\n\n        print(\"Cleaning up...\")\n        r = self._reg_endpoint_request(\"PUT\", register_cleanup_url)\n        print(r)\n\n        print(\"Requesting PIN...\")\n        r = self._reg_endpoint_request(\"POST\", register_pin_url)\n        m1 = r.json()\n\n        n1 = base64.b64decode(m1[\"a\"])\n        mac = base64.b64decode(m1[\"b\"])\n        yb = base64.b64decode(m1[\"c\"])\n        yb = int.from_bytes(yb, \"big\")\n        n2 = os.urandom(16)  # random nonce\n\n        dh = DiffieHellman()\n        ya = dh.gen_public_key()\n        ya = b\"\\x00\" + ya.to_bytes(256, \"big\")\n\n        zz = dh.gen_shared_key(yb)\n        zz = zz.to_bytes(256, \"big\")\n        yb = yb.to_bytes(256, \"big\")\n\n        derivedKey = PBKDF2(\n            passphrase=zz, salt=n1 + mac + n2, iterations=10000, digestmodule=SHA256\n        ).read(48)\n\n        authKey = derivedKey[:32]\n        keyWrapKey = derivedKey[32:]\n\n        hmac = HMAC(authKey, digestmod=SHA256)\n        hmac.update(n1 + mac + yb + n1 + n2 + mac + ya)\n        m2hmac = hmac.digest()\n\n        m2 = dict(\n            a=base64.b64encode(n1).decode(\"utf-8\"),\n            b=base64.b64encode(n2).decode(\"utf-8\"),\n            c=base64.b64encode(mac).decode(\"utf-8\"),\n            d=base64.b64encode(ya).decode(\"utf-8\"),\n            e=base64.b64encode(m2hmac).decode(\"utf-8\"),\n        )\n\n        print(\"Encoding nonce...\")\n        r = self._reg_endpoint_request(\"POST\", register_hash_url, data=m2)\n        m3 = r.json()\n\n        if base64.b64decode(m3.get(\"a\", \"\")) != n2:\n            print(\"Nonce N2 doesn't match\")\n            return\n\n        eHash = base64.b64decode(m3[\"b\"])\n        m3hmac = base64.b64decode(m3[\"e\"])\n        hmac = HMAC(authKey, digestmod=SHA256)\n        hmac.update(n1 + n2 + mac + ya + m2hmac + n2 + eHash)\n        if m3hmac != hmac.digest():\n            print(\"M3 HMAC doesn't match\")\n            return\n\n        pin = input(\"Please enter the PIN shown on the DPT-RP1: \")\n\n        hmac = HMAC(authKey, digestmod=SHA256)\n        hmac.update(pin.encode())\n        psk = hmac.digest()\n\n        rs = os.urandom(16)  # random nonce\n        hmac = HMAC(authKey, digestmod=SHA256)\n        hmac.update(rs + psk + yb + ya)\n        rHash = hmac.digest()\n\n        wrappedRs = wrap(rs, authKey, keyWrapKey)\n\n        hmac = HMAC(authKey, digestmod=SHA256)\n        hmac.update(n2 + eHash + m3hmac + n1 + rHash + wrappedRs)\n        m4hmac = hmac.digest()\n\n        m4 = dict(\n            a=base64.b64encode(n1).decode(\"utf-8\"),\n            b=base64.b64encode(rHash).decode(\"utf-8\"),\n            d=base64.b64encode(wrappedRs).decode(\"utf-8\"),\n            e=base64.b64encode(m4hmac).decode(\"utf-8\"),\n        )\n\n        print(\"Getting certificate from device CA...\")\n        r = self._reg_endpoint_request(\"POST\", register_ca_url, data=m4)\n        print(r)\n\n        m5 = r.json()\n\n        if base64.b64decode(m5[\"a\"]) != n2:\n            print(\"Nonce N2 doesn't match\")\n            return\n\n        wrappedEsCert = base64.b64decode(m5[\"d\"])\n        m5hmac = base64.b64decode(m5[\"e\"])\n\n        hmac = HMAC(authKey, digestmod=SHA256)\n        hmac.update(n1 + rHash + wrappedRs + m4hmac + n2 + wrappedEsCert)\n        if hmac.digest() != m5hmac:\n            print(\"HMAC doesn't match!\")\n            return\n\n        esCert = unwrap(wrappedEsCert, authKey, keyWrapKey)\n        es = esCert[:16]\n        cert = esCert[16:]\n\n        hmac = HMAC(authKey, digestmod=SHA256)\n        hmac.update(es + psk + yb + ya)\n        if hmac.digest() != eHash:\n            print(\"eHash does not match!\")\n            return\n\n        # print(\"Certificate: \")\n        # print(cert)\n\n        print(\"Generating RSA2048 keys\")\n        new_key = RSA.generate(2048, e=65537)\n\n        # with open(\"key.pem\", 'wb') as f:\n        #    f.write(new_key.exportKey(\"PEM\"))\n\n        keyPubC = new_key.publickey().exportKey(\"PEM\")\n\n        selfDeviceId = str(uuid.uuid4())\n        print(\"Device ID: \" + selfDeviceId)\n        selfDeviceId = selfDeviceId.encode()\n\n        # with open(\"client_id.txt\", 'wb') as f:\n        #    f.write(selfDeviceId)\n\n        wrappedDIDKPUBC = wrap(selfDeviceId + keyPubC, authKey, keyWrapKey)\n\n        hmac = HMAC(authKey, digestmod=SHA256)\n        hmac.update(n2 + wrappedEsCert + m5hmac + n1 + wrappedDIDKPUBC)\n        m6hmac = hmac.digest()\n\n        m6 = dict(\n            a=base64.b64encode(n1).decode(\"utf-8\"),\n            d=base64.b64encode(wrappedDIDKPUBC).decode(\"utf-8\"),\n            e=base64.b64encode(m6hmac).decode(\"utf-8\"),\n        )\n\n        print(\"Registering device...\")\n        r = self._reg_endpoint_request(\"POST\", register_url, data=m6)\n        print(r)\n\n        print(\"Cleaning up...\")\n        r = self._reg_endpoint_request(\"PUT\", register_cleanup_url)\n        print(r)\n\n        return (\n            cert.decode(\"utf-8\"),\n            new_key.exportKey(\"PEM\").decode(\"utf-8\"),\n            selfDeviceId.decode(\"utf-8\"),\n        )\n\n    def authenticate(self, client_id, key):\n        sig_maker = httpsig.Signer(secret=key, algorithm=\"rsa-sha256\")\n        nonce = self._get_nonce(client_id)\n        signed_nonce = sig_maker.sign(nonce)\n        data = {\"client_id\": client_id, \"nonce_signed\": signed_nonce}\n        r = self._put_endpoint(\"/auth\", data=data)\n        # cookiejar cannot parse the cookie format used by the tablet,\n        # so we have to set it manually.\n        _, credentials = r.headers[\"Set-Cookie\"].split(\"; \")[0].split(\"=\")\n        self.session.cookies[\"Credentials\"] = credentials\n        return r\n\n    ### File management\n    def list_templates(self):\n        data = self._get_endpoint('/viewer/configs/note_templates').json()\n        return data['template_list']\n\n    def list_documents(self):\n        data = self.traverse_folder_recursively(\"Document\")\n        return data\n\n    def list_all(self):\n        data = self._get_endpoint(\"/documents2?entry_type=all\").json()\n        return data[\"entry_list\"]\n\n    def list_objects_in_folder(self, remote_path):\n        remote_id = self._get_object_id(remote_path)\n        entries = self.list_folder_entries_by_id(remote_id)\n        return entries\n\n    def list_folder_entries_by_id(self, folder_id):\n        response = self._get_endpoint(f\"/folders/{folder_id}/entries\")\n        return response.json()[\"entry_list\"]\n\n    def traverse_folder(self, remote_path, fields=[]):\n        # In most cases, the request overhead of traversing folders is larger than the overhead of\n        # requesting all info. So let's just request all info and filter for remote_path on our side\n        if fields:\n            field_query = \"&fields=\" + \",\".join(fields)\n        else:\n            field_query = \"\"\n        entry_data = self._get_endpoint(\n            f\"/documents2?entry_type=all\" + field_query\n        ).json()\n\n        if entry_data.get(\"count\") != len(entry_data.get(\"entry_list\", [])):\n            # The device seems to not want to return more than 1300 items in the entry_list, meaning that we will miss entries if the device\n            # has more files/folders than this. Luckly, it can easily be detected by comparing the number of entries with the count.\n            # Perhaps there is some way to request the remaining entries from the same endpoint through some form of pagination,\n            # but we do not know how. Let's fall back to the slower recursive traversal\n            print(\"Warning: Fast folder traversal did not work. Falling back to slower, recursive folder traversal.\")\n            return self.traverse_folder_recursively(remote_path)\n        \n        all_entries = entry_data[\"entry_list\"]\n\n        return list(\n            filter(lambda e: e[\"entry_path\"].startswith(remote_path), all_entries)\n        )\n    \n    def traverse_folder_recursively(self, remote_path):\n        # This is the old recursive implementation of traverse_folder.\n        # It is slower because the main overhead when communicating with the DPT-RP1 is the request latency,\n        # and this recursive implementation makes one request per folder. However, the faster implementation\n        # above fails when there are more than 1300 items, in which case we fall back to this older implementation\n        def traverse(obj):\n            if obj['entry_type'] == 'document':\n                return [obj]\n            else:\n                children = self \\\n                  ._get_endpoint(\"/folders/{remote_id}/entries2\".format(remote_id = obj['entry_id'])) \\\n                  .json()['entry_list']\n                return [obj] + functools.reduce(lambda acc, c: traverse(c) + acc, children[::-1], [])\n        return traverse(self._resolve_object_by_path(remote_path))\n\n\n    def list_document_info(self, remote_path):\n        remote_info = self._resolve_object_by_path(remote_path)\n        return remote_info\n\n    def download(self, remote_path):\n        remote_id = self._get_object_id(remote_path)\n\n        path = \"/documents/{remote_id}/file\".format(remote_id=remote_id)\n        response = self._get_endpoint(path)\n        return response.content\n\n    def delete_document(self, remote_path):\n        try:\n            remote_id = self._get_object_id(remote_path)\n        except ResolveObjectFailed as e:\n            # Path not found\n            return\n        self.delete_document_by_id(remote_id)\n    \n    def delete_template(self,template_name):\n        template_list = self.list_templates()\n        for t in template_list:\n            if t['template_name']==template_name:\n                remote_id = t['note_template_id']\n                self.delete_template_by_id(remote_id)\n\n    def display_document(self, document_id, page=1):\n        info = {\"document_id\": document_id, \"page\": page}\n        r = self._put_endpoint(\"/viewer/controls/open2\", data=info)\n\n    def delete_folder(self, remote_path):\n        try:\n            remote_id = self._get_object_id(remote_path)\n        except ResolveObjectFailed as e:\n            # Path not found\n            return\n        self.delete_folder_by_id(remote_id)\n\n    def delete_document_by_id(self, doc_id):\n        self._delete_endpoint(f\"/documents/{doc_id}\")\n\n    def delete_folder_by_id(self, folder_id):\n        self._delete_endpoint(f\"/folders/{folder_id}\")\n    \n    def delete_template_by_id(self, template_id):\n        self._delete_endpoint(f\"/viewer/configs/note_templates/{template_id}\")\n\n    def upload_template(self, fh, remote_path):\n        filename = os.path.basename(remote_path)\n        info = {\n            \"templateName\": filename,\n            \"document_source\": \"\"\n        }\n        r = self._post_endpoint(\"/viewer/configs/note_templates\", data=info)\n        doc = r.json()\n        doc_url = \"/viewer/configs/note_templates/{}/file\".format(doc[\"note_template_id\"])\n        \n        files = { 'file': (quote_plus(filename), fh, 'rb') }\n        self._put_endpoint(doc_url, files=files)\n\n    def upload(self, fh, remote_path):\n        filename = os.path.basename(remote_path)\n\n        try:\n            # If there exists a document in the specified remote path, overwrite it.\n            doc_id = self._get_object_id(remote_path)\n        except ResolveObjectFailed as e:\n            remote_directory = os.path.dirname(remote_path)\n            self.new_folder(remote_directory)\n            directory_id = self._get_object_id(remote_directory)\n            info = {\n                \"file_name\": filename,\n                \"parent_folder_id\": directory_id,\n                \"document_source\": \"\",\n            }\n            r = self._post_endpoint(\"/documents2\", data=info)\n            doc = r.json()\n            doc_id = doc[\"document_id\"]\n\n        doc_url = \"/documents/{doc_id}/file\".format(doc_id=doc_id)\n\n        files = {\"file\": (quote_plus(filename), fh, \"rb\")}\n        self._put_endpoint(doc_url, files=files)\n\n    def new_folder(self, remote_path):\n        folder_name = os.path.basename(remote_path)\n        remote_directory = os.path.dirname(remote_path)\n        if not remote_directory:\n            return\n        if not self.path_exists(remote_directory):\n            self.new_folder(remote_directory)\n        directory_id = self._get_object_id(remote_directory)\n        info = {\"folder_name\": folder_name, \"parent_folder_id\": directory_id}\n\n        r = self._post_endpoint(\"/folders2\", data=info)\n\n    def list_folders(self):\n        if not self.folder_list:\n            data = self.list_all()\n            for d in data:\n                if d[\"entry_type\"] == \"folder\":\n                    self.folder_list.append(d[\"entry_path\"])\n        return self.folder_list\n\n    def download_file(self, remote_path, local_path):\n        local_folder = os.path.dirname(local_path)\n        # Make sure that local_folder exists so that we can write data there.\n        # If local_path is just a filename, local_folder will be '', and\n        # we won't need to create any directories.\n        if local_folder != \"\":\n            os.makedirs(os.path.dirname(local_path), exist_ok=True)\n        data = self.download(remote_path)\n        with open(local_path, \"wb\") as f:\n            f.write(data)\n\n    def upload_file(self, local_path, remote_path):\n        if self.path_is_folder(remote_path):\n            local_filename = os.path.basename(local_path)\n            remote_path = os.path.join(remote_path, local_filename)\n        with open(local_path, \"rb\") as f:\n            self.upload(f, remote_path)\n\n    def path_is_folder(self, remote_path):\n        remote_filename = os.path.basename(remote_path)\n        if not remote_filename:\n            # Always a folder if path ends in slash.\n            # Folder may not exist in this case!\n            return True\n        try:\n            remote_obj = self._resolve_object_by_path(remote_path)\n            if remote_obj[\"entry_type\"] == \"folder\":\n                return True\n        except ResolveObjectFailed:\n            pass\n        return False\n\n    def path_exists(self, remote_path):\n        try:\n            remote_id = self._get_object_id(remote_path)\n        except ResolveObjectFailed as e:\n            return False\n        return True\n\n    def sync(self, local_folder, remote_folder):\n        checkpoint_info = self.load_checkpoint(local_folder)\n        self.set_datetime()\n        self.new_folder(remote_folder)\n        print(\"Looking for changes on device... \", end=\"\", flush=True)\n        remote_info = self.traverse_folder_recursively(remote_folder)\n        print(\"done\")\n\n        # Syncing will require different comparions between local and remote paths.\n        # Let's normalize them to ensure stable comparisions,\n        # both with respect to unicode normalization and with respect to\n        # directory separator symbols.\n        def normalize_path(path):\n            return unicodedata.normalize(\"NFC\", path).replace(os.sep, \"/\")\n\n        # Create a defaultdict of defaultdict\n        # so that we can save data to it with two indexes, without having to manually create\n        # nested dictionaries.\n        # Will contain:\n        # file_data[<filename>][<checkpoint/remote/local>_time] = <timestamp>\n        file_data = defaultdict(lambda: defaultdict(lambda: None))\n\n        # When it comes to folders, we want to handle them separately and not care about creation/deletion.\n        # We therefore use a slightly different data structure:\n        # folder_data[<filename>][checkpoint/remote/local_exists] = True/False\n        folder_data = defaultdict(lambda: defaultdict(lambda: False))\n\n        # Then we will go changes locally, remotely, and in checkpoint, and save all modificaiton times to the same\n        # data structure for easy comparison, and the same with folders.\n\n        # The checkpoint and remote_info contain the same data-structure, because the checkpoint is simply a dump of\n        # remote_info at a previous point in time. Therefore, we use the same code to look through both of them:\n        for location_info, location in [\n            (checkpoint_info, \"checkpoint\"),\n            (remote_info, \"remote\"),\n        ]:\n            for f in location_info:\n                path = normalize_path(f[\"entry_path\"])\n                if path.startswith(remote_folder):\n                    if f[\"entry_type\"] == \"document\":\n                        modification_time = datetime.strptime(\n                            f[\"modified_date\"], \"%Y-%m-%dT%H:%M:%SZ\"\n                        )\n                        file_data[path][f\"{location}_time\"] = modification_time\n                    elif f[\"entry_type\"] == \"folder\":\n                        folder_data[path][f\"{location}_exists\"] = True\n\n        print(\"Looking for local changes... \", end=\"\", flush=True)\n        # Recursively traverse the local path looking for PDF files.\n        # Use relatively low-level os.scandir()-api instead of a higher-level api such as glob.glob()\n        # because os.scandir() gives access to mtime without having to perform an additional syscall on Windows,\n        # leading to much faster scanning times on Windows\n        def traverse_local_folder(path):\n            # Let's store to folder_data that this folder exists\n            relative_path = Path(path).relative_to(local_folder)\n            remote_path = normalize_path(\n                (Path(remote_folder) / relative_path).as_posix()\n            )\n            folder_data[remote_path][\"local_exists\"] = True\n            # And recursively go through all items inside of the folder\n            for entry in os.scandir(path):\n                if entry.is_dir():\n                    traverse_local_folder(entry.path)\n                # Only handle PDF files, ignore files starting with a dot.\n                elif entry.name.lower().endswith(\".pdf\") and not entry.name.startswith(\n                    \".\"\n                ):\n                    relative_path = Path(entry.path).relative_to(local_folder)\n                    remote_path = normalize_path(\n                        (Path(remote_folder) / relative_path).as_posix()\n                    )\n                    modification_time = datetime.utcfromtimestamp(entry.stat().st_mtime)\n                    file_data[remote_path][\"local_time\"] = modification_time\n\n        traverse_local_folder(local_folder)\n        print(\"done\")\n\n        # Let's loop through the data structure\n        # to create list of actions to take\n        to_download = []\n        to_delete_local = []\n        to_upload = []\n        to_delete_remote = []\n\n        missing_checkpoint_files = []\n\n        for filename, data in file_data.items():\n            if data[\"checkpoint_time\"] is None:\n                if data[\"remote_time\"] and data[\"local_time\"]:\n                    # File exists both on device and locally, but not in checkpoint.\n                    # Corrupt or missing checkpoint?\n                    # The safest bet is to assume that the two files are identical, and not sync in either directions.\n                    missing_checkpoint_files.append(filename)\n                    continue\n\n                if data[\"remote_time\"]:\n                    # File only exists on remote, so it's new and should be downloaded\n                    to_download.append(filename)\n                    continue\n                if data[\"local_time\"]:\n                    # File only exists locally, sot it's new and should be uploaded\n                    to_upload.append(filename)\n                    continue\n\n            # If we get to here, file exists in checkpoint\n            modified_local = (\n                data[\"local_time\"] and data[\"local_time\"] > data[\"checkpoint_time\"]\n            )\n            modified_remote = (\n                data[\"remote_time\"] and data[\"remote_time\"] > data[\"checkpoint_time\"]\n            )\n            deleted_local = data[\"local_time\"] is None\n            deleted_remote = data[\"remote_time\"] is None\n\n            if modified_local and modified_remote:\n                print(\n                    f\"Warning, sync conflict!  {filename} is changed both locally and remotely.\"\n                )\n                if data[\"local_time\"] > data[\"remote_time\"]:\n                    print(\"Local change is newer and will take precedence.\")\n                    to_upload.append(filename)\n                else:\n                    print(\"Remote change is newer and will take precedence.\")\n                    to_download.append(filename)\n            elif modified_local:\n                to_upload.append(filename)\n            elif modified_remote:\n                to_download.append(filename)\n            elif deleted_local:\n                to_delete_remote.append(filename)\n            elif deleted_remote:\n                to_delete_local.append(filename)\n\n        if missing_checkpoint_files:\n            print(\n                \"\\nWarning: The following files exist both locally and on the DPT, but do not seem to have been synchronized using this tool:\"\n            )\n\n            max_print = 20  # Let's only print the first max_print filenames to avoid completely flooding\n            # stdout with unusable information if missing metadata means that this happens\n            # to all files in the user's library\n            print(\"\\t\" + \"\\n\\t\".join(missing_checkpoint_files[:max_print]))\n            if len(missing_checkpoint_files) > max_print:\n                print(\n                    f\"\\t... and {len(missing_checkpoint_files)-max_print} additional files\"\n                )\n            print(\"The files will be assumed to be identical.\\n\")\n\n        # Just syncing the files will automatically create the necessary folders to store the given files, but it won't sync empty folders,\n        # or folder deletion. Therefore, let's go through the folder_data as well, to see which additional folder operations need to be performed:\n        folders_to_delete_remote = []\n        folders_to_delete_local = []\n        folders_to_create_remote = []\n        folders_to_create_local = []\n        for foldername, data in folder_data.items():\n            # data contains information about whether the given foldername exists locally, remotely, and in the checkpoint.\n            # In addition, we plan to upload/download some files, in which case we won't need to manually create the folders.\n            # So let's updte data to describe the expected situation after uploading/downloding those files, to decide which additional\n            # folder operations need to be performed.\n            data[\"remote_exists\"] = data[\"remote_exists\"] or any(\n                [f.startswith(foldername) for f in to_upload]\n            )\n            data[\"local_exists\"] = data[\"local_exists\"] or any(\n                [f.startswith(foldername) for f in to_download]\n            )\n\n            # Depending on whether the folder exists is remote/local/checkpoint, let's decide whether to create/delete the folder from remote/local.\n            create_remote = (\n                data[\"local_exists\"]\n                and (not data[\"checkpoint_exists\"])\n                and (not data[\"remote_exists\"])\n            )\n            create_local = (\n                data[\"remote_exists\"]\n                and (not data[\"checkpoint_exists\"])\n                and (not data[\"local_exists\"])\n            )\n            delete_remote = (\n                (not data[\"local_exists\"])\n                and data[\"checkpoint_exists\"]\n                and data[\"remote_exists\"]\n            )\n            delete_local = (\n                (not data[\"remote_exists\"])\n                and data[\"checkpoint_exists\"]\n                and data[\"local_exists\"]\n            )\n\n            if create_remote:\n                folders_to_create_remote.append(foldername)\n            if create_local:\n                folders_to_create_local.append(foldername)\n            if delete_remote:\n                folders_to_delete_remote.append(foldername)\n            if delete_local:\n                folders_to_delete_local.append(foldername)\n\n        # If a folder structure is deleted, let's sort the deletion so that we always select the innermost, empty, folder first.\n        folders_to_delete_remote.sort(reverse=True)\n        folders_to_delete_local.sort(reverse=True)\n\n        print(\"\")\n        print(\"Ready to sync\")\n        print(\"\")\n        actions = [\n            (to_delete_local + folders_to_delete_local, \"DELETED locally\"),\n            (to_delete_remote + folders_to_delete_remote, \"DELETED from device\"),\n            (to_upload + folders_to_create_remote, \"UPLOADED to device\"),\n            (to_download + folders_to_create_local, \"DOWNLOADED from device\"),\n        ]\n        for file_list, description in actions:\n            if file_list:\n                print(f\"{len(file_list):4d} files will be {description}\")\n\n        if not (\n            to_delete_local\n            or to_delete_remote\n            or to_upload\n            or to_download\n            or folders_to_delete_local\n            or folders_to_delete_remote\n            or folders_to_create_local\n            or folders_to_create_remote\n        ):\n            print(\"All files are in sync. Exiting.\")\n            return\n\n        # Conferm that the user actually wants to perform the actions that\n        # have been prepared.\n        print(\"\")\n        confirm = \"\"\n        while not (confirm in (\"y\", \"yes\") or self.assume_yes):\n            confirm = input(f\"Proceed (y/n/?)? \")\n            if confirm in (\"n\", \"no\"):\n                return\n            if confirm in (\"?\", \"list\", \"l\"):\n                for file_list, description in actions:\n                    if file_list:\n                        print(\"\")\n                        print(f\"The following files will be {description}:\")\n                        print(\"\\t\" + \"\\n\\t\".join(file_list))\n                        print(\"\")\n\n        # Syncing can potentially take some time, so let's display a progress bar\n        # to give the user some idea about the progress.\n        # Calling print() will interfere with the progress bar, so all print calls\n        # are replaced by tqdm.write() while the progress bar is in use\n        progress_bar = tqdm(\n            total=len(to_delete_local)\n            + len(to_delete_remote)\n            + len(to_upload)\n            + len(to_download),\n            desc=\"Synchronizing\",\n            unit=\"files\",\n        )\n\n        # Apply changes in remote to local\n        for remote_path in to_download:\n            relative_path = Path(remote_path).relative_to(remote_folder)\n            local_path = Path(local_folder) / relative_path\n            tqdm.write(\"⇣ \" + str(remote_path))\n            self.download_file(remote_path, local_path)\n            remote_time = (\n                file_data[remote_path][\"remote_time\"]\n                .replace(tzinfo=timezone.utc)\n                .astimezone(tz=None)\n            )\n            mod_time = time.mktime(remote_time.timetuple())\n            os.utime(local_path, (mod_time, mod_time))\n            progress_bar.update()\n\n        for remote_path in to_delete_local:\n            relative_path = Path(remote_path).relative_to(remote_folder)\n            local_path = Path(local_folder) / relative_path\n            if os.path.exists(local_path):\n                tqdm.write(\"X \" + str(local_path))\n                os.remove(local_path)\n            progress_bar.update()\n\n        for remote_path in folders_to_delete_local:\n            relative_path = Path(remote_path).relative_to(remote_folder)\n            local_path = Path(local_folder) / relative_path\n            if os.path.exists(local_path):\n                tqdm.write(\"X \" + str(local_path))\n                try:\n                    os.rmdir(local_path)\n                except OSError as e:\n                    if e.errno == 39:\n                        tqdm.write(\n                            f\"WARNING: The folder {local_path} is not empty and will not be deleted.\"\n                        )\n                    else:\n                        raise\n            progress_bar.update()\n\n        for remote_path in folders_to_create_local:\n            relative_path = Path(remote_path).relative_to(remote_folder)\n            local_path = Path(local_folder) / relative_path\n            tqdm.write(\"⇣ \" + str(remote_path))\n            os.makedirs(local_path, exist_ok=True)\n            progress_bar.update()\n\n        # Apply changes in local to remote\n        for remote_file in to_delete_remote:\n            if self.path_exists(remote_file):\n                tqdm.write(\"X \" + str(remote_file))\n                self.delete_document(remote_file)\n            progress_bar.update()\n\n        for remote_deletion_folder in folders_to_delete_remote:\n            if self.path_exists(remote_deletion_folder):\n                tqdm.write(\"X \" + str(remote_deletion_folder))\n                self.delete_folder(remote_deletion_folder)\n            progress_bar.update()\n\n        for remote_path in to_upload:\n            relative_path = Path(remote_path).relative_to(remote_folder)\n            local_path = Path(local_folder) / relative_path\n            tqdm.write(\"⇡ \" + str(local_path))\n            self.upload_file(local_path, remote_path)\n            progress_bar.update()\n\n        for remote_path in folders_to_create_remote:\n            relative_path = Path(remote_path).relative_to(remote_folder)\n            local_path = Path(local_folder) / relative_path\n            tqdm.write(\"⇡ \" + str(local_path))\n            self.new_folder(remote_path)\n            progress_bar.update()\n\n        progress_bar.close()\n\n        print(\"Refreshing file information... \", end=\"\", flush=True)\n        remote_info = self.traverse_folder(\n            remote_folder, fields=[\"entry_path\", \"modified_date\", \"entry_type\"]\n        )\n        self.sync_checkpoint(local_folder, remote_info)\n        print(\"done\")\n\n    def load_checkpoint(self, local_folder):\n        checkpoint_file = os.path.join(local_folder, \".sync\")\n        if not os.path.exists(checkpoint_file):\n            return []\n        with open(checkpoint_file, \"rb\") as f:\n            return pickle.load(f)\n\n    def sync_checkpoint(self, local_folder, doclist):\n        checkpoint_file = os.path.join(local_folder, \".sync\")\n        with open(checkpoint_file, \"wb\") as f:\n            pickle.dump(doclist, f)\n\n    def _copy_move_data(self, file_id, folder_id, new_filename=None):\n        data = {\"parent_folder_id\": folder_id}\n        if new_filename is not None:\n            data[\"file_name\"] = new_filename\n        return data\n\n    def copy_file_to_folder_by_id(self, file_id, folder_id, new_filename=None):\n        \"\"\"\n        Copies a file with given file_id to a folder with given folder_id.\n        If new_filename is given, rename the file.\n        \"\"\"\n        data = self._copy_move_data(file_id, folder_id, new_filename)\n        return self._post_endpoint(f\"/documents/{file_id}/copy\", data=data)\n\n    def move_file_to_folder_by_id(self, file_id, folder_id, new_filename=None):\n        \"\"\"\n        Moves a file with given file_id to a folder with given folder_id.\n        If new_filename is given, rename the file.\n        \"\"\"\n        data = self._copy_move_data(file_id, folder_id, new_filename)\n        return self._put_endpoint(f\"/documents/{file_id}\", data=data)\n\n    def _copy_move_find_ids(self, old_path, new_path):\n        old_id = self._get_object_id(old_path)\n        new_filename = None\n\n        try:  # find out whether new_path is a filename or folder\n            new_folder_id = self._get_object_id(new_path)\n        except ResolveObjectFailed:\n            new_filename = os.path.basename(new_path)\n            new_folder = os.path.dirname(new_path)\n            new_folder_id = self._get_object_id(new_folder)\n\n        return old_id, new_folder_id, new_filename\n\n    def copy_file(self, old_path, new_path):\n        \"\"\"\n        Copies a file with given path to a new path.\n        \"\"\"\n        old_id, new_folder_id, new_filename = self._copy_move_find_ids(\n            old_path, new_path\n        )\n        self.copy_file_to_folder_by_id(old_id, new_folder_id, new_filename)\n\n    def move_file(self, old_path, new_path):\n        \"\"\"\n        Moves a file with given path to a new path.\n        \"\"\"\n        old_id, new_folder_id, new_filename = self._copy_move_find_ids(\n            old_path, new_path\n        )\n        return self.move_file_to_folder_by_id(old_id, new_folder_id, new_filename)\n\n    ### Wifi\n    def wifi_list(self):\n        data = self._get_endpoint(\"/system/configs/wifi_accesspoints\").json()\n        for ap in data[\"aplist\"]:\n            ap[\"ssid\"] = base64.b64decode(ap[\"ssid\"]).decode(\"utf-8\", errors=\"replace\")\n        return data[\"aplist\"]\n\n    def wifi_scan(self):\n        data = self._post_endpoint(\"/system/controls/wifi_accesspoints/scan\").json()\n        for ap in data[\"aplist\"]:\n            ap[\"ssid\"] = base64.b64decode(ap[\"ssid\"]).decode(\"utf-8\", errors=\"replace\")\n        return data[\"aplist\"]\n\n    def configure_wifi(\n        self,\n        ssid,\n        security,\n        passwd,\n        dhcp,\n        static_address,\n        gateway,\n        network_mask,\n        dns1,\n        dns2,\n        proxy,\n    ):\n\n        #    cnf = {\n        #        \"ssid\": base64.b64encode(b'YYY').decode('utf-8'),\n        #        \"security\": \"nonsec\", # psk, nonsec, XXX\n        #        # \"passwd\": \"XXX\",\n        #        \"dhcp\": \"false\",\n        #        \"static_address\": \"172.20.123.4\",\n        #        \"gateway\": \"172.20.123.160\",\n        #        \"network_mask\": \"24\",\n        #        \"dns1\": \"172.20.123.160\",\n        #        \"dns2\": \"\",\n        #        \"proxy\": \"false\"\n        #    }\n\n        # print(kwargs['ssid'])\n        conf = dict(\n            ssid=base64.b64encode(ssid.encode()).decode(\"utf-8\"),\n            security=security,\n            passwd=passwd,\n            dhcp=dhcp,\n            static_address=static_address,\n            gateway=gateway,\n            network_mask=network_mask,\n            dns1=dns1,\n            dns2=dns2,\n            proxy=proxy,\n        )\n\n        return self._put_endpoint(\n            \"/system/controls/wifi_accesspoints/register\", data=conf\n        )\n\n    def delete_wifi(self, ssid, security):\n        url = \"/system/configs/wifi_accesspoints/{ssid}/{security}\".format(\n            ssid=ssid, security=security\n        )\n        # .format(ssid = base64.b64encode(ssid.encode()).decode('utf-8'),\n        return self._delete_endpoint(url)\n\n    def wifi_enabled(self):\n        return self._get_endpoint(\"/system/configs/wifi\").json()\n\n    def enable_wifi(self):\n        return self._put_endpoint(\"/system/configs/wifi\", data={\"value\": \"on\"})\n\n    def disable_wifi(self):\n        return self._put_endpoint(\"/system/configs/wifi\", data={\"value\": \"off\"})\n\n    ### Configuration\n\n    def get_config(self):\n        \"\"\"\n        Returns the current configuration.\n        Return value will be a dictionary of dictionaries.\n        \"\"\"\n        data = self._get_endpoint(\"/system/configs/\").json()\n        return data\n\n    def set_config(self, config):\n        \"\"\"\n        Update the device configuration.\n        Input uses the same format that get_config() returns.\n        \"\"\"\n        for key, setting in config.items():\n            data = self._put_endpoint(\"/system/configs/\" + key, data=setting)\n\n    def get_timeout(self):\n        data = self._get_endpoint(\"/system/configs/timeout_to_standby\").json()\n        return data[\"value\"]\n\n    def set_timeout(self, value):\n        data = self._put_endpoint(\n            \"/system/configs/timeout_to_standby\", data={\"value\": value}\n        )\n\n    def get_date_format(self):\n        data = self._get_endpoint(\"/system/configs/date_format\").json()\n        return data[\"value\"]\n\n    def set_date_format(self, value):\n        data = self._put_endpoint(\"/system/configs/date_format\", data={\"value\": value})\n\n    def get_time_format(self):\n        data = self._get_endpoint(\"/system/configs/time_format\").json()\n        return data[\"value\"]\n\n    def set_time_format(self, value):\n        data = self._put_endpoint(\"/system/configs/time_format\", data={\"value\": value})\n\n    def get_timezone(self):\n        data = self._get_endpoint(\"/system/configs/timezone\").json()\n        return data[\"value\"]\n\n    def set_timezone(self, value):\n        data = self._put_endpoint(\"/system/configs/timezone\", data={\"value\": value})\n\n    def get_owner(self):\n        data = self._get_endpoint(\"/system/configs/owner\").json()\n        return data[\"value\"]\n\n    def set_owner(self, value):\n        data = self._put_endpoint(\"/system/configs/owner\", data={\"value\": value})\n\n    ### System info\n\n    def get_storage(self):\n        data = self._get_endpoint(\"/system/status/storage\").json()\n        return data\n\n    def get_firmware_version(self):\n        data = self._get_endpoint(\"/system/status/firmware_version\").json()\n        return data[\"value\"]\n\n    def get_api_version(self):\n        resp = self._reg_endpoint_request(\"GET\", \"/api_version\")\n        return resp.json()[\"value\"]\n\n    def get_mac_address(self):\n        data = self._get_endpoint(\"/system/status/mac_address\").json()\n        return data[\"value\"]\n\n    def get_battery(self):\n        data = self._get_endpoint(\"/system/status/battery\").json()\n        return data\n\n    def get_info(self):\n        data = self._get_endpoint(\"/register/information\").json()\n        return data\n\n    def set_datetime(self):\n        now = datetime.utcnow().replace(microsecond=0).isoformat() + \"Z\"\n        self._put_endpoint(\"/system/configs/datetime\", data={\"value\": now})\n\n    ### Etc\n\n    def take_screenshot(self):\n        # Or \"{base_url}/system/controls/screen_shot\" for a PNG image.\n        r = self._get_endpoint(\"/system/controls/screen_shot2\", params={\"query\": \"jpeg\"})\n        return r.content\n\n    def ping(self):\n        \"\"\"\n        Returns True if we are authenticated.\n        \"\"\"\n        r = self._get_endpoint(\"/ping\")\n        return r.ok\n\n    ## Update firmware\n\n    def update_firmware(self, fwfh):\n        filename = \"FwUpdater.pkg\"\n        fw_url = \"/system/controls/update_firmware/file\".format(base_url=self.base_url)\n        files = {\"file\": (quote_plus(filename), fwfh, \"rb\")}\n        # TODO: add file transferring feedback\n        self._put_endpoint(fw_url, files=files)\n\n        precheck_msg = self._get_endpoint(\n            \"/system/controls/update_firmware/precheck\"\n        ).json()\n        battery_check = precheck_msg.get(\"battery\", \"not ok\")\n        uploaded_image_check = precheck_msg.get(\"image_file\", \"not ok\")\n\n        print(\"* battery check: {}\".format(battery_check))\n        print(\"* uploaded image check: {}\".format(uploaded_image_check))\n\n        for key in precheck_msg:\n            if not (key == \"battery\" or key == \"image_file\"):\n                print(\n                    \"! Find unrecognized key-value pair: ({0}, {1})\".format(\n                        key, precheck_msg[key]\n                    )\n                )\n\n        if battery_check == \"ok\" and uploaded_image_check == \"ok\":\n            # TODO: add check if status is 204\n            self._put_endpoint(\"/system/controls/update_firmware\")\n\n    ### Utility\n    def _reg_endpoint_request(self, method, endpoint, data=None, files=None):\n        base_url = \"http://{addr}:8080\".format(addr=self.addr)\n        req = requests.Request(method, base_url, json=data, files=files)\n        prep = self.session.prepare_request(req)\n        prep.url = prep.url.replace('%25', '%')\n        # modifying the prepared request, so that the \"endpoint\" part of\n        # the URL will not be modified by urllib.\n        prep.url += endpoint.lstrip(\"/\")\n        return self.session.send(prep)\n\n    def _endpoint_request(self, method, endpoint, data=None, files=None):\n        req = requests.Request(method, self.base_url, json=data, files=files)\n        prep = self.session.prepare_request(req)\n        prep.url = prep.url.replace('%25', '%')\n        # modifying the prepared request, so that the \"endpoint\" part of\n        # the URL will not be modified by urllib.\n        prep.url += endpoint.lstrip(\"/\")\n        return self.session.send(prep)\n\n    def _get_endpoint(self, endpoint=\"\"):\n        return self._endpoint_request(\"GET\", endpoint)\n\n    def _put_endpoint(self, endpoint=\"\", data={}, files=None):\n        return self._endpoint_request(\"PUT\", endpoint, data, files)\n\n    def _post_endpoint(self, endpoint=\"\", data={}):\n        return self._endpoint_request(\"POST\", endpoint, data)\n\n    def _delete_endpoint(self, endpoint=\"\", data={}):\n        return self._endpoint_request(\"DELETE\", endpoint, data)\n\n    def _get_nonce(self, client_id):\n        r = self._get_endpoint(f\"/auth/nonce/{client_id}\")\n        return r.json()[\"nonce\"]\n\n    def _resolve_object_by_path(self, path):\n        enc_path = quote_plus(path)\n        url = f\"/resolve/entry/path/{enc_path}\"\n        resp = self._get_endpoint(url)\n        if not resp.ok:\n            raise ResolveObjectFailed(path, resp.json()[\"message\"])\n        return resp.json()\n\n    def _get_object_id(self, path):\n        return self._resolve_object_by_path(path)[\"entry_id\"]\n\n\n# crypto helpers\ndef wrap(data, authKey, keyWrapKey):\n    hmac = HMAC(authKey, digestmod=SHA256)\n    hmac.update(data)\n    kwa = hmac.digest()[:8]\n    iv = os.urandom(16)\n    cipher = AES.new(keyWrapKey, AES.MODE_CBC, iv)\n\n    wrapped = cipher.encrypt(pad(data + kwa))\n    wrapped = wrapped + iv\n    return wrapped\n\n\n# from https://gist.github.com/adoc/8550490\ndef pad(bytestring, k=16):\n    \"\"\"\n    Pad an input bytestring according to PKCS#7\n\n    \"\"\"\n    l = len(bytestring)\n    val = k - (l % k)\n    return bytestring + bytearray([val] * val)\n\n\ndef unwrap(data, authKey, keyWrapKey):\n    iv = data[-16:]\n    cipher = AES.new(keyWrapKey, AES.MODE_CBC, iv)\n    unwrapped = cipher.decrypt(data[:-16])\n    unwrapped = unpad(unwrapped)\n\n    kwa = unwrapped[-8:]\n    unwrapped = unwrapped[:-8]\n\n    hmac = HMAC(authKey, digestmod=SHA256)\n    hmac.update(unwrapped)\n    local_kwa = hmac.digest()[:8]\n\n    if kwa != local_kwa:\n        print(\"Unwrapped kwa does not match\")\n\n    return unwrapped\n\n\ndef unpad(bytestring, k=16):\n    \"\"\"\n    Remove the PKCS#7 padding from a text bytestring.\n    \"\"\"\n\n    val = bytestring[-1]\n    if val > k:\n        raise ValueError(\"Input is not padded or padding is corrupt\")\n    l = len(bytestring) - val\n    return bytestring[:l]\n"
  },
  {
    "path": "dptrp1/pyDH.py",
    "content": "# \t\t\t   Apache License\n#         Version 2.0, January 2004\n#     Copyright 2015 Amirali Sanatinia\n\n\"\"\" Pure Python Diffie Hellman implementation \"\"\"\n\nimport os\nimport binascii\nimport hashlib\n\n# RFC 3526 - More Modular Exponential (MODP) Diffie-Hellman groups for\n# Internet Key Exchange (IKE) https://tools.ietf.org/html/rfc3526\n\nprimes = {\n    # 1536-bit\n    5: {\n        \"prime\": 0xFFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA237327FFFFFFFFFFFFFFFF,\n        \"generator\": 2,\n    },\n    # 2048-bit\n    14: {\n        \"prime\": 0xFFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AACAA68FFFFFFFFFFFFFFFF,\n        \"generator\": 2,\n    },\n    # 3072-bit\n    15: {\n        \"prime\": 0xFFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AAAC42DAD33170D04507A33A85521ABDF1CBA64ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6BF12FFA06D98A0864D87602733EC86A64521F2B18177B200CBBE117577A615D6C770988C0BAD946E208E24FA074E5AB3143DB5BFCE0FD108E4B82D120A93AD2CAFFFFFFFFFFFFFFFF,\n        \"generator\": 2,\n    },\n    # 4096-bit\n    16: {\n        \"prime\": 0xFFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AAAC42DAD33170D04507A33A85521ABDF1CBA64ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6BF12FFA06D98A0864D87602733EC86A64521F2B18177B200CBBE117577A615D6C770988C0BAD946E208E24FA074E5AB3143DB5BFCE0FD108E4B82D120A92108011A723C12A787E6D788719A10BDBA5B2699C327186AF4E23C1A946834B6150BDA2583E9CA2AD44CE8DBBBC2DB04DE8EF92E8EFC141FBECAA6287C59474E6BC05D99B2964FA090C3A2233BA186515BE7ED1F612970CEE2D7AFB81BDD762170481CD0069127D5B05AA993B4EA988D8FDDC186FFB7DC90A6C08F4DF435C934063199FFFFFFFFFFFFFFFF,\n        \"generator\": 2,\n    },\n    # 6144-bit\n    17: {\n        \"prime\": 0xFFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AAAC42DAD33170D04507A33A85521ABDF1CBA64ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6BF12FFA06D98A0864D87602733EC86A64521F2B18177B200CBBE117577A615D6C770988C0BAD946E208E24FA074E5AB3143DB5BFCE0FD108E4B82D120A92108011A723C12A787E6D788719A10BDBA5B2699C327186AF4E23C1A946834B6150BDA2583E9CA2AD44CE8DBBBC2DB04DE8EF92E8EFC141FBECAA6287C59474E6BC05D99B2964FA090C3A2233BA186515BE7ED1F612970CEE2D7AFB81BDD762170481CD0069127D5B05AA993B4EA988D8FDDC186FFB7DC90A6C08F4DF435C93402849236C3FAB4D27C7026C1D4DCB2602646DEC9751E763DBA37BDF8FF9406AD9E530EE5DB382F413001AEB06A53ED9027D831179727B0865A8918DA3EDBEBCF9B14ED44CE6CBACED4BB1BDB7F1447E6CC254B332051512BD7AF426FB8F401378CD2BF5983CA01C64B92ECF032EA15D1721D03F482D7CE6E74FEF6D55E702F46980C82B5A84031900B1C9E59E7C97FBEC7E8F323A97A7E36CC88BE0F1D45B7FF585AC54BD407B22B4154AACC8F6D7EBF48E1D814CC5ED20F8037E0A79715EEF29BE32806A1D58BB7C5DA76F550AA3D8A1FBFF0EB19CCB1A313D55CDA56C9EC2EF29632387FE8D76E3C0468043E8F663F4860EE12BF2D5B0B7474D6E694F91E6DCC4024FFFFFFFFFFFFFFFF,\n        \"generator\": 2,\n    },\n    # 8192-bit\n    18: {\n        \"prime\": 0xFFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AAAC42DAD33170D04507A33A85521ABDF1CBA64ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6BF12FFA06D98A0864D87602733EC86A64521F2B18177B200CBBE117577A615D6C770988C0BAD946E208E24FA074E5AB3143DB5BFCE0FD108E4B82D120A92108011A723C12A787E6D788719A10BDBA5B2699C327186AF4E23C1A946834B6150BDA2583E9CA2AD44CE8DBBBC2DB04DE8EF92E8EFC141FBECAA6287C59474E6BC05D99B2964FA090C3A2233BA186515BE7ED1F612970CEE2D7AFB81BDD762170481CD0069127D5B05AA993B4EA988D8FDDC186FFB7DC90A6C08F4DF435C93402849236C3FAB4D27C7026C1D4DCB2602646DEC9751E763DBA37BDF8FF9406AD9E530EE5DB382F413001AEB06A53ED9027D831179727B0865A8918DA3EDBEBCF9B14ED44CE6CBACED4BB1BDB7F1447E6CC254B332051512BD7AF426FB8F401378CD2BF5983CA01C64B92ECF032EA15D1721D03F482D7CE6E74FEF6D55E702F46980C82B5A84031900B1C9E59E7C97FBEC7E8F323A97A7E36CC88BE0F1D45B7FF585AC54BD407B22B4154AACC8F6D7EBF48E1D814CC5ED20F8037E0A79715EEF29BE32806A1D58BB7C5DA76F550AA3D8A1FBFF0EB19CCB1A313D55CDA56C9EC2EF29632387FE8D76E3C0468043E8F663F4860EE12BF2D5B0B7474D6E694F91E6DBE115974A3926F12FEE5E438777CB6A932DF8CD8BEC4D073B931BA3BC832B68D9DD300741FA7BF8AFC47ED2576F6936BA424663AAB639C5AE4F5683423B4742BF1C978238F16CBE39D652DE3FDB8BEFC848AD922222E04A4037C0713EB57A81A23F0C73473FC646CEA306B4BCBC8862F8385DDFA9D4B7FA2C087E879683303ED5BDD3A062B3CF5B3A278A66D2A13F83F44F82DDF310EE074AB6A364597E899A0255DC164F31CC50846851DF9AB48195DED7EA1B1D510BD7EE74D73FAF36BC31ECFA268359046F4EB879F924009438B481C6CD7889A002ED5EE382BC9190DA6FC026E479558E4475677E9AA9E3050E2765694DFC81F56E880B96E7160C980DD98EDD3DFFFFFFFFFFFFFFFF,\n        \"generator\": 2,\n    },\n}\n\n\nclass DiffieHellman:\n    \"\"\" Class to represent the Diffie-Hellman key exchange protocol \"\"\"\n\n    # Current minimum recommendation is 2048 bit.\n    def __init__(self, group=14):\n        if group in primes:\n            self.p = primes[group][\"prime\"]\n            self.g = primes[group][\"generator\"]\n        else:\n            raise Exception(\"Group not supported\")\n\n        self.__a = int(binascii.hexlify(os.urandom(32)), base=16)\n\n    def get_private_key(self):\n        \"\"\" Return the private key (a) \"\"\"\n        return self.__a\n\n    def gen_public_key(self):\n        \"\"\" Return A, A = g ^ a mod p \"\"\"\n        # calculate G^a mod p\n        return pow(self.g, self.__a, self.p)\n\n    def check_other_public_key(self, other_contribution):\n        # check if the other public key is valid based on NIST SP800-56\n        # 2 <= g^b <= p-2 and Lagrange for safe primes (g^bq)=1, q=(p-1)/2\n\n        if 2 <= other_contribution and other_contribution <= self.p - 2:\n            if pow(other_contribution, (self.p - 1) // 2, self.p) == 1:\n                return True\n        return False\n\n    def gen_shared_key(self, other_contribution):\n        \"\"\" Return g ^ ab mod p \"\"\"\n        # calculate the shared key G^ab mod p\n        if self.check_other_public_key(other_contribution):\n            self.shared_key = pow(other_contribution, self.__a, self.p)\n            return self.shared_key\n            # return hashlib.sha256(str(self.shared_key).encode()).digest()\n        else:\n            raise Exception(\"Bad public key from other party\")\n"
  },
  {
    "path": "samples/wifi_2.5G.json",
    "content": "{\n  \"security\": \"psk\",\n  \"ssid\": \"Roxy123\",\n  \"passwd\": \"Lucinda\",\n  \"dhcp\": \"true\",\n  \"static_address\": \"\",\n  \"gateway\": \"\",\n  \"network_mask\": \"\",\n  \"dns1\": \"\",\n  \"dns2\": \"\",\n  \"proxy\": \"false\"\n}\n"
  },
  {
    "path": "samples/wifi_5G.json",
    "content": "{\n  \"security\": \"psk\",\n  \"ssid\": \"BadBomb\",\n  \"passwd\": \"TickingLikeAHotSamosa\",\n  \"dhcp\": \"true\",\n  \"static_address\": \"\",\n  \"gateway\": \"\",\n  \"network_mask\": \"\",\n  \"dns1\": \"\",\n  \"dns2\": \"\",\n  \"proxy\": \"false\"\n}\n"
  },
  {
    "path": "samples/wifi_del_2.5G.json",
    "content": "{\n    \"security\": \"psk\",\n    \"ssid\": \"Roxy123\"\n}\n"
  },
  {
    "path": "setup.json",
    "content": "{\n    \"name\": \"dpt-rp1-py\",\n    \"version\": \"0.1.20\",\n    \"description\": \"Python package to manage a Sony DPT-RP1\",\n    \"license\": \"MIT\",\n    \"authors\": [\n        \"Jan-Gerd Tenberge\",\n        \"Cuihtlauac Alvarado\",\n        \"Juan Grigera\",\n        \"Yunjae Lee\",\n        \"Kazuhiko Sakaguchi\",\n        \"Yanzi Zhu\",\n        \"Sreepathi Pai\",\n        \"Jochen Schroeder\",\n        \"Alexander Fuchs\",\n        \"Xiang Ji\",\n        \"Håkon J. D. Johnsen\"\n    ],\n    \"emails\": [\n        \"jan-gerd.tenberge@uni-muenster.de\",\n        \"cuihtlauac.alvarado@orange.com\",\n        \"juan@grigera.com.ar\",\n        \"lyj7694@gmail.com\",\n        \"pi8027@gmail.com\",\n        \"zhuyanzi@gmail.com\",\n        \"sree314@gmail.com\",\n        \"jochen.schroeder@chalmers.se\",\n        \"alex.fu27@gmail.com\",\n        \"hi@xiangji.me\",\n        \"hakon.j.d.johnsen@ntnu.no\"\n    ],\n    \"namespace_packages\": [\n\n    ],\n    \"packages\": [\n        \"dptrp1\",\n        \"dptrp1.cli\"\n    ],\n    \"install_requires\": [\n        \"httpsig>=1.1.2\",\n        \"requests>=2.18.4\",\n        \"pbkdf2>=1.3\",\n        \"urllib3>=1.22\",\n        \"pyyaml\",\n        \"anytree\",\n        \"fusepy\",\n        \"zeroconf>=0.29.0\",\n        \"tqdm\",\n        \"setuptools\"\n    ],\n    \"entry_points\": {\n        \"console_scripts\": [\n            \"dptrp1=dptrp1.cli.dptrp1:main\",\n            \"dptmount=dptrp1.cli.dptmount:main\"\n        ]\n    },\n    \"classifiers\": [\n        \"Development Status :: 3 - Alpha\",\n        \"Programming Language :: Python :: 3\"\n    ]\n}\n\n"
  },
  {
    "path": "setup.py",
    "content": "#!/usr/bin/env python\n# coding=utf-8\n\nimport os\nimport sys\nimport json\nimport setuptools\nfrom setuptools.command.test import test as TestCommand\n\nDIRECTORY = os.path.dirname(os.path.realpath(__file__))\nSETUP_JSON = None\n\ntry:\n    with open(os.path.join(DIRECTORY, \"setup.json\"), \"r\") as f:\n        SETUP_JSON = json.load(f)\nexcept Exception as e:\n    print(\n        \"! Error loading setup.json file in the same directory as setup.py.\\n\"\n        + \"  Check your installation.\"\n    )\n    print(\"  Exception: {}\".format(e))\n    sys.exit(1)\n\n\ndef readme():\n    with open(os.path.join(DIRECTORY, \"README.md\"), encoding='utf-8') as f:\n        return f.read()\n\n\nclass PyTest(TestCommand):\n    user_options = [(\"pytest-args=\", \"a\", \"Arguments to pass to py.test\")]\n\n    def initialize_options(self):\n        TestCommand.initialize_options(self)\n        self.pytest_args = []\n\n    def run_tests(self):\n        # import here, cause outside the eggs aren't loaded\n        import pytest\n\n        errno = pytest.main(self.pytest_args)\n        sys.exit(errno)\n\n\nsetuptools.setup(\n    name=SETUP_JSON[\"name\"],\n    version=SETUP_JSON[\"version\"],\n    author=\", \".join(SETUP_JSON[\"authors\"]),\n    author_email=\", \".join(SETUP_JSON[\"emails\"]),\n    description=SETUP_JSON[\"description\"],\n    long_description=readme(),\n    long_description_content_type=\"text/markdown\",\n    license=SETUP_JSON[\"license\"],\n    keywords=\"\",\n    url=None,\n    namespace_packages=SETUP_JSON[\"namespace_packages\"],\n    packages=SETUP_JSON[\"packages\"],\n    install_requires=SETUP_JSON[\"install_requires\"],\n    tests_require=[\"pytest\"],\n    cmdclass={\"test\": PyTest},\n    entry_points=SETUP_JSON[\"entry_points\"],\n    classifiers=SETUP_JSON[\"classifiers\"],\n    include_package_data=True,\n    zip_safe=False,\n)\n"
  }
]