88namespace 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