mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-01-23 02:36:05 +00:00
- Remove dead test:shard:pfapi script from package.json - Update AGENTS.md persistence layer path to op-log and sync - Update documentation file paths in secure-storage.md, vector-clocks.md, and quick-reference.md
9 KiB
9 KiB
Secure Credential Storage Implementation Plan
Overview
Implement platform-specific secure storage for all sync provider credentials (SuperSync, WebDAV, Dropbox) with automatic silent migration from plaintext IndexedDB storage.
Confidence: 85%
Architecture
Unified Interface
interface SecureStorage {
set(key: string, value: string): Promise<void>;
get(key: string): Promise<string | null>;
remove(key: string): Promise<void>;
isAvailable(): Promise<boolean>;
}
Platform Implementations
| Platform | Mechanism | Security Level |
|---|---|---|
| Electron | safeStorage API (OS keychain) |
High |
| Android | EncryptedSharedPreferences (Keystore) |
High |
| Web | WebCrypto with non-exportable key | Medium |
Implementation Steps
Step 1: Core Interface & Service
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();
}
}
}
Step 2: Electron Implementation
New files:
electron/secure-storage.ts- Main process safeStorage handlerssrc/app/core/secure-storage/electron-secure-storage.ts- Renderer implementation
Modified 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 types
Main 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));
}
Step 3: Android Implementation
New files:
android/app/src/main/java/com/superproductivity/superproductivity/plugins/SecureStoragePlugin.ktsrc/app/core/secure-storage/android-secure-storage.ts
Modified files:
android/app/build.gradle- Add dependency:implementation "androidx.security:security-crypto:1.1.0-alpha06"android/app/src/main/java/.../CapacitorMainActivity.kt- Register plugin
Kotlin 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) { ... }
}
Step 4: Web Implementation
New files:
src/app/core/secure-storage/web-secure-storage.tssrc/app/core/secure-storage/web-crypto-key-manager.ts
Approach: 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.
Step 5: Migration Service
New file:
src/app/core/secure-storage/credential-migration.service.ts
Modified files:
src/app/core/startup/startup.service.ts- Call migration ininit()src/app/sync/providers/private-cfg-store.ts- Addclear()method
Migration 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
Step 6: Integration
Modified files:
src/app/pfapi/api/pfapi.ts- Use SecureStorage for provider credentialssrc/app/imex/sync/sync-config.service.ts- Route credential saves through SecureStorage
Storage keys:
SECURE_CRED_SuperSync- SuperSync tokensSECURE_CRED_WebDAV- WebDAV credentialsSECURE_CRED_Dropbox- Dropbox tokens
Error Handling
| 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
}
}
Files Summary
New Files (11)
| 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 |
Modified Files (9)
| 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 |
Risks & Mitigations
| 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 |
Testing Strategy
- Unit tests for each platform implementation
- Migration tests - fresh install, existing credentials, partial migration
- E2E tests - sync after migration works correctly
- Manual testing - each platform (Electron macOS/Windows/Linux, Android, Web)