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:
25
include/nocal/sync/oauth_tokens.hpp
Normal file
25
include/nocal/sync/oauth_tokens.hpp
Normal 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
|
||||
50
include/nocal/sync/secret_store.hpp
Normal file
50
include/nocal/sync/secret_store.hpp
Normal 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
|
||||
Reference in New Issue
Block a user