PrintToVeraeTimeStamp/cmd/verae-print/keydialog_windows.go

291 lines
10 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 ''
$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' }
}
}
`
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()
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("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)
}