windows-debug: elevated GUI installer with Verae key pages (sign in / create account + email code / paste / skip), hidden scripts, per-step finish page, user-scoped key save + helper scheduled task; installer registration API spec

This commit is contained in:
Grok Bot (for George) 2026-10-03 19:14:49 -04:00
commit 6e76bc7b0e
7 changed files with 1857 additions and 61 deletions

View file

@ -0,0 +1,210 @@
package main
import (
"encoding/json"
"errors"
"flag"
"fmt"
"os"
"runtime"
"strings"
"unicode/utf16"
"github.com/VeraeLLC/PrintToVeraeTimeStamp/internal/auth"
"github.com/VeraeLLC/PrintToVeraeTimeStamp/internal/vlog"
)
// writeINI writes a one-section INI (ASCII) for NSIS ReadINIStr.
func writeINI(path, section string, kv [][2]string) error {
var b strings.Builder
fmt.Fprintf(&b, "[%s]\r\n", section)
for _, p := range kv {
v := strings.NewReplacer("\r", " ", "\n", " ", "=", ":").Replace(p[1])
fmt.Fprintf(&b, "%s=%s\r\n", p[0], v)
}
return os.WriteFile(path, []byte(b.String()), 0o600)
}
func tokenName() string {
h, _ := os.Hostname()
return fmt.Sprintf("Verae Print Stamp - %s (%s)", h, runtime.GOOS)
}
// runInstallerAccount is driven (hidden) by the NSIS key pages.
//
// --op capabilities → [account] installApi, emailVerification, policy*
// --op login|register|verify|resend --in req.json (deleted immediately)
//
// Secrets arrive only via the request file; minted key goes to --token-out.
// Passwords and keys are never logged (keys masked, emails masked).
func runInstallerAccount(args []string) int {
fs := flag.NewFlagSet("installer-account", flag.ContinueOnError)
op := fs.String("op", "", "capabilities|login|register|verify|resend")
in := fs.String("in", "", "request json (deleted after read)")
out := fs.String("out", "", "result ini")
tokOut := fs.String("token-out", "", "file to receive minted key")
if err := fs.Parse(args); err != nil || *op == "" || *out == "" {
return 2
}
vlog.Init("installer", false)
res := func(kv ...[2]string) int { _ = writeINI(*out, "account", kv); return 0 }
fail := func(err error) int {
vlog.Errorf("installer-account %s: %v", *op, err)
if errors.Is(err, auth.ErrTOTPRequired) {
return res([2]string{"result", "totp_required"}, [2]string{"detail", "This account uses two-factor authentication. Enter the 6-digit code from your authenticator app."})
}
return res([2]string{"result", "error"}, [2]string{"detail", err.Error()})
}
caps := auth.GetCapabilities()
if *op == "capabilities" {
p := caps.Policy
vlog.Printf("installer-account: capabilities installApi=%v emailVerification=%v policy=%+v", caps.InstallAPI, caps.EmailVerification, p)
return res([2]string{"result", "ok"}, [2]string{"installApi", b2s(caps.InstallAPI)}, [2]string{"emailVerification", b2s(caps.EmailVerification)},
[2]string{"policyText", p.Describe()}, [2]string{"minLength", fmt.Sprint(p.MinLength)})
}
var r struct{ Name, Email, Password, Confirm, Totp, Code, PendingID string }
if *in != "" {
raw, err := readTextFile(*in)
_ = os.Remove(*in)
if err != nil {
return fail(err)
}
if strings.HasPrefix(strings.TrimSpace(raw), "{") {
if err := json.Unmarshal([]byte(raw), &r); err != nil {
return fail(fmt.Errorf("bad request"))
}
} else {
// NSIS writes "Key=Value" lines (UTF-16LE); split on the first '=' only.
for _, line := range strings.Split(strings.ReplaceAll(raw, "\r\n", "\n"), "\n") {
k, v, ok := strings.Cut(line, "=")
if !ok {
continue
}
switch strings.ToLower(strings.TrimSpace(k)) {
case "name":
r.Name = v
case "email":
r.Email = v
case "password":
r.Password = v
case "confirm":
r.Confirm = v
case "totp":
r.Totp = v
case "code":
r.Code = v
case "pendingid":
r.PendingID = v
}
}
}
}
r.Email = strings.TrimSpace(r.Email)
saveKey := func(key string) int {
if *tokOut == "" {
return fail(errors.New("no --token-out"))
}
if err := os.WriteFile(*tokOut, []byte(key+"\n"), 0o600); err != nil {
return fail(err)
}
st, det := auth.ValidateXVeraeKey(key)
vlog.Printf("installer-account %s: key issued %s validate=%s %s", *op, auth.MaskKey(key), st, det)
return res([2]string{"result", "ok"}, [2]string{"masked", auth.MaskKey(key)}, [2]string{"validate", st}, [2]string{"detail", det})
}
switch *op {
case "login":
if !auth.ValidEmail(r.Email) || r.Password == "" {
return fail(errors.New("enter your email and password"))
}
vlog.Printf("installer-account: login %s installApi=%v", auth.MaskEmail(r.Email), caps.InstallAPI)
key, err := auth.InstallLogin(caps, r.Email, r.Password, strings.TrimSpace(r.Totp), tokenName())
if err != nil {
return fail(err)
}
return saveKey(key)
case "register":
switch {
case strings.TrimSpace(r.Name) == "":
return fail(errors.New("enter your name"))
case !auth.ValidEmail(r.Email):
return fail(errors.New("enter a valid email address"))
case r.Password != r.Confirm:
return fail(errors.New("passwords do not match"))
}
if err := caps.Policy.Check(r.Password, r.Email); err != nil {
return fail(err)
}
vlog.Printf("installer-account: register %s installApi=%v", auth.MaskEmail(r.Email), caps.InstallAPI)
rr, err := auth.InstallRegister(caps, strings.TrimSpace(r.Name), r.Email, r.Password, tokenName())
if err != nil {
return fail(err)
}
if rr.PendingID != "" {
vlog.Printf("installer-account: verification code sent to %s", auth.MaskEmail(r.Email))
return res([2]string{"result", "code_sent"}, [2]string{"pendingId", rr.PendingID}, [2]string{"detail", "We emailed a 6-digit code to " + auth.MaskEmail(r.Email)})
}
return saveKey(rr.Key)
case "verify":
if r.PendingID == "" || strings.TrimSpace(r.Code) == "" {
return fail(errors.New("enter the code from the email"))
}
key, err := auth.InstallVerify(r.PendingID, r.Code, tokenName())
if err != nil {
return fail(err)
}
return saveKey(key)
case "resend":
if err := auth.InstallResend(r.PendingID); err != nil {
return fail(err)
}
return res([2]string{"result", "ok"}, [2]string{"detail", "A new code was sent."})
}
return fail(fmt.Errorf("unknown op %q", *op))
}
func b2s(b bool) string {
if b {
return "1"
}
return "0"
}
// runValidateKey: --token-file F --out INI → [validate] result=valid|invalid|badformat|unreachable|unknown
func runValidateKey(args []string) int {
fs := flag.NewFlagSet("validate-key", flag.ContinueOnError)
tf := fs.String("token-file", "", "file with key")
out := fs.String("out", "", "result ini (else stdout)")
if err := fs.Parse(args); err != nil || *tf == "" {
return 2
}
b, err := readTextFile(*tf)
if err != nil {
fmt.Fprintln(os.Stderr, err)
return 1
}
key := strings.TrimSpace(b)
st, det := auth.ValidateXVeraeKey(key)
if *out != "" {
_ = writeINI(*out, "validate", [][2]string{{"result", st}, {"detail", det}, {"masked", auth.MaskKey(key)}})
} else {
fmt.Printf("%s %s %s\n", auth.MaskKey(key), st, det)
}
return 0
}
// readTextFile decodes UTF-16LE (with BOM, as NSIS FileWriteUTF16LE writes),
// UTF-8 with/without BOM.
func readTextFile(p string) (string, error) {
b, err := os.ReadFile(p)
if err != nil {
return "", err
}
if len(b) >= 2 && b[0] == 0xFF && b[1] == 0xFE {
u := make([]uint16, 0, len(b)/2)
for i := 2; i+1 < len(b); i += 2 {
u = append(u, uint16(b[i])|uint16(b[i+1])<<8)
}
return string(utf16.Decode(u)), nil
}
return strings.TrimPrefix(string(b), "\uFEFF"), nil
}

View file

@ -0,0 +1,23 @@
package main
import (
"os"
"path/filepath"
"testing"
"unicode/utf16"
)
func TestReadTextFileUTF16(t *testing.T) {
s := "Email=a@b.co\r\nPassword=p=\"w\\x\r\n"
u := utf16.Encode([]rune(s))
b := []byte{0xFF, 0xFE}
for _, c := range u {
b = append(b, byte(c), byte(c>>8))
}
p := filepath.Join(t.TempDir(), "r.txt")
_ = os.WriteFile(p, b, 0o600)
got, err := readTextFile(p)
if err != nil || got != s {
t.Fatalf("%q %v", got, err)
}
}

View file

@ -0,0 +1,108 @@
//go:build windows
package main
import (
"os"
"os/exec"
"path/filepath"
"strings"
"syscall"
"unsafe"
"github.com/VeraeLLC/PrintToVeraeTimeStamp/internal/auth"
"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 = `
Add-Type -AssemblyName System.Windows.Forms, System.Drawing
[System.Windows.Forms.Application]::EnableVisualStyles()
$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.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
`
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)
hideWindow(cmd)
if err := cmd.Run(); err != nil {
return "", false
}
b, err := os.ReadFile(path)
if err != nil {
return "", false
}
return strings.TrimSpace(string(b)), true
}
// 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")
return false
}
st, det := auth.ValidateXVeraeKey(key)
vlog.Printf("key-dialog: entered %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)
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
}
func runKeyDialog() int {
vlog.Init("helper", false)
if keyDialogFlow("Set up your Verae signing key for the 'Verae TimeStamp PDF' printer.") {
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)
}

View file

@ -31,6 +31,12 @@ func main() {
os.Exit(runServe(os.Args[2:]))
case "ipp-selftest":
os.Exit(runIPPSelftest(os.Args[2:]))
case "installer-account":
os.Exit(runInstallerAccount(os.Args[2:]))
case "validate-key":
os.Exit(runValidateKey(os.Args[2:]))
case "key-dialog":
os.Exit(runKeyDialog())
case "setup", "login":
os.Exit(runSetup())
case "install-macos":

View file

@ -36,3 +36,8 @@ func launchSetupUI() { vlog.Printf("setup: no signing key — run: verae-print s
func runInstallPrinterWindows() int { return 1 }
func debugFromRegistry() bool { return false }
func runKeyDialog() int {
fmt.Println("key-dialog is Windows-only; run: verae-print setup")
return 1
}

View file

@ -37,11 +37,17 @@ func notify(title, subtitle string) { notifyUser(title, subtitle) }
func openFolderPath(p string) { _ = exec.Command("explorer.exe", p).Start() }
// launchSetupUI opens a console running "verae-print setup" for the user.
// launchSetupUI asks the user (GUI, no console) to set up a key, then
// re-processes jobs that were held because no key was present.
func launchSetupUI() {
cli := filepath.Join(exeDir(), "verae-print.exe")
vlog.Printf("setup: no signing key — launching %q setup in a new console", cli)
_ = exec.Command("cmd.exe", "/c", "start", "Verae Print Stamp setup", cli, "setup").Start()
go func() {
vlog.Printf("setup: no signing key — prompting user (GUI)")
if keyDialogFlow("Your print job was saved but not yet timestamped because no Verae signing key is set up on this PC.") {
retryHeldJobs()
} else {
notifyUser("Verae key not set", "Print jobs are kept and will be timestamped after you set up a key (Start > Verae > Setup signing key).")
}
}()
}
func runInstallPrinterWindows() int {

View file

@ -102,10 +102,43 @@ func errState(err error) string {
return "missing (" + err.Error() + ")"
}
var setupOnce sync.Once
var (
setupMu sync.Mutex
setupActive bool
heldMu sync.Mutex
heldJobs []winprint.Job
processor winprint.Processor
)
// promptSetup shows the key dialog at most once at a time.
func promptSetup() {
setupMu.Lock()
defer setupMu.Unlock()
if setupActive {
return
}
setupActive = true
launchSetupUI()
}
func retryHeldJobs() {
setupMu.Lock()
setupActive = false
setupMu.Unlock()
heldMu.Lock()
jobs := heldJobs
heldJobs = nil
heldMu.Unlock()
for _, j := range jobs {
vlog.Job(j.ID).Printf("retry: re-processing held job after key setup")
if err := processor(j); err != nil {
vlog.Job(j.ID).Errorf("retry failed: %v", err)
}
}
}
func makeProcessor(outDir string) winprint.Processor {
return func(j winprint.Job) error {
processor = func(j winprint.Job) error {
lj := vlog.Job(j.ID)
lj.Printf("process: source=%s title=%q user=%q format=%s bytes=%d path=%q", j.Source, j.Title, j.User, j.Format, j.Bytes, j.Path)
if !strings.Contains(j.Format, "pdf") && sniffPDF(j.Path) == false {
@ -122,7 +155,10 @@ func makeProcessor(outDir string) winprint.Processor {
ch, client.Channel, client.VeraeToken = auth.ChannelVerae, auth.ChannelVerae, jwt
break
}
setupOnce.Do(launchSetupUI)
heldMu.Lock()
heldJobs = append(heldJobs, j)
heldMu.Unlock()
promptSetup()
return fmt.Errorf("no signing key: %v — run 'verae-print setup' then reprint (PDF kept at %s)", err, j.Path)
}
client.XVeraeToken = tok
@ -147,6 +183,7 @@ func makeProcessor(outDir string) winprint.Processor {
notifyUser(fmt.Sprintf("Verae timestamp %s", res.ReceiptID), res.CertificatePDF)
return nil
}
return processor
}
func loggingDo(lj vlog.Job) func(*http.Request) (*http.Response, error) {