Skip to content

Commit f3f397c

Browse files
Fix command auth escaping for apostrophes (#298)
Co-authored-by: Owen McGirr <o.a.mcgirr@gmail.com>
1 parent 2847bde commit f3f397c

2 files changed

Lines changed: 40 additions & 2 deletions

File tree

src/SwitchifyPc.Core/Pairing/CommandAuth.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System.Globalization;
22
using System.Security.Cryptography;
33
using System.Text;
4+
using System.Text.Encodings.Web;
45
using System.Text.Json;
56
using SwitchifyPc.Protocol;
67

@@ -37,7 +38,7 @@ internal static string StableStringify(JsonElement value)
3738
{
3839
JsonValueKind.Object => StableObjectStringify(value),
3940
JsonValueKind.Array => $"[{string.Join(",", value.EnumerateArray().Select(StableStringify))}]",
40-
JsonValueKind.String => JsonSerializer.Serialize(value.GetString()),
41+
JsonValueKind.String => JsonSerializer.Serialize(value.GetString(), CanonicalJsonOptions),
4142
JsonValueKind.Number => NumberString(value),
4243
JsonValueKind.True => "true",
4344
JsonValueKind.False => "false",
@@ -59,7 +60,7 @@ private static string StableObjectStringify(JsonElement value)
5960
",",
6061
value.EnumerateObject()
6162
.OrderBy(property => property.Name, StringComparer.Ordinal)
62-
.Select(property => $"{JsonSerializer.Serialize(property.Name)}:{StableStringify(property.Value)}")) + "}";
63+
.Select(property => $"{JsonSerializer.Serialize(property.Name, CanonicalJsonOptions)}:{StableStringify(property.Value)}")) + "}";
6364
}
6465

6566
private static string CommandResponseMode(JsonElement command)
@@ -101,6 +102,11 @@ private static string Base64UrlEncode(byte[] value)
101102
.Replace("+", "-", StringComparison.Ordinal)
102103
.Replace("/", "_", StringComparison.Ordinal);
103104
}
105+
106+
private static readonly JsonSerializerOptions CanonicalJsonOptions = new()
107+
{
108+
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
109+
};
104110
}
105111

106112
public sealed record AuthValidationResult

src/SwitchifyPc.Tests/CommandAuthTests.cs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,24 @@ public async Task RejectsOversizedPayloadsThroughProtocolValidation()
122122
Assert.Equal("invalid_payload", result.Reason);
123123
}
124124

125+
[Fact]
126+
public async Task AcceptsApostropheTextPayloadWithAndroidCompatibleAuthProof()
127+
{
128+
CommandAuthValidator validator = new(CreateStore(), () => Now);
129+
130+
AuthValidationResult result = await validator.ValidateAsync(CreateTextCommand("apostrophe-1", "'", "W0OLnbhllDOCd0Gf_00WLpHRvfidYjHeY69nbcmTFYA"));
131+
132+
Assert.True(result.Ok, result.Reason);
133+
}
134+
135+
[Fact]
136+
public void EscapesTextPayloadsLikeAndroidProtocolAuth()
137+
{
138+
Assert.Equal("W0OLnbhllDOCd0Gf_00WLpHRvfidYjHeY69nbcmTFYA", CommandAuth.CreateCommandAuthProof(CreateTextCommand("apostrophe-1", "'"), Token));
139+
Assert.Equal("UfVuw9DJjDofw7UeXBZcIFAu5V2YSkEfJ5p0h8_z2UY", CommandAuth.CreateCommandAuthProof(CreateTextCommand("backslash-1", "\\"), Token));
140+
Assert.Equal("syal1vfAvjac7RNhm8S_2fj-Edk5k1KkGeIMc7eBelw", CommandAuth.CreateCommandAuthProof(CreateTextCommand("html-1", "<>&'"), Token));
141+
}
142+
125143
[Fact]
126144
public void MatchesKnownTypeScriptAuthFixtures()
127145
{
@@ -165,6 +183,20 @@ private static JsonElement CreateMouseMoveCommand(Dictionary<string, object?>? o
165183
}, overrides);
166184
}
167185

186+
private static JsonElement CreateTextCommand(string id, string text, string? authOverride = null)
187+
{
188+
return CreateSignedCommand(new Dictionary<string, object?>
189+
{
190+
["version"] = ProtocolConstants.ProtocolVersion,
191+
["id"] = id,
192+
["deviceId"] = "android-1",
193+
["timestamp"] = Now,
194+
["type"] = "keyboard.typeText",
195+
["payload"] = new Dictionary<string, object?> { ["text"] = text },
196+
["auth"] = authOverride ?? ""
197+
}, authOverride is null ? null : new Dictionary<string, object?> { ["auth"] = authOverride });
198+
}
199+
168200
private static JsonElement CreateSignedCommand(Dictionary<string, object?> command, Dictionary<string, object?>? overrides)
169201
{
170202
if (overrides is not null)

0 commit comments

Comments
 (0)