Skip to content

Deferred: GUI uninstall for MemoryCard/SeedKeeper applets #29

@Amperstrand

Description

@Amperstrand

Summary

Uninstall (delete) functionality for MemoryCard and SeedKeeper applets was removed from the GUI in the initial provisioning implementation. Users must use gp.jar (GlobalPlatformPro) to uninstall applets.

Why

  • Risk: Uninstalling an applet also wipes any stored secrets on the card. This is destructive and irreversible.
  • Complexity: Proper uninstall requires cascading delete (applet + package), session management, and error handling that adds significant surface area.
  • Simplicity: For the initial proof-of-concept, install-only keeps the scope minimal and well-tested.

How to uninstall manually

# Install GlobalPlatformPro v25.10.20+
# https://github.com/martinpaljak/GlobalPlatformPro/releases

# Delete MemoryCard applet + package
java -jar gp.jar --delete B00B5111CB01
java -jar gp.jar --delete B00B5111CB --op201

# Delete SeedKeeper applet + package
java -jar gp.jar --delete 536565644b656570657200
java -jar gp.jar --delete 536565644b6565706572 --op201

Code for re-addition

The following methods were removed from src/specter.py and should be restored when this feature is re-implemented:

_delete_memorycard(self, silent=False)

async def _delete_memorycard(self, silent=False):
    """Delete MemoryCard applet from card."""
    if not silent:
        if not await self.gui.prompt(
            "Delete MemoryCard?",
            "This will remove the MemoryCard applet\n"
            "and all its data from the card.\n\n"
            "Are you sure?",
            warning="This action cannot be undone!",
        ):
            return

    from gui.screens.provisioning import ProvisioningProgressScreen
    from keystore.javacard.util import get_connection
    from keystore.javacard.gp.profiles import JCOP4_PROFILE
    from keystore.javacard.gp.scp02 import open_session
    from keystore.javacard.gp.deleter import delete_aid

    scr = ProvisioningProgressScreen("Delete MemoryCard")
    await self.gui.load_screen(scr)

    try:
        conn = None
        session = None
        scr.set_step("Connecting to card...")
        conn = get_connection()
        _safe_connect(conn)

        scr.set_step("Authenticating...")
        session = open_session(conn, JCOP4_PROFILE)

        scr.set_step("Deleting applet...")
        applet_aid = unhexlify("B00B5111CB01")
        delete_aid(session, applet_aid)

        scr.set_step("Deleting package...")
        package_aid = unhexlify("B00B5111CB")
        try:
            delete_aid(session, package_aid)
        except Exception:
            pass

        scr.set_step("Ending session...")
        if session is not None:
            try:
                session.end_session()
            except Exception:
                pass
        try:
            conn.disconnect()
        except Exception:
            pass

        if silent:
            return
        scr.set_done()
        await scr.result()
    except Exception as e:
        if session is not None:
            try:
                session.end_session()
            except Exception:
                pass
        if conn is not None:
            try:
                conn.disconnect()
            except Exception:
                pass
        if silent:
            raise
        scr.set_error("Delete failed:\n%s" % str(e))
        await scr.result()

_delete_seedkeeper(self, silent=False)

async def _delete_seedkeeper(self, silent=False):
    """Delete SeedKeeper applet from card."""
    if not silent:
        if not await self.gui.prompt(
            "Delete SeedKeeper?",
            "This will remove the SeedKeeper applet\n"
            "and all its secrets from the card.\n\n"
            "Are you sure?",
            warning="This action cannot be undone!",
        ):
            return

    from gui.screens.provisioning import ProvisioningProgressScreen
    from keystore.javacard.util import get_connection
    from keystore.javacard.gp.profiles import JCOP4_PROFILE
    from keystore.javacard.gp.scp02 import open_session
    from keystore.javacard.gp.deleter import delete_aid
    from binascii import unhexlify

    scr = ProvisioningProgressScreen("Delete SeedKeeper")
    await self.gui.load_screen(scr)

    try:
        conn = None
        session = None
        scr.set_step("Connecting to card...")
        conn = get_connection()
        _safe_connect(conn)

        scr.set_step("Authenticating...")
        session = open_session(conn, JCOP4_PROFILE)

        scr.set_step("Deleting applet...")
        sk_inst = unhexlify("536565644b656570657200")
        delete_aid(session, sk_inst)

        scr.set_step("Deleting package...")
        sk_pkg = unhexlify("536565644b6565706572")
        try:
            delete_aid(session, sk_pkg)
        except Exception:
            pass

        scr.set_step("Ending session...")
        if session is not None:
            try:
                session.end_session()
            except Exception:
                pass
        try:
            conn.disconnect()
        except Exception:
            pass

        if silent:
            return
        scr.set_done()
        await scr.result()
    except Exception as e:
        if session is not None:
            try:
                session.end_session()
            except Exception:
                pass
        if conn is not None:
            try:
                conn.disconnect()
            except Exception:
                pass
        if silent:
            raise
        scr.set_error("Delete failed:\n%s" % str(e))
        await scr.result()

