/* * $Id$ * Copyright 2008-2012 The Eraser Project * Original Author: Joel Low * Modified By: * * This file is part of Eraser. * * Eraser is free software: you can redistribute it and/or modify it under the * terms of the GNU General Public License as published by the Free Software * Foundation, either version 3 of the License, or (at your option) any later * version. * * Eraser is distributed in the hope that it will be useful, but WITHOUT ANY * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR * A PARTICULAR PURPOSE. See the GNU General Public License for more details. * * A copy of the GNU General Public License can be found at * . */ using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.IO; using System.Threading; using Eraser.Util; using Eraser.Util.ExtensionMethods; using Eraser.Plugins; using Eraser.Plugins.ExtensionPoints; namespace Eraser.DefaultPlugins { /// /// Base class for all Windows filesystems. /// abstract class WindowsFileSystem : FileSystemBase { public override void ResetFileTimes(FileSystemInfo info) { //Reset the file access times: after every rename the file times may change. info.SetTimes(MinTimestamp, MinTimestamp, MinTimestamp, MinTimestamp); } public override void DeleteFile(FileInfo info) { //If the user wants plausible deniability, find a random file from the //list of decoys and write it over. if (Host.Instance.Settings.PlausibleDeniability) { using (FileStream fileStream = info.OpenWrite()) CopyPlausibleDeniabilityFile(fileStream); } DeleteFileSystemInfo(info); } public override void DeleteFolder(DirectoryInfo info, bool recursive) { if ((info.Attributes & FileAttributes.ReparsePoint) == 0) { if (!recursive && info.GetFileSystemInfos().Length != 0) throw new InvalidOperationException(S._("The folder {0} cannot be deleted as it is " + "not empty.")); foreach (DirectoryInfo dir in info.GetDirectories()) DeleteFolder(dir, true); foreach (FileInfo file in info.GetFiles()) DeleteFile(file); } DeleteFileSystemInfo(info); } /// /// Deletes a directory or file from disk. This assumes that directories already /// have been deleted. /// /// The file or directory to delete. private void DeleteFileSystemInfo(FileSystemInfo info) { //If the file/directory doesn't exist, pass. if (!info.Exists) return; //Reset the file attributes to non-content indexed so indexing //services will not lock the file. try { info.Attributes = FileAttributes.NotContentIndexed; } catch (ArgumentException e) { //This is an undocumented exception: when the path we are setting //cannot be accessed (ERROR_ACCESS_DENIED is returned) an //ArgumentException is raised (no idea why!) throw new UnauthorizedAccessException(e.Message, e); } //Rename the file a few times to erase the entry from the file system //table. for (int i = 0, tries = 0; i < FileNameErasePasses; ++tries) { //Generate a new file name for the file/directory. string newPath = GenerateRandomFileName(info.GetParent(), info.Name.Length); try { ResetFileTimes(info); //Try to rename the file. If it fails, it is probably due to another //process locking the file. Defer, then rename again. info.MoveTo(newPath); ++i; } catch (IOException e) { switch (System.Runtime.InteropServices.Marshal.GetLastWin32Error()) { case Win32ErrorCode.AccessDenied: throw new UnauthorizedAccessException(S._("The file {0} could not " + "be erased because the file's permissions prevent access to the file.", info.FullName)); case Win32ErrorCode.SharingViolation: //If after FilenameEraseTries the file is still locked, some program is //definitely using the file; throw an exception. if (tries > FileNameEraseTries) { //Try to force the handle closed. if (tries > FileNameEraseTries + 1 || !Host.Instance.Settings.ForceUnlockLockedFiles) { throw new IOException(S._("The file {0} is currently in use " + "and cannot be removed.", info.FullName), e); } StringBuilder processStr = new StringBuilder(); foreach (OpenHandle handle in OpenHandle.Close(info.FullName)) { try { processStr.AppendFormat( System.Globalization.CultureInfo.InvariantCulture, "{0}, ", handle.Process.MainModule.FileName); } catch (System.ComponentModel.Win32Exception) { processStr.AppendFormat( System.Globalization.CultureInfo.InvariantCulture, "Process ID {0}, ", handle.Process.Id); } } if (processStr.Length != 0) Logger.Log(S._("Could not force closure of file \"{0}\" {1}", info.FullName, S._("(locked by {0})", processStr.ToString().Remove(processStr.Length - 2)).Trim()), LogLevel.Error); } //Let the process locking the file release the lock Thread.Sleep(100); break; case Win32ErrorCode.DiskFull: //If the disk is full, we can't do anything except manually deleting //the file, break out of this loop. i = FileNameEraseTries; break; default: throw; } } } //Then delete the file. for (int i = 0; i < FileNameEraseTries; ++i) try { info.Delete(); break; } catch (IOException e) { switch (System.Runtime.InteropServices.Marshal.GetLastWin32Error()) { case Win32ErrorCode.AccessDenied: throw new UnauthorizedAccessException(S._("The file {0} could not " + "be erased because the file's permissions prevent access to the file.", info.FullName), e); case Win32ErrorCode.SharingViolation: //If after FilenameEraseTries the file is still locked, some program is //definitely using the file; throw an exception. if (i > FileNameEraseTries) throw new IOException(S._("The file {0} is currently in use and " + "cannot be removed.", info.FullName), e); //Let the process locking the file release the lock Thread.Sleep(100); break; default: throw; } } } /// /// Writes a file for plausible deniability over the current stream. /// /// The stream to write the data to. private static void CopyPlausibleDeniabilityFile(Stream stream) { //Get the template file to copy FileInfo shadowFileInfo; { string shadowFile = null; List entries = new List( Host.Instance.Settings.PlausibleDeniabilityFiles); IPrng prng = Host.Instance.Prngs.ActivePrng; do { if (entries.Count == 0) throw new FatalException(S._("Plausible deniability was selected, " + "but no decoy files were found. The current file has been only " + "replaced with random data.")); //Get an item from the list of files, and then check that the item exists. int index = prng.Next(entries.Count - 1); shadowFile = entries[index]; if (File.Exists(shadowFile) || Directory.Exists(shadowFile)) { if ((File.GetAttributes(shadowFile) & FileAttributes.Directory) != 0) { DirectoryInfo dir = new DirectoryInfo(shadowFile); FileInfo[] files = dir.GetFiles("*", SearchOption.AllDirectories); entries.Capacity += files.Length; foreach (FileInfo f in files) entries.Add(f.FullName); } else shadowFile = entries[index]; } else shadowFile = null; entries.RemoveAt(index); } while (string.IsNullOrEmpty(shadowFile)); shadowFileInfo = new FileInfo(shadowFile); } //Dump the copy (the first 4MB, or less, depending on the file size and size of //the original file) long amountToCopy = Math.Min(stream.Length, Math.Min(4 * 1024 * 1024, shadowFileInfo.Length)); using (FileStream shadowFileStream = shadowFileInfo.OpenRead()) { while (stream.Position < amountToCopy) { byte[] buf = new byte[524288]; int bytesRead = shadowFileStream.Read(buf, 0, buf.Length); //Stop bothering if the input stream is at the end if (bytesRead == 0) break; //Dump the read contents onto the file to be deleted stream.Write(buf, 0, (int)Math.Min(bytesRead, amountToCopy - stream.Position)); } } } public override void EraseClusterTips(VolumeInfo info, IErasureMethod method, ClusterTipsSearchProgress searchCallback, ClusterTipsEraseProgress eraseCallback) { //List all the files which can be erased. List files = new List(); if (!info.IsMounted) throw new InvalidOperationException(S._("Could not erase cluster tips in {0} " + "as the volume is not mounted.", info.VolumeId)); ListFiles(info.MountPoints[0], files, searchCallback); //For every file, erase the cluster tips. for (int i = 0, j = files.Count; i != j; ++i) { //Get the file attributes for restoring later StreamInfo streamInfo = new StreamInfo(files[i]); if (!streamInfo.Exists) continue; try { EraseFileClusterTips(streamInfo, method); } catch (UnauthorizedAccessException) { Logger.Log(S._("{0} did not have its cluster tips erased because you do not " + "have the required permissions to erase the file cluster tips.", files[i]), LogLevel.Information); } catch (IOException e) { Logger.Log(S._("{0} did not have its cluster tips erased. The error returned " + "was: {1}", files[i], e.Message), LogLevel.Warning); } eraseCallback(i, files.Count, files[i]); } } private void ListFiles(DirectoryInfo info, List files, ClusterTipsSearchProgress searchCallback) { try { //Skip this directory if it is a reparse point if ((info.Attributes & FileAttributes.ReparsePoint) != 0) { Logger.Log(S._("Files in {0} did not have their cluster tips erased because " + "it is a hard link or a symbolic link.", info.FullName), LogLevel.Information); return; } foreach (FileInfo file in info.GetFiles()) if (file.IsProtectedSystemFile()) Logger.Log(S._("{0} did not have its cluster tips erased, because it is " + "a system file", file.FullName), LogLevel.Information); else if ((file.Attributes & FileAttributes.ReparsePoint) != 0) Logger.Log(S._("{0} did not have its cluster tips erased because it is a " + "hard link or a symbolic link.", file.FullName), LogLevel.Information); else if ((file.Attributes & FileAttributes.Compressed) != 0 || (file.Attributes & FileAttributes.Encrypted) != 0 || (file.Attributes & FileAttributes.SparseFile) != 0) { Logger.Log(S._("{0} did not have its cluster tips erased because it is " + "compressed, encrypted or a sparse file.", file.FullName), LogLevel.Information); } else { try { foreach (StreamInfo stream in file.GetADSes()) files.Add(stream.FullName); files.Add(file.FullName); } catch (UnauthorizedAccessException e) { Logger.Log(S._("{0} did not have its cluster tips erased because of " + "the following error: {1}", file.FullName, e.Message), LogLevel.Information); } catch (IOException e) { Logger.Log(S._("{0} did not have its cluster tips erased because of " + "the following error: {1}", file.FullName, e.Message), LogLevel.Warning); } } foreach (DirectoryInfo subDirInfo in info.GetDirectories()) { searchCallback(subDirInfo.FullName); ListFiles(subDirInfo, files, searchCallback); } } catch (UnauthorizedAccessException e) { Logger.Log(S._("Files in {0} did not have its cluster tips erased because of the " + "following error: {1}", info.FullName, e.Message), LogLevel.Information); } catch (IOException e) { Logger.Log(S._("Files in {0} did not have its cluster tips erased because of the " + "following error: {1}", info.FullName, e.Message), LogLevel.Warning); } } /// /// Erases the cluster tips of the given file. /// /// The stream to erase. /// The erasure method to use. private void EraseFileClusterTips(StreamInfo streamInfo, IErasureMethod method) { //Get the file access times DateTime lastAccess = streamInfo.LastAccessTime; DateTime lastWrite = streamInfo.LastWriteTime; DateTime created = streamInfo.CreationTime; //Get the file attributes FileAttributes attributes = streamInfo.Attributes; //And get the file lengths to know how much to overwrite long fileArea = GetFileArea(streamInfo); long fileLength = streamInfo.Length; //If the file length equals the file area there is no cluster tip to overwrite if (fileArea == fileLength) return; //Otherwise, unset any read-only flags, create the stream, lengthen the //file, then tell the erasure method to erase the cluster tips. try { streamInfo.Attributes = FileAttributes.Normal; FileStream stream = streamInfo.Open(FileMode.Open, FileAccess.Write, FileShare.None, FileOptions.WriteThrough); try { stream.SetLength(fileArea); stream.Seek(fileLength, SeekOrigin.Begin); //Erase the file method.Erase(stream, long.MaxValue, Host.Instance.Prngs.ActivePrng, null); } finally { //Make sure the file length is restored! stream.SetLength(fileLength); //Then destroy the stream stream.Close(); } } catch (ArgumentException e) { //This is an undocumented exception: when the path we are setting //cannot be accessed (ERROR_ACCESS_DENIED is returned) an //ArgumentException is raised (no idea why!) throw new UnauthorizedAccessException(e.Message, e); } finally { //Reset the file attributes if (streamInfo.Attributes != attributes) streamInfo.Attributes = attributes; //Reset the file times streamInfo.SetTimes(MinTimestamp, created, lastWrite, lastAccess); } } public override long GetFileArea(StreamInfo info) { VolumeInfo volume = VolumeInfo.FromMountPoint(info.Directory.FullName); long clusterSize = volume.ClusterSize; return (info.Length + (clusterSize - 1)) & ~(clusterSize - 1); } /// /// The minimum timestamp the file system can take. This is for secure file /// deletion. /// protected abstract DateTime MinTimestamp { get; } } }