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:

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
@@ -7,7 +7,7 @@
 import flask
 import werkzeug
 from db import db
-from encryption import EncryptionManager
+from encryption import EncryptionManager, GpgKeyNotFoundError
 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
@@ -402,8 +402,11 @@ 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
-    EncryptionManager.get_default().delete_source_key_pair(filesystem_id)
+    # Delete the source's reply keypair, if it exists
+    try:
+        EncryptionManager.get_default().delete_source_key_pair(filesystem_id)
+    except GpgKeyNotFoundError:
+        pass
 
     # 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
@@ -176,13 +176,6 @@ 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,
@@ -313,6 +306,13 @@ 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,6 +14,7 @@
 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,16 +120,19 @@ 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:
-        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
+        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()
 
 
 def test_generate_as_post(source_app):
@@ -364,18 +368,22 @@ def _dummy_submission(app):
     )
 
 
-def test_initial_submission_notification(source_app):
+def test_initial_submission(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:
-        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
+        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()
 
 
 def test_submit_message(source_app):