Repository: freedomofpress/securedrop

Issue:
Generate reply key on first submission
Note that this change is being reverted!

## Description

When a source visits /lookup for the first time, a GPG reply key is generated. However, if the source does not submit a message, it is impossible to reply to them, as their account is flagged as `pending` and not displayed in the JI. To avoid creating keys unnecessarily, the key could be created on submission instead if it does not already exist.

## User Research Evidence

Observed behavior of sd instances

## User Stories

As an admin, I want to keep a manageable number of source accounts and reply keys


Discussion:
This is superfluous with the job to purge pending keys, and it is a potential source of race condition bugs, so I'm gonna revert the change.

Resolving patch:
diff --git a/securedrop/journalist_app/utils.py b/securedrop/journalist_app/utils.py
--- a/securedrop/journalist_app/utils.py
+++ b/securedrop/journalist_app/utils.py
@@ -6,7 +6,7 @@
 import flask
 import werkzeug
 from db import db
-from encryption import EncryptionManager, GpgKeyNotFoundError
+from encryption import EncryptionManager
 from flask import Markup, abort, current_app, escape, flash, redirect, send_file, url_for
 from flask_babel import gettext, ngettext
 from journalist_app.sessions import session
@@ -397,11 +397,8 @@ def delete_collection(filesystem_id: str) -> None:
     if os.path.exists(path):
         Storage.get_default().move_to_shredder(path)
 
-    # Delete the source's reply keypair, if it exists
-    try:
-        EncryptionManager.get_default().delete_source_key_pair(filesystem_id)
-    except GpgKeyNotFoundError:
-        pass
+    # Delete the source's reply keypair
+    EncryptionManager.get_default().delete_source_key_pair(filesystem_id)
 
     # Delete their entry in the db
     source = get_source(filesystem_id, include_deleted=True)
diff --git a/securedrop/source_app/main.py b/securedrop/source_app/main.py
--- a/securedrop/source_app/main.py
+++ b/securedrop/source_app/main.py
@@ -175,6 +175,13 @@ def lookup(logged_in_source: SourceUser) -> str:
         # Sort the replies by date
         replies.sort(key=operator.attrgetter("date"), reverse=True)
 
+        # If not done yet, generate a keypair to encrypt replies from the journalist
+        encryption_mgr = EncryptionManager.get_default()
+        try:
+            encryption_mgr.get_source_public_key(logged_in_source.filesystem_id)
+        except GpgKeyNotFoundError:
+            encryption_mgr.generate_source_key_pair(logged_in_source)
+
         return render_template(
             "lookup.html",
             is_user_logged_in=True,
@@ -305,13 +312,6 @@ def submit(logged_in_source: SourceUser) -> werkzeug.Response:
 
         normalize_timestamps(logged_in_source)
 
-        # If not done yet, generate a keypair to encrypt replies from the journalist
-        encryption_mgr = EncryptionManager.get_default()
-        try:
-            encryption_mgr.get_source_public_key(logged_in_source.filesystem_id)
-        except GpgKeyNotFoundError:
-            encryption_mgr.generate_source_key_pair(logged_in_source)
-
         return redirect(url_for("main.lookup"))
 
     @view.route("/delete", methods=("POST",))


Test patch:
diff --git a/securedrop/tests/test_source.py b/securedrop/tests/test_source.py
--- a/securedrop/tests/test_source.py
+++ b/securedrop/tests/test_source.py
@@ -14,7 +14,6 @@
 import pytest
 import version
 from db import db
-from encryption import EncryptionManager
 from flaky import flaky
 from flask import escape, g, request, session, url_for
 from flask_babel import gettext
@@ -119,19 +118,16 @@ def test_generate_already_logged_in(source_app):
 
 
 def test_create_new_source(source_app):
-    """Create new source, ensuring that reply key is not created at the same time"""
     with source_app.test_client() as app:
-        with patch.object(EncryptionManager, "get_source_public_key") as get_key:
-            resp = app.post(url_for("main.generate"), data=GENERATE_DATA)
-            assert resp.status_code == 200
-            tab_id = next(iter(session["codenames"].keys()))
-            resp = app.post(url_for("main.create"), data={"tab_id": tab_id}, follow_redirects=True)
-            assert SessionManager.is_user_logged_in(db_session=db.session)
-            # should be redirected to /lookup
-            text = resp.data.decode("utf-8")
-            assert "Submit Files" in text
-            assert "codenames" not in session
-            get_key.assert_not_called()
+        resp = app.post(url_for("main.generate"), data=GENERATE_DATA)
+        assert resp.status_code == 200
+        tab_id = next(iter(session["codenames"].keys()))
+        resp = app.post(url_for("main.create"), data={"tab_id": tab_id}, follow_redirects=True)
+        assert SessionManager.is_user_logged_in(db_session=db.session)
+        # should be redirected to /lookup
+        text = resp.data.decode("utf-8")
+        assert "Submit Files" in text
+        assert "codenames" not in session
 
 
 def test_generate_as_post(source_app):
@@ -367,22 +363,18 @@ def _dummy_submission(app):
     )
 
 
-def test_initial_submission(source_app):
+def test_initial_submission_notification(source_app):
     """
     Regardless of the type of submission (message, file, or both), the
     first submission is always greeted with a notification
     reminding sources to check back later for replies.
-
-    A GPG keypair for replies should also be created.
     """
     with source_app.test_client() as app:
-        with patch.object(EncryptionManager, "generate_source_key_pair") as gen_key:
-            new_codename(app, session)
-            resp = _dummy_submission(app)
-            assert resp.status_code == 200
-            text = resp.data.decode("utf-8")
-            assert "Thank you for sending this information to us." in text
-            gen_key.assert_called_once()
+        new_codename(app, session)
+        resp = _dummy_submission(app)
+        assert resp.status_code == 200
+        text = resp.data.decode("utf-8")
+        assert "Thank you for sending this information to us." in text
 
 
 def test_submit_message(source_app):