diff --git a/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt b/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt index 16d197f6..080cd4d3 100644 --- a/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt +++ b/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt @@ -1931,7 +1931,9 @@ class MainActivity: FlutterFragmentActivity() { "initExtensionSystem" -> { val extensionsDir = call.argument("extensions_dir") ?: "" val dataDir = call.argument("data_dir") ?: "" + val masterKey = call.argument("master_key") ?: "" withContext(Dispatchers.IO) { + Gobackend.setExtensionStorageMasterKey(masterKey) Gobackend.initExtensionSystem(extensionsDir, dataDir) } result.success(null) diff --git a/go_backend/exports_extensions.go b/go_backend/exports_extensions.go index 90a31ca7..2001dd65 100644 --- a/go_backend/exports_extensions.go +++ b/go_backend/exports_extensions.go @@ -282,6 +282,9 @@ func getEnabledExtensionProviderMetadataResponse(providerID, resourceType, resou } func InitExtensionSystem(extensionsDir, dataDir string) error { + if !extensionStorageKeyConfigured() { + return fmt.Errorf("extension storage master key is not configured") + } manager := getExtensionManager() if err := manager.SetDirectories(extensionsDir, dataDir); err != nil { return err diff --git a/go_backend/extension_runtime_storage.go b/go_backend/extension_runtime_storage.go index 06b4d927..7c537355 100644 --- a/go_backend/extension_runtime_storage.go +++ b/go_backend/extension_runtime_storage.go @@ -316,6 +316,10 @@ func (r *extensionRuntime) getOrCreateSalt() ([]byte, error) { } func (r *extensionRuntime) getEncryptionKey() ([]byte, error) { + return deriveExtensionStorageKey(r.extensionID, "credentials") +} + +func (r *extensionRuntime) getLegacyEncryptionKey() ([]byte, error) { salt, err := r.getOrCreateSalt() if err != nil { return nil, err @@ -340,7 +344,23 @@ func (r *extensionRuntime) readCredentialsFileLocked() (map[string]any, error) { } decrypted, err := decryptAES(data, key) if err != nil { - return nil, fmt.Errorf("failed to decrypt credentials: %w", err) + legacyKey, legacyKeyErr := r.getLegacyEncryptionKey() + if legacyKeyErr != nil { + return nil, fmt.Errorf("failed to decrypt credentials: %w", err) + } + decrypted, legacyKeyErr = decryptAES(data, legacyKey) + if legacyKeyErr != nil { + return nil, fmt.Errorf("failed to decrypt credentials: %w", err) + } + // Transparently replace the legacy extension-id-derived ciphertext while + // the caller holds the per-file lock. + migrated, migrateErr := encryptAES(decrypted, key) + if migrateErr != nil { + return nil, fmt.Errorf("failed to migrate credentials: %w", migrateErr) + } + if migrateErr = writeExtensionFileLocked(r.getCredentialsPath(), migrated); migrateErr != nil { + return nil, fmt.Errorf("failed to migrate credentials: %w", migrateErr) + } } creds := make(map[string]any) if err := json.Unmarshal(decrypted, &creds); err != nil { diff --git a/go_backend/extension_runtime_supplement_test.go b/go_backend/extension_runtime_supplement_test.go index 3d0ed1f7..e0fadb7f 100644 --- a/go_backend/extension_runtime_supplement_test.go +++ b/go_backend/extension_runtime_supplement_test.go @@ -1,6 +1,7 @@ package gobackend import ( + "bytes" "encoding/json" "fmt" "io" @@ -661,6 +662,23 @@ func TestExtensionStoreSettingsAndRuntimeStorage(t *testing.T) { if all := settingsStore.GetAll("ext"); all["a"] != float64(1) { t.Fatalf("settings all = %#v", all) } + settingsCiphertext, err := os.ReadFile(settingsStore.getSettingsPath("ext")) + if err != nil { + t.Fatalf("read encrypted settings: %v", err) + } + if bytes.Contains(settingsCiphertext, []byte("hidden")) || bytes.Contains(settingsCiphertext, []byte("quality")) { + t.Fatal("extension settings were stored as plaintext") + } + if _, err := os.Stat(settingsStore.getLegacySettingsPath("ext")); !os.IsNotExist(err) { + t.Fatalf("plaintext settings file still exists: %v", err) + } + reloadedWithData := &ExtensionSettingsStore{settings: map[string]map[string]any{}} + if err := reloadedWithData.SetDataDir(settingsStore.dataDir); err != nil { + t.Fatalf("reload encrypted settings: %v", err) + } + if reloadedWithData.GetAll("ext")["_secret"] != "hidden" { + t.Fatal("encrypted settings did not round-trip") + } if err := settingsStore.Remove("ext", "a"); err != nil { t.Fatalf("settings Remove: %v", err) } diff --git a/go_backend/extension_secret_key.go b/go_backend/extension_secret_key.go new file mode 100644 index 00000000..721be5a4 --- /dev/null +++ b/go_backend/extension_secret_key.go @@ -0,0 +1,54 @@ +package gobackend + +import ( + "crypto/hmac" + "crypto/sha256" + "encoding/base64" + "fmt" + "sync" +) + +const extensionStorageMasterKeyBytes = 32 + +var extensionStorageKeyState struct { + sync.RWMutex + key []byte +} + +// SetExtensionStorageMasterKey installs the platform-keystore-backed key used +// to encrypt extension settings and credentials. The key itself is never +// persisted by Go; Flutter stores it in Keychain/Android Keystore. +func SetExtensionStorageMasterKey(encodedKey string) error { + key, err := base64.StdEncoding.DecodeString(encodedKey) + if err != nil || len(key) != extensionStorageMasterKeyBytes { + return fmt.Errorf("extension storage master key must be 32 base64-encoded bytes") + } + extensionStorageKeyState.Lock() + extensionStorageKeyState.key = append(extensionStorageKeyState.key[:0], key...) + extensionStorageKeyState.Unlock() + return nil +} + +func extensionStorageKeyConfigured() bool { + extensionStorageKeyState.RLock() + configured := len(extensionStorageKeyState.key) == extensionStorageMasterKeyBytes + extensionStorageKeyState.RUnlock() + return configured +} + +func deriveExtensionStorageKey(extensionID, purpose string) ([]byte, error) { + extensionStorageKeyState.RLock() + if len(extensionStorageKeyState.key) != extensionStorageMasterKeyBytes { + extensionStorageKeyState.RUnlock() + return nil, fmt.Errorf("extension storage master key is not configured") + } + masterKey := append([]byte(nil), extensionStorageKeyState.key...) + extensionStorageKeyState.RUnlock() + + mac := hmac.New(sha256.New, masterKey) + _, _ = mac.Write([]byte("SpotiFLAC Mobile extension storage v2\x00")) + _, _ = mac.Write([]byte(purpose)) + _, _ = mac.Write([]byte{0}) + _, _ = mac.Write([]byte(extensionID)) + return mac.Sum(nil), nil +} diff --git a/go_backend/extension_settings.go b/go_backend/extension_settings.go index 2980712a..a1ba1cd7 100644 --- a/go_backend/extension_settings.go +++ b/go_backend/extension_settings.go @@ -31,9 +31,12 @@ func GetExtensionSettingsStore() *ExtensionSettingsStore { func (s *ExtensionSettingsStore) SetDataDir(dataDir string) error { s.mu.Lock() defer s.mu.Unlock() + if !extensionStorageKeyConfigured() { + return fmt.Errorf("extension storage master key is not configured") + } s.dataDir = dataDir - if err := os.MkdirAll(dataDir, 0755); err != nil { + if err := os.MkdirAll(dataDir, 0700); err != nil { return fmt.Errorf("failed to create settings directory: %w", err) } @@ -41,6 +44,10 @@ func (s *ExtensionSettingsStore) SetDataDir(dataDir string) error { } func (s *ExtensionSettingsStore) getSettingsPath(extensionID string) string { + return filepath.Join(s.dataDir, extensionID, "settings.enc") +} + +func (s *ExtensionSettingsStore) getLegacySettingsPath(extensionID string) string { return filepath.Join(s.dataDir, extensionID, "settings.json") } @@ -73,10 +80,18 @@ func (s *ExtensionSettingsStore) loadSettings(extensionID string) (map[string]an data, err := os.ReadFile(settingsPath) if err != nil { if os.IsNotExist(err) { - return make(map[string]any), nil + return s.migrateLegacySettings(extensionID) } return nil, err } + key, err := deriveExtensionStorageKey(extensionID, "settings") + if err != nil { + return nil, err + } + data, err = decryptAES(data, key) + if err != nil { + return nil, fmt.Errorf("failed to decrypt settings: %w", err) + } var settings map[string]any if err := json.Unmarshal(data, &settings); err != nil { @@ -86,20 +101,53 @@ func (s *ExtensionSettingsStore) loadSettings(extensionID string) (map[string]an return settings, nil } +func (s *ExtensionSettingsStore) migrateLegacySettings(extensionID string) (map[string]any, error) { + legacyPath := s.getLegacySettingsPath(extensionID) + data, err := os.ReadFile(legacyPath) + if err != nil { + if os.IsNotExist(err) { + return make(map[string]any), nil + } + return nil, err + } + settings := make(map[string]any) + if err := json.Unmarshal(data, &settings); err != nil { + return nil, fmt.Errorf("failed to read legacy settings: %w", err) + } + if err := s.saveSettings(extensionID, settings); err != nil { + return nil, fmt.Errorf("failed to encrypt legacy settings: %w", err) + } + if err := os.Remove(legacyPath); err != nil && !os.IsNotExist(err) { + return nil, fmt.Errorf("failed to remove legacy settings: %w", err) + } + return settings, nil +} + func (s *ExtensionSettingsStore) saveSettings(extensionID string, settings map[string]any) error { settingsPath := s.getSettingsPath(extensionID) dir := filepath.Dir(settingsPath) - if err := os.MkdirAll(dir, 0755); err != nil { + if err := os.MkdirAll(dir, 0700); err != nil { return err } - data, err := json.MarshalIndent(settings, "", " ") + data, err := json.Marshal(settings) if err != nil { return err } - - return os.WriteFile(settingsPath, data, 0644) + key, err := deriveExtensionStorageKey(extensionID, "settings") + if err != nil { + return err + } + data, err = encryptAES(data, key) + if err != nil { + return err + } + fileMu := extensionFileMu(settingsPath) + fileMu.Lock() + err = writeExtensionFileLocked(settingsPath, data) + fileMu.Unlock() + return err } func (s *ExtensionSettingsStore) Get(extensionID, key string) (any, error) { @@ -180,6 +228,10 @@ func (s *ExtensionSettingsStore) RemoveAll(extensionID string) error { if err := os.Remove(settingsPath); err != nil && !os.IsNotExist(err) { return err } + legacyPath := s.getLegacySettingsPath(extensionID) + if err := os.Remove(legacyPath); err != nil && !os.IsNotExist(err) { + return fmt.Errorf("failed to remove legacy extension settings: %w", err) + } return nil } diff --git a/go_backend/security_test_setup_test.go b/go_backend/security_test_setup_test.go new file mode 100644 index 00000000..29065574 --- /dev/null +++ b/go_backend/security_test_setup_test.go @@ -0,0 +1,15 @@ +package gobackend + +import "encoding/base64" + +func init() { + // Production receives this key from platform secure storage before the + // extension system starts. Tests install a deterministic process-local key. + key := make([]byte, extensionStorageMasterKeyBytes) + for index := range key { + key[index] = byte(index + 1) + } + if err := SetExtensionStorageMasterKey(base64.StdEncoding.EncodeToString(key)); err != nil { + panic(err) + } +} diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index 3bc8e7c9..b5dda825 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -711,6 +711,9 @@ import Gobackend let args = call.arguments as! [String: Any] let extensionsDir = args["extensions_dir"] as! String let dataDir = args["data_dir"] as! String + let masterKey = args["master_key"] as! String + GobackendSetExtensionStorageMasterKey(masterKey, &error) + if let error = error { throw error } GobackendInitExtensionSystem(extensionsDir, dataDir, &error) if let error = error { throw error } return nil diff --git a/lib/main.dart b/lib/main.dart index 0590313e..4b36a9be 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -3,7 +3,6 @@ import 'dart:io'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:path_provider/path_provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:spotiflac_android/app.dart'; import 'package:spotiflac_android/models/settings.dart'; @@ -18,6 +17,7 @@ import 'package:spotiflac_android/services/platform_bridge.dart'; import 'package:spotiflac_android/services/share_intent_service.dart'; import 'package:spotiflac_android/services/cover_cache_manager.dart'; import 'package:spotiflac_android/services/app_state_database.dart'; +import 'package:spotiflac_android/services/extension_storage_service.dart'; import 'package:spotiflac_android/utils/local_library_scan_prefs.dart'; import 'package:spotiflac_android/utils/logger.dart'; @@ -395,16 +395,15 @@ class _EagerInitializationState extends ConsumerState<_EagerInitialization> Future _initializeExtensions() async { try { - final appDir = await getApplicationDocumentsDirectory(); - final extensionsDir = '${appDir.path}/extensions'; - final dataDir = '${appDir.path}/extension_data'; - - await Directory(extensionsDir).create(recursive: true); - await Directory(dataDir).create(recursive: true); + final storage = await ExtensionStorageService.prepare(); await ref .read(extensionProvider.notifier) - .initialize(extensionsDir, dataDir); + .initialize( + storage.extensionsDir, + storage.dataDir, + masterKey: storage.masterKey, + ); } catch (e) { debugPrint('Failed to initialize extensions: $e'); } diff --git a/lib/providers/extension_provider.dart b/lib/providers/extension_provider.dart index ccc65b8b..5c222bfe 100644 --- a/lib/providers/extension_provider.dart +++ b/lib/providers/extension_provider.dart @@ -67,7 +67,11 @@ class ExtensionNotifier extends Notifier { } } - Future initialize(String extensionsDir, String dataDir) async { + Future initialize( + String extensionsDir, + String dataDir, { + required String masterKey, + }) async { if (state.isInitialized) return; if (_initializationCompleter != null) { await _initializationCompleter!.future; @@ -100,7 +104,11 @@ class ExtensionNotifier extends Notifier { _log.w('Runtime state restore unavailable: $e'); } } - await PlatformBridge.initExtensionSystem(extensionsDir, dataDir); + await PlatformBridge.initExtensionSystem( + extensionsDir, + dataDir, + masterKey: masterKey, + ); await loadExtensions(extensionsDir); await loadProviderPriority(); await loadMetadataProviderPriority(); diff --git a/lib/screens/repo/extension_details_screen.dart b/lib/screens/repo/extension_details_screen.dart index 21702615..b40c0106 100644 --- a/lib/screens/repo/extension_details_screen.dart +++ b/lib/screens/repo/extension_details_screen.dart @@ -4,6 +4,7 @@ import 'package:path_provider/path_provider.dart'; import 'package:spotiflac_android/l10n/l10n.dart'; import 'package:spotiflac_android/providers/repo_provider.dart'; import 'package:spotiflac_android/providers/extension_provider.dart'; +import 'package:spotiflac_android/services/extension_storage_service.dart'; import 'package:spotiflac_android/utils/adaptive_layout.dart'; import 'package:spotiflac_android/utils/nav_bar_inset.dart'; @@ -531,12 +532,11 @@ class _ExtensionDetailsScreenState Future _installExtension(RepoExtension ext) async { final tempDir = await getTemporaryDirectory(); - final appDir = await getApplicationDocumentsDirectory(); - final extensionsDir = '${appDir.path}/extensions'; + final storage = await ExtensionStorageService.prepare(); final success = await ref .read(repoProvider.notifier) - .installExtension(ext.id, tempDir.path, extensionsDir); + .installExtension(ext.id, tempDir.path, storage.extensionsDir); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( diff --git a/lib/screens/repo_tab.dart b/lib/screens/repo_tab.dart index 273ff659..5f97de80 100644 --- a/lib/screens/repo_tab.dart +++ b/lib/screens/repo_tab.dart @@ -10,6 +10,7 @@ import 'package:spotiflac_android/providers/repo_provider.dart'; import 'package:spotiflac_android/widgets/settings_group.dart'; import 'package:spotiflac_android/widgets/animation_utils.dart'; import 'package:spotiflac_android/screens/repo/extension_details_screen.dart'; +import 'package:spotiflac_android/services/extension_storage_service.dart'; import 'package:spotiflac_android/utils/nav_bar_inset.dart'; class RepoTab extends ConsumerStatefulWidget { @@ -532,12 +533,11 @@ class _RepoTabState extends ConsumerState { Future _installExtension(RepoExtension ext) async { final tempDir = await getTemporaryDirectory(); - final appDir = await getApplicationDocumentsDirectory(); - final extensionsDir = '${appDir.path}/extensions'; + final storage = await ExtensionStorageService.prepare(); final success = await ref .read(repoProvider.notifier) - .installExtension(ext.id, tempDir.path, extensionsDir); + .installExtension(ext.id, tempDir.path, storage.extensionsDir); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( diff --git a/lib/screens/settings/extensions_page.dart b/lib/screens/settings/extensions_page.dart index 90054368..1f6f0c7d 100644 --- a/lib/screens/settings/extensions_page.dart +++ b/lib/screens/settings/extensions_page.dart @@ -4,7 +4,6 @@ import 'package:spotiflac_android/widgets/extension_row.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:file_picker/file_picker.dart'; import 'package:path/path.dart' as p; -import 'package:path_provider/path_provider.dart'; import 'package:spotiflac_android/l10n/l10n.dart'; import 'package:spotiflac_android/models/settings.dart'; import 'package:spotiflac_android/providers/extension_provider.dart'; @@ -14,6 +13,7 @@ import 'package:spotiflac_android/screens/settings/download_fallback_extensions_ import 'package:spotiflac_android/screens/settings/extension_detail_page.dart'; import 'package:spotiflac_android/screens/settings/metadata_provider_priority_page.dart'; import 'package:spotiflac_android/screens/settings/provider_priority_page.dart'; +import 'package:spotiflac_android/services/extension_storage_service.dart'; import 'package:spotiflac_android/widgets/settings_group.dart'; import 'package:spotiflac_android/widgets/app_sliver_header.dart'; @@ -34,16 +34,14 @@ class _ExtensionsPageState extends ConsumerState { Future _initializeExtensions() async { final extState = ref.read(extensionProvider); if (!extState.isInitialized) { - final appDir = await getApplicationDocumentsDirectory(); - final extensionsDir = '${appDir.path}/extensions'; - final dataDir = '${appDir.path}/extension_data'; - - await Directory(extensionsDir).create(recursive: true); - await Directory(dataDir).create(recursive: true); - + final storage = await ExtensionStorageService.prepare(); await ref .read(extensionProvider.notifier) - .initialize(extensionsDir, dataDir); + .initialize( + storage.extensionsDir, + storage.dataDir, + masterKey: storage.masterKey, + ); } else { ref.read(extensionProvider.notifier).refreshEnabledExtensionHealth(); } diff --git a/lib/services/extension_storage_service.dart b/lib/services/extension_storage_service.dart new file mode 100644 index 00000000..c1b33cc5 --- /dev/null +++ b/lib/services/extension_storage_service.dart @@ -0,0 +1,155 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:math'; + +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:path/path.dart' as p; +import 'package:path_provider/path_provider.dart'; + +const _extensionMasterKeyName = 'extension_storage_master_key_v2'; + +class ExtensionStoragePaths { + const ExtensionStoragePaths({ + required this.extensionsDir, + required this.dataDir, + required this.masterKey, + }); + + final String extensionsDir; + final String dataDir; + final String masterKey; +} + +class ExtensionStorageService { + ExtensionStorageService._(); + + static const FlutterSecureStorage _secureStorage = FlutterSecureStorage(); + static Future? _preparing; + + static Future prepare() { + return _preparing ??= _prepare(); + } + + static Future _prepare() async { + final supportDir = await getApplicationSupportDirectory(); + final documentsDir = await getApplicationDocumentsDirectory(); + final extensionsDir = Directory(p.join(supportDir.path, 'extensions')); + final dataDir = Directory(p.join(supportDir.path, 'extension_data')); + + await _migrateDirectory( + Directory(p.join(documentsDir.path, 'extensions')), + extensionsDir, + ); + await _migrateDirectory( + Directory(p.join(documentsDir.path, 'extension_data')), + dataDir, + ); + await extensionsDir.create(recursive: true); + await dataDir.create(recursive: true); + + return ExtensionStoragePaths( + extensionsDir: extensionsDir.path, + dataDir: dataDir.path, + masterKey: await _loadOrCreateMasterKey(), + ); + } + + static Future _loadOrCreateMasterKey() async { + final existing = await _secureStorage.read(key: _extensionMasterKeyName); + if (existing != null) { + try { + if (base64Decode(existing).length == 32) return existing; + } on FormatException { + // Replace malformed legacy data with a fresh keystore-backed key. + } + } + + final random = Random.secure(); + final key = List.generate(32, (_) => random.nextInt(256)); + final encoded = base64Encode(key); + await _secureStorage.write(key: _extensionMasterKeyName, value: encoded); + return encoded; + } + + static Future _migrateDirectory( + Directory source, + Directory destination, + ) async { + if (p.equals(source.path, destination.path)) { + return; + } + final sourceType = await FileSystemEntity.type( + source.path, + followLinks: false, + ); + if (sourceType == FileSystemEntityType.notFound) return; + if (sourceType == FileSystemEntityType.link) { + await Link(source.path).delete(); + return; + } + if (sourceType != FileSystemEntityType.directory) { + throw FileSystemException( + 'Extension storage path is not a directory', + source.path, + ); + } + if (await FileSystemEntity.type(destination.path, followLinks: false) == + FileSystemEntityType.link) { + throw FileSystemException( + 'Refusing extension storage symlink', + destination.path, + ); + } + + await destination.create(recursive: true); + await for (final entity in source.list(followLinks: false)) { + final targetPath = p.join(destination.path, p.basename(entity.path)); + if (entity is Directory) { + await _migrateDirectory(entity, Directory(targetPath)); + } else if (entity is File) { + final target = File(targetPath); + await target.parent.create(recursive: true); + if (await target.exists()) { + final sourceModified = await entity.lastModified(); + final targetModified = await target.lastModified(); + if (!sourceModified.isAfter(targetModified)) { + await entity.delete(); + continue; + } + } + await _copyFileAtomically(entity, target); + await entity.delete(); + } else if (entity is Link) { + // Extension storage must never preserve links into another sandbox. + await entity.delete(); + } + } + if (await source.exists() && await source.list().isEmpty) { + await source.delete(); + } + } + + static Future _copyFileAtomically(File source, File target) async { + final nonce = Random.secure().nextInt(0x7fffffff).toRadixString(16); + final temporary = File('${target.path}.migration-$nonce.tmp'); + try { + await source.openRead().pipe(temporary.openWrite()); + final sourceLength = await source.length(); + if (await temporary.length() != sourceLength) { + throw const FileSystemException('Incomplete extension storage copy'); + } + try { + await temporary.rename(target.path); + } on FileSystemException { + // Windows cannot atomically replace an existing file. Mobile platforms + // take the first branch; this fallback keeps development migrations + // functional while retaining the source until replacement succeeds. + if (!await target.exists()) rethrow; + await target.delete(); + await temporary.rename(target.path); + } + } finally { + if (await temporary.exists()) await temporary.delete(); + } + } +} diff --git a/lib/services/platform_bridge.dart b/lib/services/platform_bridge.dart index f1286430..88b2b359 100644 --- a/lib/services/platform_bridge.dart +++ b/lib/services/platform_bridge.dart @@ -1446,12 +1446,14 @@ class PlatformBridge { static Future initExtensionSystem( String extensionsDir, - String dataDir, - ) async { + String dataDir, { + required String masterKey, + }) async { _log.d('initExtensionSystem: $extensionsDir, $dataDir'); await _channel.invokeMethod('initExtensionSystem', { 'extensions_dir': extensionsDir, 'data_dir': dataDir, + 'master_key': masterKey, }); }