Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
247 changes: 247 additions & 0 deletions src/seedsigner/helpers/globalplatform_native.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
import importlib.util
import shlex
from dataclasses import dataclass
from pathlib import Path


class GlobalPlatformNativeError(Exception):
pass


@dataclass
class GlobalPlatformCommand:
list_verbose: bool = False
install_path: str | None = None
create_aid: str | None = None
params: str | None = None
delete_aid: str | None = None
force_delete: bool = False
key_hex: str | None = None
lock_keys: tuple[str, str, str] | None = None
unlock: bool = False


class GlobalPlatformNativeRunner:
"""Executes the subset of GlobalPlatformPro CLI operations used by SeedSigner."""

def __init__(self):
self.backend = _PyGlobalPlatformBackend()

def run(self, command: str) -> str:
parsed = self.parse_command(command)

if parsed.list_verbose:
rows = self.backend.list_packages(verbose=True)
return "\n".join([f"PKG: {aid} (|{label}|)" for aid, label in rows])

if parsed.install_path:
self.backend.install_cap(
cap_path=parsed.install_path,
create_aid=parsed.create_aid,
install_params=parsed.params,
key_hex=parsed.key_hex,
)
return "OK"

if parsed.delete_aid:
self.backend.delete_aid(parsed.delete_aid, force=parsed.force_delete, key_hex=parsed.key_hex)
return "OK"

if parsed.unlock:
if not parsed.key_hex:
raise GlobalPlatformNativeError("--unlock requires --key")
self.backend.unlock_card(parsed.key_hex)
return "OK"

if parsed.lock_keys:
self.backend.lock_card(*parsed.lock_keys)
return "OK"

raise GlobalPlatformNativeError(f"Unsupported GlobalPlatform command: {command}")

@staticmethod
def parse_command(command: str) -> GlobalPlatformCommand:
args = shlex.split(command)
parsed = GlobalPlatformCommand()

i = 0
while i < len(args):
token = args[i]
if token == "-l":
parsed.list_verbose = True
elif token == "-v":
parsed.list_verbose = True
elif token == "--install":
i += 1
parsed.install_path = args[i]
elif token == "--create":
i += 1
parsed.create_aid = _clean_hex(args[i])
elif token == "--params":
i += 1
parsed.params = _clean_hex(args[i])
elif token == "--delete":
i += 1
parsed.delete_aid = _clean_hex(args[i])
elif token == "-force":
parsed.force_delete = True
elif token == "--key":
i += 1
key = args[i]
if key.lower() != "default":
parsed.key_hex = _clean_hex(key)
elif token == "--unlock":
parsed.unlock = True
elif token == "--lock":
if i + 3 >= len(args):
raise GlobalPlatformNativeError("--lock requires ENC MAC DEK keys")
parsed.lock_keys = (
_clean_hex(args[i + 1]),
_clean_hex(args[i + 2]),
_clean_hex(args[i + 3]),
)
i += 3
i += 1

return parsed


class _PyGlobalPlatformBackend:
def __init__(self):
gp_module = "pyGlobalPlatform.globalplatformlib"
if importlib.util.find_spec(gp_module) is None:
raise GlobalPlatformNativeError(
"pyGlobalPlatform is not installed. Install pyGlobalPlatform/libglobalplatform to use native GP operations."
)
from pyGlobalPlatform import globalplatformlib as gp

self.gp = gp

def _open(self):
context = self.gp.establishContext()
readers = self.gp.listReaders(context)
if not readers:
self.gp.releaseContext(context)
raise GlobalPlatformNativeError("SCARD_E_NO_SMARTCARD")
card = self.gp.connectCard(context, readers[0], self.gp.SCARD_PROTOCOL_Tx)
return context, card

def _authenticate(self, context, card, key_hex: str | None = None):
key = bytes.fromhex(key_hex) if key_hex else self.gp.DEFAULT_KEY.encode("latin1")
return self.gp.mutualAuthentication(
context,
card,
key,
key,
key,
key,
0x00,
0x00,
2,
0x15,
0x03,
0x00,
)

def _close(self, context, card):
self.gp.disconnectCard(context, card)
self.gp.releaseContext(context)

def list_packages(self, verbose: bool = True):
context, card = self._open()
try:
sec = self._authenticate(context, card)
data = self.gp.getStatus(context, card, sec, 0x10)
rows = []
for entry in data or []:
aid = ""
if isinstance(entry, dict):
aid = entry.get("AID", "") or entry.get("aid", "")
elif isinstance(entry, (tuple, list)) and entry:
aid = entry[0]
else:
aid = str(entry)
aid = aid.hex().upper() if isinstance(aid, (bytes, bytearray)) else str(aid).upper()
rows.append((aid, aid))
return rows
finally:
self._close(context, card)

def install_cap(self, cap_path: str, create_aid: str | None, install_params: str | None, key_hex: str | None):
cap = Path(cap_path)
if not cap.exists():
raise GlobalPlatformNativeError(f"CAP file not found: {cap}")

