PrintToVeraeTimeStamp/cmd/verae-print/keysetup_darwin.go

370 lines
12 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)
}
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
}