Problem
When importing or deleting secrets rapidly on the SeedKeeper card, the card's EEPROM becomes corrupted with orphaned secret entries. After ~25+ duplicate imports in a single session, all subsequent operations return 0x9C06 (SW_UNAUTHORIZED) — even after correct PIN verification and secure channel initialization.
Error code: 0x9C06 (SW_UNAUTHORIZED)
From the SeedKeeper applet source (SeedKeeper.java):
/** Required operation was not authorized because of a lack of privileges */
private final static short SW_UNAUTHORIZED = (short) 0x9C06;
From pysatochip's CardConnector.py, 0x9C06 triggers automatic PIN re-verification:
# PIN authentication is required
if (sw1==0x9C) and (sw2==0x06):
(response, sw1, sw2)= self.card_verify_PIN_simple()
However, in our case, PIN re-verification doesn't help — the card's EEPROM object manager is corrupted.
Root cause
The ObjectManager on the SeedKeeper card allocates fixed-size EEPROM slots for each secret. When secrets are deleted, the slot may not be properly freed (or the free list becomes corrupted). After many rapid import/delete cycles:
- Card EEPROM fills up with orphaned secret entries
INS_LIST_SECRET_HEADERS (0xA6) fails with 0x9C06
INS_DELETE_SECRET (0xA5) also fails with 0x9C06
- Even
INS_IMPORT_SECRET (0xA1) may fail
- The only recovery is physical card removal/reinsertion (full power cycle clears volatile state but NOT EEPROM corruption)
Reproduction
- Import the same secret 25+ times via
TEST_IMPORT_SECRET:<hex>:abandon
- Try to delete all secrets via
TEST_DELETE_SECRET:<id> for each
- After ~25 deletes, all subsequent operations return
0x9C06
TEST_CARD_RESET (GPIO power cycle) does NOT fix it
- Only physical card removal and reinsertion recovers the card
SeedKeeper error code reference
From the applet source, the relevant SW codes are:
| Code |
Constant |
Meaning |
0x63cX |
SW_PIN_FAILED |
Wrong PIN, X attempts remaining |
0x9c01 |
SW_NO_MEMORY_LEFT |
EEPROM full |
0x9c03 |
SW_OPERATION_NOT_ALLOWED |
Operation not allowed |
0x9c04 |
SW_SETUP_NOT_DONE |
Setup not complete |
0x9c05 |
SW_UNSUPPORTED_FEATURE |
Feature not supported |
0x9c06 |
SW_UNAUTHORIZED |
PIN/auth required |
0x9c08 |
SW_OBJECT_NOT_FOUND |
Object missing |
0x9c12 |
SW_SEQUENCE_END |
No more data |
0x9c20 |
SW_SECURE_CHANNEL_REQUIRED |
Secure channel needed |
0x9c21 |
SW_SECURE_CHANNEL_UNINITIALIZED |
Secure channel not init |
0x9c23 |
SW_SECURE_CHANNEL_WRONG_MAC |
Bad MAC in secure channel |
0x9c30 |
SW_LOCK_ERROR |
Lock mechanism error |
0x9c31 |
SW_EXPORT_NOT_ALLOWED |
Export not allowed |
0x9c32 |
SW_IMPORTED_DATA_TOO_LONG |
Secret too long |
0x9c36 |
SW_USAGE_NOT_ALLOWED |
Usage not allowed |
0x9c38 |
SW_WRONG_SECRET_TYPE |
Wrong secret type |
Fixes needed
1. Rate-limit HIL test imports (firmware)
In _load_seedkeeper(), the retry loop imported the abandon secret multiple times before detecting the "No secrets" screen. Add a guard:
# Only import if TEST_SECRETS shows empty
resp = self.gui.command("TEST_SECRETS", timeout=2)
if b"OK:SECRETS:" in resp and resp.split(b"OK:SECRETS:")[1].strip():
# Secrets exist, don't import
pass
else:
self.card_import_bip39(...)
2. Add 0x9C06 handling to secure_request
In satochip_securechannel.py, detect 0x9C06 and provide a meaningful error message:
if sw == 0x9C06:
raise ISOException("9c06", "Operation not authorized - card may need physical reset")
3. Add TEST_WIPE_SECRETS HIL command
A dedicated command to delete all BIP39 secrets in a single operation, with proper error handling for EEPROM full state:
TEST_WIPE_SECRETS → deletes all secrets, returns OK:WIPED_SECRETS or ERR:EEPROM_FULL
4. Consider batch delete in firmware
Instead of individual INS_DELETE_SECRET calls, implement a single "delete all BIP39" flow that minimizes EEPROM writes.
Workaround
Physical card removal and reinsertion is the only reliable recovery. GPIO power cycling (TEST_CARD_RESET) does NOT clear EEPROM corruption.
Environment
- SeedKeeper card: protocol v0.2, applet v0.1
- Tested on STM32F469 Discovery board with HIL test framework
st-flash v1.8.0, openocd v0.12.0
Problem
When importing or deleting secrets rapidly on the SeedKeeper card, the card's EEPROM becomes corrupted with orphaned secret entries. After ~25+ duplicate imports in a single session, all subsequent operations return
0x9C06(SW_UNAUTHORIZED) — even after correct PIN verification and secure channel initialization.Error code: 0x9C06 (SW_UNAUTHORIZED)
From the SeedKeeper applet source (SeedKeeper.java):
From pysatochip's CardConnector.py,
0x9C06triggers automatic PIN re-verification:However, in our case, PIN re-verification doesn't help — the card's EEPROM object manager is corrupted.
Root cause
The
ObjectManageron the SeedKeeper card allocates fixed-size EEPROM slots for each secret. When secrets are deleted, the slot may not be properly freed (or the free list becomes corrupted). After many rapid import/delete cycles:INS_LIST_SECRET_HEADERS(0xA6) fails with0x9C06INS_DELETE_SECRET(0xA5) also fails with0x9C06INS_IMPORT_SECRET(0xA1) may failReproduction
TEST_IMPORT_SECRET:<hex>:abandonTEST_DELETE_SECRET:<id>for each0x9C06TEST_CARD_RESET(GPIO power cycle) does NOT fix itSeedKeeper error code reference
From the applet source, the relevant SW codes are:
0x63cXSW_PIN_FAILED0x9c01SW_NO_MEMORY_LEFT0x9c03SW_OPERATION_NOT_ALLOWED0x9c04SW_SETUP_NOT_DONE0x9c05SW_UNSUPPORTED_FEATURE0x9c06SW_UNAUTHORIZED0x9c08SW_OBJECT_NOT_FOUND0x9c12SW_SEQUENCE_END0x9c20SW_SECURE_CHANNEL_REQUIRED0x9c21SW_SECURE_CHANNEL_UNINITIALIZED0x9c23SW_SECURE_CHANNEL_WRONG_MAC0x9c30SW_LOCK_ERROR0x9c31SW_EXPORT_NOT_ALLOWED0x9c32SW_IMPORTED_DATA_TOO_LONG0x9c36SW_USAGE_NOT_ALLOWED0x9c38SW_WRONG_SECRET_TYPEFixes needed
1. Rate-limit HIL test imports (firmware)
In
_load_seedkeeper(), the retry loop imported the abandon secret multiple times before detecting the "No secrets" screen. Add a guard:2. Add 0x9C06 handling to secure_request
In
satochip_securechannel.py, detect0x9C06and provide a meaningful error message:3. Add TEST_WIPE_SECRETS HIL command
A dedicated command to delete all BIP39 secrets in a single operation, with proper error handling for EEPROM full state:
4. Consider batch delete in firmware
Instead of individual
INS_DELETE_SECRETcalls, implement a single "delete all BIP39" flow that minimizes EEPROM writes.Workaround
Physical card removal and reinsertion is the only reliable recovery. GPIO power cycling (
TEST_CARD_RESET) does NOT clear EEPROM corruption.Environment
st-flashv1.8.0,openocdv0.12.0