_install_seedkeeper(self)

async def _install_seedkeeper(self):
    """Install SeedKeeper applet from DGP file on filesystem."""
    from keystore.javacard.gp.profiles import APPLET_AIDS

    sk_info = APPLET_AIDS.get("seedkeeper")
    dgp_path = sk_info["dgp_file"] if sk_info else "/flash/gp/SeedKeeper.dgp"

    try:
        f = open(dgp_path, "rb")
        f.close()
    except Exception:
        await self.gui.alert(
            "SeedKeeper.dgp not found",
            "Copy the DGP file to the device:\n\n"
            "mpremote cp SeedKeeper.dgp :%s" % dgp_path
        )
        return

    from keystore.javacard.util import get_connection
    from keystore.javacard.card_scanner import scan_card_applets

    conn = get_connection()
    scan = scan_card_applets(conn)
    already_installed = "SeedKeeper" in scan.get("applets", [])

    if already_installed:
        if not await self.gui.prompt(
            "SeedKeeper already installed",
            "SeedKeeper is already on this card.\n\n"
            "Reinstalling will delete existing secrets.\n\n"
            "Continue?",
            warning="All stored secrets will be lost!",
        ):
            return
        await self._delete_seedkeeper(silent=True)

    if not await self.gui.prompt(
        "Install SeedKeeper?",
        "This will install the SeedKeeper applet\n"
        "on the JavaCard.\n\n"
        "The card will be modified.\n\n"
        "Continue?",
    ):
        return

    from gui.screens.provisioning import ProvisioningProgressScreen
    from keystore.javacard.gp.profiles import JCOP4_PROFILE
    from keystore.javacard.gp.scp02 import open_session
    from keystore.javacard.gp.loader import install_from_dgp, verify_install
    from binascii import unhexlify

    scr = ProvisioningProgressScreen("Install SeedKeeper")
    await self.gui.load_screen(scr)

    try:
        scr.set_step("Loading DGP file...")
        f = open(dgp_path, "rb")
        dgp_data = f.read()
        f.close()

        scr.set_step("Connecting to card...")
        conn = get_connection()
        _safe_connect(conn)

        scr.set_step("Authenticating...")
        session = open_session(conn, JCOP4_PROFILE)

        scr.set_step("Installing (%d bytes)..." % len(dgp_data))
        sd_aid = unhexlify("A000000151000000")
        pkg_aid = install_from_dgp(session, dgp_data, sd_aid)

        scr.set_step("Verifying...")
        sk_inst = unhexlify("536565644b656570657200")
        if verify_install(session, sk_inst):
            scr.set_done()
        else:
            scr.set_error("Verification failed")

        try:
            conn.disconnect()
        except Exception:
            pass
        await scr.result()
    except Exception as e:
        try:
            conn.disconnect()
        except Exception:
            pass
        scr.set_error("Install failed:\n%s" % str(e))
        await scr.result()

Requirements for re-addition

  1. Add warning about data loss in the confirmation prompt
  2. Show which applets/secrets will be deleted before confirmation
  3. Add "Use a different card" flow after delete (card swap)
  4. Consider adding a "factory reset" option that deletes ALL applets
  5. Test delete → reinstall → verify cycle thoroughly
  6. Test delete when card has stored mnemonic (confirm data is gone)

Metadata

Metadata

Assignees

No one assigned

    Labels

    feature creepIncreases merge complexity; scope should be reduced or deferredpriority: lowNice to have, not blockingseedkeeperSeedKeeper keystore support

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions