1+ using KeeperData . Core . Crypto ;
2+ using System . Security . Cryptography ;
3+ using System . Text ;
4+
5+ namespace KeeperData . Infrastructure . Crypto ;
6+
7+ public class AesCryptoTransform : IAesCryptoTransform
8+ {
9+ private const int PbeKeySpecIterationsDefault = 32 ;
10+ private const int PbeKeySpecKeyLenDefault = 256 ;
11+ private const int BufferSize = 64 * 1024 ;
12+ private const int ProgressReportInterval = 1 ;
13+
14+ public async Task EncryptFileAsync ( string inputFilePath , string outputFilePath , string password , byte [ ] salt ,
15+ ProgressCallback ? progressCallback = null , CancellationToken cancellationToken = default )
16+ {
17+ if ( ! File . Exists ( inputFilePath ) )
18+ {
19+ throw new FileNotFoundException ( $ "Input file not found: { inputFilePath } ") ;
20+ }
21+
22+ var fileInfo = new FileInfo ( inputFilePath ) ;
23+ var totalBytes = fileInfo . Length ;
24+
25+ using var inputStream = new FileStream ( inputFilePath , FileMode . Open , FileAccess . Read ) ;
26+ using var outputStream = new FileStream ( outputFilePath , FileMode . Create , FileAccess . Write ) ;
27+
28+ await EncryptStreamAsync ( inputStream , outputStream , password , salt , totalBytes , progressCallback , cancellationToken ) ;
29+ }
30+
31+ public async Task DecryptFileAsync ( string inputFilePath ,
32+ string outputFilePath ,
33+ string password ,
34+ byte [ ] salt ,
35+ ProgressCallback ? progressCallback = null ,
36+ CancellationToken cancellationToken = default )
37+ {
38+ if ( ! File . Exists ( inputFilePath ) )
39+ {
40+ throw new FileNotFoundException ( $ "Input file not found: { inputFilePath } ") ;
41+ }
42+
43+ var fileInfo = new FileInfo ( inputFilePath ) ;
44+ var totalBytes = fileInfo . Length ;
45+
46+ using var inputStream = new FileStream ( inputFilePath , FileMode . Open , FileAccess . Read ) ;
47+ using var outputStream = new FileStream ( outputFilePath , FileMode . Create , FileAccess . Write ) ;
48+
49+ await DecryptStreamAsync ( inputStream , outputStream , password , salt , totalBytes , progressCallback , cancellationToken ) ;
50+ }
51+
52+ public async Task EncryptStreamAsync ( Stream inputStream ,
53+ Stream outputStream ,
54+ string password ,
55+ byte [ ] salt ,
56+ long ? totalBytes = null ,
57+ ProgressCallback ? progressCallback = null ,
58+ CancellationToken cancellationToken = default )
59+ {
60+ var key = DeriveKey ( password , salt ) ;
61+
62+ using var aes = Aes . Create ( ) ;
63+ aes . Key = key ;
64+ aes . Mode = CipherMode . ECB ;
65+ aes . Padding = PaddingMode . PKCS7 ;
66+
67+ using var encryptor = aes . CreateEncryptor ( ) ;
68+ using var cryptoStream = new CryptoStream ( outputStream , encryptor , CryptoStreamMode . Write , leaveOpen : true ) ;
69+
70+ await ProcessStreamAsync ( inputStream , cryptoStream , totalBytes , progressCallback , "Encrypting" , cancellationToken ) ;
71+
72+ cryptoStream . FlushFinalBlock ( ) ;
73+ }
74+
75+ public async Task DecryptStreamAsync ( Stream inputStream ,
76+ Stream outputStream ,
77+ string password ,
78+ byte [ ] salt ,
79+ long ? totalBytes = null ,
80+ ProgressCallback ? progressCallback = null ,
81+ CancellationToken cancellationToken = default )
82+ {
83+ var key = DeriveKey ( password , salt ) ;
84+
85+ using var aes = Aes . Create ( ) ;
86+ aes . Key = key ;
87+ aes . Mode = CipherMode . ECB ;
88+ aes . Padding = PaddingMode . PKCS7 ;
89+
90+ using var decryptor = aes . CreateDecryptor ( ) ;
91+ using var cryptoStream = new CryptoStream ( inputStream , decryptor , CryptoStreamMode . Read , leaveOpen : true ) ;
92+
93+ await ProcessStreamAsync ( cryptoStream , outputStream , totalBytes , progressCallback , "Decrypting" , cancellationToken ) ;
94+ }
95+
96+ public async Task EncryptFileAsync ( string inputFilePath ,
97+ string outputFilePath ,
98+ string password ,
99+ string salt ,
100+ ProgressCallback ? progressCallback = null ,
101+ CancellationToken cancellationToken = default )
102+ {
103+ var saltBytes = string . IsNullOrEmpty ( salt ) ? new byte [ 0 ] : Encoding . UTF8 . GetBytes ( salt ) ;
104+ await EncryptFileAsync ( inputFilePath , outputFilePath , password , saltBytes , progressCallback , cancellationToken ) ;
105+ }
106+
107+ public async Task DecryptFileAsync ( string inputFilePath ,
108+ string outputFilePath ,
109+ string password ,
110+ string salt ,
111+ ProgressCallback ? progressCallback = null ,
112+ CancellationToken cancellationToken = default )
113+ {
114+ var saltBytes = string . IsNullOrEmpty ( salt ) ? new byte [ 0 ] : Encoding . UTF8 . GetBytes ( salt ) ;
115+ await DecryptFileAsync ( inputFilePath , outputFilePath , password , saltBytes , progressCallback , cancellationToken ) ;
116+ }
117+
118+ public async Task EncryptStreamAsync ( Stream inputStream ,
119+ Stream outputStream ,
120+ string password ,
121+ string salt ,
122+ long ? totalBytes = null ,
123+ ProgressCallback ? progressCallback = null ,
124+ CancellationToken cancellationToken = default )
125+ {
126+ var saltBytes = string . IsNullOrEmpty ( salt ) ? new byte [ 0 ] : Encoding . UTF8 . GetBytes ( salt ) ;
127+ await EncryptStreamAsync ( inputStream , outputStream , password , saltBytes , totalBytes , progressCallback , cancellationToken ) ;
128+ }
129+
130+ public async Task DecryptStreamAsync ( Stream inputStream ,
131+ Stream outputStream ,
132+ string password ,
133+ string salt ,
134+ long ? totalBytes = null ,
135+ ProgressCallback ? progressCallback = null ,
136+ CancellationToken cancellationToken = default )
137+ {
138+ var saltBytes = string . IsNullOrEmpty ( salt ) ? new byte [ 0 ] : Encoding . UTF8 . GetBytes ( salt ) ;
139+ await DecryptStreamAsync ( inputStream , outputStream , password , saltBytes , totalBytes , progressCallback , cancellationToken ) ;
140+ }
141+
142+ private static byte [ ] DeriveKey ( string password , byte [ ] salt )
143+ {
144+ var actualSalt = salt ;
145+ if ( salt . Length == 0 )
146+ {
147+ actualSalt = new byte [ 8 ] ;
148+ }
149+ else if ( salt . Length < 8 )
150+ {
151+ actualSalt = new byte [ 8 ] ;
152+ Array . Copy ( salt , actualSalt , salt . Length ) ;
153+ }
154+
155+ using var pbkdf2 = new Rfc2898DeriveBytes ( password , actualSalt , PbeKeySpecIterationsDefault , HashAlgorithmName . SHA1 ) ;
156+ return pbkdf2 . GetBytes ( PbeKeySpecKeyLenDefault / 8 ) ;
157+ }
158+
159+ private static async Task ProcessStreamAsync ( Stream inputStream ,
160+ Stream outputStream ,
161+ long ? totalBytes ,
162+ ProgressCallback ? progressCallback ,
163+ string operation ,
164+ CancellationToken cancellationToken = default )
165+ {
166+ var buffer = new byte [ BufferSize ] ;
167+ long totalBytesProcessed = 0 ;
168+ var lastReportedProgress = - 1 ;
169+
170+ progressCallback ? . Invoke ( 0 , $ "{ operation } started") ;
171+
172+ int bytesRead ;
173+ while ( ( bytesRead = await inputStream . ReadAsync ( buffer , cancellationToken ) ) > 0 )
174+ {
175+ await outputStream . WriteAsync ( buffer . AsMemory ( 0 , bytesRead ) , cancellationToken ) ;
176+ totalBytesProcessed += bytesRead ;
177+
178+ if ( totalBytes . HasValue && totalBytes . Value > 0 && progressCallback != null )
179+ {
180+ var progressPercentage = ( int ) ( totalBytesProcessed * 100 / totalBytes . Value ) ;
181+
182+ if ( progressPercentage != lastReportedProgress && progressPercentage % ProgressReportInterval == 0 )
183+ {
184+ progressCallback . Invoke ( progressPercentage ,
185+ $ "{ operation } { progressPercentage } % - { FormatBytes ( totalBytesProcessed ) } of { FormatBytes ( totalBytes . Value ) } ") ;
186+ lastReportedProgress = progressPercentage ;
187+ }
188+ }
189+ else if ( progressCallback != null )
190+ {
191+ progressCallback . Invoke ( 0 , $ "{ operation } - { FormatBytes ( totalBytesProcessed ) } processed") ;
192+ }
193+ }
194+
195+ if ( outputStream is not CryptoStream )
196+ {
197+ await outputStream . FlushAsync ( cancellationToken ) ;
198+ }
199+
200+ progressCallback ? . Invoke ( 100 , $ "{ operation } completed - { FormatBytes ( totalBytesProcessed ) } processed") ;
201+ }
202+
203+ private static string FormatBytes ( long bytes )
204+ {
205+ const long kb = 1024 ;
206+ const long mb = kb * 1024 ;
207+ const long gb = mb * 1024 ;
208+
209+ return bytes switch
210+ {
211+ >= gb => $ "{ bytes / ( double ) gb : F2} GB",
212+ >= mb => $ "{ bytes / ( double ) mb : F2} MB",
213+ >= kb => $ "{ bytes / ( double ) kb : F2} KB",
214+ _ => $ "{ bytes } bytes"
215+ } ;
216+ }
217+ }
0 commit comments