Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions CredentialCache.Test/UnknownPayloadTests.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Tests that a stored payload which is not a known credential reads back as "not found"
/// rather than throwing, whatever shape the JSON takes.
/// </summary>
[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);
}
}

/// <summary>
/// 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.
/// </summary>
public sealed class RawBlobCredentialStore : ICredentialStore
{
public ConcurrentDictionary<PersonaGUID, byte[]> 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 _);
}
10 changes: 10 additions & 0 deletions CredentialCache/Storage/CredentialSerialization.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}

/// <summary>
Expand Down Expand Up @@ -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;
}
}
}
Loading