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
2 changes: 1 addition & 1 deletion infrastructure/eid-wallet/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "eid-wallet",
"version": "0.5.0",
"version": "0.6.0",
"description": "",
"type": "module",
"scripts": {
Expand Down
8 changes: 5 additions & 3 deletions infrastructure/eid-wallet/src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "eID for W3DS",
"version": "0.5.0",
"version": "0.6.0",
"identifier": "foundation.metastate.eid-wallet",
"build": {
"beforeDevCommand": "pnpm dev",
Expand All @@ -18,15 +18,17 @@
}
],
"security": {
"capabilities": ["mobile-capability"],
"capabilities": [
"mobile-capability"
],
"csp": null
}
},
"bundle": {
"active": true,
"targets": "all",
"android": {
"versionCode": 12
"versionCode": 22
},
"icon": [
"icons/32x32.png",
Expand Down
103 changes: 94 additions & 9 deletions infrastructure/eid-wallet/src/lib/global/controllers/key.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ type PersistedContext = {
lastUsed: string;
};

type HardwareFallbackEvent = {
keyId: string;
context: KeyServiceContext;
originalError: unknown;
};

const CONTEXTS_KEY = "keyService.contexts";
const READY_KEY = "keyService.ready";

Expand All @@ -24,6 +30,9 @@ export class KeyService {
#managerCache = new Map<string, KeyManager>();
#contexts = new Map<string, PersistedContext>();
#ready = false;
#onHardwareFallback:
| ((event: HardwareFallbackEvent) => Promise<void> | void)
| null = null;

constructor(store: Store) {
this.#store = store;
Expand Down Expand Up @@ -176,16 +185,70 @@ export class KeyService {
);
}

console.log("=".repeat(70));
const signature = await manager.signPayload(keyId, payload);
console.log(
`✅ [KeyService] Signature created: ${signature.substring(0, 50)}...`,
);
console.log(`Signature length: ${signature.length} chars`);
console.log("=".repeat(70));
const cacheKey = this.#getCacheKey(keyId, context);
try {
console.log("=".repeat(70));
const signature = await manager.signPayload(keyId, payload);
console.log(
`✅ [KeyService] Signature created: ${signature.substring(0, 50)}...`,
);
console.log(`Signature length: ${signature.length} chars`);
console.log("=".repeat(70));

await this.#touchContext(this.#getCacheKey(keyId, context), manager);
return signature;
await this.#touchContext(cacheKey, manager);
return signature;
} catch (signError) {
if (managerType !== "hardware") {
throw signError;
}

console.warn(
"[KeyService] Hardware signing failed; falling back to software key",
{
keyId,
context,
error:
signError instanceof Error
? signError.message
: String(signError),
},
);

const softwareManager = await KeyManagerFactory.getKeyManager({
keyId,
useHardware: false,
preVerificationMode: false,
});

const softwareKeyExists = await softwareManager.exists(keyId);
if (!softwareKeyExists) {
await softwareManager.generate(keyId);
}

const fallbackSignature = await softwareManager.signPayload(
keyId,
payload,
);

this.#managerCache.set(cacheKey, softwareManager);
await this.#persistContext(
cacheKey,
softwareManager,
keyId,
context,
);
await this.#runHardwareFallbackCallback({
keyId,
context,
originalError: signError,
});

console.log(
`✅ [KeyService] Fallback signature created: ${fallbackSignature.substring(0, 50)}...`,
);
console.log("=".repeat(70));
return fallbackSignature;
}
}

async verifySignature(
Expand Down Expand Up @@ -266,4 +329,26 @@ export class KeyService {
await this.#store.delete(READY_KEY);
this.#ready = false;
}

setHardwareFallbackHandler(
handler:
| ((event: HardwareFallbackEvent) => Promise<void> | void)
| null,
): void {
this.#onHardwareFallback = handler;
}

async #runHardwareFallbackCallback(
event: HardwareFallbackEvent,
): Promise<void> {
if (!this.#onHardwareFallback) return;
try {
await this.#onHardwareFallback(event);
} catch (callbackError) {
console.error(
"[KeyService] Hardware fallback callback failed:",
callbackError,
);
}
}
}
27 changes: 27 additions & 0 deletions infrastructure/eid-wallet/src/lib/global/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,33 @@ export class GlobalState {
this.#walletSdkAdapter,
);
this.notificationService = NotificationService.getInstance();

this.keyService.setHardwareFallbackHandler(
async ({ keyId, context, originalError }) => {
console.warn(
"[GlobalState] Hardware signing fallback triggered; syncing software public key",
{
keyId,
context,
error:
originalError instanceof Error
? originalError.message
: String(originalError),
},
);

try {
const vault = await this.vaultController.vault;
if (!vault?.ename) return;
await this.vaultController.syncPublicKey(vault.ename);
} catch (syncError) {
console.error(
"[GlobalState] Failed to sync fallback public key:",
syncError,
);
}
},
);
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<script lang="ts">
import { cn } from "$lib/utils";
import type { Snippet } from "svelte";
import type { HTMLAttributes } from "svelte/elements";

