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;
+ }
}
}