context, card = self._open()
try:
sec = self._authenticate(context, card, key_hex=key_hex)
# libglobalplatform handles CAP parsing internally.
self.gp.load(context, card, sec, None, str(cap))
if create_aid:
aid = bytes.fromhex(create_aid)
self.gp.installForInstallAndMakeSelectable(
context,
card,
sec,
aid,
aid,
aid,
0,
0,
0,
b"",
None,
)
elif install_params:
# install params are TLV C9 in GPPro; pass raw params bytes.
self.gp.installForInstallAndMakeSelectable(
context,
card,
sec,
None,
None,
None,
0,
0,
0,
bytes.fromhex(install_params),
None,
)
finally:
self._close(context, card)

def delete_aid(self, aid_hex: str, force: bool, key_hex: str | None = None):
context, card = self._open()
try:
sec = self._authenticate(context, card, key_hex=key_hex)
self.gp.deleteApplication(context, card, sec, [bytes.fromhex(aid_hex)])
finally:
self._close(context, card)

def unlock_card(self, current_key_hex: str):
self._set_sc_keys(current_key_hex, ("404142434445464748494A4B4C4D4E4F",) * 3)

def lock_card(self, enc_hex: str, mac_hex: str, dek_hex: str):
self._set_sc_keys("404142434445464748494A4B4C4D4E4F", (enc_hex, mac_hex, dek_hex))

def _set_sc_keys(self, old_key_hex: str, new_keys_hex: tuple[str, str, str]):
context, card = self._open()
try:
sec = self._authenticate(context, card, key_hex=old_key_hex)
self.gp.putSCKey(
context,
card,
sec,
0x00,
0x01,
bytes.fromhex(new_keys_hex[0]),
bytes.fromhex(new_keys_hex[0]),
bytes.fromhex(new_keys_hex[1]),
bytes.fromhex(new_keys_hex[2]),
)
finally:
self._close(context, card)


def _clean_hex(value: str) -> str:
return "".join(ch for ch in value if ch.isalnum()).upper()
74 changes: 22 additions & 52 deletions src/seedsigner/helpers/seedkeeper_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -429,28 +429,18 @@ def init_satochip(parentObject, init_card_filter=None, require_pin=True):
def run_globalplatform(
parentObject, command, loadingText="Loading", successtext="Success"
):
from subprocess import run
from seedsigner.models.settings import (
Settings,
SettingsConstants,
SettingsDefinition,
)
from seedsigner.helpers.globalplatform_native import GlobalPlatformNativeRunner
from seedsigner.models.settings import SettingsConstants

parentObject.loading_screen = LoadingScreenThread(text=loadingText)
parentObject.loading_screen.start()

hostname = platform.uname()[1]
if hostname == "seedsigner-os":
commandString = (
"/mnt/diy/jdk/bin/java -jar /mnt/diy/Satochip-DIY/gp.jar " + command
)
elif os.path.exists("/home/pi/Satochip-DIY/gp.jar"):
commandString = "java -jar /home/pi/Satochip-DIY/gp.jar " + command
else:
# Assume gp.jar is available in the current working directory
commandString = "java -jar gp.jar " + command

data = run(commandString, capture_output=True, shell=True, text=True)
data_stdout = ""
errors_cleaned = ""
try:
data_stdout = GlobalPlatformNativeRunner().run(command)
except Exception as exc:
errors_cleaned = str(exc)

# This process often kills IFD-NFC, so restart it if required
scinterface = parentObject.settings.get_value(
Expand All @@ -463,28 +453,6 @@ def run_globalplatform(

parentObject.loading_screen.stop()

print("StdOut:", data.stdout)
print("StdErr:", data.stderr)

# data.stderr = data.stderr.replace("Warning: no keys given, defaulting to 404142434445464748494A4B4C4D4E4F", "")

data.stderr = data.stderr.split("\n")

errors_cleaned = []
for errorLine in data.stderr:
if "[INFO]" in errorLine:
continue
elif "404142434445464748494A4B4C4D4E4F" in errorLine:
continue
elif len(errorLine) < 1:
continue

errors_cleaned.append(errorLine)

print("StdErr (Cleaned):", errors_cleaned)

errors_cleaned = " ".join(errors_cleaned)

if len(errors_cleaned) > 1:
uninstall_required = False

Expand All @@ -493,7 +461,7 @@ def run_globalplatform(
if "is not present on card" in errors_cleaned:
failureText = "Applet is not on the card, nothing to uninstall."

elif "Multiple readers, must choose one" in errors_cleaned:
elif "Multiple readers" in errors_cleaned:
failureText = "Multiple readers connected, please run with a single reader connected/activated."

elif "Card cryptogram invalid" in errors_cleaned:
Expand Down Expand Up @@ -528,6 +496,9 @@ def run_globalplatform(
):
failureText = "Unable to complete secure connection... (App or reader may need restart)"

elif "pyGlobalPlatform is not installed" in errors_cleaned:
failureText = "Install pyGlobalPlatform/libglobalplatform to use native GP commands."

logger.error(failureText)
parentObject.run_screen(
WarningScreen,
Expand Down Expand Up @@ -557,15 +528,14 @@ def run_globalplatform(
)
return None

else:
if successtext:
print(successtext)
parentObject.run_screen(
LargeIconStatusScreen,
title="Success",
status_headline=None,
text=successtext,
show_back_button=False,
)
if successtext:
print(successtext)
parentObject.run_screen(
LargeIconStatusScreen,
title="Success",
status_headline=None,
text=successtext,
show_back_button=False,
)

return data.stdout
return data_stdout
Loading