@tduncan / Cortana / commits / 291ab32

feat: implement secure storage service and add drift and shared_preferences dependencies

abdulmominsakib committed Apr 8, 2026 at 11:33 UTC 291ab3299c89574ace44e49cd449e5dd156d1da4
12 files changed +278 -17
README.md
+1
@@ -5,6 +5,7 @@
5 LocalMind is a premium Flutter mobile application designed to provide a beautiful, fast, and privacy-respecting interface for local LLM servers and cloud providers.
6
7 ![LocalMind Header Image](docs/cover.webp)
8 +![LocalMind Screenshot](docs/app_screenshots.webp)
9
10 ## 🚀 Vision
11
docs/app_screenshots.webp
Binary files /dev/null and b/docs/app_screenshots.webp differ
ios/Podfile.lock
+6
@@ -34,6 +34,8 @@ PODS:
34 - DKImagePickerController/PhotoGallery
35 - Flutter
36 - Flutter (1.0.0)
37 + - flutter_secure_storage (6.0.0):
38 + - Flutter
39 - flutter_tts (0.0.1):
40 - Flutter
41 - haptic_feedback (0.5.1):
@@ -46,6 +48,7 @@ PODS:
48 DEPENDENCIES:
49 - file_picker (from `.symlinks/plugins/file_picker/ios`)
50 - Flutter (from `Flutter`)
51 + - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
52 - flutter_tts (from `.symlinks/plugins/flutter_tts/ios`)
53 - haptic_feedback (from `.symlinks/plugins/haptic_feedback/ios`)
54
@@ -61,6 +64,8 @@ EXTERNAL SOURCES:
64 :path: ".symlinks/plugins/file_picker/ios"
65 Flutter:
66 :path: Flutter
67 + flutter_secure_storage:
68 + :path: ".symlinks/plugins/flutter_secure_storage/ios"
69 flutter_tts:
70 :path: ".symlinks/plugins/flutter_tts/ios"
71 haptic_feedback:
@@ -71,6 +76,7 @@ SPEC CHECKSUMS:
76 DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
77 file_picker: b159e0c068aef54932bb15dc9fd1571818edaf49
78 Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
79 + flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12
80 flutter_tts: 91ad1884033cd95ef6e9338754adad54f01f090f
81 haptic_feedback: a366a27ec760f0e865651605b95de8a9bb04ef01
82 SDWebImage: e9fc87c1aab89a8ab1bbd74eba378c6f53be8abf
lib/core/providers/storage_providers.dart
+3 -6
@@ -20,24 +20,21 @@ final conversationsBoxProvider = Provider<Box<Conversation>>((ref) {
20 return boxes.conversations;
21 });
22
23 -final conversationsProvider = Provider<List<Conversation>>((ref) {
23 +final directBoxConversationsProvider = Provider<List<Conversation>>((ref) {
24 final box = ref.watch(conversationsBoxProvider);
25 final conversations = box.values.toList();
26 conversations.sort((a, b) => b.updatedAt.compareTo(a.updatedAt));
27 return conversations;
28 });
29
30 -final recentConversationsProvider = Provider<List<Conversation>>((ref) {
31 - final all = ref.watch(conversationsProvider);
32 - return all.take(3).toList();
33 -});
30 +
31
32 final personasBoxProvider = Provider<Box<Persona>>((ref) {
33 final boxes = ref.watch(hiveBoxesProvider);
34 return boxes.personas;
35 });
36
40 -final personasProvider = Provider<List<Persona>>((ref) {
37 +final directBoxPersonasProvider = Provider<List<Persona>>((ref) {
38 final box = ref.watch(personasBoxProvider);
39 return box.values.toList();
40 });
lib/core/storage/hive_initializer.dart
+24 -5
@@ -7,6 +7,7 @@ import 'package:localmind/features/personas/data/models/persona.dart';
7 import 'package:localmind/features/settings/data/models/app_settings.dart';
8 import 'package:localmind/core/models/enums.dart';
9 import 'hive_keys.dart';
10 +import 'secure_storage_service.dart';
11
12 class HiveBoxes {
13 final Box<Server> servers;
@@ -41,13 +42,17 @@ class HiveInitializer {
42 Hive.registerAdapter(ThemeModeAdapter());
43 Hive.registerAdapter(SyntaxThemeNameAdapter());
44
44 - final servers = await Hive.openBox<Server>(HiveKeys.servers);
45 - final messages = await Hive.openBox<Message>(HiveKeys.messages);
46 - final conversations = await Hive.openBox<Conversation>(
45 + final encryptionKey = await SecureStorageService.getOrGenerateKey();
46 + final cipher = HiveAesCipher(encryptionKey);
47 +
48 + final servers = await _openEncryptedBox<Server>(HiveKeys.servers, cipher);
49 + final messages = await _openEncryptedBox<Message>(HiveKeys.messages, cipher);
50 + final conversations = await _openEncryptedBox<Conversation>(
51 HiveKeys.conversations,
52 + cipher,
53 );
49 - final personas = await Hive.openBox<Persona>(HiveKeys.personas);
50 - final settings = await Hive.openBox<dynamic>(HiveKeys.settings);
54 + final personas = await _openEncryptedBox<Persona>(HiveKeys.personas, cipher);
55 + final settings = await _openEncryptedBox<dynamic>(HiveKeys.settings, cipher);
56
57 return HiveBoxes(
58 servers: servers,
@@ -57,4 +62,18 @@ class HiveInitializer {
62 settings: settings,
63 );
64 }
65 +
66 + static Future<Box<T>> _openEncryptedBox<T>(
67 + String name,
68 + HiveAesCipher cipher,
69 + ) async {
70 + try {
71 + return await Hive.openBox<T>(name, encryptionCipher: cipher);
72 + } catch (e) {
73 + // If opening fails (likely due to encryption mismatch),
74 + // delete the box and start fresh.
75 + await Hive.deleteBoxFromDisk(name);
76 + return await Hive.openBox<T>(name, encryptionCipher: cipher);
77 + }
78 + }
79 }
lib/core/storage/secure_storage_service.dart new
+32
@@ -0,0 +1,32 @@
1 +import 'dart:convert';
2 +import 'dart:typed_data';
3 +import 'package:flutter_secure_storage/flutter_secure_storage.dart';
4 +import 'package:hive_ce/hive.dart';
5 +
6 +class SecureStorageService {
7 + static const _storage = FlutterSecureStorage();
8 + static const _keyName = 'hive_encryption_key_v1';
9 +
10 + /// Returns the encryption key as a Uint8List.
11 + /// If no key exists, it generates a new one and saves it.
12 + static Future<Uint8List> getOrGenerateKey() async {
13 + final encryptionKeyString = await _storage.read(key: _keyName);
14 +
15 + if (encryptionKeyString == null) {
16 + final key = Hive.generateSecureKey();
17 + await _storage.write(
18 + key: _keyName,
19 + value: base64UrlEncode(key),
20 + );
21 + return Uint8List.fromList(key);
22 + }
23 +
24 + return base64Url.decode(encryptionKeyString);
25 + }
26 +
27 + /// Clears the encryption key.
28 + /// CAUTION: This will make all existing encrypted data unreadable.
29 + static Future<void> clearKey() async {
30 + await _storage.delete(key: _keyName);
31 + }
32 +}
lib/features/chat/providers/chat_providers.dart
+12 -1
@@ -727,10 +727,21 @@ class ChatNotifier extends Notifier<ChatState> {
727 .renameConversation(_currentConversationId!, title);
728 }
729
730 - void cancelStream() {
730 + Future<void> cancelStream() async {
731 _streamSubscription?.cancel();
732 _streamSubscription = null;
733 ref.read(chatServiceProvider).cancelStream();
734 +
735 + final streamingMessage = state.streamingMessage;
736 + if (streamingMessage != null) {
737 + final boxes = ref.read(hiveBoxesProvider);
738 + final finalMessage = streamingMessage.copyWith(
739 + status: MessageStatus.complete,
740 + isProcessing: false,
741 + );
742 + await boxes.messages.put(finalMessage.id, finalMessage);
743 + }
744 +
745 state = state.copyWith(isStreaming: false, clearStreaming: true);
746 }
747
lib/features/chat/views/chat_screen.dart
+1 -1
@@ -218,7 +218,7 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
218 ref.read(chatProvider.notifier).sendMessage(prompt),
219 quickPrompts: _quickPrompts,
220 recentConversations: ref.watch(
221 - recentConversationsProvider,
221 + conv.recentConversationsProvider,
222 ),
223 onSeeAll: () {
224 if (Scaffold.maybeOf(context)?.hasDrawer ?? false) {
lib/features/conversations/providers/conversation_providers.dart
+5
@@ -229,6 +229,11 @@ final filteredConversationsProvider = Provider<List<Conversation>>((ref) {
229 }).toList();
230 });
231
232 +final recentConversationsProvider = Provider<List<Conversation>>((ref) {
233 + final all = ref.watch(conversationsProvider);
234 + return all.take(3).toList();
235 +});
236 +
237 final groupedConversationsProvider = Provider<Map<String, List<Conversation>>>((
238 ref,
239 ) {
lib/features/settings/screens/settings_screen.dart
+2 -1
@@ -7,6 +7,7 @@ import 'package:localmind/core/theme/app_theme.dart';
7 import 'package:localmind/features/conversations/providers/conversation_providers.dart';
8 import 'package:localmind/features/servers/providers/server_providers.dart';
9 import 'package:localmind/features/settings/data/models/app_settings.dart';
10 +import 'package:localmind/features/personas/providers/personas_providers.dart';
11
12 class SettingsScreen extends ConsumerWidget {
13 const SettingsScreen({super.key});
@@ -161,7 +162,7 @@ class SettingsScreen extends ConsumerWidget {
162 label: 'Default Persona',
163 currentValue: settings.defaultPersonaId,
164 items: ref
164 - .watch(storage.personasProvider)
165 + .watch(personasNotifierProvider)
166 .map((p) => (p.id, '${p.emoji} ${p.name}'))
167 .toList(),
168 onChanged: (v) =>
pubspec.lock
+186 -2
@@ -121,6 +121,14 @@ packages:
121 url: "https://pub.dev"
122 source: hosted
123 version: "1.4.1"
124 + charcode:
125 + dependency: transitive
126 + description:
127 + name: charcode
128 + sha256: fb0f1107cac15a5ea6ef0a6ef71a807b9e4267c713bb93e00e92d737cc8dbd8a
129 + url: "https://pub.dev"
130 + source: hosted
131 + version: "1.4.0"
132 checked_yaml:
133 dependency: transitive
134 description:
@@ -137,6 +145,14 @@ packages:
145 url: "https://pub.dev"
146 source: hosted
147 version: "0.2.0"
148 + cli_util:
149 + dependency: transitive
150 + description:
151 + name: cli_util
152 + sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c
153 + url: "https://pub.dev"
154 + source: hosted
155 + version: "0.4.2"
156 clock:
157 dependency: transitive
158 description:
@@ -241,6 +257,30 @@ packages:
257 url: "https://pub.dev"
258 source: hosted
259 version: "2.1.2"
260 + drift:
261 + dependency: "direct main"
262 + description:
263 + name: drift
264 + sha256: "055c249d1f91be5a47fe447f88afc24c4ca6f4cd6c5ed66767b4797d48acc2e5"
265 + url: "https://pub.dev"
266 + source: hosted
267 + version: "2.32.1"
268 + drift_dev:
269 + dependency: "direct dev"
270 + description:
271 + name: drift_dev
272 + sha256: "88a9de3af8571518148a6d8a513b57779fd1e60a026d3ab8a481a878fba01d91"
273 + url: "https://pub.dev"
274 + source: hosted
275 + version: "2.32.1"
276 + drift_flutter:
277 + dependency: "direct main"
278 + description:
279 + name: drift_flutter
280 + sha256: "887fdec622174dc7eaefd0048403e34ee07cc18626ac8a7544cc3b8a4a172166"
281 + url: "https://pub.dev"
282 + source: hosted
283 + version: "0.3.0"
284 extended_image:
285 dependency: transitive
286 description:
@@ -347,6 +387,54 @@ packages:
387 url: "https://pub.dev"
388 source: hosted
389 version: "3.3.1"
390 + flutter_secure_storage:
391 + dependency: "direct main"
392 + description:
393 + name: flutter_secure_storage
394 + sha256: "9cad52d75ebc511adfae3d447d5d13da15a55a92c9410e50f67335b6d21d16ea"
395 + url: "https://pub.dev"
396 + source: hosted
397 + version: "9.2.4"
398 + flutter_secure_storage_linux:
399 + dependency: transitive
400 + description:
401 + name: flutter_secure_storage_linux
402 + sha256: be76c1d24a97d0b98f8b54bce6b481a380a6590df992d0098f868ad54dc8f688
403 + url: "https://pub.dev"
404 + source: hosted
405 + version: "1.2.3"
406 + flutter_secure_storage_macos:
407 + dependency: transitive
408 + description:
409 + name: flutter_secure_storage_macos
410 + sha256: "6c0a2795a2d1de26ae202a0d78527d163f4acbb11cde4c75c670f3a0fc064247"
411 + url: "https://pub.dev"
412 + source: hosted
413 + version: "3.1.3"
414 + flutter_secure_storage_platform_interface:
415 + dependency: transitive
416 + description:
417 + name: flutter_secure_storage_platform_interface
418 + sha256: cf91ad32ce5adef6fba4d736a542baca9daf3beac4db2d04be350b87f69ac4a8
419 + url: "https://pub.dev"
420 + source: hosted
421 + version: "1.1.2"
422 + flutter_secure_storage_web:
423 + dependency: transitive
424 + description:
425 + name: flutter_secure_storage_web
426 + sha256: f4ebff989b4f07b2656fb16b47852c0aab9fed9b4ec1c70103368337bc1886a9
427 + url: "https://pub.dev"
428 + source: hosted
429 + version: "1.2.1"
430 + flutter_secure_storage_windows:
431 + dependency: transitive
432 + description:
433 + name: flutter_secure_storage_windows
434 + sha256: b20b07cb5ed4ed74fc567b78a72936203f587eba460af1df11281c9326cd3709
435 + url: "https://pub.dev"
436 + source: hosted
437 + version: "3.1.2"
438 flutter_shaders:
439 dependency: transitive
440 description:
@@ -537,10 +625,10 @@ packages:
625 dependency: transitive
626 description:
627 name: js
540 - sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc"
628 + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
629 url: "https://pub.dev"
630 source: hosted
543 - version: "0.7.2"
631 + version: "0.6.7"
632 json_annotation:
633 dependency: transitive
634 description:
@@ -789,6 +877,14 @@ packages:
877 url: "https://pub.dev"
878 source: hosted
879 version: "1.5.0"
880 + recase:
881 + dependency: transitive
882 + description:
883 + name: recase
884 + sha256: e4eb4ec2dcdee52dcf99cb4ceabaffc631d7424ee55e56f280bc039737f89213
885 + url: "https://pub.dev"
886 + source: hosted
887 + version: "4.1.0"
888 riverpod:
889 dependency: transitive
890 description:
@@ -813,6 +909,62 @@ packages:
909 url: "https://pub.dev"
910 source: hosted
911 version: "0.53.3"
912 + shared_preferences:
913 + dependency: "direct main"
914 + description:
915 + name: shared_preferences
916 + sha256: c3025c5534b01739267eb7d76959bbc25a6d10f6988e1c2a3036940133dd10bf
917 + url: "https://pub.dev"
918 + source: hosted
919 + version: "2.5.5"
920 + shared_preferences_android:
921 + dependency: transitive
922 + description:
923 + name: shared_preferences_android
924 + sha256: e8d4762b1e2e8578fc4d0fd548cebf24afd24f49719c08974df92834565e2c53
925 + url: "https://pub.dev"
926 + source: hosted
927 + version: "2.4.23"
928 + shared_preferences_foundation:
929 + dependency: transitive
930 + description:
931 + name: shared_preferences_foundation
932 + sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f"
933 + url: "https://pub.dev"
934 + source: hosted
935 + version: "2.5.6"
936 + shared_preferences_linux:
937 + dependency: transitive
938 + description:
939 + name: shared_preferences_linux
940 + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f"
941 + url: "https://pub.dev"
942 + source: hosted
943 + version: "2.4.1"
944 + shared_preferences_platform_interface:
945 + dependency: transitive
946 + description:
947 + name: shared_preferences_platform_interface
948 + sha256: "649dc798a33931919ea356c4305c2d1f81619ea6e92244070b520187b5140ef9"
949 + url: "https://pub.dev"
950 + source: hosted
951 + version: "2.4.2"
952 + shared_preferences_web:
953 + dependency: transitive
954 + description:
955 + name: shared_preferences_web
956 + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019
957 + url: "https://pub.dev"
958 + source: hosted
959 + version: "2.4.3"
960 + shared_preferences_windows:
961 + dependency: transitive
962 + description:
963 + name: shared_preferences_windows
964 + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1"
965 + url: "https://pub.dev"
966 + source: hosted
967 + version: "2.4.1"
968 shelf:
969 dependency: transitive
970 description:
@@ -906,6 +1058,38 @@ packages:
1058 url: "https://pub.dev"
1059 source: hosted
1060 version: "1.10.2"
1061 + sqlcipher_flutter_libs:
1062 + dependency: transitive
1063 + description:
1064 + name: sqlcipher_flutter_libs
1065 + sha256: "38d62d659d2fb8739bf25a42c9a350d1fdd6c29a5a61f13a946778ec75d27929"
1066 + url: "https://pub.dev"
1067 + source: hosted
1068 + version: "0.7.0+eol"
1069 + sqlite3:
1070 + dependency: transitive
1071 + description:
1072 + name: sqlite3
1073 + sha256: b3fe250b4ebd681a8ba20b12794c42b00c1375e5b51005568f0b8731265beb03
1074 + url: "https://pub.dev"
1075 + source: hosted
1076 + version: "3.3.0"
1077 + sqlite3_flutter_libs:
1078 + dependency: transitive
1079 + description:
1080 + name: sqlite3_flutter_libs
1081 + sha256: "3ed7553eee7bb368f8950f58ba29f634e06e813c029aff6a0d60862b96de8454"
1082 + url: "https://pub.dev"
1083 + source: hosted
1084 + version: "0.6.0+eol"
1085 + sqlparser:
1086 + dependency: transitive
1087 + description:
1088 + name: sqlparser
1089 + sha256: ab2b467425f1d4f3acfa5fd11a08226f7d6c26ff102c06be1807e1dff34e050b
1090 + url: "https://pub.dev"
1091 + source: hosted
1092 + version: "0.44.3"
1093 stack_trace:
1094 dependency: transitive
1095 description:
pubspec.yaml
+6 -1
@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
16 # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
17 # In Windows, build-name is used as the major, minor, and patch parts
18 # of the product and file versions while build-number is used as the build suffix.
19 -version: 1.0.0+5
19 +version: 1.0.0+6
20
21 environment:
22 sdk: ^3.11.0
@@ -40,6 +40,7 @@ dependencies:
40 hive_ce_flutter: ^2.3.4
41 shadcn_ui: ^0.53.3
42 dio: ^5.9.2
43 + flutter_secure_storage: ^9.2.3
44 flutter_markdown_plus: ^1.0.7
45 flutter_tts: ^4.2.5
46 file_picker: ^10.3.10
@@ -50,6 +51,9 @@ dependencies:
51 flutter_syntax_view: 4.1.7
52 animations: 2.1.2
53 logger: ^2.7.0
54 + drift: ^2.32.1
55 + drift_flutter: ^0.3.0
56 + shared_preferences: ^2.5.5
57
58 dev_dependencies:
59 flutter_test:
@@ -64,6 +68,7 @@ dev_dependencies:
68 build_runner: ^2.13.1
69 hive_ce_generator: ^1.11.1
70 change_app_package_name: ^1.4.0
71 + drift_dev: ^2.32.1
72
73 # For information on the generic Dart part of this file, see the
74 # following page: https://dart.dev/tools/pub/pubspec