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


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

И кажется, в этом проблемы нет. В стандартной библиотеке 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.

Оставить комментарий