Skip to content

Commit 5d57faf

Browse files
authored
feat(wallet): HD & offline private key export + unban pubkeys (#2982)
* chore: roll sdk for overhauled privkey export Roll SDK for overhauled privkey export in KDF branch `offline_key_export` and implement privkey methods into SDK * chore: roll SDK * wip: settings private key export * chore: roll sdk * chore: roll SDK * fix(ui): polish private key export * chore: roll SDK * fix(lint): fix lint warning * fix: minor private key fixes * feat(ui): Simplify/polish private key export * feat: pubkey unbanning * refactor: clean up pubkey unbanning * fix: remove duplicate pubkey unbanning message * fix(ui): Enforce max scaffold message width for unbanning * fix(ui): Remove redundant pubkey unban control Remove redundant pubkey unban control in the general settings page since it is also in the security settings page. * refactor: make password confirmation re-usable * fix(password): allow auto-fill for password confirmation * feat: password manager integration for password confirmation * chore: roll SDK for pubkey unban fix * chore: roll SDK * fix: include failed activations in privkey export * fix(ui): show tooltip for sparkline chart Display a tooltip for the sparkline chart so users can understand what the data represents, considering that we no longer display table headings. * chore: roll SDK Roll SDK for address generation errors * chore: delete unused types Delete HD-related types. Previous references have likely been migrated to the SDK types. TODO: Investigate if there are any other similar cases. * chore: roll SDK
1 parent b8bdf68 commit 5d57faf

31 files changed

+3036
-856
lines changed

assets/translations/en.json

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -334,7 +334,7 @@
334334
"viewSeedPhrase": "View seed phrase",
335335
"backupSeedPhrase": "Backup seed phrase",
336336
"seedOr": "OR",
337-
"seedDownload": "Download seed phrase file",
337+
"seedDownload": "Download seed phrase",
338338
"seedSaveAndRemember": "Save and remember",
339339
"seedIntroWarning": "This phrase is the main access to your\nassets, save and never share this phrase",
340340
"seedSettings": "Seed phrase",
@@ -517,6 +517,23 @@
517517
"logs": "Logs",
518518
"resetActivatedCoinsTitle": "Reset Activated Coins",
519519
"privateKeys": "Private Keys",
520+
"exportPrivateKeys": "Export Private Keys",
521+
"exportPrivateKeysDescription": "Export private keys for your activated coins to import into other wallets",
522+
"privateKeyExportTitle": "Private Keys Export",
523+
"privateKeyExportDescription": "Your private keys provide full control over your funds. Handle with extreme caution.",
524+
"showPrivateKeys": "Show Private Keys",
525+
"copyAllKeys": "Copy All Keys",
526+
"downloadAllKeys": "Download All Keys",
527+
"shareAllKeys": "Share All Keys",
528+
"confirmPrivateKeyBackup": "Confirm Private Key Backup",
529+
"confirmPrivateKeyBackupDescription": "Please confirm you have securely saved your private keys before proceeding.",
530+
"importantSecurityNotice": "Important Security Notice",
531+
"privateKeySecurityWarning": "Your private keys provide complete control over your funds. Keep them secure and never share them with anyone. Store them in a safe place offline.",
532+
"privateKeyBackupConfirmation": "I have securely saved my private keys in a safe location and understand that anyone with access to these keys can control my funds.",
533+
"confirmBackupComplete": "Confirm Backup Complete",
534+
"privateKeyExportSuccessTitle": "Private Keys Exported Successfully",
535+
"privateKeyExportSuccessDescription": "Your private keys have been exported and confirmed as backed up. Keep them secure and never share them with anyone.",
536+
"iHaveSavedMyPrivateKeys": "I've Saved My Private Keys",
520537
"copyWarning": "Your clipboard isn't a safe place for your private key! Copying the seed phrase or private keys can make them vulnerable to clipboard hacks. Please handle with caution and only copy if absolutely necessary.",
521538
"seedConfirmTitle": "Let's double check your seed phrase",
522539
"seedConfirmDescription": "Your seed phrase is the only way to access Your funds. That's why we want to ensure you saved it safely. Please input your seed phrase into text filed below.",
@@ -682,6 +699,7 @@
682699
"amountFieldCheckboxListTile": "Send maximum amount",
683700
"customFeeToggleTitle": "Custom fee",
684701
"priceChartCenterText": "Select an interval to load data",
702+
"priceHistorySparklineTooltip": "24-hour price trend chart",
685703
"statistics": "Statistics",
686704
"ibcTransferFieldTitle": "IBC Transfer",
687705
"ibcTransferFieldSubtitle": "Send to another Cosmos chain",
@@ -697,5 +715,26 @@
697715
"searchAddresses": "Search addresses",
698716
"trend7d": "7d trend",
699717
"tradingDisabledTooltip": "Trading features are currently disabled",
700-
"tradingDisabled": "Trading unavailable in your location"
701-
}
718+
"tradingDisabled": "Trading unavailable in your location",
719+
"unbanPubkeysResults": "Unban Pubkeys Results",
720+
"unbannedPubkeys": {
721+
"zero": "{} Unbanned Pubkeys",
722+
"one": "{} Unbanned Pubkey",
723+
"two": "{} Unbanned Pubkeys",
724+
"many": "{} Unbanned Pubkeys",
725+
"few": "{} Unbanned Pubkeys",
726+
"other": "{} Unbanned Pubkeys"
727+
},
728+
"stillBannedPubkeys": "Still Banned Pubkeys",
729+
"wereNotBannedPubkeys": "Were Not Banned Pubkeys",
730+
"reason": "Reason",
731+
"unbanPubkeys": "Unban Pubkeys",
732+
"unbanPubkeysDescription": "Unban public keys that were previously banned due to failed transactions or security concerns.",
733+
"noBannedPubkeys": "No banned pubkeys found",
734+
"unbanPubkeysFailed": "Failed to unban pubkeys",
735+
"privateKeyRetrievalFailed": "Failed to retrieve private keys. Please try again.",
736+
"fetchingPrivateKeysTitle": "Fetching Private Keys...",
737+
"fetchingPrivateKeysMessage": "Please wait while we securely fetch your private keys...",
738+
"pubkeyType": "Type",
739+
"securitySettings": "Security Settings"
740+
}
Lines changed: 221 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,70 @@
11
import 'dart:async';
22

