source: branches/eraser6/Manager/DirectExecutor.cs @ 348

Revision 348, 30.4 KB checked in by lowjoel, 6 years ago (diff)

Embarrassing - Left one reference to Foobar untouched...

  • Property svn:keywords set to Id
Line 
1/*
2 * $Id$
3 * Copyright 2008 The Eraser Project
4 * Original Author: Joel Low <lowjoel@users.sourceforge.net>
5 * Modified By:
6 *
7 * This file is part of Eraser.
8 *
9 * Eraser is free software: you can redistribute it and/or modify it under the
10 * terms of the GNU General Public License as published by the Free Software
11 * Foundation, either version 3 of the License, or (at your option) any later
12 * version.
13 *
14 * Eraser is distributed in the hope that it will be useful, but WITHOUT ANY
15 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
16 * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
17 *
18 * A copy of the GNU General Public License can be found at
19 * <http://www.gnu.org/licenses/>.
20 */
21
22using System;
23using System.Collections.Generic;
24using System.Collections.Specialized;
25using System.Text;
26using System.Threading;
27using System.IO;
28
29using Eraser.Util;
30using System.Security.Principal;
31using System.Runtime.Serialization;
32using System.Runtime.Serialization.Formatters.Binary;
33
34namespace Eraser.Manager
35{
36    /// <summary>
37    /// The DirectExecutor class is used by the Eraser GUI directly when the program
38    /// is run without the help of a Service.
39    /// </summary>
40    public class DirectExecutor : Executor, IDisposable
41    {
42        public DirectExecutor()
43        {
44            thread = new Thread(delegate()
45                {
46                    Main();
47                }
48            );
49
50            thread.Start();
51            Thread.Sleep(0);
52        }
53
54        void IDisposable.Dispose()
55        {
56            thread.Abort();
57            schedulerInterrupt.Set();
58        }
59
60        public override void AddTask(ref Task task)
61        {
62            lock (unusedIdsLock)
63            {
64                if (unusedIds.Count != 0)
65                {
66                    task.id = unusedIds[0];
67                    unusedIds.RemoveAt(0);
68                }
69                else
70                    task.id = ++nextId;
71            }
72
73            //Set the executor of the task
74            task.executor = this;
75
76            //Add the task to the set of tasks
77            lock (tasksLock)
78            {
79                tasks.Add(task.ID, task);
80
81                //If the task is scheduled to run now, break the waiting thread and
82                //run it immediately
83                if (task.Schedule == Schedule.RunNow)
84                {
85                    QueueTask(task);
86                }
87                //If the task is scheduled, add the next execution time to the list
88                //of schduled tasks.
89                else if (task.Schedule != Schedule.RunOnRestart)
90                {
91                    scheduledTasks.Add((task.Schedule as RecurringSchedule).NextRun, task);
92                }
93            }
94        }
95
96        public override bool DeleteTask(uint taskId)
97        {
98            lock (tasksLock)
99            {
100                if (!tasks.ContainsKey(taskId))
101                    return false;
102
103                lock (unusedIdsLock)
104                    unusedIds.Add(taskId);
105                tasks.Remove(taskId);
106
107                for (int i = 0; i != scheduledTasks.Count; ++i)
108                    if (scheduledTasks.Values[i].id == taskId)
109                        scheduledTasks.RemoveAt(i);
110            }
111
112            return true;
113        }
114
115        public override void ReplaceTask(Task task)
116        {
117            lock (tasksLock)
118            {
119                //Replace the task in the global set
120                if (!tasks.ContainsKey(task.ID))
121                    return;
122
123                tasks[task.ID] = task;
124
125                //Then replace the task if it is in the queue
126                for (int i = 0; i != scheduledTasks.Count; ++i)
127                    if (scheduledTasks.Values[i].id == task.ID)
128                        scheduledTasks.Values[i] = task;
129            }
130        }
131
132        public override void QueueTask(Task task)
133        {
134            lock (tasksLock)
135            {
136                //Set the task variable to indicate that the task is already
137                //waiting to be executed.
138                task.queued = true;
139
140                //Queue the task to be run immediately.
141                scheduledTasks.Add(DateTime.Now, task);
142                schedulerInterrupt.Set();
143            }
144        }
145
146        public override void QueueRestartTasks()
147        {
148            lock (tasksLock)
149            {
150                foreach (Task task in tasks.Values)
151                    if (task.Schedule == Schedule.RunOnRestart)
152                        QueueTask(task);
153            }
154        }
155
156        public override void CancelTask(Task task)
157        {
158            lock (currentTask)
159            {
160                if (currentTask == task)
161                {
162                    currentTask.cancelled = true;
163                    return;
164                }
165            }
166
167            lock (tasksLock)
168                for (int i = 0; i != scheduledTasks.Count; ++i)
169                    if (scheduledTasks.Values[i] == task)
170                    {
171                        scheduledTasks.RemoveAt(i);
172                        return;
173                    }
174
175            throw new ArgumentOutOfRangeException("The task to be cancelled must " +
176                "either be currently executing or queued.");
177        }
178
179        public override Task GetTask(uint taskId)
180        {
181            lock (tasksLock)
182            {
183                if (!tasks.ContainsKey(taskId))
184                    return null;
185                return tasks[taskId];
186            }
187        }
188
189        public override List<Task> GetTasks()
190        {
191            lock (tasksLock)
192            {
193                Task[] result = new Task[tasks.Count];
194                tasks.Values.CopyTo(result, 0);
195                return new List<Task>(result);
196            }
197        }
198
199        public override void SaveTaskList(Stream stream)
200        {
201            lock (tasksLock)
202                new BinaryFormatter().Serialize(stream, tasks);
203        }
204
205        public override void LoadTaskList(Stream stream)
206        {
207            lock (tasksLock)
208            {
209                //Load the list into the dictionary
210                tasks = (Dictionary<uint, Task>)new BinaryFormatter().Deserialize(stream);
211
212                //Ignore the next portion if there are no tasks
213                if (tasks.Count == 0)
214                    return;
215
216                lock (unusedIdsLock)
217                {
218                    //Find gaps in the numbering
219                    nextId = 1;
220                    foreach (uint id in tasks.Keys)
221                    {
222                        tasks[id].executor = this;
223                        while (id > nextId)
224                            unusedIds.Add(++nextId);
225                        ++nextId;
226                    }
227
228                    //Decrement the ID, since the next ID will be preincremented
229                    //before use.
230                    --nextId;
231                }
232            }
233        }
234
235        /// <summary>
236        /// The thread entry point for this object. This object operates on a queue
237        /// and hence the thread will sequentially execute tasks.
238        /// </summary>
239        private void Main()
240        {
241            //The waiting thread will utilize a polling loop to check for new
242            //scheduled tasks. This will be checked every 30 seconds. However,
243            //when the thread is waiting for a new task, it can be interrupted.
244            while (thread.ThreadState != ThreadState.AbortRequested)
245            {
246                //Check for a new task
247                Task task = null;
248                lock (tasksLock)
249                {
250                    if (scheduledTasks.Count != 0 &&
251                        (scheduledTasks.Values[0].Schedule == Schedule.RunNow ||
252                         scheduledTasks.Keys[0] <= DateTime.Now))
253                    {
254                        task = scheduledTasks.Values[0];
255                        scheduledTasks.RemoveAt(0);
256                    }
257                }
258
259                if (task != null)
260                {
261                    //Set the currently executing task.
262                    currentTask = task;
263
264                    try
265                    {
266                        //Broadcast the task started event.
267                        task.queued = false;
268                        task.cancelled = false;
269                        task.OnTaskStarted(new TaskEventArgs(task));
270
271                        //Start a new log session to separate this session's events
272                        //from previous ones.
273                        task.Log.NewSession();
274
275                        //Run the task
276                        foreach (Task.ErasureTarget target in task.Targets)
277                            try
278                            {
279                                if (target is Task.UnusedSpace)
280                                    EraseUnusedSpace(task, (Task.UnusedSpace)target);
281                                else if (target is Task.FilesystemObject)
282                                    EraseFilesystemObject(task, (Task.FilesystemObject)target);
283                                else
284                                    throw new ArgumentException("Unknown erasure target.");
285                            }
286                            catch (FatalException)
287                            {
288                                throw;
289                            }
290                            catch (Exception e)
291                            {
292                                task.Log.Add(new LogEntry(e.Message, LogLevel.ERROR));
293                            }
294                    }
295                    catch (FatalException e)
296                    {
297                        task.Log.Add(new LogEntry(e.Message, LogLevel.FATAL));
298                    }
299                    finally
300                    {
301                        //If the task is a recurring task, reschedule it since we are done.
302                        if (task.Schedule is RecurringSchedule)
303                            ((RecurringSchedule)task.Schedule).Reschedule(DateTime.Now);
304
305                        //If the task is an execute on restart task, it is only run
306                        //once and can now be restored to an immediately executed task
307                        if (task.Schedule == Schedule.RunOnRestart)
308                            task.Schedule = Schedule.RunNow;
309
310                        //And the task finished event.
311                        task.OnTaskFinished(new TaskEventArgs(task));
312                    }
313                }
314
315                //Wait for half a minute to check for the next scheduled task.
316                schedulerInterrupt.WaitOne(30000, false);
317            }
318        }
319
320        private class WriteStatistics
321        {
322            public WriteStatistics()
323            {
324                startTime = DateTime.Now;
325            }
326
327            public int Speed
328            {
329                get
330                {
331                    if (DateTime.Now == startTime)
332                        return 0;
333
334                    if ((DateTime.Now - lastSpeedCalc).Seconds < 10 && lastSpeed != 0)
335                        return lastSpeed;
336
337                    lastSpeedCalc = DateTime.Now;
338                    lastSpeed = (int)(dataWritten / (DateTime.Now - startTime).TotalSeconds);
339                    return lastSpeed;
340                }
341            }
342
343            public long DataWritten
344            {
345                get { return dataWritten; }
346                set { dataWritten = value; }
347            }
348
349            private DateTime startTime;
350            private DateTime lastSpeedCalc;
351            private long dataWritten;
352            private int lastSpeed;
353        }
354
355        /// <summary>
356        /// Executes a unused space erase.
357        /// </summary>
358        /// <param name="target">The target of the unused space erase.</param>
359        private void EraseUnusedSpace(Task task, Task.UnusedSpace target)
360        {
361            //Check for sufficient privileges to run the unused space erasure.
362            if (!Permissions.IsAdministrator())
363            {
364                string exceptionString = "The program does not have the required permissions " +
365                    "to erase the unused space on disk";
366                if (Environment.OSVersion.Platform == PlatformID.Win32NT &&
367                    Environment.OSVersion.Version >= new Version(6, 0))
368                {
369                    exceptionString += ". Run the program as administrator and retry the operation.";
370                }
371                throw new Exception(exceptionString);
372            }
373
374            //If the user is under disk quotas, log a warning message
375            if (Drive.HasQuota(target.Drive))
376                task.Log.Add(new LogEntry("The drive which is having its unused space erased has " +
377                    "disk quotas active. This will prevent the complete erasure of unused space and " +
378                    "will pose a security concern", LogLevel.WARNING));
379
380            //Get the erasure method if the user specified he wants the default.
381            ErasureMethod method = target.Method;
382            if (method == ErasureMethodManager.Default)
383                method = ErasureMethodManager.GetInstance(
384                    ManagerLibrary.Instance.Settings.DefaultUnusedSpaceErasureMethod);
385
386            TaskProgressEventArgs eventArgs = new TaskProgressEventArgs(task, 0, 0);
387            eventArgs.currentTarget = target;
388            eventArgs.currentItemName = "Cluster tips";
389            eventArgs.totalPasses = method.Passes;
390            eventArgs.timeLeft = -1;
391            task.OnProgressChanged(eventArgs);
392
393            //Erase the cluster tips of every file on the drive.
394            if (target.EraseClusterTips)
395            {
396                EraseClusterTips(task, target, method,
397                    delegate(int currentFile, string currentFilePath, int totalFiles)
398                    {
399                        eventArgs.currentItemName = "(Tips) " + currentFilePath;
400                        eventArgs.currentItemProgress = (int)((float)currentFile / totalFiles * 100);
401                        eventArgs.overallProgress = eventArgs.CurrentItemProgress / 10;
402                        task.OnProgressChanged(eventArgs);
403                    }
404                );
405            }
406
407            //Make a folder to dump our temporary files in
408            DirectoryInfo info = new DirectoryInfo(target.Drive).Root;
409            {
410                string directoryName;
411                do
412                    directoryName = GenerateRandomFileName(18);
413                while (Directory.Exists(info.FullName + Path.DirectorySeparatorChar + directoryName));
414                info = info.CreateSubdirectory(directoryName);
415            }
416
417            try
418            {
419                //Set the folder's compression flag off since we want to use as much
420                //space as possible
421                if (Eraser.Util.File.IsCompressed(info.FullName))
422                    Eraser.Util.File.SetCompression(info.FullName, false);
423
424                //Determine the total amount of data that needs to be written.
425                WriteStatistics statistics = new WriteStatistics();
426                DriveInfo driveInfo = new DriveInfo(info.Root.FullName);
427                long totalSize = method.CalculateEraseDataSize(null, driveInfo.TotalFreeSpace);
428
429                //Continue creating files while there is free space.
430                eventArgs.currentItemName = "Unused space";
431                task.OnProgressChanged(eventArgs);
432                while (driveInfo.TotalFreeSpace > 0)
433                {
434                    //Generate a non-existant file name
435                    string currFile;
436                    do
437                        currFile = info.FullName + Path.DirectorySeparatorChar +
438                            GenerateRandomFileName(18);
439                    while (System.IO.File.Exists(currFile));
440                   
441                    //Create the stream
442                    using (FileStream stream = new FileStream(currFile,
443                        FileMode.CreateNew, FileAccess.Write))
444                    {
445                        //Set the length of the file to be the amount of free space left
446                        //or the maximum size of one of these dumps.
447                        stream.SetLength(Math.Min(ErasureMethod.FreeSpaceFileUnit,
448                            driveInfo.TotalFreeSpace));
449
450                        //Then run the erase task
451                        method.Erase(stream, long.MaxValue,
452                            PRNGManager.GetInstance(ManagerLibrary.Instance.Settings.ActivePRNG),
453                            delegate(long lastWritten, int currentPass)
454                            {
455                                statistics.DataWritten += lastWritten;
456                                eventArgs.currentPass = currentPass;
457                                eventArgs.currentItemProgress = (int)(statistics.DataWritten * 100 / totalSize);
458                                eventArgs.overallProgress = (int)((10 + eventArgs.currentItemProgress * 0.8));
459
460                                if (statistics.Speed == 0)
461                                    eventArgs.timeLeft = -1;
462                                else
463                                    eventArgs.timeLeft = (int)((totalSize - statistics.DataWritten) / statistics.Speed);
464                                task.OnProgressChanged(eventArgs);
465
466                                lock (currentTask)
467                                    if (currentTask.cancelled)
468                                        throw new FatalException("The task was cancelled.");
469                            }
470                        );
471                    }
472                }
473
474                //Erase old file system records
475                eventArgs.currentItemName = "Old file system records";
476                task.OnProgressChanged(eventArgs);
477                EraseFilesystemRecords(info, method);
478            }
479            finally
480            {
481                eventArgs.currentItemName = "Removing temporary files";
482                task.OnProgressChanged(eventArgs);
483
484                //Remove the folder holding all our temporary files.
485                RemoveFolder(info);
486            }
487
488
489            //NotImplemented: clear directory entries: Eraser.cpp@2321
490        }
491
492        private delegate void SubFoldersHandler(DirectoryInfo info);
493        private delegate void ClusterTipsEraseProgress(int currentFile,
494            string currentFilePath, int totalFiles);
495        private static void EraseClusterTips(Task task, Task.UnusedSpace target,
496            ErasureMethod method, ClusterTipsEraseProgress callback)
497        {
498            //List all the files which can be erased.
499            List<string> files = new List<string>();
500            SubFoldersHandler subFolders = null;
501            subFolders = delegate(DirectoryInfo info)
502            {
503                try
504                {
505                    foreach (FileInfo file in info.GetFiles())
506                        if (Eraser.Util.File.IsProtectedSystemFile(file.FullName))
507                            task.Log.Add(new LogEntry(string.Format("{0} did not have its cluster tips " +
508                                "erased, because it is a system file", file.FullName), LogLevel.INFORMATION));
509                        else
510                        {
511                            foreach (string i in Util.File.GetADSes(file))
512                                files.Add(file.FullName + ':' + i);
513                            files.Add(file.FullName);
514                        }
515
516                    foreach (DirectoryInfo subDir in info.GetDirectories())
517                        subFolders(subDir);
518                }
519                catch (UnauthorizedAccessException)
520                {
521                }
522                catch (IOException)
523                {
524                }
525            };
526
527            subFolders(new DirectoryInfo(target.Drive));
528
529            //For every file, erase the cluster tips.
530            for (int i = 0, j = files.Count; i != j; ++i)
531            {
532                EraseFileClusterTips(files[i], method);
533                callback(i, files[i], files.Count);
534            }
535        }
536
537        /// <summary>
538        /// Erases the cluster tips of the given file.
539        /// </summary>
540        /// <param name="file">The file to erase.</param>
541        /// <param name="method">The erasure method to use.</param>
542        private static void EraseFileClusterTips(string file, ErasureMethod method)
543        {
544            //Get the file access times
545            StreamInfo streamInfo = new StreamInfo(file);
546            DateTime lastAccess = DateTime.MinValue, lastWrite = DateTime.MinValue,
547                     created = DateTime.MinValue;
548            {
549                FileInfo info = streamInfo.File;
550                if (info != null)
551                {
552                    lastAccess = info.LastAccessTime;
553                    lastWrite = info.LastWriteTime;
554                    created = info.CreationTime;
555                }
556            }
557
558            //Create the stream, lengthen the file, then tell the erasure method
559            //to erase the tips.
560            using (FileStream stream = streamInfo.Open(FileMode.Open, FileAccess.Write))
561            {
562                long fileLength = stream.Length;
563                long fileArea = GetFileArea(file);
564
565                try
566                {
567                    stream.SetLength(fileArea);
568                    stream.Seek(fileLength, SeekOrigin.Begin);
569
570                    //Erase the file
571                    method.Erase(stream, long.MaxValue, PRNGManager.GetInstance(
572                        ManagerLibrary.Instance.Settings.ActivePRNG), null);
573                }
574                finally
575                {
576                    //Make sure the file is restored!
577                    stream.SetLength(fileLength);
578                }
579            }
580
581            //Set the file times
582            FileInfo fileInfo = streamInfo.File;
583            if (fileInfo != null)
584            {
585                fileInfo.LastAccessTime = lastAccess;
586                fileInfo.LastWriteTime = lastWrite;
587                fileInfo.CreationTime = created;
588            }
589        }
590
591        /// <summary>
592        /// Erases the old MFT or FAT records. This creates small one-byte files
593        /// until the MFT or FAT grows, if the disk is not full. If the disk is
594        /// full, just keep forcing the MFT to store files until nothing else
595        /// fits.
596        /// </summary>
597        /// <param name="info">The directory information structure containing
598        /// the path to store the temporary one-byte files. The MFT of that
599        /// drive will be erased.</param>
600        /// <param name="method">The method used to erase the records.</param>
601        private void EraseFilesystemRecords(DirectoryInfo info, ErasureMethod method)
602        {
603            DriveInfo driveInfo = new DriveInfo(info.Root.FullName);
604            string driveFormat = driveInfo.DriveFormat;
605            if (driveFormat == "NTFS")
606            {
607                //If the volume is full, squeeze one-byte files.
608                try
609                {
610                    FileInfo mftInfo = new FileInfo(driveInfo.RootDirectory.FullName +
611                        Path.DirectorySeparatorChar + "$MFT");
612                    long oldMFTSize = mftInfo.Length;
613                    for ( ; ; )
614                    {
615                        //Open this stream
616                        using (FileStream strm = new FileStream(info.FullName +
617                            Path.DirectorySeparatorChar + GenerateRandomFileName(18),
618                            FileMode.CreateNew, FileAccess.Write))
619                        {
620                            //Stretch the file size to the size of one MFT record
621                            strm.SetLength(1);
622
623                            //Then run the erase task
624                            method.Erase(strm, long.MaxValue,
625                                PRNGManager.GetInstance(ManagerLibrary.Instance.Settings.ActivePRNG),
626                                null);
627                        }
628
629                        //Determine if we can stop. We will stop if the disk is not
630                        //full and the MFT has grown in size.
631                        if (driveInfo.TotalFreeSpace != 0 && mftInfo.Length != oldMFTSize)
632                            break;
633                    }
634                }
635                catch (IOException)
636                {
637                    //OK, enough squeezing.
638                }
639            }
640            else
641                throw new NotImplementedException("Could not erase old file system " +
642                    "records: Unsupported File system");
643        }
644
645        /// <summary>
646        /// Erases a file or folder on the volume.
647        /// </summary>
648        /// <param name="target">The target of the erasure.</param>
649        private void EraseFilesystemObject(Task task, Task.FilesystemObject target)
650        {
651            //Retrieve the list of files to erase.
652            long dataTotal = 0;
653            List<string> paths = target.GetPaths(out dataTotal);
654            TaskProgressEventArgs eventArgs = new TaskProgressEventArgs(task, 0, 0);
655
656            //Get the erasure method if the user specified he wants the default.
657            ErasureMethod method = target.Method;
658            if (method == ErasureMethodManager.Default)
659                method = ErasureMethodManager.GetInstance(ManagerLibrary.Instance.Settings.DefaultFileErasureMethod);
660
661            //Calculate the total amount of data required to finish the wipe.
662            dataTotal = method.CalculateEraseDataSize(paths, dataTotal);
663
664            //Record the start of the erasure pass so we can calculate speed of erasures
665            WriteStatistics statistics = new WriteStatistics();
666
667            //Iterate over every path, and erase the path.
668            for (int i = 0; i < paths.Count; ++i)
669            {
670                //Update the task progress
671                eventArgs.overallProgress = (i * 100) / paths.Count;
672                eventArgs.currentTarget = target;
673                eventArgs.currentItemName = paths[i];
674                eventArgs.currentItemProgress = 0;
675                eventArgs.totalPasses = method.Passes;
676                task.OnProgressChanged(eventArgs);
677
678                //Make sure the file does not have any attributes which may affect
679                //the erasure process
680                bool isReadOnly = false;
681                StreamInfo info = new StreamInfo(paths[i]);
682                if ((info.Attributes & FileAttributes.Compressed) != 0 ||
683                    (info.Attributes & FileAttributes.Encrypted) != 0 ||
684                    (info.Attributes & FileAttributes.SparseFile) != 0 ||
685                    (info.Attributes & FileAttributes.ReparsePoint) != 0)
686                {
687                    //Log the error
688                    throw new ArgumentException("Compressed, encrypted, or sparse" +
689                        "files cannot be erased with Eraser.");
690                }
691
692                //Remove the read-only flag, if it is set.
693                if (isReadOnly = info.IsReadOnly)
694                    info.IsReadOnly = false;
695
696                try
697                {
698                    //Create the file stream, and call the erasure method to write to
699                    //the stream.
700                    using (FileStream strm = info.Open(FileMode.Open, FileAccess.Write,
701                        FileShare.None, FileOptions.WriteThrough))
702                    {
703                        //Set the end of the stream after the wrap-round the cluster size
704                        strm.SetLength(GetFileArea(paths[i]));
705
706                        //If the stream is empty, there's nothing to overwrite. Continue
707                        //to the next entry
708                        if (strm.Length != 0)
709                        {
710                            //Then erase the file.
711                            long itemWritten = 0,
712                                 itemTotal = method.CalculateEraseDataSize(null, strm.Length);
713                            method.Erase(strm, long.MaxValue,
714                                PRNGManager.GetInstance(ManagerLibrary.Instance.Settings.ActivePRNG),
715                                delegate(long lastWritten, int currentPass)
716                                {
717                                    statistics.DataWritten += lastWritten;
718                                    eventArgs.currentPass = currentPass;
719                                    eventArgs.currentItemProgress = (int)
720                                        ((itemWritten += lastWritten) * 100 / itemTotal);
721                                    eventArgs.overallProgress = (int)
722                                        (statistics.DataWritten * 100 / dataTotal);
723
724                                    if (statistics.Speed != 0)
725                                        eventArgs.timeLeft = (int)
726                                            (dataTotal - statistics.DataWritten) / statistics.Speed;
727                                    else
728                                        eventArgs.timeLeft = -1;
729                                    task.OnProgressChanged(eventArgs);
730
731                                    lock (currentTask)
732                                        if (currentTask.cancelled)
733                                            throw new FatalException("The task was cancelled.");
734                                }
735                            );
736                        }
737
738                        //Set the length of the file to 0.
739                        strm.Seek(0, SeekOrigin.Begin);
740                        strm.SetLength(0);
741                    }
742
743                    //Remove the file.
744                    FileInfo fileInfo = info.File;
745                    if (fileInfo != null)
746                        RemoveFile(fileInfo);
747                }
748                finally
749                {
750                    //Re-set the read-only flag
751                    info.IsReadOnly = isReadOnly;
752                }
753            }
754
755            //If the user requested a folder removal, do it.
756            if (target is Task.Folder)
757            {
758                Task.Folder fldr = (Task.Folder)target;
759                if (fldr.DeleteIfEmpty)
760                    RemoveFolder(new DirectoryInfo(fldr.Path));
761            }
762        }
763
764        /// <summary>
765        /// Retrieves the size of the file on disk, calculated by the amount of
766        /// clusters allocated by it.
767        /// </summary>
768        /// <param name="filePath">The path to the file.</param>
769        /// <returns>The area of the file.</returns>
770        private static long GetFileArea(string filePath)
771        {
772            StreamInfo info = new StreamInfo(filePath);
773            uint clusterSize = Drive.GetClusterSize(info.Directory.Root.FullName);
774            return (info.Length + (clusterSize - 1)) & ~(clusterSize - 1);
775        }
776
777        /// <summary>
778        /// Securely removes files.
779        /// </summary>
780        /// <param name="info">The FileInfo object representing the file.</param>
781        private static void RemoveFile(FileInfo info)
782        {
783            //Set the date of the file to be invalid to prevent forensic
784            //detection
785            info.CreationTime = info.LastWriteTime = info.LastAccessTime =
786                new DateTime(1800, 1, 1, 0, 0, 0);
787            info.Attributes = FileAttributes.Normal;
788            info.Attributes = FileAttributes.NotContentIndexed;
789
790            //Rename the file a few times to erase the record from the MFT.
791            for (int i = 0; i < FilenameErasePasses; ++i)
792            {
793                //Rename the file.
794                string newPath = info.DirectoryName + Path.DirectorySeparatorChar +
795                    GenerateRandomFileName(info.Name.Length);
796
797                //Try to rename the file. If it fails, it is probably due to another
798                //process locking the file. Defer, then rename again.
799                try
800                {
801                    info.MoveTo(newPath);
802                }
803                catch (IOException)
804                {
805                    Thread.Sleep(100);
806                    --i;
807                }
808            }
809
810            //If the user wants plausible deniability, find a random file on the same
811            //volume and write it over.
812            if (Manager.ManagerLibrary.Instance.Settings.PlausibleDeniability)
813            {
814                //Get the template file to copy
815                FileInfo shadowFileInfo;
816                {
817                    string shadowFile;
818                    List<string> entries = ManagerLibrary.Instance.Settings.PlausibleDeniabilityFiles.GetRange(
819                        0, ManagerLibrary.Instance.Settings.PlausibleDeniabilityFiles.Count);
820                    PRNG prng = PRNGManager.GetInstance(ManagerLibrary.Instance.Settings.ActivePRNG);
821                    do
822                    {
823                        if (entries.Count == 0)
824                            throw new FatalException("Plausible deniability was selected, but no decoy files " +
825                                "were found. The current file has been only replaced with random data.");
826
827                        int index = prng.Next(entries.Count - 1);
828                        if ((System.IO.File.GetAttributes(entries[index]) & FileAttributes.Directory) != 0)
829                            shadowFile = GetRandomFileName(new DirectoryInfo(entries[index]));
830                        else
831                            shadowFile = entries[index];
832
833                        entries.RemoveAt(index);
834                    }
835                    while (shadowFile.Length == 0 || Path.GetDirectoryName(shadowFile) == info.DirectoryName);
836                    shadowFileInfo = new FileInfo(shadowFile);
837                }
838
839                //Rename the file to have the same name as the shadow
840                info.MoveTo(info.DirectoryName + shadowFileInfo.Name);
841
842                //Dump the copy (the first 4MB, or less, depending on the file size and available
843                //user space)
844                long amountToCopy = Math.Min(4 * 1024 * 1024, shadowFileInfo.Length);
845                using (FileStream shadowFileStream = shadowFileInfo.OpenRead())
846                using (FileStream destFileStream = info.OpenWrite())
847                {
848                    while (destFileStream.Position < amountToCopy)
849                    {
850                        byte[] buf = new byte[524288];
851                        int bytesRead = shadowFileStream.Read(buf, 0, buf.Length);
852
853                        //Stop bothering if the input stream is at the end
854                        if (bytesRead == 0)
855                            break;
856
857                        //Dump the read contents onto the file to be deleted
858                        destFileStream.Write(buf, 0,
859                            (int)Math.Min(bytesRead, amountToCopy - destFileStream.Position));
860                    }
861                }
862            }
863
864            //Then delete the file.
865            info.Delete();
866        }
867
868        /// <summary>
869        /// Removes the folder and all its contents.
870        /// </summary>
871        /// <param name="info">The folder to remove.</param>
872        private static void RemoveFolder(DirectoryInfo info)
873        {
874            foreach (FileInfo file in info.GetFiles())
875                RemoveFile(file);
876            foreach (DirectoryInfo dir in info.GetDirectories())
877                RemoveFolder(dir);
878
879            //Then clean up this folder.
880            for (int i = 0; i < FilenameErasePasses; ++i)
881            {
882                //Rename the folder.
883                string newPath = info.Parent.FullName + Path.DirectorySeparatorChar +
884                    GenerateRandomFileName(info.Name.Length);
885
886                //Try to rename the file. If it fails, it is probably due to another
887                //process locking the file. Defer, then rename again.
888                try
889                {
890                    info.MoveTo(newPath);
891                }
892                catch (IOException)
893                {
894                    Thread.Sleep(100);
895                    --i;
896                }
897            }
898
899            //Remove the folder
900            info.Delete();
901        }
902
903        /// <summary>
904        /// Generates a random file name with the given length.
905        /// </summary>
906        /// <param name="length">The length of the file name to generate.</param>
907        /// <returns>A random file name.</returns>
908        private static string GenerateRandomFileName(int length)
909        {
910            //Get a random file name
911            PRNG prng = PRNGManager.GetInstance(ManagerLibrary.Instance.Settings.ActivePRNG);
912            byte[] newFileNameAry = new byte[length];
913            prng.NextBytes(newFileNameAry);
914
915            //Validate the name
916            string validFileNameChars = "0123456789abcdefghijklmnopqrstuvwxyz" +
917                "ABCDEFGHIJKLMNOPQRSTUVWXYZ _+=-()[]{}',`~!";
918            for (int j = 0, k = newFileNameAry.Length; j < k; ++j)
919                newFileNameAry[j] = (byte)validFileNameChars[
920                    (int)newFileNameAry[j] % validFileNameChars.Length];
921
922            return new System.Text.UTF8Encoding().GetString(newFileNameAry);
923        }
924
925        /// <summary>
926        /// Gets a random file from within the provided directory.
927        /// </summary>
928        /// <param name="info">The directory to get a random file name from.</param>
929        /// <returns>A string containing the full path to the file.</returns>
930        private static string GetRandomFileName(DirectoryInfo info)
931        {
932            //First retrieve the list of files and folders in the provided directory.
933            FileSystemInfo[] entries = null;
934            try
935            {
936                entries = info.GetFileSystemInfos();
937            }
938            catch (Exception)
939            {
940                return string.Empty;
941            }
942            if (entries.Length == 0)
943                return string.Empty;
944
945            //Find a random entry.
946            PRNG prng = PRNGManager.GetInstance(ManagerLibrary.Instance.Settings.ActivePRNG);
947            string result = string.Empty;
948            while (result.Length == 0)
949            {
950                int index = prng.Next(entries.Length - 1);
951                if (entries[index] is DirectoryInfo)
952                    result = GetRandomFileName((DirectoryInfo)entries[index]);
953                else 
954                    result = ((FileInfo)entries[index]).FullName;
955            }
956
957            return result;
958        }
959
960        /// <summary>
961        /// The thread object.
962        /// </summary>
963        private Thread thread;
964
965        /// <summary>
966        /// The lock preventing concurrent access for the tasks list and the
967        /// tasks queue.
968        /// </summary>
969        private object tasksLock = new object();
970
971        /// <summary>
972        /// The list of tasks. Includes all immediate, reboot, and recurring tasks
973        /// </summary>
974        private Dictionary<uint, Task> tasks = new Dictionary<uint, Task>();
975
976        /// <summary>
977        /// The queue of tasks. This queue is executed when the first element's
978        /// timestamp (the key) has been past. This list assumes that all tasks
979        /// are sorted by timestamp, smallest one first.
980        /// </summary>
981        private SortedList<DateTime, Task> scheduledTasks =
982            new SortedList<DateTime, Task>();
983
984        /// <summary>
985        /// The currently executing task.
986        /// </summary>
987        Task currentTask;
988
989        /// <summary>
990        /// The list of task IDs for recycling.
991        /// </summary>
992        private List<uint> unusedIds = new List<uint>();
993
994        /// <summary>
995        /// Lock preventing concurrent access for the IDs.
996        /// </summary>
997        private object unusedIdsLock = new object();
998
999        /// <summary>
1000        /// Incrementing ID. This value is incremented by one every time an ID
1001        /// is required by no unused IDs remain.
1002        /// </summary>
1003        private uint nextId = 0;
1004
1005        /// <summary>
1006        /// An automatically reset event allowing the addition of new tasks to
1007        /// interrupt the thread's sleeping state waiting for the next recurring
1008        /// task to be due.
1009        /// </summary>
1010        AutoResetEvent schedulerInterrupt = new AutoResetEvent(true);
1011    }
1012}
Note: See TracBrowser for help on using the repository browser.