Затирание файлов в 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.
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 33

      +1
      Заголовок вводит в заблуждение. Если сопоставить его с содержанием статьи, может создаться впечатление, что и в общем случае мы обычно удаляем файлы некорректно.
        +1
        Совершенно согласен. Речь тут совсем не про «корректность», как ее обычно понимают.
          +2
          Согласен, лучше переименовать «Затирание файлов в Java»
          +4
          Совсем не уверен, что в случае SSD контроллер действительно будет записывать байты физически в те же ячейки при многократной перезаписи, а в таком случае затея теряет смысл.
            –1
            Приложения писалося для ПК, и для удаления/затирания подошло очень хорошо. Если есть какие-то замечания по коду, буду очень рад замечанием. Можете писать issues на github.
            code
              +5
              SSD может стоять как в ПК, так и в лептопах.
              Имелось в виду, что перезаписывание данных на SSD — совершенно бесполезная затея, так как организация хранения данных на физическом уровне в корне отличается от таковой для HDD.
              Грубо говоря, если мы хотим программно перезаписать некие данные в ячейке А, то новые данные могут попасть в ячейку Б, а ячейка А будет помечена для очистки командой TRIM и физически данные будут удалены в случайный момент времени по команде ОС или контроллера.

              PS Но если через api OS прочитать «перезаписанные» сектора — то мы получим новые данные, а не старые, так что минимальный смысл тут все таки есть. Но все равно, физически старые данные на SSD не удаляются по команде «перезаписать вот эти байты»
            +4
            Все это прекрасно, но — увы — бессмысленно. Нет никакой гарантии, что при затирании файла вы затираете именно старые данные на диске. К примеру, если файловая система поддерживает версионность или CoW, то вы будете старательно каждый из 35 раз писать в новое место.
              –2
              Можно проверить той же Recuva, до и после затирания файлов в директории.
                +1
                В процессе работы вашей программы операционная система делает снапшот файловой системы, или lvm snapshot. Все, ваша куча методов затирания бессмысленна.
                +1
                Все эти утилиты «секьюрного удаление по военным алгоритмам» — это вообще такой фан-сервис для домашних параноиков. Гарантию может обеспечить только затирание накопителя целиком.
                  0
                  Вы не будете каждый час или каждый день затирать весь диск.
                    0
                    Если мне нужна безопасность, а не её иллюзия — да, придётся.
                      +2

                      Надо сменять парадигму. Самое простое — писать на диск шифрованный файл. Вопрос только в хранение ключей доступа. Но это уже существенно проще, чем пытаться стирать файлы (примеры, когда это не работает уже приводились — CoW ФС, SSD накопители, т.е. практически ВСЕГДА гарантированно файл удалить нельзя).

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

                  Для того, чтобы использовать std::filesystem — можно, но совершенно не нужно. Все необходимые проверки делает операционная система при открытии файла. Нельзя открыть на запись несуществующий файл или если у процесса нет прав в него писать.


                  Проверку на символическую ссылку тоже следует делать во время открытия файла (для этого есть специальные флажки — увы, не в std). Делать проверку до или после по пути файла — типичный пример ошибки TOCTOU.

                    0
                    Для проверки в fs::exists(std::path), нужен объект std::path. То есть на момент проверки, файл еще не открыт (например через fopen()), а выполняется проверка пути к файлу. Открытие файла осуществляется в момент перезатирания файла.

                    bool kl::erase_content::overwrite_byte(const int pass, const uint8_t byte) {
                    	...
                    	this->file = kl::fs_util::make_open_file(file_name, "r+b");
                    
                    	if (!overwrite_data(pass)) {
                    		return false;
                    	}
                    
                    	return true;
                    }
                    
                      +1
                      То есть на момент проверки, файл еще не открыт

                      Вот я и говорю, что в этом недостаток. std::path — это просто строка с путём. Между моментом проверок и моментом собственно открытия файловая система может измениться. Представьте, что когда выполняется проверка — файл есть и это не символическая ссылка, а когда его надо открыть — туда подсунута символическая ссылка на /dev/null. Проверка в итоге бесполезна.

                    +1
                    А почему бы просто не шифровать данные на лету в эти временные файлы? На производительности это почти не скажется и решает проблему/задачу скрытия данных.
                      0
                      Как вариант можно и шифровать. Многие использует поход шифрования.
                        +1

                        Да, полностью поддерживаю.
                        Вопросы, которые остаются:


                        1. алгоритм шифрования. При активной работе — может влиять на быстродействие, причем значительно. Но есть же более простые алгоритмы, которые аппаратно ускорены (AES-128?)
                        2. вопрос хранения ключей шифровки-дешифровки.
                      +1
                      создавайте файлы в RAM-диске, если не хотите оставлять следов
                        +1
                        А он точно в своп не отсвопится?
                          0

                          От данных в свопе ничего, наверное, не спасет. И ничего не спасет от прямого лампа расшифрованных данных из ОЗУ. Надо просто проработать модель рисков — от кого защищаемся

                        +1
                        От данных в свопе ничего, наверное, не спасет.

                        mlock(2) спасает. Данные в памяти можно надёжно спрятать от других процессов с помощью prctl(2) (PR_SET_DUMPABLE). На других системах есть свои аналоги. Остаётся доверие ядру ОС. Ну и rowhammer, а также физический доступ к машине. Но в целом оперативная память процесса — это самое безопасное место, после какого-нибудь TPM.

                          +5

                          Причём тут Java, если почти всё написали на C++? На Java точно так же можно свободно в файл что угодно писать, хоть через FileOutputStream, хоть через NIO file channels.

                            0
                            Сначала в проекте решено было использовать утилиту secure-delete. Но нужно установить и проверить где установлена. Что не очень хорошо.
                            Потом подумали чтобы через JNI привязать код secure-delete. И так и сделали.
                            Но когда нужно было отлаживать, вот тогда стало плохо. Потому что в secure-delete очень много макросов.
                            Потом посмотрев на современный С++ решили реализовать на чистом С++.
                              +2

                              Все равно непонятно. Записать нули или что-то рандомное/закриптованное в файл — тривиальная процедура на Джаве. Зачем же тащить в проект совершенно не нужный С++ код и лишний бинарник? Все как-то из пальца высосано.

                            0
                            this->file = kl::fs_util::make_open_file(file_name, «r+b»);
                            Если собираемся затирать файл почему «r+b»?

                            Но хочется чтобы файлы были удалены без возможности востановления их в будущем.
                            Для это следует сразу писать шифрованные файлы. И потом просто терять ключ.
                              0

                              https://habr.com/ru/company/wrike/blog/475854/ — 2й доклад послушайте

                                0
                                Еще надежнее написать драйвер ядра File System MiniFilter, В нем можно помечать файлы созданные именно вашим процессом и не давать другим процессам с ними что-то делать.
                                  +1

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

                                    0

                                    Не просто криптованные, а еще и экономящие место на диске через сжатие.

                                      0
                                      это все тоже делается через подобный драйвер
                                  0
                                  А еще можно вспомнить сетевые файловые системы и разные виды RAID построений.

                                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                  Самое читаемое