1+ using System . Diagnostics . CodeAnalysis ;
2+ using System . Security . Cryptography ;
3+ using System . Text ;
4+ using Org . BouncyCastle . Crypto ;
5+ using Org . BouncyCastle . Crypto . Engines ;
6+ using Org . BouncyCastle . Crypto . Macs ;
7+ using Org . BouncyCastle . Crypto . Modes ;
8+ using Org . BouncyCastle . Crypto . Parameters ;
9+ using Org . BouncyCastle . Utilities ;
10+
11+ namespace Pandatech . Crypto . Helpers ;
12+
13+ public static class Aes256Siv
14+ {
15+ private static string ? GlobalKey { get ; set ; }
16+
17+ internal static void RegisterKey ( string key )
18+ {
19+ ValidateKey ( key ) ;
20+ GlobalKey = key ;
21+ }
22+
23+ public static byte [ ] Encrypt ( string plaintext , string ? key = null )
24+ {
25+ var bytes = Encoding . UTF8 . GetBytes ( plaintext ) ;
26+ return Encrypt ( bytes , key ) ;
27+ }
28+
29+ public static byte [ ] Encrypt ( byte [ ] plaintext , string ? key = null )
30+ {
31+ if ( plaintext . Length == 0 )
32+ {
33+ return [ ] ;
34+ }
35+
36+ var keyBytes = GetKeyBytes ( key ) ;
37+ var macKey = keyBytes [ ..16 ] ;
38+ var encKey = keyBytes [ 16 ..] ;
39+
40+ var siv = ComputeS2V ( macKey , plaintext ) ;
41+ var cipher = AesCtr ( encKey , siv , plaintext ) ;
42+ return Arrays . Concatenate ( siv , cipher ) ;
43+ }
44+
45+ public static void Encrypt ( Stream input , Stream output , string ? key = null )
46+ {
47+ ArgumentNullException . ThrowIfNull ( input ) ;
48+ ArgumentNullException . ThrowIfNull ( output ) ;
49+
50+ using var ms = new MemoryStream ( ) ;
51+ input . CopyTo ( ms ) ;
52+ var encrypted = Encrypt ( ms . ToArray ( ) , key ) ;
53+ output . Write ( encrypted , 0 , encrypted . Length ) ;
54+ }
55+
56+ public static string Decrypt ( byte [ ] ciphertext , string ? key = null )
57+ {
58+ var plain = DecryptToBytes ( ciphertext , key ) ;
59+ return Encoding . UTF8 . GetString ( plain ) ;
60+ }
61+
62+ public static byte [ ] DecryptToBytes ( byte [ ] ciphertext , string ? key = null )
63+ {
64+ var keyBytes = GetKeyBytes ( key ) ;
65+
66+ switch ( ciphertext . Length )
67+ {
68+ case 0 :
69+ return [ ] ;
70+ case < 16 :
71+ throw new ArgumentException ( "At least 16 bytes are required for the SIV." ) ;
72+ }
73+
74+ var macKey = keyBytes [ ..16 ] ;
75+ var encKey = keyBytes [ 16 ..] ;
76+
77+ var siv = ciphertext [ ..16 ] ;
78+ var encrypted = ciphertext [ 16 ..] ;
79+
80+ var plain = AesCtr ( encKey , siv , encrypted ) ;
81+ var expectedSiv = ComputeS2V ( macKey , plain ) ;
82+
83+ if ( ! CryptographicOperations . FixedTimeEquals ( siv , expectedSiv ) )
84+ throw new CryptographicException ( "Invalid SIV / authentication tag." ) ;
85+
86+ return plain ;
87+ }
88+
89+ public static void Decrypt ( Stream input , Stream output , string ? key = null )
90+ {
91+ ArgumentNullException . ThrowIfNull ( input ) ;
92+ ArgumentNullException . ThrowIfNull ( output ) ;
93+
94+ using var ms = new MemoryStream ( ) ;
95+ input . CopyTo ( ms ) ;
96+ var decrypted = DecryptToBytes ( ms . ToArray ( ) , key ) ;
97+ output . Write ( decrypted , 0 , decrypted . Length ) ;
98+ }
99+
100+ private static byte [ ] ComputeS2V ( byte [ ] macKey , byte [ ] data )
101+ {
102+ var cmac = new CMac ( new AesEngine ( ) ) ;
103+ cmac . Init ( new KeyParameter ( macKey ) ) ;
104+ var D = CmacHash ( cmac , new byte [ 16 ] ) ;
105+
106+ if ( data . Length >= 16 )
107+ {
108+ var block = new byte [ 16 ] ;
109+ Array . Copy ( data , data . Length - 16 , block , 0 , 16 ) ;
110+ for ( var i = 0 ; i < 16 ; i ++ )
111+ block [ i ] ^= D [ i ] ;
112+ return CmacHash ( cmac , block ) ;
113+ }
114+ else
115+ {
116+ D = DoubleBlock ( D ) ;
117+ var block = Pad ( data ) ;
118+ for ( var i = 0 ; i < 16 ; i ++ )
119+ block [ i ] ^= D [ i ] ;
120+ return CmacHash ( cmac , block ) ;
121+ }
122+ }
123+
124+ private static byte [ ] AesCtr ( byte [ ] key , byte [ ] iv , byte [ ] input )
125+ {
126+ var cipher = new BufferedBlockCipher ( new SicBlockCipher ( new AesEngine ( ) ) ) ;
127+ cipher . Init ( true , new ParametersWithIV ( new KeyParameter ( key ) , iv ) ) ;
128+ return cipher . DoFinal ( input ) ;
129+ }
130+
131+ private static byte [ ] CmacHash ( IMac cmac , byte [ ] input )
132+ {
133+ cmac . Reset ( ) ;
134+ cmac . BlockUpdate ( input , 0 , input . Length ) ;
135+ var output = new byte [ cmac . GetMacSize ( ) ] ;
136+ cmac . DoFinal ( output , 0 ) ;
137+ return output ;
138+ }
139+
140+ private static byte [ ] Pad ( byte [ ] input )
141+ {
142+ var padded = new byte [ 16 ] ;
143+ Array . Copy ( input , padded , input . Length ) ;
144+ padded [ input . Length ] = 0x80 ;
145+ return padded ;
146+ }
147+
148+ private static byte [ ] DoubleBlock ( byte [ ] block )
149+ {
150+ var output = new byte [ 16 ] ;
151+ var carry = 0 ;
152+ for ( var i = 15 ; i >= 0 ; i -- )
153+ {
154+ var val = ( block [ i ] & 0xFF ) << 1 ;
155+ val |= carry ;
156+ output [ i ] = ( byte ) ( val & 0xFF ) ;
157+ carry = ( val >> 8 ) & 1 ;
158+ }
159+
160+ if ( ( block [ 0 ] & 0x80 ) != 0 )
161+ output [ 15 ] ^= 0x87 ;
162+ return output ;
163+ }
164+
165+ private static byte [ ] GetKeyBytes ( string ? overrideKey )
166+ {
167+ if ( ! string . IsNullOrEmpty ( overrideKey ) )
168+ {
169+ ValidateKey ( overrideKey ) ;
170+ return Convert . FromBase64String ( overrideKey ) ;
171+ }
172+
173+ if ( GlobalKey is null )
174+ {
175+ throw new InvalidOperationException ( "AES256 Key not configured. Call RegisterKey(...) or provide a key." ) ;
176+ }
177+
178+ return Convert . FromBase64String ( GlobalKey ) ;
179+ }
180+
181+ private static void ValidateKey ( [ NotNull ] string ? key )
182+ {
183+ if ( string . IsNullOrWhiteSpace ( key ) || ! IsBase64String ( key ) )
184+ throw new ArgumentException ( "Key must be valid Base64." ) ;
185+ if ( Convert . FromBase64String ( key )
186+ . Length != 32 )
187+ throw new ArgumentException ( "Key must be 32 bytes (256 bits)." ) ;
188+ }
189+
190+ private static bool IsBase64String ( string input )
191+ {
192+ var buffer = new Span < byte > ( new byte [ input . Length ] ) ;
193+ return Convert . TryFromBase64String ( input , buffer , out _ ) ;
194+ }
195+ }
0 commit comments