What's hand-rolled
ProjectDirectorOptions (a AppData<ProjectDirectorOptions>, serialized to JSON under %AppData% by ktsu.AppDataStorage) carries a personal access token per configured GitHub owner in plain fields with no [JsonIgnore]:
|
public GitHubLogin GitHubLogin { get; set; } = new(); |
|
public GitHubToken GitHubToken { get; set; } = new(); |
|
public OpenAIToken OpenAIToken { get; set; } = new(); |
|
public Dictionary<string, bool> PanelStates { get; init; } = []; |
|
public Dictionary<string, List<float>> DividerStates { get; init; } = []; |
|
public Dictionary<GitHubOwnerName, GitHubToken> GitHubOwners { get; init; } = []; |
— GitHubLogin, GitHubToken, and Dictionary<GitHubOwnerName, GitHubToken> GitHubOwners
Those tokens are written when a new owner is added and read every time repos are synced:
|
GitHubClient = new(new ProductHeaderValue("ktsu.ProjectDirector")); |
|
|
|
if (!string.IsNullOrEmpty(Options.GitHubLogin) && !string.IsNullOrEmpty(Options.GitHubToken)) |
|
{ |
|
GitHubClient.Credentials = new(Options.GitHubLogin, Options.GitHubToken); |
— initial GitHubClient.Credentials set from Options.GitHubLogin/Options.GitHubToken
|
_ = Options.GitHubOwners.TryAdd(newName, GitHubToken.Create<GitHubToken>(string.Empty)); |
— a new owner's PAT is seeded as GitHubToken.Create<GitHubToken>(string.Empty) into Options.GitHubOwners
|
private void ScanRemoteAccountsForRepos() |
|
{ |
|
Dictionary<GitHubOwnerName, GitHubToken> knownOwners = Options.GitHubOwners; |
|
foreach ((GitHubOwnerName owner, GitHubToken pat) in knownOwners) |
|
{ |
|
if (!string.IsNullOrEmpty(pat) || (!string.IsNullOrEmpty(Options.GitHubLogin) && !string.IsNullOrEmpty(Options.GitHubToken))) |
|
{ |
|
GitHubClient.Credentials = !string.IsNullOrEmpty(pat) ? new(owner, pat) : new(Options.GitHubLogin, Options.GitHubToken); |
|
} |
— ScanRemoteAccountsForRepos reads each owner's PAT back out of Options.GitHubOwners to authenticate GitHubClient
AppData<T> serializes the whole ProjectDirectorOptions object as-is, so every owner's PAT lands in the plaintext settings JSON on disk alongside window-state and UI preferences.
What ktsu.CredentialCache provides
ktsu.CredentialCache.CredentialCache caches credentials in memory and persists each one through the host's native secret store (Windows Credential Manager / macOS Keychain / libsecret), rather than a plain file:
https://github.com/ktsu-dev/CredentialCache/blob/5db1e8aaa1b84f1521711123808563481b4ddb4c/CredentialCache/CredentialCache.cs
Relevant members:
A PersonaGUID per GitHubOwnerName (or a deterministic mapping between the two, stored in ProjectDirectorOptions instead of the token itself) would let Options.GitHubOwners keep tracking which owners are configured while the PAT itself never touches the settings JSON.
Why it's worth it
This is the same plaintext-PAT-in-AppData-JSON pattern already flagged and filed against ktsu-dev/BuildMonitor#278 and ktsu-dev/OAICLI#42 earlier in this same audit rotation — ktsu.CredentialCache exists specifically to route secrets like this through the OS's own credential store instead of a file on disk.
Compatibility
- Subject targets:
ProjectDirector.csproj → net10.0.
ktsu.CredentialCache targets: net9.0;net10.0.
- Dependency direction:
ktsu.CredentialCache's Directory.Packages.props has no reference to ktsu.ProjectDirector, so no cycle.
Sketch
// before (ProjectDirectorOptions, serialized to plaintext JSON)
public GitHubToken GitHubToken { get; set; } = new();
public Dictionary<GitHubOwnerName, GitHubToken> GitHubOwners { get; init; } = [];
// after — Options keeps only which owners are configured, not their secrets
public Dictionary<GitHubOwnerName, PersonaGUID> GitHubOwnerPersonas { get; init; } = [];
// storing a PAT
CredentialCache.Instance.AddOrReplace(persona, new CredentialWithToken { Token = CredentialToken.Create<CredentialToken>(pat) });
// reading it back
if (CredentialCache.Instance.TryGet(persona, out Credential? credential) && credential is CredentialWithToken tokenCredential)
{
GitHubClient.Credentials = new(owner, tokenCredential.Token);
}
Caveats
This changes ProjectDirectorOptions's public shape (GitHubToken/GitHubOwners as token-bearing fields go away) and the settings JSON format, so existing users would need their PAT re-entered once after upgrading (there's no PAT already in the OS credential store to migrate from). Options.GitHubLogin alone is not a secret and can stay as-is.
What's hand-rolled
ProjectDirectorOptions(aAppData<ProjectDirectorOptions>, serialized to JSON under%AppData%byktsu.AppDataStorage) carries a personal access token per configured GitHub owner in plain fields with no[JsonIgnore]:ProjectDirector/ProjectDirector/ProjectDirectorOptions.cs
Lines 24 to 29 in 2fa1411
GitHubLogin,GitHubToken, andDictionary<GitHubOwnerName, GitHubToken> GitHubOwnersThose tokens are written when a new owner is added and read every time repos are synced:
ProjectDirector/ProjectDirector/ProjectDirector.cs
Lines 91 to 95 in 2fa1411
GitHubClient.Credentialsset fromOptions.GitHubLogin/Options.GitHubTokenProjectDirector/ProjectDirector/ProjectDirector.cs
Line 665 in 2fa1411
GitHubToken.Create<GitHubToken>(string.Empty)intoOptions.GitHubOwnersProjectDirector/ProjectDirector/ProjectDirector.cs
Lines 884 to 892 in 2fa1411
ScanRemoteAccountsForReposreads each owner's PAT back out ofOptions.GitHubOwnersto authenticateGitHubClientAppData<T>serializes the wholeProjectDirectorOptionsobject as-is, so every owner's PAT lands in the plaintext settings JSON on disk alongside window-state and UI preferences.What ktsu.CredentialCache provides
ktsu.CredentialCache.CredentialCachecaches credentials in memory and persists each one through the host's native secret store (Windows Credential Manager / macOS Keychain / libsecret), rather than a plain file:https://github.com/ktsu-dev/CredentialCache/blob/5db1e8aaa1b84f1521711123808563481b4ddb4c/CredentialCache/CredentialCache.cs
Relevant members:
CredentialCache.Instance(process-wide singleton,CredentialCache.cs:52)bool TryGet(PersonaGUID persona, out Credential? credential)(CredentialCache.cs:121)void AddOrReplace(PersonaGUID persona, Credential credential)(CredentialCache.cs:149) — persists to the store before updating the in-memory cacheCredentialWithToken.Token(aCredentialToken) for token-shaped credentials:https://github.com/ktsu-dev/CredentialCache/blob/5db1e8aaa1b84f1521711123808563481b4ddb4c/CredentialCache/CredentialWithToken.cs
A
PersonaGUIDperGitHubOwnerName(or a deterministic mapping between the two, stored inProjectDirectorOptionsinstead of the token itself) would letOptions.GitHubOwnerskeep tracking which owners are configured while the PAT itself never touches the settings JSON.Why it's worth it
This is the same plaintext-PAT-in-
AppData-JSON pattern already flagged and filed againstktsu-dev/BuildMonitor#278andktsu-dev/OAICLI#42earlier in this same audit rotation —ktsu.CredentialCacheexists specifically to route secrets like this through the OS's own credential store instead of a file on disk.Compatibility
ProjectDirector.csproj→net10.0.ktsu.CredentialCachetargets:net9.0;net10.0.ktsu.CredentialCache'sDirectory.Packages.propshas no reference toktsu.ProjectDirector, so no cycle.Sketch
Caveats
This changes
ProjectDirectorOptions's public shape (GitHubToken/GitHubOwnersas token-bearing fields go away) and the settings JSON format, so existing users would need their PAT re-entered once after upgrading (there's no PAT already in the OS credential store to migrate from).Options.GitHubLoginalone is not a secret and can stay as-is.