33
import 'package:bloc/bloc.dart';
4+
import 'package:komodo_defi_sdk/komodo_defi_sdk.dart';
5+
import 'package:komodo_defi_rpc_methods/komodo_defi_rpc_methods.dart';
46
import 'package:web_dex/bloc/security_settings/security_settings_event.dart';
57
import 'package:web_dex/bloc/security_settings/security_settings_state.dart';
68

9+
/// BLoC for managing security settings flow and authentication.
10+
///
11+
/// **Security Architecture**: This BLoC follows a hybrid approach for maximum security:
12+
/// - **Non-sensitive operations** (authentication, loading states, navigation) are managed here
13+
/// - **Sensitive data** (actual private keys) are handled directly in the UI layer
14+
/// - This minimizes the lifetime and scope of sensitive data in memory
15+
///
16+
/// The BLoC authenticates users and manages the flow, but never stores private keys.
17+
/// Private key retrieval and storage happens in the UI layer after authentication succeeds.
718
class SecuritySettingsBloc
819
extends Bloc<SecuritySettingsEvent, SecuritySettingsState> {
9-
SecuritySettingsBloc(super.state) {
20+
/// Creates a new SecuritySettingsBloc.
21+
///
22+
/// [initialState] The initial state for the bloc.
23+
/// [kdfSdk] The Komodo DeFi SDK instance for authentication operations.
24+
SecuritySettingsBloc(super.initialState, {required KomodoDefiSdk? kdfSdk})
25+
: _kdfSdk = kdfSdk {
26+
// Seed phrase events
1027
on<ResetEvent>(_onReset);
1128
on<ShowSeedEvent>(_onShowSeed);
1229
on<SeedConfirmEvent>(_onSeedConfirm);
1330
on<SeedConfirmedEvent>(_onSeedConfirmed);
1431
on<ShowSeedWordsEvent>(_onShowSeedWords);
15-
on<PasswordUpdateEvent>(_onPasswordUpdate);
1632
on<ShowSeedCopiedEvent>(_onSeedCopied);
33+
on<PasswordUpdateEvent>(_onPasswordUpdate);
34+
35+
// Private key events - hybrid security approach
36+
on<AuthenticateForPrivateKeysEvent>(_onAuthenticateForPrivateKeys);
37+
on<ShowPrivateKeysEvent>(_onShowPrivateKeys);
38+
on<ShowPrivateKeysWordsEvent>(_onShowPrivateKeysWords);
39+
on<ShowPrivateKeysCopiedEvent>(_onPrivateKeysCopied);
40+
on<PrivateKeysDownloadRequestedEvent>(_onPrivateKeysDownloadRequested);
41+
on<ClearAuthenticationErrorEvent>(_onClearAuthenticationError);
42+
43+
// Unban pubkeys events
44+
on<UnbanPubkeysEvent>(_onUnbanPubkeys);
45+
on<UnbanPubkeysCompletedEvent>(_onUnbanPubkeysCompleted);
46+
on<UnbanPubkeysFailedEvent>(_onUnbanPubkeysFailed);
1747
}
1848

19-
void _onReset(
20-
ResetEvent event,
21-
Emitter<SecuritySettingsState> emit,
22-
) {
49+
/// The Komodo DeFi SDK instance for authentication operations.
50+
/// This is optional to support testing scenarios.
51+
final KomodoDefiSdk? _kdfSdk;
52+
53+
/// Handles resetting the security settings to initial state.
54+
void _onReset(ResetEvent event, Emitter<SecuritySettingsState> emit) {
2355
emit(SecuritySettingsState.initialState());
2456
}
2557

26-
void _onShowSeed(
27-
ShowSeedEvent event,
28-
Emitter<SecuritySettingsState> emit,
29-
) {
58+
/// Handles showing the seed phrase backup screen.
59+
void _onShowSeed(ShowSeedEvent event, Emitter<SecuritySettingsState> emit) {
3060
final newState = state.copyWith(
3161
step: SecuritySettingsStep.seedShow,
3262
showSeedWords: false,
3363
);
3464
emit(newState);
3565
}
3666

67+
/// Handles toggling seed word visibility and tracking if user has viewed them.
3768
Future<void> _onShowSeedWords(
3869
ShowSeedWordsEvent event,
3970
Emitter<SecuritySettingsState> emit,
@@ -46,17 +77,7 @@ class SecuritySettingsBloc
4677
emit(newState);
4778
}
4879

49-
void _onPasswordUpdate(
50-
PasswordUpdateEvent event,
51-
Emitter<SecuritySettingsState> emit,
52-
) {
53-
final newState = state.copyWith(
54-
step: SecuritySettingsStep.passwordUpdate,
55-
showSeedWords: false,
56-
);
57-
emit(newState);
58-
}
59-
80+
/// Handles proceeding to seed phrase confirmation.
6081
void _onSeedConfirm(
6182
SeedConfirmEvent event,
6283
Emitter<SecuritySettingsState> emit,
@@ -68,6 +89,7 @@ class SecuritySettingsBloc
6889
emit(newState);
6990
}
7091

92+
/// Handles successful seed phrase confirmation.
7193
Future<void> _onSeedConfirmed(
7294
SeedConfirmedEvent event,
7395
Emitter<SecuritySettingsState> emit,
@@ -79,10 +101,188 @@ class SecuritySettingsBloc
79101
emit(newState);
80102
}
81103

104+
/// Handles seed phrase being copied to clipboard.
82105
Future<void> _onSeedCopied(
83106
ShowSeedCopiedEvent event,
84107
Emitter<SecuritySettingsState> emit,
85108
) async {
86109
emit(state.copyWith(isSeedSaved: true));
87110
}
111+
112+
/// Handles showing the password update screen.
113+
void _onPasswordUpdate(
114+
PasswordUpdateEvent event,
115+
Emitter<SecuritySettingsState> emit,
116+
) {
117+
final newState = state.copyWith(
118+
step: SecuritySettingsStep.passwordUpdate,
119+
showSeedWords: false,
120+
);
121+
emit(newState);
122+
}
123+
124+
/// Handles authentication for private key access.
125+
///
126+
/// **Security Note**: This method only validates that a user is authenticated.
127+
/// It does NOT store or handle actual private keys. After successful
128+
/// authentication, the UI layer is responsible for fetching and managing
129+
/// private keys directly to minimize their memory exposure.
130+
///
131+
/// The authentication success state triggers the UI to safely retrieve
132+
/// private keys using the SecurityManager.
133+
Future<void> _onAuthenticateForPrivateKeys(
134+
AuthenticateForPrivateKeysEvent event,
135+
Emitter<SecuritySettingsState> emit,
136+
) async {
137+
emit(state.copyWith(isAuthenticating: true, clearAuthError: true));
138+
139+
try {
140+
// Verify user is authenticated without handling sensitive data
141+
if (_kdfSdk == null) {
142+
throw Exception('SDK not available');
143+
}
144+
145+
final currentUser = await _kdfSdk.auth.currentUser;
146+
if (currentUser == null) {
147+
emit(
148+
state.copyWith(
149+
isAuthenticating: false,
150+
authError: 'User not authenticated',
151+
),
152+
);
153+
return;
154+
}
155+
156+
// Authentication successful - signal UI to fetch private keys
157+
emit(
158+
state.copyWith(
159+
isAuthenticating: false,
160+
privateKeyAuthenticationSuccess: true,
161+
),
162+
);
163+
} catch (e) {
164+
emit(
165+
state.copyWith(
166+
isAuthenticating: false,
167+
authError: 'Authentication failed: ${e.toString()}',
168+
),
169+
);
170+
}
171+
}
172+
173+
/// Handles showing the private keys screen.
174+
///
175+
/// **Security Note**: This only manages UI flow state. Actual private
176+
/// key data is handled in the UI layer for security reasons.
177+
void _onShowPrivateKeys(
178+
ShowPrivateKeysEvent event,
179+
Emitter<SecuritySettingsState> emit,
180+
) {
181+
final newState = state.copyWith(
182+
step: SecuritySettingsStep.privateKeyShow,
183+
showPrivateKeys: false,
184+
// Reset authentication success flag after use
185+
privateKeyAuthenticationSuccess: false,
186+
);
187+
emit(newState);
188+
}
189+
190+
/// Handles toggling private key visibility in the UI.
191+
///
192+
/// **Security Note**: This only controls UI visibility state.
193+
/// The actual private key data remains in the UI layer.
194+
Future<void> _onShowPrivateKeysWords(
195+
ShowPrivateKeysWordsEvent event,
196+
Emitter<SecuritySettingsState> emit,
197+
) async {
198+
final newState = state.copyWith(
199+
step: SecuritySettingsStep.privateKeyShow,
200+
showPrivateKeys: event.isShow,
201+
arePrivateKeysSaved: state.arePrivateKeysSaved || event.isShow,
202+
);
203+
emit(newState);
204+
}
205+
206+
/// Handles private keys being copied to clipboard.
207+
Future<void> _onPrivateKeysCopied(
208+
ShowPrivateKeysCopiedEvent event,
209+
Emitter<SecuritySettingsState> emit,
210+
) async {
211+
emit(state.copyWith(arePrivateKeysSaved: true));
212+
}
213+
214+
/// Handles private keys being downloaded to a file.
215+
Future<void> _onPrivateKeysDownloadRequested(
216+
PrivateKeysDownloadRequestedEvent event,
217+
Emitter<SecuritySettingsState> emit,
218+
) async {
219+
emit(state.copyWith(arePrivateKeysSaved: true));
220+
}
221+
222+
/// Handles clearing authentication errors.
223+
void _onClearAuthenticationError(
224+
ClearAuthenticationErrorEvent event,
225+
Emitter<SecuritySettingsState> emit,
226+
) {
227+
emit(state.copyWith(clearAuthError: true));
228+
}
229+
230+
/// Handles unbanning all banned public keys.
231+
///
232+
/// This method directly calls the SDK to unban pubkeys without requiring
233+
/// password authentication, as it's considered a non-destructive operation
234+
/// that improves wallet functionality.
235+
Future<void> _onUnbanPubkeys(
236+
UnbanPubkeysEvent event,
237+
Emitter<SecuritySettingsState> emit,
238+
) async {
239+
if (_kdfSdk == null) {
240+
add(const UnbanPubkeysFailedEvent('SDK not available'));
241+
return;
242+
}
243+
244+
emit(
245+
state.copyWith(
246+
isUnbanningPubkeys: true,
247+
unbanError: null,
248+
clearUnbanError: true,
249+
),
250+
);
251+
252+
try {
253+
final result = await _kdfSdk.pubkeys.unbanPubkeys(UnbanBy.all());
254+
add(UnbanPubkeysCompletedEvent(result));
255+
} catch (e) {
256+
add(UnbanPubkeysFailedEvent(e.toString()));
257+
}
258+
}
259+
260+
/// Handles successful completion of pubkey unbanning.
261+
void _onUnbanPubkeysCompleted(
262+
UnbanPubkeysCompletedEvent event,
263+
Emitter<SecuritySettingsState> emit,
264+
) {
265+
emit(
266+
state.copyWith(
267+
isUnbanningPubkeys: false,
268+
unbanResult: event.result,
269+
unbanError: null,
270+
clearUnbanError: true,
271+
),
272+
);
273+
}
274+
275+
/// Handles failed pubkey unbanning.
276+
void _onUnbanPubkeysFailed(
277+
UnbanPubkeysFailedEvent event,
278+
Emitter<SecuritySettingsState> emit,
279+
) {
280+
emit(
281+
state.copyWith(
282+
isUnbanningPubkeys: false,
283+
unbanError: event.error,
284+
unbanResult: null,
285+
),
286+
);
287+
}
88288
}

0 commit comments

Comments
 (0)