basic desktop change passphrase functionality

This commit is contained in:
julian 2022-11-14 13:24:40 -06:00
parent bdf1008424
commit e053764554
2 changed files with 474 additions and 388 deletions

View file

@ -1,9 +1,13 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/src/widgets/framework.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import 'package:stackwallet/notifications/show_flush_bar.dart';
import 'package:stackwallet/providers/desktop/storage_crypto_handler_provider.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/enums/flush_bar_type.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/widgets/desktop/primary_button.dart';
@ -47,6 +51,63 @@ class _SecuritySettings extends ConsumerState<SecuritySettings> {
String passwordFeedback =
"Add another word or two. Uncommon words are better. Use a few words, avoid common phrases. No need for symbols, digits, or uppercase letters.";
Future<bool> attemptChangePW() async {
final String pw = passwordCurrentController.text;
final String pwNew = passwordController.text;
final String pwNewRepeat = passwordRepeatController.text;
final verified =
await ref.read(storageCryptoHandlerProvider).verifyPassphrase(pw);
if (verified) {
if (pwNew != pwNewRepeat) {
unawaited(
showFloatingFlushBar(
type: FlushBarType.warning,
message: "New passphrase does not match!",
context: context,
),
);
return false;
} else {
final success =
await ref.read(storageCryptoHandlerProvider).changePassphrase(
pw,
pwNew,
);
if (success) {
unawaited(
showFloatingFlushBar(
type: FlushBarType.success,
message: "Passphrase successfully changed",
context: context,
),
);
return true;
} else {
unawaited(
showFloatingFlushBar(
type: FlushBarType.warning,
message: "Passphrase change failed",
context: context,
),
);
return false;
}
}
} else {
unawaited(
showFloatingFlushBar(
type: FlushBarType.warning,
message: "Current passphrase is not valid!",
context: context,
),
);
return false;
}
}
@override
void initState() {
passwordCurrentController = TextEditingController();
@ -78,411 +139,394 @@ class _SecuritySettings extends ConsumerState<SecuritySettings> {
debugPrint("BUILD: $runtimeType");
return Column(
children: [
Padding(
padding: const EdgeInsets.only(
right: 30,
),
child: RoundedWhiteContainer(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SvgPicture.asset(
Assets.svg.circleLock,
width: 48,
height: 48,
),
Center(
child: Padding(
padding: const EdgeInsets.all(10),
child: RichText(
textAlign: TextAlign.start,
text: TextSpan(
children: [
TextSpan(
text: "Change Password",
style: STextStyles.desktopTextSmall(context),
),
TextSpan(
text:
"\n\nProtect your Stack Wallet with a strong password. Stack Wallet does not store "
"your password, and is therefore NOT able to restore it. Keep your password safe and secure.",
style:
STextStyles.desktopTextExtraExtraSmall(context),
),
],
),
),
),
),
Column(
Row(
children: [
Expanded(
child: RoundedWhiteContainer(
radiusMultiplier: 2,
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: EdgeInsets.all(
10,
),
child: changePassword
? SizedBox(
width: 512,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Current password",
style:
STextStyles.desktopTextExtraExtraSmall(
context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textDark3),
textAlign: TextAlign.left,
),
const SizedBox(height: 10),
ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
SvgPicture.asset(
Assets.svg.circleLock,
width: 48,
height: 48,
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 16,
),
Text(
"Change Password",
style: STextStyles.desktopTextSmall(context),
),
const SizedBox(
height: 8,
),
Text(
"Protect your Stack Wallet with a strong password. Stack Wallet does not store "
"your password, and is therefore NOT able to restore it. Keep your password safe and secure.",
style:
STextStyles.desktopTextExtraExtraSmall(context),
),
const SizedBox(
height: 20,
),
changePassword
? SizedBox(
width: 512,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Current password",
style: STextStyles
.desktopTextExtraExtraSmall(
context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textDark3),
textAlign: TextAlign.left,
),
child: TextField(
key: const Key(
"desktopSecurityRestoreFromFilePasswordFieldKey"),
focusNode: passwordCurrentFocusNode,
controller: passwordCurrentController,
style: STextStyles.field(context),
obscureText: hidePassword,
enableSuggestions: false,
autocorrect: false,
decoration: standardInputDecoration(
"Enter current password",
passwordCurrentFocusNode,
context,
).copyWith(
labelStyle:
STextStyles.fieldLabel(context),
suffixIcon: UnconstrainedBox(
child: Row(
children: [
const SizedBox(
width: 16,
),
GestureDetector(
key: const Key(
"desktopSecurityRestoreFromFilePasswordFieldShowPasswordButtonKey"),
onTap: () async {
setState(() {
hidePassword =
!hidePassword;
});
},
child: SvgPicture.asset(
hidePassword
? Assets.svg.eye
: Assets.svg.eyeSlash,
color: Theme.of(context)
.extension<StackColors>()!
.textDark3,
width: 16,
height: 16,
),
),
const SizedBox(
width: 12,
),
],
),
),
const SizedBox(height: 10),
ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
onChanged: (newValue) {},
),
),
const SizedBox(height: 16),
Text(
"New password",
style:
STextStyles.desktopTextExtraExtraSmall(
context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textDark3),
textAlign: TextAlign.left,
),
const SizedBox(height: 10),
ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
child: TextField(
key: const Key(
"desktopSecurityCreateNewPasswordFieldKey1"),
focusNode: passwordFocusNode,
controller: passwordController,
style: STextStyles.field(context),
obscureText: hidePassword,
enableSuggestions: false,
autocorrect: false,
decoration: standardInputDecoration(
"Enter new password",
passwordFocusNode,
context,
).copyWith(
labelStyle:
STextStyles.fieldLabel(context),
suffixIcon: UnconstrainedBox(
child: Row(
children: [
const SizedBox(
width: 16,
),
GestureDetector(
key: const Key(
"desktopSecurityCreateNewPasswordButtonKey1"),
onTap: () async {
setState(() {
hidePassword =
!hidePassword;
});
},
child: SvgPicture.asset(
hidePassword
? Assets.svg.eye
: Assets.svg.eyeSlash,
color: Theme.of(context)
.extension<StackColors>()!
.textDark3,
width: 16,
height: 16,
),
),
const SizedBox(
width: 12,
),
],
),
),
),
onChanged: (newValue) {
if (newValue.isEmpty) {
setState(() {
passwordFeedback = "";
});
return;
}
final result =
zxcvbn.evaluate(newValue);
String suggestionsAndTips = "";
for (var sug in result
.feedback.suggestions!
.toSet()) {
suggestionsAndTips += "$sug\n";
}
suggestionsAndTips +=
result.feedback.warning!;
String feedback =
// "Password Strength: ${((result.score! / 4.0) * 100).toInt()}%\n"
suggestionsAndTips;
passwordStrength = result.score! / 4;
// hack fix to format back string returned from zxcvbn
if (feedback
.contains("phrasesNo need")) {
feedback = feedback.replaceFirst(
"phrasesNo need",
"phrases\nNo need");
}
if (feedback.endsWith("\n")) {
feedback = feedback.substring(
0, feedback.length - 2);
}
setState(() {
passwordFeedback = feedback;
});
},
),
),
if (passwordFocusNode.hasFocus ||
passwordRepeatFocusNode.hasFocus ||
passwordController.text.isNotEmpty)
Padding(
padding: EdgeInsets.only(
left: 12,
right: 12,
top:
passwordFeedback.isNotEmpty ? 4 : 0,
),
child: passwordFeedback.isNotEmpty
? Text(
passwordFeedback,
style: STextStyles.infoSmall(
context),
)
: null,
),
if (passwordFocusNode.hasFocus ||
passwordRepeatFocusNode.hasFocus ||
passwordController.text.isNotEmpty)
Padding(
padding: const EdgeInsets.only(
left: 12,
right: 12,
top: 10,
),
child: ProgressBar(
child: TextField(
key: const Key(
"desktopSecurityCreateStackBackUpProgressBar"),
width: 450,
height: 5,
fillColor: passwordStrength < 0.51
? Theme.of(context)
.extension<StackColors>()!
.accentColorRed
: passwordStrength < 1
? Theme.of(context)
.extension<StackColors>()!
.accentColorYellow
: Theme.of(context)
.extension<StackColors>()!
.accentColorGreen,
backgroundColor: Theme.of(context)
.extension<StackColors>()!
.buttonBackSecondary,
percent: passwordStrength < 0.25
? 0.03
: passwordStrength,
),
),
const SizedBox(height: 16),
Text(
"Confirm new password",
style:
STextStyles.desktopTextExtraExtraSmall(
context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textDark3),
textAlign: TextAlign.left,
),
const SizedBox(height: 10),
ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
child: TextField(
key: const Key(
"desktopSecurityCreateNewPasswordFieldKey2"),
focusNode: passwordRepeatFocusNode,
controller: passwordRepeatController,
style: STextStyles.field(context),
obscureText: hidePassword,
enableSuggestions: false,
autocorrect: false,
decoration: standardInputDecoration(
"Confirm new password",
passwordRepeatFocusNode,
context,
).copyWith(
labelStyle:
STextStyles.fieldLabel(context),
suffixIcon: UnconstrainedBox(
child: Row(
children: [
const SizedBox(
width: 16,
),
GestureDetector(
key: const Key(
"desktopSecurityCreateNewPasswordButtonKey2"),
onTap: () async {
setState(() {
hidePassword =
!hidePassword;
});
},
child: SvgPicture.asset(
hidePassword
? Assets.svg.eye
: Assets.svg.eyeSlash,
color: Theme.of(context)
.extension<StackColors>()!
.textDark3,
"desktopSecurityRestoreFromFilePasswordFieldKey"),
focusNode: passwordCurrentFocusNode,
controller: passwordCurrentController,
style: STextStyles.field(context),
obscureText: hidePassword,
enableSuggestions: false,
autocorrect: false,
decoration: standardInputDecoration(
"Enter current password",
passwordCurrentFocusNode,
context,
).copyWith(
labelStyle:
STextStyles.fieldLabel(context),
suffixIcon: UnconstrainedBox(
child: Row(
children: [
const SizedBox(
width: 16,
height: 16,
),
),
const SizedBox(
width: 12,
),
],
GestureDetector(
key: const Key(
"desktopSecurityRestoreFromFilePasswordFieldShowPasswordButtonKey"),
onTap: () async {
setState(() {
hidePassword =
!hidePassword;
});
},
child: SvgPicture.asset(
hidePassword
? Assets.svg.eye
: Assets.svg.eyeSlash,
color: Theme.of(context)
.extension<
StackColors>()!
.textDark3,
width: 16,
height: 16,
),
),
const SizedBox(
width: 12,
),
],
),
),
),
onChanged: (newValue) {
setState(() {});
},
),
onChanged: (newValue) {
setState(() {});
},
),
),
const SizedBox(height: 20),
PrimaryButton(
width: 142,
desktopMed: true,
enabled: shouldEnableSave,
label: "Save changes",
onPressed: () {
setState(() {
changePassword = false;
});
},
)
],
const SizedBox(height: 16),
Text(
"New password",
style: STextStyles
.desktopTextExtraExtraSmall(
context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textDark3),
textAlign: TextAlign.left,
),
const SizedBox(height: 10),
ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
child: TextField(
key: const Key(
"desktopSecurityCreateNewPasswordFieldKey1"),
focusNode: passwordFocusNode,
controller: passwordController,
style: STextStyles.field(context),
obscureText: hidePassword,
enableSuggestions: false,
autocorrect: false,
decoration: standardInputDecoration(
"Enter new password",
passwordFocusNode,
context,
).copyWith(
labelStyle:
STextStyles.fieldLabel(context),
suffixIcon: UnconstrainedBox(
child: Row(
children: [
const SizedBox(
width: 16,
),
GestureDetector(
key: const Key(
"desktopSecurityCreateNewPasswordButtonKey1"),
onTap: () async {
setState(() {
hidePassword =
!hidePassword;
});
},
child: SvgPicture.asset(
hidePassword
? Assets.svg.eye
: Assets.svg.eyeSlash,
color: Theme.of(context)
.extension<
StackColors>()!
.textDark3,
width: 16,
height: 16,
),
),
const SizedBox(
width: 12,
),
],
),
),
),
onChanged: (newValue) {
if (newValue.isEmpty) {
setState(() {
passwordFeedback = "";
});
return;
}
final result =
zxcvbn.evaluate(newValue);
String suggestionsAndTips = "";
for (var sug in result
.feedback.suggestions!
.toSet()) {
suggestionsAndTips += "$sug\n";
}
suggestionsAndTips +=
result.feedback.warning!;
String feedback =
// "Password Strength: ${((result.score! / 4.0) * 100).toInt()}%\n"
suggestionsAndTips;
passwordStrength = result.score! / 4;
// hack fix to format back string returned from zxcvbn
if (feedback
.contains("phrasesNo need")) {
feedback = feedback.replaceFirst(
"phrasesNo need",
"phrases\nNo need");
}
if (feedback.endsWith("\n")) {
feedback = feedback.substring(
0, feedback.length - 2);
}
setState(() {
passwordFeedback = feedback;
});
},
),
),
if (passwordFocusNode.hasFocus ||
passwordRepeatFocusNode.hasFocus ||
passwordController.text.isNotEmpty)
Padding(
padding: EdgeInsets.only(
left: 12,
right: 12,
top: passwordFeedback.isNotEmpty
? 4
: 0,
),
child: passwordFeedback.isNotEmpty
? Text(
passwordFeedback,
style: STextStyles.infoSmall(
context),
)
: null,
),
if (passwordFocusNode.hasFocus ||
passwordRepeatFocusNode.hasFocus ||
passwordController.text.isNotEmpty)
Padding(
padding: const EdgeInsets.only(
left: 12,
right: 12,
top: 10,
),
child: ProgressBar(
key: const Key(
"desktopSecurityCreateStackBackUpProgressBar"),
width: 450,
height: 5,
fillColor: passwordStrength < 0.51
? Theme.of(context)
.extension<StackColors>()!
.accentColorRed
: passwordStrength < 1
? Theme.of(context)
.extension<StackColors>()!
.accentColorYellow
: Theme.of(context)
.extension<StackColors>()!
.accentColorGreen,
backgroundColor: Theme.of(context)
.extension<StackColors>()!
.buttonBackSecondary,
percent: passwordStrength < 0.25
? 0.03
: passwordStrength,
),
),
const SizedBox(height: 16),
Text(
"Confirm new password",
style: STextStyles
.desktopTextExtraExtraSmall(
context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textDark3),
textAlign: TextAlign.left,
),
const SizedBox(height: 10),
ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
child: TextField(
key: const Key(
"desktopSecurityCreateNewPasswordFieldKey2"),
focusNode: passwordRepeatFocusNode,
controller: passwordRepeatController,
style: STextStyles.field(context),
obscureText: hidePassword,
enableSuggestions: false,
autocorrect: false,
decoration: standardInputDecoration(
"Confirm new password",
passwordRepeatFocusNode,
context,
).copyWith(
labelStyle:
STextStyles.fieldLabel(context),
suffixIcon: UnconstrainedBox(
child: Row(
children: [
const SizedBox(
width: 16,
),
GestureDetector(
key: const Key(
"desktopSecurityCreateNewPasswordButtonKey2"),
onTap: () async {
setState(() {
hidePassword =
!hidePassword;
});
},
child: SvgPicture.asset(
hidePassword
? Assets.svg.eye
: Assets.svg.eyeSlash,
color: Theme.of(context)
.extension<
StackColors>()!
.textDark3,
width: 16,
height: 16,
),
),
const SizedBox(
width: 12,
),
],
),
),
),
onChanged: (newValue) {
setState(() {});
},
),
),
const SizedBox(height: 20),
PrimaryButton(
width: 160,
desktopMed: true,
enabled: shouldEnableSave,
label: "Save changes",
onPressed: () async {
final didChangePW =
await attemptChangePW();
if (didChangePW) {
setState(() {
changePassword = false;
});
}
},
)
],
),
)
: PrimaryButton(
width: 210,
desktopMed: true,
enabled: true,
label: "Set up new password",
onPressed: () {
setState(() {
changePassword = true;
});
},
),
)
: PrimaryButton(
width: 192,
desktopMed: true,
enabled: true,
label: "Set up new password",
onPressed: () {
setState(() {
changePassword = true;
});
},
),
],
),
],
),
],
),
),
),
const SizedBox(
width: 40,
),
],
),
],
);
}
}
class NewPasswordButton extends ConsumerWidget {
const NewPasswordButton({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
return SizedBox(
width: 200,
height: 48,
child: TextButton(
style: Theme.of(context)
.extension<StackColors>()!
.getPrimaryEnabledButtonColor(context),
onPressed: () {},
child: Text(
"Set up new password",
style: STextStyles.button(context),
),
),
);
}
}

View file

@ -115,6 +115,48 @@ class DPS {
}
}
Future<bool> changePassphrase(
String passphraseOld,
String passphraseNew,
) async {
final box = await Hive.openBox<String>(DB.boxNameDesktopData);
final keyBlob = DB.instance.get<String>(
boxName: DB.boxNameDesktopData,
key: _kKeyBlobKey,
);
await box.close();
if (keyBlob == null) {
// no passphrase key blob found so any passphrase is technically bad
return false;
}
if (!(await verifyPassphrase(passphraseOld))) {
return false;
}
try {
await _handler!.resetPassphrase(passphraseNew);
final box = await Hive.openBox<String>(DB.boxNameDesktopData);
await DB.instance.put<String>(
boxName: DB.boxNameDesktopData,
key: _kKeyBlobKey,
value: await _handler!.getKeyBlob(),
);
await box.close();
// successfully updated passphrase
return true;
} catch (e, s) {
Logging.instance.log(
"${_getMessageFromException(e)}\n$s",
level: LogLevel.Warning,
);
return false;
}
}
Future<bool> hasPassword() async {
final keyBlob = DB.instance.get<String>(
boxName: DB.boxNameDesktopData,