refactor: own CredentialStore from PersonalizationApplet instance

Prepares for AMD-H by moving the singleton's anchor off a static field
onto the PersonalizationApplet instance. The static INSTANCE remains
solely as an in-package publish-point so AliroApplet/StepUpApplet keep
working unchanged.

Adds CredentialStore.bootstrap() (called from PersonalizationApplet's
constructor) and CredentialStore.republish() (reserved for the upcoming
onRestore hook). Drops the lazy-init path in get() — every install path
now goes through PersonalizationApplet first, so INSTANCE is always set
by the time AliroApplet looks it up. Test setUps that previously called
CredentialStore.get().resetForTesting() before the applet was installed
are reordered or dropped: each test now gets a fresh store via the
constructor's bootstrap() call.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
michael
2026-05-25 20:20:22 -07:00
parent 86429b1f4d
commit 1f54a690d0
5 changed files with 92 additions and 12 deletions

View File

@@ -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.
*
* <p>Two applets in the same CAP file share this class's static state
* directly — no {@code Shareable} interface needed.
* <p>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.
*
* <p>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.
*
* <p>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;
}