using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Text; using System.Threading; using System.IO; using Eraser.Util; using System.Runtime.Serialization; using System.Runtime.Serialization.Formatters.Binary; namespace Eraser.Manager { /// /// The DirectExecutor class is used by the Eraser GUI directly when the program /// is run without the help of a Service. /// public class DirectExecutor : Executor, IDisposable { public DirectExecutor() { thread = new Thread(delegate() { Main(); }); thread.Start(); Thread.Sleep(0); } void IDisposable.Dispose() { thread.Abort(); schedulerInterrupt.Set(); } public override void AddTask(ref Task task) { lock (unusedIdsLock) { if (unusedIds.Count != 0) { task.id = unusedIds[0]; unusedIds.RemoveAt(0); } else task.id = ++nextId; } //Set the executor of the task task.executor = this; //Add the task to the set of tasks lock (tasksLock) { tasks.Add(task.ID, task); //If the task is scheduled to run now, break the waiting thread and //run it immediately if (task.Schedule == Schedule.RunNow) { QueueTask(task); } //If the task is scheduled, add the next execution time to the list //of schduled tasks. else if (task.Schedule != Schedule.RunOnRestart) { scheduledTasks.Add((task.Schedule as RecurringSchedule).NextRun, task); } } } public override bool DeleteTask(uint taskId) { lock (tasksLock) { if (!tasks.ContainsKey(taskId)) return false; lock (unusedIdsLock) unusedIds.Add(taskId); tasks.Remove(taskId); for (int i = 0; i != scheduledTasks.Count; ++i) if (scheduledTasks.Values[i].id == taskId) scheduledTasks.RemoveAt(i); } return true; } public override void ReplaceTask(Task task) { lock (tasksLock) { //Replace the task in the global set if (!tasks.ContainsKey(task.ID)) return; tasks[task.ID] = task; //Then replace the task if it is in the queue for (int i = 0; i != scheduledTasks.Count; ++i) if (scheduledTasks.Values[i].id == task.ID) scheduledTasks.Values[i] = task; } } public override void QueueTask(Task task) { lock (tasksLock) { //Set the task variable to indicate that the task is already //waiting to be executed. task.queued = true; //Queue the task to be run immediately. scheduledTasks.Add(DateTime.Now, task); schedulerInterrupt.Set(); } } public override void QueueRestartTasks() { lock (tasksLock) { foreach (Task task in tasks.Values) if (task.Schedule == Schedule.RunOnRestart) QueueTask(task); } } public override void CancelTask(Task task) { lock (currentTask) { if (currentTask == task) { currentTask.cancelled = true; return; } } lock (tasksLock) for (int i = 0; i != scheduledTasks.Count; ++i) if (scheduledTasks.Values[i] == task) { scheduledTasks.RemoveAt(i); return; } throw new ArgumentOutOfRangeException("The task to be cancelled must " + "either be currently executing or queued."); } public override Task GetTask(uint taskId) { lock (tasksLock) { if (!tasks.ContainsKey(taskId)) return null; return tasks[taskId]; } } public override List GetTasks() { lock (tasksLock) { Task[] result = new Task[tasks.Count]; tasks.Values.CopyTo(result, 0); return new List(result); } } public override void SaveTaskList(Stream stream) { lock (tasksLock) new BinaryFormatter().Serialize(stream, tasks); } public override void LoadTaskList(Stream stream) { lock (tasksLock) { //Load the list into the dictionary tasks = (Dictionary)new BinaryFormatter().Deserialize(stream); //Ignore the next portion if there are no tasks if (tasks.Count == 0) return; lock (unusedIdsLock) { //Find gaps in the numbering nextId = 1; foreach (uint id in tasks.Keys) { while (id > nextId) unusedIds.Add(++nextId); ++nextId; } //Decrement the ID, since the next ID will be preincremented //before use. --nextId; } } } /// /// The thread entry point for this object. This object operates on a queue /// and hence the thread will sequentially execute tasks. /// private void Main() { //The waiting thread will utilize a polling loop to check for new //scheduled tasks. This will be checked every 30 seconds. However, //when the thread is waiting for a new task, it can be interrupted. while (thread.ThreadState != ThreadState.AbortRequested) { //Check for a new task Task task = null; lock (tasksLock) { if (scheduledTasks.Count != 0 && (scheduledTasks.Values[0].Schedule == Schedule.RunNow || scheduledTasks.Keys[0] <= DateTime.Now)) { task = scheduledTasks.Values[0]; scheduledTasks.RemoveAt(0); } } if (task != null) { //Set the currently executing task. currentTask = task; try { //Broadcast the task started event. task.queued = false; task.cancelled = false; task.OnTaskStarted(new TaskEventArgs(task)); //Run the task foreach (Task.ErasureTarget target in task.Targets) try { if (target is Task.UnusedSpace) EraseUnusedSpace(task, (Task.UnusedSpace)target); else if (target is Task.FilesystemObject) EraseFilesystemObject(task, (Task.FilesystemObject)target); else throw new ArgumentException("Unknown erasure target."); } catch (FatalException) { throw; } catch (Exception e) { task.LogEntry(new LogEntry(e.Message, LogLevel.ERROR)); } } catch (FatalException e) { task.LogEntry(new LogEntry(e.Message, LogLevel.FATAL)); } finally { //If the task is a recurring task, reschedule it since we are done. if (task.Schedule is RecurringSchedule) ((RecurringSchedule)task.Schedule).Reschedule(DateTime.Now); //If the task is an execute on restart task, it is only run //once and can now be restored to an immediately executed task if (task.Schedule == Schedule.RunOnRestart) task.Schedule = Schedule.RunNow; //And the task finished event. task.OnTaskFinished(new TaskEventArgs(task)); } } //Wait for half a minute to check for the next scheduled task. schedulerInterrupt.WaitOne(30000, false); } } /// /// Executes a unused space erase. /// /// The target of the unused space erase. private void EraseUnusedSpace(Task task, Task.UnusedSpace target) { throw new NotImplementedException("Unused space erasures are not "+ "currently implemented"); } /// /// Erases a file or folder on the volume. /// /// The target of the erasure. private void EraseFilesystemObject(Task task, Task.FilesystemObject target) { //Retrieve the list of files to erase. long totalSize = 0; List paths = target.GetPaths(out totalSize); TaskProgressEventArgs eventArgs = new TaskProgressEventArgs(task, 0, 0); //Get the erasure method if the user specified he wants the default. ErasureMethod method = target.Method; if (method == ErasureMethodManager.Default) method = ErasureMethodManager.GetInstance(Globals.Settings.DefaultFileErasureMethod); //Calculate the total amount of data required to finish the wipe. This //value is just the total about of data to be erased multiplied by //number of passes totalSize = method.CalculateEraseDataSize(paths, totalSize); //Record the start of the erasure pass so we can calculate speed of erasures long totalLeft = totalSize; long overallWriteSpeed = 0; DateTime startTime = DateTime.Now; //Iterate over every path, and erase the path. for (int i = 0; i < paths.Count; ++i) { //Update the task progress eventArgs.overallProgress = (i * 100) / paths.Count; eventArgs.currentTarget = target; eventArgs.currentItemName = paths[i]; eventArgs.currentItemProgress = 0; eventArgs.totalPasses = method.Passes; task.OnProgressChanged(eventArgs); //Make sure the file does not have any attributes which may //affect the erasure process FileInfo info = new FileInfo(paths[i]); if ((info.Attributes & FileAttributes.Compressed) != 0 || (info.Attributes & FileAttributes.Encrypted) != 0 || (info.Attributes & FileAttributes.SparseFile) != 0 || (info.Attributes & FileAttributes.ReparsePoint) != 0) { //Log the error throw new ArgumentException("Compressed, encrypted, or sparse" + "files cannot be erased with Eraser."); } //Remove the read-only flag, if it is set. if ((info.Attributes & FileAttributes.ReadOnly) != 0) info.Attributes &= ~FileAttributes.ReadOnly; //Create the file stream, and call the erasure method //to write to the stream. using (FileStream strm = new FileStream(info.FullName, FileMode.Open, FileAccess.Write, FileShare.None, 8, FileOptions.WriteThrough)) { //Set the end of the stream after the wrap-round the cluster size uint clusterSize = Drive.GetClusterSize(info.Directory.Root.FullName); long roundUpFileLength = strm.Length % clusterSize; if (roundUpFileLength != 0) strm.SetLength(strm.Length + (clusterSize - roundUpFileLength)); //Then erase the file. method.Erase(strm, long.MaxValue, PRNGManager.GetInstance(Globals.Settings.ActivePRNG), delegate(float currentProgress, int currentPass) { long amountWritten = (long)(currentProgress * (info.Length * method.Passes)); if (overallWriteSpeed != 0) eventArgs.timeLeft = (int)((totalLeft - amountWritten) / overallWriteSpeed); else if (amountWritten != 0 && (DateTime.Now - startTime).TotalSeconds != 0) overallWriteSpeed = (long)(amountWritten / (DateTime.Now - startTime).TotalSeconds); eventArgs.currentPass = currentPass; eventArgs.currentItemProgress = (int) ((float)currentProgress * 100.0); eventArgs.overallProgress = (int) (((i + currentProgress) / (float)paths.Count) * 100); task.OnProgressChanged(eventArgs); lock (currentTask) if (currentTask.cancelled) throw new FatalException("The task was cancelled."); } ); //Update the speed-influencing statistics. totalLeft -= info.Length * method.Passes; overallWriteSpeed = (long)((totalSize - totalLeft) / (DateTime.Now - startTime).TotalSeconds); //Set the length of the file to 0. strm.Seek(0, SeekOrigin.Begin); strm.SetLength(0); } //Remove the file. RemoveFile(info); } //If the user requested a folder removal, do it. if (target is Task.Folder) { Task.Folder fldr = (Task.Folder)target; if (fldr.DeleteIfEmpty) RemoveFolder(new DirectoryInfo(fldr.Path)); } } /// /// Securely removes files. /// /// The FileInfo object representing the file. private static void RemoveFile(FileInfo info) { //Set the date of the file to be invalid to prevent forensic //detection info.CreationTime = info.LastWriteTime = info.LastAccessTime = new DateTime(1800, 1, 1, 0, 0, 0); info.Attributes = FileAttributes.Normal; info.Attributes = FileAttributes.NotContentIndexed; //Rename the file a few times to erase the record from the MFT. for (int i = 0; i < FilenameErasePasses; ++i) { //Rename the file. string newPath = info.DirectoryName + Path.DirectorySeparatorChar + GetRandomFileName(info.Name.Length); //Try to rename the file. If it fails, it is probably due to another //process locking the file. Defer, then rename again. try { info.MoveTo(newPath); } catch (IOException) { Thread.Sleep(100); --i; } } //Then delete the file. info.Delete(); } private static void RemoveFolder(DirectoryInfo info) { foreach (FileInfo file in info.GetFiles()) RemoveFile(file); foreach (DirectoryInfo dir in info.GetDirectories()) RemoveFolder(dir); //Then clean up this folder. for (int i = 0; i < FilenameErasePasses; ++i) { //Rename the folder. string newPath = info.Parent.FullName + Path.DirectorySeparatorChar + GetRandomFileName(info.Name.Length); //Try to rename the file. If it fails, it is probably due to another //process locking the file. Defer, then rename again. try { info.MoveTo(newPath); } catch (IOException) { Thread.Sleep(100); --i; } } //Remove the folder info.Delete(); } /// /// Generates a random file name with the given length. /// /// The length of the file name to generate. /// A random file name. private static string GetRandomFileName(int length) { //Get a random file name PRNG prng = PRNGManager.GetInstance(Globals.Settings.ActivePRNG); byte[] newFileNameAry = new byte[length]; prng.NextBytes(newFileNameAry); //Validate the name string validFileNameChars = "0123456789abcdefghijklmnopqrstuvwxyz" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ _+=-()[]{}',`~!"; for (int j = 0, k = newFileNameAry.Length; j < k; ++j) newFileNameAry[j] = (byte)validFileNameChars[ (int)newFileNameAry[j] % validFileNameChars.Length]; return new System.Text.UTF8Encoding().GetString(newFileNameAry); } /// /// The thread object. /// private Thread thread; /// /// The lock preventing concurrent access for the tasks list and the /// tasks queue. /// private object tasksLock = new object(); /// /// The list of tasks. Includes all immediate, reboot, and recurring tasks /// private Dictionary tasks = new Dictionary(); /// /// The queue of tasks. This queue is executed when the first element's /// timestamp (the key) has been past. This list assumes that all tasks /// are sorted by timestamp, smallest one first. /// private SortedList scheduledTasks = new SortedList(); /// /// The currently executing task. /// Task currentTask; /// /// The list of task IDs for recycling. /// private List unusedIds = new List(); /// /// Lock preventing concurrent access for the IDs. /// private object unusedIdsLock = new object(); /// /// Incrementing ID. This value is incremented by one every time an ID /// is required by no unused IDs remain. /// private uint nextId = 0; /// /// An automatically reset event allowing the addition of new tasks to /// interrupt the thread's sleeping state waiting for the next recurring /// task to be due. /// AutoResetEvent schedulerInterrupt = new AutoResetEvent(true); } }