Skip to content

Commit 267229a

Browse files
committed
feat: key manager self healing
1 parent 8885f6c commit 267229a

2 files changed

Lines changed: 83 additions & 30 deletions

File tree

infrastructure/eid-wallet/src/lib/crypto/SoftwareKeyManager.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,21 @@ export class SoftwareKeyManager implements KeyManager {
6565
}
6666
}
6767

68+
/**
69+
* Discard any stored key and generate a fresh one. Used by the key service
70+
* to recover from an unusable key (e.g. a hardware key the OS evicted) by
71+
* rotating onto a brand-new software key. Unlike generate(), this never
72+
* no-ops on an existing key.
73+
*/
74+
async regenerate(): Promise<void> {
75+
try {
76+
localStorage.removeItem(STORAGE_KEY);
77+
} catch {
78+
// ignore — generate() will surface any genuine storage error
79+
}
80+
await this.generate();
81+
}
82+
6883
async getPublicKey(): Promise<string> {
6984
const keyPair = this.#read();
7085
const buf = base64ToArrayBuffer(keyPair.publicKey);

infrastructure/eid-wallet/src/lib/global/controllers/key.ts

Lines changed: 68 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import { KeyManagerFactory, migrateLegacySoftwareKey } from "$lib/crypto";
1+
import {
2+
KeyManagerError,
3+
KeyManagerFactory,
4+
migrateLegacySoftwareKey,
5+
} from "$lib/crypto";
26
import type { KeyManager } from "$lib/crypto";
37
import type { Store } from "@tauri-apps/plugin-store";
48

@@ -161,49 +165,83 @@ export class KeyService {
161165
}
162166

163167
/**
164-
* Run a key operation. On failure, check whether our local public key is
165-
* bound on the eVault; if not, sync it and retry once. If the retry still
166-
* fails — or the local key was already bound — the original error stands.
168+
* Recover from an unusable signing key by rotating onto a fresh software
169+
* key. Hardware is what failed (and the plugin can't delete a keystore
170+
* alias from JS), so recovery always lands on software. The caller is
171+
* responsible for (re)binding the new key to the eVault.
172+
*/
173+
async #rotateToFreshKey(): Promise<void> {
174+
await this.#demoteToSoftware();
175+
await KeyManagerFactory.getSoftware().regenerate();
176+
}
177+
178+
/**
179+
* Run a key operation with self-healing. On failure:
180+
* 1. If the local key is usable but not yet registered on the eVault, bind
181+
* it and retry once.
182+
* 2. If the local key itself is unusable — the common failure on Android
183+
* devices whose OS evicts/invalidates keystore entries, where the key
184+
* still "exists" and its public key may still be bound but can no longer
185+
* sign — rotate onto a fresh software key, (re)bind it (additive: the
186+
* dead key is left in place but is inert), and retry once.
187+
* If neither remedy resolves it, the original error stands.
167188
*/
168189
async #withEvaultSync<T>(op: () => Promise<T>): Promise<T> {
169190
try {
170191
return await op();
171192
} catch (firstError) {
172-
if (!this.#evaultKeyResolver || !this.#evaultSyncHandler) {
193+
if (!this.#evaultSyncHandler) {
173194
throw firstError;
174195
}
175196

176-
let localPk: string;
177-
try {
178-
localPk = await this.getPublicKey();
179-
} catch {
180-
throw firstError;
197+
// Remedy 1: the local key works but isn't registered on the eVault
198+
// yet — bind it and retry. If the key is unreadable or the retry
199+
// still fails, fall through to rotation.
200+
if (this.#evaultKeyResolver) {
201+
try {
202+
const localPk = await this.getPublicKey();
203+
const bound = await this.#evaultKeyResolver();
204+
if (!bound.includes(localPk)) {
205+
console.warn(
206+
"[KeyService] Local key not bound on eVault; syncing and retrying",
207+
);
208+
if (await this.#evaultSyncHandler()) {
209+
return await op();
210+
}
211+
}
212+
} catch (rebindError) {
213+
console.warn(
214+
"[KeyService] eVault re-bind did not resolve the failure:",
215+
rebindError,
216+
);
217+
}
181218
}
182219

183-
let bound: string[];
184-
try {
185-
bound = await this.#evaultKeyResolver();
186-
} catch (resolverError) {
220+
// Remedy 2: the local key can't perform the operation (e.g. a
221+
// hardware key the OS evicted). Only attempt this for genuine key
222+
// failures. Rotate to a fresh software key, bind it, retry once.
223+
if (firstError instanceof KeyManagerError) {
187224
console.warn(
188-
"[KeyService] eVault key resolver failed during recovery:",
189-
resolverError,
225+
"[KeyService] Signing key unusable; rotating to a fresh key and re-binding:",
226+
firstError,
190227
);
191-
throw firstError;
192-
}
193-
194-
if (bound.includes(localPk)) {
195-
// Our key is already bound — the failure is real.
196-
throw firstError;
228+
try {
229+
await this.#rotateToFreshKey();
230+
if (await this.#evaultSyncHandler()) {
231+
return await op();
232+
}
233+
console.error(
234+
"[KeyService] failed to bind rotated key to eVault",
235+
);
236+
} catch (rotateError) {
237+
console.warn(
238+
"[KeyService] key rotation recovery failed:",
239+
rotateError,
240+
);
241+
}
197242
}
198243

199-
console.warn(
200-
"[KeyService] Local key not bound on eVault; syncing and retrying",
201-
);
202-
const synced = await this.#evaultSyncHandler();
203-
if (!synced) {
204-
throw firstError;
205-
}
206-
return await op();
244+
throw firstError;
207245
}
208246
}
209247

0 commit comments

Comments
 (0)