using System.Buffers.Binary; using System.Security.Cryptography; using System.Text; using System.Text.Json; using WebDrop.Core; namespace WebDrop.Core.Tests; /// /// 用 test-vectors/vectors.json 校验服务端这一侧能复现协议的每个字节。 /// 向量由 node:crypto 独立生成,这里用 .NET 的 HKDF / AesGcm / SHA256 再算一遍; /// 两套互不相干的实现一致,才说明 protocol.md 没有歧义。 /// public class VectorTests { private static readonly JsonElement V = JsonDocument.Parse(File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "vectors.json"))).RootElement; private static byte[] H(JsonElement e) => Convert.FromHexString(e.GetString()!); private static byte[] H(string s) => Convert.FromHexString(s); private static byte[] Hkdf(byte[] ikm, byte[] salt, string info) => HKDF.DeriveKey(HashAlgorithmName.SHA256, ikm, 32, salt, Encoding.UTF8.GetBytes(info)); private static byte[] ChannelId => H(V.GetProperty("inputs").GetProperty("channel_id")); private static byte[] KEnc => Hkdf(H(V.GetProperty("inputs").GetProperty("k")), ChannelId, "webdrop/v1/enc"); private static byte[] ItemKey(byte[] itemId) => Hkdf(KEnc, itemId, "webdrop/v1/item"); private static byte[] Aad(byte[] channelId, byte[] envelope) { // "WD1" ‖ channel_id ‖ envelope[0..22) var aad = new byte[3 + 16 + 22]; "WD1"u8.CopyTo(aad); channelId.CopyTo(aad, 3); envelope.AsSpan(0, 22).CopyTo(aad.AsSpan(19)); return aad; } /// 用 AES-GCM 解密一帧,返回明文;认证失败抛 CryptographicException。 private static byte[] Open(byte[] channelId, byte[] envelope) { Assert.True(Envelope.TryParse(envelope, out var env)); var iv = new byte[12]; BinaryPrimitives.WriteUInt32BigEndian(iv.AsSpan(8), env.FrameIndex); var payload = envelope.AsSpan(Envelope.HeaderSize); var plain = new byte[payload.Length - 16]; using var gcm = new AesGcm(ItemKey(env.ItemId.ToArray()), 16); gcm.Decrypt(iv, payload[..^16], payload[^16..], plain, Aad(channelId, envelope)); return plain; } [Fact] public void KeyDerivationMatchesTheVectors() { var inputs = V.GetProperty("inputs"); var kdf = V.GetProperty("kdf"); var k = H(inputs.GetProperty("k")); var a = H(inputs.GetProperty("a")); Assert.Equal(H(kdf.GetProperty("k_enc")), Hkdf(k, ChannelId, "webdrop/v1/enc")); Assert.Equal(H(kdf.GetProperty("t_read")), Hkdf(k, ChannelId, "webdrop/v1/token/read")); Assert.Equal(H(kdf.GetProperty("t_write")), Hkdf(k, ChannelId, "webdrop/v1/token/write")); Assert.Equal(H(kdf.GetProperty("t_owner")), Hkdf(a, ChannelId, "webdrop/v1/token/owner")); Assert.Equal(H(kdf.GetProperty("k_label")), Hkdf(KEnc, ChannelId, "webdrop/v1/label")); foreach (var item in V.GetProperty("item_keys").EnumerateArray()) Assert.Equal(H(item.GetProperty("k_item")), ItemKey(H(item.GetProperty("item_id")))); } [Fact] public void SignatureFramingAndEcdsaMatchTheVectors() { var vector = V.GetProperty("signatures"); static byte[] B64Url(string value) => Convert.FromBase64String(value.Replace('-', '+').Replace('_', '/') + new string('=', (4 - value.Length % 4) % 4)); var x = B64Url(vector.GetProperty("x").GetString()!); var y = B64Url(vector.GetProperty("y").GetString()!); var payload = H(vector.GetProperty("payload")); var kind = vector.GetProperty("kind").GetString()!; var signature = H(vector.GetProperty("signature")); Assert.Equal(H(vector.GetProperty("message")), SignatureCodec.BuildMessage(kind, payload)); Assert.True(SignatureCodec.VerifyP256(x, y, kind, payload, signature)); Assert.False(SignatureCodec.VerifyP256(x, y, "chat.edit", payload, signature)); payload[0] ^= 1; Assert.False(SignatureCodec.VerifyP256(x, y, kind, payload, signature)); } [Fact] public void ContentEventPayloadAndFrameManifestMatchTheVectors() { var vector = V.GetProperty("content_event"); var digests = vector.GetProperty("frame_digests").EnumerateArray().Select(H).ToList(); var manifest = SignatureCodec.FrameManifestHash(digests); Assert.Equal(H(vector.GetProperty("manifest_hash")), manifest); byte[] id(string property) => H(vector.GetProperty(property)); Assert.Equal(H(vector.GetProperty("payload")), SignatureCodec.ContentEventPayload( id("space_id"), vector.GetProperty("channel_id").GetString()!, vector.GetProperty("username").GetString()!, vector.GetProperty("device_id").GetString()!, id("event_id"), vector.GetProperty("kind").GetString()!, CryptoDomain.Chat, vector.GetProperty("epoch").GetInt32(), id("item_id"), vector.GetProperty("frame_count").GetInt32(), manifest, id("target_item_id"))); } [Fact] public void ServerTokenHashMatchesTheStoredHashes() { // 服务端存的是令牌的 SHA-256;向量里的 *_hash 必须与 TokenHash.Compute 一致, // 客户端才能发出服务端认得的哈希 var kdf = V.GetProperty("kdf"); foreach (var (token, hash) in new[] { ("t_read", "read_hash"), ("t_write", "write_hash"), ("t_owner", "owner_hash"), ("raw_write_token", "raw_write_hash") }) { byte[] t = H(kdf.GetProperty(token)); Assert.Equal(H(kdf.GetProperty(hash)), TokenHash.Compute(t)); Assert.True(TokenHash.Matches(t, H(kdf.GetProperty(hash)))); } } [Fact] public void IvAndAadMatchTheVectors() { foreach (var iv in V.GetProperty("iv").EnumerateArray()) { var expected = new byte[12]; BinaryPrimitives.WriteUInt32BigEndian(expected.AsSpan(8), (uint)iv.GetProperty("index").GetInt64()); Assert.Equal(H(iv.GetProperty("iv")), expected); } foreach (var c in V.GetProperty("aad").EnumerateArray()) { // 用一个只含头部的封装(payload_len = 0)构造 AAD 的前 22 字节 var env = new byte[Envelope.HeaderSize]; env[0] = 1; env[1] = (byte)c.GetProperty("flags").GetInt32(); H(c.GetProperty("item_id")).CopyTo(env, 2); BinaryPrimitives.WriteUInt32BigEndian(env.AsSpan(18), (uint)c.GetProperty("index").GetInt64()); Assert.Equal(H(c.GetProperty("aad")), Aad(ChannelId, env)); } } [Fact] public void PaddingTiersMatchThePolicy() { foreach (var p in V.GetProperty("padding").EnumerateArray()) { int len = p.GetProperty("data_len").GetInt32(); int tier = p.GetProperty("plaintext_len").GetInt32(); Assert.True(PayloadTiers.IsValidEncPayloadLength(tier + 16), $"data_len={len}"); Assert.True(tier >= 4 + len); } } [Fact] public void EveryFrameVectorDecryptsToTheExpectedPlaintextAndPassesServerValidation() { foreach (var f in V.GetProperty("frames").EnumerateArray()) { string name = f.GetProperty("name").GetString()!; byte[] envelope = H(f.GetProperty("envelope")); // 服务端的封装解析与校验:这些帧就是合法客户端会发出的 Assert.True(Envelope.TryParse(envelope, out var env), name); Assert.Equal((FrameFlags)f.GetProperty("flags").GetInt32(), env.Flags); Assert.Equal((uint)f.GetProperty("index").GetInt64(), env.FrameIndex); Assert.True(env.IsEnc, name); Assert.True(PayloadTiers.IsValidEncPayloadLength(env.PayloadLength), name); // 解密得到的明文(含 padding)必须逐字节一致 byte[] plaintext = Open(ChannelId, envelope); Assert.Equal(H(f.GetProperty("plaintext")), plaintext); // 明文布局:u32 data_len ‖ data ‖ 零填充 int dataLen = (int)BinaryPrimitives.ReadUInt32BigEndian(plaintext); Assert.Equal(H(f.GetProperty("data")), plaintext.AsSpan(4, dataLen).ToArray()); Assert.All(plaintext.AsSpan(4 + dataLen).ToArray(), b => Assert.Equal(0, b)); } } [Fact] public void ItemVectorsFollowTheServerFramingRules() { foreach (var item in V.GetProperty("items").EnumerateArray()) { string name = item.GetProperty("name").GetString()!; bool gated = item.GetProperty("gated").GetBoolean(); var frames = item.GetProperty("frames").EnumerateArray().Select(e => H(e)).ToList(); var content = new List(); for (int i = 0; i < frames.Count; i++) { Assert.True(Envelope.TryParse(frames[i], out var env), name); Assert.Equal((uint)i, env.FrameIndex); Assert.Equal(gated, env.IsGated); Assert.Equal(item.GetProperty("snapshot").GetBoolean(), env.IsSnapshot); Assert.Equal(i == frames.Count - 1, env.IsFinal); byte[] plain = Open(ChannelId, frames[i]); int dataLen = (int)BinaryPrimitives.ReadUInt32BigEndian(plain); var data = plain.AsSpan(4, dataLen).ToArray(); if (i == 0) { // 第 0 帧:u16 meta_len ‖ meta_json ‖ 内容 int metaLen = BinaryPrimitives.ReadUInt16BigEndian(data); Assert.Equal(item.GetProperty("meta_json").GetString(), Encoding.UTF8.GetString(data.AsSpan(2, metaLen))); content.AddRange(data.AsSpan(2 + metaLen).ToArray()); } else { content.AddRange(data); } } Assert.Equal(H(item.GetProperty("content")), content.ToArray()); // 非 GATED 条目必须是单帧且 FINAL,这是服务端强制的规则 if (!gated) Assert.Single(frames); } } [Fact] public void RawFrameVectorsParse() { foreach (var f in V.GetProperty("raw_frames").EnumerateArray()) { Assert.True(Envelope.TryParse(H(f.GetProperty("envelope")), out var env)); Assert.False(env.IsEnc); Assert.Equal((FrameFlags)f.GetProperty("flags").GetInt32(), env.Flags); Assert.Equal(H(f.GetProperty("payload")).Length, env.PayloadLength); } } [Fact] public void LabelVectorsDecryptWithTheDerivedKey() { byte[] kLabel = Hkdf(KEnc, ChannelId, "webdrop/v1/label"); foreach (var l in V.GetProperty("labels").EnumerateArray()) { byte[] blob = H(l.GetProperty("blob")); Assert.True(blob.Length <= 256, "设备名总长不得超过 256 字节(服务端上限)"); var plain = new byte[blob.Length - 12 - 16]; using var gcm = new AesGcm(kLabel, 16); gcm.Decrypt(blob.AsSpan(0, 12), blob.AsSpan(12, plain.Length), blob.AsSpan(blob.Length - 16), plain, H(l.GetProperty("aad"))); Assert.Equal(l.GetProperty("name").GetString(), Encoding.UTF8.GetString(plain)); } } [Fact] public void LogRecordVectorsMatchTheServerEncoders() { foreach (var r in V.GetProperty("log_records").EnumerateArray()) { var parsed = r.GetProperty("parsed"); long seq = parsed.GetProperty("seq").GetInt64(); byte[] expected = H(r.GetProperty("hex")); switch (parsed.GetProperty("kind").GetString()) { case "tombstone": var item = Id16.FromBytes(H(parsed.GetProperty("item_id"))); Assert.Equal(expected, LogRecord.EncodeTombstone(seq, item, (TombstoneReason)parsed.GetProperty("reason").GetInt32())); break; case "announce": Assert.Equal(expected, LogRecord.EncodeAnnounce(seq, Id16.FromBytes(H(parsed.GetProperty("item_id"))), (FrameFlags)parsed.GetProperty("flags").GetInt32(), parsed.GetProperty("created_at").GetInt64())); break; case "frame": Assert.Equal(expected, LogRecord.Encode(seq, LogKind.Frame, H(parsed.GetProperty("envelope")))); break; } } } [Fact] public void LinkIdsAreCanonicalForTheServer() { string id = V.GetProperty("inputs").GetProperty("channel_id_b64").GetString()!; Assert.True(Id16.TryParse(id, out var parsed)); Assert.Equal(ChannelId, parsed.ToArray()); Assert.False(Id16.TryParse("AAAAAAAAAAAAAAAAAAAAAB", out _)); // 非规范编码,对应向量 non_canonical_id } [Fact] public void NegativeVectorsAreRejectedByTheServerSideChecksOrByAuthentication() { byte[] original = ChannelId; foreach (var n in V.GetProperty("negative").EnumerateArray()) { string name = n.GetProperty("name").GetString()!; string expect = n.GetProperty("expect").GetString()!; byte[] envelope = H(n.GetProperty("envelope")); byte[] channel = n.TryGetProperty("channel_id", out var c) ? H(c) : original; switch (expect) { case "bad_envelope": Assert.False(Envelope.TryParse(envelope, out _), name); break; case "auth_failed": // 结构合法,但 GCM 认证必须失败(篡改、换头部、换频道都被 AAD 抓住) Assert.True(Envelope.TryParse(envelope, out _), name); Assert.ThrowsAny(() => Open(channel, envelope)); break; case "wrong_mode": // 没有 ENC 标志的帧:封装合法,但在 enc 频道上服务端会以 mode_mismatch 拒绝 Assert.True(Envelope.TryParse(envelope, out var env), name); Assert.False(env.IsEnc, name); break; default: Assert.Fail($"未知的期望类型 {expect}"); break; } } } }