Skip to content
Merged
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

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 15 additions & 8 deletions infrastructure/eid-wallet/src/lib/crypto/KeyManagerFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,17 @@ export class KeyManagerFactory {
* Get a key manager instance based on the configuration
*/
static async getKeyManager(config: KeyManagerConfig): Promise<KeyManager> {
// If explicitly requesting hardware and not in pre-verification mode
if (config.useHardware && !config.preVerificationMode) {
return KeyManagerFactory.getHardwareKeyManager();
}

// If in pre-verification mode, always use software keys
// If in pre-verification mode, ALWAYS use software keys (never hardware)
if (config.preVerificationMode) {
console.log("Using software key manager for pre-verification mode");
return KeyManagerFactory.getSoftwareKeyManager();
}

// If explicitly requesting hardware and not in pre-verification mode
if (config.useHardware) {
return KeyManagerFactory.getHardwareKeyManager();
}

// Default behavior: try hardware first, fallback to software
try {
const hardwareManager = KeyManagerFactory.getHardwareKeyManager();
Expand Down Expand Up @@ -82,11 +82,18 @@ export class KeyManagerFactory {
static async getKeyManagerForContext(
keyId: string,
context: "onboarding" | "signing" | "verification" | "pre-verification",
isFake: boolean,
): Promise<KeyManager> {
// Pre-verification users (isFake=true) or pre-verification context should NEVER use hardware
const shouldUseHardware =
!isFake &&
context !== "pre-verification" &&
(await KeyManagerFactory.isHardwareAvailable());

const config: KeyManagerConfig = {
keyId,
useHardware: context !== "pre-verification",
preVerificationMode: context === "pre-verification",
useHardware: shouldUseHardware,
preVerificationMode: context === "pre-verification" || isFake,
};

return KeyManagerFactory.getKeyManager(config);
Expand Down
35 changes: 33 additions & 2 deletions infrastructure/eid-wallet/src/lib/global/controllers/key.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { KeyManagerFactory } from "$lib/crypto";
import type { KeyManager } from "$lib/crypto";
import type { Store } from "@tauri-apps/plugin-store";
import type { UserController } from "./user";

export type KeyServiceContext =
| "onboarding"
Expand Down Expand Up @@ -67,20 +68,50 @@ export class KeyService {
const cachedManager = this.#managerCache.get(cacheKey);
if (cachedManager) {
await this.#touchContext(cacheKey, cachedManager);
return cachedManager;
// If user is pre-verification, ensure we're using software keys
const isFake = await this.#isPreVerificationUser();
if (isFake && cachedManager.getType() === "hardware") {
// Force software keys for pre-verification users
this.#managerCache.delete(cacheKey);
} else {
return cachedManager;
}
}
this.#managerCache.delete(cacheKey);
}

const isFake = await this.#isPreVerificationUser();
// Force pre-verification mode if user is fake/pre-verification
const effectiveContext = isFake ? "pre-verification" : context;
const manager = await KeyManagerFactory.getKeyManagerForContext(
keyId,
context,
effectiveContext,
isFake ?? false,
);
this.#managerCache.set(cacheKey, manager);
await this.#persistContext(cacheKey, manager, keyId, context);
return manager;
}

/**
* Check if the current user is a pre-verification (demo) user
*/
async #isPreVerificationUser(): Promise<boolean> {
const isFake = await this.#store
.get<boolean>("fake")
.then((f) => {
if (!f) {
return false;
}
return f;
})
.catch((error) => {
console.error("Failed to get fake:", error);
return false;
});
return isFake;
}

async ensureKey(
keyId: string,
context: KeyServiceContext,
Expand Down
150 changes: 130 additions & 20 deletions infrastructure/eid-wallet/src/routes/(auth)/onboarding/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,59 @@ let verificationId = $state("");
let demoName = $state("");
let verificationSuccess = $state(false);
let keyManager: KeyManager | null = $state(null);
let showHardwareError = $state(false);
let checkingHardware = $state(false);
const KEY_ID = "default";

const handleGetStarted = async () => {
//get started functionality
isPaneOpen = true;
preVerified = false;
checkingHardware = true;
showHardwareError = false;
error = null;

try {
if (!globalState) {
globalState = getContext<() => GlobalState>("globalState")();
}

// Actually try to generate a test hardware key
const testKeyId = `hardware-test-${Date.now()}`;
console.log(
"Testing hardware key generation with test key:",
testKeyId,
);

try {
const { manager, created } = await globalState.keyService.ensureKey(
testKeyId,
"onboarding",
);
console.log(
"Test key result - Manager type:",
manager.getType(),
"Created:",
created,
);

// Check if we got hardware manager and it actually created a key
if (manager.getType() !== "hardware") {
throw new Error("Got software fallback instead of hardware");
}

// Hardware works! Clean up test key and proceed
console.log("Hardware keys are working");
checkingHardware = false;
} catch (keyError) {
console.error("Hardware key test failed:", keyError);
showHardwareError = true;
checkingHardware = false;
}
} catch (err) {
console.error("Error checking hardware:", err);
showHardwareError = true;
checkingHardware = false;
}
Comment on lines +42 to +78
Copy link
Contributor

@coderabbitai coderabbitai bot Nov 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Stop generating throwaway hardware keys during capability check

ensureKey actually runs the full hardware credential ceremony. That means the “capability check” now forces a biometric/PIN prompt, and any cancellation or timeout will be misread as “hardware unavailable”, pushing real hardware-capable users into the pre-verification path. On top of that, every click leaves behind a hardware-test-<timestamp> credential that we never clean up, which quickly pollutes the authenticator and can exhaust limited key slots. Please switch this probe to the existing isHardwareAvailable() call (or similar non-destructive check) and drop the throwaway key creation altogether.

Recommended change:

@@
-    checkingHardware = true;
-    showHardwareError = false;
-    error = null;
-
-    try {
-        if (!globalState) {
-            globalState = getContext<() => GlobalState>("globalState")();
-        }
-
-        // Actually try to generate a test hardware key
-        const testKeyId = `hardware-test-${Date.now()}`;
-        console.log(
-            "Testing hardware key generation with test key:",
-            testKeyId,
-        );
-
-        try {
-            const { manager, created } = await globalState.keyService.ensureKey(
-                testKeyId,
-                "onboarding",
-            );
-            console.log(
-                "Test key result - Manager type:",
-                manager.getType(),
-                "Created:",
-                created,
-            );
-
-            // Check if we got hardware manager and it actually created a key
-            if (manager.getType() !== "hardware") {
-                throw new Error("Got software fallback instead of hardware");
-            }
-
-            // Hardware works! Clean up test key and proceed
-            console.log("Hardware keys are working");
-            checkingHardware = false;
-        } catch (keyError) {
-            console.error("Hardware key test failed:", keyError);
-            showHardwareError = true;
-            checkingHardware = false;
-        }
-    } catch (err) {
-        console.error("Error checking hardware:", err);
-        showHardwareError = true;
-        checkingHardware = false;
-    }
+    checkingHardware = true;
+    showHardwareError = false;
+    error = null;
+
+    try {
+        if (!globalState) {
+            globalState = getContext<() => GlobalState>("globalState")();
+        }
+
+        const hardwareAvailable =
+            await globalState.keyService.isHardwareAvailable();
+
+        checkingHardware = false;
+        showHardwareError = !hardwareAvailable;
+    } catch (err) {
+        console.error("Error checking hardware:", err);
+        showHardwareError = true;
+        checkingHardware = false;
+    }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
in infrastructure/eid-wallet/src/routes/(auth)/onboarding/+page.svelte around
lines 42-78, stop creating throwaway hardware keys with ensureKey because it
triggers the full credential ceremony and leaves persistent test credentials;
instead call the service's non-destructive probe (e.g., isHardwareAvailable() or
similar) to detect hardware capability, remove the testKeyId generation and
ensureKey invocation, and use the boolean result to set showHardwareError and
checkingHardware accordingly; also remove any catch branches specific to key
creation and only handle probe errors (set showHardwareError=true,
checkingHardware=false) without creating or attempting to clean up credentials.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nack, that's the only way to do it

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find the key service implementation to see what methods are available
echo "=== Searching for KeyService class definition ==="
ast-grep --pattern 'class KeyService {
  $$$
}'

echo -e "\n=== Searching for isHardwareAvailable method ==="
rg -n "isHardwareAvailable" --type ts

echo -e "\n=== Searching for hardware capability check methods ==="
rg -nP "(checkHardware|isHardware|hardwareAvailable|canUseHardware)" --type ts -C2

echo -e "\n=== Finding KeyService file ==="
fd -e ts -e js "KeyService" --full-path

echo -e "\n=== Searching for KeyManager availability checks ==="
rg -n "getType\(\)" --type ts -g "*KeyManager*" -C2

Length of output: 21879


🏁 Script executed:

#!/bin/bash
# Get the full implementation of isHardwareAvailable to understand what it checks
echo "=== KeyManagerFactory.isHardwareAvailable implementation ==="
ast-grep --pattern 'static async isHardwareAvailable(): Promise<boolean> {
  $$$
}'

echo -e "\n=== Find the full KeyManagerFactory file ==="
fd "KeyManagerFactory.ts" --full-path

echo -e "\n=== Get lines 65-80 of KeyManagerFactory to see isHardwareAvailable ==="
find . -name "KeyManagerFactory.ts" -exec sed -n '65,80p' {} \;

echo -e "\n=== Check what getHardwareKeyManager does ==="
ast-grep --pattern 'static getHardwareKeyManager() {
  $$$
}'

Length of output: 880


🏁 Script executed:

#!/bin/bash
# Check what the exists() method does in HardwareKeyManager
echo "=== HardwareKeyManager.exists() implementation ==="
cat infrastructure/eid-wallet/src/lib/crypto/HardwareKeyManager.ts

echo -e "\n=== Check if exists() might trigger a credential ceremony ==="
rg -n "exists.*credential|exists.*prompt|exists.*biometric" --type ts -C3

Length of output: 4760


@coodos, understood — if generating a real test key is the only reliable way to verify hardware support, I'll defer to your experience with the platform.

That said, the test keys created with hardware-test-${Date.now()} are never cleaned up. Each "Get Started" click will leave behind a persistent credential in the hardware keystore, which could accumulate over time or exhaust limited key slots on some devices.

Would you consider adding cleanup logic after the hardware check completes (success or failure)? Something like:

try {
    const { manager, created } = await globalState.keyService.ensureKey(
        testKeyId,
        "onboarding",
    );
    // ... existing checks ...
    
    // Clean up test key
    if (created && manager.getType() === "hardware") {
        // Delete the test key if your KeyManager supports it
        // or track it for later cleanup
    }
} catch (keyError) {
    // ... existing error handling ...
}

If the hardware API doesn't expose a delete/cleanup method, at minimum tracking these test key IDs for bulk cleanup later would prevent them from accumulating indefinitely.

};

