Как стать автором
Обновить

Затирание файлов в Java

Время на прочтение5 мин
Количество просмотров12K

При разработке проекта постала необходимость удалять файлы, созданные приложением во время своего выполнения. Но требовалось, чтобы файлы удалялись не по завершению сеанса, работы ПК, а по требованию пользователя.

И кажется, в этом проблемы нет. В стандартной библиотеке Java есть метод delete() в классе java.io.File для удаления файла.

File file = new File("path/to/file");

if (file.delete()) {  
	System.out.println(file.getName() + " deleted");   
} else {
	System.out.println(file.getName() + " not deleted");   
} 

Метод delete() в классе java.io.File вызывает под капотом нативную функцию для удаления файла в зависимости от ОС. А современные ОС при удалении файла сразу не удаляют файл, а только удаляют имя файла. Содержимое файла остается, и память занимаемая под уже удаленный файл может быть в будущем переиспользована. Но все таки некоторое время кажется уже удаленный файл является доступный.

И если посмотреть на просторы интернета, то имеется немало программ для восстановления удаленных файлов, например Recuva.

Но хочется чтобы файлы были удалены без возможности востановления их в будущем. Начав искать в интернете, оказывается удаление файла без восстановление (затирание) очень не тривиальная задача. И при реализации такой задачи требуется учитывать особенности работы с файлами в определенной ОС. И при этом нужно для этого использовать либо вручную написанное нативное API или какую-то нативную библиотеку.

Поскольку приложения разрабатовалось в Ubuntu, то эта ОС предоставляет немало готовых решений в виде утилит командной строки. Например, утилита secure-delete, которая позволяет удалять файлы без востановления используя разные подходы.

$ srm -vz private/*

Приложения должно проверять установлена ли утилита и выводить ошибку если не находит. Но в случае использованием приложения для другой ОС, то нужно использовать подобную утилиту.

Что очень не удобно и хочется уйти от этих проблем. Если посмотреть исходный код утилиты secure-delete, то она позволяет работать под разные операционные системы. Написана на С99 и использует разную препроцессорную магию и платформо-зависимый API. Отлаживать такой нативный код в случае ошибки очень сложно и еще та задача.

Если разобраться как работает утилита secure-delete, то можно выделить следующие этапы.

  • сначала проверяется существует ли файл и корректность прав.
  • в зависимости от указаного алгоритма перезаписывает содержимое файла.
  • сокращает размер файла к нуль байтам.
  • переименовует файл рандомной последовательностю символов.
  • удаляет файл.

secure-delete позволяет разными алгоритми перезаписывать содержимое файла:

  • Simple алгоритм — перезаписывает 1 проходом 0x00 байтами.
  • DOE алгоритм — перезаписывает 3 проходами random, random, «DoE».
  • RCMP алгоритм — перезаписывает 3 проходами 0x00 ,0xFF, «RCMP».
  • OPENBSD алгоритм — перезаписывает 3 проходами 0xFF, 0x00, 0xFF байтами.
  • DOD алгоритм — перезаписывает 7 проходами.
  • Gutmann алгоритм — перезаписывает 35 проходами.

Хотелось бы чтобы код был платформо-независимым и работал под разные операционные системы. Если посмотреть на современный С++, то все этапы которые secure-delete проделывает для затирания файлов можно осуществить.

Для того чтобы проверить существует ли файл и имеет ли он корректные права можно использовать std::filesystem, которая была добавлена в C++17.

Для предыдущих версий стандарта можно использовать boost::filesystem.

namespace fs = std::filesystem;
	
if (!fs::exists(file)) {
	env->ThrowNew(exception_class, "File doesn't exist");
}
	
if (!fs::is_regular_file(file)) {
	env->ThrowNew(exception_class, "Path doesn't regular file or symlink");
}

if (!eraser.check_permision(file, fs::status(file).permissions())) {
	env->ThrowNew(exception_class, "File hasn't enough permision (maybe not user)");
}
	
bool kl::erase_content::check_permision(const fs::path& entry, fs::perms permision) {
	try {
		fs::permissions(entry, fs::perms::owner_read | fs::perms::owner_write,
					fs::perm_options::add);
		return true;
	} catch (fs::filesystem_error& e) {
		return false;
	}
}

Для перезаписывания содержимого файла в зависимости от выбраного алгортма можно оставить реализацию, как в secure-delete.

bool kl::erase_content::overwrite() {
	switch (entry.get_mode()) {
	case kl::overwrite_mode::SIMPLE_MODE:
		if (!overwrite_byte(1, 0x00)) { return false; }
		break;
	case kl::overwrite_mode::DOE_MODE:
		if (!overwrite_random(1)) { return false; }
		if (!overwrite_random(2)) { return false; }
	        if (!overwrite_bytes(3, "DoE")) { return false; }
		break;
	case kl::overwrite_mode::OPENBSD_MODE:
		/* override OPENBSD_MODE method */
		break;
	case kl::overwrite_mode::RCMP_MODE:
		/* override RCMP_MODE method */
		break;
	case kl::overwrite_mode::DOD_MODE:
		/* override DOD_MODE method */
		break;
	case kl::overwrite_mode::GUTMAN_MODE:
		/* override GUTMAN_MODE method */
		break;
	default:
		std::cerr << "overwrite mode doesn't choose" << std::endl;
	}

	return true;
}	

