From d0032c18be0a4e8e4275e4f5a00e8d06fdc5f55d Mon Sep 17 00:00:00 2001 From: Trenton H <797416+stumpylog@users.noreply.github.com> Date: Sat, 24 Jan 2026 19:29:54 -0800 Subject: [PATCH] Breaking: Remove support for document and thumbnail encryption (#11850) --- docker/install_management_commands.sh | 3 +- docker/rootfs/usr/local/bin/decrypt_documents | 14 --- docs/administration.md | 30 ------ docs/migration.md | 6 ++ src/documents/__init__.py | 3 +- src/documents/admin.py | 1 - src/documents/checks.py | 48 --------- src/documents/conditionals.py | 2 +- src/documents/consumer.py | 8 +- src/documents/file_handling.py | 5 - .../management/commands/decrypt_documents.py | 93 ------------------ .../management/commands/document_exporter.py | 60 +++-------- .../management/commands/document_importer.py | 2 - .../0004_remove_document_storage_type.py | 16 +++ src/documents/models.py | 24 +---- src/documents/templating/filepath.py | 1 - .../samples/documents/originals/0000004.pdf | Bin 0 -> 23578 bytes .../documents/originals/0000004.pdf.gpg | Bin 17779 -> 0 bytes .../samples/documents/thumbnails/0000004.webp | Bin 0 -> 2624 bytes .../documents/thumbnails/0000004.webp.gpg | Bin 2712 -> 0 bytes src/documents/tests/test_checks.py | 50 ---------- src/documents/tests/test_file_handling.py | 39 ++------ src/documents/tests/test_management.py | 62 ------------ .../tests/test_management_exporter.py | 10 +- src/documents/views.py | 10 +- src/paperless/db.py | 17 ---- src/paperless/settings.py | 13 --- 27 files changed, 57 insertions(+), 460 deletions(-) delete mode 100755 docker/rootfs/usr/local/bin/decrypt_documents delete mode 100644 src/documents/management/commands/decrypt_documents.py create mode 100644 src/documents/migrations/0004_remove_document_storage_type.py create mode 100644 src/documents/tests/samples/documents/originals/0000004.pdf delete mode 100644 src/documents/tests/samples/documents/originals/0000004.pdf.gpg create mode 100644 src/documents/tests/samples/documents/thumbnails/0000004.webp delete mode 100644 src/documents/tests/samples/documents/thumbnails/0000004.webp.gpg delete mode 100644 src/paperless/db.py 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 0000000000000000000000000000000000000000..953bb88ab8acd1b69f9d0e2963e2e7e64933f4f7 GIT binary patch literal 23578 zcmagG19T2vP6=e+;j_ud}6#zNK7 zx8}E2Rn0kyOhH79mXVGPK-OQ}-_if8KM%k}$UtasWCh^ip_ehWGk38dWd6!gq8GEY zaWQqG7qc;RF%>a2wl^{5;{!OmIGGyS0z9%+<;MX*42T^^)DJoe?#jlfLOoCZ=sXgj zyvg{*i?dPFgm;fP1W=}i=BXJgri`Rd^_1>&-pBYT>7@n)0zHVH0BFT@Nsx@{bZ z{{+=dG~H6WGwvm7G}gfiu=&+w?bS4IM&k^LK2*}UcdRvG6%<5B?se3}LjWhfdARF# z`x61OETDwqM5-RkiACdB;2VwkC9&$J@*e@4y-aKl0fBH@k5BAMEm9c4H)DWaMCE`%fz! zczSE#Eaev}*ID zi@-Umi@0rJzp%@qx9!X#dbr?Pu@SD9eSCN*m9$G@v;9N+O4j*)VcX%TymX#6wEJ;5 zA(0x9z(U7Bt>CPEFy0PEgf7}p&(RVV?!GJMlY1_a;A{EwrRc@ zc|}Y#@rf@lBg1?B$gV>w;u?y0o)IxhviIQL9p~~r8F8+{f)v%R#&23Uya?CTCYqQy zp0jY!5g~R-q-?g?N4cp0Z5SH&QD$gZ=(48Peea13w>Ar~k*HDo$H?IgIJWgj|HuDdu>X|

6T)g=R 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 K)LVU4SZMNPYqQP( 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 0000000000000000000000000000000000000000..a7ff623b24a24c1490dec1742efb28ce7da545d1 GIT binary patch literal 2624 zcmV-G3cvMINk&FE3IG6CMM6+kP&gng3IG7`KLDKpD)a%406vjOp-UyCqoJiWn24|v z32AQObfFvbd-1RJp4C6aep2<=`Fq-~SUo}JSMqgTjulSz% zUf4Ns^#b*m>VNIOzxe6uiQvCr59Z(1y)N&WpeN~HR$ub`n0;yNBmCq0xBTH$)_p+u zH5g^;>IcE7!!K7*J`F}0db)w|YB0;y)DDcpile-!BGTR)&60>d2p)3sf6InmuAqDw z(zfHF^ZC$xro3zu6@{+aHz-mtsTgJcd<38+QQ@mdETJM%iOZvQy!xIj3qkyA${-N5 zj83zqC02XQNxa@6kuO{uWYOk37rSKg`K1qE#g|5wtU+|~?81Gud0~<+;GL1UlnMNa z^YX;l)-rD8ie|?>j8_ku>96IT4%vmuYq}*fiMEo@CUpRSgTUXBz948*b@7D4G?+{oB8)00C3a`crLiAJMIT;)F0Q4@pTg= zk8TKVpSj9BlOD^U_Vtd@u7Vcfp9Z50yIcE7!!K7*J`F}0db)w|YB0;y z)DMGEhF-3qd>V{0^>qW_)M1yas2>KS47C6N{{LS<022BlC{>70i&@QiW1W$cX@RHh z&$~wT&cZZ5ZzWxr&}HBKxV?IK33{DDzuXf!8n}3C7gB94(TK^{}EA}4n>7Ny_jMijR%-xoD0JK1< zyK!Jib*~}pltq>L8HnpYIzUk?U7LhyFOmM&;|Oaxa~Z3HP-s^zLrsn#NN`#;_GUTb z#_n!>RiP(Hvb9K2Hk@AT!spjPHv9q)i9Uf)E6V$WuUbKH@;b%pn^qQ7wH%ZNW}0)w z^)wQ`^WXd5QE_pS%vl$vBN~*Kf|1vnY61ukCkU=c=WuddH#0*TXMPx(hDV$KU$9?t zL`#!I*2q>euUU5S`lpk!qAd5ZRhy-VzxSa_Ni>LBnx;L79obH{#=Ek9gBFrH58KRz z#=)6S;p~6=11R3l$2Pvo7&{`sQQfb zOLbS48SXz(_@ThOkBlq6zj!KZl-FN}lQ4XA7HyfGEi7%ZjhRS~Q`_)}eG)=3yC?4s z5(*>Wv?@8<=+_CfU@&q}TNL*l8N~_>A-c8;(=aXtM9-r7ad)ImdcAw6dxW zHOgelATJ~5IScJi`6*Za&Z2wv_?9Di&mZ>0`}6iUi?H2MoxkW)5giHsT|DilWJSYF z6)stGfpvDp@j115D$=z6n z1NQQ%yGt(wgViY8yNi(~DXe2*aR9`R(HSqKSx8cxwEyZf94ZJ1NTyw(#NX9w{y#?he7v5k8d`_t1RdIrvWe+Xj& z9oEoqKHr<;{>AnSd zogP_2+qlLnz5AZ>&`>GF34=)WG26*ow0t*dkIC}WW??y8K)T0YOP$#88XSJis;)y+ z*WZZ0A=;4oECTVqLUYyE7D7`>x%e2G zo{0(_rv;S~AWM5T-2JDuSS0Dv{=VU1e%Y|=c&jeRFr1`TRNa3DQpjy( zw=NLvPru9rt~Z*{vndHkP6F3aDyrsm_h59Zi?FIim z(it9Y+jd%l9+(yb00VJ@&f>q+jT|$+?Z9-iqY#k~@(HG}3kJ3P z!6}u8ApsDt)v-C_yDz+g%y_M-JyR z9c!iH=&>aHv6tVv#Ajj3|M=^^QZUl{D>D9!Q_^0L9!LSF1|&vuvX9vCa(JWBs8wiiWY2ZMY7 z=Fg#yHB6Uauc{ARHobWbgz?8I@ewfUyN_n#_Hce4J*GGX1dge6M@dLdOnp^IwwL1% z{$)m7+JGHYw_OY2m1jCU4_>*%di}Z&5YdW`bFhv0jBvu*{T2##(O7KYCLTpg3R5KX zFG=K{F4X%&n8MI7PsvqQvSUE(+d>69)d5kDpLU4(nI&Du=l;!@7H7BZ0r3|ZvA^vG z_SP<+yflz%bRnTrAWxtgHF+~K(}?AU9FTy*Ux3ZPk7Mbb2lUfcp$H5nn@|2}8Ajvx zfp0~o9C*82Qrid4sxr?fhFjr?FOf9*Ou&}io*@Xhb|8=^-OKRnG#j<&uUQ{nirzGM z%c@AWw^JRP|7d|%)Ey4>(LYcw%hwYkcXnfJq`W64oBzs-Fuw`^h}H)4BY7n|!WYX0 z*qOc&(G$0+_JSNHOUL!#{jBH~3PCqOqswuP-fu1a{XN5XCZ_n^l&yQ8X>q3eQ@04? z&3h(EFCzTACB=gUy=weuxqRZlXe%;B@sVnZD#7eSth1XrSgHT+rY{TrtQBa?BFZu8 in}?oeZhY4Q)o&wyi3@~zh-WNDh`8wL1V8`)0002IHZUas literal 0 HcmV?d00001 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 3abc69d360a4c61dba6b12e6c833bdf6b441d8f4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2712 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 S|3^~Dece>pl*7~{#c}PaRZyY; 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")