interface BottomSheetProps extends HTMLAttributes<HTMLDivElement> {
isOpen?: boolean;
dismissible?: boolean;
children?: Snippet;
onOpenChange?: (value: boolean) => void;
}

let {
isOpen = $bindable(false),
dismissible = true,
children = undefined,
onOpenChange,
...restProps
}: BottomSheetProps = $props();

let lastReportedOpen = isOpen;

function handleClose() {
if (!dismissible) return;
isOpen = false;
onOpenChange?.(false);
}

$effect(() => {
if (isOpen !== lastReportedOpen) {
lastReportedOpen = isOpen;
onOpenChange?.(isOpen);
}
});
</script>

{#if isOpen}
<div
class="fixed inset-0 z-40 bg-black/40"
aria-hidden="true"
onclick={handleClose}
></div>
<div
{...restProps}
role="dialog"
aria-modal="true"
class={cn(
"fixed inset-x-0 bottom-0 z-50 bg-white rounded-t-3xl shadow-xl flex flex-col gap-4 max-h-[88svh] overflow-y-auto",
restProps.class,
)}
style={`padding: 1.5rem 1.5rem max(1.5rem, env(safe-area-inset-bottom)); ${restProps.style ?? ""}`}
>
{@render children?.()}
</div>
{/if}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script lang="ts">
import { ButtonAction, Drawer } from "$lib/ui";
import { BottomSheet, ButtonAction } from "$lib/ui";

interface CameraPermissionDialogProps {
isOpen: boolean;
Expand All @@ -21,21 +21,16 @@ let {
dismissible = false,
}: CameraPermissionDialogProps = $props();

function handleSwipe(value: boolean | undefined) {
// Only allow swipe to close when dismissible is true and onOpenChange is provided
if (!dismissible || !onOpenChange) {
return;
}
if (value) {
isOpen = false;
onOpenChange(false);
}
function handleOpenChange(value: boolean) {
if (!dismissible && !value) return;
isOpen = value;
onOpenChange?.(value);
}
</script>

<Drawer
isPaneOpen={isOpen}
handleSwipe={dismissible && onOpenChange ? handleSwipe : undefined}
<BottomSheet
{isOpen}
onOpenChange={handleOpenChange}
dismissible={dismissible}
>
<div class="flex flex-col items-center text-center pb-4">
Expand Down Expand Up @@ -96,4 +91,4 @@ function handleSwipe(value: boolean | undefined) {
{/if}
</div>
</div>
</Drawer>
</BottomSheet>
1 change: 1 addition & 0 deletions infrastructure/eid-wallet/src/lib/ui/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export { default as Drawer } from "./Drawer/Drawer.svelte";
export { default as BottomSheet } from "./BottomSheet/BottomSheet.svelte";
export { default as InputPin } from "./InputPin/InputPin.svelte";
export { default as ButtonAction } from "./Button/ButtonAction.svelte";
export { default as Selector } from "./Selector/Selector.svelte";
Expand Down
5 changes: 4 additions & 1 deletion infrastructure/eid-wallet/src/routes/(app)/+layout.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,10 @@ $effect(() => {
</div>
{/if} -->

<div class="p-6 pt-10">
<div
class="p-6"
style="padding-top: max(2.5rem, calc(env(safe-area-inset-top) + 1rem));"
>
{@render children()}
</div>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ async function startKycUpgrade() {
await globalState.walletSdkAdapter.ensureKey("default", "onboarding");

const { data } = await axios.post(
new URL("/verification", PUBLIC_PROVISIONER_URL).toString(),
new URL("/verification/v2", PUBLIC_PROVISIONER_URL).toString(),
{},
{
headers: {
Expand Down Expand Up @@ -234,7 +234,7 @@ const handleDiditComplete = async (result: DiditCompleteResult) => {
try {
const { data: decision } = await axios.get<DiditDecision>(
new URL(
`/verification/decision/${result.session.sessionId}`,
`/verification/v2/decision/${result.session.sessionId}`,
PUBLIC_PROVISIONER_URL,
).toString(),
{
Expand Down Expand Up @@ -291,7 +291,10 @@ async function handleUpgrade() {
kycStep = "upgrading";
try {
const { data } = await axios.post(
new URL("/verification/upgrade", PUBLIC_PROVISIONER_URL).toString(),
new URL(
"/verification/v2/upgrade",
PUBLIC_PROVISIONER_URL,
).toString(),
{ diditSessionId: sessionId, w3id },
{
headers: {
Expand Down
Loading