diff --git a/docker/install_management_commands.sh b/docker/install_management_commands.sh index be972d605..f7a175e9e 100755 --- a/docker/install_management_commands.sh +++ b/docker/install_management_commands.sh @@ -4,8 +4,7 @@ set -eu -for command in decrypt_documents \ - document_archiver \ +for command in document_archiver \ document_exporter \ document_importer \ mail_fetcher \ diff --git a/docker/rootfs/usr/local/bin/decrypt_documents b/docker/rootfs/usr/local/bin/decrypt_documents deleted file mode 100755 index 4da1549ee..000000000 --- a/docker/rootfs/usr/local/bin/decrypt_documents +++ /dev/null @@ -1,14 +0,0 @@ -#!/command/with-contenv /usr/bin/bash -# shellcheck shell=bash - -set -e - -cd "${PAPERLESS_SRC_DIR}" - -if [[ $(id -u) == 0 ]]; then - s6-setuidgid paperless python3 manage.py decrypt_documents "$@" -elif [[ $(id -un) == "paperless" ]]; then - python3 manage.py decrypt_documents "$@" -else - echo "Unknown user." -fi diff --git a/docs/administration.md b/docs/administration.md index ddf51bf9a..2fb70a806 100644 --- a/docs/administration.md +++ b/docs/administration.md @@ -580,36 +580,6 @@ document. documents, such as encrypted PDF documents. The archiver will skip over these documents each time it sees them. -### Managing encryption {#encryption} - -!!! warning - - Encryption was removed in [paperless-ng 0.9](changelog.md#paperless-ng-090) - because it did not really provide any additional security, the passphrase - was stored in a configuration file on the same system as the documents. - Furthermore, the entire text content of the documents is stored plain in - the database, even if your documents are encrypted. Filenames are not - encrypted as well. Finally, the web server provides transparent access to - your encrypted documents. - - Consider running paperless on an encrypted filesystem instead, which - will then at least provide security against physical hardware theft. - -#### Enabling encryption - -Enabling encryption is no longer supported. - -#### Disabling encryption - -Basic usage to disable encryption of your document store: - -(Note: If `PAPERLESS_PASSPHRASE` isn't set already, you need to specify -it here) - -``` -decrypt_documents [--passphrase SECR3TP4SSPHRA$E] -``` - ### Detecting duplicates {#fuzzy_duplicate} Paperless already catches and prevents upload of exactly matching documents, diff --git a/docs/migration.md b/docs/migration.md index 2ef850cbe..1c934e6df 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -17,3 +17,9 @@ separating the directory ignore from the file ignore. | `CONSUMER_POLLING_RETRY_COUNT` | _Removed_ | Automatic with stability tracking | | `CONSUMER_IGNORE_PATTERNS` | [`CONSUMER_IGNORE_PATTERNS`](configuration.md#PAPERLESS_CONSUMER_IGNORE_PATTERNS) | **Now regex, not fnmatch**; user patterns are added to (not replacing) default ones | | _New_ | [`CONSUMER_IGNORE_DIRS`](configuration.md#PAPERLESS_CONSUMER_IGNORE_DIRS) | Additional directories to ignore; user entries are added to (not replacing) defaults | + +## Encryption Support + +Document and thumbnail encryption is no longer supported. This was previously deprecated in [paperless-ng 0.9.3](https://github.com/paperless-ngx/paperless-ngx/blob/dev/docs/changelog.md#paperless-ng-093) + +Users must decrypt their document using the `decrypt_documents` command before upgrading. diff --git a/src/documents/__init__.py b/src/documents/__init__.py index dd8c76d19..861c45185 100644 --- a/src/documents/__init__.py +++ b/src/documents/__init__.py @@ -1,5 +1,4 @@ # this is here so that django finds the checks. -from documents.checks import changed_password_check from documents.checks import parser_check -__all__ = ["changed_password_check", "parser_check"] +__all__ = ["parser_check"] diff --git a/src/documents/admin.py b/src/documents/admin.py index c6f179e2a..1ebbdc9ce 100644 --- a/src/documents/admin.py +++ b/src/documents/admin.py @@ -60,7 +60,6 @@ class DocumentAdmin(GuardedModelAdmin): "added", "modified", "mime_type", - "storage_type", "filename", "checksum", "archive_filename", diff --git a/src/documents/checks.py b/src/documents/checks.py index 8f8fbf4f9..b6e9e90fc 100644 --- a/src/documents/checks.py +++ b/src/documents/checks.py @@ -1,60 +1,12 @@ -import textwrap - from django.conf import settings from django.core.checks import Error from django.core.checks import Warning from django.core.checks import register -from django.core.exceptions import FieldError -from django.db.utils import OperationalError -from django.db.utils import ProgrammingError from documents.signals import document_consumer_declaration from documents.templating.utils import convert_format_str_to_template_format -@register() -def changed_password_check(app_configs, **kwargs): - from documents.models import Document - from paperless.db import GnuPG - - try: - encrypted_doc = ( - Document.objects.filter( - storage_type=Document.STORAGE_TYPE_GPG, - ) - .only("pk", "storage_type") - .first() - ) - except (OperationalError, ProgrammingError, FieldError): - return [] # No documents table yet - - if encrypted_doc: - if not settings.PASSPHRASE: - return [ - Error( - "The database contains encrypted documents but no password is set.", - ), - ] - - if not GnuPG.decrypted(encrypted_doc.source_file): - return [ - Error( - textwrap.dedent( - """ - The current password doesn't match the password of the - existing documents. - - If you intend to change your password, you must first export - all of the old documents, start fresh with the new password - and then re-import them." - """, - ), - ), - ] - - return [] - - @register() def parser_check(app_configs, **kwargs): parsers = [] diff --git a/src/documents/conditionals.py b/src/documents/conditionals.py index 47d9bfe4b..b93cabf62 100644 --- a/src/documents/conditionals.py +++ b/src/documents/conditionals.py @@ -128,7 +128,7 @@ def thumbnail_last_modified(request, pk: int) -> datetime | None: Cache should be (slightly?) faster than filesystem """ try: - doc = Document.objects.only("storage_type").get(pk=pk) + doc = Document.objects.only("pk").get(pk=pk) if not doc.thumbnail_path.exists(): return None doc_key = get_thumbnail_modified_key(pk) diff --git a/src/documents/consumer.py b/src/documents/consumer.py index 2c1cf025b..4c8c4dd28 100644 --- a/src/documents/consumer.py +++ b/src/documents/consumer.py @@ -497,7 +497,6 @@ class ConsumerPlugin( create_source_path_directory(document.source_path) self._write( - document.storage_type, self.unmodified_original if self.unmodified_original is not None else self.working_copy, @@ -505,7 +504,6 @@ class ConsumerPlugin( ) self._write( - document.storage_type, thumbnail, document.thumbnail_path, ) @@ -517,7 +515,6 @@ class ConsumerPlugin( ) create_source_path_directory(document.archive_path) self._write( - document.storage_type, archive_path, document.archive_path, ) @@ -637,8 +634,6 @@ class ConsumerPlugin( ) self.log.debug(f"Creation date from st_mtime: {create_date}") - storage_type = Document.STORAGE_TYPE_UNENCRYPTED - if self.metadata.filename: title = Path(self.metadata.filename).stem else: @@ -665,7 +660,6 @@ class ConsumerPlugin( checksum=hashlib.md5(file_for_checksum.read_bytes()).hexdigest(), created=create_date, modified=create_date, - storage_type=storage_type, page_count=page_count, original_filename=self.filename, ) @@ -736,7 +730,7 @@ class ConsumerPlugin( } CustomFieldInstance.objects.create(**args) # adds to document - def _write(self, storage_type, source, target): + def _write(self, source, target): with ( Path(source).open("rb") as read_file, Path(target).open("wb") as write_file, diff --git a/src/documents/file_handling.py b/src/documents/file_handling.py index 48cd57311..39831016d 100644 --- a/src/documents/file_handling.py +++ b/src/documents/file_handling.py @@ -126,7 +126,6 @@ def generate_filename( doc: Document, *, counter=0, - append_gpg=True, archive_filename=False, ) -> Path: base_path: Path | None = None @@ -170,8 +169,4 @@ def generate_filename( final_filename = f"{doc.pk:07}{counter_str}{filetype_str}" full_path = Path(final_filename) - # Add GPG extension if needed - if append_gpg and doc.storage_type == doc.STORAGE_TYPE_GPG: - full_path = full_path.with_suffix(full_path.suffix + ".gpg") - return full_path diff --git a/src/documents/management/commands/decrypt_documents.py b/src/documents/management/commands/decrypt_documents.py deleted file mode 100644 index 793cac4bb..000000000 --- a/src/documents/management/commands/decrypt_documents.py +++ /dev/null @@ -1,93 +0,0 @@ -from pathlib import Path - -from django.conf import settings -from django.core.management.base import BaseCommand -from django.core.management.base import CommandError - -from documents.models import Document -from paperless.db import GnuPG - - -class Command(BaseCommand): - help = ( - "This is how you migrate your stored documents from an encrypted " - "state to an unencrypted one (or vice-versa)" - ) - - def add_arguments(self, parser) -> None: - parser.add_argument( - "--passphrase", - help=( - "If PAPERLESS_PASSPHRASE isn't set already, you need to specify it here" - ), - ) - - def handle(self, *args, **options) -> None: - try: - self.stdout.write( - self.style.WARNING( - "\n\n" - "WARNING: This script is going to work directly on your " - "document originals, so\n" - "WARNING: you probably shouldn't run " - "this unless you've got a recent backup\n" - "WARNING: handy. It " - "*should* work without a hitch, but be safe and backup your\n" - "WARNING: stuff first.\n\n" - "Hit Ctrl+C to exit now, or Enter to " - "continue.\n\n", - ), - ) - _ = input() - except KeyboardInterrupt: - return - - passphrase = options["passphrase"] or settings.PASSPHRASE - if not passphrase: - raise CommandError( - "Passphrase not defined. Please set it with --passphrase or " - "by declaring it in your environment or your config.", - ) - - self.__gpg_to_unencrypted(passphrase) - - def __gpg_to_unencrypted(self, passphrase: str) -> None: - encrypted_files = Document.objects.filter( - storage_type=Document.STORAGE_TYPE_GPG, - ) - - for document in encrypted_files: - self.stdout.write(f"Decrypting {document}") - - old_paths = [document.source_path, document.thumbnail_path] - - with document.source_file as file_handle: - raw_document = GnuPG.decrypted(file_handle, passphrase) - with document.thumbnail_file as file_handle: - raw_thumb = GnuPG.decrypted(file_handle, passphrase) - - document.storage_type = Document.STORAGE_TYPE_UNENCRYPTED - - ext: str = Path(document.filename).suffix - - if not ext == ".gpg": - raise CommandError( - f"Abort: encrypted file {document.source_path} does not " - f"end with .gpg", - ) - - document.filename = Path(document.filename).stem - - with document.source_path.open("wb") as f: - f.write(raw_document) - - with document.thumbnail_path.open("wb") as f: - f.write(raw_thumb) - - Document.objects.filter(id=document.id).update( - storage_type=document.storage_type, - filename=document.filename, - ) - - for path in old_paths: - path.unlink() diff --git a/src/documents/management/commands/document_exporter.py b/src/documents/management/commands/document_exporter.py index 88daeddf5..77b3b6416 100644 --- a/src/documents/management/commands/document_exporter.py +++ b/src/documents/management/commands/document_exporter.py @@ -3,7 +3,6 @@ import json import os import shutil import tempfile -import time from pathlib import Path from typing import TYPE_CHECKING @@ -56,7 +55,6 @@ from documents.settings import EXPORTER_FILE_NAME from documents.settings import EXPORTER_THUMBNAIL_NAME from documents.utils import copy_file_with_basic_stats from paperless import version -from paperless.db import GnuPG from paperless.models import ApplicationConfiguration from paperless_mail.models import MailAccount from paperless_mail.models import MailRule @@ -316,20 +314,17 @@ class Command(CryptMixin, BaseCommand): total=len(document_manifest), disable=self.no_progress_bar, ): - # 3.1. store files unencrypted - document_dict["fields"]["storage_type"] = Document.STORAGE_TYPE_UNENCRYPTED - document = document_map[document_dict["pk"]] - # 3.2. generate a unique filename + # 3.1. generate a unique filename base_name = self.generate_base_name(document) - # 3.3. write filenames into manifest + # 3.2. write filenames into manifest original_target, thumbnail_target, archive_target = ( self.generate_document_targets(document, base_name, document_dict) ) - # 3.4. write files to target folder + # 3.3. write files to target folder if not self.data_only: self.copy_document_files( document, @@ -423,7 +418,6 @@ class Command(CryptMixin, BaseCommand): base_name = generate_filename( document, counter=filename_counter, - append_gpg=False, ) else: base_name = document.get_public_filename(counter=filename_counter) @@ -482,46 +476,24 @@ class Command(CryptMixin, BaseCommand): If the document is encrypted, the files are decrypted before copying them to the target location. """ - if document.storage_type == Document.STORAGE_TYPE_GPG: - t = int(time.mktime(document.created.timetuple())) + self.check_and_copy( + document.source_path, + document.checksum, + original_target, + ) - original_target.parent.mkdir(parents=True, exist_ok=True) - with document.source_file as out_file: - original_target.write_bytes(GnuPG.decrypted(out_file)) - os.utime(original_target, times=(t, t)) + if thumbnail_target: + self.check_and_copy(document.thumbnail_path, None, thumbnail_target) - if thumbnail_target: - thumbnail_target.parent.mkdir(parents=True, exist_ok=True) - with document.thumbnail_file as out_file: - thumbnail_target.write_bytes(GnuPG.decrypted(out_file)) - os.utime(thumbnail_target, times=(t, t)) - - if archive_target: - archive_target.parent.mkdir(parents=True, exist_ok=True) - if TYPE_CHECKING: - assert isinstance(document.archive_path, Path) - with document.archive_path as out_file: - archive_target.write_bytes(GnuPG.decrypted(out_file)) - os.utime(archive_target, times=(t, t)) - else: + if archive_target: + if TYPE_CHECKING: + assert isinstance(document.archive_path, Path) self.check_and_copy( - document.source_path, - document.checksum, - original_target, + document.archive_path, + document.archive_checksum, + archive_target, ) - if thumbnail_target: - self.check_and_copy(document.thumbnail_path, None, thumbnail_target) - - if archive_target: - if TYPE_CHECKING: - assert isinstance(document.archive_path, Path) - self.check_and_copy( - document.archive_path, - document.archive_checksum, - archive_target, - ) - def check_and_write_json( self, content: list[dict] | dict, diff --git a/src/documents/management/commands/document_importer.py b/src/documents/management/commands/document_importer.py index 3e614c6a6..ba3d793b3 100644 --- a/src/documents/management/commands/document_importer.py +++ b/src/documents/management/commands/document_importer.py @@ -383,8 +383,6 @@ class Command(CryptMixin, BaseCommand): else: archive_path = None - document.storage_type = Document.STORAGE_TYPE_UNENCRYPTED - with FileLock(settings.MEDIA_LOCK): if Path(document.source_path).is_file(): raise FileExistsError(document.source_path) diff --git a/src/documents/migrations/0004_remove_document_storage_type.py b/src/documents/migrations/0004_remove_document_storage_type.py new file mode 100644 index 000000000..e138d5d78 --- /dev/null +++ b/src/documents/migrations/0004_remove_document_storage_type.py @@ -0,0 +1,16 @@ +# Generated by Django 5.2.9 on 2026-01-24 23:05 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("documents", "0003_workflowaction_order"), + ] + + operations = [ + migrations.RemoveField( + model_name="document", + name="storage_type", + ), + ] diff --git a/src/documents/models.py b/src/documents/models.py index 372fafaf2..88d33f1fe 100644 --- a/src/documents/models.py +++ b/src/documents/models.py @@ -154,13 +154,6 @@ class StoragePath(MatchingModel): class Document(SoftDeleteModel, ModelWithOwner): - STORAGE_TYPE_UNENCRYPTED = "unencrypted" - STORAGE_TYPE_GPG = "gpg" - STORAGE_TYPES = ( - (STORAGE_TYPE_UNENCRYPTED, _("Unencrypted")), - (STORAGE_TYPE_GPG, _("Encrypted with GNU Privacy Guard")), - ) - correspondent = models.ForeignKey( Correspondent, blank=True, @@ -250,14 +243,6 @@ class Document(SoftDeleteModel, ModelWithOwner): db_index=True, ) - storage_type = models.CharField( - _("storage type"), - max_length=11, - choices=STORAGE_TYPES, - default=STORAGE_TYPE_UNENCRYPTED, - editable=False, - ) - added = models.DateTimeField( _("added"), default=timezone.now, @@ -353,12 +338,7 @@ class Document(SoftDeleteModel, ModelWithOwner): @property def source_path(self) -> Path: - if self.filename: - fname = str(self.filename) - else: - fname = f"{self.pk:07}{self.file_type}" - if self.storage_type == self.STORAGE_TYPE_GPG: - fname += ".gpg" # pragma: no cover + fname = str(self.filename) if self.filename else f"{self.pk:07}{self.file_type}" return (settings.ORIGINALS_DIR / Path(fname)).resolve() @@ -407,8 +387,6 @@ class Document(SoftDeleteModel, ModelWithOwner): @property def thumbnail_path(self) -> Path: webp_file_name = f"{self.pk:07}.webp" - if self.storage_type == self.STORAGE_TYPE_GPG: - webp_file_name += ".gpg" webp_file_path = settings.THUMBNAIL_DIR / Path(webp_file_name) diff --git a/src/documents/templating/filepath.py b/src/documents/templating/filepath.py index 805cefbdb..3647948ea 100644 --- a/src/documents/templating/filepath.py +++ b/src/documents/templating/filepath.py @@ -108,7 +108,6 @@ def create_dummy_document(): page_count=5, created=timezone.now(), modified=timezone.now(), - storage_type=Document.STORAGE_TYPE_UNENCRYPTED, added=timezone.now(), filename="/dummy/filename.pdf", archive_filename="/dummy/archive_filename.pdf", diff --git a/src/documents/tests/samples/documents/originals/0000004.pdf b/src/documents/tests/samples/documents/originals/0000004.pdf new file mode 100644 index 000000000..953bb88ab Binary files /dev/null and b/src/documents/tests/samples/documents/originals/0000004.pdf differ diff --git a/src/documents/tests/samples/documents/originals/0000004.pdf.gpg b/src/documents/tests/samples/documents/originals/0000004.pdf.gpg deleted file mode 100644 index 754efcbf6..000000000 Binary files a/src/documents/tests/samples/documents/originals/0000004.pdf.gpg and /dev/null differ diff --git a/src/documents/tests/samples/documents/thumbnails/0000004.webp b/src/documents/tests/samples/documents/thumbnails/0000004.webp new file mode 100644 index 000000000..a7ff623b2 Binary files /dev/null and b/src/documents/tests/samples/documents/thumbnails/0000004.webp differ diff --git a/src/documents/tests/samples/documents/thumbnails/0000004.webp.gpg b/src/documents/tests/samples/documents/thumbnails/0000004.webp.gpg deleted file mode 100644 index 3abc69d36..000000000 Binary files a/src/documents/tests/samples/documents/thumbnails/0000004.webp.gpg and /dev/null differ diff --git a/src/documents/tests/test_checks.py b/src/documents/tests/test_checks.py index 4af05746f..304074e37 100644 --- a/src/documents/tests/test_checks.py +++ b/src/documents/tests/test_checks.py @@ -1,4 +1,3 @@ -import textwrap from unittest import mock from django.core.checks import Error @@ -6,60 +5,11 @@ from django.core.checks import Warning from django.test import TestCase from django.test import override_settings -from documents.checks import changed_password_check from documents.checks import filename_format_check from documents.checks import parser_check -from documents.models import Document -from documents.tests.factories import DocumentFactory class TestDocumentChecks(TestCase): - def test_changed_password_check_empty_db(self): - self.assertListEqual(changed_password_check(None), []) - - def test_changed_password_check_no_encryption(self): - DocumentFactory.create(storage_type=Document.STORAGE_TYPE_UNENCRYPTED) - self.assertListEqual(changed_password_check(None), []) - - def test_encrypted_missing_passphrase(self): - DocumentFactory.create(storage_type=Document.STORAGE_TYPE_GPG) - msgs = changed_password_check(None) - self.assertEqual(len(msgs), 1) - msg_text = msgs[0].msg - self.assertEqual( - msg_text, - "The database contains encrypted documents but no password is set.", - ) - - @override_settings( - PASSPHRASE="test", - ) - @mock.patch("paperless.db.GnuPG.decrypted") - @mock.patch("documents.models.Document.source_file") - def test_encrypted_decrypt_fails(self, mock_decrypted, mock_source_file): - mock_decrypted.return_value = None - mock_source_file.return_value = b"" - - DocumentFactory.create(storage_type=Document.STORAGE_TYPE_GPG) - - msgs = changed_password_check(None) - - self.assertEqual(len(msgs), 1) - msg_text = msgs[0].msg - self.assertEqual( - msg_text, - textwrap.dedent( - """ - The current password doesn't match the password of the - existing documents. - - If you intend to change your password, you must first export - all of the old documents, start fresh with the new password - and then re-import them." - """, - ), - ) - def test_parser_check(self): self.assertEqual(parser_check(None), []) diff --git a/src/documents/tests/test_file_handling.py b/src/documents/tests/test_file_handling.py index befc7050f..f6764d3f8 100644 --- a/src/documents/tests/test_file_handling.py +++ b/src/documents/tests/test_file_handling.py @@ -34,22 +34,14 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase): def test_generate_source_filename(self): document = Document() document.mime_type = "application/pdf" - document.storage_type = Document.STORAGE_TYPE_UNENCRYPTED document.save() self.assertEqual(generate_filename(document), Path(f"{document.pk:07d}.pdf")) - document.storage_type = Document.STORAGE_TYPE_GPG - self.assertEqual( - generate_filename(document), - Path(f"{document.pk:07d}.pdf.gpg"), - ) - @override_settings(FILENAME_FORMAT="{correspondent}/{correspondent}") def test_file_renaming(self): document = Document() document.mime_type = "application/pdf" - document.storage_type = Document.STORAGE_TYPE_UNENCRYPTED document.save() # Test default source_path @@ -63,11 +55,6 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase): # Ensure that filename is properly generated self.assertEqual(document.filename, Path("none/none.pdf")) - # Enable encryption and check again - document.storage_type = Document.STORAGE_TYPE_GPG - document.filename = generate_filename(document) - self.assertEqual(document.filename, Path("none/none.pdf.gpg")) - document.save() # test that creating dirs for the source_path creates the correct directory @@ -87,14 +74,14 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase): settings.ORIGINALS_DIR / "none", ) self.assertIsFile( - settings.ORIGINALS_DIR / "test" / "test.pdf.gpg", + settings.ORIGINALS_DIR / "test" / "test.pdf", ) @override_settings(FILENAME_FORMAT="{correspondent}/{correspondent}") def test_file_renaming_missing_permissions(self): document = Document() document.mime_type = "application/pdf" - document.storage_type = Document.STORAGE_TYPE_UNENCRYPTED + document.save() # Ensure that filename is properly generated @@ -128,14 +115,13 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase): def test_file_renaming_database_error(self): Document.objects.create( mime_type="application/pdf", - storage_type=Document.STORAGE_TYPE_UNENCRYPTED, checksum="AAAAA", ) document = Document() document.mime_type = "application/pdf" document.checksum = "BBBBB" - document.storage_type = Document.STORAGE_TYPE_UNENCRYPTED + document.save() # Ensure that filename is properly generated @@ -170,7 +156,7 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase): def test_document_delete(self): document = Document() document.mime_type = "application/pdf" - document.storage_type = Document.STORAGE_TYPE_UNENCRYPTED + document.save() # Ensure that filename is properly generated @@ -196,7 +182,7 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase): def test_document_delete_trash_dir(self): document = Document() document.mime_type = "application/pdf" - document.storage_type = Document.STORAGE_TYPE_UNENCRYPTED + document.save() # Ensure that filename is properly generated @@ -221,7 +207,7 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase): # Create an identical document and ensure it is trashed under a new name document = Document() document.mime_type = "application/pdf" - document.storage_type = Document.STORAGE_TYPE_UNENCRYPTED + document.save() document.filename = generate_filename(document) document.save() @@ -235,7 +221,7 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase): def test_document_delete_nofile(self): document = Document() document.mime_type = "application/pdf" - document.storage_type = Document.STORAGE_TYPE_UNENCRYPTED + document.save() document.delete() @@ -245,7 +231,7 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase): def test_directory_not_empty(self): document = Document() document.mime_type = "application/pdf" - document.storage_type = Document.STORAGE_TYPE_UNENCRYPTED + document.save() # Ensure that filename is properly generated @@ -362,7 +348,7 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase): def test_nested_directory_cleanup(self): document = Document() document.mime_type = "application/pdf" - document.storage_type = Document.STORAGE_TYPE_UNENCRYPTED + document.save() # Ensure that filename is properly generated @@ -390,7 +376,6 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase): document = Document() document.pk = 1 document.mime_type = "application/pdf" - document.storage_type = Document.STORAGE_TYPE_UNENCRYPTED self.assertEqual(generate_filename(document), Path("0000001.pdf")) @@ -403,7 +388,6 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase): document = Document() document.pk = 1 document.mime_type = "application/pdf" - document.storage_type = Document.STORAGE_TYPE_UNENCRYPTED self.assertEqual(generate_filename(document), Path("0000001.pdf")) @@ -429,7 +413,6 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase): document = Document() document.pk = 1 document.mime_type = "application/pdf" - document.storage_type = Document.STORAGE_TYPE_UNENCRYPTED self.assertEqual(generate_filename(document), Path("0000001.pdf")) @@ -438,7 +421,6 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase): document = Document() document.pk = 1 document.mime_type = "application/pdf" - document.storage_type = Document.STORAGE_TYPE_UNENCRYPTED self.assertEqual(generate_filename(document), Path("0000001.pdf")) @@ -1258,7 +1240,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase): title="doc1", mime_type="application/pdf", ) - document.storage_type = Document.STORAGE_TYPE_UNENCRYPTED + document.save() # Ensure that filename is properly generated @@ -1732,7 +1714,6 @@ class TestPathDateLocalization: document = DocumentFactory.create( title="My Document", mime_type="application/pdf", - storage_type=Document.STORAGE_TYPE_UNENCRYPTED, created=self.TEST_DATE, # 2023-10-26 (which is a Thursday) ) with override_settings(FILENAME_FORMAT=filename_format): diff --git a/src/documents/tests/test_management.py b/src/documents/tests/test_management.py index 014f5d673..e1b88633c 100644 --- a/src/documents/tests/test_management.py +++ b/src/documents/tests/test_management.py @@ -1,7 +1,5 @@ import filecmp -import hashlib import shutil -import tempfile from io import StringIO from pathlib import Path from unittest import mock @@ -96,66 +94,6 @@ class TestArchiver(DirectoriesMixin, FileSystemAssertsMixin, TestCase): self.assertEqual(doc2.archive_filename, "document_01.pdf") -class TestDecryptDocuments(FileSystemAssertsMixin, TestCase): - @mock.patch("documents.management.commands.decrypt_documents.input") - def test_decrypt(self, m): - media_dir = tempfile.mkdtemp() - originals_dir = Path(media_dir) / "documents" / "originals" - thumb_dir = Path(media_dir) / "documents" / "thumbnails" - originals_dir.mkdir(parents=True, exist_ok=True) - thumb_dir.mkdir(parents=True, exist_ok=True) - - with override_settings( - ORIGINALS_DIR=originals_dir, - THUMBNAIL_DIR=thumb_dir, - PASSPHRASE="test", - FILENAME_FORMAT=None, - ): - doc = Document.objects.create( - checksum="82186aaa94f0b98697d704b90fd1c072", - title="wow", - filename="0000004.pdf.gpg", - mime_type="application/pdf", - storage_type=Document.STORAGE_TYPE_GPG, - ) - - shutil.copy( - ( - Path(__file__).parent - / "samples" - / "documents" - / "originals" - / "0000004.pdf.gpg" - ), - originals_dir / "0000004.pdf.gpg", - ) - shutil.copy( - ( - Path(__file__).parent - / "samples" - / "documents" - / "thumbnails" - / "0000004.webp.gpg" - ), - thumb_dir / f"{doc.id:07}.webp.gpg", - ) - - call_command("decrypt_documents") - - doc.refresh_from_db() - - self.assertEqual(doc.storage_type, Document.STORAGE_TYPE_UNENCRYPTED) - self.assertEqual(doc.filename, "0000004.pdf") - self.assertIsFile(Path(originals_dir) / "0000004.pdf") - self.assertIsFile(doc.source_path) - self.assertIsFile(Path(thumb_dir) / f"{doc.id:07}.webp") - self.assertIsFile(doc.thumbnail_path) - - with doc.source_file as f: - checksum: str = hashlib.md5(f.read()).hexdigest() - self.assertEqual(checksum, doc.checksum) - - class TestMakeIndex(TestCase): @mock.patch("documents.management.commands.document_index.index_reindex") def test_reindex(self, m): diff --git a/src/documents/tests/test_management_exporter.py b/src/documents/tests/test_management_exporter.py index b01b8d47e..81262779a 100644 --- a/src/documents/tests/test_management_exporter.py +++ b/src/documents/tests/test_management_exporter.py @@ -86,9 +86,8 @@ class TestExportImport( content="Content", checksum="82186aaa94f0b98697d704b90fd1c072", title="wow_dec", - filename="0000004.pdf.gpg", + filename="0000004.pdf", mime_type="application/pdf", - storage_type=Document.STORAGE_TYPE_GPG, ) self.note = Note.objects.create( @@ -242,11 +241,6 @@ class TestExportImport( checksum = hashlib.md5(f.read()).hexdigest() self.assertEqual(checksum, element["fields"]["checksum"]) - self.assertEqual( - element["fields"]["storage_type"], - Document.STORAGE_TYPE_UNENCRYPTED, - ) - if document_exporter.EXPORTER_ARCHIVE_NAME in element: fname = ( self.target / element[document_exporter.EXPORTER_ARCHIVE_NAME] @@ -436,7 +430,7 @@ class TestExportImport( Document.objects.create( checksum="AAAAAAAAAAAAAAAAA", title="wow", - filename="0000004.pdf", + filename="0000010.pdf", mime_type="application/pdf", ) self.assertRaises(FileNotFoundError, call_command, "document_exporter", target) diff --git a/src/documents/views.py b/src/documents/views.py index 730a6dc1a..96b1f50b0 100644 --- a/src/documents/views.py +++ b/src/documents/views.py @@ -195,7 +195,6 @@ from paperless import version from paperless.celery import app as celery_app from paperless.config import AIConfig from paperless.config import GeneralConfig -from paperless.db import GnuPG from paperless.models import ApplicationConfiguration from paperless.serialisers import GroupSerializer from paperless.serialisers import UserSerializer @@ -1071,10 +1070,8 @@ class DocumentViewSet( doc, ): return HttpResponseForbidden("Insufficient permissions") - if doc.storage_type == Document.STORAGE_TYPE_GPG: - handle = GnuPG.decrypted(doc.thumbnail_file) - else: - handle = doc.thumbnail_file + + handle = doc.thumbnail_file return HttpResponse(handle, content_type="image/webp") except (FileNotFoundError, Document.DoesNotExist): @@ -2824,9 +2821,6 @@ def serve_file(*, doc: Document, use_archive: bool, disposition: str): if mime_type in {"application/csv", "text/csv"} and disposition == "inline": mime_type = "text/plain" - if doc.storage_type == Document.STORAGE_TYPE_GPG: - file_handle = GnuPG.decrypted(file_handle) - response = HttpResponse(file_handle, content_type=mime_type) # Firefox is not able to handle unicode characters in filename field # RFC 5987 addresses this issue diff --git a/src/paperless/db.py b/src/paperless/db.py deleted file mode 100644 index 286ccb094..000000000 --- a/src/paperless/db.py +++ /dev/null @@ -1,17 +0,0 @@ -import gnupg -from django.conf import settings - - -class GnuPG: - """ - A handy singleton to use when handling encrypted files. - """ - - gpg = gnupg.GPG(gnupghome=settings.GNUPG_HOME) - - @classmethod - def decrypted(cls, file_handle, passphrase=None): - if not passphrase: - passphrase = settings.PASSPHRASE - - return cls.gpg.decrypt_file(file_handle, passphrase=passphrase).data diff --git a/src/paperless/settings.py b/src/paperless/settings.py index 024c7f076..30ee213d1 100644 --- a/src/paperless/settings.py +++ b/src/paperless/settings.py @@ -1203,19 +1203,6 @@ EMAIL_PARSE_DEFAULT_LAYOUT = __get_int( 1, # MailRule.PdfLayout.TEXT_HTML but that can't be imported here ) -# Pre-2.x versions of Paperless stored your documents locally with GPG -# encryption, but that is no longer the default. This behaviour is still -# available, but it must be explicitly enabled by setting -# `PAPERLESS_PASSPHRASE` in your environment or config file. The default is to -# store these files unencrypted. -# -# Translation: -# * If you're a new user, you can safely ignore this setting. -# * If you're upgrading from 1.x, this must be set, OR you can run -# `./manage.py change_storage_type gpg unencrypted` to decrypt your files, -# after which you can unset this value. -PASSPHRASE = os.getenv("PAPERLESS_PASSPHRASE") - # Trigger a script after every successful document consumption? PRE_CONSUME_SCRIPT = os.getenv("PAPERLESS_PRE_CONSUME_SCRIPT") POST_CONSUME_SCRIPT = os.getenv("PAPERLESS_POST_CONSUME_SCRIPT")