Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1658eb92b3 | |||
| 94ef650f80 | |||
| 74fe60f2d4 | |||
| e10b70df4e | |||
| d6d4489142 | |||
| ef80f8f271 | |||
| 1fe46149b9 | |||
| e7d92fba8a |
+141
-34
@@ -17,12 +17,15 @@
|
||||
*/
|
||||
"""
|
||||
|
||||
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
|
||||
import usb.core
|
||||
|
||||
class Platform(NamedIntEnum):
|
||||
RP2040 = 0
|
||||
@@ -36,38 +39,111 @@ 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):
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
rdrs = readers()
|
||||
if len(rdrs) > 0:
|
||||
if (slot >= 0 and slot >= len(rdrs)):
|
||||
raise Exception('Slot number out of range')
|
||||
|
||||
if (slot >= 0 and slot < len(rdrs)):
|
||||
reader = rdrs[slot]
|
||||
connection = reader_has_card(reader)
|
||||
if (connection is None):
|
||||
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
|
||||
self.__card = connection
|
||||
self.__connection_type = ConnectionType.SMARTCARD
|
||||
|
||||
self.__monitor = CardMonitor()
|
||||
self.__observer = PicoCardObserver(self)
|
||||
self.__monitor.addObserver(self.__observer)
|
||||
break
|
||||
|
||||
if (self.__card is None):
|
||||
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:
|
||||
pass
|
||||
if disconnected:
|
||||
self.__device.close()
|
||||
try:
|
||||
self.__card = RescuePicoKey()
|
||||
self.__connection_type = ConnectionType.RESCUE
|
||||
self.__observer = PicoRescueObserver(self)
|
||||
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)
|
||||
try:
|
||||
resp, sw1, sw2 = self.select_applet(rescue=True)
|
||||
if (sw1 == 0x90 and sw2 == 0x00):
|
||||
@@ -79,11 +155,39 @@ class PicoKey:
|
||||
self.product = Product(Product.UNKNOWN)
|
||||
self.version = (0, 0)
|
||||
|
||||
def transmit(self, apdu):
|
||||
@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):
|
||||
if (not self.__card):
|
||||
return
|
||||
if isinstance(self.__card, RescuePicoKey):
|
||||
self.__monitor.stop()
|
||||
self.__monitor = None
|
||||
self.__observer = None
|
||||
self.__card.close()
|
||||
else:
|
||||
self.__card.disconnect()
|
||||
self.__card.release()
|
||||
self.__card = None
|
||||
|
||||
def transmit(self, apdu: list[int]):
|
||||
if (not self.__card):
|
||||
raise Exception('No device connected')
|
||||
response, sw1, sw2 = self.__card.transmit(apdu)
|
||||
return response, sw1, sw2
|
||||
|
||||
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] = []):
|
||||
if (not self.__card):
|
||||
raise Exception('No device connected')
|
||||
lc = []
|
||||
dataf = []
|
||||
if (data):
|
||||
@@ -109,7 +213,10 @@ class PicoKey:
|
||||
response, sw1, sw2 = self.__card.transmit(apdu)
|
||||
except Exception:
|
||||
self.__card.reconnect()
|
||||
response, sw1, sw2 = self.__card.transmit(apdu)
|
||||
try:
|
||||
response, sw1, sw2 = self.__card.transmit(apdu)
|
||||
except Exception as e:
|
||||
raise Exception("APDU transmission error after reconnect: " + str(e))
|
||||
|
||||
code = (sw1<<8|sw2)
|
||||
if (sw1 != 0x90):
|
||||
@@ -149,24 +256,24 @@ class PicoKey:
|
||||
|
||||
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):
|
||||
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):
|
||||
def select_applet(self, rescue : bool = 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 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)
|
||||
return PhyData.parse(resp)
|
||||
except APDUResponse:
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
else:
|
||||
@@ -198,6 +305,6 @@ 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):
|
||||
data = bytes([bootkey_index & 0xFF, 1 if lock else 0])
|
||||
self.send(0x1C, cla=0x80, p1=0x02, data=data)
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
"""
|
||||
/*
|
||||
* 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:
|
||||
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)
|
||||
@@ -27,6 +27,11 @@ from .ICCD import ICCD
|
||||
class RescuePicoKey:
|
||||
|
||||
def __init__(self):
|
||||
self.__dev = None
|
||||
self.__in = None
|
||||
self.__out = None
|
||||
self.__int = None
|
||||
self.__active = None
|
||||
|
||||
class find_class(object):
|
||||
def __init__(self, class_):
|
||||
@@ -74,6 +79,24 @@ class RescuePicoKey:
|
||||
if (not found):
|
||||
raise Exception('Not found any Pico Key device')
|
||||
|
||||
@property
|
||||
def device(self):
|
||||
return self.__dev
|
||||
|
||||
def close(self):
|
||||
if self.__dev:
|
||||
usb.util.dispose_resources(self.__dev)
|
||||
self.__dev = None
|
||||
|
||||
def has_card(self):
|
||||
return self.__dev is not None
|
||||
|
||||
def __exit__(self, exc_type, exc, tb):
|
||||
self.close()
|
||||
|
||||
def __del__(self):
|
||||
self.close()
|
||||
|
||||
def __str__(self):
|
||||
return str(self.__dev)
|
||||
|
||||
|
||||
+1
-1
@@ -18,4 +18,4 @@
|
||||
*/
|
||||
"""
|
||||
|
||||
__version__ = "1.1.4"
|
||||
__version__ = "1.1.7"
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
[build-system]
|
||||
requires = ["setuptools>=61.0"]
|
||||
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