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:
parent
1cb714b173
commit
6e76bc7b0e
18 changed files with 1857 additions and 61 deletions
210
cmd/verae-print/installer_cmds.go
Normal file
210
cmd/verae-print/installer_cmds.go
Normal 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
|
||||
}
|
||||
23
cmd/verae-print/installer_cmds_test.go
Normal file
23
cmd/verae-print/installer_cmds_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
108
cmd/verae-print/keydialog_windows.go
Normal file
108
cmd/verae-print/keydialog_windows.go
Normal 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)
|
||||
}
|
||||
|
|
@ -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":
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
204
docs/installer-registration-api.md
Normal file
204
docs/installer-registration-api.md
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
# Verae installer registration API (proposed)
|
||||
|
||||
Status: **proposal + client implementation**. The client side is implemented in
|
||||
`internal/auth/portal_install.go` and driven by the Windows installer
|
||||
(`platform/windows/installer-debug.nsi` → `verae-print installer-account`).
|
||||
The server side does **not exist yet**. The portal backend source was not
|
||||
available, so nothing was changed or deployed on the server. Until the server
|
||||
implements it, the client automatically falls back to the endpoints the portal
|
||||
web client already uses (see §5).
|
||||
|
||||
## 1. Why a new API
|
||||
|
||||
How Verae accounts work today (from `https://customer.verae.ai/portal/app.js`):
|
||||
|
||||
| Today | Endpoint | Notes |
|
||||
|---|---|---|
|
||||
| Sign in | `POST /portal/api/login` `{email,password,totpCode?}` | → `{token:<session>}`; error `totp_required` when 2FA is on |
|
||||
| Sign up | `POST /portal/api/signup` `{name,email,password}` | → session **immediately**; **no email verification** |
|
||||
| Mint key | `POST /portal/api/tokens` (Bearer session) `{name,expiresInDays?}` | → `{token:"xVerae-…"}` (shown once) |
|
||||
| Sign out | `POST /portal/api/logout` | |
|
||||
| Validate key | `GET https://zap.verae.ai/v1/status` with `x-api-key` | free; 401 = invalid |
|
||||
|
||||
Gaps for a desktop installer:
|
||||
1. No email-ownership check at signup. Anyone can register any address.
|
||||
2. No published password policy. The installer cannot show or enforce the rule the server uses.
|
||||
3. Getting a key takes three calls (login → tokens → logout), and a full web session is handed to a desktop client.
|
||||
4. No device/installer-specific rate limiting or audit field.
|
||||
|
||||
## 2. Endpoints (all under `https://customer.verae.ai/portal/api`)
|
||||
|
||||
```yaml
|
||||
openapi: 3.0.3
|
||||
info: { title: Verae installer registration, version: 1.0.0 }
|
||||
servers: [ { url: https://customer.verae.ai/portal/api } ]
|
||||
paths:
|
||||
/install/capabilities:
|
||||
get:
|
||||
summary: Feature discovery + password policy (no auth)
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/Capabilities" }
|
||||
"404": { description: Not implemented → client uses legacy flow (§5) }
|
||||
/install/register:
|
||||
post:
|
||||
summary: Start account creation; emails a 6-digit code
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [name, email, password]
|
||||
properties:
|
||||
name: { type: string, maxLength: 200 }
|
||||
email: { type: string, format: email }
|
||||
password: { type: string, format: password }
|
||||
client: { $ref: "#/components/schemas/Client" }
|
||||
responses:
|
||||
"202":
|
||||
description: Code sent (also returned when the email already exists, to avoid account enumeration; in that case the email says "you already have an account – sign in instead")
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
pendingId: { type: string, description: "opaque, ≥128-bit random" }
|
||||
expiresInSeconds: { type: integer, example: 900 }
|
||||
resendAfterSeconds: { type: integer, example: 60 }
|
||||
"400": { $ref: "#/components/responses/Error" } # weak_password, invalid_email, name_required
|
||||
"429": { $ref: "#/components/responses/Error" }
|
||||
/install/verify:
|
||||
post:
|
||||
summary: Confirm the code; creates the account and issues a key
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [pendingId, code]
|
||||
properties:
|
||||
pendingId: { type: string }
|
||||
code: { type: string, pattern: "^[0-9]{6}$" }
|
||||
tokenName: { type: string, example: "Verae Print Stamp - NIMO (windows)" }
|
||||
responses:
|
||||
"201":
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/Issued" }
|
||||
"400": { $ref: "#/components/responses/Error" } # invalid_code (attemptsLeft), expired
|
||||
"410": { $ref: "#/components/responses/Error" } # too many attempts / expired pendingId
|
||||
/install/resend:
|
||||
post:
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema: { type: object, required: [pendingId], properties: { pendingId: { type: string } } }
|
||||
responses:
|
||||
"202": { description: New code sent; previous code invalidated }
|
||||
"429": { $ref: "#/components/responses/Error" }
|
||||
/install/login:
|
||||
post:
|
||||
summary: Existing account → issue a key in one call (no web session returned)
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [email, password]
|
||||
properties:
|
||||
email: { type: string }
|
||||
password: { type: string, format: password }
|
||||
totpCode: { type: string }
|
||||
tokenName: { type: string }
|
||||
client: { $ref: "#/components/schemas/Client" }
|
||||
responses:
|
||||
"201":
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/Issued" }
|
||||
"401":
|
||||
description: "error = invalid_credentials | totp_required | totp_invalid | email_unverified"
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/Error" }
|
||||
"429": { $ref: "#/components/responses/Error" }
|
||||
components:
|
||||
responses:
|
||||
Error:
|
||||
description: Error
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/Error" }
|
||||
schemas:
|
||||
Error:
|
||||
type: object
|
||||
properties:
|
||||
error: { type: string, description: machine code }
|
||||
message: { type: string, description: human text shown verbatim in the installer }
|
||||
attemptsLeft: { type: integer }
|
||||
retryAfterSeconds: { type: integer }
|
||||
Capabilities:
|
||||
type: object
|
||||
properties:
|
||||
installApi: { type: boolean }
|
||||
emailVerification: { type: boolean }
|
||||
passwordPolicy:
|
||||
type: object
|
||||
properties:
|
||||
minLength: { type: integer, example: 10 }
|
||||
requireLetter: { type: boolean }
|
||||
requireDigit: { type: boolean }
|
||||
requireSymbol: { type: boolean }
|
||||
disallowEmail: { type: boolean }
|
||||
Client:
|
||||
type: object
|
||||
properties:
|
||||
product: { type: string, example: verae-print }
|
||||
version: { type: string, example: 0.3.0-debug }
|
||||
os: { type: string, example: windows }
|
||||
host: { type: string }
|
||||
Issued:
|
||||
type: object
|
||||
properties:
|
||||
token: { type: string, example: "xVerae-…" }
|
||||
tokenId: { type: string }
|
||||
name: { type: string }
|
||||
expiresAt: { type: string, format: date-time, nullable: true }
|
||||
```
|
||||
|
||||
## 3. Password policy
|
||||
|
||||
* The server is the source of truth. It publishes the policy in `capabilities.passwordPolicy`, enforces it in `/install/register` (and in `/signup` too, for consistency), and rejects a failing password with `400 {error:"weak_password", message:"…"}`.
|
||||
* The client checks the same rule before sending (`PasswordPolicy.Check`) and shows `Describe()` text on the Create page. Password and confirm must match (checked in the installer and again in the CLI).
|
||||
* Client default when the server publishes nothing: **≥10 characters, at least one letter and one digit, must not contain the email's local part**. Recommend also checking against a breached-password list (k-anonymity HIBP range API) on the server.
|
||||
|
||||
## 4. Security requirements
|
||||
|
||||
* **HTTPS only.** The client refuses non-https portal URLs (`VERAE_PORTAL_BASE` override, plus `VERAE_ALLOW_INSECURE_PORTAL=1` only for local tests). Server: HSTS, TLS 1.2+.
|
||||
* **Secrets are never logged** by either side. Mask request bodies on `/install/*`, `/login`, `/signup`, `/tokens` in access logs and APM. Client: passwords are never written to any log. Keys are only logged masked (`xVerae-AbC1…9xYz`). The installer passes secrets to the helper only through a temp file in its private `$PLUGINSDIR`, which is deleted right after it is read. Secrets are never passed on a command line.
|
||||
* **The password is never stored on the client.** Only the issued key is saved, to `<target user profile>\.verae\xverae-token` (`token + "\n"`, the format of `auth.SaveXVeraeToken`). The ACL is the user + SYSTEM + Administrators, with inheritance removed. If the installer is elevated with a different admin account, the key still goes to the *logged-in* user (owner of explorer.exe).
|
||||
* **Codes**: 6 digits, CSPRNG, stored hashed. TTL 15 min, 5 attempts per pendingId, resend at most once per 60 s and 5 per hour. A new code invalidates the old one.
|
||||
* **Rate limits** (suggested): register 5/h per IP and 3/h per email; login 10 per 15 min per IP+email, with exponential back-off; verify 5 per pendingId. Return 429 with `retryAfterSeconds`.
|
||||
* **No enumeration**: register answers 202 whether or not the email exists. Login uses one generic `invalid_credentials`.
|
||||
* **The key is scoped and auditable**: name it after `tokenName`, record `client` (product/version/os/host), and let users list and revoke it in the portal Security tab as usual.
|
||||
* `/install/login` does not return a session cookie or token, only the key. It does not create a web session (or revokes it immediately).
|
||||
|
||||
## 5. Client fallback (today's server)
|
||||
|
||||
`GET /install/capabilities` → 404 (or a network error) ⇒ `installApi=0`:
|
||||
* Existing account: `POST /login` → `POST /tokens {name}` → `POST /logout`. `totp_required` is shown to the user and the TOTP field is used.
|
||||
* New account: the password policy is checked client-side, then `POST /signup` → `POST /tokens` → `POST /logout`. **No email code** (the Create page says so). The Verify page is skipped.
|
||||
* The key is validated afterwards with `GET zap.verae.ai/v1/status` (free).
|
||||
|
||||
## 6. Installer UI mapping
|
||||
|
||||
| Page | Calls |
|
||||
|---|---|
|
||||
| Key status | `user-token.ps1 -Mode status` (masked key + free validation), `installer-account --op capabilities` |
|
||||
| Sign in | `installer-account --op login` (email, password, optional TOTP) |
|
||||
| Create account | `--op register` → `code_sent` (Verify page) or `ok` (legacy) |
|
||||
| Verify email | `--op verify`; "Send a new code" → `--op resend` |
|
||||
| Paste key | button opens customer.verae.ai; `validate-key` before Next |
|
||||
| Skip | warning; the helper shows a key dialog on first print and holds the job until a key is saved |
|
||||
143
internal/auth/portal.go
Normal file
143
internal/auth/portal.go
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Portal account API — the same JSON endpoints the customer.verae.ai/portal web
|
||||
// client (portal/app.js) uses: POST /portal/api/login|signup → {token: session},
|
||||
// POST /portal/api/tokens {name} (Bearer session) → {token: "xVerae-…"},
|
||||
// POST /portal/api/logout. There is no device-code / email-code flow, so the
|
||||
// installer uses email+password (+ optional authenticator code) and mints a key.
|
||||
|
||||
// PortalBase is overridable for testing via VERAE_PORTAL_BASE.
|
||||
func PortalBase() string {
|
||||
if v := strings.TrimRight(os.Getenv("VERAE_PORTAL_BASE"), "/"); v != "" {
|
||||
return v
|
||||
}
|
||||
return "https://customer.verae.ai"
|
||||
}
|
||||
|
||||
// ErrTOTPRequired means the account has 2FA and a 6-digit code is needed.
|
||||
var ErrTOTPRequired = fmt.Errorf("totp_required")
|
||||
|
||||
var portalHTTP = &http.Client{Timeout: 20 * time.Second}
|
||||
|
||||
func decodeJSON(res *http.Response, v any) error {
|
||||
return json.NewDecoder(io.LimitReader(res.Body, 1<<20)).Decode(v)
|
||||
}
|
||||
|
||||
func portalPost(path, bearer string, body any) (map[string]any, int, error) {
|
||||
if !strings.HasPrefix(PortalBase(), "https://") && os.Getenv("VERAE_ALLOW_INSECURE_PORTAL") != "1" {
|
||||
return nil, 0, fmt.Errorf("refusing non-HTTPS portal base %q", PortalBase())
|
||||
}
|
||||
b, _ := json.Marshal(body)
|
||||
req, err := http.NewRequest(http.MethodPost, PortalBase()+"/portal/api"+path, bytes.NewReader(b))
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("User-Agent", "VeraePrintStamp-Installer")
|
||||
if bearer != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+bearer)
|
||||
}
|
||||
res, err := portalHTTP.Do(req)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
raw, _ := io.ReadAll(io.LimitReader(res.Body, 1<<20))
|
||||
out := map[string]any{}
|
||||
_ = json.Unmarshal(raw, &out)
|
||||
if res.StatusCode >= 300 {
|
||||
msg, _ := out["error"].(string)
|
||||
if msg == "totp_required" {
|
||||
return out, res.StatusCode, ErrTOTPRequired
|
||||
}
|
||||
if msg == "" {
|
||||
msg = strings.TrimSpace(string(raw))
|
||||
if len(msg) > 200 {
|
||||
msg = msg[:200]
|
||||
}
|
||||
}
|
||||
return out, res.StatusCode, fmt.Errorf("HTTP %d: %s", res.StatusCode, msg)
|
||||
}
|
||||
return out, res.StatusCode, nil
|
||||
}
|
||||
|
||||
// PortalMintKey signs in (or signs up), mints a long-lived xVerae- token named
|
||||
// tokenName, and signs out. Credentials are never stored.
|
||||
func PortalMintKey(signup bool, name, email, password, totp, tokenName string) (string, error) {
|
||||
var body map[string]any
|
||||
path := "/login"
|
||||
if signup {
|
||||
path = "/signup"
|
||||
body = map[string]any{"name": name, "email": email, "password": password}
|
||||
} else {
|
||||
body = map[string]any{"email": email, "password": password}
|
||||
if totp != "" {
|
||||
body["totpCode"] = totp
|
||||
}
|
||||
}
|
||||
out, _, err := portalPost(path, "", body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
session, _ := out["token"].(string)
|
||||
if session == "" {
|
||||
return "", fmt.Errorf("portal %s returned no session", path)
|
||||
}
|
||||
defer func() { _, _, _ = portalPost("/logout", session, map[string]any{}) }()
|
||||
tk, _, err := portalPost("/tokens", session, map[string]any{"name": tokenName})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("mint key: %w", err)
|
||||
}
|
||||
key, _ := tk["token"].(string)
|
||||
if !strings.HasPrefix(key, "xVerae-") && !strings.HasPrefix(key, "key-") {
|
||||
return "", fmt.Errorf("mint key: unexpected response (no xVerae- token)")
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
|
||||
// ValidateXVeraeKey checks a key against zap.verae.ai GET /v1/status (documented
|
||||
// as free). Returns "valid", "invalid", "badformat", "unreachable" or "unknown".
|
||||
func ValidateXVeraeKey(key string) (string, string) {
|
||||
key = strings.TrimSpace(key)
|
||||
if !(strings.HasPrefix(key, "xVerae-") || strings.HasPrefix(key, "key-")) || len(key) < 16 || strings.ContainsAny(key, " \t\r\n") {
|
||||
return "badformat", "must start with xVerae- (or legacy key-)"
|
||||
}
|
||||
req, _ := http.NewRequest(http.MethodGet, "https://zap.verae.ai/v1/status", nil)
|
||||
req.Header.Set("x-api-key", key)
|
||||
res, err := (&http.Client{Timeout: 10 * time.Second}).Do(req)
|
||||
if err != nil {
|
||||
return "unreachable", err.Error()
|
||||
}
|
||||
res.Body.Close()
|
||||
switch {
|
||||
case res.StatusCode < 300:
|
||||
return "valid", fmt.Sprintf("HTTP %d", res.StatusCode)
|
||||
case res.StatusCode == 401 || res.StatusCode == 403:
|
||||
return "invalid", fmt.Sprintf("HTTP %d from Verae", res.StatusCode)
|
||||
default:
|
||||
return "unknown", fmt.Sprintf("HTTP %d from Verae", res.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
// MaskKey renders xVerae-...abcd.
|
||||
func MaskKey(k string) string {
|
||||
k = strings.TrimSpace(k)
|
||||
if len(k) <= 12 {
|
||||
if len(k) > 4 {
|
||||
return k[:4] + "..."
|
||||
}
|
||||
return "..."
|
||||
}
|
||||
return k[:7] + "..." + k[len(k)-4:]
|
||||
}
|
||||
192
internal/auth/portal_install.go
Normal file
192
internal/auth/portal_install.go
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
// Installer-friendly registration API (proposed; spec in
|
||||
// docs/installer-registration-api.md). When the server does not implement it
|
||||
// (GET /portal/api/install/capabilities → 404) the client falls back to the
|
||||
// endpoints the portal web client uses today (/login, /signup, /tokens), which
|
||||
// have no email-verification step.
|
||||
|
||||
// PasswordPolicy mirrors GET /portal/api/install/capabilities .passwordPolicy.
|
||||
type PasswordPolicy struct {
|
||||
MinLength int `json:"minLength"`
|
||||
RequireLetter bool `json:"requireLetter"`
|
||||
RequireDigit bool `json:"requireDigit"`
|
||||
RequireSymbol bool `json:"requireSymbol"`
|
||||
DisallowEmail bool `json:"disallowEmail"`
|
||||
}
|
||||
|
||||
// DefaultPolicy is used when the server does not publish one (conservative).
|
||||
var DefaultPolicy = PasswordPolicy{MinLength: 10, RequireLetter: true, RequireDigit: true, DisallowEmail: true}
|
||||
|
||||
// Capabilities describes what the server supports for installers.
|
||||
type Capabilities struct {
|
||||
InstallAPI bool
|
||||
EmailVerification bool
|
||||
Policy PasswordPolicy
|
||||
}
|
||||
|
||||
// Describe renders the policy for UI text.
|
||||
func (p PasswordPolicy) Describe() string {
|
||||
parts := []string{fmt.Sprintf("at least %d characters", p.MinLength)}
|
||||
if p.RequireLetter {
|
||||
parts = append(parts, "a letter")
|
||||
}
|
||||
if p.RequireDigit {
|
||||
parts = append(parts, "a number")
|
||||
}
|
||||
if p.RequireSymbol {
|
||||
parts = append(parts, "a symbol")
|
||||
}
|
||||
if p.DisallowEmail {
|
||||
parts = append(parts, "not containing your email")
|
||||
}
|
||||
return "Password: " + strings.Join(parts, ", ")
|
||||
}
|
||||
|
||||
// Check validates a password client-side.
|
||||
func (p PasswordPolicy) Check(pw, email string) error {
|
||||
if len([]rune(pw)) < p.MinLength {
|
||||
return fmt.Errorf("password must be at least %d characters", p.MinLength)
|
||||
}
|
||||
var l, d, s bool
|
||||
for _, r := range pw {
|
||||
switch {
|
||||
case unicode.IsLetter(r):
|
||||
l = true
|
||||
case unicode.IsDigit(r):
|
||||
d = true
|
||||
case !unicode.IsSpace(r):
|
||||
s = true
|
||||
}
|
||||
}
|
||||
if p.RequireLetter && !l {
|
||||
return errors.New("password must contain a letter")
|
||||
}
|
||||
if p.RequireDigit && !d {
|
||||
return errors.New("password must contain a number")
|
||||
}
|
||||
if p.RequireSymbol && !s {
|
||||
return errors.New("password must contain a symbol")
|
||||
}
|
||||
if p.DisallowEmail && email != "" {
|
||||
local := strings.ToLower(strings.SplitN(email, "@", 2)[0])
|
||||
if len(local) >= 3 && strings.Contains(strings.ToLower(pw), local) {
|
||||
return errors.New("password must not contain your email name")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var emailRe = regexp.MustCompile(`^[^@\s]+@[^@\s]+\.[^@\s]+$`)
|
||||
|
||||
// ValidEmail is a light syntax check.
|
||||
func ValidEmail(e string) bool { return emailRe.MatchString(strings.TrimSpace(e)) }
|
||||
|
||||
// MaskEmail renders g***@example.com for logs.
|
||||
func MaskEmail(e string) string {
|
||||
at := strings.LastIndex(e, "@")
|
||||
if at < 1 {
|
||||
return "***"
|
||||
}
|
||||
return e[:1] + "***" + e[at:]
|
||||
}
|
||||
|
||||
// GetCapabilities probes GET /portal/api/install/capabilities.
|
||||
func GetCapabilities() Capabilities {
|
||||
c := Capabilities{Policy: DefaultPolicy}
|
||||
req, _ := http.NewRequest(http.MethodGet, PortalBase()+"/portal/api/install/capabilities", nil)
|
||||
req.Header.Set("User-Agent", "VeraePrintStamp-Installer")
|
||||
res, err := portalHTTP.Do(req)
|
||||
if err != nil {
|
||||
return c
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != 200 || !strings.Contains(res.Header.Get("Content-Type"), "json") {
|
||||
return c
|
||||
}
|
||||
var body struct {
|
||||
InstallAPI bool `json:"installApi"`
|
||||
EmailVerification bool `json:"emailVerification"`
|
||||
PasswordPolicy *PasswordPolicy `json:"passwordPolicy"`
|
||||
}
|
||||
if err := decodeJSON(res, &body); err != nil {
|
||||
return c
|
||||
}
|
||||
c.InstallAPI, c.EmailVerification = body.InstallAPI, body.EmailVerification
|
||||
if body.PasswordPolicy != nil && body.PasswordPolicy.MinLength > 0 {
|
||||
c.Policy = *body.PasswordPolicy
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// RegisterResult is returned by InstallRegister.
|
||||
type RegisterResult struct {
|
||||
PendingID string // non-empty → email code sent; call InstallVerify
|
||||
Key string // non-empty → account created and key minted (fallback path)
|
||||
}
|
||||
|
||||
// InstallRegister starts a new-account registration.
|
||||
func InstallRegister(caps Capabilities, name, email, password, tokenName string) (RegisterResult, error) {
|
||||
if !caps.InstallAPI {
|
||||
// Fallback: current portal API creates the account immediately (no email code).
|
||||
k, err := PortalMintKey(true, name, email, password, "", tokenName)
|
||||
return RegisterResult{Key: k}, err
|
||||
}
|
||||
out, _, err := portalPost("/install/register", "", map[string]any{"name": name, "email": email, "password": password, "client": "VeraePrintStamp-Windows"})
|
||||
if err != nil {
|
||||
return RegisterResult{}, err
|
||||
}
|
||||
id, _ := out["pendingId"].(string)
|
||||
if id == "" {
|
||||
return RegisterResult{}, errors.New("register: server returned no pendingId")
|
||||
}
|
||||
return RegisterResult{PendingID: id}, nil
|
||||
}
|
||||
|
||||
// InstallVerify submits the emailed code and returns the minted key.
|
||||
func InstallVerify(pendingID, code, tokenName string) (string, error) {
|
||||
out, _, err := portalPost("/install/verify", "", map[string]any{"pendingId": pendingID, "code": strings.TrimSpace(code), "tokenName": tokenName})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return keyFrom(out)
|
||||
}
|
||||
|
||||
// InstallResend asks for a new code.
|
||||
func InstallResend(pendingID string) error {
|
||||
_, _, err := portalPost("/install/resend", "", map[string]any{"pendingId": pendingID})
|
||||
return err
|
||||
}
|
||||
|
||||
// InstallLogin signs in and mints a key (install API or fallback).
|
||||
func InstallLogin(caps Capabilities, email, password, totp, tokenName string) (string, error) {
|
||||
if !caps.InstallAPI {
|
||||
return PortalMintKey(false, "", email, password, totp, tokenName)
|
||||
}
|
||||
body := map[string]any{"email": email, "password": password, "tokenName": tokenName}
|
||||
if totp != "" {
|
||||
body["totpCode"] = totp
|
||||
}
|
||||
out, _, err := portalPost("/install/login", "", body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return keyFrom(out)
|
||||
}
|
||||
|
||||
func keyFrom(out map[string]any) (string, error) {
|
||||
k, _ := out["token"].(string)
|
||||
if !strings.HasPrefix(k, "xVerae-") && !strings.HasPrefix(k, "key-") {
|
||||
return "", errors.New("server returned no xVerae- token")
|
||||
}
|
||||
return k, nil
|
||||
}
|
||||
116
internal/auth/portal_test.go
Normal file
116
internal/auth/portal_test.go
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func fakePortal(t *testing.T, installAPI bool) *httptest.Server {
|
||||
mux := http.NewServeMux()
|
||||
js := func(w http.ResponseWriter, code int, v any) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(code)
|
||||
_ = json.NewEncoder(w).Encode(v)
|
||||
}
|
||||
body := func(r *http.Request) map[string]any {
|
||||
m := map[string]any{}
|
||||
_ = json.NewDecoder(r.Body).Decode(&m)
|
||||
return m
|
||||
}
|
||||
if installAPI {
|
||||
mux.HandleFunc("/portal/api/install/capabilities", func(w http.ResponseWriter, r *http.Request) {
|
||||
js(w, 200, map[string]any{"installApi": true, "emailVerification": true, "passwordPolicy": map[string]any{"minLength": 12, "requireLetter": true, "requireDigit": true}})
|
||||
})
|
||||
mux.HandleFunc("/portal/api/install/register", func(w http.ResponseWriter, r *http.Request) { js(w, 202, map[string]any{"pendingId": "p1"}) })
|
||||
mux.HandleFunc("/portal/api/install/verify", func(w http.ResponseWriter, r *http.Request) {
|
||||
if body(r)["code"] != "123456" {
|
||||
js(w, 400, map[string]any{"error": "invalid_code"})
|
||||
return
|
||||
}
|
||||
js(w, 201, map[string]any{"token": "xVerae-verified-abcdefgh"})
|
||||
})
|
||||
mux.HandleFunc("/portal/api/install/login", func(w http.ResponseWriter, r *http.Request) {
|
||||
b := body(r)
|
||||
if b["totpCode"] == nil {
|
||||
js(w, 401, map[string]any{"error": "totp_required"})
|
||||
return
|
||||
}
|
||||
js(w, 200, map[string]any{"token": "xVerae-login-abcdefgh"})
|
||||
})
|
||||
}
|
||||
mux.HandleFunc("/portal/api/login", func(w http.ResponseWriter, r *http.Request) { js(w, 200, map[string]any{"token": "sess"}) })
|
||||
mux.HandleFunc("/portal/api/signup", func(w http.ResponseWriter, r *http.Request) { js(w, 200, map[string]any{"token": "sess"}) })
|
||||
mux.HandleFunc("/portal/api/tokens", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Header.Get("Authorization") != "Bearer sess" {
|
||||
js(w, 401, map[string]any{"error": "no"})
|
||||
return
|
||||
}
|
||||
js(w, 200, map[string]any{"token": "xVerae-minted-abcdefgh"})
|
||||
})
|
||||
mux.HandleFunc("/portal/api/logout", func(w http.ResponseWriter, r *http.Request) { js(w, 200, map[string]any{}) })
|
||||
s := httptest.NewServer(mux)
|
||||
t.Cleanup(s.Close)
|
||||
t.Setenv("VERAE_PORTAL_BASE", s.URL)
|
||||
t.Setenv("VERAE_ALLOW_INSECURE_PORTAL", "1")
|
||||
return s
|
||||
}
|
||||
|
||||
func TestPortalFallback(t *testing.T) {
|
||||
fakePortal(t, false)
|
||||
caps := GetCapabilities()
|
||||
if caps.InstallAPI || caps.Policy.MinLength != DefaultPolicy.MinLength {
|
||||
t.Fatalf("caps %+v", caps)
|
||||
}
|
||||
k, err := InstallLogin(caps, "a@b.co", "pw", "", "n")
|
||||
if err != nil || k != "xVerae-minted-abcdefgh" {
|
||||
t.Fatal(k, err)
|
||||
}
|
||||
rr, err := InstallRegister(caps, "N", "a@b.co", "pw", "n")
|
||||
if err != nil || rr.Key == "" || rr.PendingID != "" {
|
||||
t.Fatal(rr, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPortalInstallAPI(t *testing.T) {
|
||||
fakePortal(t, true)
|
||||
caps := GetCapabilities()
|
||||
if !caps.InstallAPI || caps.Policy.MinLength != 12 {
|
||||
t.Fatalf("caps %+v", caps)
|
||||
}
|
||||
if _, err := InstallLogin(caps, "a@b.co", "pw", "", "n"); !errors.Is(err, ErrTOTPRequired) {
|
||||
t.Fatal("want totp_required", err)
|
||||
}
|
||||
if k, err := InstallLogin(caps, "a@b.co", "pw", "123456", "n"); err != nil || k != "xVerae-login-abcdefgh" {
|
||||
t.Fatal(k, err)
|
||||
}
|
||||
rr, err := InstallRegister(caps, "N", "a@b.co", "pw", "n")
|
||||
if err != nil || rr.PendingID != "p1" {
|
||||
t.Fatal(rr, err)
|
||||
}
|
||||
if _, err := InstallVerify("p1", "000000", "n"); err == nil {
|
||||
t.Fatal("bad code accepted")
|
||||
}
|
||||
if k, err := InstallVerify("p1", "123456", "n"); err != nil || k != "xVerae-verified-abcdefgh" {
|
||||
t.Fatal(k, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPolicyAndHTTPS(t *testing.T) {
|
||||
p := DefaultPolicy
|
||||
for pw, ok := range map[string]bool{"short1": false, "longenoughnodigit": false, "longenough123": true, "george12345x": false} {
|
||||
if (p.Check(pw, "george@x.com") == nil) != ok {
|
||||
t.Errorf("policy %q want ok=%v", pw, ok)
|
||||
}
|
||||
}
|
||||
t.Setenv("VERAE_PORTAL_BASE", "http://example.com")
|
||||
t.Setenv("VERAE_ALLOW_INSECURE_PORTAL", "")
|
||||
if _, _, err := portalPost("/login", "", map[string]any{}); err == nil {
|
||||
t.Fatal("non-https portal allowed")
|
||||
}
|
||||
if MaskKey("xVerae-abcdefghijklmnop") != "xVerae-...mnop" || MaskEmail("george@x.com") != "g***@x.com" {
|
||||
t.Fatal("masking")
|
||||
}
|
||||
}
|
||||
|
|
@ -12,7 +12,9 @@ What this build adds
|
|||
C:\ProgramData\Verae\spool\fallback\verae-job.pdf. The same helper picks the
|
||||
file up.
|
||||
* Heavy logging in C:\ProgramData\Verae\logs:
|
||||
install-<time>.log full installer transcript (every step OK/FAIL)
|
||||
installer-<time>.log installer page/step log (no secrets)
|
||||
installer-details-<time>.log copy of the installer's details pane
|
||||
install-<time>.log printer-script transcript (every step OK/FAIL)
|
||||
install-summary-<time>.json
|
||||
helper-<user>.log per-job IDs like J20261003-190000-ipp-1
|
||||
cli-<user>.log
|
||||
|
|
@ -21,17 +23,32 @@ What this build adds
|
|||
|
||||
Test steps
|
||||
----------
|
||||
1. Run VeraePrintStamp-windows-amd64-0.2.0-debug-setup.exe and accept the UAC
|
||||
prompt. If SmartScreen appears, choose More info > Run anyway (the build is
|
||||
unsigned).
|
||||
The install details pane should end with "Verae printer install: OK".
|
||||
(The old per-user 0.1.2 install is removed automatically.)
|
||||
2. Open PowerShell (not elevated) and check:
|
||||
1. Double-click VeraePrintStamp-windows-amd64-0.3.0-debug-setup.exe and accept
|
||||
the UAC prompt. If SmartScreen appears, choose More info > Run anyway (the
|
||||
build is unsigned). No console windows should appear at any point.
|
||||
2. "Verae signing key" page: it shows your Windows user and any key it found
|
||||
(masked, e.g. xVerae-AbC1...9xYz), and whether Verae accepted it. Choose:
|
||||
* Keep the existing key, or
|
||||
* Sign in: email + password (+ authenticator code if you use 2FA). A new
|
||||
key named "Verae Print Stamp - <PC name> (windows)" is created and saved, or
|
||||
* Create a new Verae account: name, email, password + confirm (the strength
|
||||
rule is shown on the page). If the server supports it you get an emailed
|
||||
6-digit code on the next page ("Send a new code" resends it), or
|
||||
* Paste a key: "Open customer.verae.ai" > Security > create an xVerae- token,
|
||||
then paste it. The key is checked with Verae before you can continue, or
|
||||
* Skip for now: you are warned, and the helper asks for a key in a small
|
||||
window the first time you print.
|
||||
Your password goes only to https://customer.verae.ai and is never saved or
|
||||
logged. Only the key is saved, in %USERPROFILE%\.verae\xverae-token,
|
||||
readable only by you (and SYSTEM/Administrators).
|
||||
3. The progress page shows each step. The finish page lists every step as
|
||||
OK / FAIL / SKIP (files, key, spooler, autostart task, helper running as
|
||||
you, both printers), with a link to open the logs folder. If something
|
||||
failed, a verae-diag-*.zip is already in the logs folder.
|
||||
Quick check in a normal PowerShell:
|
||||
Get-Printer | ? Name -like 'Verae*' | ft Name,DriverName,PortName
|
||||
verae-print ipp-selftest
|
||||
& "C:\Program Files\Verae\PrintStamp\bin\verae-print.exe" ipp-selftest
|
||||
Expected: two printers, and "IPP helper OK".
|
||||
3. If you have never saved a signing key: Start > Verae > Setup signing key.
|
||||
This box already has %USERPROFILE%\.verae\xverae-token.
|
||||
4. Open Notepad, type something, File > Print, choose "Verae TimeStamp PDF",
|
||||
click Print.
|
||||
Within about 10 s you should get a balloon "Verae timestamp <receipt>", and
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Build the Windows DEBUG installer natively on Windows (Go + NSIS).
|
||||
# Usage: powershell -ExecutionPolicy Bypass -File platform\windows\build-windows-debug.ps1 [-Arch amd64|arm64]
|
||||
param([string]$Arch = "amd64", [string]$Version = "0.2.0-debug")
|
||||
param([string]$Arch = "amd64", [string]$Version = "0.3.0-debug")
|
||||
$ErrorActionPreference = "Stop"
|
||||
$Root = (Resolve-Path (Join-Path $PSScriptRoot "..\..")).Path
|
||||
$Out = Join-Path $Root "dist\windows-debug"
|
||||
|
|
@ -13,7 +13,7 @@ Push-Location $Root
|
|||
try {
|
||||
$env:CGO_ENABLED = "0"; $env:GOOS = "windows"; $env:GOARCH = $Arch
|
||||
& $Go version
|
||||
& $Go test ./internal/... ; if ($LASTEXITCODE) { throw "go test failed" }
|
||||
& $Go test ./internal/... ./cmd/... ; if ($LASTEXITCODE) { throw "go test failed" }
|
||||
& $Go build -trimpath -o (Join-Path $Stage "verae-print.exe") ./cmd/verae-print; if ($LASTEXITCODE) { throw "build cli failed" }
|
||||
& $Go build -trimpath -ldflags "-H windowsgui" -o (Join-Path $Stage "verae-printhelper.exe") ./cmd/verae-print; if ($LASTEXITCODE) { throw "build helper failed" }
|
||||
} finally { Pop-Location; Remove-Item Env:GOOS, Env:GOARCH -ErrorAction SilentlyContinue }
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
; Verae Print Stamp — Windows DEBUG installer with "Verae TimeStamp PDF" printer.
|
||||
; Build: platform\windows\build-windows-debug.ps1 (on Windows) or makensis with -DSRCDIR.
|
||||
; Verae Print Stamp — Windows DEBUG installer.
|
||||
; One interactive, elevated install: files, Verae key (detect / sign in / create
|
||||
; account / paste / skip), IPP printer + Local Port fallback printer, helper as the
|
||||
; logged-in user (scheduled task), autostart, diagnostics on failure, per-step
|
||||
; results on the finish page. All scripts run hidden (nsExec); secrets are passed
|
||||
; only through files in $PLUGINSDIR that are deleted immediately and are never logged.
|
||||
!ifndef VERSION
|
||||
!define VERSION "0.2.0-debug"
|
||||
!define VERSION "0.3.0-debug"
|
||||
!endif
|
||||
!ifndef ARCH
|
||||
!define ARCH "amd64"
|
||||
|
|
@ -15,6 +19,7 @@
|
|||
|
||||
!define PRODUCT_NAME "Verae Print Stamp (debug)"
|
||||
!define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\VeraePrintStampDebug"
|
||||
!define PORTAL_URL "https://customer.verae.ai/portal/"
|
||||
|
||||
Name "${PRODUCT_NAME} ${VERSION}"
|
||||
OutFile "${OUTDIR}\VeraePrintStamp-windows-${ARCH}-${VERSION}-setup.exe"
|
||||
|
|
@ -24,56 +29,558 @@ Unicode true
|
|||
SetCompressor /SOLID lzma
|
||||
ShowInstDetails show
|
||||
ShowUninstDetails show
|
||||
BrandingText "Verae TimeStamp PDF ${VERSION}"
|
||||
|
||||
!include "MUI2.nsh"
|
||||
!include "nsDialogs.nsh"
|
||||
!include "x64.nsh"
|
||||
!include "LogicLib.nsh"
|
||||
!include "FileFunc.nsh"
|
||||
!include "WinMessages.nsh"
|
||||
|
||||
Var LogDir
|
||||
Var LogFile
|
||||
Var PS
|
||||
Var Exe
|
||||
; key page state
|
||||
Var TargetUser
|
||||
Var KeyPresent
|
||||
Var KeyMasked
|
||||
Var KeyCheck
|
||||
Var KeyChoice ; keep|login|create|paste|skip
|
||||
Var TokenReady ; 1 when $PLUGINSDIR\newtoken.txt holds a key to save
|
||||
Var TokenMasked
|
||||
Var PendingId
|
||||
Var PolicyText
|
||||
Var InstallApi
|
||||
Var Checked
|
||||
; controls
|
||||
Var Dlg
|
||||
Var hR1
|
||||
Var hR2
|
||||
Var hR3
|
||||
Var hR4
|
||||
Var hR5
|
||||
Var hName
|
||||
Var hEmail
|
||||
Var hPass
|
||||
Var hConfirm
|
||||
Var hTotp
|
||||
Var hCode
|
||||
Var hKey
|
||||
; results
|
||||
Var FinishTitle
|
||||
Var FinishText
|
||||
|
||||
!define MUI_ABORTWARNING
|
||||
!define MUI_FINISHPAGE_RUN "$INSTDIR\bin\verae-print.exe"
|
||||
!define MUI_FINISHPAGE_RUN_PARAMETERS "setup"
|
||||
!define MUI_FINISHPAGE_RUN_TEXT "Run signing-key setup (only needed if not done before)"
|
||||
!define MUI_FINISHPAGE_RUN_NOTCHECKED
|
||||
!define MUI_FINISHPAGE_TITLE "$FinishTitle"
|
||||
!define MUI_FINISHPAGE_TEXT "$FinishText"
|
||||
!define MUI_FINISHPAGE_TEXT_LARGE
|
||||
!define MUI_FINISHPAGE_LINK "Open the logs folder"
|
||||
!define MUI_FINISHPAGE_LINK_LOCATION "$LogDir"
|
||||
!define MUI_FINISHPAGE_SHOWREADME "$INSTDIR\README-TEST.txt"
|
||||
!define MUI_FINISHPAGE_SHOWREADME_TEXT "Open test instructions"
|
||||
!define MUI_FINISHPAGE_SHOWREADME_NOTCHECKED
|
||||
!define MUI_FINISHPAGE_NOREBOOTSUPPORT
|
||||
|
||||
!insertmacro MUI_PAGE_WELCOME
|
||||
!insertmacro MUI_PAGE_LICENSE "${SRCDIR}\LICENSE.txt"
|
||||
Page custom KeyPageCreate KeyPageLeave
|
||||
Page custom LoginPageCreate LoginPageLeave
|
||||
Page custom CreatePageCreate CreatePageLeave
|
||||
Page custom VerifyPageCreate VerifyPageLeave
|
||||
Page custom PastePageCreate PastePageLeave
|
||||
!insertmacro MUI_PAGE_INSTFILES
|
||||
!insertmacro MUI_PAGE_FINISH
|
||||
!insertmacro MUI_UNPAGE_CONFIRM
|
||||
!insertmacro MUI_UNPAGE_INSTFILES
|
||||
!insertmacro MUI_LANGUAGE "English"
|
||||
|
||||
!define PS '"$WINDIR\Sysnative\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -ExecutionPolicy Bypass'
|
||||
; ---------- logging (never pass secrets to these) ----------
|
||||
!macro LOG text
|
||||
Push `${text}`
|
||||
Call WriteLog
|
||||
!macroend
|
||||
!define LOG "!insertmacro LOG"
|
||||
|
||||
Var LogDir
|
||||
Function WriteLog
|
||||
Exch $0
|
||||
Push $1
|
||||
Push $2
|
||||
${GetTime} "" "L" $R1 $R2 $R3 $R4 $R5 $R6 $R7
|
||||
FileOpen $1 $LogFile a
|
||||
${If} $1 != ""
|
||||
FileSeek $1 0 END
|
||||
FileWriteUTF16LE $1 "$R3-$R2-$R1 $R5:$R6:$R7 $0$\r$\n"
|
||||
FileClose $1
|
||||
${EndIf}
|
||||
Pop $2
|
||||
Pop $1
|
||||
Pop $0
|
||||
FunctionEnd
|
||||
|
||||
; Copy the install details list (incl. nsExec output) to a file.
|
||||
!ifndef LVM_GETITEMCOUNT
|
||||
!define LVM_GETITEMCOUNT 0x1004
|
||||
!endif
|
||||
!ifndef LVM_GETITEMTEXTW
|
||||
!define LVM_GETITEMTEXTW 0x1073
|
||||
!endif
|
||||
Function DumpLog
|
||||
Exch $5
|
||||
Push $0
|
||||
Push $1
|
||||
Push $2
|
||||
Push $3
|
||||
Push $4
|
||||
Push $6
|
||||
FindWindow $0 "#32770" "" $HWNDPARENT
|
||||
GetDlgItem $0 $0 1016
|
||||
StrCmp $0 0 dl_exit
|
||||
FileOpen $5 $5 "w"
|
||||
StrCmp $5 "" dl_exit
|
||||
FileWriteWord $5 0xFEFF
|
||||
SendMessage $0 ${LVM_GETITEMCOUNT} 0 0 $6
|
||||
System::Call '*(&t${NSIS_MAX_STRLEN})p.r3'
|
||||
StrCpy $2 0
|
||||
System::Call "*(i, i, i, i, i, p, i, i, i) p (0, 0, 0, 0, 0, r3, ${NSIS_MAX_STRLEN}) .r1"
|
||||
dl_loop:
|
||||
StrCmp $2 $6 dl_done
|
||||
System::Call "User32::SendMessage(p, i, p, p) p ($0, ${LVM_GETITEMTEXTW}, $2, r1)"
|
||||
System::Call "*$3(&t${NSIS_MAX_STRLEN} .r4)"
|
||||
FileWriteUTF16LE $5 "$4$\r$\n"
|
||||
IntOp $2 $2 + 1
|
||||
Goto dl_loop
|
||||
dl_done:
|
||||
FileClose $5
|
||||
System::Free $1
|
||||
System::Free $3
|
||||
dl_exit:
|
||||
Pop $6
|
||||
Pop $4
|
||||
Pop $3
|
||||
Pop $2
|
||||
Pop $1
|
||||
Pop $0
|
||||
Pop $5
|
||||
FunctionEnd
|
||||
|
||||
; write "Key=Value" request lines (UTF-16LE + BOM) — values may contain any chars
|
||||
!macro REQ_OPEN path
|
||||
FileOpen $9 `${path}` w
|
||||
FileWriteWord $9 0xFEFF
|
||||
!macroend
|
||||
!macro REQ_KV key val
|
||||
FileWriteUTF16LE $9 `${key}=${val}$\r$\n`
|
||||
!macroend
|
||||
!macro REQ_CLOSE
|
||||
FileClose $9
|
||||
!macroend
|
||||
|
||||
Function .onInit
|
||||
ReadEnvStr $LogDir "ProgramData"
|
||||
StrCpy $LogDir "$LogDir\Verae\logs"
|
||||
${If} ${RunningX64}
|
||||
SetRegView 64
|
||||
${Else}
|
||||
MessageBox MB_ICONSTOP "This debug build targets 64-bit Windows."
|
||||
${IfNot} ${RunningX64}
|
||||
MessageBox MB_ICONSTOP "This build targets 64-bit Windows."
|
||||
Abort
|
||||
${EndIf}
|
||||
SetRegView 64
|
||||
SetShellVarContext all
|
||||
ReadEnvStr $LogDir "ProgramData"
|
||||
StrCpy $LogDir "$LogDir\Verae\logs"
|
||||
CreateDirectory $LogDir
|
||||
${GetTime} "" "L" $R1 $R2 $R3 $R4 $R5 $R6 $R7
|
||||
StrCpy $LogFile "$LogDir\installer-$R3$R2$R1-$R5$R6$R7.log"
|
||||
StrCpy $PS '"$WINDIR\Sysnative\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden'
|
||||
InitPluginsDir
|
||||
SetOutPath $PLUGINSDIR
|
||||
File "${SRCDIR}\verae-print.exe"
|
||||
File "${SRCDIR}\scripts\common.ps1"
|
||||
File "${SRCDIR}\scripts\user-token.ps1"
|
||||
StrCpy $Exe "$PLUGINSDIR\verae-print.exe"
|
||||
StrCpy $Checked 0
|
||||
StrCpy $TokenReady 0
|
||||
StrCpy $KeyChoice "skip"
|
||||
${LOG} "installer ${VERSION} start; elevated process; logdir=$LogDir"
|
||||
FunctionEnd
|
||||
|
||||
Function un.onInit
|
||||
SetRegView 64
|
||||
SetShellVarContext all
|
||||
FunctionEnd
|
||||
|
||||
; ======================= Page: Verae key status / choice =======================
|
||||
Function KeyPageCreate
|
||||
!insertmacro MUI_HEADER_TEXT "Verae signing key" "Timestamps are registered with your Verae account."
|
||||
${If} $Checked == 0
|
||||
Banner::show /NOUNLOAD "Checking for an existing Verae key..."
|
||||
nsExec::Exec '$PS -File "$PLUGINSDIR\user-token.ps1" -Mode status -IniOut "$PLUGINSDIR\tok.ini"'
|
||||
Pop $0
|
||||
nsExec::Exec '"$Exe" installer-account --op capabilities --out "$PLUGINSDIR\caps.ini"'
|
||||
Pop $1
|
||||
Banner::destroy
|
||||
ReadINIStr $TargetUser "$PLUGINSDIR\tok.ini" token account
|
||||
ReadINIStr $KeyPresent "$PLUGINSDIR\tok.ini" token present
|
||||
ReadINIStr $KeyMasked "$PLUGINSDIR\tok.ini" token masked
|
||||
ReadINIStr $KeyCheck "$PLUGINSDIR\tok.ini" token check
|
||||
ReadINIStr $PolicyText "$PLUGINSDIR\caps.ini" account policyText
|
||||
ReadINIStr $InstallApi "$PLUGINSDIR\caps.ini" account installApi
|
||||
${LOG} "key status: user=$TargetUser present=$KeyPresent key=$KeyMasked check=$KeyCheck (ps exit $0); installApi=$InstallApi (exit $1)"
|
||||
StrCpy $Checked 1
|
||||
${EndIf}
|
||||
|
||||
nsDialogs::Create 1018
|
||||
Pop $Dlg
|
||||
${NSD_CreateLabel} 0 0 100% 12u "Windows user: $TargetUser"
|
||||
Pop $0
|
||||
${If} $KeyPresent == "1"
|
||||
${If} $KeyCheck == "valid"
|
||||
StrCpy $1 "An existing Verae key was found: $KeyMasked (verified with Verae)."
|
||||
${ElseIf} $KeyCheck == "invalid"
|
||||
StrCpy $1 "A saved key was found ($KeyMasked) but Verae REJECTED it. Please sign in or paste a new key."
|
||||
${Else}
|
||||
StrCpy $1 "An existing Verae key was found: $KeyMasked (could not verify right now: $KeyCheck)."
|
||||
${EndIf}
|
||||
${Else}
|
||||
StrCpy $1 "No Verae key was found for this user. You need one to timestamp documents."
|
||||
${EndIf}
|
||||
${NSD_CreateLabel} 0 14u 100% 20u "$1"
|
||||
Pop $0
|
||||
|
||||
${NSD_CreateRadioButton} 10u 38u 95% 12u "Keep the existing key ($KeyMasked)"
|
||||
Pop $hR1
|
||||
${NSD_CreateRadioButton} 10u 52u 95% 12u "Sign in to my Verae account (email + password) and create a key"
|
||||
Pop $hR2
|
||||
${NSD_CreateRadioButton} 10u 66u 95% 12u "Create a new Verae account"
|
||||
Pop $hR3
|
||||
${NSD_CreateRadioButton} 10u 80u 95% 12u "I already have an xVerae- key - paste it"
|
||||
Pop $hR4
|
||||
${NSD_CreateRadioButton} 10u 94u 95% 12u "Skip for now (the printer will ask for a key on first print)"
|
||||
Pop $hR5
|
||||
${NSD_CreateLabel} 0 112u 100% 24u "Your password is sent only to customer.verae.ai over HTTPS and is never stored; only the issued key is saved in $TargetUser's profile (.verae\xverae-token)."
|
||||
Pop $0
|
||||
|
||||
${If} $KeyPresent == "1"
|
||||
${AndIf} $KeyCheck != "invalid"
|
||||
${NSD_Check} $hR1
|
||||
${Else}
|
||||
EnableWindow $hR1 0
|
||||
${NSD_Check} $hR2
|
||||
${EndIf}
|
||||
nsDialogs::Show
|
||||
FunctionEnd
|
||||
|
||||
Function KeyPageLeave
|
||||
${NSD_GetState} $hR1 $0
|
||||
${NSD_GetState} $hR2 $1
|
||||
${NSD_GetState} $hR3 $2
|
||||
${NSD_GetState} $hR4 $3
|
||||
${If} $0 == ${BST_CHECKED}
|
||||
StrCpy $KeyChoice "keep"
|
||||
${ElseIf} $1 == ${BST_CHECKED}
|
||||
StrCpy $KeyChoice "login"
|
||||
${ElseIf} $2 == ${BST_CHECKED}
|
||||
StrCpy $KeyChoice "create"
|
||||
${ElseIf} $3 == ${BST_CHECKED}
|
||||
StrCpy $KeyChoice "paste"
|
||||
${Else}
|
||||
StrCpy $KeyChoice "skip"
|
||||
MessageBox MB_YESNO|MB_ICONEXCLAMATION "Continue without a Verae key?$\r$\n$\r$\nThe printer will be installed, but documents cannot be timestamped until you add a key. On your first print you will be asked for one (or use Start > Verae > Setup signing key)." IDYES +2
|
||||
Abort
|
||||
${EndIf}
|
||||
StrCpy $TokenReady 0
|
||||
StrCpy $PendingId ""
|
||||
${LOG} "key choice: $KeyChoice"
|
||||
FunctionEnd
|
||||
|
||||
; ======================= Page: sign in =======================
|
||||
Function LoginPageCreate
|
||||
${If} $KeyChoice != "login"
|
||||
Abort
|
||||
${EndIf}
|
||||
!insertmacro MUI_HEADER_TEXT "Sign in to Verae" "A new signing key will be created for this PC."
|
||||
nsDialogs::Create 1018
|
||||
Pop $Dlg
|
||||
${NSD_CreateLabel} 0 0 30% 12u "Email"
|
||||
Pop $0
|
||||
${NSD_CreateText} 30% 0 70% 12u ""
|
||||
Pop $hEmail
|
||||
${NSD_CreateLabel} 0 18u 30% 12u "Password"
|
||||
Pop $0
|
||||
${NSD_CreatePassword} 30% 18u 70% 12u ""
|
||||
Pop $hPass
|
||||
${NSD_CreateLabel} 0 36u 30% 12u "Authenticator code"
|
||||
Pop $0
|
||||
${NSD_CreateText} 30% 36u 30% 12u ""
|
||||
Pop $hTotp
|
||||
${NSD_CreateLabel} 62% 37u 38% 12u "(only if you use 2FA)"
|
||||
Pop $0
|
||||
${NSD_CreateLabel} 0 60u 100% 24u "Forgot your password or prefer the website? Open the customer portal, create an xVerae- key under Security, then go Back and choose 'paste it'."
|
||||
Pop $0
|
||||
${NSD_CreateLink} 0 86u 100% 12u "Open customer.verae.ai in your browser"
|
||||
Pop $0
|
||||
${NSD_OnClick} $0 OpenPortal
|
||||
${NSD_SetFocus} $hEmail
|
||||
nsDialogs::Show
|
||||
FunctionEnd
|
||||
|
||||
Function OpenPortal
|
||||
Pop $0
|
||||
ExecShell "open" "${PORTAL_URL}"
|
||||
FunctionEnd
|
||||
|
||||
Function LoginPageLeave
|
||||
${NSD_GetText} $hEmail $1
|
||||
${NSD_GetText} $hPass $2
|
||||
${NSD_GetText} $hTotp $3
|
||||
${If} $1 == ""
|
||||
${OrIf} $2 == ""
|
||||
MessageBox MB_ICONEXCLAMATION "Please enter your email and password."
|
||||
Abort
|
||||
${EndIf}
|
||||
!insertmacro REQ_OPEN "$PLUGINSDIR\req.txt"
|
||||
!insertmacro REQ_KV "Email" "$1"
|
||||
!insertmacro REQ_KV "Password" "$2"
|
||||
!insertmacro REQ_KV "Totp" "$3"
|
||||
!insertmacro REQ_CLOSE
|
||||
StrCpy $2 ""
|
||||
${LOG} "sign-in attempt"
|
||||
Banner::show /NOUNLOAD "Signing in to Verae..."
|
||||
nsExec::Exec '"$Exe" installer-account --op login --in "$PLUGINSDIR\req.txt" --out "$PLUGINSDIR\acct.ini" --token-out "$PLUGINSDIR\newtoken.txt"'
|
||||
Pop $0
|
||||
Banner::destroy
|
||||
Delete "$PLUGINSDIR\req.txt"
|
||||
Call HandleAccountResult
|
||||
FunctionEnd
|
||||
|
||||
; reads acct.ini; on success sets TokenReady, else Abort (stay on page)
|
||||
Function HandleAccountResult
|
||||
ReadINIStr $4 "$PLUGINSDIR\acct.ini" account result
|
||||
ReadINIStr $5 "$PLUGINSDIR\acct.ini" account detail
|
||||
ReadINIStr $TokenMasked "$PLUGINSDIR\acct.ini" account masked
|
||||
Delete "$PLUGINSDIR\acct.ini"
|
||||
${LOG} "account result: $4 ($5)"
|
||||
${If} $4 == "ok"
|
||||
StrCpy $TokenReady 1
|
||||
MessageBox MB_ICONINFORMATION "A new Verae key was created for this PC.$\r$\nIt will be saved for $TargetUser."
|
||||
${ElseIf} $4 == "code_sent"
|
||||
; handled by caller
|
||||
${ElseIf} $4 == "totp_required"
|
||||
MessageBox MB_ICONINFORMATION "$5"
|
||||
${NSD_SetFocus} $hTotp
|
||||
Abort
|
||||
${Else}
|
||||
${If} $4 == ""
|
||||
StrCpy $5 "No response from the helper (exit $0)."
|
||||
${EndIf}
|
||||
MessageBox MB_ICONEXCLAMATION "Verae said: $5"
|
||||
Abort
|
||||
${EndIf}
|
||||
FunctionEnd
|
||||
|
||||
; ======================= Page: create account =======================
|
||||
Function CreatePageCreate
|
||||
${If} $KeyChoice != "create"
|
||||
Abort
|
||||
${EndIf}
|
||||
!insertmacro MUI_HEADER_TEXT "Create a Verae account" "Your account holds your timestamps and signing keys."
|
||||
nsDialogs::Create 1018
|
||||
Pop $Dlg
|
||||
${NSD_CreateLabel} 0 0 30% 12u "Name"
|
||||
Pop $0
|
||||
${NSD_CreateText} 30% 0 70% 12u ""
|
||||
Pop $hName
|
||||
${NSD_CreateLabel} 0 16u 30% 12u "Email"
|
||||
Pop $0
|
||||
${NSD_CreateText} 30% 16u 70% 12u ""
|
||||
Pop $hEmail
|
||||
${NSD_CreateLabel} 0 32u 30% 12u "Password"
|
||||
Pop $0
|
||||
${NSD_CreatePassword} 30% 32u 70% 12u ""
|
||||
Pop $hPass
|
||||
${NSD_CreateLabel} 0 48u 30% 12u "Confirm password"
|
||||
Pop $0
|
||||
${NSD_CreatePassword} 30% 48u 70% 12u ""
|
||||
Pop $hConfirm
|
||||
${If} $PolicyText == ""
|
||||
StrCpy $PolicyText "Password: at least 10 characters, a letter and a number"
|
||||
${EndIf}
|
||||
${NSD_CreateLabel} 30% 64u 70% 20u "$PolicyText"
|
||||
Pop $0
|
||||
${If} $InstallApi == "1"
|
||||
StrCpy $1 "We will email you a 6-digit code to verify your address."
|
||||
${Else}
|
||||
StrCpy $1 "Your account is created immediately (this Verae server does not send verification codes yet)."
|
||||
${EndIf}
|
||||
${NSD_CreateLabel} 0 88u 100% 20u "$1"
|
||||
Pop $0
|
||||
${NSD_SetFocus} $hName
|
||||
nsDialogs::Show
|
||||
FunctionEnd
|
||||
|
||||
Function CreatePageLeave
|
||||
${NSD_GetText} $hName $1
|
||||
${NSD_GetText} $hEmail $2
|
||||
${NSD_GetText} $hPass $3
|
||||
${NSD_GetText} $hConfirm $6
|
||||
${If} $3 != $6
|
||||
MessageBox MB_ICONEXCLAMATION "The passwords do not match."
|
||||
Abort
|
||||
${EndIf}
|
||||
!insertmacro REQ_OPEN "$PLUGINSDIR\req.txt"
|
||||
!insertmacro REQ_KV "Name" "$1"
|
||||
!insertmacro REQ_KV "Email" "$2"
|
||||
!insertmacro REQ_KV "Password" "$3"
|
||||
!insertmacro REQ_KV "Confirm" "$6"
|
||||
!insertmacro REQ_CLOSE
|
||||
StrCpy $3 ""
|
||||
StrCpy $6 ""
|
||||
${LOG} "create-account attempt"
|
||||
Banner::show /NOUNLOAD "Creating your Verae account..."
|
||||
nsExec::Exec '"$Exe" installer-account --op register --in "$PLUGINSDIR\req.txt" --out "$PLUGINSDIR\acct.ini" --token-out "$PLUGINSDIR\newtoken.txt"'
|
||||
Pop $0
|
||||
Banner::destroy
|
||||
Delete "$PLUGINSDIR\req.txt"
|
||||
ReadINIStr $4 "$PLUGINSDIR\acct.ini" account result
|
||||
${If} $4 == "code_sent"
|
||||
ReadINIStr $PendingId "$PLUGINSDIR\acct.ini" account pendingId
|
||||
ReadINIStr $5 "$PLUGINSDIR\acct.ini" account detail
|
||||
${LOG} "verification code sent"
|
||||
Delete "$PLUGINSDIR\acct.ini"
|
||||
${Else}
|
||||
Call HandleAccountResult
|
||||
${EndIf}
|
||||
FunctionEnd
|
||||
|
||||
; ======================= Page: email verification code =======================
|
||||
Function VerifyPageCreate
|
||||
${If} $PendingId == ""
|
||||
Abort
|
||||
${EndIf}
|
||||
!insertmacro MUI_HEADER_TEXT "Verify your email" "Enter the code we sent you."
|
||||
nsDialogs::Create 1018
|
||||
Pop $Dlg
|
||||
${NSD_CreateLabel} 0 0 100% 24u "We emailed a 6-digit verification code to the address you entered. It may take a minute; check your spam folder."
|
||||
Pop $0
|
||||
${NSD_CreateLabel} 0 30u 30% 12u "Code"
|
||||
Pop $0
|
||||
${NSD_CreateText} 30% 30u 30% 12u ""
|
||||
Pop $hCode
|
||||
${NSD_CreateButton} 30% 50u 30% 14u "Send a new code"
|
||||
Pop $0
|
||||
${NSD_OnClick} $0 ResendCode
|
||||
${NSD_SetFocus} $hCode
|
||||
nsDialogs::Show
|
||||
FunctionEnd
|
||||
|
||||
Function ResendCode
|
||||
Pop $0
|
||||
!insertmacro REQ_OPEN "$PLUGINSDIR\req.txt"
|
||||
!insertmacro REQ_KV "PendingId" "$PendingId"
|
||||
!insertmacro REQ_CLOSE
|
||||
nsExec::Exec '"$Exe" installer-account --op resend --in "$PLUGINSDIR\req.txt" --out "$PLUGINSDIR\acct.ini"'
|
||||
Pop $0
|
||||
ReadINIStr $5 "$PLUGINSDIR\acct.ini" account detail
|
||||
Delete "$PLUGINSDIR\acct.ini"
|
||||
MessageBox MB_ICONINFORMATION "$5"
|
||||
FunctionEnd
|
||||
|
||||
Function VerifyPageLeave
|
||||
${NSD_GetText} $hCode $1
|
||||
!insertmacro REQ_OPEN "$PLUGINSDIR\req.txt"
|
||||
!insertmacro REQ_KV "PendingId" "$PendingId"
|
||||
!insertmacro REQ_KV "Code" "$1"
|
||||
!insertmacro REQ_CLOSE
|
||||
Banner::show /NOUNLOAD "Verifying..."
|
||||
nsExec::Exec '"$Exe" installer-account --op verify --in "$PLUGINSDIR\req.txt" --out "$PLUGINSDIR\acct.ini" --token-out "$PLUGINSDIR\newtoken.txt"'
|
||||
Pop $0
|
||||
Banner::destroy
|
||||
Delete "$PLUGINSDIR\req.txt"
|
||||
Call HandleAccountResult
|
||||
FunctionEnd
|
||||
|
||||
; ======================= Page: paste key =======================
|
||||
Function PastePageCreate
|
||||
${If} $KeyChoice != "paste"
|
||||
Abort
|
||||
${EndIf}
|
||||
!insertmacro MUI_HEADER_TEXT "Paste your Verae key" "Keys start with xVerae- and are shown once when created."
|
||||
nsDialogs::Create 1018
|
||||
Pop $Dlg
|
||||
${NSD_CreateLabel} 0 0 100% 30u "1. Click the button to open the Verae customer portal and sign in.$\r$\n2. Open Security and create an xVerae- token (it is shown only once).$\r$\n3. Copy it and paste it below, then click Next."
|
||||
Pop $0
|
||||
${NSD_CreateButton} 0 34u 60% 14u "Open customer.verae.ai"
|
||||
Pop $0
|
||||
${NSD_OnClick} $0 OpenPortal
|
||||
${NSD_CreateLabel} 0 56u 100% 12u "xVerae- key"
|
||||
Pop $0
|
||||
${NSD_CreatePassword} 0 70u 100% 12u ""
|
||||
Pop $hKey
|
||||
${NSD_SetFocus} $hKey
|
||||
nsDialogs::Show
|
||||
FunctionEnd
|
||||
|
||||
Function PastePageLeave
|
||||
${NSD_GetText} $hKey $1
|
||||
${If} $1 == ""
|
||||
MessageBox MB_ICONEXCLAMATION "Please paste your key (or go Back and choose Skip)."
|
||||
Abort
|
||||
${EndIf}
|
||||
FileOpen $9 "$PLUGINSDIR\newtoken.txt" w
|
||||
FileWriteWord $9 0xFEFF
|
||||
FileWriteUTF16LE $9 "$1"
|
||||
FileClose $9
|
||||
StrCpy $1 ""
|
||||
Banner::show /NOUNLOAD "Checking the key with Verae..."
|
||||
nsExec::Exec '"$Exe" validate-key --token-file "$PLUGINSDIR\newtoken.txt" --out "$PLUGINSDIR\val.ini"'
|
||||
Pop $0
|
||||
Banner::destroy
|
||||
ReadINIStr $4 "$PLUGINSDIR\val.ini" validate result
|
||||
ReadINIStr $5 "$PLUGINSDIR\val.ini" validate detail
|
||||
ReadINIStr $TokenMasked "$PLUGINSDIR\val.ini" validate masked
|
||||
Delete "$PLUGINSDIR\val.ini"
|
||||
${LOG} "pasted key $TokenMasked: $4 ($5)"
|
||||
${If} $4 == "valid"
|
||||
StrCpy $TokenReady 1
|
||||
${ElseIf} $4 == "invalid"
|
||||
Delete "$PLUGINSDIR\newtoken.txt"
|
||||
MessageBox MB_ICONEXCLAMATION "Verae rejected this key ($5). Please check it and paste again."
|
||||
Abort
|
||||
${ElseIf} $4 == "badformat"
|
||||
Delete "$PLUGINSDIR\newtoken.txt"
|
||||
MessageBox MB_ICONEXCLAMATION "That does not look like a Verae key: $5"
|
||||
Abort
|
||||
${Else}
|
||||
MessageBox MB_YESNO|MB_ICONQUESTION "Could not reach Verae to check the key ($5).$\r$\nSave it anyway?" IDYES +3
|
||||
Delete "$PLUGINSDIR\newtoken.txt"
|
||||
Abort
|
||||
StrCpy $TokenReady 1
|
||||
${EndIf}
|
||||
FunctionEnd
|
||||
|
||||
; ======================= Install =======================
|
||||
!macro STEP_RESULT label value
|
||||
StrCpy $FinishText "$FinishText${label}: ${value}$\r$\n"
|
||||
!macroend
|
||||
|
||||
Section "Verae Print Stamp + printer" SecMain
|
||||
SectionIn RO
|
||||
; stop a running helper so files can be replaced
|
||||
SetDetailsPrint both
|
||||
DetailPrint "Installer log: $LogFile"
|
||||
${LOG} "section start; target=$TargetUser keyChoice=$KeyChoice tokenReady=$TokenReady"
|
||||
|
||||
DetailPrint "Stopping running helper (if any)"
|
||||
nsExec::ExecToLog 'schtasks /End /TN "Verae Print Helper"'
|
||||
Pop $0
|
||||
nsExec::ExecToLog 'taskkill /F /IM verae-printhelper.exe'
|
||||
Pop $0
|
||||
|
||||
StrCpy $R9 "OK"
|
||||
ClearErrors
|
||||
SetOutPath "$INSTDIR\bin"
|
||||
File "${SRCDIR}\verae-print.exe"
|
||||
File "${SRCDIR}\verae-printhelper.exe"
|
||||
SetOutPath "$INSTDIR\scripts"
|
||||
File "${SRCDIR}\scripts\common.ps1"
|
||||
File "${SRCDIR}\scripts\user-token.ps1"
|
||||
File "${SRCDIR}\scripts\install-printer.ps1"
|
||||
File "${SRCDIR}\scripts\uninstall-printer.ps1"
|
||||
File "${SRCDIR}\scripts\verae-diag.ps1"
|
||||
|
|
@ -81,37 +588,85 @@ Section "Verae Print Stamp + printer" SecMain
|
|||
File "${SRCDIR}\README-TEST.txt"
|
||||
File "${SRCDIR}\LICENSE.txt"
|
||||
WriteUninstaller "$INSTDIR\Uninstall.exe"
|
||||
|
||||
; Remove the old per-user 0.1.2 CLI-only install (it shadows PATH and has no printer)
|
||||
DetailPrint "Removing old per-user install in $LOCALAPPDATA\Verae\PrintStamp (if any)"
|
||||
Delete "$LOCALAPPDATA\Verae\PrintStamp\bin\verae-print.exe"
|
||||
DeleteRegKey HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\VeraePrintStamp"
|
||||
|
||||
${If} ${Errors}
|
||||
StrCpy $R9 "FAIL"
|
||||
${EndIf}
|
||||
${LOG} "files: $R9"
|
||||
WriteRegStr HKLM "${UNINST_KEY}" "DisplayName" "${PRODUCT_NAME} ${VERSION}"
|
||||
WriteRegStr HKLM "${UNINST_KEY}" "UninstallString" '"$INSTDIR\Uninstall.exe"'
|
||||
WriteRegStr HKLM "${UNINST_KEY}" "DisplayVersion" "${VERSION}"
|
||||
WriteRegStr HKLM "${UNINST_KEY}" "Publisher" "Verae"
|
||||
|
||||
CreateDirectory "$SMPROGRAMS\Verae"
|
||||
CreateShortCut "$SMPROGRAMS\Verae\Verae - Collect diagnostics.lnk" "$WINDIR\System32\WindowsPowerShell\v1.0\powershell.exe" '-NoProfile -ExecutionPolicy Bypass -NoExit -File "$INSTDIR\scripts\verae-diag.ps1"'
|
||||
CreateShortCut "$SMPROGRAMS\Verae\Verae - Setup signing key.lnk" "$INSTDIR\bin\verae-print.exe" "setup"
|
||||
CreateShortCut "$SMPROGRAMS\Verae\Verae - Setup signing key.lnk" "$INSTDIR\bin\verae-printhelper.exe" "key-dialog"
|
||||
CreateShortCut "$SMPROGRAMS\Verae\Verae - Start print helper.lnk" "$INSTDIR\bin\verae-printhelper.exe"
|
||||
CreateShortCut "$SMPROGRAMS\Verae\Verae - Logs folder.lnk" "$WINDIR\explorer.exe" '"$LogDir"'
|
||||
CreateShortCut "$SMPROGRAMS\Verae\Verae - Collect diagnostics.lnk" "$WINDIR\System32\WindowsPowerShell\v1.0\powershell.exe" '-NoProfile -ExecutionPolicy Bypass -File "$INSTDIR\scripts\verae-diag.ps1"'
|
||||
CreateShortCut "$SMPROGRAMS\Verae\Uninstall Verae Print Stamp (debug).lnk" "$INSTDIR\Uninstall.exe"
|
||||
|
||||
DetailPrint "Installing printer(s) — full transcript in $LogDir"
|
||||
nsExec::ExecToLog '${PS} -File "$INSTDIR\scripts\install-printer.ps1" -InstallDir "$INSTDIR"'
|
||||
Pop $0
|
||||
${If} $0 == "0"
|
||||
DetailPrint "Verae printer install: OK"
|
||||
StrCpy $FinishText ""
|
||||
!insertmacro STEP_RESULT "Program files" "$R9"
|
||||
|
||||
; ---- key ----
|
||||
${If} $TokenReady == "1"
|
||||
DetailPrint "Saving Verae key ($TokenMasked) for $TargetUser"
|
||||
nsExec::ExecToLog '$PS -File "$INSTDIR\scripts\user-token.ps1" -Mode save -TokenFile "$PLUGINSDIR\newtoken.txt" -IniOut "$PLUGINSDIR\save.ini"'
|
||||
Pop $0
|
||||
Delete "$PLUGINSDIR\newtoken.txt"
|
||||
ReadINIStr $1 "$PLUGINSDIR\save.ini" save result
|
||||
ReadINIStr $2 "$PLUGINSDIR\save.ini" save path
|
||||
ReadINIStr $3 "$PLUGINSDIR\save.ini" save detail
|
||||
${LOG} "key save: $1 $2 $3"
|
||||
!insertmacro STEP_RESULT "Verae key" "$1 - $TokenMasked saved to $2 $3"
|
||||
${ElseIf} $KeyChoice == "keep"
|
||||
!insertmacro STEP_RESULT "Verae key" "OK - kept existing $KeyMasked"
|
||||
${Else}
|
||||
DetailPrint "Verae printer install: FAILED (exit $0) — run Start menu > Verae > Collect diagnostics"
|
||||
MessageBox MB_ICONEXCLAMATION "Printer setup reported a problem (exit $0).$\r$\nLogs: $LogDir$\r$\nPlease run Start > Verae > Collect diagnostics and send the zip from your Desktop."
|
||||
!insertmacro STEP_RESULT "Verae key" "SKIPPED - you will be asked on first print"
|
||||
${EndIf}
|
||||
|
||||
; ---- printers + helper ----
|
||||
DetailPrint "Installing printers and starting the helper (transcript in $LogDir)"
|
||||
nsExec::ExecToLog '$PS -File "$INSTDIR\scripts\install-printer.ps1" -InstallDir "$INSTDIR" -ResultIni "$PLUGINSDIR\printer.ini"'
|
||||
Pop $0
|
||||
${LOG} "install-printer.ps1 exit=$0"
|
||||
ReadINIStr $1 "$PLUGINSDIR\printer.ini" printer task
|
||||
ReadINIStr $2 "$PLUGINSDIR\printer.ini" printer helper
|
||||
ReadINIStr $3 "$PLUGINSDIR\printer.ini" printer ipp
|
||||
ReadINIStr $4 "$PLUGINSDIR\printer.ini" printer fallback
|
||||
ReadINIStr $5 "$PLUGINSDIR\printer.ini" printer overall
|
||||
ReadINIStr $6 "$PLUGINSDIR\printer.ini" printer spooler
|
||||
ReadINIStr $7 "$PLUGINSDIR\printer.ini" printer oplog
|
||||
${If} $5 == ""
|
||||
StrCpy $5 "FAIL"
|
||||
StrCpy $3 "FAIL - script did not report (exit $0)"
|
||||
${EndIf}
|
||||
!insertmacro STEP_RESULT "Print Spooler" "$6"
|
||||
!insertmacro STEP_RESULT "PrintService log" "$7"
|
||||
!insertmacro STEP_RESULT "Helper autostart task" "$1"
|
||||
!insertmacro STEP_RESULT "Helper running as you" "$2"
|
||||
!insertmacro STEP_RESULT "Printer 'Verae TimeStamp PDF'" "$3"
|
||||
!insertmacro STEP_RESULT "Printer '(Local Port)'" "$4"
|
||||
${LOG} "results: spooler=$6 oplog=$7 task=$1 helper=$2 ipp=$3 fallback=$4 overall=$5"
|
||||
|
||||
${If} $5 == "OK"
|
||||
StrCpy $FinishTitle "Verae TimeStamp PDF is installed"
|
||||
StrCpy $FinishText "$FinishText$\r$\nPrint from any app and choose 'Verae TimeStamp PDF'. Results go to Documents\Verae Timestamps."
|
||||
${Else}
|
||||
StrCpy $FinishTitle "Installed with problems"
|
||||
DetailPrint "Problem detected - collecting diagnostics"
|
||||
nsExec::ExecToLog '$PS -File "$INSTDIR\scripts\verae-diag.ps1" -OutDir "$LogDir"'
|
||||
Pop $0
|
||||
${LOG} "diagnostics collected (exit $0)"
|
||||
StrCpy $FinishText "$FinishText$\r$\nA diagnostics zip (verae-diag-*.zip) was saved in the logs folder. Please send it to support."
|
||||
${EndIf}
|
||||
${LOG} "section end"
|
||||
${GetTime} "" "L" $R1 $R2 $R3 $R4 $R5 $R6 $R7
|
||||
Push "$LogDir\installer-details-$R3$R2$R1-$R5$R6$R7.log"
|
||||
Call DumpLog
|
||||
SectionEnd
|
||||
|
||||
Section "Uninstall"
|
||||
nsExec::ExecToLog '${PS} -File "$INSTDIR\scripts\uninstall-printer.ps1"'
|
||||
nsExec::ExecToLog '"$WINDIR\Sysnative\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File "$INSTDIR\scripts\uninstall-printer.ps1"'
|
||||
Pop $0
|
||||
nsExec::ExecToLog 'taskkill /F /IM verae-printhelper.exe'
|
||||
Pop $0
|
||||
|
|
@ -128,5 +683,5 @@ Section "Uninstall"
|
|||
RMDir "$SMPROGRAMS\Verae"
|
||||
DeleteRegKey HKLM "${UNINST_KEY}"
|
||||
DeleteRegKey HKLM "Software\Verae\PrintStamp"
|
||||
; logs in %ProgramData%\Verae\logs are kept on purpose
|
||||
; logs and the user's .verae\xverae-token are kept on purpose
|
||||
SectionEnd
|
||||
|
|
|
|||
53
platform/windows/scripts/common.ps1
Normal file
53
platform/windows/scripts/common.ps1
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
# Shared helpers for the Verae installer scripts (dot-source me).
|
||||
|
||||
# The interactive (logged-in, non-elevated) user of THIS session — not the admin
|
||||
# account that may have been used for UAC elevation. Owner of explorer.exe in our
|
||||
# session; falls back to Win32_ComputerSystem.UserName (console user).
|
||||
function Get-VeraeTargetUser {
|
||||
$sess = (Get-Process -Id $PID).SessionId
|
||||
$res = $null
|
||||
foreach ($p in @(Get-CimInstance Win32_Process -Filter "Name='explorer.exe'" -ErrorAction SilentlyContinue | Where-Object { $_.SessionId -eq $sess })) {
|
||||
$o = Invoke-CimMethod -InputObject $p -MethodName GetOwner -ErrorAction SilentlyContinue
|
||||
$s = Invoke-CimMethod -InputObject $p -MethodName GetOwnerSid -ErrorAction SilentlyContinue
|
||||
if ($o -and $o.User) { $res = [pscustomobject]@{ Domain = $o.Domain; User = $o.User; Sid = $s.Sid; Source = "explorer(session $sess)" }; break }
|
||||
}
|
||||
if (-not $res) {
|
||||
$cu = (Get-CimInstance Win32_ComputerSystem).UserName
|
||||
if ($cu) {
|
||||
$d, $u = $cu -split '\\', 2
|
||||
$sid = (New-Object Security.Principal.NTAccount($cu)).Translate([Security.Principal.SecurityIdentifier]).Value
|
||||
$res = [pscustomobject]@{ Domain = $d; User = $u; Sid = $sid; Source = "console user" }
|
||||
}
|
||||
}
|
||||
if (-not $res) {
|
||||
$id = [Security.Principal.WindowsIdentity]::GetCurrent()
|
||||
$d, $u = $id.Name -split '\\', 2
|
||||
$res = [pscustomobject]@{ Domain = $d; User = $u; Sid = $id.User.Value; Source = "current process (fallback)" }
|
||||
}
|
||||
$prof = (Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\$($res.Sid)" -ErrorAction SilentlyContinue).ProfileImagePath
|
||||
if (-not $prof) { $prof = Join-Path (Split-Path $env:USERPROFILE -Parent) $res.User }
|
||||
$res | Add-Member -NotePropertyName Profile -NotePropertyValue $prof
|
||||
$res | Add-Member -NotePropertyName Account -NotePropertyValue "$($res.Domain)\$($res.User)"
|
||||
return $res
|
||||
}
|
||||
|
||||
function Get-MaskedToken([string]$t) {
|
||||
if (-not $t) { return "" }
|
||||
$t = $t.Trim()
|
||||
if ($t.Length -le 12) { return ($t.Substring(0, [Math]::Min(4, $t.Length)) + "...") }
|
||||
return ($t.Substring(0, 7) + "..." + $t.Substring($t.Length - 4))
|
||||
}
|
||||
|
||||
# Write a flat INI (ASCII) readable by NSIS ReadINIStr.
|
||||
function Write-VeraeIni([string]$Path, [string]$Section, [System.Collections.IDictionary]$Values) {
|
||||
$lines = @("[$Section]")
|
||||
foreach ($k in $Values.Keys) { $v = "$($Values[$k])" -replace "[\r\n]+", " "; $lines += "$k=$v" }
|
||||
if (Test-Path $Path) {
|
||||
# keep other sections
|
||||
$old = Get-Content $Path -ErrorAction SilentlyContinue
|
||||
$keep = @(); $skip = $false
|
||||
foreach ($l in $old) { if ($l -match '^\[(.+)\]$') { $skip = ($Matches[1] -eq $Section) }; if (-not $skip) { $keep += $l } }
|
||||
$lines = $keep + $lines
|
||||
}
|
||||
Set-Content -Path $Path -Value $lines -Encoding ASCII
|
||||
}
|
||||
|
|
@ -5,7 +5,8 @@ param(
|
|||
[string]$InstallDir = (Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path)),
|
||||
[switch]$SkipIpp,
|
||||
[switch]$SkipFallback,
|
||||
[string]$IppAddr = "127.0.0.1:8631"
|
||||
[string]$IppAddr = "127.0.0.1:8631",
|
||||
[string]$ResultIni = ""
|
||||
)
|
||||
$ErrorActionPreference = "Continue"
|
||||
$PrinterName = "Verae TimeStamp PDF"
|
||||
|
|
@ -18,6 +19,8 @@ $Helper = Join-Path $Bin "verae-printhelper.exe"
|
|||
$Cli = Join-Path $Bin "verae-print.exe"
|
||||
$IppUrl = "http://$IppAddr/ipp/print"
|
||||
$ts = Get-Date -Format "yyyyMMdd-HHmmss"
|
||||
. (Join-Path $PSScriptRoot "common.ps1")
|
||||
$TaskName = "Verae Print Helper"
|
||||
New-Item -ItemType Directory -Force -Path $LogDir, $DropDir | Out-Null
|
||||
$Transcript = Join-Path $LogDir "install-$ts.log"
|
||||
Start-Transcript -Path $Transcript -Force | Out-Null
|
||||
|
|
@ -39,6 +42,8 @@ Write-Host "Verae printer install (debug) $ts"
|
|||
Write-Host "InstallDir=$InstallDir User=$(whoami) PS=$($PSVersionTable.PSVersion) 64bitProcess=$([Environment]::Is64BitProcess)"
|
||||
$os = Get-CimInstance Win32_OperatingSystem
|
||||
Write-Host "OS: $($os.Caption) $($os.Version) build $($os.BuildNumber) $($os.OSArchitecture)"
|
||||
$Target = Get-VeraeTargetUser
|
||||
Write-Host "Target (logged-in) user: $($Target.Account) sid=$($Target.Sid) profile=$($Target.Profile) via $($Target.Source)"
|
||||
$isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
|
||||
Write-Host "Elevated: $isAdmin"
|
||||
if (-not $isAdmin) { Write-Host "ERROR: not elevated — printer/port creation will fail (Access denied)." }
|
||||
|
|
@ -63,9 +68,19 @@ Step "Debug flag HKLM\Software\Verae\PrintStamp\Debug=1" {
|
|||
Set-ItemProperty 'HKLM:\Software\Verae\PrintStamp' -Name Debug -Value 1 -Type DWord
|
||||
Set-ItemProperty 'HKLM:\Software\Verae\PrintStamp' -Name InstallDir -Value $InstallDir
|
||||
} { (Get-ItemProperty 'HKLM:\Software\Verae\PrintStamp').Debug -eq 1 } | Out-Null
|
||||
Step "Helper autostart (HKLM Run)" {
|
||||
Set-ItemProperty 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Run' -Name VeraePrintHelper -Value "`"$Helper`""
|
||||
} { (Get-ItemProperty 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Run').VeraePrintHelper -like "*verae-printhelper.exe*" } | Out-Null
|
||||
$script:taskOk = Step "Helper autostart: scheduled task '$TaskName' for $($Target.Account) (at logon, non-elevated)" {
|
||||
Remove-ItemProperty 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Run' -Name VeraePrintHelper -ErrorAction SilentlyContinue
|
||||
$action = New-ScheduledTaskAction -Execute $Helper -WorkingDirectory $Bin
|
||||
$trigger = New-ScheduledTaskTrigger -AtLogOn -User $Target.Account
|
||||
$principal = New-ScheduledTaskPrincipal -UserId $Target.Account -LogonType Interactive -RunLevel Limited
|
||||
$settings = New-ScheduledTaskSettingsSet -ExecutionTimeLimit ([TimeSpan]::Zero) -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -MultipleInstances IgnoreNew -StartWhenAvailable -RestartCount 3 -RestartInterval (New-TimeSpan -Minutes 1)
|
||||
Register-ScheduledTask -TaskName $TaskName -Action $action -Trigger $trigger -Principal $principal -Settings $settings -Description "Verae TimeStamp PDF print helper (IPP loopback 127.0.0.1:8631)" -Force | Out-Null
|
||||
} { $t = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue; if ($t) { Write-Host " task principal=$($t.Principal.UserId) runlevel=$($t.Principal.RunLevel)" }; [bool]$t }
|
||||
Step "Remove old per-user 0.1.2 install for $($Target.Account)" {
|
||||
$old = Join-Path $Target.Profile "AppData\Local\Verae\PrintStamp\bin\verae-print.exe"
|
||||
if (Test-Path $old) { Remove-Item -Force $old; Write-Host " removed $old" } else { Write-Host " none" }
|
||||
Remove-Item "Registry::HKEY_USERS\$($Target.Sid)\Software\Microsoft\Windows\CurrentVersion\Uninstall\VeraePrintStamp" -Recurse -ErrorAction SilentlyContinue
|
||||
} $null | Out-Null
|
||||
Step "Add bin to machine PATH" {
|
||||
$p = [Environment]::GetEnvironmentVariable("Path","Machine")
|
||||
if ($p -notlike "*$Bin*") { [Environment]::SetEnvironmentVariable("Path", ($p.TrimEnd(';') + ";" + $Bin), "Machine") }
|
||||
|
|
@ -75,22 +90,24 @@ function Test-Ipp { try { $r = Invoke-WebRequest -UseBasicParsing -TimeoutSec 3
|
|||
|
||||
# ---------------- Primary: IPP class driver → loopback helper ----------------
|
||||
if (-not $SkipIpp) {
|
||||
$helperUp = Step "Helper listening on $IppAddr" {
|
||||
Get-Process verae-printhelper -ErrorAction SilentlyContinue | ForEach-Object { Write-Host " existing helper pid=$($_.Id)" }
|
||||
$script:tempHelper = $null
|
||||
$helperUp = Step "Start helper as $($Target.Account) and wait for $IppAddr" {
|
||||
if (-not (Test-Ipp)) {
|
||||
# explorer.exe launches the exe with the (non-elevated) shell token = the real user session
|
||||
Write-Host " starting helper un-elevated via explorer.exe"
|
||||
if ($script:taskOk) { Write-Host " Start-ScheduledTask '$TaskName'"; Start-ScheduledTask -TaskName $TaskName }
|
||||
for ($i = 0; $i -lt 30 -and -not (Test-Ipp); $i++) { Start-Sleep -Milliseconds 500 }
|
||||
}
|
||||
if (-not (Test-Ipp)) {
|
||||
Write-Host " task did not bring IPP up; trying explorer.exe (shell token) launch"
|
||||
Start-Process explorer.exe -ArgumentList "`"$Helper`""
|
||||
for ($i = 0; $i -lt 20 -and -not (Test-Ipp); $i++) { Start-Sleep -Milliseconds 500 }
|
||||
}
|
||||
if (-not (Test-Ipp)) {
|
||||
Write-Host " explorer launch did not bring IPP up; starting elevated temporary helper for discovery"
|
||||
Start-Process -FilePath $Helper -WindowStyle Hidden
|
||||
Write-Host " still down; starting a TEMPORARY elevated helper only for printer discovery (stopped afterwards)"
|
||||
$script:tempHelper = Start-Process -FilePath $Cli -ArgumentList "serve --no-watch" -WindowStyle Hidden -PassThru
|
||||
for ($i = 0; $i -lt 20 -and -not (Test-Ipp); $i++) { Start-Sleep -Milliseconds 500 }
|
||||
}
|
||||
& $Cli ipp-selftest --addr $IppAddr | Out-Host
|
||||
} { Test-Ipp }
|
||||
|
||||
if ($helperUp) {
|
||||
Get-Printer -Name $PrinterName -ErrorAction SilentlyContinue | ForEach-Object { Write-Host " removing stale '$($_.Name)'"; Remove-Printer -Name $_.Name }
|
||||
$before = @(Get-Printer | Select-Object -ExpandProperty Name)
|
||||
|
|
@ -117,6 +134,17 @@ if (-not $SkipIpp) {
|
|||
} else { $summary.ipp = "FAIL: helper not reachable" }
|
||||
}
|
||||
|
||||
if ($script:tempHelper) { Write-Host "Stopping temporary elevated helper pid=$($script:tempHelper.Id)"; Stop-Process -Id $script:tempHelper.Id -Force -ErrorAction SilentlyContinue }
|
||||
$script:helperOwner = ""
|
||||
$helperRunning = Step "Helper running as logged-in user $($Target.Account)" { } {
|
||||
$p = Get-CimInstance Win32_Process -Filter "Name='verae-printhelper.exe'" -ErrorAction SilentlyContinue | Select-Object -First 1
|
||||
if (-not $p) { Write-Host " helper process not found"; return $false }
|
||||
$o = Invoke-CimMethod -InputObject $p -MethodName GetOwner
|
||||
$script:helperOwner = "$($o.Domain)\$($o.User)"
|
||||
Write-Host " helper pid=$($p.ProcessId) owner=$script:helperOwner session=$($p.SessionId)"
|
||||
($script:helperOwner -eq $Target.Account) -and (Test-Ipp)
|
||||
}
|
||||
|
||||
# ---------------- Fallback: Microsoft Print To PDF → Local file port ----------------
|
||||
if (-not $SkipFallback) {
|
||||
$drv = Step "Driver 'Microsoft Print To PDF' present" {
|
||||
|
|
@ -142,6 +170,8 @@ foreach ($ln in 'Microsoft-Windows-PrintService/Admin','Microsoft-Windows-PrintS
|
|||
Get-WinEvent -FilterHashtable @{LogName=$ln; StartTime=(Get-Date).AddMinutes(-15)} -ErrorAction SilentlyContinue |
|
||||
Format-Table TimeCreated,Id,LevelDisplayName,Message -Wrap | Out-String -Width 250 | Write-Host
|
||||
}
|
||||
$summary.target = $Target.Account
|
||||
$summary.helperOwner = $script:helperOwner
|
||||
$summary.finished = (Get-Date).ToString("o")
|
||||
$summary.transcript = $Transcript
|
||||
$summaryPath = Join-Path $LogDir "install-summary-$ts.json"
|
||||
|
|
@ -150,5 +180,19 @@ $okAny = ($summary.ipp -like "OK*") -or ($summary.fallback -eq "OK")
|
|||
Write-Host "`nVerae printer install: $(if ($okAny) {'OK'} else {'FAIL'}) ipp=[$($summary.ipp)] fallback=[$($summary.fallback)]"
|
||||
Write-Host "Log: $Transcript"
|
||||
Write-Host "Summary: $summaryPath"
|
||||
if ($ResultIni) {
|
||||
$stepOk = { param($pat) $x = $summary.steps | Where-Object { $_.step -like $pat } | Select-Object -Last 1; if ($null -eq $x) { "SKIP" } elseif ($x.ok) { "OK" } else { "FAIL" } }
|
||||
Write-VeraeIni $ResultIni "printer" ([ordered]@{
|
||||
target = $Target.Account
|
||||
spooler = (& $stepOk "Spooler running")
|
||||
oplog = (& $stepOk "Enable PrintService*")
|
||||
task = (& $stepOk "Helper autostart*")
|
||||
helper = $(if ($helperRunning) { "OK ($script:helperOwner)" } else { "FAIL ($script:helperOwner)" })
|
||||
ipp = $summary.ipp
|
||||
fallback = $summary.fallback
|
||||
overall = $(if ($okAny) { "OK" } else { "FAIL" })
|
||||
transcript = $Transcript
|
||||
summary = $summaryPath })
|
||||
}
|
||||
Stop-Transcript | Out-Null
|
||||
if ($okAny) { exit 0 } else { exit 1 }
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ Get-Printer | Where-Object { $_.Name -like "Verae TimeStamp PDF*" } | ForEach-Ob
|
|||
Start-Sleep 1
|
||||
$port = Join-Path $env:ProgramData "Verae\spool\fallback\verae-job.pdf"
|
||||
if (Get-PrinterPort -Name $port -ErrorAction SilentlyContinue) { Write-Host "Remove-PrinterPort $port"; Remove-PrinterPort -Name $port }
|
||||
Unregister-ScheduledTask -TaskName "Verae Print Helper" -Confirm:$false -ErrorAction SilentlyContinue
|
||||
Remove-ItemProperty 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Run' -Name VeraePrintHelper -ErrorAction SilentlyContinue
|
||||
Get-Printer | Format-Table Name,DriverName,PortName -AutoSize | Out-String | Write-Host
|
||||
Stop-Transcript | Out-Null
|
||||
|
|
|
|||
76
platform/windows/scripts/user-token.ps1
Normal file
76
platform/windows/scripts/user-token.ps1
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
# Verae signing key handling for the installer (runs elevated, acts for the LOGGED-IN user).
|
||||
# -Mode status : detect target user + existing key → INI [token]
|
||||
# -Mode validate : check key in -TokenFile against Verae (free) → INI [validate]
|
||||
# -Mode save : validate-format + write <profile>\.verae\xverae-token with user-only ACL → INI [save]
|
||||
# The key is never printed; only a masked form (xVerae-...abcd) is logged.
|
||||
param(
|
||||
[ValidateSet("status","validate","save")][string]$Mode = "status",
|
||||
[string]$TokenFile,
|
||||
[Parameter(Mandatory=$true)][string]$IniOut
|
||||
)
|
||||
$ErrorActionPreference = "Continue"
|
||||
. (Join-Path $PSScriptRoot "common.ps1")
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||
$ValidateUrl = "https://zap.verae.ai/v1/status" # documented as free; 401 = bad key
|
||||
|
||||
function Test-VeraeKey([string]$tok) {
|
||||
if ($tok -notmatch '^(xVerae-|key-)\S{8,}$') { return @{ result = "badformat"; detail = "must start with xVerae- (or legacy key-)" } }
|
||||
try {
|
||||
$r = Invoke-WebRequest -UseBasicParsing -TimeoutSec 10 -Uri $ValidateUrl -Headers @{ "x-api-key" = $tok }
|
||||
return @{ result = "valid"; detail = "HTTP $($r.StatusCode)" }
|
||||
} catch {
|
||||
$code = $null
|
||||
if ($_.Exception.Response) { $code = [int]$_.Exception.Response.StatusCode }
|
||||
if ($code -eq 401 -or $code -eq 403) { return @{ result = "invalid"; detail = "HTTP $code from Verae" } }
|
||||
if ($code) { return @{ result = "unknown"; detail = "HTTP $code from Verae" } }
|
||||
return @{ result = "unreachable"; detail = $_.Exception.Message }
|
||||
}
|
||||
}
|
||||
|
||||
$u = Get-VeraeTargetUser
|
||||
$tokPath = Join-Path $u.Profile ".verae\xverae-token"
|
||||
Write-Host "user-token.ps1 mode=$Mode target=$($u.Account) sid=$($u.Sid) profile=$($u.Profile) via=$($u.Source) runningAs=$([Security.Principal.WindowsIdentity]::GetCurrent().Name)"
|
||||
|
||||
switch ($Mode) {
|
||||
"status" {
|
||||
$existing = $null
|
||||
if (Test-Path $tokPath) { $existing = (Get-Content $tokPath -Raw -ErrorAction SilentlyContinue) }
|
||||
if ($existing) { $existing = $existing.Trim() }
|
||||
$v = @{ result = "none"; detail = "" }
|
||||
if ($existing) { $v = Test-VeraeKey $existing }
|
||||
Write-Host "existing key: $([bool]$existing) $(Get-MaskedToken $existing) check=$($v.result) $($v.detail)"
|
||||
Write-VeraeIni $IniOut "token" ([ordered]@{
|
||||
account = $u.Account; sid = $u.Sid; profile = $u.Profile; source = $u.Source; path = $tokPath
|
||||
present = $(if ($existing) { 1 } else { 0 }); masked = (Get-MaskedToken $existing)
|
||||
check = $v.result; detail = $v.detail })
|
||||
}
|
||||
"validate" {
|
||||
$tok = (Get-Content $TokenFile -Raw).Trim()
|
||||
$v = Test-VeraeKey $tok
|
||||
Write-Host "validate $(Get-MaskedToken $tok): $($v.result) $($v.detail)"
|
||||
Write-VeraeIni $IniOut "validate" ([ordered]@{ result = $v.result; detail = $v.detail; masked = (Get-MaskedToken $tok) })
|
||||
}
|
||||
"save" {
|
||||
$tok = (Get-Content $TokenFile -Raw).Trim()
|
||||
$ok = $false; $err = ""
|
||||
try {
|
||||
if ($tok -notmatch '^(xVerae-|key-)\S{8,}$') { throw "bad token format" }
|
||||
$dir = Split-Path $tokPath -Parent
|
||||
New-Item -ItemType Directory -Force -Path $dir | Out-Null
|
||||
# same format as internal/auth.SaveXVeraeToken: token + "\n", no BOM
|
||||
[IO.File]::WriteAllText($tokPath, $tok + "`n", (New-Object Text.UTF8Encoding($false)))
|
||||
# ACL: the logged-in user owns it; only user, SYSTEM, Administrators have access
|
||||
icacls $dir /grant "*$($u.Sid):(OI)(CI)F" /C | Out-Host
|
||||
icacls $dir /setowner "*$($u.Sid)" /C | Out-Host
|
||||
icacls $tokPath /inheritance:r /grant:r "*$($u.Sid):F" "*S-1-5-18:F" "*S-1-5-32-544:F" /C | Out-Host
|
||||
icacls $tokPath /setowner "*$($u.Sid)" /C | Out-Host
|
||||
$back = (Get-Content $tokPath -Raw).Trim()
|
||||
$ok = ($back -eq $tok)
|
||||
if (-not $ok) { $err = "read-back mismatch" }
|
||||
icacls $tokPath | Out-Host
|
||||
} catch { $err = $_.Exception.Message }
|
||||
Write-Host "save $(Get-MaskedToken $tok) -> $tokPath : $(if ($ok) {'OK'} else {"FAIL $err"})"
|
||||
Write-VeraeIni $IniOut "save" ([ordered]@{ result = $(if ($ok) { "OK" } else { "FAIL" }); detail = $err; path = $tokPath; masked = (Get-MaskedToken $tok); account = $u.Account })
|
||||
}
|
||||
}
|
||||
exit 0
|
||||
Loading…
Reference in a new issue