mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-08-01 19:18:28 -04:00
fix(network): authenticate Fortinet VPNs natively (#2959)
* fix(network): authenticate Fortinet VPNs natively * cleanup and add test --------- Co-authored-by: Grant Mosha <grant.mosha@nida.go.tz> Co-authored-by: bbedward <bbedward@gmail.com>
This commit is contained in:
@@ -302,17 +302,17 @@ func (a *SecretAgent) GetSecrets(
|
||||
}
|
||||
a.backend.cachedVPNCredsMu.Unlock()
|
||||
|
||||
a.backend.cachedGPSamlMu.Lock()
|
||||
cachedGPSaml := a.backend.cachedGPSamlCookie
|
||||
if cachedGPSaml != nil && cachedGPSaml.ConnectionUUID == connUuid {
|
||||
a.backend.cachedGPSamlCookie = nil
|
||||
a.backend.cachedGPSamlMu.Unlock()
|
||||
a.backend.cachedOpenConnectMu.Lock()
|
||||
cachedOpenConnect := a.backend.cachedOpenConnectAuth
|
||||
if cachedOpenConnect != nil && cachedOpenConnect.ConnectionUUID == connUuid {
|
||||
a.backend.cachedOpenConnectAuth = nil
|
||||
a.backend.cachedOpenConnectMu.Unlock()
|
||||
|
||||
log.Infof("[SecretAgent] Using cached GlobalProtect SAML cookie for %s", connUuid)
|
||||
log.Infof("[SecretAgent] Using cached OpenConnect authentication for %s", connUuid)
|
||||
|
||||
return buildGPSamlSecretsResponse(settingName, cachedGPSaml.Cookie, cachedGPSaml.Host, cachedGPSaml.Fingerprint), nil
|
||||
return buildOpenConnectSecretsResponse(settingName, cachedOpenConnect.Cookie, cachedOpenConnect.Host, cachedOpenConnect.Fingerprint), nil
|
||||
}
|
||||
a.backend.cachedGPSamlMu.Unlock()
|
||||
a.backend.cachedOpenConnectMu.Unlock()
|
||||
|
||||
if len(fields) == 1 && fields[0] == "gp-saml" {
|
||||
gateway := ""
|
||||
@@ -347,17 +347,17 @@ func (a *SecretAgent) GetSecrets(
|
||||
|
||||
log.Infof("[SecretAgent] GlobalProtect SAML authentication successful, returning cookie to NetworkManager")
|
||||
|
||||
a.backend.cachedGPSamlMu.Lock()
|
||||
a.backend.cachedGPSamlCookie = &cachedGPSamlCookie{
|
||||
a.backend.cachedOpenConnectMu.Lock()
|
||||
a.backend.cachedOpenConnectAuth = &cachedOpenConnectAuth{
|
||||
ConnectionUUID: connUuid,
|
||||
Cookie: authResult.Cookie,
|
||||
Host: authResult.Host,
|
||||
User: authResult.User,
|
||||
Fingerprint: authResult.Fingerprint,
|
||||
}
|
||||
a.backend.cachedGPSamlMu.Unlock()
|
||||
a.backend.cachedOpenConnectMu.Unlock()
|
||||
|
||||
return buildGPSamlSecretsResponse(settingName, authResult.Cookie, authResult.Host, authResult.Fingerprint), nil
|
||||
return buildOpenConnectSecretsResponse(settingName, authResult.Cookie, authResult.Host, authResult.Fingerprint), nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -987,7 +987,7 @@ func buildWiFiSecretsResponse(settingName string, secrets map[string]string) nmS
|
||||
return out
|
||||
}
|
||||
|
||||
func buildGPSamlSecretsResponse(settingName, cookie, host, fingerprint string) nmSettingMap {
|
||||
func buildOpenConnectSecretsResponse(settingName, cookie, host, fingerprint string) nmSettingMap {
|
||||
out := nmSettingMap{}
|
||||
vpnSec := nmVariantMap{}
|
||||
|
||||
|
||||
@@ -122,7 +122,7 @@ func TestNeedsExternalBrowserAuth(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildGPSamlSecretsResponse(t *testing.T) {
|
||||
func TestBuildOpenConnectSecretsResponse(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
settingName string
|
||||
@@ -155,7 +155,7 @@ func TestBuildGPSamlSecretsResponse(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := buildGPSamlSecretsResponse(tt.settingName, tt.cookie, tt.host, tt.fingerprint)
|
||||
result := buildOpenConnectSecretsResponse(tt.settingName, tt.cookie, tt.host, tt.fingerprint)
|
||||
|
||||
assert.NotNil(t, result)
|
||||
assert.Contains(t, result, tt.settingName)
|
||||
|
||||
@@ -80,16 +80,16 @@ type NetworkManagerBackend struct {
|
||||
|
||||
hotspotPendingDevice string
|
||||
|
||||
pendingVPNSave *pendingVPNCredentials
|
||||
pendingVPNSaveMu sync.Mutex
|
||||
cachedVPNCreds *cachedVPNCredentials
|
||||
cachedVPNCredsMu sync.Mutex
|
||||
cachedPKCS11PIN *cachedPKCS11PIN
|
||||
cachedPKCS11Mu sync.Mutex
|
||||
cachedGPSamlCookie *cachedGPSamlCookie
|
||||
cachedGPSamlMu sync.Mutex
|
||||
cachedWiFiSecret *cachedWiFiSecret
|
||||
cachedWiFiSecretMu sync.Mutex
|
||||
pendingVPNSave *pendingVPNCredentials
|
||||
pendingVPNSaveMu sync.Mutex
|
||||
cachedVPNCreds *cachedVPNCredentials
|
||||
cachedVPNCredsMu sync.Mutex
|
||||
cachedPKCS11PIN *cachedPKCS11PIN
|
||||
cachedPKCS11Mu sync.Mutex
|
||||
cachedOpenConnectAuth *cachedOpenConnectAuth
|
||||
cachedOpenConnectMu sync.Mutex
|
||||
cachedWiFiSecret *cachedWiFiSecret
|
||||
cachedWiFiSecretMu sync.Mutex
|
||||
|
||||
onStateChange func()
|
||||
}
|
||||
@@ -100,8 +100,9 @@ type pendingVPNCredentials struct {
|
||||
Password string
|
||||
// Secrets holds all VPN secret fields keyed by name (e.g. "cert-pass");
|
||||
// falls back to Password under the "password" key when empty.
|
||||
Secrets map[string]string
|
||||
SavePassword bool
|
||||
Secrets map[string]string
|
||||
PersistentSecrets map[string]string
|
||||
SavePassword bool
|
||||
}
|
||||
|
||||
type cachedVPNCredentials struct {
|
||||
@@ -124,7 +125,7 @@ type cachedWiFiSecret struct {
|
||||
Secrets map[string]string
|
||||
}
|
||||
|
||||
type cachedGPSamlCookie struct {
|
||||
type cachedOpenConnectAuth struct {
|
||||
ConnectionUUID string
|
||||
Cookie string
|
||||
Host string
|
||||
|
||||
@@ -3,6 +3,7 @@ package network
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
@@ -10,16 +11,29 @@ import (
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
)
|
||||
|
||||
type gpSamlAuthResult struct {
|
||||
type openConnectAuthResult struct {
|
||||
Cookie string
|
||||
Host string
|
||||
User string
|
||||
Fingerprint string
|
||||
}
|
||||
|
||||
type openConnectAuthError struct {
|
||||
cause error
|
||||
serverCert string
|
||||
}
|
||||
|
||||
func (e *openConnectAuthError) Error() string {
|
||||
return fmt.Sprintf("openconnect --authenticate failed: %v", e.cause)
|
||||
}
|
||||
|
||||
func (e *openConnectAuthError) Unwrap() error {
|
||||
return e.cause
|
||||
}
|
||||
|
||||
// runGlobalProtectSAMLAuth handles GlobalProtect SAML/SSO authentication using gp-saml-gui.
|
||||
// Only supports protocol=gp. Other protocols need their own implementations.
|
||||
func (b *NetworkManagerBackend) runGlobalProtectSAMLAuth(ctx context.Context, gateway, protocol string) (*gpSamlAuthResult, error) {
|
||||
func (b *NetworkManagerBackend) runGlobalProtectSAMLAuth(ctx context.Context, gateway, protocol string) (*openConnectAuthResult, error) {
|
||||
if gateway == "" {
|
||||
return nil, fmt.Errorf("GP SAML auth: gateway is empty")
|
||||
}
|
||||
@@ -63,7 +77,7 @@ func (b *NetworkManagerBackend) runGlobalProtectSAMLAuth(ctx context.Context, ga
|
||||
}
|
||||
}()
|
||||
|
||||
result := &gpSamlAuthResult{Host: gateway}
|
||||
result := &openConnectAuthResult{Host: gateway}
|
||||
var allOutput []string
|
||||
|
||||
scanner := bufio.NewScanner(stdout)
|
||||
@@ -117,13 +131,8 @@ func (b *NetworkManagerBackend) runGlobalProtectSAMLAuth(ctx context.Context, ga
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func convertGPPreloginCookie(ctx context.Context, gateway, preloginCookie, user string) (*gpSamlAuthResult, error) {
|
||||
ocPath, err := exec.LookPath("openconnect")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("openconnect not found: %w", err)
|
||||
}
|
||||
|
||||
args := []string{
|
||||
func convertGPPreloginCookie(ctx context.Context, gateway, preloginCookie, user string) (*openConnectAuthResult, error) {
|
||||
return runOpenConnectAuthenticate(ctx, []string{
|
||||
"--protocol=gp",
|
||||
"--usergroup=gateway:prelogin-cookie",
|
||||
"--user=" + user,
|
||||
@@ -131,18 +140,83 @@ func convertGPPreloginCookie(ctx context.Context, gateway, preloginCookie, user
|
||||
"--allow-insecure-crypto",
|
||||
"--authenticate",
|
||||
gateway,
|
||||
}, preloginCookie)
|
||||
}
|
||||
|
||||
func runOpenConnectPasswordAuth(
|
||||
ctx context.Context,
|
||||
data map[string]string,
|
||||
username, password, serverCert string,
|
||||
) (*openConnectAuthResult, error) {
|
||||
if data["protocol"] != "fortinet" {
|
||||
return nil, fmt.Errorf("only Fortinet password authentication is supported")
|
||||
}
|
||||
gateway := data["gateway"]
|
||||
if gateway == "" {
|
||||
return nil, fmt.Errorf("OpenConnect gateway is empty")
|
||||
}
|
||||
if username == "" || password == "" {
|
||||
return nil, fmt.Errorf("OpenConnect username and password are required")
|
||||
}
|
||||
|
||||
args := []string{
|
||||
"--protocol=fortinet",
|
||||
"--user=" + username,
|
||||
"--passwd-on-stdin",
|
||||
"--non-inter",
|
||||
}
|
||||
if usergroup := data["usergroup"]; usergroup != "" {
|
||||
args = append(args, "--usergroup="+usergroup)
|
||||
}
|
||||
if serverCert != "" {
|
||||
args = append(args, "--servercert="+serverCert)
|
||||
}
|
||||
args = append(args, "--authenticate", gateway)
|
||||
|
||||
result, err := runOpenConnectAuthenticate(ctx, args, password)
|
||||
if err == nil {
|
||||
result.Host = gateway
|
||||
if result.Fingerprint == "" {
|
||||
result.Fingerprint = serverCert
|
||||
}
|
||||
}
|
||||
return result, err
|
||||
}
|
||||
|
||||
func runOpenConnectAuthenticate(ctx context.Context, args []string, secret string) (*openConnectAuthResult, error) {
|
||||
ocPath, err := exec.LookPath("openconnect")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("openconnect not found: %w", err)
|
||||
}
|
||||
|
||||
cmd := exec.CommandContext(ctx, ocPath, args...)
|
||||
cmd.Stdin = strings.NewReader(preloginCookie)
|
||||
cmd.Stdin = strings.NewReader(secret + "\n")
|
||||
|
||||
output, err := cmd.CombinedOutput()
|
||||
result := parseOpenConnectAuthenticateOutput(string(output))
|
||||
serverCert := suggestedOpenConnectServerCert(string(output))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("openconnect --authenticate failed: %w\noutput: %s", err, string(output))
|
||||
if ctx.Err() != nil {
|
||||
return nil, fmt.Errorf("openconnect authentication timed out or was cancelled: %w", ctx.Err())
|
||||
}
|
||||
return nil, &openConnectAuthError{cause: err, serverCert: serverCert}
|
||||
}
|
||||
if result.Cookie == "" {
|
||||
return nil, &openConnectAuthError{
|
||||
cause: errors.New("no COOKIE in command output"),
|
||||
serverCert: serverCert,
|
||||
}
|
||||
}
|
||||
|
||||
result := &gpSamlAuthResult{}
|
||||
for _, line := range strings.Split(string(output), "\n") {
|
||||
log.Infof("[OpenConnect] Authentication successful: cookie_len=%d, host=%s, has_fingerprint=%v",
|
||||
len(result.Cookie), result.Host, result.Fingerprint != "")
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func parseOpenConnectAuthenticateOutput(output string) *openConnectAuthResult {
|
||||
result := &openConnectAuthResult{}
|
||||
for _, line := range strings.Split(output, "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
switch {
|
||||
case strings.HasPrefix(line, "COOKIE="):
|
||||
@@ -158,15 +232,7 @@ func convertGPPreloginCookie(ctx context.Context, gateway, preloginCookie, user
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if result.Cookie == "" {
|
||||
return nil, fmt.Errorf("no COOKIE in openconnect --authenticate output: %s", string(output))
|
||||
}
|
||||
|
||||
log.Infof("[GP-SAML] openconnect --authenticate: cookie_len=%d, host=%s, fingerprint=%s",
|
||||
len(result.Cookie), result.Host, result.Fingerprint)
|
||||
|
||||
return result, nil
|
||||
return result
|
||||
}
|
||||
|
||||
func unshellQuote(s string) string {
|
||||
@@ -179,7 +245,7 @@ func unshellQuote(s string) string {
|
||||
return s
|
||||
}
|
||||
|
||||
func parseGPSamlFromCommandLine(line string, result *gpSamlAuthResult) {
|
||||
func parseGPSamlFromCommandLine(line string, result *openConnectAuthResult) {
|
||||
if !strings.Contains(line, "openconnect") {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -71,7 +75,7 @@ func TestParseGPSamlFromCommandLine(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
line string
|
||||
initialResult *gpSamlAuthResult
|
||||
initialResult *openConnectAuthResult
|
||||
expectedCookie string
|
||||
expectedUser string
|
||||
expectedFP string
|
||||
@@ -79,7 +83,7 @@ func TestParseGPSamlFromCommandLine(t *testing.T) {
|
||||
{
|
||||
name: "full openconnect command",
|
||||
line: "openconnect --protocol=gp --cookie=AUTH123 --servercert=pin-sha256:ABC --user=john",
|
||||
initialResult: &gpSamlAuthResult{},
|
||||
initialResult: &openConnectAuthResult{},
|
||||
expectedCookie: "AUTH123",
|
||||
expectedUser: "john",
|
||||
expectedFP: "pin-sha256:ABC",
|
||||
@@ -87,7 +91,7 @@ func TestParseGPSamlFromCommandLine(t *testing.T) {
|
||||
{
|
||||
name: "with equals signs in cookie",
|
||||
line: "openconnect --cookie=authcookie=xyz123&portal=GATE --user=jane",
|
||||
initialResult: &gpSamlAuthResult{},
|
||||
initialResult: &openConnectAuthResult{},
|
||||
expectedCookie: "authcookie=xyz123&portal=GATE",
|
||||
expectedUser: "jane",
|
||||
expectedFP: "",
|
||||
@@ -95,7 +99,7 @@ func TestParseGPSamlFromCommandLine(t *testing.T) {
|
||||
{
|
||||
name: "non-openconnect line",
|
||||
line: "some other output",
|
||||
initialResult: &gpSamlAuthResult{},
|
||||
initialResult: &openConnectAuthResult{},
|
||||
expectedCookie: "",
|
||||
expectedUser: "",
|
||||
expectedFP: "",
|
||||
@@ -103,7 +107,7 @@ func TestParseGPSamlFromCommandLine(t *testing.T) {
|
||||
{
|
||||
name: "preserves existing values",
|
||||
line: "openconnect --user=newuser",
|
||||
initialResult: &gpSamlAuthResult{Cookie: "existing", Fingerprint: "existing-fp"},
|
||||
initialResult: &openConnectAuthResult{Cookie: "existing", Fingerprint: "existing-fp"},
|
||||
expectedCookie: "existing",
|
||||
expectedUser: "newuser",
|
||||
expectedFP: "existing-fp",
|
||||
@@ -111,7 +115,7 @@ func TestParseGPSamlFromCommandLine(t *testing.T) {
|
||||
{
|
||||
name: "only updates empty fields",
|
||||
line: "openconnect --cookie=NEW --user=NEW",
|
||||
initialResult: &gpSamlAuthResult{Cookie: "OLD"},
|
||||
initialResult: &openConnectAuthResult{Cookie: "OLD"},
|
||||
expectedCookie: "OLD",
|
||||
expectedUser: "NEW",
|
||||
expectedFP: "",
|
||||
@@ -119,7 +123,7 @@ func TestParseGPSamlFromCommandLine(t *testing.T) {
|
||||
{
|
||||
name: "real gp-saml-gui output",
|
||||
line: "openconnect --protocol=gp --user=john.doe@example.com --os=linux-64 --usergroup=gateway:prelogin-cookie --passwd-on-stdin",
|
||||
initialResult: &gpSamlAuthResult{},
|
||||
initialResult: &openConnectAuthResult{},
|
||||
expectedCookie: "",
|
||||
expectedUser: "john.doe@example.com",
|
||||
expectedFP: "",
|
||||
@@ -127,7 +131,7 @@ func TestParseGPSamlFromCommandLine(t *testing.T) {
|
||||
{
|
||||
name: "with server cert flag",
|
||||
line: "openconnect --servercert=pin-sha256:xp3scfzy3rOgQEXnfPiYKrUk7D66a8b8O+gEXaMPleE= vpn.example.com",
|
||||
initialResult: &gpSamlAuthResult{},
|
||||
initialResult: &openConnectAuthResult{},
|
||||
expectedCookie: "",
|
||||
expectedUser: "",
|
||||
expectedFP: "pin-sha256:xp3scfzy3rOgQEXnfPiYKrUk7D66a8b8O+gEXaMPleE=",
|
||||
@@ -158,7 +162,7 @@ func TestParseGPSamlFromCommandLine_MultipleLines(t *testing.T) {
|
||||
"",
|
||||
}
|
||||
|
||||
result := &gpSamlAuthResult{}
|
||||
result := &openConnectAuthResult{}
|
||||
for _, line := range lines {
|
||||
parseGPSamlFromCommandLine(line, result)
|
||||
}
|
||||
@@ -167,3 +171,19 @@ func TestParseGPSamlFromCommandLine_MultipleLines(t *testing.T) {
|
||||
assert.Empty(t, result.Cookie, "cookie should not be parsed from command line")
|
||||
assert.Empty(t, result.Fingerprint)
|
||||
}
|
||||
|
||||
func TestRunOpenConnectAuthenticateSanitizesFailure(t *testing.T) {
|
||||
binDir := t.TempDir()
|
||||
openConnectPath := filepath.Join(binDir, "openconnect")
|
||||
script := "#!/bin/sh\nprintf '%s\\n' 'Cookie: should-not-leak' 'Add --servercert pin-sha256:TEST-FINGERPRINT' >&2\nexit 1\n"
|
||||
assert.NoError(t, os.WriteFile(openConnectPath, []byte(script), 0o755))
|
||||
t.Setenv("PATH", binDir)
|
||||
|
||||
_, err := runOpenConnectAuthenticate(context.Background(), []string{"--authenticate", "vpn.example.test"}, "password")
|
||||
assert.Error(t, err)
|
||||
assert.NotContains(t, err.Error(), "should-not-leak")
|
||||
|
||||
var authErr *openConnectAuthError
|
||||
assert.True(t, errors.As(err, &authErr))
|
||||
assert.Equal(t, "pin-sha256:TEST-FINGERPRINT", authErr.serverCert)
|
||||
}
|
||||
|
||||
@@ -326,6 +326,7 @@ func (b *NetworkManagerBackend) ConnectVPN(uuidOrName string, singleActive bool)
|
||||
}
|
||||
|
||||
authAction := detectVPNAuthAction(vpnServiceType, vpnData)
|
||||
var openConnectAuth *openConnectAuthResult
|
||||
|
||||
switch authAction {
|
||||
case "openvpn_username":
|
||||
@@ -335,6 +336,19 @@ func (b *NetworkManagerBackend) ConnectVPN(uuidOrName string, singleActive bool)
|
||||
if err := b.handleOpenVPNUsernameAuth(targetConn, connName, targetUUID, vpnServiceType); err != nil {
|
||||
return err
|
||||
}
|
||||
case "openconnect_password":
|
||||
if err := b.ensureOpenConnectAgentFlags(targetConn, vpnData); err != nil {
|
||||
return fmt.Errorf("failed to prepare OpenConnect connection: %w", err)
|
||||
}
|
||||
|
||||
authCtx, authCancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
openConnectAuth, err = b.handleOpenConnectPasswordAuth(
|
||||
authCtx, targetConn, connName, targetUUID, vpnServiceType, vpnData,
|
||||
)
|
||||
authCancel()
|
||||
if err != nil {
|
||||
return fmt.Errorf("OpenConnect authentication failed: %w", err)
|
||||
}
|
||||
case "gp_saml":
|
||||
gateway := vpnData["gateway"]
|
||||
protocol := vpnData["protocol"]
|
||||
@@ -345,7 +359,7 @@ func (b *NetworkManagerBackend) ConnectVPN(uuidOrName string, singleActive bool)
|
||||
log.Infof("[ConnectVPN] GlobalProtect SAML/SSO authentication required for %s (gateway=%s)", connName, gateway)
|
||||
|
||||
samlCtx, samlCancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
authResult, err := b.runGlobalProtectSAMLAuth(samlCtx, gateway, protocol)
|
||||
openConnectAuth, err = b.runGlobalProtectSAMLAuth(samlCtx, gateway, protocol)
|
||||
samlCancel()
|
||||
if err != nil {
|
||||
errMsg := err.Error()
|
||||
@@ -363,16 +377,6 @@ func (b *NetworkManagerBackend) ConnectVPN(uuidOrName string, singleActive bool)
|
||||
}
|
||||
}
|
||||
|
||||
b.cachedGPSamlMu.Lock()
|
||||
b.cachedGPSamlCookie = &cachedGPSamlCookie{
|
||||
ConnectionUUID: targetUUID,
|
||||
Cookie: authResult.Cookie,
|
||||
Host: authResult.Host,
|
||||
User: authResult.User,
|
||||
Fingerprint: authResult.Fingerprint,
|
||||
}
|
||||
b.cachedGPSamlMu.Unlock()
|
||||
|
||||
if err := targetConn.ClearSecrets(); err != nil {
|
||||
log.Warnf("[ConnectVPN] ClearSecrets failed (non-fatal): %v", err)
|
||||
} else {
|
||||
@@ -382,6 +386,19 @@ func (b *NetworkManagerBackend) ConnectVPN(uuidOrName string, singleActive bool)
|
||||
log.Infof("[ConnectVPN] GlobalProtect SAML cookie cached for %s, proceeding with activation", connName)
|
||||
}
|
||||
|
||||
if openConnectAuth != nil {
|
||||
b.cachedOpenConnectMu.Lock()
|
||||
b.cachedOpenConnectAuth = &cachedOpenConnectAuth{
|
||||
ConnectionUUID: targetUUID,
|
||||
Cookie: openConnectAuth.Cookie,
|
||||
Host: openConnectAuth.Host,
|
||||
User: openConnectAuth.User,
|
||||
Fingerprint: openConnectAuth.Fingerprint,
|
||||
}
|
||||
b.cachedOpenConnectMu.Unlock()
|
||||
log.Infof("[ConnectVPN] OpenConnect authentication cached for %s, proceeding with activation", connName)
|
||||
}
|
||||
|
||||
b.stateMutex.Lock()
|
||||
b.state.IsConnectingVPN = true
|
||||
b.state.ConnectingVPNUUID = targetUUID
|
||||
@@ -394,6 +411,13 @@ func (b *NetworkManagerBackend) ConnectVPN(uuidOrName string, singleActive bool)
|
||||
nm := b.nmConn.(gonetworkmanager.NetworkManager)
|
||||
_, err = nm.ActivateConnection(targetConn, nil, nil)
|
||||
if err != nil {
|
||||
b.cachedOpenConnectMu.Lock()
|
||||
b.cachedOpenConnectAuth = nil
|
||||
b.cachedOpenConnectMu.Unlock()
|
||||
b.pendingVPNSaveMu.Lock()
|
||||
b.pendingVPNSave = nil
|
||||
b.pendingVPNSaveMu.Unlock()
|
||||
|
||||
b.stateMutex.Lock()
|
||||
b.state.IsConnectingVPN = false
|
||||
b.state.ConnectingVPNUUID = ""
|
||||
@@ -425,6 +449,9 @@ func detectVPNAuthAction(serviceType string, data map[string]string) string {
|
||||
log.Infof("[VPN] External browser auth detected for protocol '%s' but only GlobalProtect (gp) is currently supported", protocol)
|
||||
}
|
||||
}
|
||||
if protocol == "fortinet" && data["authtype"] == "password" {
|
||||
return "openconnect_password"
|
||||
}
|
||||
case strings.Contains(serviceType, "openvpn"):
|
||||
connType := data["connection-type"]
|
||||
username := data["username"]
|
||||
@@ -435,6 +462,200 @@ func detectVPNAuthAction(serviceType string, data map[string]string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func setOpenConnectAgentFlags(data map[string]string) bool {
|
||||
changed := false
|
||||
for _, field := range []string{"cookie", "gateway", "gwcert"} {
|
||||
key := field + "-flags"
|
||||
if data[key] != "2" {
|
||||
data[key] = "2"
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
return changed
|
||||
}
|
||||
|
||||
func (b *NetworkManagerBackend) ensureOpenConnectAgentFlags(conn gonetworkmanager.Connection, data map[string]string) error {
|
||||
if !setOpenConnectAgentFlags(data) {
|
||||
return nil
|
||||
}
|
||||
if b.dbusConn == nil {
|
||||
return fmt.Errorf("NetworkManager D-Bus connection is unavailable")
|
||||
}
|
||||
|
||||
connObj := b.dbusConn.Object("org.freedesktop.NetworkManager", conn.GetPath())
|
||||
var existingSettings map[string]map[string]dbus.Variant
|
||||
if err := connObj.Call("org.freedesktop.NetworkManager.Settings.Connection.GetSettings", 0).Store(&existingSettings); err != nil {
|
||||
return fmt.Errorf("failed to get connection settings: %w", err)
|
||||
}
|
||||
|
||||
vpn, ok := existingSettings["vpn"]
|
||||
if !ok {
|
||||
return fmt.Errorf("VPN settings are missing")
|
||||
}
|
||||
vpn["data"] = dbus.MakeVariant(data)
|
||||
|
||||
var stored map[string]map[string]dbus.Variant
|
||||
if err := connObj.Call("org.freedesktop.NetworkManager.Settings.Connection.GetSecrets", 0, "vpn").Store(&stored); err != nil {
|
||||
return fmt.Errorf("failed to preserve VPN secrets: %w", err)
|
||||
}
|
||||
if storedVPN, ok := stored["vpn"]; ok {
|
||||
if secrets, ok := storedVPN["secrets"]; ok {
|
||||
vpn["secrets"] = secrets
|
||||
}
|
||||
}
|
||||
|
||||
settings := map[string]map[string]dbus.Variant{"vpn": vpn}
|
||||
if connection, ok := existingSettings["connection"]; ok {
|
||||
settings["connection"] = connection
|
||||
}
|
||||
|
||||
var result map[string]dbus.Variant
|
||||
if err := connObj.Call("org.freedesktop.NetworkManager.Settings.Connection.Update2", 0,
|
||||
settings, uint32(0x1), map[string]dbus.Variant{}).Store(&result); err != nil {
|
||||
return fmt.Errorf("failed to set NetworkManager secret-agent flags: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *NetworkManagerBackend) handleOpenConnectPasswordAuth(
|
||||
ctx context.Context,
|
||||
targetConn gonetworkmanager.Connection,
|
||||
connName, targetUUID, vpnServiceType string,
|
||||
data map[string]string,
|
||||
) (*openConnectAuthResult, error) {
|
||||
username := data["username"]
|
||||
secrets := map[string]string{}
|
||||
if stored, err := targetConn.GetSecrets("vpn"); err == nil {
|
||||
if vpn, ok := stored["vpn"]; ok {
|
||||
if saved, ok := vpn["secrets"].(map[string]string); ok {
|
||||
secrets = saved
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
password := secrets["password"]
|
||||
serverCert := secrets["certificate:"+data["gateway"]]
|
||||
if serverCert == "" {
|
||||
serverCert = secrets["gwcert"]
|
||||
}
|
||||
|
||||
var reply PromptReply
|
||||
if username == "" || password == "" {
|
||||
if b.promptBroker == nil {
|
||||
return nil, fmt.Errorf("password authentication requires an interactive prompt")
|
||||
}
|
||||
|
||||
fields := []string{}
|
||||
fieldsInfo := []FieldInfo{}
|
||||
if username == "" {
|
||||
fields = append(fields, "username")
|
||||
fieldsInfo = append(fieldsInfo, FieldInfo{Name: "username", Label: "Username", IsSecret: false})
|
||||
}
|
||||
if password == "" {
|
||||
fields = append(fields, "password")
|
||||
fieldsInfo = append(fieldsInfo, FieldInfo{Name: "password", Label: "Password", IsSecret: true})
|
||||
}
|
||||
|
||||
token, err := b.promptBroker.Ask(ctx, PromptRequest{
|
||||
Name: connName,
|
||||
ConnType: "vpn",
|
||||
VpnService: vpnServiceType,
|
||||
SettingName: "vpn",
|
||||
Fields: fields,
|
||||
FieldsInfo: fieldsInfo,
|
||||
Reason: "required",
|
||||
ConnectionId: connName,
|
||||
ConnectionUuid: targetUUID,
|
||||
ConnectionPath: string(targetConn.GetPath()),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to request credentials: %w", err)
|
||||
}
|
||||
|
||||
reply, err = b.promptBroker.Wait(ctx, token)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("credentials prompt failed: %w", err)
|
||||
}
|
||||
if username == "" {
|
||||
username = reply.Secrets["username"]
|
||||
}
|
||||
if password == "" {
|
||||
password = reply.Secrets["password"]
|
||||
}
|
||||
}
|
||||
|
||||
auth, err := runOpenConnectPasswordAuth(ctx, data, username, password, serverCert)
|
||||
persistentSecrets := map[string]string{}
|
||||
var authErr *openConnectAuthError
|
||||
if err != nil && errors.As(err, &authErr) && authErr.serverCert != "" && authErr.serverCert != serverCert {
|
||||
if b.promptBroker == nil {
|
||||
return nil, fmt.Errorf("VPN server certificate is untrusted: %s", authErr.serverCert)
|
||||
}
|
||||
|
||||
reason := "server-certificate"
|
||||
if serverCert != "" {
|
||||
reason = "server-certificate-changed"
|
||||
}
|
||||
|
||||
token, promptErr := b.promptBroker.Ask(ctx, PromptRequest{
|
||||
Name: connName,
|
||||
ConnType: "vpn",
|
||||
VpnService: vpnServiceType,
|
||||
SettingName: "vpn",
|
||||
Hints: []string{authErr.serverCert},
|
||||
Reason: reason,
|
||||
ConnectionId: connName,
|
||||
ConnectionUuid: targetUUID,
|
||||
ConnectionPath: string(targetConn.GetPath()),
|
||||
})
|
||||
if promptErr != nil {
|
||||
return nil, fmt.Errorf("failed to request certificate confirmation: %w", promptErr)
|
||||
}
|
||||
if _, promptErr = b.promptBroker.Wait(ctx, token); promptErr != nil {
|
||||
return nil, fmt.Errorf("certificate confirmation failed: %w", promptErr)
|
||||
}
|
||||
|
||||
auth, err = runOpenConnectPasswordAuth(ctx, data, username, password, authErr.serverCert)
|
||||
if err == nil {
|
||||
persistentSecrets["certificate:"+data["gateway"]] = authErr.serverCert
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(reply.Secrets) > 0 || len(persistentSecrets) > 0 {
|
||||
creds := &pendingVPNCredentials{
|
||||
ConnectionPath: string(targetConn.GetPath()),
|
||||
PersistentSecrets: persistentSecrets,
|
||||
}
|
||||
if _, ok := reply.Secrets["username"]; ok {
|
||||
creds.Username = username
|
||||
}
|
||||
if reply.Save {
|
||||
creds.Username = username
|
||||
creds.Password = password
|
||||
creds.Secrets = map[string]string{"password": password}
|
||||
creds.SavePassword = true
|
||||
}
|
||||
b.pendingVPNSaveMu.Lock()
|
||||
b.pendingVPNSave = creds
|
||||
b.pendingVPNSaveMu.Unlock()
|
||||
}
|
||||
|
||||
return auth, nil
|
||||
}
|
||||
|
||||
func suggestedOpenConnectServerCert(output string) string {
|
||||
for _, field := range strings.Fields(output) {
|
||||
field = strings.Trim(field, "'\".,")
|
||||
if strings.HasPrefix(field, "pin-sha256:") {
|
||||
return field
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (b *NetworkManagerBackend) handleOpenVPNUsernameAuth(targetConn gonetworkmanager.Connection, connName, targetUUID, vpnServiceType string) error {
|
||||
log.Infof("[ConnectVPN] OpenVPN requires username in vpn.data - prompting before activation")
|
||||
|
||||
@@ -758,13 +979,13 @@ func (b *NetworkManagerBackend) updateVPNConnectionState() {
|
||||
b.state.VPNErrorUuid = ""
|
||||
b.stateMutex.Unlock()
|
||||
|
||||
// Clear cached PKCS11 PIN and SAML cookie on success
|
||||
// Clear cached one-shot authentication values on success.
|
||||
b.cachedPKCS11Mu.Lock()
|
||||
b.cachedPKCS11PIN = nil
|
||||
b.cachedPKCS11Mu.Unlock()
|
||||
b.cachedGPSamlMu.Lock()
|
||||
b.cachedGPSamlCookie = nil
|
||||
b.cachedGPSamlMu.Unlock()
|
||||
b.cachedOpenConnectMu.Lock()
|
||||
b.cachedOpenConnectAuth = nil
|
||||
b.cachedOpenConnectMu.Unlock()
|
||||
|
||||
b.pendingVPNSaveMu.Lock()
|
||||
pending := b.pendingVPNSave
|
||||
@@ -787,13 +1008,16 @@ func (b *NetworkManagerBackend) updateVPNConnectionState() {
|
||||
b.state.VPNErrorUuid = connectingVPNUUID
|
||||
b.stateMutex.Unlock()
|
||||
|
||||
// Clear cached PKCS11 PIN and SAML cookie on failure
|
||||
// Clear cached one-shot authentication values on failure.
|
||||
b.cachedPKCS11Mu.Lock()
|
||||
b.cachedPKCS11PIN = nil
|
||||
b.cachedPKCS11Mu.Unlock()
|
||||
b.cachedGPSamlMu.Lock()
|
||||
b.cachedGPSamlCookie = nil
|
||||
b.cachedGPSamlMu.Unlock()
|
||||
b.cachedOpenConnectMu.Lock()
|
||||
b.cachedOpenConnectAuth = nil
|
||||
b.cachedOpenConnectMu.Unlock()
|
||||
b.pendingVPNSaveMu.Lock()
|
||||
b.pendingVPNSave = nil
|
||||
b.pendingVPNSaveMu.Unlock()
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -811,13 +1035,16 @@ func (b *NetworkManagerBackend) updateVPNConnectionState() {
|
||||
b.state.VPNErrorUuid = connectingVPNUUID
|
||||
b.stateMutex.Unlock()
|
||||
|
||||
// Clear cached PKCS11 PIN and SAML cookie
|
||||
// Clear cached one-shot authentication values.
|
||||
b.cachedPKCS11Mu.Lock()
|
||||
b.cachedPKCS11PIN = nil
|
||||
b.cachedPKCS11Mu.Unlock()
|
||||
b.cachedGPSamlMu.Lock()
|
||||
b.cachedGPSamlCookie = nil
|
||||
b.cachedGPSamlMu.Unlock()
|
||||
b.cachedOpenConnectMu.Lock()
|
||||
b.cachedOpenConnectAuth = nil
|
||||
b.cachedOpenConnectMu.Unlock()
|
||||
b.pendingVPNSaveMu.Lock()
|
||||
b.pendingVPNSave = nil
|
||||
b.pendingVPNSaveMu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -863,17 +1090,40 @@ func (b *NetworkManagerBackend) saveVPNCredentials(creds *pendingVPNCredentials)
|
||||
log.Infof("[saveVPNCredentials] Saving username")
|
||||
}
|
||||
|
||||
// Save secrets if requested
|
||||
if creds.SavePassword {
|
||||
secs := creds.Secrets
|
||||
if len(secs) == 0 {
|
||||
secs = map[string]string{"password": creds.Password}
|
||||
secs := map[string]string{}
|
||||
if len(creds.PersistentSecrets) > 0 {
|
||||
var stored map[string]map[string]dbus.Variant
|
||||
if err := connObj.Call("org.freedesktop.NetworkManager.Settings.Connection.GetSecrets", 0, "vpn").Store(&stored); err != nil {
|
||||
log.Warnf("[saveVPNCredentials] GetSecrets failed: %v", err)
|
||||
return
|
||||
}
|
||||
for field := range secs {
|
||||
if storedVPN, ok := stored["vpn"]; ok {
|
||||
if storedSecrets, ok := storedVPN["secrets"]; ok {
|
||||
saved, _ := storedSecrets.Value().(map[string]string)
|
||||
for field, value := range saved {
|
||||
secs[field] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
for field, value := range creds.PersistentSecrets {
|
||||
secs[field] = value
|
||||
data[field+"-flags"] = "0"
|
||||
}
|
||||
}
|
||||
|
||||
if creds.SavePassword {
|
||||
toSave := creds.Secrets
|
||||
if len(toSave) == 0 {
|
||||
toSave = map[string]string{"password": creds.Password}
|
||||
}
|
||||
for field, value := range toSave {
|
||||
secs[field] = value
|
||||
data[field+"-flags"] = "0"
|
||||
}
|
||||
}
|
||||
if len(secs) > 0 {
|
||||
vpn["secrets"] = dbus.MakeVariant(secs)
|
||||
log.Infof("[saveVPNCredentials] Saving %d secret field(s) with flags=0", len(secs))
|
||||
log.Infof("[saveVPNCredentials] Saving %d secret field(s)", len(secs))
|
||||
}
|
||||
|
||||
vpn["data"] = dbus.MakeVariant(data)
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
mock_gonetworkmanager "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/github.com/Wifx/gonetworkmanager/v2"
|
||||
"github.com/Wifx/gonetworkmanager/v2"
|
||||
"github.com/godbus/dbus/v5"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -136,3 +140,137 @@ func TestNetworkManagerBackend_UpdateVPNConnectionState_EmptyUUID(t *testing.T)
|
||||
backend.updateVPNConnectionState()
|
||||
})
|
||||
}
|
||||
|
||||
func TestDetectVPNAuthAction_FortinetPasswordOnly(t *testing.T) {
|
||||
service := "org.freedesktop.NetworkManager.openconnect"
|
||||
|
||||
assert.Equal(t, "openconnect_password", detectVPNAuthAction(service, map[string]string{
|
||||
"protocol": "fortinet",
|
||||
"authtype": "password",
|
||||
}))
|
||||
assert.Empty(t, detectVPNAuthAction(service, map[string]string{
|
||||
"protocol": "anyconnect",
|
||||
"authtype": "password",
|
||||
}))
|
||||
assert.Empty(t, detectVPNAuthAction(service, map[string]string{
|
||||
"protocol": "fortinet",
|
||||
"authtype": "saml",
|
||||
}))
|
||||
}
|
||||
|
||||
func TestEnsureOpenConnectAgentFlags(t *testing.T) {
|
||||
data := map[string]string{"protocol": "fortinet"}
|
||||
assert.True(t, setOpenConnectAgentFlags(data))
|
||||
assert.Equal(t, "2", data["cookie-flags"])
|
||||
assert.Equal(t, "2", data["gateway-flags"])
|
||||
assert.Equal(t, "2", data["gwcert-flags"])
|
||||
assert.False(t, setOpenConnectAgentFlags(data))
|
||||
}
|
||||
|
||||
func TestOpenConnectCertificateConfirmation(t *testing.T) {
|
||||
binDir := t.TempDir()
|
||||
openConnectPath := filepath.Join(binDir, "openconnect")
|
||||
script := `#!/bin/sh
|
||||
case "$*" in
|
||||
*--servercert=pin-sha256:TEST-FINGERPRINT*)
|
||||
printf '%s\n' "COOKIE='SVPNCOOKIE=test'" "HOST='vpn.example.test'" "FINGERPRINT='pin-sha256:TEST-FINGERPRINT'"
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
printf '%s\n' 'Add --servercert pin-sha256:TEST-FINGERPRINT' >&2
|
||||
exit 1
|
||||
`
|
||||
assert.NoError(t, os.WriteFile(openConnectPath, []byte(script), 0o755))
|
||||
t.Setenv("PATH", binDir)
|
||||
|
||||
conn := mock_gonetworkmanager.NewMockConnection(t)
|
||||
connPath := dbus.ObjectPath("/org/freedesktop/NetworkManager/Settings/999")
|
||||
conn.EXPECT().GetSecrets("vpn").Return(gonetworkmanager.ConnectionSettings{
|
||||
"vpn": {"secrets": map[string]string{"password": "test-password"}},
|
||||
}, nil)
|
||||
conn.EXPECT().GetPath().Return(connPath).Twice()
|
||||
|
||||
broker := &fakePromptBroker{
|
||||
asked: make(chan PromptRequest, 1),
|
||||
reply: PromptReply{},
|
||||
}
|
||||
backend := &NetworkManagerBackend{promptBroker: broker}
|
||||
data := map[string]string{
|
||||
"gateway": "vpn.example.test:443",
|
||||
"protocol": "fortinet",
|
||||
"authtype": "password",
|
||||
"username": "test-user",
|
||||
}
|
||||
|
||||
result, err := backend.handleOpenConnectPasswordAuth(
|
||||
context.Background(), conn, "Test VPN", "test-uuid",
|
||||
"org.freedesktop.NetworkManager.openconnect", data,
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "SVPNCOOKIE=test", result.Cookie)
|
||||
assert.Equal(t, "vpn.example.test:443", result.Host)
|
||||
|
||||
prompt := <-broker.asked
|
||||
assert.Equal(t, "server-certificate", prompt.Reason)
|
||||
assert.Equal(t, []string{"pin-sha256:TEST-FINGERPRINT"}, prompt.Hints)
|
||||
|
||||
assert.Equal(t, map[string]string{
|
||||
"certificate:vpn.example.test:443": "pin-sha256:TEST-FINGERPRINT",
|
||||
}, backend.pendingVPNSave.PersistentSecrets)
|
||||
}
|
||||
|
||||
func TestOpenConnectCertificateRotationReprompts(t *testing.T) {
|
||||
binDir := t.TempDir()
|
||||
openConnectPath := filepath.Join(binDir, "openconnect")
|
||||
script := `#!/bin/sh
|
||||
case "$*" in
|
||||
*--servercert=pin-sha256:NEW-FINGERPRINT*)
|
||||
printf '%s\n' "COOKIE='SVPNCOOKIE=test'" "HOST='vpn.example.test'" "FINGERPRINT='pin-sha256:NEW-FINGERPRINT'"
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
printf '%s\n' 'Add --servercert pin-sha256:NEW-FINGERPRINT' >&2
|
||||
exit 1
|
||||
`
|
||||
assert.NoError(t, os.WriteFile(openConnectPath, []byte(script), 0o755))
|
||||
t.Setenv("PATH", binDir)
|
||||
|
||||
conn := mock_gonetworkmanager.NewMockConnection(t)
|
||||
connPath := dbus.ObjectPath("/org/freedesktop/NetworkManager/Settings/999")
|
||||
conn.EXPECT().GetSecrets("vpn").Return(gonetworkmanager.ConnectionSettings{
|
||||
"vpn": {"secrets": map[string]string{
|
||||
"password": "test-password",
|
||||
"certificate:vpn.example.test:443": "pin-sha256:OLD-FINGERPRINT",
|
||||
}},
|
||||
}, nil)
|
||||
conn.EXPECT().GetPath().Return(connPath).Twice()
|
||||
|
||||
broker := &fakePromptBroker{
|
||||
asked: make(chan PromptRequest, 1),
|
||||
reply: PromptReply{},
|
||||
}
|
||||
backend := &NetworkManagerBackend{promptBroker: broker}
|
||||
data := map[string]string{
|
||||
"gateway": "vpn.example.test:443",
|
||||
"protocol": "fortinet",
|
||||
"authtype": "password",
|
||||
"username": "test-user",
|
||||
}
|
||||
|
||||
result, err := backend.handleOpenConnectPasswordAuth(
|
||||
context.Background(), conn, "Test VPN", "test-uuid",
|
||||
"org.freedesktop.NetworkManager.openconnect", data,
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "SVPNCOOKIE=test", result.Cookie)
|
||||
|
||||
prompt := <-broker.asked
|
||||
assert.Equal(t, "server-certificate-changed", prompt.Reason)
|
||||
assert.Equal(t, []string{"pin-sha256:NEW-FINGERPRINT"}, prompt.Hints)
|
||||
|
||||
assert.Equal(t, map[string]string{
|
||||
"certificate:vpn.example.test:443": "pin-sha256:NEW-FINGERPRINT",
|
||||
}, backend.pendingVPNSave.PersistentSecrets)
|
||||
assert.False(t, backend.pendingVPNSave.SavePassword)
|
||||
assert.Empty(t, backend.pendingVPNSave.Secrets)
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ DankModal {
|
||||
property string promptToken: ""
|
||||
property string promptReason: ""
|
||||
property var promptFields: []
|
||||
property var promptHints: []
|
||||
property string promptSetting: ""
|
||||
|
||||
property bool isVpnPrompt: false
|
||||
@@ -40,17 +41,21 @@ DankModal {
|
||||
property var fieldsInfo: []
|
||||
property var secretValues: ({})
|
||||
|
||||
readonly property bool isCertificateChangedPrompt: promptReason === "server-certificate-changed"
|
||||
readonly property bool isCertificatePrompt: promptReason === "server-certificate" || isCertificateChangedPrompt
|
||||
readonly property string serverCertificateFingerprint: promptHints.length > 0 ? promptHints[0] : ""
|
||||
readonly property bool showUsernameField: requiresEnterprise && !isVpnPrompt && fieldsInfo.length === 0
|
||||
readonly property bool showPasswordField: fieldsInfo.length === 0
|
||||
readonly property bool showPasswordField: fieldsInfo.length === 0 && !isCertificatePrompt
|
||||
readonly property bool showAnonField: requiresEnterprise && !isVpnPrompt
|
||||
readonly property bool showDomainField: requiresEnterprise && !isVpnPrompt
|
||||
readonly property bool showSavePasswordCheckbox: (isVpnPrompt || fieldsInfo.length > 0) && promptReason !== "pkcs11"
|
||||
readonly property bool showSavePasswordCheckbox: (isVpnPrompt || fieldsInfo.length > 0) && promptReason !== "pkcs11" && !isCertificatePrompt
|
||||
|
||||
readonly property int inputFieldHeight: Theme.fontSizeMedium + Theme.spacingL * 2
|
||||
readonly property int inputFieldWithSpacing: inputFieldHeight + Theme.spacingM
|
||||
readonly property int checkboxRowHeight: Theme.fontSizeMedium + Theme.spacingS
|
||||
readonly property int headerHeight: Theme.fontSizeLarge + Theme.fontSizeMedium + Theme.spacingM * 2
|
||||
readonly property int buttonRowHeight: 36 + Theme.spacingM
|
||||
readonly property int certificateWarningHeight: certificateWarningColumn.implicitHeight + Theme.spacingM * 2
|
||||
|
||||
property int calculatedHeight: {
|
||||
let h = headerHeight + buttonRowHeight + Theme.spacingL * 2;
|
||||
@@ -67,10 +72,16 @@ DankModal {
|
||||
h += inputFieldWithSpacing;
|
||||
if (showSavePasswordCheckbox)
|
||||
h += checkboxRowHeight;
|
||||
if (isCertificatePrompt)
|
||||
h += certificateWarningHeight + Theme.spacingM;
|
||||
return h;
|
||||
}
|
||||
|
||||
function focusFirstField() {
|
||||
if (isCertificatePrompt) {
|
||||
connectButton.forceActiveFocus();
|
||||
return;
|
||||
}
|
||||
if (fieldsInfo.length > 0) {
|
||||
if (dynamicFieldsRepeater.count > 0) {
|
||||
const firstItem = dynamicFieldsRepeater.itemAt(0);
|
||||
@@ -101,6 +112,7 @@ DankModal {
|
||||
promptToken = "";
|
||||
promptReason = "";
|
||||
promptFields = [];
|
||||
promptHints = [];
|
||||
promptSetting = "";
|
||||
isVpnPrompt = false;
|
||||
connectionName = "";
|
||||
@@ -127,6 +139,7 @@ DankModal {
|
||||
promptToken = "";
|
||||
promptReason = "";
|
||||
promptFields = [];
|
||||
promptHints = [];
|
||||
promptSetting = "";
|
||||
isVpnPrompt = false;
|
||||
connectionName = "";
|
||||
@@ -145,6 +158,7 @@ DankModal {
|
||||
promptToken = token;
|
||||
promptReason = reason;
|
||||
promptFields = fields || [];
|
||||
promptHints = hints || [];
|
||||
promptSetting = setting || "802-11-wireless-security";
|
||||
connectionType = connType || "802-11-wireless";
|
||||
connectionName = connName || ssid || "";
|
||||
@@ -324,6 +338,8 @@ DankModal {
|
||||
text: {
|
||||
if (promptReason === "pkcs11")
|
||||
return I18n.tr("Smartcard Authentication");
|
||||
if (isCertificatePrompt)
|
||||
return I18n.tr("Untrusted VPN certificate", "Title for VPN server certificate trust confirmation");
|
||||
if (isVpnPrompt)
|
||||
return I18n.tr("Connect to VPN");
|
||||
if (isHiddenNetwork)
|
||||
@@ -343,6 +359,8 @@ DankModal {
|
||||
text: {
|
||||
if (promptReason === "pkcs11")
|
||||
return I18n.tr("Enter PIN for ") + wifiPasswordSSID;
|
||||
if (isCertificatePrompt)
|
||||
return wifiPasswordSSID;
|
||||
if (fieldsInfo.length > 0)
|
||||
return I18n.tr("Enter credentials for ") + wifiPasswordSSID;
|
||||
if (isVpnPrompt)
|
||||
@@ -383,6 +401,45 @@ DankModal {
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: certificateWarningBox
|
||||
|
||||
readonly property color warningTone: isCertificateChangedPrompt ? Theme.error : Theme.warning
|
||||
|
||||
width: parent.width
|
||||
height: certificateWarningHeight
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(warningTone, 0.12)
|
||||
border.color: Theme.withAlpha(warningTone, 0.5)
|
||||
border.width: 1
|
||||
visible: isCertificatePrompt
|
||||
|
||||
Column {
|
||||
id: certificateWarningColumn
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
spacing: Theme.spacingS
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: isCertificateChangedPrompt ? I18n.tr("The server certificate has changed since it was last trusted. Only continue if you recognize the new fingerprint.", "Warning shown when a trusted VPN server certificate no longer matches") : I18n.tr("Only continue if you recognize this server certificate fingerprint.", "Warning shown before trusting an unverified VPN server certificate")
|
||||
wrapMode: Text.Wrap
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: serverCertificateFingerprint
|
||||
wrapMode: Text.WrapAnywhere
|
||||
font.family: SettingsData.monoFontFamily
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: certificateWarningBox.warningTone
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: inputFieldHeight
|
||||
@@ -690,10 +747,15 @@ DankModal {
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: connectButton
|
||||
|
||||
width: Math.max(80, connectText.contentWidth + Theme.spacingM * 2)
|
||||
height: 36
|
||||
radius: Theme.cornerRadius
|
||||
color: connectArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary
|
||||
border.color: activeFocus ? Theme.surfaceText : "transparent"
|
||||
border.width: activeFocus ? 2 : 0
|
||||
activeFocusOnTab: true
|
||||
enabled: {
|
||||
if (fieldsInfo.length > 0) {
|
||||
for (var i = 0; i < fieldsInfo.length; i++) {
|
||||
@@ -705,6 +767,8 @@ DankModal {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (isCertificatePrompt)
|
||||
return serverCertificateFingerprint.length > 0;
|
||||
if (isVpnPrompt)
|
||||
return passwordInput.text.length > 0;
|
||||
if (isHiddenNetwork)
|
||||
@@ -716,7 +780,7 @@ DankModal {
|
||||
StyledText {
|
||||
id: connectText
|
||||
anchors.centerIn: parent
|
||||
text: I18n.tr("Connect")
|
||||
text: isCertificatePrompt ? I18n.tr("Trust", "Button that approves a VPN server certificate fingerprint") : I18n.tr("Connect")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.background
|
||||
font.weight: Font.Medium
|
||||
@@ -731,6 +795,22 @@ DankModal {
|
||||
onClicked: submitCredentialsAndClose()
|
||||
}
|
||||
|
||||
Keys.onReturnPressed: event => {
|
||||
if (enabled)
|
||||
submitCredentialsAndClose();
|
||||
event.accepted = true;
|
||||
}
|
||||
Keys.onEnterPressed: event => {
|
||||
if (enabled)
|
||||
submitCredentialsAndClose();
|
||||
event.accepted = true;
|
||||
}
|
||||
Keys.onSpacePressed: event => {
|
||||
if (enabled)
|
||||
submitCredentialsAndClose();
|
||||
event.accepted = true;
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
|
||||
Reference in New Issue
Block a user