|
1 | | -import { KeyManagerFactory, migrateLegacySoftwareKey } from "$lib/crypto"; |
| 1 | +import { |
| 2 | + KeyManagerError, |
| 3 | + KeyManagerFactory, |
| 4 | + migrateLegacySoftwareKey, |
| 5 | +} from "$lib/crypto"; |
2 | 6 | import type { KeyManager } from "$lib/crypto"; |
3 | 7 | import type { Store } from "@tauri-apps/plugin-store"; |
4 | 8 |
|
@@ -161,49 +165,83 @@ export class KeyService { |
161 | 165 | } |
162 | 166 |
|
163 | 167 | /** |
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. |
167 | 188 | */ |
168 | 189 | async #withEvaultSync<T>(op: () => Promise<T>): Promise<T> { |
169 | 190 | try { |
170 | 191 | return await op(); |
171 | 192 | } catch (firstError) { |
172 | | - if (!this.#evaultKeyResolver || !this.#evaultSyncHandler) { |
| 193 | + if (!this.#evaultSyncHandler) { |
173 | 194 | throw firstError; |
174 | 195 | } |
175 | 196 |
|
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 | + } |
181 | 218 | } |
182 | 219 |
|
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) { |
187 | 224 | 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, |
190 | 227 | ); |
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 | + } |
197 | 242 | } |
198 | 243 |
|
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; |
207 | 245 | } |
208 | 246 | } |
209 | 247 |
|
|
0 commit comments