package ui import ( "crypto/subtle" "gioui.org/layout" "gioui.org/unit" "gioui.org/widget" "gioui.org/widget/material" "git.wxccs.org/iceking2nd/winauth-go/internal/i18n" ) // changePasswordDialog asks for the current passphrase, then a new // passphrase + confirmation. The dialog refuses to submit unless the // current passphrase matches what the store already holds (via a // constant-time compare to avoid leaking length / prefix information). type changePasswordDialog struct { oldEd widget.Editor newEd widget.Editor confirmEd widget.Editor okBtn widget.Clickable cancelBtn widget.Clickable errorMsg string } func newChangePasswordDialog() *changePasswordDialog { d := &changePasswordDialog{} d.oldEd.SingleLine = true d.oldEd.Mask = '*' d.newEd.SingleLine = true d.newEd.Mask = '*' d.confirmEd.SingleLine = true d.confirmEd.Mask = '*' return d } // changePasswordResult tells the caller what to do. cancel skips any // state change. ok=true means commit newPw via store.SetPassword. type changePasswordResult struct { cancel bool newPw []byte } // Layout takes a verify callback that returns true when oldPw matches // what the store believes the current passphrase is. The caller is // responsible for that comparison so the dialog itself never has to // hold a copy of the live passphrase. func (d *changePasswordDialog) Layout( gtx layout.Context, th *material.Theme, verifyOld func(oldPw []byte) bool, onDone func(changePasswordResult), ) layout.Dimensions { if d.cancelBtn.Clicked(gtx) { onDone(changePasswordResult{cancel: true}) return layout.Dimensions{Size: gtx.Constraints.Max} } if d.okBtn.Clicked(gtx) { oldPw := []byte(d.oldEd.Text()) newPw := []byte(d.newEd.Text()) confirm := []byte(d.confirmEd.Text()) switch { case !verifyOld(oldPw): // Use the constant-length wrong-password string so the // presence of an error is not itself a side channel. d.errorMsg = i18n.T("msg_password_wrong") case subtle.ConstantTimeCompare(newPw, confirm) != 1: d.errorMsg = i18n.T("msg_password_mismatch") default: onDone(changePasswordResult{newPw: newPw}) return layout.Dimensions{Size: gtx.Constraints.Max} } } body := func(gtx layout.Context) layout.Dimensions { return layout.Flex{Axis: layout.Vertical}.Layout(gtx, layout.Rigid(labeledEditor(th, i18n.T("label_password_current"), &d.oldEd, "")), layout.Rigid(labeledEditor(th, i18n.T("label_password_new"), &d.newEd, "")), layout.Rigid(labeledEditor(th, i18n.T("label_password_confirm"), &d.confirmEd, "")), layout.Rigid(layout.Spacer{Height: unit.Dp(4)}.Layout), layout.Rigid(material.Caption(th, i18n.T("hint_password_empty_disables")).Layout), layout.Rigid(errorLabel(th, d.errorMsg)), ) } return modalCard(gtx, th, i18n.T("dialog_change_password_title"), i18n.T("btn_ok"), i18n.T("btn_cancel"), &d.okBtn, &d.cancelBtn, body) }