refact(password): Store permanent password as hashed verifier (#14619)

* refact(password): Store permanent password as hashed verifier

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(password): remove unused code

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(password): mobile, password dialog, width 500

Signed-off-by: fufesou <linlong1266@gmail.com>

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
This commit is contained in:
fufesou
2026-03-26 14:49:54 +08:00
committed by GitHub
parent 285e29d2dc
commit 170516572e
64 changed files with 563 additions and 192 deletions

View File

@@ -908,12 +908,17 @@ class _DesktopHomePageState extends State<DesktopHomePage>
}
void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
final pw = await bind.mainGetPermanentPassword();
final p0 = TextEditingController(text: pw);
final p1 = TextEditingController(text: pw);
final p0 = TextEditingController(text: "");
final p1 = TextEditingController(text: "");
var errMsg0 = "";
var errMsg1 = "";
final RxString rxPass = pw.trim().obs;
final localPasswordSet =
(await bind.mainGetCommon(key: "local-permanent-password-set")) == "true";
final permanentPasswordSet =
(await bind.mainGetCommon(key: "permanent-password-set")) == "true";
final presetPassword = permanentPasswordSet && !localPasswordSet;
var canSubmit = false;
final RxString rxPass = "".obs;
final rules = [
DigitValidationRule(),
UppercaseValidationRule(),
@@ -922,9 +927,21 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
MinCharactersValidationRule(8),
];
final maxLength = bind.mainMaxEncryptLen();
final statusTip = localPasswordSet
? translate('password-hidden-tip')
: (presetPassword ? translate('preset-password-in-use-tip') : '');
final showStatusTipOnMobile =
statusTip.isNotEmpty && !isDesktop && !isWebDesktop;
gFFI.dialogManager.show((setState, close, context) {
submit() {
updateCanSubmit() {
canSubmit = p0.text.trim().isNotEmpty || p1.text.trim().isNotEmpty;
}
submit() async {
if (!canSubmit) {
return;
}
setState(() {
errMsg0 = "";
errMsg1 = "";
@@ -947,7 +964,13 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
});
return;
}
bind.mainSetPermanentPassword(password: pass);
final ok = await bind.mainSetPermanentPasswordWithResult(password: pass);
if (!ok) {
setState(() {
errMsg0 = '${translate('Prompt')}: ${translate("Failed")}';
});
return;
}
if (pass.isNotEmpty) {
notEmptyCallback?.call();
}
@@ -955,14 +978,20 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
}
return CustomAlertDialog(
title: Text(translate("Set Password")),
title: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.key, color: MyTheme.accent),
Text(translate("Set Password")).paddingOnly(left: 10),
],
),
content: ConstrainedBox(
constraints: const BoxConstraints(minWidth: 500),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 8.0,
SizedBox(
height: showStatusTipOnMobile ? 0.0 : 6.0,
),
Row(
children: [
@@ -978,6 +1007,7 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
rxPass.value = value.trim();
setState(() {
errMsg0 = '';
updateCanSubmit();
});
},
maxLength: maxLength,
@@ -989,9 +1019,9 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
children: [
Expanded(child: PasswordStrengthIndicator(password: rxPass)),
],
).marginSymmetric(vertical: 8),
const SizedBox(
height: 8.0,
).marginOnly(top: 2, bottom: showStatusTipOnMobile ? 2 : 8),
SizedBox(
height: showStatusTipOnMobile ? 0.0 : 8.0,
),
Row(
children: [
@@ -1005,6 +1035,7 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
onChanged: (value) {
setState(() {
errMsg1 = '';
updateCanSubmit();
});
},
maxLength: maxLength,
@@ -1012,11 +1043,23 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
),
],
),
const SizedBox(
height: 8.0,
if (statusTip.isNotEmpty)
Row(
children: [
Icon(Icons.info, color: Colors.amber, size: 18)
.marginOnly(right: 6),
Expanded(
child: Text(
statusTip,
style: const TextStyle(fontSize: 13, height: 1.1),
))
],
).marginOnly(top: 6, bottom: 2),
SizedBox(
height: showStatusTipOnMobile ? 0.0 : 8.0,
),
Obx(() => Wrap(
runSpacing: 8,
runSpacing: showStatusTipOnMobile ? 2.0 : 8.0,
spacing: 4,
children: rules.map((e) {
var checked = e.validate(rxPass.value.trim());
@@ -1036,11 +1079,67 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
],
),
),
actions: [
dialogButton("Cancel", onPressed: close, isOutline: true),
dialogButton("OK", onPressed: submit),
],
onSubmit: submit,
actions: (() {
final cancelButton = dialogButton(
"Cancel",
icon: Icon(Icons.close_rounded),
onPressed: close,
isOutline: true,
);
final removeButton = dialogButton(
"Remove",
icon: Icon(Icons.delete_outline_rounded),
onPressed: () async {
setState(() {
errMsg0 = "";
errMsg1 = "";
});
final ok =
await bind.mainSetPermanentPasswordWithResult(password: "");
if (!ok) {
setState(() {
errMsg0 = '${translate('Prompt')}: ${translate("Failed")}';
});
return;
}
close();
},
buttonStyle: ButtonStyle(
backgroundColor: MaterialStatePropertyAll(Colors.red)),
);
final okButton = dialogButton(
"OK",
icon: Icon(Icons.done_rounded),
onPressed: canSubmit ? submit : null,
);
if (!isDesktop && !isWebDesktop && localPasswordSet) {
return [
Align(
alignment: Alignment.centerRight,
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.centerRight,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
cancelButton,
const SizedBox(width: 4),
removeButton,
const SizedBox(width: 4),
okButton,
],
),
),
),
];
}
return [
cancelButton,
if (localPasswordSet) removeButton,
okButton,
];
})(),
onSubmit: canSubmit ? submit : null,
onCancel: close,
);
});

View File

@@ -1109,8 +1109,9 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
if (value ==
passwordValues[passwordKeys
.indexOf(kUsePermanentPassword)] &&
(await bind.mainGetPermanentPassword())
.isEmpty) {
(await bind.mainGetCommon(
key: "permanent-password-set")) !=
"true") {
if (isChangePermanentPasswordDisabled()) {
await callback();
return;