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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user