1+ using System . Buffers . Binary ;
2+ using System . Diagnostics . CodeAnalysis ;
3+ using System . Security . Cryptography ;
4+
5+ namespace Pandatech . Crypto . Helpers ;
6+
7+ public static class Aes256Gcm
8+ {
9+ private const int KeySize = 32 ; // 256-bit
10+ private const int NonceSize = 12 ; // GCM recommended
11+ private const int TagSize = 16 ; // 128-bit tag
12+ private const int DefaultChunkSize = 64 * 1024 ;
13+
14+ // [Magic: 'PGCM'][Version:1][BaseNonce:12][ChunkSize:4 LE]
15+ // Frames: [PlainLen:4 LE][Tag:16][Ciphertext:PlainLen]
16+ private static readonly byte [ ] Magic = "PGCM"u8 . ToArray ( ) ;
17+
18+ private const byte Version = 1 ;
19+
20+ private static string ? GlobalKey { get ; set ; }
21+
22+ internal static void RegisterKey ( string key )
23+ {
24+ ValidateKey ( key ) ;
25+ GlobalKey = key ;
26+ }
27+
28+ public static void Encrypt ( Stream input , Stream output ) => Encrypt ( input , output , null ) ;
29+
30+ public static void Encrypt ( Stream input , Stream output , string ? key )
31+ {
32+ ArgumentNullException . ThrowIfNull ( input ) ;
33+ ArgumentNullException . ThrowIfNull ( output ) ;
34+
35+ var k = GetKeyBytes ( key ) ;
36+
37+ Span < byte > baseNonce = stackalloc byte [ NonceSize ] ;
38+ RandomNumberGenerator . Fill ( baseNonce ) ;
39+
40+ // header
41+ Span < byte > header = stackalloc byte [ Magic . Length + 1 + NonceSize + 4 ] ;
42+ Magic . CopyTo ( header ) ;
43+ header [ 4 ] = Version ;
44+ baseNonce . CopyTo ( header . Slice ( 5 , NonceSize ) ) ;
45+ BinaryPrimitives . WriteUInt32LittleEndian ( header . Slice ( 5 + NonceSize , 4 ) , ( uint ) DefaultChunkSize ) ;
46+ output . Write ( header ) ;
47+
48+ using var aes = new AesGcm ( k , TagSize ) ;
49+
50+ var plain = new byte [ DefaultChunkSize ] ;
51+ var cipher = new byte [ DefaultChunkSize ] ; // <— you removed this; we need it
52+ var tagBuf = new byte [ TagSize ] ;
53+ var nonceBuf = new byte [ NonceSize ] ;
54+ var frameLen4 = new byte [ 4 ] ;
55+ var aadLen = 5 + NonceSize + 4 ;
56+
57+ ulong counter = 0 ;
58+ int read ;
59+
60+ // --- data frames ---
61+ while ( ( read = input . Read ( plain , 0 , plain . Length ) ) > 0 )
62+ {
63+ DeriveNonce ( baseNonce , counter , nonceBuf ) ;
64+
65+ var p = plain . AsSpan ( 0 , read ) ;
66+ var c = cipher . AsSpan ( 0 , read ) ;
67+ var tag = tagBuf . AsSpan ( ) ;
68+
69+ aes . Encrypt ( nonceBuf , p , c , tag , header [ ..aadLen ] ) ;
70+
71+ BinaryPrimitives . WriteUInt32LittleEndian ( frameLen4 , ( uint ) read ) ;
72+ output . Write ( frameLen4 ) ; // len
73+ output . Write ( tagBuf ) ; // tag
74+ output . Write ( c ) ; // ciphertext
75+
76+ counter ++ ;
77+ }
78+
79+ // --- single terminal 0-length authenticated frame ---
80+ DeriveNonce ( baseNonce , counter , nonceBuf ) ;
81+ aes . Encrypt ( nonceBuf ,
82+ ReadOnlySpan < byte > . Empty ,
83+ Span < byte > . Empty ,
84+ tagBuf ,
85+ header [ ..aadLen ] ) ;
86+
87+ BinaryPrimitives . WriteUInt32LittleEndian ( frameLen4 , 0u ) ;
88+ output . Write ( frameLen4 ) ;
89+ output . Write ( tagBuf ) ;
90+ }
91+
92+
93+ public static void Decrypt ( Stream input , Stream output ) => Decrypt ( input , output , null ) ;
94+
95+ public static void Decrypt ( Stream input , Stream output , string ? key )
96+ {
97+ ArgumentNullException . ThrowIfNull ( input ) ;
98+ ArgumentNullException . ThrowIfNull ( output ) ;
99+
100+ var k = GetKeyBytes ( key ) ;
101+
102+ // header (single stackalloc outside loops)
103+ Span < byte > header = stackalloc byte [ Magic . Length + 1 + NonceSize + 4 ] ;
104+ ReadExactly ( input , header ) ;
105+
106+ if ( ! header [ ..4 ]
107+ . SequenceEqual ( Magic ) )
108+ {
109+ throw new CryptographicException ( "Invalid header." ) ;
110+ }
111+
112+ if ( header [ 4 ] != Version )
113+ {
114+ throw new CryptographicException ( "Unsupported version." ) ;
115+ }
116+
117+ var baseNonce = header . Slice ( 5 , NonceSize )
118+ . ToArray ( ) ;
119+ var chunkSize = BinaryPrimitives . ReadUInt32LittleEndian ( header . Slice ( 5 + NonceSize , 4 ) ) ;
120+ if ( chunkSize == 0 || chunkSize > 16 * 1024 * 1024 )
121+ throw new CryptographicException ( "Invalid chunk size." ) ;
122+
123+ using var aes = new AesGcm ( k , TagSize ) ;
124+
125+ var cipher = new byte [ chunkSize ] ;
126+ var plain = new byte [ chunkSize ] ;
127+ var tagBuf = new byte [ TagSize ] ;
128+ var nonceBuf = new byte [ NonceSize ] ;
129+ var lenBuf4 = new byte [ 4 ] ;
130+ var aadLen = 5 + NonceSize + 4 ;
131+
132+ ulong counter = 0 ;
133+ var sawTerminal = false ;
134+
135+ while ( true )
136+ {
137+ var got = input . Read ( lenBuf4 ) ;
138+ if ( got == 0 )
139+ break ; // we’ll check sawTerminal below
140+
141+ if ( got != 4 )
142+ throw new CryptographicException ( "Truncated frame." ) ;
143+
144+ var len = BinaryPrimitives . ReadUInt32LittleEndian ( lenBuf4 ) ;
145+ if ( len > chunkSize )
146+ throw new CryptographicException ( "Frame too large." ) ;
147+
148+ ReadExactly ( input , tagBuf ) ;
149+
150+ DeriveNonce ( baseNonce , counter , nonceBuf ) ;
151+
152+ if ( len == 0 )
153+ {
154+ // terminal frame: verify tag on empty payload
155+ aes . Decrypt ( nonceBuf ,
156+ ReadOnlySpan < byte > . Empty ,
157+ tagBuf ,
158+ Span < byte > . Empty ,
159+ header [ ..aadLen ] ) ;
160+
161+ sawTerminal = true ;
162+
163+ // after terminal, there MUST be no extra data
164+ if ( input . Read ( lenBuf4 ) != 0 )
165+ throw new CryptographicException ( "Trailing data after terminal frame." ) ;
166+
167+ break ;
168+ }
169+
170+ var c = cipher . AsSpan ( 0 , ( int ) len ) ;
171+ ReadExactly ( input , c ) ;
172+
173+ var p = plain . AsSpan ( 0 , ( int ) len ) ;
174+ aes . Decrypt ( nonceBuf , c , tagBuf , p , header [ ..aadLen ] ) ;
175+ output . Write ( p ) ;
176+
177+ counter ++ ;
178+ }
179+
180+ if ( ! sawTerminal )
181+ throw new CryptographicException ( "Missing terminal authentication frame." ) ;
182+ }
183+
184+ private static void DeriveNonce ( ReadOnlySpan < byte > baseNonce , ulong counter , Span < byte > outNonce )
185+ {
186+ // outNonce = baseNonce; then XOR LE64(counter) into last 8 bytes — no temporaries.
187+ baseNonce . CopyTo ( outNonce ) ;
188+ for ( var i = 0 ; i < 8 ; i ++ )
189+ {
190+ var b = ( byte ) ( ( counter >> ( 8 * i ) ) & 0xFF ) ;
191+ outNonce [ NonceSize - 8 + i ] ^= b ;
192+ }
193+ }
194+
195+ private static void ReadExactly ( Stream s , Span < byte > buffer )
196+ {
197+ var total = 0 ;
198+ while ( total < buffer . Length )
199+ {
200+ var r = s . Read ( buffer . Slice ( total ) ) ;
201+ if ( r == 0 ) throw new CryptographicException ( "Truncated input." ) ;
202+ total += r ;
203+ }
204+ }
205+
206+ private static byte [ ] GetKeyBytes ( string ? overrideKey )
207+ {
208+ if ( string . IsNullOrEmpty ( overrideKey ) )
209+ {
210+ return GlobalKey is null
211+ ? throw new InvalidOperationException ( "AES256 Key not configured. Call RegisterKey(...) or provide a key." )
212+ : Convert . FromBase64String ( GlobalKey ) ;
213+ }
214+
215+ ValidateKey ( overrideKey ) ;
216+ return Convert . FromBase64String ( overrideKey ) ;
217+ }
218+
219+ private static void ValidateKey ( [ NotNull ] string ? key )
220+ {
221+ if ( string . IsNullOrWhiteSpace ( key ) || ! IsBase64String ( key ) )
222+ {
223+ throw new ArgumentException ( "Key must be valid Base64." ) ;
224+ }
225+
226+ if ( Convert . FromBase64String ( key )
227+ . Length != KeySize )
228+ {
229+ throw new ArgumentException ( "Key must be 32 bytes (256 bits)." ) ;
230+ }
231+ }
232+
233+ private static bool IsBase64String ( string input )
234+ {
235+ var buffer = new Span < byte > ( new byte [ input . Length ] ) ;
236+ return Convert . TryFromBase64String ( input , buffer , out _ ) ;
237+ }
238+ }
0 commit comments