feat: CalDAV credential persistence in Secret Service

Move CalDAV username/password from in-memory config to Secret Service
(gnome-keyring). CalDAVSyncProvider now loads credentials at sync time
and erases them on disconnect. OAuthSecretStore extended with
store/load/erase_caldav_credential methods using a separate libsecret
schema.
This commit is contained in:
2026-07-23 20:39:16 +01:00
parent 5efcfdafb9
commit 23d95e957c
8 changed files with 230 additions and 12 deletions

View File

@@ -40,6 +40,23 @@ const SecretSchema oauth_schema = {
nullptr,
};
const SecretSchema caldav_schema = {
"dev.nomarchy.nocal.caldav",
SECRET_SCHEMA_NONE,
{
{"account", SECRET_SCHEMA_ATTRIBUTE_STRING},
{nullptr, SECRET_SCHEMA_ATTRIBUTE_STRING},
},
0,
nullptr,
nullptr,
nullptr,
nullptr,
nullptr,
nullptr,
nullptr,
};
[[nodiscard]] bool contains_nul_or_control(std::string_view value) {
return std::ranges::any_of(value, [](unsigned char character) {
return character == 0U || character < 0x20U || character == 0x7fU;
@@ -78,6 +95,17 @@ void validate_tokens(const OAuthTokens& tokens) {
}
}
void validate_caldav_credential(const CalDAVCredential& credential) {
if (credential.username.empty() || credential.password.empty()
|| contains_nul_or_control(credential.username)
|| contains_nul_or_control(credential.password)
|| credential.username.size() > maximum_secret_size
|| credential.password.size() > maximum_secret_size) {
throw SecretStoreError(
SecretStoreError::Kind::invalid_input, "invalid CalDAV credential set");
}
}
class DuplicateKey final : public std::exception {};
[[nodiscard]] nlohmann::json parse_unique_json(std::string_view serialized) {
@@ -294,6 +322,53 @@ void reject_pre_cancelled(std::stop_token stop) {
}
}
[[nodiscard]] SensitiveString serialize_caldav_credential(
const CalDAVCredential& credential) {
try {
nlohmann::json document = {
{"version", 1},
{"username", credential.username},
{"password", credential.password},
};
std::string serialized = document.dump();
if (serialized.size() > maximum_secret_size) {
throw SecretStoreError(
SecretStoreError::Kind::invalid_input, "invalid CalDAV credential set");
}
return SensitiveString{std::move(serialized)};
} catch (const SecretStoreError&) {
throw;
} catch (...) {
throw SecretStoreError(
SecretStoreError::Kind::invalid_input, "invalid CalDAV credential set");
}
}
[[nodiscard]] CalDAVCredential parse_caldav_credential(std::string_view serialized) {
if (serialized.empty() || serialized.size() > maximum_secret_size) {
throw SecretStoreError(
SecretStoreError::Kind::corrupt_secret, "stored CalDAV credential is invalid");
}
try {
const nlohmann::json document = parse_unique_json(serialized);
if (!document.is_object() || document.size() != 3 || !document.contains("version")
|| !document["version"].is_number_integer()
|| document["version"].get<int>() != 1
|| !document.contains("username") || !document["username"].is_string()
|| !document.contains("password") || !document["password"].is_string()) {
throw std::runtime_error("invalid document shape");
}
CalDAVCredential credential;
credential.username = document["username"].get<std::string>();
credential.password = document["password"].get<std::string>();
validate_caldav_credential(credential);
return credential;
} catch (...) {
throw SecretStoreError(
SecretStoreError::Kind::corrupt_secret, "stored CalDAV credential is invalid");
}
}
} // namespace
SecretStoreError::SecretStoreError(Kind kind, std::string message)
@@ -347,4 +422,51 @@ void LibsecretOAuthSecretStore::erase(std::string account_id, std::stop_token st
}
}
void LibsecretOAuthSecretStore::store_caldav_credential(
std::string account_id, const CalDAVCredential& credential, std::stop_token stop) {
validate_account(account_id);
validate_caldav_credential(credential);
reject_pre_cancelled(stop);
SensitiveString serialized = serialize_caldav_credential(credential);
Cancellation cancellation{stop};
ErrorHolder error;
const gboolean stored = secret_password_store_sync(&caldav_schema, SECRET_COLLECTION_DEFAULT,
account_id.c_str(), serialized.c_str(), cancellation.get(), error.output(), "account",
account_id.c_str(), nullptr);
if (!stored) {
throw_operation_error(error.get(), stop);
}
}
std::optional<CalDAVCredential> LibsecretOAuthSecretStore::load_caldav_credential(
std::string account_id, std::stop_token stop) {
validate_account(account_id);
reject_pre_cancelled(stop);
Cancellation cancellation{stop};
ErrorHolder error;
ReturnedSecret secret{secret_password_lookup_sync(
&caldav_schema, cancellation.get(), error.output(), "account", account_id.c_str(), nullptr)};
if (secret.get() == nullptr) {
if (error.get() != nullptr) {
throw_operation_error(error.get(), stop);
}
return std::nullopt;
}
const std::size_t size = std::char_traits<char>::length(secret.get());
return parse_caldav_credential(std::string_view{secret.get(), size});
}
void LibsecretOAuthSecretStore::erase_caldav_credential(std::string account_id,
std::stop_token stop) {
validate_account(account_id);
reject_pre_cancelled(stop);
Cancellation cancellation{stop};
ErrorHolder error;
static_cast<void>(secret_password_clear_sync(
&caldav_schema, cancellation.get(), error.output(), "account", account_id.c_str(), nullptr));
if (error.get() != nullptr) {
throw_operation_error(error.get(), stop);
}
}
} // namespace nocal::sync