8 Commits

Author SHA1 Message Date
Pol Henarejos 1658eb92b3 Version 1.1.7
Signed-off-by: Pol Henarejos <pol.henarejos@cttc.es>
2025-11-26 00:05:45 +01:00
Pol Henarejos 94ef650f80 Fix monitor notifier.
Signed-off-by: Pol Henarejos <pol.henarejos@cttc.es>
2025-11-26 00:05:24 +01:00
Pol Henarejos 74fe60f2d4 Version 1.1.6
Signed-off-by: Pol Henarejos <pol.henarejos@cttc.es>
2025-11-24 17:13:29 +01:00
Pol Henarejos e10b70df4e Add pyproject.toml
Signed-off-by: Pol Henarejos <pol.henarejos@cttc.es>
2025-11-24 17:09:39 +01:00
Pol Henarejos d6d4489142 Add Observer to close the connection if needed.
Signed-off-by: Pol Henarejos <pol.henarejos@cttc.es>
2025-11-24 17:05:21 +01:00
Pol Henarejos ef80f8f271 Add methods.
Signed-off-by: Pol Henarejos <pol.henarejos@cttc.es>
2025-11-24 17:04:08 +01:00
Pol Henarejos 1fe46149b9 Version 1.1.5
Signed-off-by: Pol Henarejos <pol.henarejos@cttc.es>
2025-11-23 20:44:32 +01:00
Pol Henarejos e7d92fba8a Add close() method.
Signed-off-by: Pol Henarejos <pol.henarejos@cttc.es>
2025-11-23 20:44:12 +01:00
5 changed files with 295 additions and 35 deletions
+141 -34
View File
@@ -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)
+79
View File
@@ -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)
+23
View File
@@ -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
View File
@@ -18,4 +18,4 @@
*/
"""
__version__ = "1.1.4"
__version__ = "1.1.7"
+51
View File
@@ -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" }