291 lines
10 KiB
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)
|
|
}
|