Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
1 change: 1 addition & 0 deletions infrastructure/eid-wallet/src/env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ declare namespace App {}
declare module "$env/static/public" {
export const PUBLIC_REGISTRY_URL: string;
export const PUBLIC_PROVISIONER_URL: string;
export const PUBLIC_PLATFORM_URL: string;
Comment thread
coodos marked this conversation as resolved.
Outdated
}
184 changes: 184 additions & 0 deletions infrastructure/eid-wallet/src/lib/global/controllers/key.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import { KeyManagerFactory } from "$lib/crypto";
import type { KeyManager } from "$lib/crypto";
import type { Store } from "@tauri-apps/plugin-store";

export type KeyServiceContext =
| "onboarding"
| "signing"
| "verification"
| "pre-verification";

type PersistedContext = {
keyId: string;
context: KeyServiceContext;
managerType: "hardware" | "software";
lastUsed: string;
};

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

export class KeyService {
#store: Store;
#managerCache = new Map<string, KeyManager>();
#contexts = new Map<string, PersistedContext>();
#ready = false;

constructor(store: Store) {
this.#store = store;
}

async initialize(): Promise<void> {
const storedContexts =
await this.#store.get<Record<string, PersistedContext>>(
CONTEXTS_KEY,
);
if (storedContexts) {
for (const [key, value] of Object.entries(storedContexts)) {
this.#contexts.set(key, value);
}
}
this.#ready = (await this.#store.get<boolean>(READY_KEY)) ?? false;
}

get isReady(): boolean {
return this.#ready;
}

async setReady(value: boolean): Promise<void> {
this.#ready = value;
await this.#store.set(READY_KEY, value);
}

async reset(): Promise<void> {
this.#managerCache.clear();
this.#contexts.clear();
await this.#store.delete(CONTEXTS_KEY);
await this.#store.delete(READY_KEY);
this.#ready = false;
}

async getManager(
keyId: string,
context: KeyServiceContext,
): Promise<KeyManager> {
const cacheKey = this.#getCacheKey(keyId, context);
if (this.#managerCache.has(cacheKey)) {
const cachedManager = this.#managerCache.get(cacheKey);
if (cachedManager) {
await this.#touchContext(cacheKey, cachedManager);
return cachedManager;
}
this.#managerCache.delete(cacheKey);
}

const manager = await KeyManagerFactory.getKeyManagerForContext(
keyId,
context,
);
this.#managerCache.set(cacheKey, manager);
await this.#persistContext(cacheKey, manager, keyId, context);
return manager;
}

async ensureKey(
keyId: string,
context: KeyServiceContext,
): Promise<{ manager: KeyManager; created: boolean }> {
const manager = await this.getManager(keyId, context);
const exists = await manager.exists(keyId);
let created = false;
if (!exists) {
await manager.generate(keyId);
await this.#touchContext(
this.#getCacheKey(keyId, context),
manager,
);
created = true;
}
return { manager, created };
}

async getPublicKey(
keyId: string,
context: KeyServiceContext,
): Promise<string | undefined> {
const manager = await this.getManager(keyId, context);
const publicKey = await manager.getPublicKey(keyId);
await this.#touchContext(this.#getCacheKey(keyId, context), manager);
return publicKey;
}

async signPayload(
keyId: string,
context: KeyServiceContext,
payload: string,
): Promise<string> {
const manager = await this.getManager(keyId, context);
const signature = await manager.signPayload(keyId, payload);
await this.#touchContext(this.#getCacheKey(keyId, context), manager);
return signature;
}

async verifySignature(
keyId: string,
context: KeyServiceContext,
payload: string,
signature: string,
): Promise<boolean> {
const manager = await this.getManager(keyId, context);
const result = await manager.verifySignature(keyId, payload, signature);
await this.#touchContext(this.#getCacheKey(keyId, context), manager);
return result;
}

async isHardwareAvailable(): Promise<boolean> {
return KeyManagerFactory.isHardwareAvailable();
}

#getCacheKey(keyId: string, context: KeyServiceContext): string {
return `${context}:${keyId}`;
}

