diff --git a/cmd/verae-print/acquire_other.go b/cmd/verae-print/acquire_other.go new file mode 100644 index 0000000..5872a56 --- /dev/null +++ b/cmd/verae-print/acquire_other.go @@ -0,0 +1,10 @@ +//go:build !windows && !darwin + +package main + +import "github.com/VeraeLLC/PrintToVeraeTimeStamp/internal/vlog" + +func acquireKeyInteractive(reason string) bool { + vlog.Printf("sign-in needed: %s — run: verae-print setup", reason) + return false +} diff --git a/cmd/verae-print/keydialog_windows.go b/cmd/verae-print/keydialog_windows.go index 157fe9e..4578f97 100644 --- a/cmd/verae-print/keydialog_windows.go +++ b/cmd/verae-print/keydialog_windows.go @@ -3,6 +3,9 @@ package main import ( + "bytes" + "encoding/base64" + "errors" "os" "os/exec" "path/filepath" @@ -14,68 +17,238 @@ import ( "github.com/VeraeLLC/PrintToVeraeTimeStamp/internal/vlog" ) -// WinForms dialog (powershell, console hidden): masked key field + "Get a key" -// button that opens the customer portal. Writes the key to the file in $env:VERAE_KEY_OUT. -const keyDialogPS = ` +// 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 - signing key'; $f.Width = 520; $f.Height = 300; $f.StartPosition = 'CenterScreen'; $f.TopMost = $true +$f.Text = 'Verae TimeStamp PDF'; $f.Width = 540; $f.StartPosition = 'CenterScreen'; $f.TopMost = $true $f.FormBorderStyle = 'FixedDialog'; $f.MaximizeBox = $false; $f.MinimizeBox = $false -$l = New-Object Windows.Forms.Label; $l.Left = 15; $l.Top = 12; $l.Width = 480; $l.Height = 95 -$l.Text = $env:VERAE_KEY_MSG -$f.Controls.Add($l) -$b = New-Object Windows.Forms.Button; $b.Text = 'Get a Verae key (opens customer.verae.ai)'; $b.Left = 15; $b.Top = 110; $b.Width = 300 -$b.Add_Click({ Start-Process 'https://customer.verae.ai/portal/' }); $f.Controls.Add($b) -$k = New-Object Windows.Forms.Label; $k.Text = 'Paste your xVerae- key:'; $k.Left = 15; $k.Top = 150; $k.Width = 300; $f.Controls.Add($k) -$t = New-Object Windows.Forms.TextBox; $t.Left = 15; $t.Top = 172; $t.Width = 475; $t.UseSystemPasswordChar = $true; $f.Controls.Add($t) -$ok = New-Object Windows.Forms.Button; $ok.Text = 'Save key'; $ok.Left = 300; $ok.Top = 215; $ok.Width = 90; $ok.DialogResult = 'OK'; $f.AcceptButton = $ok; $f.Controls.Add($ok) -$c = New-Object Windows.Forms.Button; $c.Text = 'Not now'; $c.Left = 400; $c.Top = 215; $c.Width = 90; $c.DialogResult = 'Cancel'; $f.CancelButton = $c; $f.Controls.Add($c) -if ($f.ShowDialog() -eq 'OK' -and $t.Text.Trim()) { [IO.File]::WriteAllText($env:VERAE_KEY_OUT, $t.Text.Trim()) ; exit 0 } -exit 1 +$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 '' + $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)" } else { $script:result } + } + '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' } + } +} ` -func showKeyForm(msg string) (string, bool) { - tmp, err := os.CreateTemp("", "verae-key-*.txt") - if err != nil { - return "", false - } - path := tmp.Name() - tmp.Close() - defer os.Remove(path) - cmd := exec.Command("powershell.exe", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-WindowStyle", "Hidden", "-Command", keyDialogPS) - cmd.Env = append(os.Environ(), "VERAE_KEY_OUT="+path, "VERAE_KEY_MSG="+msg) +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 { - return "", false + vlog.Errorf("dialog %s: %v", kind, err) + return "", nil, errDialog } - b, err := os.ReadFile(path) - if err != nil { - return "", false + 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)) } - return strings.TrimSpace(string(b)), true + for len(fields) < 4 { + fields = append(fields, "") + } + return parts[0], fields, nil } -// keyDialogFlow loops until a valid key is saved or the user cancels. -func keyDialogFlow(reason string) bool { - msg := reason + "\r\n\r\nClick 'Get a Verae key', sign in (or create an account), open Security and create an xVerae- token, then paste it below." - for i := 0; i < 5; i++ { - key, ok := showKeyForm(msg) - if !ok { - vlog.Printf("key-dialog: cancelled by user") +// 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() + continue + } + 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 +} + +// 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("key-dialog: entered %s validate=%s %s", auth.MaskKey(key), st, det) + 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 } - home, _ := os.UserHomeDir() - vlog.Printf("key-dialog: saved %s to %s", auth.MaskKey(key), filepath.Join(home, ".verae", "xverae-token")) - messageBox("Verae TimeStamp PDF", "Signing key saved ("+auth.MaskKey(key)+").", 0x40) + 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." @@ -86,9 +259,19 @@ func keyDialogFlow(reason string) bool { 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) - if keyDialogFlow("Set up your Verae signing key for the 'Verae TimeStamp PDF' printer.") { + 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 diff --git a/cmd/verae-print/keysetup_darwin.go b/cmd/verae-print/keysetup_darwin.go new file mode 100644 index 0000000..6fe3d5e --- /dev/null +++ b/cmd/verae-print/keysetup_darwin.go @@ -0,0 +1,370 @@ +//go:build darwin + +package main + +import ( + "errors" + "flag" + "fmt" + "os" + "os/exec" + "strings" + + "github.com/VeraeLLC/PrintToVeraeTimeStamp/internal/auth" + "github.com/VeraeLLC/PrintToVeraeTimeStamp/internal/settings" + "github.com/VeraeLLC/PrintToVeraeTimeStamp/internal/vlog" +) + +// macOS in-installer key registration, run by the pkg postinstall as the +// console user (launchctl asuser + sudo -H -u). Native AppleScript dialogs; +// passwords come back through osascript's stdout pipe only — never argv, +// never files, never logs. Keys are logged masked only. + +const dlgTitle = "Verae Print Stamp" + +// AppleScript gets all user-visible text via argv (no string interpolation → +// no injection). Output: "