PrintToVeraeTimeStamp/cmd/verae-print/keydialog_windows.go
George Lambert 179c974b60 sign-in: in-app password reset (emailed code) on Mac/Linux and Windows; v0.3.4
- auth.PasswordForgot / auth.PasswordReset (+ httptest tests); ErrResetUnavailable
  on 404/401/405/5xx so dialogs offer https://customer.verae.ai/portal/
- keyflow_gui: 'Forgot password?' on the password step and after a failed sign-in
- Windows: Forgot password? link, forgot/reset/unavailable forms
- bump HelperVersion, deb and pkg default VERSION to 0.3.4
2026-10-04 14:14:55 -04:00

422 lines
15 KiB
Go

//go:build windows
package main
import (
"bytes"
"encoding/base64"
"errors"
"os"
"os/exec"
"path/filepath"
"strings"
"syscall"
"unsafe"
"github.com/VeraeLLC/PrintToVeraeTimeStamp/internal/auth"
"github.com/VeraeLLC/PrintToVeraeTimeStamp/internal/vlog"
)
// Native WinForms dialogs (PowerShell, console hidden) shown in the user's
// session by the helper. Results come back on stdout as base64 fields
// ("action|b64|b64..."), so passwords/keys never touch disk, argv or logs.
const formsPS = `
$ErrorActionPreference = 'Stop'
Add-Type -AssemblyName System.Windows.Forms, System.Drawing
[System.Windows.Forms.Application]::EnableVisualStyles()
function B([string]$s) { [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($s)) }
function U([string]$s) { if ($s) { [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($s)) } else { '' } }
$kind = $env:VERAE_DLG_KIND; $msg = U $env:VERAE_DLG_MSG; $pre = U $env:VERAE_DLG_EMAIL
$f = New-Object Windows.Forms.Form
$f.Text = 'Verae TimeStamp PDF'; $f.Width = 540; $f.StartPosition = 'CenterScreen'; $f.TopMost = $true
$f.FormBorderStyle = 'FixedDialog'; $f.MaximizeBox = $false; $f.MinimizeBox = $false
$f.Font = New-Object Drawing.Font('Segoe UI', 9)
$script:y = 12; $script:result = 'cancel'
$l = New-Object Windows.Forms.Label; $l.Left = 15; $l.Top = $script:y; $l.Width = 495; $l.Height = 72; $l.Text = $msg; $f.Controls.Add($l); $script:y += 78
function Field([string]$label, [bool]$hidden, [string]$val) {
$a = New-Object Windows.Forms.Label; $a.Text = $label; $a.Left = 15; $a.Top = $script:y + 3; $a.Width = 150; $f.Controls.Add($a)
$t = New-Object Windows.Forms.TextBox; $t.Left = 170; $t.Top = $script:y; $t.Width = 340; $t.UseSystemPasswordChar = $hidden; $t.Text = $val; $f.Controls.Add($t)
$script:y += 30; return $t
}
function Btn([string]$text, [int]$x, [int]$w, [string]$res) {
$b = New-Object Windows.Forms.Button; $b.Text = $text; $b.Left = $x; $b.Top = $script:y; $b.Width = $w; $b.Height = 28
$b.Tag = $res; $b.Add_Click({ $script:result = $this.Tag; $f.Close() }); $f.Controls.Add($b); return $b
}
switch ($kind) {
'login' {
$e = Field 'Email' $false $pre; $p = Field 'Password' $true ''; $t = Field '2FA code (if enabled)' $false ''
$fl = New-Object Windows.Forms.LinkLabel; $fl.Text = 'Forgot password?'; $fl.Left = 170; $fl.Top = $script:y; $fl.Width = 200
$fl.Add_LinkClicked({ $script:result = 'forgot'; $f.Close() }); $f.Controls.Add($fl); $script:y += 24
$script:y += 8
$ok = Btn 'Sign in' 15 100 'login'; $f.AcceptButton = $ok
Btn 'Create account' 125 120 'create' | Out-Null
Btn 'Paste key' 255 100 'paste' | Out-Null
$c = Btn 'Cancel' 410 100 'cancel'; $f.CancelButton = $c
$f.Height = $script:y + 75
if ($pre) { $f.Add_Shown({ $p.Focus() }) }
[void]$f.ShowDialog()
if ($script:result -eq 'login') { "login|$(B $e.Text)|$(B $p.Text)|$(B $t.Text)" }
elseif ($script:result -eq 'forgot') { "forgot|$(B $e.Text)" } else { $script:result }
}
'forgot' {
$e = Field 'Email' $false $pre
$script:y += 8
$ok = Btn 'Send code' 15 100 'send'; $f.AcceptButton = $ok
$c = Btn 'Back' 410 100 'cancel'; $f.CancelButton = $c
$f.Height = $script:y + 75
[void]$f.ShowDialog()
if ($script:result -eq 'send') { "send|$(B $e.Text)" } else { 'cancel' }
}
'reset' {
$k = Field 'Code from the email' $false ''; $p = Field 'New password' $true ''; $p2 = Field 'Confirm new password' $true ''
$script:y += 8
$ok = Btn 'Reset password' 15 130 'reset'; $f.AcceptButton = $ok
Btn 'Send a new code' 155 130 'resend' | Out-Null
$c = Btn 'Cancel' 410 100 'cancel'; $f.CancelButton = $c
$f.Height = $script:y + 75
[void]$f.ShowDialog()
if ($script:result -eq 'reset') { "reset|$(B $k.Text)|$(B $p.Text)|$(B $p2.Text)" } else { $script:result }
}
'unavail' {
$ok = Btn 'Open customer.verae.ai' 15 200 'open'; $f.AcceptButton = $ok
$c = Btn 'Back' 410 100 'cancel'; $f.CancelButton = $c
$f.Height = $script:y + 75
[void]$f.ShowDialog()
if ($script:result -eq 'open') { Start-Process 'https://customer.verae.ai/portal/' }
'cancel'
}
'create' {
$n = Field 'Your name' $false ''; $e = Field 'Email' $false $pre; $p = Field 'Password' $true ''; $p2 = Field 'Confirm password' $true ''
$script:y += 8
$ok = Btn 'Create account' 15 130 'create'; $f.AcceptButton = $ok
$c = Btn 'Back' 410 100 'cancel'; $f.CancelButton = $c
$f.Height = $script:y + 75
[void]$f.ShowDialog()
if ($script:result -eq 'create') { "create|$(B $n.Text)|$(B $e.Text)|$(B $p.Text)|$(B $p2.Text)" } else { 'cancel' }
}
'code' {
$k = Field 'Code from the email' $false ''
$script:y += 8
$ok = Btn 'Verify' 15 100 'verify'; $f.AcceptButton = $ok
Btn 'Send a new code' 125 130 'resend' | Out-Null
$c = Btn 'Cancel' 410 100 'cancel'; $f.CancelButton = $c
$f.Height = $script:y + 75
[void]$f.ShowDialog()
if ($script:result -eq 'verify') { "verify|$(B $k.Text)" } else { $script:result }
}
'paste' {
$g = New-Object Windows.Forms.Button; $g.Text = 'Open customer.verae.ai'; $g.Left = 15; $g.Top = $script:y; $g.Width = 200; $g.Height = 28
$g.Add_Click({ Start-Process 'https://customer.verae.ai/portal/' }); $f.Controls.Add($g)
$script:y += 40
$k = Field 'xVerae- key' $true ''
$script:y += 8
$ok = Btn 'Save key' 15 100 'paste'; $f.AcceptButton = $ok
$c = Btn 'Back' 410 100 'cancel'; $f.CancelButton = $c
$f.Height = $script:y + 75
[void]$f.ShowDialog()
if ($script:result -eq 'paste') { "paste|$(B $k.Text)" } else { 'cancel' }
}
}
`
var errDialog = errors.New("dialog failed")
func b64(s string) string { return base64.StdEncoding.EncodeToString([]byte(s)) }
// winForm shows one form; returns the action and decoded fields.
func winForm(kind, msg, email string) (string, []string, error) {
cmd := exec.Command("powershell.exe", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-WindowStyle", "Hidden", "-Command", formsPS)
cmd.Env = append(os.Environ(), "VERAE_DLG_KIND="+kind, "VERAE_DLG_MSG="+b64(msg), "VERAE_DLG_EMAIL="+b64(email))
hideWindow(cmd)
var out bytes.Buffer
cmd.Stdout = &out
if err := cmd.Run(); err != nil {
vlog.Errorf("dialog %s: %v", kind, err)
return "", nil, errDialog
}
parts := strings.Split(strings.TrimSpace(out.String()), "|")
var fields []string
for _, p := range parts[1:] {
d, _ := base64.StdEncoding.DecodeString(p)
fields = append(fields, string(d))
}
for len(fields) < 4 {
fields = append(fields, "")
}
return parts[0], fields, nil
}
// acquireKeyInteractive: sign in / create account / paste key, then save the
// key for this user. True when a new key was saved.
func acquireKeyInteractive(reason string) bool {
caps := auth.GetCapabilities()
msg, email := reason, ""
for i := 0; i < 25; i++ {
act, f, err := winForm("login", msg, email)
if err != nil {
return false
}
var key string
switch act {
case "login":
email = strings.TrimSpace(f[0])
if !auth.ValidEmail(email) || f[1] == "" {
msg = "Please enter your email and password."
continue
}
vlog.Printf("sign-in: login %s", auth.MaskEmail(email))
key, err = auth.InstallLogin(caps, email, f[1], strings.TrimSpace(f[2]), tokenName())
f[1] = ""
if errors.Is(err, auth.ErrTOTPRequired) {
msg = "This account uses two-factor authentication. Enter the 6-digit code from your authenticator app, then click Sign in."
continue
}
if err != nil {
vlog.Errorf("sign-in: login failed for %s: %v", auth.MaskEmail(email), err)
msg = "Sign-in failed: " + err.Error() + "\r\nForgot your password? Click \"Forgot password?\" below."
continue
}
case "forgot":
if e := strings.TrimSpace(f[0]); e != "" {
email = e
}
k, ok, m := winForgotFlow(caps, email)
if m != "" {
msg = m
}
if !ok {
continue
}
key = k
case "create":
k, ok := winCreateFlow(caps, email)
if !ok {
continue
}
key = k
case "paste":
if pasteFlow(reason) {
return true
}
continue
default:
vlog.Printf("sign-in: cancelled by user")
return false
}
if err := auth.SaveXVeraeToken(key); err != nil {
msg = "The key could not be saved: " + err.Error()
continue
}
home, _ := os.UserHomeDir()
vlog.Printf("sign-in: key %s saved to %s", auth.MaskKey(key), filepath.Join(home, ".verae", "xverae-token"))
return true
}
return false
}
func winCreateFlow(caps auth.Capabilities, email string) (string, bool) {
msg := "Create a Verae account.\r\n" + caps.Policy.Describe()
for i := 0; i < 10; i++ {
act, f, err := winForm("create", msg, email)
if err != nil || act != "create" {
return "", false
}
name, em := strings.TrimSpace(f[0]), strings.TrimSpace(f[1])
switch {
case name == "":
msg = "Please enter your name."
continue
case !auth.ValidEmail(em):
msg = "Please enter a valid email address."
continue
case f[2] != f[3]:
msg = "The passwords do not match."
continue
}
if perr := caps.Policy.Check(f[2], em); perr != nil {
msg = perr.Error() + "\r\n" + caps.Policy.Describe()
continue
}
vlog.Printf("sign-in: register %s installApi=%v", auth.MaskEmail(em), caps.InstallAPI)
rr, err := auth.InstallRegister(caps, name, em, f[2], tokenName())
f[2], f[3] = "", ""
if err != nil {
msg = "Could not create the account: " + err.Error()
continue
}
if rr.PendingID == "" {
return rr.Key, true
}
cmsg := "We emailed a 6-digit code to " + auth.MaskEmail(em) + ". It may take a minute; check your spam folder."
for j := 0; j < 10; j++ {
act, cf, err := winForm("code", cmsg, "")
if err != nil || act == "cancel" || act == "" {
return "", false
}
if act == "resend" {
if rerr := auth.InstallResend(rr.PendingID); rerr != nil {
cmsg = "Could not send a new code: " + rerr.Error()
} else {
cmsg = "A new code was sent to " + auth.MaskEmail(em) + "."
}
continue
}
key, verr := auth.InstallVerify(rr.PendingID, cf[0], tokenName())
if verr != nil {
cmsg = verr.Error()
continue
}
return key, true
}
return "", false
}
return "", false
}
// winResetUnavailable: in-app reset not supported; offer the web portal.
func winResetUnavailable() {
_, _, _ = winForm("unavail", "Password reset isn't available in the app yet.\r\n\r\nYou can reset your password on the Verae customer portal in your browser ("+auth.PortalResetURL+").", "")
}
// winForgotFlow: emailed-code password reset. Returns (key, ok, message for the sign-in form).
func winForgotFlow(caps auth.Capabilities, email string) (string, bool, string) {
msg := "Reset your Verae password.\r\nWe'll email a 6-digit code to this address."
for i := 0; i < 10; i++ {
act, f, err := winForm("forgot", msg, email)
if err != nil || act != "send" {
return "", false, ""
}
email = strings.TrimSpace(f[0])
if !auth.ValidEmail(email) {
msg = "Please enter a valid email address."
continue
}
vlog.Printf("sign-in: password reset requested for %s", auth.MaskEmail(email))
if ferr := auth.PasswordForgot(email); ferr != nil {
vlog.Errorf("sign-in: password forgot %s: %v", auth.MaskEmail(email), ferr)
if errors.Is(ferr, auth.ErrResetUnavailable) {
winResetUnavailable()
return "", false, ""
}
msg = "Could not send a reset code: " + ferr.Error()
continue
}
cmsg := "If " + email + " has a Verae account, we emailed it a 6-digit code (valid 15 minutes). Check your spam folder.\r\n" + caps.Policy.Describe()
for j := 0; j < 15; j++ {
act, cf, err := winForm("reset", cmsg, "")
if err != nil || act == "cancel" || act == "" {
return "", false, ""
}
if act == "resend" {
if rerr := auth.PasswordForgot(email); rerr != nil {
if errors.Is(rerr, auth.ErrResetUnavailable) {
winResetUnavailable()
return "", false, ""
}
cmsg = "Could not send a new code: " + rerr.Error()
} else {
cmsg = "A new code was sent to " + email + ".\r\n" + caps.Policy.Describe()
}
continue
}
code, pw := strings.TrimSpace(cf[0]), cf[1]
switch {
case len(code) != 6 || strings.Trim(code, "0123456789") != "":
cmsg = "Enter the 6-digit code from the email.\r\n" + caps.Policy.Describe()
continue
case pw != cf[2]:
cmsg = "The passwords do not match.\r\n" + caps.Policy.Describe()
continue
}
if perr := caps.Policy.Check(pw, email); perr != nil {
cmsg = perr.Error() + "\r\n" + caps.Policy.Describe()
continue
}
key, rerr := auth.PasswordReset(email, code, pw, tokenName())
cf[1], cf[2] = "", ""
if rerr != nil {
vlog.Errorf("sign-in: password reset %s: %v", auth.MaskEmail(email), rerr)
if errors.Is(rerr, auth.ErrResetUnavailable) {
winResetUnavailable()
return "", false, ""
}
cmsg = "Could not reset the password: " + rerr.Error()
continue
}
vlog.Printf("sign-in: password reset ok for %s (key returned=%v)", auth.MaskEmail(email), key != "")
if key != "" {
return key, true, ""
}
key, lerr := auth.InstallLogin(caps, email, pw, "", tokenName())
pw = ""
if lerr != nil {
if errors.Is(lerr, auth.ErrTOTPRequired) {
return "", false, "Your password was changed. Sign in with your new password and your 6-digit authenticator code."
}
return "", false, "Your password was changed, but signing in failed: " + lerr.Error() + "\r\nSign in with your new password."
}
return key, true, ""
}
return "", false, ""
}
return "", false, ""
}
// pasteFlow: masked key field + portal button; validated before saving.
func pasteFlow(reason string) bool {
msg := "Open customer.verae.ai, sign in, open Security and create an xVerae- token (shown once). Paste it below."
for i := 0; i < 10; i++ {
act, f, err := winForm("paste", msg, "")
if err != nil || act != "paste" {
return false
}
key := strings.TrimSpace(f[0])
st, det := auth.ValidateXVeraeKey(key)
vlog.Printf("sign-in: pasted %s validate=%s %s", auth.MaskKey(key), st, det)
switch st {
case "valid", "unreachable", "unknown":
if err := auth.SaveXVeraeToken(key); err != nil {
msg = "Could not save the key: " + err.Error()
continue
}
vlog.Printf("sign-in: key %s saved", auth.MaskKey(key))
return true
case "invalid":
msg = "Verae rejected that key (" + det + "). Please check it and paste again."
default:
msg = "That does not look like a Verae key (" + det + "). Keys start with xVerae-."
}
}
return false
}
// keyDialogFlow is the Start-menu "Setup signing key" entry point.
func keyDialogFlow(reason string) bool { return acquireKeyInteractive(reason) }
func runKeyDialog() int {
vlog.Init("helper", false)
release, ok := promptLock()
if !ok {
messageBox("Verae TimeStamp PDF", "A Verae sign-in window is already open.", 0x40)
return 1
}
defer release()
if keyDialogFlow("Sign in to Verae to set up the signing key for the 'Verae TimeStamp PDF' printer.") {
messageBox("Verae TimeStamp PDF", "Your Verae signing key is saved. Waiting print jobs (if any) will be timestamped within a minute.", 0x40)
return 0
}
return 1
}
var (
user32 = syscall.NewLazyDLL("user32.dll")
pMessageBox = user32.NewProc("MessageBoxW")
)
// messageBox shows a native message box; flags e.g. 0x40 info, 0x30 warning, 0x4 yes/no. Returns button id (6 = Yes).
func messageBox(title, text string, flags uintptr) int {
t, _ := syscall.UTF16PtrFromString(title)
m, _ := syscall.UTF16PtrFromString(text)
r, _, _ := pMessageBox.Call(0, uintptr(unsafe.Pointer(m)), uintptr(unsafe.Pointer(t)), flags|0x00040000 /*MB_TOPMOST*/)
return int(r)
}