#!/usr/bin/env python3 """USB Files from the command line: the same protocol files.wadamesh.com speaks. Open USB Files on the device first (app drawer), then: scripts/usbfiles.py info scripts/usbfiles.py ls /sd/tiles scripts/usbfiles.py get /internal/meshcore-backup.json backup.json scripts/usbfiles.py put notes.txt /sd/transfer/notes.txt [--overwrite] scripts/usbfiles.py rm /sd/transfer/notes.txt scripts/usbfiles.py mkdir /sd/transfer/new scripts/usbfiles.py mv /sd/transfer/a.txt /sd/transfer/b.txt scripts/usbfiles.py df /internal scripts/usbfiles.py selftest # round trips, access rules, throughput Protocol: src/helpers/esp32/UsbFilesProtocol.h. The port is found automatically (the first /dev/cu.usbmodem*, /dev/ttyACM* or COM port) unless --port is given. """ import argparse import binascii import glob import json import os import random import struct import sys import time try: import serial except ImportError: print("Missing dependency: pyserial (python3 -m pip install pyserial)", file=sys.stderr) sys.exit(2) MAGIC = b"\xE7\x5A" T_HELLO, T_LIST, T_STAT, T_READ = 0x01, 0x02, 0x03, 0x04 T_WRITE_BEGIN, T_WRITE_DATA, T_WRITE_END = 0x05, 0x06, 0x07 T_REMOVE, T_MKDIR, T_RENAME, T_SPACE, T_ABORT, T_PING = 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D T_BEACON, T_BYE, T_REPLY = 0x40, 0x41, 0x80 STATUS = { 0: "ok", 1: "bad request", 2: "not found", 3: "exists", 4: "protected", 5: "no space", 6: "i/o error", 7: "bad path", 8: "busy", 9: "too big", 10: "bad checksum", 11: "bad offset", 12: "unsupported", 13: "no media", 14: "not empty", } S_OK, S_NOT_FOUND, S_EXISTS, S_PROTECTED, S_BAD_PATH, S_BAD_OFFSET = 0, 2, 3, 4, 7, 11 MAX_PAYLOAD = 4096 + 256 class DeviceError(Exception): def __init__(self, status, raw): self.status = status self.raw = raw # the reply body after the status byte message = "" if status == S_BAD_OFFSET else raw.decode("utf-8", "replace") super().__init__("%s: %s" % (STATUS.get(status, "status %d" % status), message)) def crc32(data): return binascii.crc32(data) & 0xFFFFFFFF def encode(ftype, seq, payload=b""): head = struct.pack(" MAX_PAYLOAD: del self.buf[:1] continue total = 7 + length + 4 if len(self.buf) < total: return body = bytes(self.buf[2:7 + length]) (crc,) = struct.unpack_from(" 4 else 0 key = (name, is_dir) if key in seen: continue seen.add(key) merged.append((name, bool(is_dir), size, bool(writable), threat)) return merged def stat(self, path): return json.loads(self.request(T_STAT, path.encode()).decode()) def get(self, path): size = self.stat(path)["s"] data = bytearray() chunk = int(self.hello.get("chunk", 4096)) while len(data) < size: body = self.request(T_READ, struct.pack(" the PE signature pe[64:68] = b"PE\0\0" samples = {"autorun.inf": b"[autorun]\r\n", "photo.jpg": bytes(pe), "notes.txt": b"hello"} for name, data in samples.items(): link.put(folder + "/" + name, data, overwrite=True) flags = {e[0]: e[4] for e in link.ls(folder)} assert flags.get("autorun.inf") == 1 and flags.get("notes.txt") == 0, flags assert flags.get("photo.jpg") == 0, flags # a harmless name... assert link.stat(folder + "/photo.jpg")["t"] == 5 # ...caught by its content assert link.stat(folder + "/notes.txt")["t"] == 0 for name in samples: link.rm(folder + "/" + name) print(" malware flags ok") # Throughput, on the first writable target. if targets: data = os.urandom(512 * 1024) path = targets[0] + "/speed.bin" try: link.rm(path) except DeviceError: pass t0 = time.time() link.put(path, data) up = time.time() - t0 t0 = time.time() back = link.get(path) down = time.time() - t0 link.rm(path) assert back == data print(" 512 KB to %s: up %.0f KB/s, down %.0f KB/s" % (targets[0], 512 / up, 512 / down)) print("selftest passed") def main(): ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) ap.add_argument("--port", help="serial port (default: first USB serial port found)") ap.add_argument("-v", "--verbose", action="store_true", help="show the device's log lines") ap.add_argument("command", choices=["info", "ls", "stat", "get", "put", "rm", "mkdir", "mv", "df", "selftest"]) ap.add_argument("args", nargs="*") ap.add_argument("--overwrite", action="store_true") ap.add_argument("--wait", type=float, default=15.0, help="seconds to wait for USB Files to open") a = ap.parse_args() port = a.port or find_port() if not port: raise SystemExit("No USB serial port found. Is the device plugged in?") link = Link(port, a.verbose) link.beacon_wait = a.wait try: beacon, hello = link.connect() if a.command == "info": print(json.dumps(hello, indent=2)) elif a.command == "ls": path = a.args[0] if a.args else "/" risk = {1: "autorun file", 2: "Windows program", 3: "Windows script", 4: "Windows shortcut", 5: "renamed Windows program"} for name, is_dir, size, writable, threat in link.ls(path): print("%s %10s %s%s%s" % ("rw" if writable else "r-", "" if is_dir else human(size), name, "/" if is_dir else "", (" !! MALWARE RISK: " + risk.get(threat, "?")) if threat else "")) elif a.command == "stat": print(json.dumps(link.stat(a.args[0]))) elif a.command == "get": data = link.get(a.args[0]) dest = a.args[1] if len(a.args) > 1 else os.path.basename(a.args[0]) with open(dest, "wb") as f: f.write(data) print("%s -> %s (%s)" % (a.args[0], dest, human(len(data)))) elif a.command == "put": with open(a.args[0], "rb") as f: data = f.read() link.put(a.args[1], data, a.overwrite) print("%s -> %s (%s)" % (a.args[0], a.args[1], human(len(data)))) elif a.command == "rm": link.rm(a.args[0]) elif a.command == "mkdir": link.mkdir(a.args[0]) elif a.command == "mv": link.mv(a.args[0], a.args[1]) elif a.command == "df": d = link.df(a.args[0]) print("%s used of %s, %s kept free for the device" % (human(d["u"]), human(d["t"]), human(d["r"]))) elif a.command == "selftest": selftest(link) except DeviceError as e: raise SystemExit("device: %s" % e) finally: link.close() if __name__ == "__main__": main()