Заполняется буфер определеного размера, определеным набором данных, в зависимости от алгоритма и записывает это буфер в файл, пока не достигнет конца.

bool kl::erase_content::overwrite_byte(const int pass, const uint8_t byte) {
	const auto& [file_name, file_size, buffer_size, mode] = entry;

	this->buffer = std::make_unique<uint8_t[]>(buffer_size);
	std::memset(buffer.get(), byte, buffer_size);

	this->file = kl::fs_util::make_open_file(file_name, "r+b");

	if (!overwrite_data(pass)) {
		return false;
	}

	return true;
}
	
bool kl::erase_content::overwrite_data(const int pass) {
	const auto& [file_name, file_size, buffer_size, mode] = entry;

	const size_t count = file_size / buffer_size;
	const size_t tail  = file_size % buffer_size;
	size_t writted = 0;		

	if (fseek(file.get(), 0, SEEK_SET) != 0) {
		std::cerr << "couldn't seek in file" << std::endl;
		return false;
	}

	writted = write_buffer(count, tail);

	if (writted != file_size) {
		std::cerr << "couldn't write buffer in file" << std::endl;
		return false;
	}

	fflush(file.get());

	if (fseek(file.get(), 0, SEEK_SET) != 0) {
		std::cerr << "couldn't seek in file" << std::endl;
		return false;
	}

	file.reset();

	return true;
}

Потом сократим размер файла к нуль байтам, используя для этого функцию std::filesystem::resize_file().

try {
	fs::resize_file(file, 0);
} catch (fs::filesystem_error& e) {
	env->ThrowNew(exception_class, "truncate file fail");
}

Следующим этапом переименовуем файл рандомной последовательностю символов, используя для этого std::random() и std::filesystem::file::replace_filename().

std::string parent_path = file.parent_path();
std::string file_name   = file.filename();
fs::path copy_file = file;

file_name = random_text(file_name.size());
copy_file.replace_filename(fs::path(file_name));

try {
	fs::rename(file, copy_file);
} catch (fs::filesystem_error& e) {
	env->ThrowNew(exception_class, "can't rename file");
}

return true;

И на завершающем этапе нужно просто удалить файл, используя для этого std::filesystem::remove().

try {
	fs::remove(copy_file);
} catch (fs::filesystem_error& e) {
	env->ThrowNew(exception_class, "can't remove file");
}

Ну и для использования на Java нужно объявить нативные методы.

public class EraseFS {
	static {
		System.loadLibrary("jefl"); 
	}
		
	public static native boolean eraseFile(String path) throws EraseException;
	public static native boolean eraseFile(String path, OverwrideMode mode) throws EraseException;
		
	public static native boolean eraseFiles(String... paths) throws EraseException;
	public static native boolean eraseFiles(OverwrideMode mode, String... paths) throws EraseException;
		
	public static native boolean eraseDirectory(String path, boolean recursived) throws EraseException;
		
	public static native boolean eraseDirectory(String path, OverwrideMode mode, boolean recursived) throws EraseException;
}

Нативная реализация, использует только стандартную библиотеку С++, что позволяет легко портировать на другие платформы. И главное нету никакой препроцессорной макросной магии, которую не так легко отлаживать в случае ошибок.

Стандарт С++17 уже поддерживают все популярные компиляторы: MVSC, Clang, GCC.
Полный исходной код можно посмотреть на github: code.
Теги:
Хабы:
Всего голосов 8: ↑3 и ↓5+2
Комментарии33

Публикации

Истории

Работа

QT разработчик
4 вакансии
Java разработчик
347 вакансий
Программист C++
107 вакансий

Ближайшие события

7 – 8 ноября
Конференция byteoilgas_conf 2024
МоскваОнлайн
7 – 8 ноября
Конференция «Матемаркетинг»
МоскваОнлайн
15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань