1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-01-31 08:52:49 -05:00

Compare commits

...

3 Commits

Author SHA1 Message Date
bbedward
37f972d075 vpn: update pksc11 handling 2025-12-31 15:42:41 -05:00
Oscar R.
7d8de6e6f0 Improving the logic for start-hyprland wrapper use (#1220)
* Adding a way to use the start-hyprland wrapper when it's needed from Hyprland 0.53 it's recommended because offers more security if happens a fail

* Deleting unnecessary things and doing verifications

* fix pre-commit

* Changing to not depend on hyprctl to obtain version and avoid posible problems

---------

Co-authored-by: bbedward <bbedward@gmail.com>
2025-12-31 13:27:05 -05:00
bbedward
7ff751f8a2 vpn: attempt to support pkcs11 prompts 2025-12-31 10:03:49 -05:00
7 changed files with 223 additions and 119 deletions

View File

@@ -233,6 +233,9 @@ func (a *SecretAgent) GetSecrets(
if a.manager != nil && connType == "802-11-wireless" && a.manager.WasRecentlyFailed(ssid) { if a.manager != nil && connType == "802-11-wireless" && a.manager.WasRecentlyFailed(ssid) {
reason = "wrong-password" reason = "wrong-password"
} }
if settingName == "vpn" && isPKCS11Auth(conn, vpnSvc) {
reason = "pkcs11"
}
var connId, connUuid string var connId, connUuid string
if c, ok := conn["connection"]; ok { if c, ok := conn["connection"]; ok {
@@ -579,6 +582,15 @@ func inferVPNFields(conn map[string]nmVariantMap, vpnService string) []string {
connType := dataMap["connection-type"] connType := dataMap["connection-type"]
switch { switch {
case strings.Contains(vpnService, "openconnect"):
authType := dataMap["authtype"]
userCert := dataMap["usercert"]
if authType == "cert" && strings.HasPrefix(userCert, "pkcs11:") {
return []string{"key_pass"}
}
if dataMap["username"] == "" {
fields = []string{"username", "password"}
}
case strings.Contains(vpnService, "openvpn"): case strings.Contains(vpnService, "openvpn"):
if connType == "password" || connType == "password-tls" { if connType == "password" || connType == "password-tls" {
if dataMap["username"] == "" { if dataMap["username"] == "" {
@@ -586,7 +598,7 @@ func inferVPNFields(conn map[string]nmVariantMap, vpnService string) []string {
} }
} }
case strings.Contains(vpnService, "vpnc"), strings.Contains(vpnService, "l2tp"), case strings.Contains(vpnService, "vpnc"), strings.Contains(vpnService, "l2tp"),
strings.Contains(vpnService, "pptp"), strings.Contains(vpnService, "openconnect"): strings.Contains(vpnService, "pptp"):
if dataMap["username"] == "" { if dataMap["username"] == "" {
fields = []string{"username", "password"} fields = []string{"username", "password"}
} }
@@ -597,6 +609,8 @@ func inferVPNFields(conn map[string]nmVariantMap, vpnService string) []string {
func vpnFieldMeta(field, vpnService string) (label string, isSecret bool) { func vpnFieldMeta(field, vpnService string) (label string, isSecret bool) {
switch field { switch field {
case "key_pass":
return "PIN", true
case "password": case "password":
return "Password", true return "Password", true
case "Xauth password": case "Xauth password":
@@ -624,6 +638,25 @@ func vpnFieldMeta(field, vpnService string) (label string, isSecret bool) {
return titleCaser.String(strings.ReplaceAll(field, "-", " ")), false return titleCaser.String(strings.ReplaceAll(field, "-", " ")), false
} }
func isPKCS11Auth(conn map[string]nmVariantMap, vpnService string) bool {
if !strings.Contains(vpnService, "openconnect") {
return false
}
vpnSettings, ok := conn["vpn"]
if !ok {
return false
}
dataVariant, ok := vpnSettings["data"]
if !ok {
return false
}
dataMap, ok := dataVariant.Value().(map[string]string)
if !ok {
return false
}
return dataMap["authtype"] == "cert" && strings.HasPrefix(dataMap["usercert"], "pkcs11:")
}
func readVPNPasswordFlags(conn map[string]nmVariantMap, settingName string) uint32 { func readVPNPasswordFlags(conn map[string]nmVariantMap, settingName string) uint32 {
if settingName != "vpn" { if settingName != "vpn" {
return 0xFFFF return 0xFFFF

View File

@@ -282,111 +282,26 @@ func (b *NetworkManagerBackend) ConnectVPN(uuidOrName string, singleActive bool)
} }
} }
needsUsernamePrePrompt := false
var vpnServiceType string var vpnServiceType string
var vpnData map[string]string
if vpnSettings, ok := targetSettings["vpn"]; ok { if vpnSettings, ok := targetSettings["vpn"]; ok {
if svc, ok := vpnSettings["service-type"].(string); ok { if svc, ok := vpnSettings["service-type"].(string); ok {
vpnServiceType = svc vpnServiceType = svc
} }
if data, ok := vpnSettings["data"].(map[string]string); ok { if data, ok := vpnSettings["data"].(map[string]string); ok {
connType := data["connection-type"] vpnData = data
username := data["username"]
// OpenVPN password auth needs username in vpn.data
if strings.Contains(vpnServiceType, "openvpn") &&
(connType == "password" || connType == "password-tls") &&
username == "" {
needsUsernamePrePrompt = true
}
} }
} }
// If username is needed but missing, prompt for it before activating authAction := detectVPNAuthAction(vpnServiceType, vpnData)
if needsUsernamePrePrompt && b.promptBroker != nil {
log.Infof("[ConnectVPN] OpenVPN requires username in vpn.data - prompting before activation")
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) switch authAction {
defer cancel() case "openvpn_username":
if b.promptBroker == nil {
token, err := b.promptBroker.Ask(ctx, PromptRequest{ return fmt.Errorf("OpenVPN password authentication requires interactive prompt")
Name: connName,
ConnType: "vpn",
VpnService: vpnServiceType,
SettingName: "vpn",
Fields: []string{"username", "password"},
FieldsInfo: []FieldInfo{{Name: "username", Label: "Username", IsSecret: false}, {Name: "password", Label: "Password", IsSecret: true}},
Reason: "required",
ConnectionId: connName,
ConnectionUuid: targetUUID,
ConnectionPath: string(targetConn.GetPath()),
})
if err != nil {
return fmt.Errorf("failed to request credentials: %w", err)
} }
if err := b.handleOpenVPNUsernameAuth(targetConn, connName, targetUUID, vpnServiceType); err != nil {
reply, err := b.promptBroker.Wait(ctx, token) return err
if err != nil {
return fmt.Errorf("credentials prompt failed: %w", err)
}
username := reply.Secrets["username"]
password := reply.Secrets["password"]
if username != "" {
connObj := b.dbusConn.Object("org.freedesktop.NetworkManager", targetConn.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 settings for username save: %w", err)
}
settings := make(map[string]map[string]dbus.Variant)
if connSection, ok := existingSettings["connection"]; ok {
settings["connection"] = connSection
}
vpn := existingSettings["vpn"]
var data map[string]string
if dataVariant, ok := vpn["data"]; ok {
if dm, ok := dataVariant.Value().(map[string]string); ok {
data = make(map[string]string)
for k, v := range dm {
data[k] = v
}
} else {
data = make(map[string]string)
}
} else {
data = make(map[string]string)
}
data["username"] = username
if reply.Save && password != "" {
data["password-flags"] = "0"
secs := make(map[string]string)
secs["password"] = password
vpn["secrets"] = dbus.MakeVariant(secs)
log.Infof("[ConnectVPN] Saving username and password to vpn.data")
} else {
log.Infof("[ConnectVPN] Saving username to vpn.data (password will be prompted)")
}
vpn["data"] = dbus.MakeVariant(data)
settings["vpn"] = vpn
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 save username: %w", err)
}
log.Infof("[ConnectVPN] Username saved to connection, now activating")
if password != "" && !reply.Save {
b.cachedVPNCredsMu.Lock()
b.cachedVPNCreds = &cachedVPNCredentials{
ConnectionUUID: targetUUID,
Password: password,
SavePassword: reply.Save,
}
b.cachedVPNCredsMu.Unlock()
log.Infof("[ConnectVPN] Cached password for GetSecrets")
}
} }
} }
@@ -417,6 +332,119 @@ func (b *NetworkManagerBackend) ConnectVPN(uuidOrName string, singleActive bool)
return nil return nil
} }
func detectVPNAuthAction(serviceType string, data map[string]string) string {
if data == nil {
return ""
}
switch {
case strings.Contains(serviceType, "openvpn"):
connType := data["connection-type"]
username := data["username"]
if (connType == "password" || connType == "password-tls") && username == "" {
return "openvpn_username"
}
}
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")
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancel()
token, err := b.promptBroker.Ask(ctx, PromptRequest{
Name: connName,
ConnType: "vpn",
VpnService: vpnServiceType,
SettingName: "vpn",
Fields: []string{"username", "password"},
FieldsInfo: []FieldInfo{{Name: "username", Label: "Username", IsSecret: false}, {Name: "password", Label: "Password", IsSecret: true}},
Reason: "required",
ConnectionId: connName,
ConnectionUuid: targetUUID,
ConnectionPath: string(targetConn.GetPath()),
})
if err != nil {
return fmt.Errorf("failed to request credentials: %w", err)
}
reply, err := b.promptBroker.Wait(ctx, token)
if err != nil {
return fmt.Errorf("credentials prompt failed: %w", err)
}
if reply.Cancel {
return fmt.Errorf("user cancelled authentication")
}
username := reply.Secrets["username"]
password := reply.Secrets["password"]
if username == "" {
return nil
}
connObj := b.dbusConn.Object("org.freedesktop.NetworkManager", targetConn.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 settings for username save: %w", err)
}
settings := make(map[string]map[string]dbus.Variant)
if connSection, ok := existingSettings["connection"]; ok {
settings["connection"] = connSection
}
vpn := existingSettings["vpn"]
var data map[string]string
if dataVariant, ok := vpn["data"]; ok {
if dm, ok := dataVariant.Value().(map[string]string); ok {
data = make(map[string]string)
for k, v := range dm {
data[k] = v
}
} else {
data = make(map[string]string)
}
} else {
data = make(map[string]string)
}
data["username"] = username
if reply.Save && password != "" {
data["password-flags"] = "0"
secs := make(map[string]string)
secs["password"] = password
vpn["secrets"] = dbus.MakeVariant(secs)
log.Infof("[ConnectVPN] Saving username and password to vpn.data")
} else {
log.Infof("[ConnectVPN] Saving username to vpn.data (password will be prompted)")
}
vpn["data"] = dbus.MakeVariant(data)
settings["vpn"] = vpn
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 save username: %w", err)
}
log.Infof("[ConnectVPN] Username saved to connection")
if password != "" && !reply.Save {
b.cachedVPNCredsMu.Lock()
b.cachedVPNCreds = &cachedVPNCredentials{
ConnectionUUID: targetUUID,
Password: password,
SavePassword: reply.Save,
}
b.cachedVPNCredsMu.Unlock()
log.Infof("[ConnectVPN] Cached password for GetSecrets")
}
return nil
}
func (b *NetworkManagerBackend) DisconnectVPN(uuidOrName string) error { func (b *NetworkManagerBackend) DisconnectVPN(uuidOrName string) error {
nm := b.nmConn.(gonetworkmanager.NetworkManager) nm := b.nmConn.(gonetworkmanager.NetworkManager)

View File

@@ -10,6 +10,7 @@ FloatingWindow {
property string passwordInput: "" property string passwordInput: ""
property var currentFlow: PolkitService.agent?.flow property var currentFlow: PolkitService.agent?.flow
property bool isLoading: false property bool isLoading: false
readonly property int inputFieldHeight: Theme.fontSizeMedium + Theme.spacingL * 2
property int calculatedHeight: Math.max(240, headerRow.implicitHeight + mainColumn.implicitHeight + Theme.spacingM * 3) property int calculatedHeight: Math.max(240, headerRow.implicitHeight + mainColumn.implicitHeight + Theme.spacingM * 3)
function focusPasswordField() { function focusPasswordField() {
@@ -202,7 +203,7 @@ FloatingWindow {
Rectangle { Rectangle {
width: parent.width width: parent.width
height: 50 height: inputFieldHeight
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.surfaceHover color: Theme.surfaceHover
border.color: passwordField.activeFocus ? Theme.primary : Theme.outlineStrong border.color: passwordField.activeFocus ? Theme.primary : Theme.outlineStrong

View File

@@ -28,14 +28,29 @@ FloatingWindow {
property var fieldsInfo: [] property var fieldsInfo: []
property var secretValues: ({}) property var secretValues: ({})
readonly property bool showUsernameField: requiresEnterprise && !isVpnPrompt && fieldsInfo.length === 0
readonly property bool showPasswordField: fieldsInfo.length === 0
readonly property bool showAnonField: requiresEnterprise && !isVpnPrompt
readonly property bool showDomainField: requiresEnterprise && !isVpnPrompt
readonly property bool showShowPasswordCheckbox: fieldsInfo.length === 0
readonly property bool showSavePasswordCheckbox: (isVpnPrompt || fieldsInfo.length > 0) && promptReason !== "pkcs11"
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
property int calculatedHeight: { property int calculatedHeight: {
if (fieldsInfo.length > 0) let h = headerHeight + buttonRowHeight + Theme.spacingL * 2;
return 180 + (fieldsInfo.length * 60); h += fieldsInfo.length * inputFieldWithSpacing;
if (requiresEnterprise) if (showUsernameField) h += inputFieldWithSpacing;
return 430; if (showPasswordField) h += inputFieldWithSpacing;
if (isVpnPrompt) if (showAnonField) h += inputFieldWithSpacing;
return 260; if (showDomainField) h += inputFieldWithSpacing;
return 230; if (showShowPasswordCheckbox) h += checkboxRowHeight;
if (showSavePasswordCheckbox) h += checkboxRowHeight;
return h;
} }
function focusFirstField() { function focusFirstField() {
@@ -127,6 +142,7 @@ FloatingWindow {
case "private-key-password": case "private-key-password":
return I18n.tr("Private Key Password"); return I18n.tr("Private Key Password");
case "pin": case "pin":
case "key_pass":
return I18n.tr("PIN"); return I18n.tr("PIN");
case "psk": case "psk":
return I18n.tr("Password"); return I18n.tr("Password");
@@ -188,7 +204,13 @@ FloatingWindow {
} }
objectName: "wifiPasswordModal" objectName: "wifiPasswordModal"
title: isVpnPrompt ? I18n.tr("VPN Password") : I18n.tr("Wi-Fi Password") title: {
if (promptReason === "pkcs11")
return I18n.tr("Smartcard PIN");
if (isVpnPrompt)
return I18n.tr("VPN Password");
return I18n.tr("Wi-Fi Password");
}
minimumSize: Qt.size(420, calculatedHeight) minimumSize: Qt.size(420, calculatedHeight)
maximumSize: Qt.size(420, calculatedHeight) maximumSize: Qt.size(420, calculatedHeight)
color: Theme.surfaceContainer color: Theme.surfaceContainer
@@ -242,7 +264,7 @@ FloatingWindow {
Column { Column {
id: contentCol id: contentCol
anchors.centerIn: parent anchors.centerIn: parent
width: parent.width - Theme.spacingM * 2 width: parent.width - Theme.spacingL * 2
spacing: Theme.spacingM spacing: Theme.spacingM
Row { Row {
@@ -260,7 +282,13 @@ FloatingWindow {
spacing: Theme.spacingXS spacing: Theme.spacingXS
StyledText { StyledText {
text: isVpnPrompt ? I18n.tr("Connect to VPN") : I18n.tr("Connect to Wi-Fi") text: {
if (promptReason === "pkcs11")
return I18n.tr("Smartcard Authentication");
if (isVpnPrompt)
return I18n.tr("Connect to VPN");
return I18n.tr("Connect to Wi-Fi");
}
font.pixelSize: Theme.fontSizeLarge font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText color: Theme.surfaceText
font.weight: Font.Medium font.weight: Font.Medium
@@ -272,6 +300,8 @@ FloatingWindow {
StyledText { StyledText {
text: { text: {
if (promptReason === "pkcs11")
return I18n.tr("Enter PIN for ") + wifiPasswordSSID;
if (fieldsInfo.length > 0) if (fieldsInfo.length > 0)
return I18n.tr("Enter credentials for ") + wifiPasswordSSID; return I18n.tr("Enter credentials for ") + wifiPasswordSSID;
if (isVpnPrompt) if (isVpnPrompt)
@@ -325,7 +355,7 @@ FloatingWindow {
required property int index required property int index
width: contentCol.width width: contentCol.width
height: 50 height: inputFieldHeight
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.surfaceHover color: Theme.surfaceHover
border.color: fieldInput.activeFocus ? Theme.primary : Theme.outlineStrong border.color: fieldInput.activeFocus ? Theme.primary : Theme.outlineStrong
@@ -388,12 +418,12 @@ FloatingWindow {
Rectangle { Rectangle {
width: parent.width width: parent.width
height: 50 height: inputFieldHeight
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.surfaceHover color: Theme.surfaceHover
border.color: usernameInput.activeFocus ? Theme.primary : Theme.outlineStrong border.color: usernameInput.activeFocus ? Theme.primary : Theme.outlineStrong
border.width: usernameInput.activeFocus ? 2 : 1 border.width: usernameInput.activeFocus ? 2 : 1
visible: requiresEnterprise && !isVpnPrompt && fieldsInfo.length === 0 visible: showUsernameField
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
@@ -419,12 +449,12 @@ FloatingWindow {
Rectangle { Rectangle {
width: parent.width width: parent.width
height: 50 height: inputFieldHeight
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.surfaceHover color: Theme.surfaceHover
border.color: passwordInput.activeFocus ? Theme.primary : Theme.outlineStrong border.color: passwordInput.activeFocus ? Theme.primary : Theme.outlineStrong
border.width: passwordInput.activeFocus ? 2 : 1 border.width: passwordInput.activeFocus ? 2 : 1
visible: fieldsInfo.length === 0 visible: showPasswordField
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
@@ -456,9 +486,9 @@ FloatingWindow {
} }
Rectangle { Rectangle {
visible: requiresEnterprise && !isVpnPrompt visible: showAnonField
width: parent.width width: parent.width
height: 50 height: inputFieldHeight
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.surfaceHover color: Theme.surfaceHover
border.color: anonInput.activeFocus ? Theme.primary : Theme.outlineStrong border.color: anonInput.activeFocus ? Theme.primary : Theme.outlineStrong
@@ -487,9 +517,9 @@ FloatingWindow {
} }
Rectangle { Rectangle {
visible: requiresEnterprise && !isVpnPrompt visible: showDomainField
width: parent.width width: parent.width
height: 50 height: inputFieldHeight
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.surfaceHover color: Theme.surfaceHover
border.color: domainMatchInput.activeFocus ? Theme.primary : Theme.outlineStrong border.color: domainMatchInput.activeFocus ? Theme.primary : Theme.outlineStrong
@@ -523,7 +553,7 @@ FloatingWindow {
Row { Row {
spacing: Theme.spacingS spacing: Theme.spacingS
visible: fieldsInfo.length === 0 visible: showShowPasswordCheckbox
Rectangle { Rectangle {
id: showPasswordCheckbox id: showPasswordCheckbox
@@ -563,7 +593,7 @@ FloatingWindow {
Row { Row {
spacing: Theme.spacingS spacing: Theme.spacingS
visible: isVpnPrompt || fieldsInfo.length > 0 visible: showSavePasswordCheckbox
Rectangle { Rectangle {
id: savePasswordCheckbox id: savePasswordCheckbox

View File

@@ -186,7 +186,7 @@ exec = sh -c "$QS_CMD; hyprctl dispatch exit"
HYPRLAND_EOF HYPRLAND_EOF
COMPOSITOR_CONFIG="$TEMP_CONFIG" COMPOSITOR_CONFIG="$TEMP_CONFIG"
fi fi
CURRENT_VERSION=$(hyprctl version | grep "Tag:" | awk '{print $2}' | tr -d 'v,') CURRENT_VERSION=$(hyprland --version | grep "Tag:" | awk '{print $2}' | tr -d 'v,')
MINIMUM_VERSION="0.53.0" MINIMUM_VERSION="0.53.0"
if [ "$(printf '%s\n%s' "$MINIMUM_VERSION" "$CURRENT_VERSION" | sort -V | head -n1)" = "$MINIMUM_VERSION" ]; then if [ "$(printf '%s\n%s' "$MINIMUM_VERSION" "$CURRENT_VERSION" | sort -V | head -n1)" = "$MINIMUM_VERSION" ]; then
exec start-hyprland -- --config "$COMPOSITOR_CONFIG" exec start-hyprland -- --config "$COMPOSITOR_CONFIG"

View File

@@ -4,7 +4,7 @@ export XDG_SESSION_TYPE=wayland
export QT_QPA_PLATFORM=wayland export QT_QPA_PLATFORM=wayland
export QT_WAYLAND_DISABLE_WINDOWDECORATION=1 export QT_WAYLAND_DISABLE_WINDOWDECORATION=1
export EGL_PLATFORM=gbm export EGL_PLATFORM=gbm
CURRENT_VERSION=$(hyprctl version | grep "Tag:" | awk '{print $2}' | tr -d 'v,') CURRENT_VERSION=$(hyprland --version | grep "Tag:" | awk '{print $2}' | tr -d 'v,')
MINIMUM_VERSION="0.53.0" MINIMUM_VERSION="0.53.0"

View File

@@ -6677,6 +6677,18 @@
"reference": "Modules/Settings/DockTab.qml:144", "reference": "Modules/Settings/DockTab.qml:144",
"comment": "" "comment": ""
}, },
{
"term": "Smartcard Authentication",
"context": "Smartcard Authentication",
"reference": "Modals/WifiPasswordModal.qml:272",
"comment": ""
},
{
"term": "Smartcard PIN",
"context": "Smartcard PIN",
"reference": "Modals/WifiPasswordModal.qml:194",
"comment": ""
},
{ {
"term": "Some plugins require a newer version of DMS:", "term": "Some plugins require a newer version of DMS:",
"context": "Some plugins require a newer version of DMS:", "context": "Some plugins require a newer version of DMS:",