using System.Security.Cryptography; using System.Text; using WxAgent.Core; using Xunit; namespace WxAgent.Core.Tests; public sealed class MemoryScanningTests { private static byte[] EncodedLiteral(string literal) { var bytes = Encoding.ASCII.GetBytes(literal); var mask = Convert.FromHexString("d2c7442458020000004889442450488b450048844c2448488944254048584c24"); for (var i = 0; i < bytes.Length; i++) bytes[i] ^= mask[i % mask.Length]; return bytes; } [Fact] public void ConfigCipherDecodeFindsKeyAndEmbeddedSalt() { var key = "000102030405060708090a0b0c0d0e0f000102030405060708090a0b0c0d0e0f"; var salt = "101112131415161718191a1b1c1d1e1f"; var decoded = WcdbConfigCipher.Decode(EncodedLiteral($"x'{key}{salt}'")); Assert.Equal(new WcdbKeyCandidate(key, salt), Assert.Single(decoded)); } [Fact] public void ConfigCipherDecodeWindowsOnLongRuns() { var key = "000102030405060708090a0b0c0d0e0f000102030405060708090a0b0c0d0e0f"; var salt = "101112131415161718191a1b1c1d1e1f"; var run = key + salt + key + salt; var decoded = WcdbConfigCipher.Decode(EncodedLiteral($"x'{run}'")); Assert.Contains(decoded, item => item.EncKey == key && item.SaltHint == salt); Assert.Equal(4, decoded.Count); } [Fact] public void ConfigCipherDecodeRejectsBoundsAndUniformKeys() { Assert.Empty(WcdbConfigCipher.Decode([])); Assert.Empty(WcdbConfigCipher.Decode(EncodedLiteral("x'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'" + "00000000000000000000000000000000"))); Assert.Empty(WcdbConfigCipher.Decode(EncodedLiteral("plain"))); Assert.Empty(WcdbConfigCipher.Decode(EncodedLiteral("x''"))); var tooLong = EncodedLiteral($"x'{new string('e', 200)}'"); Assert.Empty(WcdbConfigCipher.Decode(tooLong)); } [Fact] public void ConfigCipherDecodeRejectsBlobsOverLimit() { var literal = $"x'{new string('f', 64)}{new string('1', 32)}'"; var blob = new List(EncodedLiteral(literal)); while (blob.Count <= WcdbConfigCipher.MaximumBlobLength) blob.Insert(0, 0x00); Assert.Empty(WcdbConfigCipher.Decode(blob.ToArray())); } [Fact] public void PatternScannerMatchesAcrossChunkBoundaries() { var needle = Encoding.ASCII.GetBytes("com.Tencent.WCDB.Config.Cipher"); var content = Encoding.ASCII.GetBytes("prefix " + Encoding.ASCII.GetString(needle) + " suffix"); var split = 10; var scanner = new MemoryPatternScanner(needle); var matches = scanner.Feed(content.AsSpan(0, split), 0x1000).ToList(); matches.AddRange(scanner.Feed(content.AsSpan(split), 0x1000 + (ulong)split)); Assert.Equal([0x1000 + 7UL], matches); } [Fact] public void PatternScannerReportsMatchesInSecondChunk() { var needle = Encoding.ASCII.GetBytes("abc"); var content = Encoding.ASCII.GetBytes("xxabc"); var scanner = new MemoryPatternScanner(needle); var matches = scanner.Feed(content.AsSpan(0, 3), 0x2000).ToList(); matches.AddRange(scanner.Feed(content.AsSpan(3), 0x2003)); Assert.Equal([0x2002UL], matches); } [Fact] public void PatternScannerDoesNotJoinAcrossGaps() { var needle = Encoding.ASCII.GetBytes("abc"); var scanner = new MemoryPatternScanner(needle); var matches = scanner.Feed("a"u8[..1], 0x3000).ToList(); matches.AddRange(scanner.Feed("b"u8[..1], 0x4000)); // Gap: end was 0x3001, next feed at 0x4000. matches.AddRange(scanner.Feed("c"u8[..1], 0x4001)); Assert.Empty(matches); } [Fact] public void PatternScannerFindsMatchSpanningThreeChunks() { var needle = Encoding.ASCII.GetBytes("abcde"); var content = Encoding.ASCII.GetBytes("xxabcde"); var scanner = new MemoryPatternScanner(needle); var matches = scanner.Feed(content.AsSpan(0, 3), 0x5000).ToList(); matches.AddRange(scanner.Feed(content.AsSpan(3, 3), 0x5003)); matches.AddRange(scanner.Feed(content.AsSpan(6), 0x5006)); Assert.Equal([0x5002UL], matches); } [Fact] public void DecodeProducesNoMatchAfterPatternError() { // Simulates the reference's own fail-open behavior after a failed read. var decoded = WcdbConfigCipher.Decode(Encoding.ASCII.GetBytes("x'abc'")); Assert.Empty(decoded); } }