Add plain text password storage option for CardDAV (Nextcloud support)

- Add storePlainTextPassword setting in SettingsCore and SettingsModel
- Add UI switch in CarddavSettingsLayout.qml to toggle plain text password
- Allow multiple CardDAV accounts by setting showAddButton = true
- Add CardDAV troubleshooting documentation in README.md

This fixes CardDAV synchronization with Nextcloud/WebDAV servers that use
HTTP Basic Authentication, which requires plain text passwords instead
of the default HA1 hash storage.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Eduard Wisch 2026-02-08 06:23:20 +01:00
parent db7ce5ef4a
commit edfd148ea6
6 changed files with 96 additions and 1 deletions

View file

@ -105,6 +105,9 @@ SettingsCore::SettingsCore(QObject *parent) : QObject(parent) {
// DND // DND
mDndEnabled = settingsModel->dndEnabled(); mDndEnabled = settingsModel->dndEnabled();
// CardDAV
mStorePlainTextPassword = settingsModel->getStorePlainTextPassword();
mDefaultDomain = settingsModel->getDefaultDomain(); mDefaultDomain = settingsModel->getDefaultDomain();
auto currentAccount = CoreModel::getInstance()->getCore()->getDefaultAccount(); auto currentAccount = CoreModel::getInstance()->getCore()->getDefaultAccount();
if (currentAccount) { if (currentAccount) {
@ -552,6 +555,14 @@ void SettingsCore::setSelf(QSharedPointer<SettingsCore> me) {
mSettingsModelConnection->makeConnectToModel(&SettingsModel::cardDAVMinCharResearchChanged, [this](int min) { mSettingsModelConnection->makeConnectToModel(&SettingsModel::cardDAVMinCharResearchChanged, [this](int min) {
mSettingsModelConnection->invokeToCore([this, min]() { setCardDAVMinCharForResearch(min); }); mSettingsModelConnection->invokeToCore([this, min]() { setCardDAVMinCharForResearch(min); });
}); });
mSettingsModelConnection->makeConnectToModel(&SettingsModel::storePlainTextPasswordChanged, [this](bool enabled) {
mSettingsModelConnection->invokeToCore([this, enabled]() {
if (mStorePlainTextPassword != enabled) {
mStorePlainTextPassword = enabled;
emit storePlainTextPasswordChanged(enabled);
}
});
});
auto settingsModel = SettingsModel::getInstance(); auto settingsModel = SettingsModel::getInstance();
@ -1234,6 +1245,20 @@ void SettingsCore::setCardDAVMinCharForResearch(int min) {
} }
} }
bool SettingsCore::getStorePlainTextPassword() const {
return mStorePlainTextPassword;
}
void SettingsCore::setStorePlainTextPassword(bool enabled) {
if (mStorePlainTextPassword != enabled) {
mStorePlainTextPassword = enabled;
mSettingsModelConnection->invokeToModel([enabled]() {
SettingsModel::getInstance()->setStorePlainTextPassword(enabled);
});
emit storePlainTextPasswordChanged(enabled);
}
}
QString SettingsCore::getConfigLocale() const { QString SettingsCore::getConfigLocale() const {
return mConfigLocale; return mConfigLocale;
} }

View file

@ -105,6 +105,10 @@ public:
Q_PROPERTY(bool showAccountDevices READ showAccountDevices WRITE setShowAccountDevices Q_PROPERTY(bool showAccountDevices READ showAccountDevices WRITE setShowAccountDevices
NOTIFY showAccountDevicesChanged) NOTIFY showAccountDevicesChanged)
// CardDAV plain text password storage (needed for HTTP Basic Auth like Nextcloud)
Q_PROPERTY(bool storePlainTextPassword READ getStorePlainTextPassword WRITE setStorePlainTextPassword NOTIFY
storePlainTextPasswordChanged)
static QSharedPointer<SettingsCore> create(); static QSharedPointer<SettingsCore> create();
SettingsCore(QObject *parent = Q_NULLPTR); SettingsCore(QObject *parent = Q_NULLPTR);
SettingsCore(const SettingsCore &settingsCore); SettingsCore(const SettingsCore &settingsCore);
@ -268,6 +272,9 @@ public:
bool getCardDAVMinCharForResearch() const; bool getCardDAVMinCharForResearch() const;
void setCardDAVMinCharForResearch(int min); void setCardDAVMinCharForResearch(int min);
bool getStorePlainTextPassword() const;
void setStorePlainTextPassword(bool enabled);
bool isCheckForUpdateAvailable() const; bool isCheckForUpdateAvailable() const;
Q_INVOKABLE void save(); Q_INVOKABLE void save();
Q_INVOKABLE void undo(); Q_INVOKABLE void undo();
@ -345,6 +352,7 @@ signals:
void cardDAVMinCharForResearchChanged(int min); void cardDAVMinCharForResearchChanged(int min);
void cardDAVAddressBookSynchronized(); void cardDAVAddressBookSynchronized();
void storePlainTextPasswordChanged(bool enabled);
void lSetCaptureDevice(QVariantMap device); void lSetCaptureDevice(QVariantMap device);
void captureDeviceChanged(const QVariantMap &device); void captureDeviceChanged(const QVariantMap &device);
@ -471,6 +479,7 @@ private:
// CardDAV // CardDAV
int mCardDAVMinCharForResearch = 0; int mCardDAVMinCharForResearch = 0;
bool mStorePlainTextPassword = false;
// Check update // Check update
bool mIsCheckForUpdateAvailable = false; bool mIsCheckForUpdateAvailable = false;

View file

@ -801,6 +801,20 @@ void SettingsModel::setCardDAVMinCharResearch(int min) {
emit cardDAVMinCharResearchChanged(min); emit cardDAVMinCharResearchChanged(min);
} }
// Store plain text password (instead of ha1 hash) - needed for HTTP Basic Auth (e.g. Nextcloud CardDAV)
bool SettingsModel::getStorePlainTextPassword() const {
mustBeInLinphoneThread(log().arg(Q_FUNC_INFO));
// store_ha1_passwd=1 means store ha1 (delete password), store_ha1_passwd=0 means keep password
return mConfig->getInt("sip", "store_ha1_passwd", 1) == 0;
}
void SettingsModel::setStorePlainTextPassword(bool enabled) {
mustBeInLinphoneThread(log().arg(Q_FUNC_INFO));
// store_ha1_passwd=0 means keep password, store_ha1_passwd=1 means delete password and store ha1
mConfig->setInt("sip", "store_ha1_passwd", enabled ? 0 : 1);
emit storePlainTextPasswordChanged(enabled);
}
// ============================================================================= // =============================================================================
// Device name. // Device name.
// ============================================================================= // =============================================================================

View file

@ -119,6 +119,9 @@ public:
int getCardDAVMinCharResearch() const; int getCardDAVMinCharResearch() const;
void setCardDAVMinCharResearch(int min); void setCardDAVMinCharResearch(int min);
bool getStorePlainTextPassword() const;
void setStorePlainTextPassword(bool enabled);
QVariantMap getRingerDevice() const; QVariantMap getRingerDevice() const;
void setRingerDevice(QVariantMap device); void setRingerDevice(QVariantMap device);
@ -267,6 +270,7 @@ signals:
void createEndToEndEncryptedMeetingsAndGroupCallsChanged(bool endtoend); void createEndToEndEncryptedMeetingsAndGroupCallsChanged(bool endtoend);
void cardDAVMinCharResearchChanged(int min); void cardDAVMinCharResearchChanged(int min);
void storePlainTextPasswordChanged(bool enabled);
void echoCancellationEnabledChanged(bool enabled); void echoCancellationEnabledChanged(bool enabled);
void automaticallyRecordCallsEnabledChanged(bool enabled); void automaticallyRecordCallsEnabledChanged(bool enabled);

View file

@ -130,6 +130,14 @@ AbstractSettingsLayout {
propertyName: "storeNewFriendsInIt" propertyName: "storeNewFriendsInIt"
propertyOwnerGui: carddavGui propertyOwnerGui: carddavGui
} }
SwitchSetting {
//: "Passwort im Klartext speichern (für HTTP Basic Auth)"
titleText: qsTr("settings_contacts_carddav_store_plain_password_title")
//: "Für Nextcloud/WebDAV-Server erforderlich. Passwort wird nicht gehasht."
subTitleText: qsTr("settings_contacts_carddav_store_plain_password_subtitle")
checked: SettingsCpp.storePlainTextPassword
onCheckedChanged: SettingsCpp.storePlainTextPassword = checked
}
} }
} }
} }

View file

@ -172,6 +172,41 @@ fc-match "Noto Color Emoji"
# should now show: NotoColorEmoji.ttf: "Noto Color Emoji" "Regular" # should now show: NotoColorEmoji.ttf: "Noto Color Emoji" "Regular"
``` ```
### CardDAV Synchronization Issues (Nextcloud, WebDAV servers)
If you encounter "Synchronization error" (Synchronisierungsfehler) when using CardDAV with Nextcloud or other WebDAV servers, the problem may be related to password storage or authentication method.
#### The Problem
Linphone by default stores passwords as a hashed value (HA1) instead of plain text. This works for SIP authentication (Digest Auth) but **does not work for HTTP Basic Authentication** which Nextcloud/WebDAV servers typically require.
When `store_ha1_passwd=1` (default), the password is hashed immediately after first use and the plain text password is deleted. On subsequent requests, the HTTP client cannot authenticate because it only has the hash, not the password needed for HTTP Basic Auth.
#### The Solution
Enable plain text password storage in the CardDAV settings:
1. Open Linphone Settings → Contacts → CardDAV
2. Enable **"Store plain text password (for HTTP Basic Auth)"**
- German: "Passwort im Klartext speichern (für HTTP Basic Auth)"
Alternatively, you can manually edit the `linphonerc` config file:
```ini
[sip]
store_ha1_passwd=0
```
**Security Note:** When this option is enabled, passwords are stored in plain text in the config file instead of being hashed. This is necessary for CardDAV/WebDAV services that use HTTP Basic Authentication.
#### Additional Troubleshooting
* If you see "SSL handshake failed: No CA Chain is set", ensure the `root_ca` path in your linphonerc points to a valid certificate file with an absolute path.
* If credentials are not persisting after restart, make sure you have enabled plain text password storage as described above.
* Multiple CardDAV accounts are now supported. You can add multiple CardDAV address books in the settings.
## Specific instructions for the Mac Os X platform ## Specific instructions for the Mac Os X platform