feat: store OAuth tokens securely

Add bounded token response parsing with refresh-token retention and a cancellable worker-thread libsecret adapter using versioned credential payloads.

Verify CRUD, isolation, cancellation, corruption, unavailable service, redaction, and cleanup against an isolated D-Bus and gnome-keyring Secret Service in normal, warning-as-error, and sanitizer profiles.
This commit is contained in:
2026-07-18 11:56:04 +01:00
parent 295acbc125
commit 3560a7d23d
13 changed files with 1405 additions and 27 deletions

View File

@@ -0,0 +1,25 @@
#pragma once
#include <cstdint>
#include <string>
#include <string_view>
namespace nocal::sync {
struct OAuthTokens {
std::string access_token;
std::string refresh_token;
std::string token_type;
std::string scope;
std::int64_t expires_at_epoch_seconds{0};
bool operator==(const OAuthTokens&) const = default;
};
// Parses a successful OAuth token response. A refresh response may omit its
// refresh token only when retained_refresh_token is supplied. Token material is
// never included in an exception message.
[[nodiscard]] OAuthTokens parse_oauth_token_response(std::string_view response_body,
std::int64_t now_epoch_seconds, std::string_view retained_refresh_token = {});
} // namespace nocal::sync

View File

@@ -0,0 +1,50 @@
#pragma once
#include "nocal/sync/oauth_tokens.hpp"
#include <optional>
#include <stdexcept>
#include <stop_token>
#include <string>
namespace nocal::sync {
class SecretStoreError : public std::runtime_error {
public:
enum class Kind {
invalid_input,
unavailable,
cancelled,
corrupt_secret,
operation_failed,
};
SecretStoreError(Kind kind, std::string message);
[[nodiscard]] Kind kind() const noexcept;
private:
Kind kind_;
};
class OAuthSecretStore {
public:
virtual ~OAuthSecretStore() = default;
virtual void store(std::string account_id, const OAuthTokens& tokens,
std::stop_token stop = {}) = 0;
[[nodiscard]] virtual std::optional<OAuthTokens> load(
std::string account_id, std::stop_token stop = {}) = 0;
virtual void erase(std::string account_id, std::stop_token stop = {}) = 0;
};
// Synchronous libsecret adapter. Call only from a worker thread: Secret Service
// activation, unlocking, and user prompts can block independently of the TUI.
class LibsecretOAuthSecretStore final : public OAuthSecretStore {
public:
void store(std::string account_id, const OAuthTokens& tokens,
std::stop_token stop = {}) override;
[[nodiscard]] std::optional<OAuthTokens> load(
std::string account_id, std::stop_token stop = {}) override;
void erase(std::string account_id, std::stop_token stop = {}) override;
};
} // namespace nocal::sync