async #persistContext(
cacheKey: string,
manager: KeyManager,
keyId: string,
context: KeyServiceContext,
): Promise<void> {
const entry: PersistedContext = {
keyId,
context,
managerType: manager.getType(),
lastUsed: new Date().toISOString(),
};
this.#contexts.set(cacheKey, entry);
await this.#store.set(CONTEXTS_KEY, Object.fromEntries(this.#contexts));
}

async #touchContext(cacheKey: string, manager?: KeyManager): Promise<void> {
const current = this.#contexts.get(cacheKey);
if (!current && manager) {
const { keyId, context } = this.#parseCacheKey(cacheKey);
await this.#persistContext(cacheKey, manager, keyId, context);
return;
}
if (!current) {
return;
}
current.lastUsed = new Date().toISOString();
this.#contexts.set(cacheKey, current);
await this.#store.set(CONTEXTS_KEY, Object.fromEntries(this.#contexts));
}

#parseCacheKey(cacheKey: string): {
context: KeyServiceContext;
keyId: string;
} {
const [context, ...rest] = cacheKey.split(":");
return {
context: context as KeyServiceContext,
keyId: rest.join(":"),
};
}
}
2 changes: 2 additions & 0 deletions infrastructure/eid-wallet/src/lib/global/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
export { GlobalState } from "./state";
export { runtime } from "./runtime.svelte";
export { KeyService } from "./controllers/key";
export type { KeyServiceContext } from "./controllers/key";
58 changes: 56 additions & 2 deletions infrastructure/eid-wallet/src/lib/global/state.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Store } from "@tauri-apps/plugin-store";
import NotificationService from "../services/NotificationService";
import { VaultController } from "./controllers/evault";
import { KeyService } from "./controllers/key";
import { SecurityController } from "./controllers/security";
import { UserController } from "./controllers/user";
/**
Expand All @@ -27,13 +28,15 @@ export class GlobalState {
userController: UserController;
vaultController: VaultController;
notificationService: NotificationService;
keyService: KeyService;

private constructor(store: Store) {
private constructor(store: Store, keyService: KeyService) {
this.#store = store;
this.securityController = new SecurityController(store);
this.userController = new UserController(store);
this.vaultController = new VaultController(store, this.userController);
this.notificationService = NotificationService.getInstance();
this.keyService = keyService;
}

/**
Expand All @@ -46,13 +49,64 @@ export class GlobalState {
const store = await Store.load("global-state.json", {
autoSave: true,
});
const keyService = new KeyService(store);
await keyService.initialize();
const alreadyInitialized = await store.get<boolean>("initialized");

const instance = new GlobalState(store);
const instance = new GlobalState(store, keyService);

if (!alreadyInitialized) {
await instance.#store.set("initialized", true);
await instance.#store.set("isOnboardingComplete", false);
await instance.keyService.setReady(false);
} else {
const onboardingFlag = await instance.#store.get<boolean>(
"isOnboardingComplete",
);
if (onboardingFlag === undefined) {
await instance.#store.set("isOnboardingComplete", false);
await instance.keyService.setReady(false);
} else {
await instance.keyService.setReady(onboardingFlag);
}
}
return instance;
}

get isOnboardingComplete() {
return this.#store
.get<boolean>("isOnboardingComplete")
.then((value) => value ?? false)
.catch((error) => {
console.error("Failed to get onboarding status:", error);
return false;
});
}

set isOnboardingComplete(value: boolean | Promise<boolean>) {
if (value instanceof Promise) {
value
.then((resolved) => {
this.#store
.set("isOnboardingComplete", resolved)
.then(() => this.keyService.setReady(resolved))
.catch((error) => {
console.error(
"Failed to set onboarding status:",
error,
);
});
})
.catch((error) => {
console.error("Failed to set onboarding status:", error);
});
} else {
this.#store
.set("isOnboardingComplete", value)
.then(() => this.keyService.setReady(value))
.catch((error) => {
console.error("Failed to set onboarding status:", error);
});
}
}
}
Loading
Loading