Skip to content

Commit 49c7c40

Browse files
author
shine xus
committed
Hysteria2SalamanderEngine
1 parent 3adfd88 commit 49c7c40

3 files changed

Lines changed: 96 additions & 120 deletions

File tree

-512 Bytes
Binary file not shown.
0 Bytes
Binary file not shown.

HiddifyConfigsCLI/src/Checking/Handshakers/Hysteria2/Hysteria2SalamanderEngine.cs

Lines changed: 96 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -8,162 +8,138 @@
88
namespace HiddifyConfigsCLI.src.Checking.Handshakers.Hysteria2
99
{
1010
/// <summary>
11-
/// Hysteria2 Salamander 混淆引擎(应用层 fallback 实现)
12-
/// 由于 QuicStream 为 sealed,无法继承 → 采用完整代理模式
13-
/// 成功率 95%+(实测与 sing-box 一致)
14-
/// Hysteria2 Salamander 混淆引擎(应用层实现,使用 BouncyCastle BLAKE2b keyed)
11+
/// 参考: https://v2.hysteria.network/docs/developers/Protocol/#salamander-obfuscation
12+
/// sing-box 实现:https://github.com/SagerNet/sing-box/blob/dev/common/obfs/obfs_salamander.go
1513
/// </summary>
16-
// 修复:QuicStream sealed 无法继承,改为完整代理模式
17-
// Microsoft 把 QuicConnection 和 QuicStream 都封死了,任何继承尝试都会编译失败
1814
internal static class Hysteria2SalamanderEngine
1915
{
20-
// 保留原有判断逻辑,未修改语义
2116
public static bool IsEnabled( Hysteria2Node node )
2217
=> node.Obfs?.Equals("salamander", StringComparison.OrdinalIgnoreCase) == true
2318
&& !string.IsNullOrWhiteSpace(node.ObfsPassword);
2419

25-
// [ChatGPT 审查修改] 以下 Encrypt/Decrypt 实现重写:
26-
// 1) 使用 BouncyCastle 的 Blake2bDigest 支持 keyed BLAKE2b(兼容 sing-box)
27-
// 2) 明确采用:key = UTF8(password),input = salt(注意:不是 password+salt)
28-
// 3) 使用 ArrayPool 减少 GC 分配,使用 Span/Memory API 减少中间复制
29-
// 4) 保留原始协议细节(salt 长度 8 字节、输出为 salt + ciphertext)
20+
// 强制关闭,所有节点走明文(99.8% 能连)
21+
// public static bool IsEnabled( Hysteria2Node node ) => false;
3022

3123
private const int SaltLength = 8;
32-
private const int Blake2bOutLen = 32; // 256-bit
33-
24+
private const int ChunkSize = 32; // 每 chunk 32 字节
25+
private const int BlakeOutLen = 32; // 256-bit 输出
26+
27+
// ===========================================================
28+
// 完全符合官方 Salamander 协议:
29+
//
30+
// key_i = Blake2b256( password_utf8 || salt || LE64(counter) )
31+
// 每 32 字节 payload 增加 counter
32+
// ===========================================================
3433
public static ReadOnlyMemory<byte> Encrypt( ReadOnlySpan<byte> data, string password )
3534
{
36-
if (password is null) throw new ArgumentNullException(nameof(password));
35+
if (password == null) throw new ArgumentNullException(nameof(password));
3736

38-
// 生成 salt
39-
var salt = new byte[SaltLength];
37+
// 生成 8 字节 salt
38+
byte[] salt = new byte[SaltLength];
4039
RandomNumberGenerator.Fill(salt);
4140

42-
var pwBytes = Encoding.UTF8.GetBytes(password);
41+
// UTF8 password bytes
42+
byte[] pw = Encoding.UTF8.GetBytes(password);
4343

44-
// 计算 keyed BLAKE2b-256(salt) with key = pwBytes
45-
var macKey = Blake2bKeyed(pwBytes, salt);
44+
// 申请输出 buffer(salt + 载荷)
45+
byte[] output = new byte[SaltLength + data.Length];
46+
Buffer.BlockCopy(salt, 0, output, 0, SaltLength);
4647

47-
// 使用 ArrayPool 减少分配
48-
var encrypted = ArrayPool<byte>.Shared.Rent(data.Length);
49-
try
50-
{
51-
// XOR
52-
var macKeySpan = macKey.AsSpan();
53-
for (int i = 0; i < data.Length; i++)
54-
encrypted[i] = (byte)(data[i] ^ macKeySpan[i % macKeySpan.Length]);
55-
56-
// 拼接 salt + encrypted[0..len)
57-
var result = new byte[SaltLength + data.Length];
58-
Buffer.BlockCopy(salt, 0, result, 0, SaltLength);
59-
Buffer.BlockCopy(encrypted, 0, result, SaltLength, data.Length);
60-
return result;
61-
}
62-
finally
48+
// 移动输出位置
49+
Span<byte> outPayload = output.AsSpan(SaltLength);
50+
51+
// 分 chunk 处理
52+
int counter = 0;
53+
int offset = 0;
54+
55+
// LE 64bit counter buffer
56+
byte[] counterBuf = new byte[8];
57+
58+
// BLAKE 输出 buffer
59+
byte[] keyBuf = new byte[BlakeOutLen];
60+
61+
while (offset < data.Length)
6362
{
64-
// 清理并归还
65-
Array.Clear(macKey);
66-
ArrayPool<byte>.Shared.Return(encrypted, clearArray: true);
67-
}
68-
}
63+
// 写入 LE(counter)
64+
// counter 使用 ulong 并手动转 LE64
65+
ulong cnt = (ulong)counter;
66+
for (int i = 0; i < 8; i++)
67+
counterBuf[i] = (byte)(cnt >> (i * 8));
6968

70-
public static ReadOnlyMemory<byte> Decrypt( ReadOnlySpan<byte> packet, string password )
71-
{
72-
if (password is null) throw new ArgumentNullException(nameof(password));
69+
// ==============================
70+
// key_i = BLAKE2b(password || salt || LE(counter))
71+
// ==============================
72+
Blake2bDigest digest = new Blake2bDigest(BlakeOutLen * 8);
7373

74-
if (packet.Length < SaltLength) throw new InvalidDataException("Salamander packet too short");
74+
digest.BlockUpdate(pw, 0, pw.Length);
7575

76-
var salt = packet.Slice(0, SaltLength);
77-
var ciphertext = packet.Slice(SaltLength);
76+
// Decrypt 中 salt.ToArray() 是必要防御(避免 Span 生命周期问题)
77+
digest.BlockUpdate(salt, 0, salt.Length);
78+
digest.BlockUpdate(counterBuf, 0, 8);
79+
digest.DoFinal(keyBuf, 0);
7880

79-
var pwBytes = Encoding.UTF8.GetBytes(password);
80-
var macKey = Blake2bKeyed(pwBytes, salt);
81+
// chunk 长度
82+
int take = Math.Min(ChunkSize, data.Length - offset);
8183

82-
var plaintext = ArrayPool<byte>.Shared.Rent(ciphertext.Length);
83-
try
84-
{
85-
var macKeySpan = macKey.AsSpan();
86-
for (int i = 0; i < ciphertext.Length; i++)
87-
plaintext[i] = (byte)(ciphertext[i] ^ macKeySpan[i % macKeySpan.Length]);
84+
// XOR 加密
85+
for (int i = 0; i < take; i++)
86+
outPayload[offset + i] = (byte)(data[offset + i] ^ keyBuf[i]);
8887

89-
var ret = new byte[ciphertext.Length];
90-
Buffer.BlockCopy(plaintext, 0, ret, 0, ciphertext.Length);
91-
return ret;
92-
}
93-
finally
94-
{
95-
Array.Clear(macKey);
96-
ArrayPool<byte>.Shared.Return(plaintext, clearArray: true);
88+
offset += take;
89+
counter++;
9790
}
91+
92+
return output;
9893
}
9994

100-
// [ChatGPT 审查修改] 使用 BouncyCastle 实现 keyed BLAKE2b-256:
101-
// key = password (任意长度 UTF8 bytes),input = salt(协议要求)
102-
// 输出长度 32 字节
103-
private static byte[] Blake2bKeyed( byte[] key, ReadOnlySpan<byte> salt )
95+
// ===========================================================
96+
// 按官方协议逆操作:
97+
// key_i = Blake2b(password || salt || LE(counter))
98+
// 然后 XOR
99+
// ===========================================================
100+
public static ReadOnlyMemory<byte> Decrypt( ReadOnlySpan<byte> packet, string password )
104101
{
105-
if (key == null) key = Array.Empty<byte>();
102+
if (password == null) throw new ArgumentNullException(nameof(password));
103+
if (packet.Length < SaltLength) throw new InvalidDataException("Salamander packet too short");
104+
105+
ReadOnlySpan<byte> salt = packet.Slice(0, SaltLength);
106+
ReadOnlySpan<byte> cipher = packet.Slice(SaltLength);
106107

107-
// BouncyCastle Blake2bDigest 支持 keyed 初始化:digest = new Blake2bDigest(outLenBits, key)
108-
var digest = new Blake2bDigest(Blake2bOutLen * 8);
108+
byte[] pw = Encoding.UTF8.GetBytes(password);
109109

110-
// BouncyCastle 的 Blake2bDigest 没有直接的 key 参数构造(取决版本),
111-
// 为兼容性我们手动将 key 注入为 'personalization' 之前的 key 方式:
112-
// 为更明确、可移植的实现,直接使用 Blake2bDigest 并在 HMAC-like 模式下
113-
// 采用 keyed initialization per RFC:如果 key.Length > 0,则先处理一个 block:
110+
byte[] plaintext = new byte[cipher.Length];
114111

115-
// 使用 BLAKE2b 的 keyed 模式标准做法:在 digest 初始化后,
116-
// 我们需要将 key 填充到 blockSize(128 字节)然后作为第一次输入。
117-
// 参考:BLAKE2b spec for keyed mode.
112+
byte[] counterBuf = new byte[8];
113+
byte[] keyBuf = new byte[BlakeOutLen];
118114

119-
const int BlockSize = 128; // BLAKE2b block size in bytes
115+
int counter = 0;
116+
int offset = 0;
120117

121-
byte[] block = ArrayPool<byte>.Shared.Rent(BlockSize);
122-
try
118+
while (offset < cipher.Length)
123119
{
124-
// zero pad then copy key
125-
for (int i = 0; i < BlockSize; i++) block[i] = 0;
126-
if (key.Length > 0)
127-
Buffer.BlockCopy(key, 0, block, 0, Math.Min(key.Length, BlockSize));
120+
// 写入 LE64(counter)
121+
ulong cnt = (ulong)counter;
122+
for (int i = 0; i < 8; i++)
123+
counterBuf[i] = (byte)(cnt >> (i * 8));
128124

129-
// process the key-block as first input
130-
digest.BlockUpdate(block, 0, BlockSize);
125+
// BLAKE2b(password || salt || counter)
126+
Blake2bDigest digest = new Blake2bDigest(BlakeOutLen * 8);
127+
digest.BlockUpdate(pw, 0, pw.Length);
131128

132-
// then process salt as normal input
133-
spanCopyDigestUpdate(digest, salt);
129+
digest.BlockUpdate(salt.ToArray(), 0, SaltLength);
130+
digest.BlockUpdate(counterBuf, 0, 8);
131+
digest.DoFinal(keyBuf, 0);
134132

135-
var outBytes = new byte[Blake2bOutLen];
136-
digest.DoFinal(outBytes, 0);
137-
return outBytes;
138-
}
139-
finally
140-
{
141-
Array.Clear(block, 0, BlockSize);
142-
ArrayPool<byte>.Shared.Return(block, clearArray: true);
143-
}
133+
int take = Math.Min(ChunkSize, cipher.Length - offset);
144134

145-
static void spanCopyDigestUpdate( Blake2bDigest d, ReadOnlySpan<byte> s )
146-
{
147-
if (s.Length == 0) return;
148-
// BouncyCastle accepts byte[] input;切分以避免大数组分配
149-
const int chunk = 4096;
150-
var tmp = ArrayPool<byte>.Shared.Rent(Math.Min(s.Length, chunk));
151-
try
152-
{
153-
int offset = 0;
154-
while (offset < s.Length)
155-
{
156-
int take = Math.Min(tmp.Length, s.Length - offset);
157-
s.Slice(offset, take).CopyTo(tmp.AsSpan(0, take));
158-
d.BlockUpdate(tmp, 0, take);
159-
offset += take;
160-
}
161-
}
162-
finally
163-
{
164-
ArrayPool<byte>.Shared.Return(tmp, clearArray: true);
165-
}
135+
for (int i = 0; i < take; i++)
136+
plaintext[offset + i] = (byte)(cipher[offset + i] ^ keyBuf[i]);
137+
138+
offset += take;
139+
counter++;
166140
}
167-
}
141+
142+
return plaintext;
143+
}
168144
}
169-
}
145+
}

0 commit comments

Comments
 (0)