diff --git a/applet/src/main/java/com/dangerousthings/aliro/CredentialStore.java b/applet/src/main/java/com/dangerousthings/aliro/CredentialStore.java index 33b1317..f8c5ca6 100644 --- a/applet/src/main/java/com/dangerousthings/aliro/CredentialStore.java +++ b/applet/src/main/java/com/dangerousthings/aliro/CredentialStore.java @@ -6,8 +6,18 @@ package com.dangerousthings.aliro; * {@link PersonalizationApplet} writes into it; {@link AliroApplet} reads * from it when it needs long-term keys for AUTH1 / step-up. * - *
Two applets in the same CAP file share this class's static state - * directly — no {@code Shareable} interface needed. + *
Ownership: the single live instance is owned by the + * {@link PersonalizationApplet} instance (created in its constructor via + * {@link #bootstrap()}). The static {@link #INSTANCE} field is just an + * in-package publish-point so AliroApplet/StepUpApplet can find it via + * {@link #get()} without going through a {@code Shareable} SIO. + * + *
The on-instance ownership matters for GlobalPlatform Amendment H + * (Executable Load File Upgrade): AMD-H preserves registered applet + * instances and their reachable object graph but wipes static fields. With + * the live reference held on the PersonalizationApplet instance, an upgrade + * keeps enrollment data; PersonalizationApplet's restore hook then calls + * {@link #republish(CredentialStore)} to re-establish the static alias. * *
After {@link #commit()} is called, further writes via the
* personalization interface are refused. The only way to re-unlock is
@@ -23,8 +33,11 @@ final class CredentialStore {
* accommodates a typical Aliro Access Document with room to spare. */
static final short ACCESS_DOC_MAX_LEN = 1024;
- /** Lazily initialized in {@link #get()}. Java Card bans {@code new} in
- * static initializers, so we can't declare {@code = new CredentialStore()}. */
+ /** Publish-point read by AliroApplet/StepUpApplet via {@link #get()}.
+ * PersonalizationApplet owns the actual instance; this is just an alias
+ * so other applets in the same package can find it without SIO. The
+ * reference is re-published after an AMD-H restore (see
+ * PersonalizationApplet.onRestore). */
private static CredentialStore INSTANCE;
private final byte[] credentialPrivKey;
@@ -49,10 +62,21 @@ final class CredentialStore {
accessDocument = new byte[ACCESS_DOC_MAX_LEN];
}
+ /** Called once from {@link PersonalizationApplet}'s constructor. */
+ static CredentialStore bootstrap() {
+ INSTANCE = new CredentialStore();
+ return INSTANCE;
+ }
+
+ /** Called by {@code PersonalizationApplet.onRestore} to re-publish a
+ * restored store after an AMD-H Executable Load File upgrade, where
+ * static fields are wiped but the PersonalizationApplet instance (and
+ * its CredentialStore reference) survive. */
+ static void republish(CredentialStore restored) {
+ INSTANCE = restored;
+ }
+
static CredentialStore get() {
- if (INSTANCE == null) {
- INSTANCE = new CredentialStore();
- }
return INSTANCE;
}
diff --git a/applet/src/main/java/com/dangerousthings/aliro/PersonalizationApplet.java b/applet/src/main/java/com/dangerousthings/aliro/PersonalizationApplet.java
index 9d1545e..fa1ba03 100644
--- a/applet/src/main/java/com/dangerousthings/aliro/PersonalizationApplet.java
+++ b/applet/src/main/java/com/dangerousthings/aliro/PersonalizationApplet.java
@@ -26,6 +26,18 @@ public class PersonalizationApplet extends Applet {
private static final byte INS_FINALIZE_ACCESS_DOC = (byte) 0x24;
private static final byte INS_COMMIT = (byte) 0x2C;
+ /** The owning reference to the shared CredentialStore. AliroApplet and
+ * StepUpApplet reach it via the {@link CredentialStore#get()} static
+ * publish-point; we keep the actual reference on this applet instance
+ * so AMD-H Executable Load File upgrades (which wipe static fields but
+ * preserve registered applet instances and their reachable object
+ * state) don't lose enrollment data. */
+ private final CredentialStore store;
+
+ public PersonalizationApplet() {
+ this.store = CredentialStore.bootstrap();
+ }
+
public static void install(byte[] bArray, short bOffset, byte bLength) {
PersonalizationApplet applet = new PersonalizationApplet();
if (bArray == null || bLength == 0) {
@@ -46,7 +58,9 @@ public class PersonalizationApplet extends Applet {
ISOException.throwIt(ISO7816.SW_CLA_NOT_SUPPORTED);
}
- CredentialStore store = CredentialStore.get();
+ // Use the instance-owned reference rather than the static publish-point
+ // so this applet keeps working even before/around an AMD-H restore.
+ CredentialStore store = this.store;
byte ins = buf[ISO7816.OFFSET_INS];
switch (ins) {
diff --git a/applet/src/test/java/com/dangerousthings/aliro/AliroAppletAuth1Test.java b/applet/src/test/java/com/dangerousthings/aliro/AliroAppletAuth1Test.java
index 6f404dd..6a88627 100644
--- a/applet/src/test/java/com/dangerousthings/aliro/AliroAppletAuth1Test.java
+++ b/applet/src/test/java/com/dangerousthings/aliro/AliroAppletAuth1Test.java
@@ -30,7 +30,9 @@ class AliroAppletAuth1Test {
@BeforeEach
void setUp() {
- CredentialStore.get().resetForTesting();
+ // reader.provision() below installs PersonalizationApplet, whose
+ // constructor calls CredentialStore.bootstrap() and gives us a fresh
+ // store for every test — no explicit reset needed any more.
sim = new CardSimulator();
AID expeditedAid = new AID(AliroAids.EXPEDITED, (short) 0, (byte) AliroAids.EXPEDITED.length);
sim.installApplet(expeditedAid, AliroApplet.class);
diff --git a/applet/src/test/java/com/dangerousthings/aliro/PersonalizationAppletTest.java b/applet/src/test/java/com/dangerousthings/aliro/PersonalizationAppletTest.java
index a6a6cb9..3204af7 100644
--- a/applet/src/test/java/com/dangerousthings/aliro/PersonalizationAppletTest.java
+++ b/applet/src/test/java/com/dangerousthings/aliro/PersonalizationAppletTest.java
@@ -2,12 +2,15 @@ package com.dangerousthings.aliro;
import com.licel.jcardsim.smartcardio.CardSimulator;
import javacard.framework.AID;
+import javacard.framework.Applet;
import javax.smartcardio.CommandAPDU;
import javax.smartcardio.ResponseAPDU;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
/**
* Tests for the PersonalizationApplet — our proprietary provisioning
@@ -28,8 +31,8 @@ class PersonalizationAppletTest {
@BeforeEach
void setUp() {
- CredentialStore.get().resetForTesting();
-
+ // installApplet → PersonalizationApplet.