- promptForFilename setting (default true; env VERAE_PROMPT_FOR_FILENAME), 'verae-print config --prompt-for-filename', last Save As folder in ~/.verae/state.json - Windows helper: common file dialog (PDF filter, overwrite prompt, topmost owner) before stamping; macOS printer path: choose file name (NSSavePanel) via 'stamp -prompt-name -title'; cancel discards the job (no stamp/credit) - job.Options.OutPath: original saved at the chosen path, certificate and .verae.json next to it with the chosen base name (never overwritten) - held jobs keep the chosen path (pendingMeta.OutPath) - installer: 'Ask for a file name for each print' checkbox; removed the 'Open the user guide' finish-page checkbox; mac-key-setup asks too - auth test no longer touches the real macOS keychain (VERAE_SKIP_KEYCHAIN) - version 0.3.2
391 lines
13 KiB
Go
391 lines
13 KiB
Go
//go:build darwin
|
|
|
|
package main
|
|
|
|
import (
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"strings"
|
|
|
|
"github.com/VeraeLLC/PrintToVeraeTimeStamp/internal/auth"
|
|
"github.com/VeraeLLC/PrintToVeraeTimeStamp/internal/settings"
|
|
"github.com/VeraeLLC/PrintToVeraeTimeStamp/internal/vlog"
|
|
)
|
|
|
|
// macOS in-installer key registration, run by the pkg postinstall as the
|
|
// console user (launchctl asuser + sudo -H -u). Native AppleScript dialogs;
|
|
// passwords come back through osascript's stdout pipe only — never argv,
|
|
// never files, never logs. Keys are logged masked only.
|
|
|
|
const dlgTitle = "Verae Print Stamp"
|
|
|
|
// AppleScript gets all user-visible text via argv (no string interpolation →
|
|
// no injection). Output: "<button>\t<text returned>" or "CANCEL".
|
|
const dialogScript = `on run argv
|
|
set msg to item 1 of argv
|
|
set defAns to item 2 of argv
|
|
set hide to (item 3 of argv) is "1"
|
|
set btns to paragraphs of (item 4 of argv)
|
|
set defBtn to item 5 of argv
|
|
set useAnswer to (item 6 of argv) is "1"
|
|
tell me to activate
|
|
try
|
|
if useAnswer then
|
|
if hide then
|
|
set r to display dialog msg default answer defAns with hidden answer buttons btns default button defBtn with title "` + dlgTitle + `" giving up after 900
|
|
else
|
|
set r to display dialog msg default answer defAns buttons btns default button defBtn with title "` + dlgTitle + `" giving up after 900
|
|
end if
|
|
if gave up of r then return "CANCEL"
|
|
return (button returned of r) & tab & (text returned of r)
|
|
else
|
|
set r to display dialog msg buttons btns default button defBtn with title "` + dlgTitle + `" giving up after 900
|
|
if gave up of r then return "CANCEL"
|
|
return (button returned of r) & tab
|
|
end if
|
|
on error number -128
|
|
return "CANCEL"
|
|
end try
|
|
end run`
|
|
|
|
const chooseScript = `on run argv
|
|
set msg to item 1 of argv
|
|
set opts to paragraphs of (item 2 of argv)
|
|
tell me to activate
|
|
set r to choose from list opts with prompt msg default items {item 1 of opts} OK button name "Continue" cancel button name "Skip" with title "` + dlgTitle + `"
|
|
if r is false then return "CANCEL"
|
|
return item 1 of r
|
|
end run`
|
|
|
|
var errCancel = errors.New("cancelled")
|
|
|
|
func osa(script string, args ...string) (string, error) {
|
|
cmd := exec.Command("osascript", append([]string{"-e", script, "--"}, args...)...)
|
|
out, err := cmd.Output()
|
|
if err != nil {
|
|
return "", fmt.Errorf("osascript: %v", err)
|
|
}
|
|
return strings.TrimRight(string(out), "\n"), nil
|
|
}
|
|
|
|
// ask shows a dialog; returns (button, answer).
|
|
func ask(msg, def string, hidden, withAnswer bool, buttons []string, defBtn string) (string, string, error) {
|
|
b2 := func(v bool) string {
|
|
if v {
|
|
return "1"
|
|
}
|
|
return "0"
|
|
}
|
|
out, err := osa(dialogScript, msg, def, b2(hidden), strings.Join(buttons, "\n"), defBtn, b2(withAnswer))
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
if out == "CANCEL" {
|
|
return "", "", errCancel
|
|
}
|
|
btn, ans, _ := strings.Cut(out, "\t")
|
|
return btn, ans, nil
|
|
}
|
|
|
|
func say(msg string) { _, _, _ = ask(msg, "", false, false, []string{"OK"}, "OK") }
|
|
|
|
func choose(msg string, opts []string) (string, error) {
|
|
out, err := osa(chooseScript, msg, strings.Join(opts, "\n"))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if out == "CANCEL" {
|
|
return "", errCancel
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
const (
|
|
optKeep = "Keep the existing key"
|
|
optLogin = "Sign in to my Verae account (email + password)"
|
|
optCreate = "Create a new Verae account"
|
|
optPaste = "Paste an xVerae- key"
|
|
optSkip = "Skip for now"
|
|
)
|
|
|
|
func runMacKeySetup(args []string) int {
|
|
fs := flag.NewFlagSet("mac-key-setup", flag.ContinueOnError)
|
|
askOpen := fs.Bool("ask-open", true, "also ask what to open after each print")
|
|
if err := fs.Parse(args); err != nil {
|
|
return 2
|
|
}
|
|
vlog.Init("installer", false)
|
|
vlog.Printf("mac-key-setup start user=%s home=%s", os.Getenv("USER"), os.Getenv("HOME"))
|
|
release, ok := promptLock()
|
|
if !ok {
|
|
say("A Verae sign-in window is already open.")
|
|
return 1
|
|
}
|
|
result, saved := keyFlow("")
|
|
release()
|
|
vlog.Printf("mac-key-setup key result: %s", result)
|
|
fmt.Println("key:", result)
|
|
if saved || strings.HasPrefix(result, "kept") {
|
|
if n := len(listPending()); n > 0 {
|
|
done, remaining, _ := retryPending(stampFile)
|
|
vlog.Printf("mac-key-setup: retried held print jobs: %d stamped, %d remaining", done, remaining)
|
|
fmt.Printf("pending jobs: %d stamped, %d remaining\n", done, remaining)
|
|
}
|
|
}
|
|
if *askOpen {
|
|
mode := askOpenOnComplete()
|
|
fmt.Println("openOnComplete:", mode)
|
|
fmt.Println("promptForFilename:", askPromptForFilename())
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// acquireKeyInteractive: native sign-in dialogs after a 401/403 or missing key.
|
|
func acquireKeyInteractive(reason string) bool {
|
|
_, saved := keyFlow(reason)
|
|
return saved
|
|
}
|
|
|
|
// keyFlow returns a masked result string and whether a NEW key was saved.
|
|
func keyFlow(reason string) (string, bool) {
|
|
existing, _ := auth.LoadXVeraeToken()
|
|
caps := auth.GetCapabilities()
|
|
vlog.Printf("mac-key-setup: installApi=%v emailVerification=%v existing=%v", caps.InstallAPI, caps.EmailVerification, existing != "")
|
|
var opts []string
|
|
intro := "No Verae signing key was found for this Mac user. You need one to timestamp documents."
|
|
if existing != "" {
|
|
st, det := auth.ValidateXVeraeKey(existing)
|
|
vlog.Printf("mac-key-setup: existing key %s check=%s %s", auth.MaskKey(existing), st, det)
|
|
switch st {
|
|
case "valid":
|
|
intro = "A Verae signing key is already saved: " + auth.MaskKey(existing) + " (verified with Verae)."
|
|
opts = append(opts, optKeep)
|
|
case "invalid":
|
|
intro = "A saved key (" + auth.MaskKey(existing) + ") was REJECTED by Verae. Please sign in or paste a new key."
|
|
default:
|
|
intro = "A Verae signing key is saved: " + auth.MaskKey(existing) + " (could not verify right now)."
|
|
opts = append(opts, optKeep)
|
|
}
|
|
}
|
|
opts = append(opts, optLogin, optCreate, optPaste, optSkip)
|
|
if reason != "" {
|
|
intro = reason + "\n\n" + intro
|
|
}
|
|
prompt := intro + "\n\nYour password is sent only to customer.verae.ai over HTTPS and is never stored. Only the issued key is saved (Keychain + ~/.verae)."
|
|
for {
|
|
c, err := choose(prompt, opts)
|
|
if err != nil {
|
|
c = optSkip
|
|
}
|
|
var key string
|
|
switch c {
|
|
case optKeep:
|
|
return "kept " + auth.MaskKey(existing), false
|
|
case optLogin:
|
|
key, err = macLogin(caps)
|
|
case optCreate:
|
|
key, err = macCreate(caps)
|
|
case optPaste:
|
|
key, err = macPaste()
|
|
default:
|
|
btn, _, _ := ask("Continue without a Verae key?\n\nThe printer is installed, but documents cannot be timestamped until you add a key. You will be asked again on your first print (or open Verae Print Stamp from Applications).", "", false, false, []string{"Go back", "Skip"}, "Go back")
|
|
if btn == "Skip" {
|
|
return "skipped", false
|
|
}
|
|
continue
|
|
}
|
|
if errors.Is(err, errCancel) {
|
|
continue // back to the menu
|
|
}
|
|
if err != nil {
|
|
vlog.Errorf("mac-key-setup: %v", err)
|
|
say("Could not get a key: " + err.Error())
|
|
continue
|
|
}
|
|
if err := auth.SaveXVeraeToken(key); err != nil {
|
|
vlog.Errorf("mac-key-setup: save failed: %v", err)
|
|
say("The key could not be saved: " + err.Error())
|
|
continue
|
|
}
|
|
vlog.Printf("mac-key-setup: key saved %s", auth.MaskKey(key))
|
|
say("Your Verae signing key (" + auth.MaskKey(key) + ") is saved.\n\nPrint from any app and choose \"Verae Timestamping\".")
|
|
return "saved " + auth.MaskKey(key), true
|
|
}
|
|
}
|
|
|
|
func macLogin(caps auth.Capabilities) (string, error) {
|
|
email := ""
|
|
for {
|
|
btn, e, err := ask("Sign in to Verae\n\nEmail:", email, false, true, []string{"Back", "Next"}, "Next")
|
|
if err != nil || btn == "Back" {
|
|
return "", errCancel
|
|
}
|
|
email = strings.TrimSpace(e)
|
|
if !auth.ValidEmail(email) {
|
|
say("Please enter a valid email address.")
|
|
continue
|
|
}
|
|
btn, pw, err := ask("Password for "+email+":", "", true, true, []string{"Back", "Sign in"}, "Sign in")
|
|
if err != nil || btn == "Back" {
|
|
continue
|
|
}
|
|
vlog.Printf("mac-key-setup: login %s", auth.MaskEmail(email))
|
|
key, err := auth.InstallLogin(caps, email, pw, "", tokenName())
|
|
if errors.Is(err, auth.ErrTOTPRequired) {
|
|
btn, code, cerr := ask("This account uses two-factor authentication.\n\nEnter the 6-digit code from your authenticator app:", "", false, true, []string{"Back", "Verify"}, "Verify")
|
|
if cerr != nil || btn == "Back" {
|
|
continue
|
|
}
|
|
key, err = auth.InstallLogin(caps, email, pw, strings.TrimSpace(code), tokenName())
|
|
}
|
|
pw = ""
|
|
if err != nil {
|
|
vlog.Errorf("mac-key-setup: login failed for %s: %v", auth.MaskEmail(email), err)
|
|
say("Sign-in failed: " + err.Error())
|
|
continue
|
|
}
|
|
return key, nil
|
|
}
|
|
}
|
|
|
|
func macCreate(caps auth.Capabilities) (string, error) {
|
|
var name, email string
|
|
for {
|
|
btn, n, err := ask("Create a Verae account\n\nYour name:", name, false, true, []string{"Back", "Next"}, "Next")
|
|
if err != nil || btn == "Back" {
|
|
return "", errCancel
|
|
}
|
|
name = strings.TrimSpace(n)
|
|
if name == "" {
|
|
continue
|
|
}
|
|
btn, e, err := ask("Email address:", email, false, true, []string{"Back", "Next"}, "Next")
|
|
if err != nil || btn == "Back" {
|
|
continue
|
|
}
|
|
email = strings.TrimSpace(e)
|
|
if !auth.ValidEmail(email) {
|
|
say("Please enter a valid email address.")
|
|
continue
|
|
}
|
|
policy := caps.Policy.Describe()
|
|
btn, pw, err := ask("Choose a password.\n"+policy, "", true, true, []string{"Back", "Next"}, "Next")
|
|
if err != nil || btn == "Back" {
|
|
continue
|
|
}
|
|
if perr := caps.Policy.Check(pw, email); perr != nil {
|
|
say(perr.Error())
|
|
continue
|
|
}
|
|
btn, pw2, err := ask("Confirm the password:", "", true, true, []string{"Back", "Create account"}, "Create account")
|
|
if err != nil || btn == "Back" {
|
|
continue
|
|
}
|
|
if pw != pw2 {
|
|
say("The passwords do not match.")
|
|
continue
|
|
}
|
|
pw2 = ""
|
|
vlog.Printf("mac-key-setup: register %s installApi=%v", auth.MaskEmail(email), caps.InstallAPI)
|
|
rr, err := auth.InstallRegister(caps, name, email, pw, tokenName())
|
|
pw = ""
|
|
if err != nil {
|
|
vlog.Errorf("mac-key-setup: register failed for %s: %v", auth.MaskEmail(email), err)
|
|
say("Could not create the account: " + err.Error())
|
|
continue
|
|
}
|
|
if rr.PendingID == "" {
|
|
return rr.Key, nil // server without email verification (legacy signup)
|
|
}
|
|
vlog.Printf("mac-key-setup: verification code sent to %s", auth.MaskEmail(email))
|
|
msg := "We emailed a 6-digit code to " + auth.MaskEmail(email) + ".\nIt may take a minute; check spam.\n\nCode:"
|
|
for {
|
|
btn, code, err := ask(msg, "", false, true, []string{"Cancel", "Send a new code", "Verify"}, "Verify")
|
|
if err != nil || btn == "Cancel" {
|
|
return "", errCancel
|
|
}
|
|
if btn == "Send a new code" {
|
|
if rerr := auth.InstallResend(rr.PendingID); rerr != nil {
|
|
say("Could not send a new code: " + rerr.Error())
|
|
} else {
|
|
say("A new code was sent.")
|
|
}
|
|
continue
|
|
}
|
|
key, verr := auth.InstallVerify(rr.PendingID, code, tokenName())
|
|
if verr != nil {
|
|
say(verr.Error())
|
|
continue
|
|
}
|
|
return key, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
func macPaste() (string, error) {
|
|
for {
|
|
btn, k, err := ask("Paste your xVerae- key.\n\nNo key yet? Click \"Open portal\", sign in, open Security and create an xVerae- token (shown once).", "", true, true, []string{"Back", "Open portal", "Save"}, "Save")
|
|
if err != nil || btn == "Back" {
|
|
return "", errCancel
|
|
}
|
|
if btn == "Open portal" {
|
|
_ = exec.Command("open", auth.PortalSignupURL).Start()
|
|
continue
|
|
}
|
|
k = strings.TrimSpace(k)
|
|
st, det := auth.ValidateXVeraeKey(k)
|
|
vlog.Printf("mac-key-setup: pasted key %s: %s %s", auth.MaskKey(k), st, det)
|
|
switch st {
|
|
case "valid":
|
|
return k, nil
|
|
case "invalid":
|
|
say("Verae rejected this key (" + det + "). Please check it and paste again.")
|
|
case "badformat":
|
|
say("That does not look like a Verae key: " + det)
|
|
default:
|
|
b, _, _ := ask("Could not reach Verae to check the key ("+det+").\nSave it anyway?", "", false, false, []string{"No", "Save anyway"}, "Save anyway")
|
|
if b == "Save anyway" {
|
|
return k, nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func askOpenOnComplete() string {
|
|
cur, _ := settings.Load()
|
|
def := map[string]string{settings.OpenFolder: "Show in Finder", settings.OpenPDF: "Open the PDF", settings.OpenNone: "Do nothing"}[cur.OpenOnComplete]
|
|
btn, _, err := ask("After each print is timestamped:\n\n• Show in Finder - reveal the new certificate PDF in Verae Timestamps (recommended)\n• Open the PDF - open the certificate in Preview\n• Do nothing", "", false, false, []string{"Do nothing", "Open the PDF", "Show in Finder"}, def)
|
|
mode := cur.OpenOnComplete
|
|
if err == nil {
|
|
mode = map[string]string{"Show in Finder": settings.OpenFolder, "Open the PDF": settings.OpenPDF, "Do nothing": settings.OpenNone}[btn]
|
|
}
|
|
if err := settings.SetOpenOnComplete(settings.UserPath(""), mode); err != nil {
|
|
vlog.Errorf("mac-key-setup: write config: %v", err)
|
|
} else {
|
|
vlog.Printf("mac-key-setup: openOnComplete=%s saved to %s", mode, settings.UserPath(""))
|
|
}
|
|
return mode
|
|
}
|
|
|
|
// askPromptForFilename: Save As dialog for every print (default on)?
|
|
func askPromptForFilename() bool {
|
|
cur, _ := settings.Load()
|
|
def := "Ask each time"
|
|
if !cur.Prompt() {
|
|
def = "Save automatically"
|
|
}
|
|
btn, _, err := ask("When you print to Verae Timestamping:\n\n• Ask each time - choose the file name and folder in a Save dialog (Cancel discards the print, nothing is stamped)\n• Save automatically - use the document title in Documents/Verae Timestamps", "", false, false, []string{"Save automatically", "Ask each time"}, def)
|
|
on := cur.Prompt()
|
|
if err == nil {
|
|
on = btn == "Ask each time"
|
|
}
|
|
if err := settings.SetPromptForFilename(settings.UserPath(""), map[bool]string{true: "true", false: "false"}[on]); err != nil {
|
|
vlog.Errorf("mac-key-setup: write config: %v", err)
|
|
} else {
|
|
vlog.Printf("mac-key-setup: promptForFilename=%v saved to %s", on, settings.UserPath(""))
|
|
}
|
|
return on
|
|
}
|