Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 577de21fbe | |||
| 823f2e32a0 | |||
| 71ff27f099 | |||
| 85639fc8e3 | |||
| d6f6ddbafd | |||
| 4b41ebc243 | |||
| eb6d371354 | |||
| f8b79e6313 | |||
| b42064a73e | |||
| b085719f98 | |||
| 1aae58a2f1 | |||
| 19ffb414de | |||
| cd5fd56a1d | |||
| 7c2f454592 | |||
| a008e506c7 | |||
| 79c1bcb178 | |||
| e2d984adb0 | |||
| 872aca7c83 | |||
| 1658eb92b3 | |||
| 94ef650f80 | |||
| 74fe60f2d4 | |||
| e10b70df4e | |||
| d6d4489142 | |||
| ef80f8f271 |
@@ -1,6 +1,9 @@
|
||||
# pypicokey
|
||||
PicoKey tools for Python
|
||||
|
||||
Forked From https://github.com/polhenarejos/pypicokey,
|
||||
If fork is unproper please tell me and i'd delete it.
|
||||
|
||||
## Introduction
|
||||
|
||||
PicoKey firmware allows to convert a Raspberry Pico into a Hardware Security Module (HSM), FIDO2 device or OpenPGP card, to store private and secret keys, perform signing and ciphering operations, without exposing the key.
|
||||
|
||||
+323
@@ -0,0 +1,323 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Interactive commissioning helper for PicoKey
|
||||
|
||||
This script collects commissioning options (VID:PID, LED, options,
|
||||
product name, curves, etc.) and writes them to a JSON file
|
||||
(`commission_config.json`). It can optionally attempt to apply the
|
||||
configuration when `--apply` is passed; the apply action is a safe
|
||||
stub and will ask for confirmation before performing any hardware
|
||||
changes.
|
||||
|
||||
Usage:
|
||||
python configure.py # interactive
|
||||
python configure.py --out mycfg.json
|
||||
python configure.py --yes --out mycfg.json
|
||||
python configure.py --apply # will prompt before touching device
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
|
||||
CFG_OUT = "commission_config.json"
|
||||
VIDPID_RE = re.compile(r"^[0-9a-fA-F]{4}:[0-9a-fA-F]{4}$")
|
||||
|
||||
|
||||
def prompt(text: str, default: Optional[str] = None) -> Optional[str]:
|
||||
if default is None:
|
||||
val = input(f"{text} ")
|
||||
else:
|
||||
val = input(f"{text} [Default: {default}] ")
|
||||
if val.strip() == "":
|
||||
return None
|
||||
return val.strip()
|
||||
|
||||
|
||||
def prompt_int(text: str, default: Optional[int] = None, allow_empty=True) -> Optional[int]:
|
||||
while True:
|
||||
s = prompt(text, str(default) if default is not None else None)
|
||||
if s is None:
|
||||
return None if allow_empty else default
|
||||
try:
|
||||
return int(s)
|
||||
except ValueError:
|
||||
print("Please enter a number, or press enter to leave blank.")
|
||||
|
||||
|
||||
def prompt_bool(text: str, default: Optional[bool] = None) -> Optional[bool]:
|
||||
while True:
|
||||
defstr = None if default is None else ("y" if default else "n")
|
||||
s = prompt(text + " (y/n)", defstr)
|
||||
if s is None:
|
||||
return None
|
||||
if s.lower() in ("y", "yes", "t", "1"):
|
||||
return True
|
||||
if s.lower() in ("n", "no", "f", "0"):
|
||||
return False
|
||||
print("Please enter either "Y" or "N", or leave blank to keep the option unchanged.")
|
||||
|
||||
|
||||
def choose(text: str, choices: list[str], default: Optional[int] = None) -> Optional[str]:
|
||||
print(text)
|
||||
for i, c in enumerate(choices, start=1):
|
||||
print(f" {i}. {c}")
|
||||
sel = prompt_int("Enter number, or leave blank to keep current setting:", default=None)
|
||||
if sel is None:
|
||||
return None
|
||||
if 1 <= sel <= len(choices):
|
||||
return choices[sel - 1]
|
||||
print("Sorry, this input's not recognised. Either enter a number, or leave blank to keep the current setting:")
|
||||
return None
|
||||
|
||||
|
||||
def validate_vidpid(v: str) -> bool:
|
||||
return bool(VIDPID_RE.match(v))
|
||||
|
||||
|
||||
def interactive_build() -> dict:
|
||||
print("Welcome to the Python PicoKey interactive Comissioner,")
|
||||
|
||||
# Vendor / VID:PID
|
||||
VENDORS = {
|
||||
"Nitrokey HSM": {"vid": "20a0", "pid": "4230"},
|
||||
"Nitrokey FIDO2": {"vid": "20a0", "pid": "42b1"},
|
||||
"Nitrokey Pro": {"vid": "20a0", "pid": "4108"},
|
||||
"Nitrokey 3": {"vid": "20a0", "pid": "42b2"},
|
||||
"Nitrokey Start": {"vid": "20a0", "pid": "4211"},
|
||||
"Yubikey 4/5": {"vid": "1050", "pid": "0407"},
|
||||
"Yubikey NEO": {"vid": "1050", "pid": "0116"},
|
||||
"Yubico YubiHSM": {"vid": "1050", "pid": "0030"},
|
||||
"FSIJ Gnuk": {"vid": "234b", "pid": "0000"},
|
||||
"GnuPG e.V.": {"vid": "1209", "pid": "2440"},
|
||||
}
|
||||
|
||||
vendor_choices = list(VENDORS.keys()) + ["Custom VID:PID"]
|
||||
vendor_choice = choose("Select a known vendor...", vendor_choices, None)
|
||||
vidpid = None
|
||||
if vendor_choice == "Custom VID:PID":
|
||||
while True:
|
||||
v = prompt("Type VID:PID in hex form (0123:abcd):")
|
||||
if v is None:
|
||||
vidpid = None
|
||||
break
|
||||
if validate_vidpid(v):
|
||||
vidpid = v.lower()
|
||||
break
|
||||
print("Wrong format. It should be in hexadecimal format. An example is 0123:abcd.")
|
||||
elif vendor_choice in VENDORS:
|
||||
e = VENDORS[vendor_choice]
|
||||
vidpid = f"{e['vid']}:{e['pid']}"
|
||||
|
||||
presence = prompt_int("Presence Button Timeout (seconds, 0=disabled):", default=None)
|
||||
led_brightness = prompt_int("LED brightness (0=off):", default=None)
|
||||
|
||||
# Options
|
||||
print("Options: 留空保持当前")
|
||||
led_dimmable = prompt_bool("LED dimmable?", default=None)
|
||||
initialize = prompt_bool("Initialize device (will reset some state)?", default=None)
|
||||
secure_boot = prompt_bool("Enable Secure Boot? (WebUSB required)", default=None)
|
||||
secure_lock = prompt_bool("Enable Secure Lock? (WebUSB required)", default=None)
|
||||
power_cycle = prompt_bool("Power Cycle on Reset? (Pico FIDO only)", default=None)
|
||||
led_steady = prompt_bool("LED steady (always on)?", default=None)
|
||||
secp256k1 = prompt_bool("Enable secp256k1 curve? (Android may not support)", default=None)
|
||||
|
||||
led_gpio = prompt_int("LED GPIO pin (number):", default=None)
|
||||
|
||||
drivers = ["PICO", "PIMORONI", "WS2812", "CYW43", "NEOPIXEL", "NONE"]
|
||||
led_driver = choose("Select a LED driver:", drivers, None)
|
||||
|
||||
product_name = prompt("Product Name (max 14 chars):")
|
||||
if product_name is not None and len(product_name) > 14:
|
||||
print("Product name 超过 14 字符,将被截断。")
|
||||
product_name = product_name[:14]
|
||||
|
||||
cfg = {
|
||||
"vendor_choice": vendor_choice,
|
||||
"vidpid": vidpid,
|
||||
"presence_timeout": presence,
|
||||
"led_brightness": led_brightness,
|
||||
"options": {
|
||||
"led_dimmable": led_dimmable,
|
||||
"initialize": initialize,
|
||||
"secure_boot": secure_boot,
|
||||
"secure_lock": secure_lock,
|
||||
"power_cycle_on_reset": power_cycle,
|
||||
"led_steady": led_steady,
|
||||
"secp256k1": secp256k1,
|
||||
},
|
||||
"led_gpio": led_gpio,
|
||||
"led_driver": led_driver,
|
||||
"product_name": product_name,
|
||||
}
|
||||
|
||||
return cfg
|
||||
|
||||
|
||||
def build_phy_bytes(cfg: dict) -> bytes:
|
||||
# mirror getPhyData() TLV construction
|
||||
PHY_VID = 0x0
|
||||
PHY_LED_GPIO = 0x4
|
||||
PHY_LED_BTNESS = 0x5
|
||||
PHY_OPTS = 0x6
|
||||
PHY_OPT_WCID = 0x1
|
||||
PHY_OPT_DIMM = 0x2
|
||||
PHY_OPT_DISABLE_POWER_RESET = 0x4
|
||||
PHY_OPT_LED_STEADY = 0x8
|
||||
PHY_UP_BUTTON = 0x8
|
||||
PHY_USB_PRODUCT = 0x9
|
||||
PHY_ENABLED_CURVES = 0xA
|
||||
PHY_LED_DRIVER = 0xC
|
||||
|
||||
PHY_CURVE_SECP256K1 = 0x8
|
||||
|
||||
PHY_LED_DRIVER_SINGLE = 0x1
|
||||
PHY_LED_DRIVER_WS2812 = 0x3
|
||||
|
||||
b = bytearray()
|
||||
|
||||
# VID/PID
|
||||
vidpid = cfg.get("vidpid")
|
||||
if vidpid:
|
||||
try:
|
||||
vid_str, pid_str = vidpid.split(":")
|
||||
vid = int(vid_str, 16)
|
||||
pid = int(pid_str, 16)
|
||||
b += bytes([PHY_VID, 4, (vid >> 8) & 0xFF, vid & 0xFF, (pid >> 8) & 0xFF, pid & 0xFF])
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# LED GPIO
|
||||
lg = cfg.get("led_gpio")
|
||||
if lg is not None:
|
||||
b += bytes([PHY_LED_GPIO, 1, int(lg) & 0xFF])
|
||||
|
||||
# LED brightness
|
||||
lb = cfg.get("led_brightness")
|
||||
if lb is None:
|
||||
lb = 0
|
||||
b += bytes([PHY_LED_BTNESS, 1, int(lb) & 0xFF])
|
||||
|
||||
# opts
|
||||
opts = 0
|
||||
opts_map = cfg.get("options", {})
|
||||
if opts_map.get("led_dimmable"):
|
||||
opts |= PHY_OPT_DIMM
|
||||
if not opts_map.get("power_cycle_on_reset"):
|
||||
opts |= PHY_OPT_DISABLE_POWER_RESET
|
||||
if opts_map.get("led_steady"):
|
||||
opts |= PHY_OPT_LED_STEADY
|
||||
|
||||
b += bytes([PHY_OPTS, 2, (opts >> 8) & 0xFF, opts & 0xFF])
|
||||
|
||||
# Presence / Up button timeout
|
||||
btn = cfg.get("presence_timeout")
|
||||
if btn is None:
|
||||
btn = 0
|
||||
b += bytes([PHY_UP_BUTTON, 1, int(btn) & 0xFF])
|
||||
|
||||
# curves
|
||||
curves = 0
|
||||
if opts_map.get("secp256k1"):
|
||||
curves |= PHY_CURVE_SECP256K1
|
||||
b += bytes([PHY_ENABLED_CURVES, 4, (curves >> 24) & 0xFF, (curves >> 16) & 0xFF, (curves >> 8) & 0xFF, curves & 0xFF])
|
||||
|
||||
# USB product string
|
||||
pn = cfg.get("product_name")
|
||||
if pn:
|
||||
s = pn.encode("ascii", "ignore")
|
||||
b += bytes([PHY_USB_PRODUCT, len(s) + 1]) + s + b"\x00"
|
||||
|
||||
# led driver
|
||||
ld = cfg.get("led_driver")
|
||||
leddrv = 0
|
||||
if ld is not None:
|
||||
if ld.upper().startswith("WS"):
|
||||
leddrv = PHY_LED_DRIVER_WS2812
|
||||
elif ld.upper().startswith("PICO"):
|
||||
leddrv = PHY_LED_DRIVER_SINGLE
|
||||
elif ld.upper() == "NONE":
|
||||
leddrv = 0xFF
|
||||
b += bytes([PHY_LED_DRIVER, 1, leddrv & 0xFF])
|
||||
|
||||
return bytes(b)
|
||||
|
||||
|
||||
def save_config(cfg: dict, out: Path):
|
||||
out.write_text(json.dumps(cfg, ensure_ascii=False, indent=2))
|
||||
print(f"配置已保存到 {out}")
|
||||
|
||||
|
||||
def apply_stub(cfg: dict) -> None:
|
||||
print("\n-- APPLY (STUB) --")
|
||||
print("脚本将在此处尝试应用配置到本机已连接的 PicoKey(本地方式)。")
|
||||
print("配置摘要:")
|
||||
print(json.dumps(cfg, ensure_ascii=False, indent=2))
|
||||
confirm = input("确认要尝试直接应用配置到本机已连接设备吗?(y/N) ")
|
||||
if confirm.lower() != "y":
|
||||
print("取消应用。配置已保存到文件,可使用本地 picokey 库或其他工具手动应用。")
|
||||
return
|
||||
# 尝试使用本地 picokey 库把 PHY bytes 写入设备
|
||||
try:
|
||||
import picokey # type: ignore
|
||||
except Exception as e:
|
||||
print("未检测到可用的 picokey 库:", e)
|
||||
print("请确保已安装 pypicokey 并在正确的 Python 环境下运行此脚本。")
|
||||
return
|
||||
|
||||
def apply_local(cfg: dict) -> None:
|
||||
try:
|
||||
pk = picokey.PicoKey()
|
||||
except Exception as e:
|
||||
print("无法连接到 PicoKey 设备:", e)
|
||||
return
|
||||
try:
|
||||
phy = build_phy_bytes(cfg)
|
||||
if not phy:
|
||||
print("未生成任何 PHY 字节,跳过写入。")
|
||||
return
|
||||
print(f"准备写入 {len(phy)} 字节到设备...")
|
||||
try:
|
||||
# PicoKey.phy expects Optional[list[int]] for data
|
||||
pk.phy(list(phy))
|
||||
print("已成功写入 PHY 配置到设备。")
|
||||
except Exception as e:
|
||||
print("写入设备时发生错误:", e)
|
||||
finally:
|
||||
try:
|
||||
pk.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
apply_local(cfg)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="PicoKey commissioning configurator")
|
||||
parser.add_argument("--out", "-o", help="输出 JSON 文件路径", default=CFG_OUT)
|
||||
parser.add_argument("--apply", action="store_true", help="交互式确认后尝试应用配置到设备(stub)")
|
||||
parser.add_argument("--yes", "-y", action="store_true", help="自动确认所有提示(留空将被视为保留当前值)")
|
||||
args = parser.parse_args()
|
||||
|
||||
cfg = interactive_build()
|
||||
|
||||
outpath = Path(args.out)
|
||||
if outpath.exists():
|
||||
if not args.yes:
|
||||
overwrite = input(f"{outpath} 已存在,是否覆盖?(y/N) ")
|
||||
if overwrite.lower() != "y":
|
||||
print("取消,未保存文件。")
|
||||
return
|
||||
save_config(cfg, outpath)
|
||||
|
||||
if args.apply:
|
||||
apply_stub(cfg)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+233
-45
@@ -17,12 +17,19 @@
|
||||
*/
|
||||
"""
|
||||
|
||||
import sys
|
||||
from typing import Optional
|
||||
import threading
|
||||
from .APDU import APDUResponse
|
||||
from .SecureChannel import SecureChannel
|
||||
from .RescuePicoKey import RescuePicoKey
|
||||
from .RescueMonitor import RescueMonitor, RescueMonitorObserver
|
||||
from .PhyData import PhyData
|
||||
from .core import NamedIntEnum
|
||||
from .core.exceptions import PicoKeyNotFoundError, PicoKeyInvalidStateError
|
||||
import usb.core
|
||||
from .core.log import get_logger
|
||||
|
||||
logger = get_logger("PicoKey")
|
||||
|
||||
class Platform(NamedIntEnum):
|
||||
RP2040 = 0
|
||||
@@ -36,62 +43,215 @@ class Product(NamedIntEnum):
|
||||
FIDO = 2
|
||||
OPENPGP = 3
|
||||
|
||||
class ConnectionType(NamedIntEnum):
|
||||
UNKNOWN = 0
|
||||
SMARTCARD = 1
|
||||
RESCUE = 2
|
||||
|
||||
class ConnectTimeout(Exception):
|
||||
pass
|
||||
|
||||
def connect_with_timeout(connection, timeout=2.0):
|
||||
result = {}
|
||||
|
||||
def worker():
|
||||
try:
|
||||
connection.connect()
|
||||
result["ok"] = True
|
||||
except Exception as e:
|
||||
result["error"] = e
|
||||
|
||||
t = threading.Thread(target=worker)
|
||||
t.daemon = True
|
||||
t.start()
|
||||
t.join(timeout)
|
||||
|
||||
if t.is_alive():
|
||||
raise ConnectTimeout("connection.connect() timed out")
|
||||
|
||||
if "error" in result:
|
||||
raise result["error"]
|
||||
|
||||
return True
|
||||
|
||||
class PicoKey:
|
||||
|
||||
def __init__(self, slot=-1, force_rescue=False):
|
||||
logger.debug("Initializing PicoKey...")
|
||||
self.__apdu = []
|
||||
self.__connection_type = ConnectionType.UNKNOWN
|
||||
self.__monitor = None
|
||||
self.__observer = None
|
||||
self.__sc = None
|
||||
if (force_rescue):
|
||||
self.__card = None
|
||||
|
||||
if (not force_rescue):
|
||||
from smartcard.System import readers
|
||||
import smartcard.Exceptions
|
||||
from smartcard.CardMonitoring import CardMonitor, CardObserver
|
||||
logger.debug("Searching for smartcard readers...")
|
||||
class PicoCardObserver(CardObserver):
|
||||
def __init__(self, device):
|
||||
self.__device = device
|
||||
|
||||
def update(self, observable, actions):
|
||||
(added, removed) = actions
|
||||
if added:
|
||||
pass
|
||||
if removed:
|
||||
self.__device.close()
|
||||
|
||||
def reader_has_card(reader):
|
||||
try:
|
||||
connection = reader.createConnection()
|
||||
connect_with_timeout(connection, timeout=1.0)
|
||||
return connection
|
||||
except smartcard.Exceptions.NoCardException:
|
||||
return None
|
||||
return None
|
||||
|
||||
logger.debug("Checking available smartcard readers...")
|
||||
try:
|
||||
rdrs = readers()
|
||||
except Exception as e:
|
||||
logger.error("Error accessing smartcard readers: " + str(e))
|
||||
rdrs = []
|
||||
if len(rdrs) > 0:
|
||||
if (slot >= 0 and slot >= len(rdrs)):
|
||||
logger.error("Slot number out of range")
|
||||
raise Exception('Slot number out of range')
|
||||
|
||||
if (slot >= 0 and slot < len(rdrs)):
|
||||
logger.debug(f"Checking reader slot {slot}")
|
||||
reader = rdrs[slot]
|
||||
connection = reader_has_card(reader)
|
||||
if (connection is None):
|
||||
logger.error(f"No card in reader slot {slot}")
|
||||
raise Exception(f'No card in reader slot {slot}')
|
||||
self.__card = connection
|
||||
else:
|
||||
for i, reader in enumerate(rdrs):
|
||||
reader = rdrs[i]
|
||||
connection = reader_has_card(reader)
|
||||
if (connection is None):
|
||||
continue
|
||||
logger.debug(f"Card found in reader slot {i}")
|
||||
self.__card = connection
|
||||
self.__connection_type = ConnectionType.SMARTCARD
|
||||
|
||||
logger.debug("Setting up card monitor...")
|
||||
self.__monitor = CardMonitor()
|
||||
logger.debug("Creating card observer...")
|
||||
self.__observer = PicoCardObserver(self)
|
||||
logger.debug("Adding observer to monitor...")
|
||||
self.__monitor.addObserver(self.__observer)
|
||||
logger.debug("Observer added to monitor")
|
||||
break
|
||||
|
||||
if (self.__card is None):
|
||||
logger.debug("Attempting to connect in rescue mode...")
|
||||
class PicoRescueObserver(RescueMonitorObserver):
|
||||
def __init__(self, device):
|
||||
self.__device = device
|
||||
|
||||
def update(self, actions: tuple[Optional[usb.core.Device], Optional[usb.core.Device]]):
|
||||
(connected, disconnected) = actions
|
||||
if connected:
|
||||
logger.debug("Observer: Rescue device connected")
|
||||
pass
|
||||
if disconnected:
|
||||
logger.debug("Observer: Rescue device disconnected, closing...")
|
||||
self.__device.close()
|
||||
try:
|
||||
self.__card = RescuePicoKey()
|
||||
logger.debug("Rescue mode card initialized")
|
||||
self.__connection_type = ConnectionType.RESCUE
|
||||
logger.debug("Setting up rescue monitor...")
|
||||
self.__observer = PicoRescueObserver(self)
|
||||
logger.debug("Creating rescue monitor...")
|
||||
self.__monitor = RescueMonitor(device=self.__card, cls_callback=self.__observer)
|
||||
except Exception:
|
||||
raise Exception('time-out: no card inserted')
|
||||
else:
|
||||
from smartcard.CardType import AnyCardType
|
||||
from smartcard.CardRequest import CardRequest
|
||||
cardtype = AnyCardType()
|
||||
try:
|
||||
# request card insertion
|
||||
readers = None
|
||||
if (slot >= 0):
|
||||
readers = CardRequest().getReaders()
|
||||
if (slot >= len(readers)):
|
||||
raise Exception('slot out of range')
|
||||
readers = [readers[slot]]
|
||||
cardrequest = CardRequest(timeout=1, cardType=cardtype, readers=readers)
|
||||
self.__card = cardrequest.waitforcard().connection
|
||||
|
||||
# connect to the card and perform a few transmits
|
||||
self.__card.connect()
|
||||
|
||||
except Exception:
|
||||
try:
|
||||
self.__card = RescuePicoKey()
|
||||
except Exception:
|
||||
raise Exception('time-out: no card inserted')
|
||||
resp, sw1, sw2 = self.select_applet(rescue=True)
|
||||
logger.error("No PicoKey device detected")
|
||||
raise PicoKeyNotFoundError('time-out: no card inserted')
|
||||
try:
|
||||
resp, sw1, sw2 = self.select_applet(rescue=True)
|
||||
logger.debug("Selecting applet in rescue mode...")
|
||||
resp, sw1, sw2 = self.select_applet()
|
||||
logger.debug(f"Applet selected with response code: 0x{sw1:02X}{sw2:02X}")
|
||||
if (sw1 == 0x90 and sw2 == 0x00):
|
||||
self.platform = Platform(resp[0])
|
||||
self.product = Product(resp[1])
|
||||
self.version = (resp[2], resp[3])
|
||||
if (len(resp) >= 12):
|
||||
self.serial_number = int.from_bytes(resp[4:12], 'big')
|
||||
else:
|
||||
self.serial_number = 0
|
||||
logger.debug(f"Device platform: {self.platform.name}, product: {self.product.name}, version: {self.version}, serial number: {self.serial_number:016X}")
|
||||
else:
|
||||
logger.error("Unexpected response code during applet selection")
|
||||
self.platform = Platform(Platform.RP2040)
|
||||
self.product = Product(Product.UNKNOWN)
|
||||
self.version = (0, 0)
|
||||
self.serial_number = 0
|
||||
except APDUResponse:
|
||||
logger.error("APDU response error during applet selection")
|
||||
self.platform = Platform(Platform.RP2040)
|
||||
self.product = Product(Product.UNKNOWN)
|
||||
self.version = (0, 0)
|
||||
self.serial_number = 0
|
||||
|
||||
@property
|
||||
def device(self):
|
||||
return self.__card
|
||||
|
||||
def has_device(self):
|
||||
return self.__card is not None
|
||||
|
||||
@property
|
||||
def connection_type(self):
|
||||
return self.__connection_type
|
||||
|
||||
def close(self):
|
||||
logger.debug("Closing device...")
|
||||
if (not self.__card):
|
||||
logger.debug("No device to close")
|
||||
return
|
||||
if isinstance(self.__card, RescuePicoKey):
|
||||
logger.debug("Stopping rescue monitor...")
|
||||
self.__monitor.stop()
|
||||
self.__monitor = None
|
||||
self.__observer = None
|
||||
self.__card.close()
|
||||
else:
|
||||
self.__card.disconnect()
|
||||
self.__card.release()
|
||||
logger.debug("Removing card monitor observer...")
|
||||
if (self.__monitor and self.__observer):
|
||||
self.__monitor.deleteObserver(self.__observer)
|
||||
self.__observer = None
|
||||
self.__monitor = None
|
||||
logger.debug("Disconnecting and releasing card...")
|
||||
try:
|
||||
self.__card.disconnect()
|
||||
logger.debug("Card disconnected")
|
||||
self.__card.release()
|
||||
except Exception as e:
|
||||
logger.error("Error during card disconnect/release: " + str(e))
|
||||
self.__card = None
|
||||
|
||||
def transmit(self, apdu):
|
||||
response, sw1, sw2 = self.__card.transmit(apdu)
|
||||
return response, sw1, sw2
|
||||
def transmit(self, apdu: list[int]):
|
||||
if (not self.__card):
|
||||
logger.error("No device connected")
|
||||
raise PicoKeyNotFoundError('No device connected')
|
||||
try:
|
||||
response, sw1, sw2 = self.__card.transmit(apdu)
|
||||
return response, sw1, sw2
|
||||
except Exception as e:
|
||||
logger.error("Transmission error: " + str(e))
|
||||
raise PicoKeyInvalidStateError("Transmission error: " + str(e))
|
||||
|
||||
def send(self, command, cla=0x00, p1=0x00, p2=0x00, ne=None, data=None, codes=[]):
|
||||
def send(self, command: int, cla: int = 0x00, p1: int =0x00, p2: int=0x00, ne : Optional[int] = None, data : Optional[list[int]] = None, codes : list[int] = []):
|
||||
logger.debug(f"Sending command {hex(command)} with cla={hex(cla)}, p1={hex(p1)}, p2={hex(p2)}, ne={ne}")
|
||||
if (not self.__card):
|
||||
logger.error("No device connected")
|
||||
raise PicoKeyNotFoundError('No device connected')
|
||||
lc = []
|
||||
dataf = []
|
||||
if (data):
|
||||
@@ -110,14 +270,27 @@ class PicoKey:
|
||||
|
||||
apdu = apdu + [p1, p2] + lc + dataf + le
|
||||
self.__apdu = apdu
|
||||
logger.trace(f"APDU -> {' '.join([f'{x:02X}' for x in apdu])}")
|
||||
if (self.__sc):
|
||||
apdu = self.__sc.wrap_apdu(apdu)
|
||||
logger.trace(f"Wrapped APDU -> {' '.join([f'{x:02X}' for x in apdu])}")
|
||||
|
||||
try:
|
||||
response, sw1, sw2 = self.__card.transmit(apdu)
|
||||
except Exception:
|
||||
self.__card.reconnect()
|
||||
response, sw1, sw2 = self.__card.transmit(apdu)
|
||||
logger.debug("Reconnecting card after transmit failure")
|
||||
|
||||
try:
|
||||
self.__card.reconnect()
|
||||
except Exception as e:
|
||||
logger.error("Reconnection failed: " + str(e))
|
||||
self.close()
|
||||
raise PicoKeyNotFoundError('Reconnection failed: ' + str(e))
|
||||
try:
|
||||
response, sw1, sw2 = self.__card.transmit(apdu)
|
||||
except Exception as e:
|
||||
logger.error("APDU transmission error after reconnect: " + str(e))
|
||||
raise PicoKeyInvalidStateError("APDU transmission error after reconnect: " + str(e))
|
||||
|
||||
code = (sw1<<8|sw2)
|
||||
if (sw1 != 0x90):
|
||||
@@ -138,8 +311,10 @@ class PicoKey:
|
||||
code = (sw1<<8|sw2)
|
||||
if (code not in codes and code != 0x9000):
|
||||
raise APDUResponse(sw1, sw2)
|
||||
logger.trace(f"Response APDU <- {' '.join([f'{x:02X}' for x in response])}, SW1={sw1:02X}, SW2={sw2:02X}")
|
||||
if (self.__sc):
|
||||
response, code = self.__sc.unwrap_rapdu(response)
|
||||
logger.trace(f"Unwrapped RAPDU <- {' '.join([f'{x:02X}' for x in response])}, Code={code:04X}")
|
||||
if (code not in codes and code != 0x9000):
|
||||
raise APDUResponse(code >> 8, code & 0xff)
|
||||
return bytes(response), code
|
||||
@@ -152,36 +327,40 @@ class PicoKey:
|
||||
try:
|
||||
response, sw1, sw2 = self.__card.transmit(apdu)
|
||||
except Exception:
|
||||
logger.debug("Reconnecting card after transmit failure")
|
||||
self.__card.reconnect()
|
||||
response, sw1, sw2 = self.__card.transmit(apdu)
|
||||
|
||||
return bytes(response), sw1, sw2
|
||||
|
||||
def open_secure_channel(self, shared, nonce, token, pbkeyBytes):
|
||||
def open_secure_channel(self, shared: bytes, nonce: bytes, token: bytes, pbkeyBytes: bytes):
|
||||
logger.debug("Opening secure channel")
|
||||
sc = SecureChannel(shared=shared, nonce=nonce)
|
||||
res = sc.verify_token(token, pbkeyBytes)
|
||||
if (not res):
|
||||
raise Exception('Secure Channel token verification failed')
|
||||
self.__sc = sc
|
||||
|
||||
def select_applet(self, rescue=False):
|
||||
if (rescue):
|
||||
return self.transmit([0x00, 0xA4, 0x04, 0x04, 0x08, 0xA0, 0x58, 0x3F, 0xC1, 0x9B, 0x7E, 0x4F, 0x21, 0x00])
|
||||
return self.transmit([0x00, 0xA4, 0x04, 0x00, 0x0B, 0xE8, 0x2B, 0x06, 0x01, 0x04, 0x01, 0x81, 0xC3, 0x1F, 0x02, 0x01, 0x00])
|
||||
def select_applet(self):
|
||||
logger.debug("Selecting rescue applet")
|
||||
return self.transmit([0x00, 0xA4, 0x04, 0x04, 0x08, 0xA0, 0x58, 0x3F, 0xC1, 0x9B, 0x7E, 0x4F, 0x21, 0x00])
|
||||
|
||||
def phy(self, data=None):
|
||||
def phy(self, data : Optional[list[int]] = None):
|
||||
if (data is None):
|
||||
try:
|
||||
resp, sw = self.send(0x1E, cla=0x80, p1=0x00, ne=256)
|
||||
self.select_applet()
|
||||
resp, sw = self.send(0x1E, cla=0x80, p1=0x01, ne=256)
|
||||
return PhyData.parse(resp)
|
||||
except APDUResponse:
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
else:
|
||||
self.send(0x1C, cla=0x80, p1=0x01, data=data)
|
||||
|
||||
def flash_info(self):
|
||||
logger.debug("Retrieving flash info")
|
||||
try:
|
||||
self.select_applet()
|
||||
resp, sw = self.send(0x1E, cla=0x80, p1=0x02)
|
||||
free = int.from_bytes(resp[0:4], 'big')
|
||||
used = int.from_bytes(resp[4:8], 'big')
|
||||
@@ -199,6 +378,8 @@ class PicoKey:
|
||||
}
|
||||
|
||||
def secure_info(self):
|
||||
logger.debug("Retrieving secure boot info")
|
||||
self.select_applet()
|
||||
resp, sw = self.send(0x1E, cla=0x80, p1=0x03)
|
||||
return {
|
||||
'enabled': resp[0] != 0,
|
||||
@@ -206,6 +387,13 @@ class PicoKey:
|
||||
'boot_key': resp[2]
|
||||
}
|
||||
|
||||
def secure_boot(self, bootkey_index=0, lock=False):
|
||||
def secure_boot(self, bootkey_index: int = 0, lock: bool = False):
|
||||
logger.debug(f"Setting secure boot: bootkey_index={bootkey_index}, lock={lock}")
|
||||
self.select_applet()
|
||||
data = bytes([bootkey_index & 0xFF, 1 if lock else 0])
|
||||
self.send(0x1C, cla=0x80, p1=0x02, data=data)
|
||||
|
||||
def reboot(self, bootsel: bool = False):
|
||||
logger.debug("Rebooting device into BOOTSEL mode" if bootsel else "Rebooting device into normal mode")
|
||||
self.select_applet()
|
||||
self.send(0x1F, cla=0x80, p1=0x01 if bootsel else 0x00)
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
"""
|
||||
/*
|
||||
* This file is part of the pypicokey distribution (https://github.com/polhenarejos/pypicokey).
|
||||
* Copyright (c) 2025 Pol Henarejos.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
import usb.core
|
||||
import threading
|
||||
import time
|
||||
from .RescuePicoKey import RescuePicoKey
|
||||
|
||||
class RescueMonitorObserver:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def notifyObservers(self, actions: tuple[Optional[usb.core.Device], Optional[usb.core.Device]]):
|
||||
func = getattr(self, "update", None)
|
||||
if callable(func):
|
||||
func(actions)
|
||||
|
||||
def on_connect(self, device: Optional[usb.core.Device]):
|
||||
self.notifyObservers((device, None))
|
||||
|
||||
def on_disconnect(self, device: Optional[usb.core.Device]):
|
||||
self.notifyObservers((None, device))
|
||||
|
||||
class RescueMonitor:
|
||||
def __init__(self, device: RescuePicoKey, cls_callback: RescueMonitorObserver, interval=0.5):
|
||||
self._dev = device
|
||||
self._cls_callback = cls_callback
|
||||
self.interval = interval
|
||||
self._running = False
|
||||
self._device_present = False
|
||||
self._thread = None
|
||||
self.start()
|
||||
|
||||
def start(self):
|
||||
if self._running:
|
||||
return
|
||||
self._running = True
|
||||
self._thread = threading.Thread(target=self._run, daemon=True)
|
||||
self._thread.start()
|
||||
|
||||
def stop(self):
|
||||
self._running = False
|
||||
#if self._thread:
|
||||
# self._thread.join()
|
||||
|
||||
def _run(self):
|
||||
while self._running:
|
||||
if (self._dev is None) or (self._dev.device is None):
|
||||
time.sleep(self.interval)
|
||||
continue
|
||||
dev = usb.core.find(idVendor=self._dev.device.idVendor, idProduct=self._dev.device.idProduct)
|
||||
|
||||
if dev and not self._device_present:
|
||||
# Device connected
|
||||
self._device_present = True
|
||||
if self._cls_callback:
|
||||
self._cls_callback.on_connect(dev)
|
||||
|
||||
if not dev and self._device_present:
|
||||
# Device disconnected
|
||||
self._device_present = False
|
||||
if self._cls_callback:
|
||||
self._cls_callback.on_disconnect(self._dev.device)
|
||||
|
||||
time.sleep(self.interval)
|
||||
@@ -17,16 +17,34 @@
|
||||
*/
|
||||
"""
|
||||
|
||||
import os
|
||||
import time
|
||||
import usb.core
|
||||
import usb.util
|
||||
import libusb_package
|
||||
import usb.backend.libusb1
|
||||
from .ICCD import ICCD
|
||||
from .core.log import get_logger
|
||||
|
||||
logger = get_logger("RescuePicoKey")
|
||||
|
||||
class RescuePicoKeyError(Exception):
|
||||
pass
|
||||
|
||||
class RescuePicoKeyNotFoundError(RescuePicoKeyError):
|
||||
pass
|
||||
|
||||
class RescuePicoKeyInvalidStateError(RescuePicoKeyError):
|
||||
pass
|
||||
|
||||
class RescuePicoKey:
|
||||
|
||||
def __init__(self):
|
||||
logger.debug("Initializing RescuePicoKey...")
|
||||
self.__dev = None
|
||||
self.__in = None
|
||||
self.__out = None
|
||||
self.__int = None
|
||||
self.__active = None
|
||||
|
||||
class find_class(object):
|
||||
def __init__(self, class_):
|
||||
@@ -40,16 +58,20 @@ class RescuePicoKey:
|
||||
return True
|
||||
return False
|
||||
|
||||
logger.debug("Searching for USB device...")
|
||||
backend = usb.backend.libusb1.get_backend(find_library=libusb_package.find_library)
|
||||
try:
|
||||
devs = usb.core.find(find_all=True, custom_match=find_class(0x0B), backend=backend)
|
||||
except Exception as e:
|
||||
print("RescuePicoKey: exception during usb.core.find:", e)
|
||||
logger.error("Exception during usb.core.find: %s", e)
|
||||
devs = []
|
||||
found = False
|
||||
for dev in devs:
|
||||
if (dev.manufacturer == 'Pol Henarejos'):
|
||||
logger.debug("Found device")
|
||||
dev.set_configuration()
|
||||
logger.debug("Device configuration set")
|
||||
logger.debug("Getting active configuration...")
|
||||
cfg = dev.get_active_configuration()
|
||||
for intf in cfg:
|
||||
if (intf.bInterfaceClass == 0xFF):
|
||||
@@ -66,17 +88,37 @@ class RescuePicoKey:
|
||||
self.__in = epin.bEndpointAddress
|
||||
self.__out = epout[0].bEndpointAddress
|
||||
self.__int = epint.bEndpointAddress if epint else None
|
||||
logger.debug(f"Endpoints - IN: 0x{self.__in:02X}, OUT: 0x{self.__out:02X}, INT: {self.__int}")
|
||||
self.__iccd = ICCD(self)
|
||||
logger.debug("ICCD interface initialized")
|
||||
self.__active = None
|
||||
logger.debug("Powering off device")
|
||||
self.powerOff()
|
||||
logger.debug("Device powered off")
|
||||
found = True
|
||||
break
|
||||
if (not found):
|
||||
raise Exception('Not found any Pico Key device')
|
||||
logger.error("No suitable device found")
|
||||
raise RescuePicoKeyNotFoundError('Not found any Pico Key device')
|
||||
|
||||
@property
|
||||
def device(self):
|
||||
return self.__dev
|
||||
|
||||
@property
|
||||
def serial_number(self) -> str:
|
||||
return usb.util.get_string(self.__dev, self.__dev.iSerialNumber)
|
||||
|
||||
def close(self):
|
||||
logger.debug("Closing device")
|
||||
if self.__dev:
|
||||
logger.debug("Disposing USB resources")
|
||||
usb.util.dispose_resources(self.__dev)
|
||||
logger.debug("Device closed")
|
||||
self.__dev = None
|
||||
|
||||
def has_card(self):
|
||||
return self.__dev is not None
|
||||
|
||||
def __exit__(self, exc_type, exc, tb):
|
||||
self.close()
|
||||
@@ -88,31 +130,53 @@ class RescuePicoKey:
|
||||
return str(self.__dev)
|
||||
|
||||
def read(self, timeout=2000):
|
||||
ret = self.__dev.read(self.__in, 4096, timeout)
|
||||
return ret
|
||||
logger.debug("Reading data from device")
|
||||
try:
|
||||
ret = self.__dev.read(self.__in, 4096, timeout)
|
||||
logger.trace(f"Data read from device: {' '.join([f'{x:02X}' for x in ret])}")
|
||||
logger.debug("Read data from device")
|
||||
return ret
|
||||
except Exception as e:
|
||||
logger.error("USB read error: " + str(e))
|
||||
raise RescuePicoKeyInvalidStateError("USB read error: " + str(e))
|
||||
|
||||
def write(self, data, timeout=2000):
|
||||
assert(self.__dev.write(self.__out, data, timeout) == len(data))
|
||||
logger.debug("Writing data to device")
|
||||
logger.trace(f"Data to write to device: {' '.join([f'{x:02X}' for x in data])}")
|
||||
try:
|
||||
assert(self.__dev.write(self.__out, data, timeout) == len(data))
|
||||
logger.debug("Wrote data to device")
|
||||
except Exception as e:
|
||||
logger.error("USB write error: " + str(e))
|
||||
raise RescuePicoKeyInvalidStateError("USB write error: " + str(e))
|
||||
|
||||
def exchange(self, data, timeout=2000):
|
||||
logger.debug("Exchanging data with device")
|
||||
try:
|
||||
self.write(data=data, timeout=timeout)
|
||||
except Exception as e:
|
||||
raise Exception("USB write error: " + str(e))
|
||||
logger.error("USB write error: " + str(e))
|
||||
raise RescuePicoKeyInvalidStateError("USB write error: " + str(e))
|
||||
try:
|
||||
ret = self.read(timeout=timeout)
|
||||
except Exception as e:
|
||||
raise Exception("USB read error: " + str(e))
|
||||
logger.error("USB read error: " + str(e))
|
||||
raise RescuePicoKeyInvalidStateError("USB read error: " + str(e))
|
||||
return ret
|
||||
|
||||
def powerOn(self):
|
||||
logger.debug("Powering on device")
|
||||
if (not self.__active):
|
||||
self.__active = True
|
||||
logger.debug("Device powered on")
|
||||
return self.__iccd.IccPowerOn()
|
||||
|
||||
def powerOff(self):
|
||||
logger.debug("Powering off device")
|
||||
if (self.__active or self.__active is None):
|
||||
logger.debug("Device powered off")
|
||||
self.__iccd.IccPowerOff()
|
||||
logger.debug("ICCD powered off")
|
||||
self.__active = False
|
||||
|
||||
def transmit(self, apdu):
|
||||
@@ -120,3 +184,16 @@ class RescuePicoKey:
|
||||
self.powerOn()
|
||||
rapdu = self.__iccd.SendApdu(apdu=apdu)
|
||||
return rapdu[:-2], rapdu[-2], rapdu[-1]
|
||||
|
||||
def reconnect(self):
|
||||
logger.debug("Reconnecting to device")
|
||||
self.close()
|
||||
time.sleep(1)
|
||||
try:
|
||||
self.__init__()
|
||||
except Exception as e:
|
||||
logger.error("Reconnection failed: %s", e)
|
||||
self.close()
|
||||
raise e
|
||||
logger.debug("Reconnected to device")
|
||||
return self
|
||||
|
||||
@@ -6,3 +6,4 @@ from .SWCodes import SWCodes
|
||||
from .RescuePicoKey import RescuePicoKey
|
||||
from .PhyData import PhyData, PhyCurve, PhyUsbItf, PhyLedDriver, PhyOpt
|
||||
from .core import enums
|
||||
from .core.exceptions import PicoKeyError, PicoKeyNotFoundError, PicoKeyInvalidStateError
|
||||
|
||||
+1
-1
@@ -18,4 +18,4 @@
|
||||
*/
|
||||
"""
|
||||
|
||||
__version__ = "1.1.5"
|
||||
__version__ = "1.3.2"
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
from .enums import *
|
||||
from .exceptions import *
|
||||
|
||||
@@ -1,5 +1,44 @@
|
||||
"""
|
||||
/*
|
||||
* This file is part of the pypicokey distribution (https://github.com/polhenarejos/pypicokey).
|
||||
* Copyright (c) 2025 Pol Henarejos.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
"""
|
||||
|
||||
import enum
|
||||
from typing import Union
|
||||
|
||||
class NamedIntEnum(enum.IntEnum):
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def __format__(self, fmt):
|
||||
if any(c in fmt for c in "xXod"):
|
||||
return format(self.value, fmt)
|
||||
return self.name
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, value: Union[str, int]) -> "NamedIntEnum":
|
||||
if not value:
|
||||
return cls.UNKNOWN
|
||||
|
||||
value = value.strip().lower()
|
||||
|
||||
for member in cls:
|
||||
if member.value == value or member.name.lower() == value:
|
||||
return member
|
||||
|
||||
return cls.UNKNOWN
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
"""
|
||||
/*
|
||||
* This file is part of the pypicokey distribution (https://github.com/polhenarejos/pypicokey).
|
||||
* Copyright (c) 2025 Pol Henarejos.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
"""
|
||||
|
||||
class PicoKeyError(Exception):
|
||||
pass
|
||||
|
||||
class PicoKeyNotFoundError(PicoKeyError):
|
||||
pass
|
||||
|
||||
class PicoKeyInvalidStateError(PicoKeyError):
|
||||
pass
|
||||
@@ -0,0 +1,58 @@
|
||||
"""
|
||||
/*
|
||||
* This file is part of the pypicokey distribution (https://github.com/polhenarejos/pypicokey).
|
||||
* Copyright (c) 2025 Pol Henarejos.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
"""
|
||||
|
||||
import os
|
||||
import logging
|
||||
|
||||
TRACE_LEVEL = 5
|
||||
logging.addLevelName(TRACE_LEVEL, "TRACE")
|
||||
|
||||
def trace(self, message, *args, **kwargs):
|
||||
if self.isEnabledFor(TRACE_LEVEL):
|
||||
self._log(TRACE_LEVEL, message, args, **kwargs)
|
||||
|
||||
# Afegeix logger.trace()
|
||||
logging.Logger.trace = trace
|
||||
|
||||
def get_logger(name: str):
|
||||
env_level = os.getenv("PICOKEY_LOG", "CRITICAL").upper()
|
||||
|
||||
valid_levels = {
|
||||
"TRACE": TRACE_LEVEL,
|
||||
"DEBUG": logging.DEBUG,
|
||||
"INFO": logging.INFO,
|
||||
"WARNING": logging.WARNING,
|
||||
"ERROR": logging.ERROR,
|
||||
"CRITICAL": logging.CRITICAL,
|
||||
}
|
||||
|
||||
level = valid_levels.get(env_level, logging.CRITICAL)
|
||||
|
||||
logger = logging.getLogger(name)
|
||||
logger.setLevel(level)
|
||||
|
||||
if not logger.handlers:
|
||||
handler = logging.StreamHandler()
|
||||
handler.setFormatter(logging.Formatter(
|
||||
fmt="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
||||
datefmt="%Y-%m-%d %H:%M:%S"
|
||||
))
|
||||
logger.addHandler(handler)
|
||||
|
||||
return logger
|
||||
@@ -0,0 +1,51 @@
|
||||
[build-system]
|
||||
requires = ["setuptools"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "pypicokey"
|
||||
dynamic = ["version", "readme"]
|
||||
description = "PicoKey for Python"
|
||||
requires-python = ">=3.8"
|
||||
license = { file = "LICENSE" }
|
||||
|
||||
authors = [
|
||||
{ name = "Pol Henarejos", email = "pol.henarejos@cttc.es" }
|
||||
]
|
||||
|
||||
dependencies = [
|
||||
"setuptools",
|
||||
"cryptography>=3.3",
|
||||
"pyusb",
|
||||
"pycvc",
|
||||
"pyscard>=2.3.1",
|
||||
"libusb",
|
||||
"libusb_package",
|
||||
]
|
||||
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"Environment :: Plugins",
|
||||
"Intended Audience :: System Administrators",
|
||||
"License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Topic :: Security",
|
||||
"Topic :: System :: Installation/Setup",
|
||||
"Topic :: System :: Networking",
|
||||
"Topic :: System :: Systems Administration",
|
||||
"Topic :: Utilities",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://github.com/polhenarejos/pypicokey"
|
||||
|
||||
[tool.setuptools]
|
||||
packages = [
|
||||
"picokey",
|
||||
"picokey.core"
|
||||
]
|
||||
include-package-data = true
|
||||
|
||||
[tool.setuptools.dynamic]
|
||||
version = { attr = "picokey._version.__version__" }
|
||||
readme = { file = "README.md" }
|
||||
Reference in New Issue
Block a user