const handlePreVerified = () => {
Expand Down Expand Up @@ -104,8 +151,24 @@ async function getApplicationPublicKey() {
}

const handleNext = async () => {
//handle next functionlity
goto("/verify");
// Initialize keys for onboarding context before going to verify
try {
loading = true;
if (!globalState) {
globalState = getContext<() => GlobalState>("globalState")();
}
await initializeKeyManager();
await ensureKeyForContext();
loading = false;
goto("/verify");
} catch (err) {
console.error("Failed to initialize keys for onboarding:", err);
error = "Failed to initialize security keys. Please try again.";
loading = false;
setTimeout(() => {
error = null;
}, 5000);
}
};

let globalState: GlobalState;
Expand Down Expand Up @@ -239,9 +302,13 @@ onMount(async () => {
>
</p>
<div class="flex justify-center whitespace-nowrap mt-1">
<ButtonAction class="w-full" callback={handleGetStarted}
>Get Started</ButtonAction
<ButtonAction
class="w-full"
callback={handleGetStarted}
disabled={checkingHardware}
>
{checkingHardware ? "Checking device..." : "Get Started"}
</ButtonAction>
</div>

<p class="mt-2 text-center">
Expand Down Expand Up @@ -304,20 +371,63 @@ onMount(async () => {
</div>
{/if}
{:else}
<h4 class="mt-[2.3svh] mb-[0.5svh]">
Your Digital Self begins with the Real You
</h4>
<p class="text-black-700">
In the Web 3.0 Data Space, identity is linked to reality. We begin
by verifying your real-world passport, which serves as the
foundation for issuing your secure ePassport. At the same time, we
generate your eName – a unique digital identifier – and create your
eVault to store and protect your personal data.
</p>
<div class="flex justify-center whitespace-nowrap my-[2.3svh]">
<ButtonAction class="w-full" callback={handleNext}
>Next</ButtonAction
>
</div>
{#if checkingHardware}
<div class="my-20">
<div
class="align-center flex w-full flex-col items-center justify-center gap-6"
>
<Shadow size={40} color="rgb(142, 82, 255);" />
<h4>Checking device capabilities...</h4>
</div>
</div>
{:else if showHardwareError}
<h4 class="mt-[2.3svh] mb-[0.5svh] text-red-600">
Hardware Security Not Available
</h4>
<p class="text-black-700 mb-4">
Your phone doesn't support hardware crypto keys, which is a requirement for verified IDs.
</p>
<p class="text-black-700 mb-4">
Please use the pre-verification code option to create a demo account instead.
</p>
<div class="flex justify-center whitespace-nowrap my-[2.3svh]">
<ButtonAction
class="w-full"
callback={() => {
isPaneOpen = false;
handlePreVerified();
}}
>
Use Pre-Verification Code
</ButtonAction>
</div>
{:else}
{#if loading}
<div class="my-20">
<div
class="align-center flex w-full flex-col items-center justify-center gap-6"
>
<Shadow size={40} color="rgb(142, 82, 255);" />
<h4>Initializing security keys...</h4>
</div>
</div>
{:else}
<h4 class="mt-[2.3svh] mb-[0.5svh]">
Your Digital Self begins with the Real You
</h4>
<p class="text-black-700">
In the Web 3.0 Data Space, identity is linked to reality. We begin
by verifying your real-world passport, which serves as the
foundation for issuing your secure ePassport. At the same time, we
generate your eName – a unique digital identifier – and create your
eVault to store and protect your personal data.
</p>
<div class="flex justify-center whitespace-nowrap my-[2.3svh]">
<ButtonAction class="w-full" callback={handleNext}
>Next</ButtonAction
>
</div>
{/if}
{/if}
{/if}
</Drawer>
59 changes: 51 additions & 8 deletions infrastructure/eid-wallet/src/routes/(auth)/verify/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -109,12 +109,41 @@ let hardwareKeyCheckComplete = $state(false);
const KEY_ID = "default";

async function handleVerification() {
const { data } = await axios.post(
new URL("/verification", PUBLIC_PROVISIONER_URL).toString(),
);
verificaitonId.set(data.id);
showVeriffModal = true;
watchEventStream(data.id);
try {
// Ensure keys are initialized before starting verification
if (!keyManager) {
try {
await initializeKeyManager();
await ensureKeyForVerification();
} catch (keyError) {
console.error("Failed to initialize keys:", keyError);
// If key initialization fails, go back to onboarding
await goto("/onboarding");
return;
}
}

const { data } = await axios.post(
new URL("/verification", PUBLIC_PROVISIONER_URL).toString(),
);
verificaitonId.set(data.id);
showVeriffModal = true;
watchEventStream(data.id);
} catch (error) {
console.error("Failed to start verification:", error);
// If verification fails due to key issues or any initialization error, go back to onboarding
const errorMessage =
error instanceof Error
? error.message.toLowerCase()
: String(error).toLowerCase();
if (
errorMessage.includes("key") ||
errorMessage.includes("initialize") ||
errorMessage.includes("manager")
) {
await goto("/onboarding");
}
}
}

function watchEventStream(id: string) {
Expand Down Expand Up @@ -227,9 +256,23 @@ onMount(async () => {
// Check hardware key support first
await checkHardwareKeySupport();

// If hardware is not available, redirect back to onboarding
if (!hardwareKeySupported) {
console.log("Hardware not available, redirecting to onboarding");
await goto("/onboarding");
return;
}

// Initialize key manager and check if default key pair exists
await initializeKeyManager();
await ensureKeyForVerification();
try {
await initializeKeyManager();
await ensureKeyForVerification();
} catch (error) {
console.error("Failed to initialize keys for verification:", error);
// If key initialization fails, redirect back to onboarding
await goto("/onboarding");
return;
}

handleContinue = async () => {
if ($status !== "approved" && $status !== "duplicate")
Expand Down