- auth.PasswordForgot / auth.PasswordReset (+ httptest tests); ErrResetUnavailable on 404/401/405/5xx so dialogs offer https://customer.verae.ai/portal/ - keyflow_gui: 'Forgot password?' on the password step and after a failed sign-in - Windows: Forgot password? link, forgot/reset/unavailable forms - bump HelperVersion, deb and pkg default VERSION to 0.3.4
365 lines
12 KiB
Go
365 lines
12 KiB
Go
//go:build darwin || linux
|
|
|
|
package main
|
|
|
|
// Shared native sign-in flow (macOS AppleScript dialogs, Linux zenity dialogs).
|
|
// Platform files provide ask/say/choose/openURL, machineWord and keyStoreDesc.
|
|
// Passwords travel only over the dialog's stdout pipe and HTTPS; never argv,
|
|
// files or logs. Keys are logged masked only.
|
|
|
|
import (
|
|
"errors"
|
|
"strings"
|
|
|
|
"github.com/VeraeLLC/PrintToVeraeTimeStamp/internal/auth"
|
|
"github.com/VeraeLLC/PrintToVeraeTimeStamp/internal/vlog"
|
|
)
|
|
|
|
var errCancel = errors.New("cancelled")
|
|
|
|
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"
|
|
)
|
|
|
|
// acquireKeyInteractive: native sign-in dialogs after a 401/403 or missing key.
|
|
func acquireKeyInteractive(reason string) bool {
|
|
if !guiAvailable() {
|
|
vlog.Printf("sign-in needed but no desktop session for dialogs: %s (run: verae-print key-dialog)", reason)
|
|
return false
|
|
}
|
|
_, 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 " + machineWord + " 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 (" + keyStoreDesc + ")."
|
|
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, _, aerr := 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 run Verae Print Stamp from your apps menu).", "", false, false, []string{"Go back", "Skip"}, "Go back")
|
|
if aerr != nil || 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
|
|
}
|
|
}
|
|
|
|
const btnForgot = "Forgot password?"
|
|
|
|
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", btnForgot, "Sign in"}, "Sign in")
|
|
if err != nil || btn == "Back" {
|
|
continue
|
|
}
|
|
if btn == btnForgot {
|
|
key, ferr := macForgot(caps, email)
|
|
if errors.Is(ferr, errCancel) {
|
|
continue
|
|
}
|
|
return key, ferr
|
|
}
|
|
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)
|
|
b, _, _ := ask("Sign-in failed: "+err.Error(), "", false, false, []string{btnForgot, "Try again"}, "Try again")
|
|
if b == btnForgot {
|
|
key, ferr := macForgot(caps, email)
|
|
if errors.Is(ferr, errCancel) {
|
|
continue
|
|
}
|
|
return key, ferr
|
|
}
|
|
continue
|
|
}
|
|
return key, nil
|
|
}
|
|
}
|
|
|
|
// offerPortalReset tells the user in-app reset is unavailable and offers the web portal.
|
|
func offerPortalReset() {
|
|
b, _, _ := ask("Password reset isn't available in the app yet.\n\nYou can reset your password on the Verae customer portal in your browser ("+auth.PortalResetURL+").", "", false, false, []string{"Back", "Open portal"}, "Open portal")
|
|
if b == "Open portal" {
|
|
openURL(auth.PortalResetURL)
|
|
}
|
|
}
|
|
|
|
// macForgot: emailed-code password reset. Returns a minted key, or errCancel.
|
|
func macForgot(caps auth.Capabilities, email string) (string, error) {
|
|
for {
|
|
btn, e, err := ask("Reset your Verae password\n\nWe'll email a 6-digit code to this address:", email, false, true, []string{"Back", "Send code"}, "Send code")
|
|
if err != nil || btn == "Back" {
|
|
return "", errCancel
|
|
}
|
|
email = strings.TrimSpace(e)
|
|
if !auth.ValidEmail(email) {
|
|
say("Please enter a valid email address.")
|
|
continue
|
|
}
|
|
vlog.Printf("mac-key-setup: password reset requested for %s", auth.MaskEmail(email))
|
|
if ferr := auth.PasswordForgot(email); ferr != nil {
|
|
vlog.Errorf("mac-key-setup: password forgot %s: %v", auth.MaskEmail(email), ferr)
|
|
if errors.Is(ferr, auth.ErrResetUnavailable) {
|
|
offerPortalReset()
|
|
return "", errCancel
|
|
}
|
|
say("Could not send a reset code: " + ferr.Error())
|
|
continue
|
|
}
|
|
msg := "If " + email + " has a Verae account, we emailed it a 6-digit code (valid 15 minutes).\nIt may take a minute; check spam.\n\nCode:"
|
|
codeLoop:
|
|
for {
|
|
btn, code, err := ask(msg, "", false, true, []string{"Cancel", "Send a new code", "Next"}, "Next")
|
|
if err != nil || btn == "Cancel" {
|
|
return "", errCancel
|
|
}
|
|
if btn == "Send a new code" {
|
|
if rerr := auth.PasswordForgot(email); rerr != nil {
|
|
if errors.Is(rerr, auth.ErrResetUnavailable) {
|
|
offerPortalReset()
|
|
return "", errCancel
|
|
}
|
|
say("Could not send a new code: " + rerr.Error())
|
|
} else {
|
|
say("A new code is on its way.")
|
|
}
|
|
continue
|
|
}
|
|
code = strings.TrimSpace(code)
|
|
if len(code) != 6 || strings.Trim(code, "0123456789") != "" {
|
|
say("Enter the 6-digit code from the email.")
|
|
continue
|
|
}
|
|
for {
|
|
btn, pw, err := ask("Choose a new password.\n"+caps.Policy.Describe(), "", true, true, []string{"Back", "Next"}, "Next")
|
|
if err != nil || btn == "Back" {
|
|
continue codeLoop
|
|
}
|
|
if perr := caps.Policy.Check(pw, email); perr != nil {
|
|
say(perr.Error())
|
|
continue
|
|
}
|
|
btn, pw2, err := ask("Confirm the new password:", "", true, true, []string{"Back", "Reset password"}, "Reset password")
|
|
if err != nil || btn == "Back" {
|
|
continue
|
|
}
|
|
if pw != pw2 {
|
|
say("The passwords do not match.")
|
|
continue
|
|
}
|
|
pw2 = ""
|
|
key, rerr := auth.PasswordReset(email, code, pw, tokenName())
|
|
if rerr != nil {
|
|
pw = ""
|
|
vlog.Errorf("mac-key-setup: password reset %s: %v", auth.MaskEmail(email), rerr)
|
|
if errors.Is(rerr, auth.ErrResetUnavailable) {
|
|
offerPortalReset()
|
|
return "", errCancel
|
|
}
|
|
say("Could not reset the password: " + rerr.Error())
|
|
continue codeLoop
|
|
}
|
|
vlog.Printf("mac-key-setup: password reset ok for %s (key returned=%v)", auth.MaskEmail(email), key != "")
|
|
if key != "" {
|
|
return key, nil
|
|
}
|
|
// No key minted (e.g. 2FA account): sign in with the new password.
|
|
key, lerr := auth.InstallLogin(caps, email, pw, "", tokenName())
|
|
if errors.Is(lerr, auth.ErrTOTPRequired) {
|
|
b, totp, cerr := ask("Your password was changed.\n\nThis account uses two-factor authentication. Enter the 6-digit code from your authenticator app:", "", false, true, []string{"Cancel", "Verify"}, "Verify")
|
|
if cerr != nil || b == "Cancel" {
|
|
pw = ""
|
|
return "", errCancel
|
|
}
|
|
key, lerr = auth.InstallLogin(caps, email, pw, strings.TrimSpace(totp), tokenName())
|
|
}
|
|
pw = ""
|
|
if lerr != nil {
|
|
say("Your password was changed, but signing in failed: " + lerr.Error() + "\n\nChoose \"Sign in\" and use your new password.")
|
|
return "", errCancel
|
|
}
|
|
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" {
|
|
openURL(auth.PortalSignupURL)
|
|
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
|
|
}
|
|
}
|
|
}
|
|
}
|