Summary
- Replace hard keytar dependency with a multi-strategy key storage:
1) safeStorage (Electron built-in) — preferred
2) keytar (system keychain) — fallback
3) file-based encryption — last resort
- Dynamically import keytar to avoid asar load failures
- Refactor cloudHandler to use const (clean code)
- Fix “Failed to load cloud accounts” in packaged app
Rationale
The keytar native module can fail to load from asar archives in production builds. This ensures the app works even when system keychain access is unavailable.
Additional changes
- Replace sync file ops with async (fs/promises)
- Add atomic file writes to prevent data races
- Implement concurrency lock for key generation
- Add security error codes for better diagnostics
- Add warnings for file-based fallback mode
- Align error handling after merge
- Migrate legacy encrypted account data
References
- Issue: #38
- Commit: 92dc2f6
79 lines
2.4 KiB
TypeScript
79 lines
2.4 KiB
TypeScript
import type { TFunction } from 'i18next';
|
|
|
|
const KEYCHAIN_ERROR_CODE = 'ERR_KEYCHAIN_UNAVAILABLE';
|
|
const KEYCHAIN_HINT_TRANSLOCATION = 'HINT_APP_TRANSLOCATION';
|
|
const KEYCHAIN_HINT_KEYCHAIN_DENIED = 'HINT_KEYCHAIN_DENIED';
|
|
const KEYCHAIN_HINT_SIGN_NOTARIZE = 'HINT_SIGN_NOTARIZE';
|
|
const DATA_MIGRATION_ERROR_CODE = 'ERR_DATA_MIGRATION_FAILED';
|
|
const DATA_MIGRATION_HINT_RELOGIN = 'HINT_RELOGIN';
|
|
const DATA_MIGRATION_HINT_CLEAR_DATA = 'HINT_CLEAR_DATA';
|
|
|
|
const KEYCHAIN_HINT_I18N_MAP: Record<string, string> = {
|
|
[KEYCHAIN_HINT_TRANSLOCATION]: 'error.keychainHint.translocation',
|
|
[KEYCHAIN_HINT_KEYCHAIN_DENIED]: 'error.keychainHint.keychainDenied',
|
|
[KEYCHAIN_HINT_SIGN_NOTARIZE]: 'error.keychainHint.signNotarize',
|
|
};
|
|
|
|
const DATA_MIGRATION_HINT_I18N_MAP: Record<string, string> = {
|
|
[DATA_MIGRATION_HINT_RELOGIN]: 'error.dataMigrationHint.relogin',
|
|
[DATA_MIGRATION_HINT_CLEAR_DATA]: 'error.dataMigrationHint.clearData',
|
|
};
|
|
|
|
function resolveKeychainMessage(hintCode: string | undefined, t: TFunction): string {
|
|
const base = t('error.keychainUnavailable');
|
|
if (!hintCode) {
|
|
return base;
|
|
}
|
|
|
|
const hintKey = KEYCHAIN_HINT_I18N_MAP[hintCode];
|
|
if (!hintKey) {
|
|
return base;
|
|
}
|
|
|
|
return `${base} ${t(hintKey)}`;
|
|
}
|
|
|
|
function resolveDataMigrationMessage(hintCode: string | undefined, t: TFunction): string {
|
|
const base = t('error.dataMigrationFailed');
|
|
if (!hintCode) {
|
|
return base;
|
|
}
|
|
|
|
const hintKey = DATA_MIGRATION_HINT_I18N_MAP[hintCode];
|
|
if (!hintKey) {
|
|
return base;
|
|
}
|
|
|
|
return `${base} ${t(hintKey)}`;
|
|
}
|
|
|
|
export function getLocalizedErrorMessage(error: unknown, t: TFunction): string {
|
|
if (error instanceof Error) {
|
|
const rawMessage = error.message;
|
|
const [code, hint] = rawMessage.split('|');
|
|
if (code === KEYCHAIN_ERROR_CODE) {
|
|
return resolveKeychainMessage(hint, t);
|
|
}
|
|
if (code === DATA_MIGRATION_ERROR_CODE) {
|
|
return resolveDataMigrationMessage(hint, t);
|
|
}
|
|
return rawMessage;
|
|
}
|
|
|
|
if (error && typeof error === 'object' && 'message' in error) {
|
|
const rawMessage = String((error as { message?: unknown }).message ?? '');
|
|
const [code, hint] = rawMessage.split('|');
|
|
if (code === KEYCHAIN_ERROR_CODE) {
|
|
return resolveKeychainMessage(hint, t);
|
|
}
|
|
if (code === DATA_MIGRATION_ERROR_CODE) {
|
|
return resolveDataMigrationMessage(hint, t);
|
|
}
|
|
if (rawMessage) {
|
|
return rawMessage;
|
|
}
|
|
}
|
|
|
|
return String(error);
|
|
}
|