mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-28 22:17:48 -04:00
network: answer openvpn dynamic challenge prompts with the challenge-response secret
fixes #3952 port 2.0
This commit is contained in:
1 parent
18af7856f9
commit
19f3e58dcb
3 files changed
+99
-11
No files matched your search
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -278,7 +279,7 @@ func (a *SecretAgent) GetSecrets(
|
||||
|
||||
a.backend.cachedVPNCredsMu.Lock()
|
||||
cached := a.backend.cachedVPNCreds
|
||||
if cached != nil && cached.ConnectionUUID == connUuid {
|
||||
if cached != nil && cached.ConnectionUUID == connUuid && slices.Contains(fields, "password") {
|
||||
a.backend.cachedVPNCreds = nil
|
||||
a.backend.cachedVPNCredsMu.Unlock()
|
||||
|
||||
@@ -451,7 +452,7 @@ func (a *SecretAgent) GetSecrets(
|
||||
VpnService: vpnSvc,
|
||||
SettingName: settingName,
|
||||
Fields: fields,
|
||||
FieldsInfo: buildFieldsInfo(settingName, fields, vpnSvc),
|
||||
FieldsInfo: buildFieldsInfo(settingName, fields, hints, vpnSvc),
|
||||
Hints: hints,
|
||||
Reason: reason,
|
||||
ConnectionId: connId,
|
||||
@@ -577,13 +578,13 @@ func (a *SecretAgent) GetSecrets(
|
||||
default:
|
||||
out[settingName] = sec
|
||||
}
|
||||
if settingName == "vpn" && a.backend != nil && !isPKCS11 && (vpnUsername != "" || reply.Save) {
|
||||
secrets := make(map[string]string)
|
||||
for k, v := range reply.Secrets {
|
||||
if k != "username" {
|
||||
secrets[k] = v
|
||||
}
|
||||
secrets := make(map[string]string)
|
||||
for k, v := range reply.Secrets {
|
||||
if k != "username" && k != "challenge-response" {
|
||||
secrets[k] = v
|
||||
}
|
||||
}
|
||||
if settingName == "vpn" && a.backend != nil && !isPKCS11 && (vpnUsername != "" || (reply.Save && len(secrets) > 0)) {
|
||||
a.backend.pendingVPNSaveMu.Lock()
|
||||
a.backend.pendingVPNSave = &pendingVPNCredentials{
|
||||
ConnectionPath: string(path),
|
||||
@@ -835,13 +836,46 @@ func fieldsNeeded(setting string, hints []string, conn map[string]nmVariantMap)
|
||||
return hints
|
||||
}
|
||||
return infer8021xFields(conn)
|
||||
case "vpn", "wireguard":
|
||||
case "vpn":
|
||||
return parseVPNHints(hints).fields
|
||||
case "wireguard":
|
||||
return hints
|
||||
default:
|
||||
return []string{}
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
vpnHintMessagePrefix = "x-vpn-message:"
|
||||
vpnHintChallengePrefix = "x-dynamic-challenge:"
|
||||
vpnHintChallengeEchoPrefix = "x-dynamic-challenge-echo:"
|
||||
)
|
||||
|
||||
type vpnPromptHints struct {
|
||||
fields []string
|
||||
echo map[string]bool
|
||||
}
|
||||
|
||||
// NM-openvpn asks for a CRV1 challenge answer as x-dynamic-challenge[-echo]:<secret>
|
||||
// and NM appends the plugin's prompt text as x-vpn-message:<text>, which the modal shows.
|
||||
func parseVPNHints(hints []string) vpnPromptHints {
|
||||
parsed := vpnPromptHints{echo: map[string]bool{}}
|
||||
for _, hint := range hints {
|
||||
switch {
|
||||
case strings.HasPrefix(hint, vpnHintMessagePrefix):
|
||||
case strings.HasPrefix(hint, vpnHintChallengeEchoPrefix):
|
||||
field := strings.TrimPrefix(hint, vpnHintChallengeEchoPrefix)
|
||||
parsed.echo[field] = true
|
||||
parsed.fields = append(parsed.fields, field)
|
||||
case strings.HasPrefix(hint, vpnHintChallengePrefix):
|
||||
parsed.fields = append(parsed.fields, strings.TrimPrefix(hint, vpnHintChallengePrefix))
|
||||
default:
|
||||
parsed.fields = append(parsed.fields, hint)
|
||||
}
|
||||
}
|
||||
return parsed
|
||||
}
|
||||
|
||||
func infer8021xFields(conn map[string]nmVariantMap) []string {
|
||||
dot1x, ok := conn["802-1x"]
|
||||
if !ok {
|
||||
@@ -877,7 +911,8 @@ func infer8021xFields(conn map[string]nmVariantMap) []string {
|
||||
return fields
|
||||
}
|
||||
|
||||
func buildFieldsInfo(setting string, fields []string, vpnService string) []FieldInfo {
|
||||
func buildFieldsInfo(setting string, fields []string, hints []string, vpnService string) []FieldInfo {
|
||||
echo := parseVPNHints(hints).echo
|
||||
result := make([]FieldInfo, 0, len(fields))
|
||||
for _, f := range fields {
|
||||
info := FieldInfo{Name: f}
|
||||
@@ -899,6 +934,9 @@ func buildFieldsInfo(setting string, fields []string, vpnService string) []Field
|
||||
}
|
||||
case "vpn":
|
||||
info.Label, info.IsSecret = vpnFieldMeta(f, vpnService)
|
||||
if echo[f] {
|
||||
info.IsSecret = false
|
||||
}
|
||||
default:
|
||||
info.Label = f
|
||||
info.IsSecret = true
|
||||
@@ -1052,6 +1090,8 @@ func vpnFieldMeta(field, _ string) (label string, isSecret bool) {
|
||||
return "IPSec Pre-Shared Key", true
|
||||
case "cert-pass":
|
||||
return "Certificate Password", true
|
||||
case "challenge-response":
|
||||
return "Challenge Response", true
|
||||
case "http-proxy-password":
|
||||
return "HTTP Proxy Password", true
|
||||
case "username":
|
||||
|
||||
@@ -308,6 +308,47 @@ func TestSecretAgent_GetSecrets_NoInteractionFlag(t *testing.T) {
|
||||
assert.Contains(t, err.Error(), "NoSecrets")
|
||||
}
|
||||
|
||||
func TestSecretAgent_GetSecrets_OpenVPNDynamicChallenge(t *testing.T) {
|
||||
conn := map[string]nmVariantMap{
|
||||
"connection": {
|
||||
"id": dbus.MakeVariant("Work VPN"),
|
||||
"type": dbus.MakeVariant("vpn"),
|
||||
},
|
||||
"vpn": {
|
||||
"service-type": dbus.MakeVariant("org.freedesktop.NetworkManager.openvpn"),
|
||||
},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
hint string
|
||||
isSecret bool
|
||||
}{
|
||||
{"noecho", "x-dynamic-challenge:challenge-response", true},
|
||||
{"echo", "x-dynamic-challenge-echo:challenge-response", false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
broker := &fakePromptBroker{
|
||||
asked: make(chan PromptRequest, 1),
|
||||
reply: PromptReply{Secrets: map[string]string{"challenge-response": "123456"}},
|
||||
}
|
||||
agent := &SecretAgent{prompts: broker}
|
||||
hints := []string{tt.hint, "x-vpn-message:Enter your OTP"}
|
||||
|
||||
out, err := agent.GetSecrets(conn, "/test/path", "vpn", hints, nmSecretAgentFlagAllowInteraction)
|
||||
assert.Nil(t, err)
|
||||
|
||||
req := <-broker.asked
|
||||
assert.Equal(t, []string{"challenge-response"}, req.Fields)
|
||||
assert.Equal(t, []FieldInfo{{Name: "challenge-response", Label: "Challenge Response", IsSecret: tt.isSecret}}, req.FieldsInfo)
|
||||
|
||||
secrets, _ := out["vpn"]["secrets"].Value().(map[string]string)
|
||||
assert.Equal(t, map[string]string{"challenge-response": "123456"}, secrets)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildWiFiSecretsResponse(t *testing.T) {
|
||||
t.Run("wpa-psk returns psk", func(t *testing.T) {
|
||||
out := buildWiFiSecretsResponse("802-11-wireless-security", map[string]string{"psk": "hunter2"})
|
||||
|
||||
@@ -47,13 +47,18 @@ DModal {
|
||||
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 string vpnPromptMessage: {
|
||||
const hint = promptHints.find(h => typeof h === "string" && h.startsWith("x-vpn-message:"));
|
||||
return hint ? hint.slice("x-vpn-message:".length) : "";
|
||||
}
|
||||
readonly property bool isChallengePrompt: promptHints.some(h => typeof h === "string" && h.startsWith("x-dynamic-challenge"))
|
||||
readonly property bool showUsernameField: requiresEnterprise && !isVpnPrompt && fieldsInfo.length === 0
|
||||
readonly property bool showPasswordField: fieldsInfo.length === 0 && !isCertificatePrompt
|
||||
readonly property bool showEapFields: requiresEnterprise && !isVpnPrompt && !isPromptMode
|
||||
readonly property bool showPhase2Dropdown: eapMethodValue === "peap" || eapMethodValue === "ttls"
|
||||
readonly property bool showAnonField: requiresEnterprise && !isVpnPrompt && !isPromptMode && eapMethodValue !== "pwd"
|
||||
readonly property bool showDomainField: showAnonField
|
||||
readonly property bool showSavePasswordCheckbox: (isVpnPrompt || fieldsInfo.length > 0) && promptReason !== "pkcs11" && !isCertificatePrompt
|
||||
readonly property bool showSavePasswordCheckbox: (isVpnPrompt || fieldsInfo.length > 0) && promptReason !== "pkcs11" && !isCertificatePrompt && !isChallengePrompt
|
||||
|
||||
readonly property int certificateWarningHeight: certificateWarningColumn.implicitHeight + Theme.spacingM * 2
|
||||
|
||||
@@ -318,6 +323,8 @@ DModal {
|
||||
return I18n.tr("Enter PIN for %1", "%1 is the device or network name").arg(wifiPasswordSSID);
|
||||
if (isCertificatePrompt)
|
||||
return wifiPasswordSSID;
|
||||
if (vpnPromptMessage)
|
||||
return vpnPromptMessage;
|
||||
if (fieldsInfo.length > 0)
|
||||
return I18n.tr("Enter credentials for %1", "%1 is the network name").arg(wifiPasswordSSID);
|
||||
if (isVpnPrompt)
|
||||
|
||||
Reference in new issue
Block a user