docs/long-term-plans/secure-storage.md
Status: Planned
Implement platform-specific secure storage for all sync provider credentials (SuperSync, WebDAV, Dropbox) with automatic silent migration from plaintext IndexedDB storage.
Confidence: 85%
interface SecureStorage {
set(key: string, value: string): Promise<void>;
get(key: string): Promise<string | null>;
remove(key: string): Promise<void>;
isAvailable(): Promise<boolean>;
}
| Platform | Mechanism | Security Level |
|---|---|---|
| Electron | safeStorage API (OS keychain) | High |
| Android | EncryptedSharedPreferences (Keystore) | High |
| Web | WebCrypto with non-exportable key | Medium |
New files:
src/app/core/secure-storage/secure-storage.interface.ts - Interface definitionsrc/app/core/secure-storage/secure-storage.service.ts - Angular service with platform detection// secure-storage.service.ts
@Injectable({ providedIn: 'root' })
export class SecureStorageService implements SecureStorage {
private _impl: SecureStorage;
constructor() {
if (IS_ELECTRON) {
this._impl = new ElectronSecureStorage();
} else if (IS_ANDROID_WEB_VIEW) {
this._impl = new AndroidSecureStorage();
} else {
this._impl = new WebSecureStorage();
}
}
}
New files:
electron/secure-storage.ts - Main process safeStorage handlerssrc/app/core/secure-storage/electron-secure-storage.ts - Renderer implementationModified files:
electron/shared-with-frontend/ipc-events.const.ts - Add IPC events:
SECURE_STORAGE_SET = 'SECURE_STORAGE_SET',
SECURE_STORAGE_GET = 'SECURE_STORAGE_GET',
SECURE_STORAGE_REMOVE = 'SECURE_STORAGE_REMOVE',
SECURE_STORAGE_IS_AVAILABLE = 'SECURE_STORAGE_IS_AVAILABLE',
electron/preload.ts - Add methods to ElectronAPIelectron/electronAPI.d.ts - Type definitionselectron/ipc-handler.ts - Register handlerssrc/app/core/window-ea.d.ts - Frontend typesMain process handler pattern:
// electron/secure-storage.ts
import { safeStorage } from 'electron';
import * as fs from 'fs/promises';
const SECURE_FILE = path.join(app.getPath('userData'), 'secureCredentials.enc');
export async function secureStorageSet(key: string, value: string): Promise<void> {
if (!safeStorage.isEncryptionAvailable()) throw new Error('Not available');
const existing = await loadFile();
existing[key] = safeStorage.encryptString(value).toString('base64');
await fs.writeFile(SECURE_FILE, JSON.stringify(existing));
}
New files:
android/app/src/main/java/com/superproductivity/superproductivity/plugins/SecureStoragePlugin.ktsrc/app/core/secure-storage/android-secure-storage.tsModified files:
android/app/build.gradle - Add dependency:
implementation "androidx.security:security-crypto:1.1.0-alpha06"
android/app/src/main/java/.../CapacitorMainActivity.kt - Register pluginKotlin implementation:
@CapacitorPlugin(name = "SecureStorage")
class SecureStoragePlugin : Plugin() {
private lateinit var encryptedPrefs: SharedPreferences
override fun load() {
val masterKey = MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
encryptedPrefs = EncryptedSharedPreferences.create(...)
}
@PluginMethod fun set(call: PluginCall) { ... }
@PluginMethod fun get(call: PluginCall) { ... }
}
New files:
src/app/core/secure-storage/web-secure-storage.tssrc/app/core/secure-storage/web-crypto-key-manager.tsApproach: Generate non-exportable AES-256-GCM key stored in IndexedDB. Encrypt credentials before storing.
// web-crypto-key-manager.ts
async getOrCreateKey(): Promise<CryptoKey> {
const existing = await this.loadKey();
if (existing) return existing;
return crypto.subtle.generateKey(
{ name: 'AES-GCM', length: 256 },
false, // non-extractable - key cannot be exported
['encrypt', 'decrypt']
);
}
Note: Web has weaker security (XSS can still access keys). This provides defense-in-depth, not absolute protection.
New file:
src/app/core/secure-storage/credential-migration.service.tsModified files:
src/app/core/startup/startup.service.ts - Call migration in init()src/app/sync/providers/private-cfg-store.ts - Add clear() methodMigration flow:
App Start → migrateIfNeeded()
│
├─ Check localStorage flag 'secure_storage_migration_v1'
│ └─ If set → skip (already migrated)
│
├─ For each provider (SuperSync, WebDAV, Dropbox):
│ ├─ Check secure storage → if exists, skip
│ ├─ Load from plaintext IndexedDB
│ ├─ Encrypt and save to secure storage
│ └─ Delete from plaintext IndexedDB
│
└─ Set migration flag
Modified files:
src/app/pfapi/api/pfapi.ts - Use SecureStorage for provider credentialssrc/app/imex/sync/sync-config.service.ts - Route credential saves through SecureStorageStorage keys:
SECURE_CRED_SuperSync - SuperSync tokensSECURE_CRED_WebDAV - WebDAV credentialsSECURE_CRED_Dropbox - Dropbox tokens| Scenario | Handling |
|---|---|
| Decryption fails (different machine) | Clear corrupted entry, prompt re-auth |
| Secure storage unavailable (Linux no keyring) | Fall back to plaintext with warning |
| Migration fails mid-way | Idempotent - retry on next startup |
async get(key: string): Promise<string | null> {
try {
return await this._getInternal(key);
} catch (error) {
PFLog.err('Decryption failed', { key, error });
await this.remove(key).catch(() => {});
return null; // Triggers re-authentication
}
}
| Path | Purpose |
|---|---|
src/app/core/secure-storage/secure-storage.interface.ts | Interface |
src/app/core/secure-storage/secure-storage.service.ts | Platform router |
src/app/core/secure-storage/electron-secure-storage.ts | Electron impl |
src/app/core/secure-storage/android-secure-storage.ts | Android impl |
src/app/core/secure-storage/web-secure-storage.ts | Web impl |
src/app/core/secure-storage/web-crypto-key-manager.ts | Web key mgmt |
src/app/core/secure-storage/credential-migration.service.ts | Migration |
electron/secure-storage.ts | Main process |
electron/ipc-handlers/secure-storage.ts | IPC registration |
android/.../plugins/SecureStoragePlugin.kt | Android plugin |
src/app/core/secure-storage/index.ts | Barrel export |
| Path | Changes |
|---|---|
electron/shared-with-frontend/ipc-events.const.ts | Add 4 IPC events |
electron/preload.ts | Add 4 methods |
electron/electronAPI.d.ts | Type definitions |
electron/ipc-handler.ts | Register handlers |
src/app/core/window-ea.d.ts | Frontend types |
src/app/core/startup/startup.service.ts | Trigger migration |
src/app/sync/providers/private-cfg-store.ts | Add clear() |
android/app/build.gradle | Add security-crypto |
android/.../CapacitorMainActivity.kt | Register plugin |
| Risk | Mitigation |
|---|---|
| Linux without Secret Service | Fallback to plaintext + user warning |
| Migration corrupts credentials | Atomic operations, idempotent retry |
| Web XSS can still access keys | CSP hardening, defense-in-depth only |