SecretStore.h header

#include <ew/app/SecretStore.h>

Namespace ew::app

SecretStore class

class ew::app::SecretStore

Protects a secret at rest using the operating system's own facility, so a value the user must keep – their AI provider API key – is not sitting in plain text anywhere the settings store can be read.

This exists because two binding documents required it and nothing implemented it: CODING_STANDARDS section 17 ("API keys/tokens go to the OS secure store") and DESIGN section 6.7 ("Keys in OS secure store"). The key was written verbatim into QSettings – on Windows, HKCU\\Software\\LoomWright Games\\Easy Writer, visible in regedit to anyone looking (S-06).

What this does and does not buy, stated plainly, because a security seam that oversells itself is worse than none. Sealing binds the value to the current user account, so it survives being copied off the machine only as ciphertext: an exported registry hive, a synced profile, a backup or a support bundle no longer carries a usable key, and another account on the same machine cannot read it. It does NOT stop code already running as that user – such code can ask the OS to unseal it exactly as this does. The protection is against the secret TRAVELLING, not against a compromised session.

An interface, for the reason Settings is one: tests need a double, and the implementation is the one place in libs/ that touches a platform API (CODING_STANDARDS section 18 allows exactly that, confined to a small implementation file).

Members

ew::app::settings::SecretStore::SecretStore()=default

Defaulted.

virtual ew::app::settings::SecretStore::~SecretStore()=default

Defaulted.

ew::app::settings::SecretStore::SecretStore(const SecretStore &)=delete

Interface type: non-copyable and non-movable (held via pointer/reference).

ew::app::settings::SecretStore::SecretStore(SecretStore &&)=delete

Not movable.

SecretStore & ew::app::settings::SecretStore::operator=(const SecretStore &)=delete

Not copyable.

SecretStore & ew::app::settings::SecretStore::operator=(SecretStore &&)=delete

Not movable.

virtual bool ew::app::settings::SecretStore::isAvailable() const =0

Whether this store can actually protect a secret on the machine it is running on.

Asked rather than inferred from a failed seal, because the two call for opposite responses: a platform with no secret facility must still let the user configure a provider, while a platform that has one and refused is a fault worth surfacing.

virtual std::optional< QString > ew::app::settings::SecretStore::seal(const QString &secret) const =0

secret sealed to the current user as text safe to store anywhere a QString can go, or nullopt when it could not be protected.

Sealing an EMPTY secret yields nullopt: there is nothing to protect, and a caller that stored the result would be recording a sealed blob for "no key", which reads on the next load as a key that failed to unseal.

virtual std::optional< QString > ew::app::settings::SecretStore::unseal(const QString &sealed) const =0

The secret behind sealed, or nullopt when it cannot be unsealed – because it was sealed by another user, on another machine, or is not a sealed value at all.

Functions

const SecretStore & ew::app::settings::platformSecretStore()

The platform's own secret facility: Windows DPAPI (CryptProtectData, current-user scope) with a fixed application entropy, so another program running as the same user cannot unseal the blob simply by handing it back to the OS. That entropy is compiled in and is therefore not itself a secret; it raises the bar from "any process can unseal this" to "any process that knows this program", which is what application entropy is for.

On a platform with no implementation here, isAvailable answers false and both operations return nullopt, so the caller keeps its previous behaviour rather than losing the user's key. The desktop ships on Windows today; adding a platform means adding a branch to one file.