From 82ccfed3fd8ddae328a82d9b47f492ddc66cf7ed Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 27 Sep 2026 09:25:10 +0000 Subject: [PATCH] [patch] Return null, not throw, for stored JSON with no $type discriminator System.Text.Json's polymorphic deserializer throws NotSupportedException for well-formed JSON that lacks a $type property, such as {} or {"Token":"x"}. Deserialize and DeserializeFromString caught only JsonException, so a corrupt or foreign entry in a native store made CredentialCache.TryGet throw instead of returning false. Both now treat that payload as unknown. Fixes ktsu-dev/CredentialCache#164 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_014jCUYfoRMwhybsUaSa1NFm --- CredentialCache.Test/UnknownPayloadTests.cs | 73 +++++++++++++++++++ .../Storage/CredentialSerialization.cs | 10 +++ 2 files changed, 83 insertions(+) create mode 100644 CredentialCache.Test/UnknownPayloadTests.cs diff --git a/CredentialCache.Test/UnknownPayloadTests.cs b/CredentialCache.Test/UnknownPayloadTests.cs new file mode 100644 index 0000000..4616647 --- /dev/null +++ b/CredentialCache.Test/UnknownPayloadTests.cs @@ -0,0 +1,73 @@ +// Copyright (c) 2023-2026 ktsu-dev contributors + +namespace ktsu.CredentialCache.Test; + +using System.Collections.Concurrent; +using System.Text; +using ktsu.CredentialCache.Storage; + +/// +/// Tests that a stored payload which is not a known credential reads back as "not found" +/// rather than throwing, whatever shape the JSON takes. +/// +[TestClass] +public class UnknownPayloadTests +{ + [TestMethod] + [DataRow("{}")] + [DataRow("{\"Token\":\"x\"}")] + [DataRow("{\"$type\":\"Bogus\"}")] + [DataRow("[]")] + [DataRow("123")] + public void DeserializeReturnsNullForJsonThatIsNotAKnownCredential(string json) => + Assert.IsNull(CredentialSerialization.Deserialize(Encoding.UTF8.GetBytes(json))); + + [TestMethod] + [DataRow("{}")] + [DataRow("{\"Token\":\"x\"}")] + [DataRow("{\"$type\":\"Bogus\"}")] + [DataRow("[]")] + [DataRow("123")] + public void DeserializeFromStringReturnsNullForJsonThatIsNotAKnownCredential(string json) => + Assert.IsNull(CredentialSerialization.DeserializeFromString(json)); + + [TestMethod] + [DataRow("{}")] + [DataRow("{\"Token\":\"x\"}")] + public void TryGetReturnsFalseForAStoredEntryWithoutATypeDiscriminator(string json) + { + RawBlobCredentialStore store = new(); + using CredentialCache cache = new(store); + PersonaGUID persona = CredentialCache.CreatePersonaGUID(); + store.Blobs[persona] = Encoding.UTF8.GetBytes(json); + + bool found = cache.TryGet(persona, out Credential? credential); + + Assert.IsFalse(found); + Assert.IsNull(credential); + } +} + +/// +/// A store that holds raw bytes and reads them back the way the native stores do, so an entry +/// written by another tool, or damaged, can be planted directly. +/// +public sealed class RawBlobCredentialStore : ICredentialStore +{ + public ConcurrentDictionary Blobs { get; } = new(); + + public string Name => "RawBlob"; + + public bool TryLoad(PersonaGUID persona, out Credential? credential) + { + credential = Blobs.TryGetValue(persona, out byte[]? blob) + ? CredentialSerialization.DeserializeAndScrub([.. blob]) + : null; + return credential is not null; + } + + public void Save(PersonaGUID persona, Credential credential) => + Blobs[persona] = CredentialSerialization.Serialize(credential); + + public bool Remove(PersonaGUID persona) => Blobs.TryRemove(persona, out _); +} diff --git a/CredentialCache/Storage/CredentialSerialization.cs b/CredentialCache/Storage/CredentialSerialization.cs index 1b2aba4..1f462b3 100644 --- a/CredentialCache/Storage/CredentialSerialization.cs +++ b/CredentialCache/Storage/CredentialSerialization.cs @@ -59,6 +59,11 @@ public static string SerializeToString(Credential credential) => { return null; } + catch (NotSupportedException) + { + // Well-formed JSON with no $type discriminator is not a known credential either + return null; + } } /// @@ -123,5 +128,10 @@ internal static void Zero(byte[] buffer) { return null; } + catch (NotSupportedException) + { + // Well-formed JSON with no $type discriminator is not a known credential either + return null; + } } }