We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
When sources that are "flagged" generate a gpg key they don't move to the top
The list of sources is sorted chronologically by latest activity.

If there's a source that's flagged for response and then they login and therefore generate a gpg key, SecureDrop 0.3 doesn't consider that new activity. This means that the journalist is in no way notified that they can now write a response to that source if they want to.

SecureDrop 0.2.1 did consider it new activity, and that source rose to the top of the list.

This is an extremely minor issue though, since in most cases now the "flag for response" feature will never be used anyway, unless there's an entropy DoS.


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `securedrop/source.py`
Content:
```
1 # -*- coding: utf-8 -*-
2 import os
3 from datetime import datetime
4 import uuid
5 from functools import wraps
6 import zipfile
7 from cStringIO import StringIO
8 import subprocess
9 
10 import logging
11 # This module's logger is explicitly labeled so the correct logger is used,
12 # even when this is run from the command line (e.g. during development)
13 log = logging.getLogger('source')
14 
15 from flask import (Flask, request, render_template, session, redirect, url_for,
16                    flash, abort, g, send_file)
17 from flask_wtf.csrf import CsrfProtect
18 
19 from sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound
20 from sqlalchemy.exc import IntegrityError
21 
22 import config
23 import version
24 import crypto_util
25 import store
26 import background
27 import template_filters
28 from db import db_session, Source, Submission
29 from jinja2 import evalcontextfilter
30 
31 app = Flask(__name__, template_folder=config.SOURCE_TEMPLATES_DIR)
32 app.config.from_object(config.SourceInterfaceFlaskConfig)
33 CsrfProtect(app)
34 
35 app.jinja_env.globals['version'] = version.__version__
36 if getattr(config, 'CUSTOM_HEADER_IMAGE', None):
37     app.jinja_env.globals['header_image'] = config.CUSTOM_HEADER_IMAGE
38     app.jinja_env.globals['use_custom_header_image'] = True
39 else:
40     app.jinja_env.globals['header_image'] = 'logo.png'
41     app.jinja_env.globals['use_custom_header_image'] = False
42 
43 app.jinja_env.filters['datetimeformat'] = template_filters.datetimeformat
44 app.jinja_env.filters['nl2br'] = evalcontextfilter(template_filters.nl2br)
45 
46 @app.teardown_appcontext
47 def shutdown_session(exception=None):
48     """Automatically remove database sessions at the end of the request, or
49     when the application shuts down"""
50     db_session.remove()
51 
52 
53 def logged_in():
54     return 'logged_in' in session
55 
56 
57 def login_required(f):
58     @wraps(f)
59     def decorated_function(*args, **kwargs):
60         if not logged_in():
61             return redirect(url_for('login'))
62         return f(*args, **kwargs)
63     return decorated_function
64 
65 
66 def ignore_static(f):
67     """Only executes the wrapped function if we're not loading a static resource."""
68     @wraps(f)
69     def decorated_function(*args, **kwargs):
70         if request.path.startswith('/static'):
71             return  # don't execute the decorated function
72         return f(*args, **kwargs)
73     return decorated_function
74 
75 
76 @app.before_request
77 @ignore_static
78 def setup_g():
79     """Store commonly used values in Flask's special g object"""
80     # ignore_static here because `crypto_util.hash_codename` is scrypt (very
81     # time consuming), and we don't need to waste time running if we're just
82     # serving a static resource that won't need to access these common values.
83     if logged_in():
84         g.codename = session['codename']
85         g.sid = crypto_util.hash_codename(g.codename)
86         try:
87             g.source = Source.query.filter(Source.filesystem_id == g.sid).one()
88         except MultipleResultsFound as e:
89             app.logger.error("Found multiple Sources when one was expected: %s" % (e,))
90             abort(500)
91         except NoResultFound as e:
92             app.logger.error("Found no Sources when one was expected: %s" % (e,))
93             del session['logged_in']
94             del session['codename']
95             return redirect(url_for('index'))
96         g.loc = store.path(g.sid)
97 
98 
99 @app.before_request
100 @ignore_static
101 def check_tor2web():
102         # ignore_static here so we only flash a single message warning about Tor2Web,
103         # corresponding to the intial page load.
104     if 'X-tor2web' in request.headers:
105         flash('<strong>WARNING:</strong> You appear to be using Tor2Web. '
106               'This <strong>does not</strong> provide anonymity. '
107               '<a href="/tor2web-warning">Why is this dangerous?</a>',
108               "banner-warning")
109 
110 
111 @app.route('/')
112 def index():
113     return render_template('index.html')
114 
115 
116 def generate_unique_codename(num_words):
117     """Generate random codenames until we get an unused one"""
118     while True:
119         codename = crypto_util.genrandomid(num_words)
120         sid = crypto_util.hash_codename(codename) # scrypt (slow)
121         matching_sources = Source.query.filter(Source.filesystem_id == sid).all()
122         if len(matching_sources) == 0:
123             return codename
124 
125 
126 @app.route('/generate', methods=('GET', 'POST'))
127 def generate():
128     # Popping this key prevents errors when a logged in user returns to /generate.
129     # TODO: is this the best experience? A logged in user will be automatically
130     # logged out if they navigate to /generate by accident, which could be
131     # confusing. It might be better to instead redirect them to the lookup
132     # page, or inform them that they're logged in.
133     session.pop('logged_in', None)
134 
135     num_words = 7
136     if request.method == 'POST':
137         num_words = int(request.form['number-words'])
138         if num_words not in range(7, 11):
139             abort(403)
140 
141     codename = generate_unique_codename(num_words)
142     session['codename'] = codename
143     return render_template('generate.html', codename=codename, num_words=num_words)
144 
145 
146 @app.route('/create', methods=['POST'])
147 def create():
148     sid = crypto_util.hash_codename(session['codename'])
149 
150     source = Source(sid, crypto_util.display_id())
151     db_session.add(source)
152     try:
153         db_session.commit()
154     except IntegrityError as e:
155         app.logger.error("Attempt to create a source with duplicate codename: %s" % (e,))
156     else:
157         os.mkdir(store.path(sid))
158 
159     session['logged_in'] = True
160     return redirect(url_for('lookup'))
161 
162 
163 @app.route('/lookup', methods=('GET',))
164 @login_required
165 def lookup():
166     replies = []
167     for fn in os.listdir(g.loc):
168         if fn.endswith('-reply.gpg'):
169             try:
170                 msg = crypto_util.decrypt(g.codename,
171                         file(store.path(g.sid, fn)).read()).decode("utf-8")
172             except UnicodeDecodeError:
173                 app.logger.error("Could not decode reply %s" % fn)
174             else:
175                 date = datetime.utcfromtimestamp(os.stat(store.path(g.sid, fn)).st_mtime)
176                 replies.append(dict(id=fn, date=date, msg=msg))
177 
178     # Sort the replies by date
179     replies.sort(key=lambda reply: reply['date'], reverse=True)
180 
181     def async_genkey(sid, codename):
182         with app.app_context():
183             background.execute(lambda: crypto_util.genkeypair(sid, codename))
184 
185     # Generate a keypair to encrypt replies from the journalist
186     # Only do this if the journalist has flagged the source as one
187     # that they would like to reply to. (Issue #140.)
188     if not crypto_util.getkey(g.sid) and g.source.flagged:
189         async_genkey(g.sid, g.codename)
190 
191     return render_template('lookup.html', codename=g.codename, replies=replies,
192             flagged=g.source.flagged, haskey=crypto_util.getkey(g.sid))
193 
194 
195 def normalize_timestamps(sid):
196     """
197     Update the timestamps on all of the source's submissions to match that of
198     the latest submission. This minimizes metadata that could be useful to
199     investigators. See #301.
200     """
201     sub_paths = [ store.path(sid, submission.filename)
202                   for submission in g.source.submissions ]
203     if len(sub_paths) > 1:
204         args = ["touch"]
205         args.extend(sub_paths[:-1])
206         rc = subprocess.call(args)
207         if rc != 0:
208             app.logger.warning("Couldn't normalize submission timestamps (touch exited with %d)" % rc)
209 
210 
211 @app.route('/submit', methods=('POST',))
212 @login_required
213 def submit():
214     msg = request.form['msg']
215     fh = request.files['fh']
216 
217     fnames = []
218     journalist_filename = g.source.journalist_filename()
219 
220     if msg:
221         g.source.interaction_count += 1
222         fnames.append(store.save_message_submission(g.sid, g.source.interaction_count,
223             journalist_filename, msg))
224         flash("Thanks! We received your message.", "notification")
225     if fh:
226         g.source.interaction_count += 1
227         fnames.append(store.save_file_submission(g.sid, g.source.interaction_count,
228             journalist_filename, fh.filename, fh.stream))
229         flash('{} "{}".'.format("Thanks! We received your document",
230                                 fh.filename or '[unnamed]'), "notification")
231     for fname in fnames:
232         submission = Submission(g.source, fname)
233         db_session.add(submission)
234 
235     if g.source.pending:
236         g.source.pending = False
237 
238         # Generate a keypair now, if there's enough entropy (issue #303)
239         entropy_avail = int(open('/proc/sys/kernel/random/entropy_avail').read())
240         if entropy_avail >= 2400:
241             crypto_util.genkeypair(g.sid, g.codename)
242 
243     g.source.last_updated = datetime.utcnow()
244     db_session.commit()
245     normalize_timestamps(g.sid)
246 
247     return redirect(url_for('lookup'))
248 
249 
250 @app.route('/delete', methods=('POST',))
251 @login_required
252 def delete():
253     msgid = request.form['msgid']
254     assert '/' not in msgid
255     potential_files = os.listdir(g.loc)
256     if msgid not in potential_files:
257         abort(404)  # TODO are the checks necessary?
258     store.secure_unlink(store.path(g.sid, msgid))
259     flash("Reply deleted", "notification")
260 
261     return redirect(url_for('lookup'))
262 
263 
264 def valid_codename(codename):
265     return os.path.exists(store.path(crypto_util.hash_codename(codename)))
266 
267 @app.route('/login', methods=('GET', 'POST'))
268 def login():
269     if request.method == 'POST':
270         codename = request.form['codename']
271         try:
272             valid = valid_codename(codename)
273         except crypto_util.CryptoException:
274             pass
275         else:
276             if valid:
277                 session.update(codename=codename, logged_in=True)
278                 return redirect(url_for('lookup', from_login='1'))
279         flash("Sorry, that is not a recognized codename.", "error")
280     return render_template('login.html')
281 
282 
283 @app.route('/howto-disable-js')
284 def howto_disable_js():
285     return render_template("howto-disable-js.html")
286 
287 
288 @app.route('/tor2web-warning')
289 def tor2web_warning():
290     return render_template("tor2web-warning.html")
291 
292 
293 @app.route('/journalist-key')
294 def download_journalist_pubkey():
295     journalist_pubkey = crypto_util.gpg.export_keys(config.JOURNALIST_KEY)
296     return send_file(StringIO(journalist_pubkey),
297                      mimetype="application/pgp-keys",
298                      attachment_filename=config.JOURNALIST_KEY + ".asc",
299                      as_attachment=True)
300 
301 
302 @app.route('/why-journalist-key')
303 def why_download_journalist_pubkey():
304     return render_template("why-journalist-key.html")
305 
306 
307 @app.errorhandler(404)
308 def page_not_found(error):
309     return render_template('notfound.html'), 404
310 
311 @app.errorhandler(500)
312 def internal_error(error):
313     return render_template('error.html'), 500
314 
315 def write_pidfile():
316     pid = str(os.getpid())
317     with open(config.SOURCE_PIDFILE, 'w') as fp:
318         fp.write(pid)
319 
320 if __name__ == "__main__":
321     write_pidfile()
322     # TODO make sure debug is not on in production
323     app.run(debug=True, host='0.0.0.0', port=8080)
324 
325
```

Path: `securedrop/background.py`
Content:
```
1 import threading
2 
3 
4 def execute(func):
5     threading.Thread(target=func).start()
6
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/securedrop/background.py b/securedrop/background.py
deleted file mode 100644
--- a/securedrop/background.py
+++ /dev/null
@@ -1,5 +0,0 @@
-import threading
-
-
-def execute(func):
-    threading.Thread(target=func).start()
diff --git a/securedrop/source.py b/securedrop/source.py
--- a/securedrop/source.py
+++ b/securedrop/source.py
@@ -6,6 +6,7 @@
 import zipfile
 from cStringIO import StringIO
 import subprocess
+from threading import Thread
 
 import logging
 # This module's logger is explicitly labeled so the correct logger is used,
@@ -23,7 +24,6 @@
 import version
 import crypto_util
 import store
-import background
 import template_filters
 from db import db_session, Source, Submission
 from jinja2 import evalcontextfilter
@@ -160,6 +160,27 @@
     return redirect(url_for('lookup'))
 
 
+def async(f):
+    def wrapper(*args, **kwargs):
+        thread = Thread(target=f, args=args, kwargs=kwargs)
+        thread.start()
+    return wrapper
+
+@async
+def async_genkey(sid, codename):
+    crypto_util.genkeypair(sid, codename)
+
+    # Register key generation as update to the source, so sources will
+    # filter to the top of the list in the document interface if a
+    # flagged source logs in and has a key generated for them. #789
+    try:
+        source = Source.query.filter(Source.filesystem_id == sid).one()
+        source.last_updated = datetime.utcnow()
+        db_session.commit()
+    except Exception as e:
+        app.logger.error("async_genkey for source (sid={}): {}".format(sid, e))
+
+
 @app.route('/lookup', methods=('GET',))
 @login_required
 def lookup():
@@ -178,10 +199,6 @@
     # Sort the replies by date
     replies.sort(key=lambda reply: reply['date'], reverse=True)
 
-    def async_genkey(sid, codename):
-        with app.app_context():
-            background.execute(lambda: crypto_util.genkeypair(sid, codename))
-
     # Generate a keypair to encrypt replies from the journalist
     # Only do this if the journalist has flagged the source as one
     # that they would like to reply to. (Issue #140.)
@@ -238,7 +255,7 @@
         # Generate a keypair now, if there's enough entropy (issue #303)
         entropy_avail = int(open('/proc/sys/kernel/random/entropy_avail').read())
         if entropy_avail >= 2400:
-            crypto_util.genkeypair(g.sid, g.codename)
+            async_genkey(g.sid, g.codename)
 
     g.source.last_updated = datetime.utcnow()
     db_session.commit()

{"golden_diff": "diff --git a/securedrop/background.py b/securedrop/background.py\ndeleted file mode 100644\n--- a/securedrop/background.py\n+++ /dev/null\n@@ -1,5 +0,0 @@\n-import threading\n-\n-\n-def execute(func):\n-    threading.Thread(target=func).start()\ndiff --git a/securedrop/source.py b/securedrop/source.py\n--- a/securedrop/source.py\n+++ b/securedrop/source.py\n@@ -6,6 +6,7 @@\n import zipfile\n from cStringIO import StringIO\n import subprocess\n+from threading import Thread\n \n import logging\n # This module's logger is explicitly labeled so the correct logger is used,\n@@ -23,7 +24,6 @@\n import version\n import crypto_util\n import store\n-import background\n import template_filters\n from db import db_session, Source, Submission\n from jinja2 import evalcontextfilter\n@@ -160,6 +160,27 @@\n     return redirect(url_for('lookup'))\n \n \n+def async(f):\n+    def wrapper(*args, **kwargs):\n+        thread = Thread(target=f, args=args, kwargs=kwargs)\n+        thread.start()\n+    return wrapper\n+\n+@async\n+def async_genkey(sid, codename):\n+    crypto_util.genkeypair(sid, codename)\n+\n+    # Register key generation as update to the source, so sources will\n+    # filter to the top of the list in the document interface if a\n+    # flagged source logs in and has a key generated for them. #789\n+    try:\n+        source = Source.query.filter(Source.filesystem_id == sid).one()\n+        source.last_updated = datetime.utcnow()\n+        db_session.commit()\n+    except Exception as e:\n+        app.logger.error(\"async_genkey for source (sid={}): {}\".format(sid, e))\n+\n+\n @app.route('/lookup', methods=('GET',))\n @login_required\n def lookup():\n@@ -178,10 +199,6 @@\n     # Sort the replies by date\n     replies.sort(key=lambda reply: reply['date'], reverse=True)\n \n-    def async_genkey(sid, codename):\n-        with app.app_context():\n-            background.execute(lambda: crypto_util.genkeypair(sid, codename))\n-\n     # Generate a keypair to encrypt replies from the journalist\n     # Only do this if the journalist has flagged the source as one\n     # that they would like to reply to. (Issue #140.)\n@@ -238,7 +255,7 @@\n         # Generate a keypair now, if there's enough entropy (issue #303)\n         entropy_avail = int(open('/proc/sys/kernel/random/entropy_avail').read())\n         if entropy_avail >= 2400:\n-            crypto_util.genkeypair(g.sid, g.codename)\n+            async_genkey(g.sid, g.codename)\n \n     g.source.last_updated = datetime.utcnow()\n     db_session.commit()\n", "issue": "When sources that are \"flagged\" generate a gpg key they don't move to the top\nThe list of sources is sorted chronologically by latest activity.\n\nIf there's a source that's flagged for response and then they login and therefore generate a gpg key, SecureDrop 0.3 doesn't consider that new activity. This means that the journalist is in no way notified that they can now write a response to that source if they want to.\n\nSecureDrop 0.2.1 did consider it new activity, and that source rose to the top of the list.\n\nThis is an extremely minor issue though, since in most cases now the \"flag for response\" feature will never be used anyway, unless there's an entropy DoS.\n\n", "before_files": [{"content": "# -*- coding: utf-8 -*-\nimport os\nfrom datetime import datetime\nimport uuid\nfrom functools import wraps\nimport zipfile\nfrom cStringIO import StringIO\nimport subprocess\n\nimport logging\n# This module's logger is explicitly labeled so the correct logger is used,\n# even when this is run from the command line (e.g. during development)\nlog = logging.getLogger('source')\n\nfrom flask import (Flask, request, render_template, session, redirect, url_for,\n                   flash, abort, g, send_file)\nfrom flask_wtf.csrf import CsrfProtect\n\nfrom sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound\nfrom sqlalchemy.exc import IntegrityError\n\nimport config\nimport version\nimport crypto_util\nimport store\nimport background\nimport template_filters\nfrom db import db_session, Source, Submission\nfrom jinja2 import evalcontextfilter\n\napp = Flask(__name__, template_folder=config.SOURCE_TEMPLATES_DIR)\napp.config.from_object(config.SourceInterfaceFlaskConfig)\nCsrfProtect(app)\n\napp.jinja_env.globals['version'] = version.__version__\nif getattr(config, 'CUSTOM_HEADER_IMAGE', None):\n    app.jinja_env.globals['header_image'] = config.CUSTOM_HEADER_IMAGE\n    app.jinja_env.globals['use_custom_header_image'] = True\nelse:\n    app.jinja_env.globals['header_image'] = 'logo.png'\n    app.jinja_env.globals['use_custom_header_image'] = False\n\napp.jinja_env.filters['datetimeformat'] = template_filters.datetimeformat\napp.jinja_env.filters['nl2br'] = evalcontextfilter(template_filters.nl2br)\n\n@app.teardown_appcontext\ndef shutdown_session(exception=None):\n    \"\"\"Automatically remove database sessions at the end of the request, or\n    when the application shuts down\"\"\"\n    db_session.remove()\n\n\ndef logged_in():\n    return 'logged_in' in session\n\n\ndef login_required(f):\n    @wraps(f)\n    def decorated_function(*args, **kwargs):\n        if not logged_in():\n            return redirect(url_for('login'))\n        return f(*args, **kwargs)\n    return decorated_function\n\n\ndef ignore_static(f):\n    \"\"\"Only executes the wrapped function if we're not loading a static resource.\"\"\"\n    @wraps(f)\n    def decorated_function(*args, **kwargs):\n        if request.path.startswith('/static'):\n            return  # don't execute the decorated function\n        return f(*args, **kwargs)\n    return decorated_function\n\n\n@app.before_request\n@ignore_static\ndef setup_g():\n    \"\"\"Store commonly used values in Flask's special g object\"\"\"\n    # ignore_static here because `crypto_util.hash_codename` is scrypt (very\n    # time consuming), and we don't need to waste time running if we're just\n    # serving a static resource that won't need to access these common values.\n    if logged_in():\n        g.codename = session['codename']\n        g.sid = crypto_util.hash_codename(g.codename)\n        try:\n            g.source = Source.query.filter(Source.filesystem_id == g.sid).one()\n        except MultipleResultsFound as e:\n            app.logger.error(\"Found multiple Sources when one was expected: %s\" % (e,))\n            abort(500)\n        except NoResultFound as e:\n            app.logger.error(\"Found no Sources when one was expected: %s\" % (e,))\n            del session['logged_in']\n            del session['codename']\n            return redirect(url_for('index'))\n        g.loc = store.path(g.sid)\n\n\n@app.before_request\n@ignore_static\ndef check_tor2web():\n        # ignore_static here so we only flash a single message warning about Tor2Web,\n        # corresponding to the intial page load.\n    if 'X-tor2web' in request.headers:\n        flash('<strong>WARNING:</strong> You appear to be using Tor2Web. '\n              'This <strong>does not</strong> provide anonymity. '\n              '<a href=\"/tor2web-warning\">Why is this dangerous?</a>',\n              \"banner-warning\")\n\n\n@app.route('/')\ndef index():\n    return render_template('index.html')\n\n\ndef generate_unique_codename(num_words):\n    \"\"\"Generate random codenames until we get an unused one\"\"\"\n    while True:\n        codename = crypto_util.genrandomid(num_words)\n        sid = crypto_util.hash_codename(codename) # scrypt (slow)\n        matching_sources = Source.query.filter(Source.filesystem_id == sid).all()\n        if len(matching_sources) == 0:\n            return codename\n\n\n@app.route('/generate', methods=('GET', 'POST'))\ndef generate():\n    # Popping this key prevents errors when a logged in user returns to /generate.\n    # TODO: is this the best experience? A logged in user will be automatically\n    # logged out if they navigate to /generate by accident, which could be\n    # confusing. It might be better to instead redirect them to the lookup\n    # page, or inform them that they're logged in.\n    session.pop('logged_in', None)\n\n    num_words = 7\n    if request.method == 'POST':\n        num_words = int(request.form['number-words'])\n        if num_words not in range(7, 11):\n            abort(403)\n\n    codename = generate_unique_codename(num_words)\n    session['codename'] = codename\n    return render_template('generate.html', codename=codename, num_words=num_words)\n\n\n@app.route('/create', methods=['POST'])\ndef create():\n    sid = crypto_util.hash_codename(session['codename'])\n\n    source = Source(sid, crypto_util.display_id())\n    db_session.add(source)\n    try:\n        db_session.commit()\n    except IntegrityError as e:\n        app.logger.error(\"Attempt to create a source with duplicate codename: %s\" % (e,))\n    else:\n        os.mkdir(store.path(sid))\n\n    session['logged_in'] = True\n    return redirect(url_for('lookup'))\n\n\n@app.route('/lookup', methods=('GET',))\n@login_required\ndef lookup():\n    replies = []\n    for fn in os.listdir(g.loc):\n        if fn.endswith('-reply.gpg'):\n            try:\n                msg = crypto_util.decrypt(g.codename,\n                        file(store.path(g.sid, fn)).read()).decode(\"utf-8\")\n            except UnicodeDecodeError:\n                app.logger.error(\"Could not decode reply %s\" % fn)\n            else:\n                date = datetime.utcfromtimestamp(os.stat(store.path(g.sid, fn)).st_mtime)\n                replies.append(dict(id=fn, date=date, msg=msg))\n\n    # Sort the replies by date\n    replies.sort(key=lambda reply: reply['date'], reverse=True)\n\n    def async_genkey(sid, codename):\n        with app.app_context():\n            background.execute(lambda: crypto_util.genkeypair(sid, codename))\n\n    # Generate a keypair to encrypt replies from the journalist\n    # Only do this if the journalist has flagged the source as one\n    # that they would like to reply to. (Issue #140.)\n    if not crypto_util.getkey(g.sid) and g.source.flagged:\n        async_genkey(g.sid, g.codename)\n\n    return render_template('lookup.html', codename=g.codename, replies=replies,\n            flagged=g.source.flagged, haskey=crypto_util.getkey(g.sid))\n\n\ndef normalize_timestamps(sid):\n    \"\"\"\n    Update the timestamps on all of the source's submissions to match that of\n    the latest submission. This minimizes metadata that could be useful to\n    investigators. See #301.\n    \"\"\"\n    sub_paths = [ store.path(sid, submission.filename)\n                  for submission in g.source.submissions ]\n    if len(sub_paths) > 1:\n        args = [\"touch\"]\n        args.extend(sub_paths[:-1])\n        rc = subprocess.call(args)\n        if rc != 0:\n            app.logger.warning(\"Couldn't normalize submission timestamps (touch exited with %d)\" % rc)\n\n\n@app.route('/submit', methods=('POST',))\n@login_required\ndef submit():\n    msg = request.form['msg']\n    fh = request.files['fh']\n\n    fnames = []\n    journalist_filename = g.source.journalist_filename()\n\n    if msg:\n        g.source.interaction_count += 1\n        fnames.append(store.save_message_submission(g.sid, g.source.interaction_count,\n            journalist_filename, msg))\n        flash(\"Thanks! We received your message.\", \"notification\")\n    if fh:\n        g.source.interaction_count += 1\n        fnames.append(store.save_file_submission(g.sid, g.source.interaction_count,\n            journalist_filename, fh.filename, fh.stream))\n        flash('{} \"{}\".'.format(\"Thanks! We received your document\",\n                                fh.filename or '[unnamed]'), \"notification\")\n    for fname in fnames:\n        submission = Submission(g.source, fname)\n        db_session.add(submission)\n\n    if g.source.pending:\n        g.source.pending = False\n\n        # Generate a keypair now, if there's enough entropy (issue #303)\n        entropy_avail = int(open('/proc/sys/kernel/random/entropy_avail').read())\n        if entropy_avail >= 2400:\n            crypto_util.genkeypair(g.sid, g.codename)\n\n    g.source.last_updated = datetime.utcnow()\n    db_session.commit()\n    normalize_timestamps(g.sid)\n\n    return redirect(url_for('lookup'))\n\n\n@app.route('/delete', methods=('POST',))\n@login_required\ndef delete():\n    msgid = request.form['msgid']\n    assert '/' not in msgid\n    potential_files = os.listdir(g.loc)\n    if msgid not in potential_files:\n        abort(404)  # TODO are the checks necessary?\n    store.secure_unlink(store.path(g.sid, msgid))\n    flash(\"Reply deleted\", \"notification\")\n\n    return redirect(url_for('lookup'))\n\n\ndef valid_codename(codename):\n    return os.path.exists(store.path(crypto_util.hash_codename(codename)))\n\n@app.route('/login', methods=('GET', 'POST'))\ndef login():\n    if request.method == 'POST':\n        codename = request.form['codename']\n        try:\n            valid = valid_codename(codename)\n        except crypto_util.CryptoException:\n            pass\n        else:\n            if valid:\n                session.update(codename=codename, logged_in=True)\n                return redirect(url_for('lookup', from_login='1'))\n        flash(\"Sorry, that is not a recognized codename.\", \"error\")\n    return render_template('login.html')\n\n\n@app.route('/howto-disable-js')\ndef howto_disable_js():\n    return render_template(\"howto-disable-js.html\")\n\n\n@app.route('/tor2web-warning')\ndef tor2web_warning():\n    return render_template(\"tor2web-warning.html\")\n\n\n@app.route('/journalist-key')\ndef download_journalist_pubkey():\n    journalist_pubkey = crypto_util.gpg.export_keys(config.JOURNALIST_KEY)\n    return send_file(StringIO(journalist_pubkey),\n                     mimetype=\"application/pgp-keys\",\n                     attachment_filename=config.JOURNALIST_KEY + \".asc\",\n                     as_attachment=True)\n\n\n@app.route('/why-journalist-key')\ndef why_download_journalist_pubkey():\n    return render_template(\"why-journalist-key.html\")\n\n\n@app.errorhandler(404)\ndef page_not_found(error):\n    return render_template('notfound.html'), 404\n\n@app.errorhandler(500)\ndef internal_error(error):\n    return render_template('error.html'), 500\n\ndef write_pidfile():\n    pid = str(os.getpid())\n    with open(config.SOURCE_PIDFILE, 'w') as fp:\n        fp.write(pid)\n\nif __name__ == \"__main__\":\n    write_pidfile()\n    # TODO make sure debug is not on in production\n    app.run(debug=True, host='0.0.0.0', port=8080)\n\n", "path": "securedrop/source.py"}, {"content": "import threading\n\n\ndef execute(func):\n    threading.Thread(target=func).start()\n", "path": "securedrop/background.py"}], "after_files": [{"content": "# -*- coding: utf-8 -*-\nimport os\nfrom datetime import datetime\nimport uuid\nfrom functools import wraps\nimport zipfile\nfrom cStringIO import StringIO\nimport subprocess\nfrom threading import Thread\n\nimport logging\n# This module's logger is explicitly labeled so the correct logger is used,\n# even when this is run from the command line (e.g. during development)\nlog = logging.getLogger('source')\n\nfrom flask import (Flask, request, render_template, session, redirect, url_for,\n                   flash, abort, g, send_file)\nfrom flask_wtf.csrf import CsrfProtect\n\nfrom sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound\nfrom sqlalchemy.exc import IntegrityError\n\nimport config\nimport version\nimport crypto_util\nimport store\nimport template_filters\nfrom db import db_session, Source, Submission\nfrom jinja2 import evalcontextfilter\n\napp = Flask(__name__, template_folder=config.SOURCE_TEMPLATES_DIR)\napp.config.from_object(config.SourceInterfaceFlaskConfig)\nCsrfProtect(app)\n\napp.jinja_env.globals['version'] = version.__version__\nif getattr(config, 'CUSTOM_HEADER_IMAGE', None):\n    app.jinja_env.globals['header_image'] = config.CUSTOM_HEADER_IMAGE\n    app.jinja_env.globals['use_custom_header_image'] = True\nelse:\n    app.jinja_env.globals['header_image'] = 'logo.png'\n    app.jinja_env.globals['use_custom_header_image'] = False\n\napp.jinja_env.filters['datetimeformat'] = template_filters.datetimeformat\napp.jinja_env.filters['nl2br'] = evalcontextfilter(template_filters.nl2br)\n\n@app.teardown_appcontext\ndef shutdown_session(exception=None):\n    \"\"\"Automatically remove database sessions at the end of the request, or\n    when the application shuts down\"\"\"\n    db_session.remove()\n\n\ndef logged_in():\n    return 'logged_in' in session\n\n\ndef login_required(f):\n    @wraps(f)\n    def decorated_function(*args, **kwargs):\n        if not logged_in():\n            return redirect(url_for('login'))\n        return f(*args, **kwargs)\n    return decorated_function\n\n\ndef ignore_static(f):\n    \"\"\"Only executes the wrapped function if we're not loading a static resource.\"\"\"\n    @wraps(f)\n    def decorated_function(*args, **kwargs):\n        if request.path.startswith('/static'):\n            return  # don't execute the decorated function\n        return f(*args, **kwargs)\n    return decorated_function\n\n\n@app.before_request\n@ignore_static\ndef setup_g():\n    \"\"\"Store commonly used values in Flask's special g object\"\"\"\n    # ignore_static here because `crypto_util.hash_codename` is scrypt (very\n    # time consuming), and we don't need to waste time running if we're just\n    # serving a static resource that won't need to access these common values.\n    if logged_in():\n        g.codename = session['codename']\n        g.sid = crypto_util.hash_codename(g.codename)\n        try:\n            g.source = Source.query.filter(Source.filesystem_id == g.sid).one()\n        except MultipleResultsFound as e:\n            app.logger.error(\"Found multiple Sources when one was expected: %s\" % (e,))\n            abort(500)\n        except NoResultFound as e:\n            app.logger.error(\"Found no Sources when one was expected: %s\" % (e,))\n            del session['logged_in']\n            del session['codename']\n            return redirect(url_for('index'))\n        g.loc = store.path(g.sid)\n\n\n@app.before_request\n@ignore_static\ndef check_tor2web():\n        # ignore_static here so we only flash a single message warning about Tor2Web,\n        # corresponding to the intial page load.\n    if 'X-tor2web' in request.headers:\n        flash('<strong>WARNING:</strong> You appear to be using Tor2Web. '\n              'This <strong>does not</strong> provide anonymity. '\n              '<a href=\"/tor2web-warning\">Why is this dangerous?</a>',\n              \"banner-warning\")\n\n\n@app.route('/')\ndef index():\n    return render_template('index.html')\n\n\ndef generate_unique_codename(num_words):\n    \"\"\"Generate random codenames until we get an unused one\"\"\"\n    while True:\n        codename = crypto_util.genrandomid(num_words)\n        sid = crypto_util.hash_codename(codename) # scrypt (slow)\n        matching_sources = Source.query.filter(Source.filesystem_id == sid).all()\n        if len(matching_sources) == 0:\n            return codename\n\n\n@app.route('/generate', methods=('GET', 'POST'))\ndef generate():\n    # Popping this key prevents errors when a logged in user returns to /generate.\n    # TODO: is this the best experience? A logged in user will be automatically\n    # logged out if they navigate to /generate by accident, which could be\n    # confusing. It might be better to instead redirect them to the lookup\n    # page, or inform them that they're logged in.\n    session.pop('logged_in', None)\n\n    num_words = 7\n    if request.method == 'POST':\n        num_words = int(request.form['number-words'])\n        if num_words not in range(7, 11):\n            abort(403)\n\n    codename = generate_unique_codename(num_words)\n    session['codename'] = codename\n    return render_template('generate.html', codename=codename, num_words=num_words)\n\n\n@app.route('/create', methods=['POST'])\ndef create():\n    sid = crypto_util.hash_codename(session['codename'])\n\n    source = Source(sid, crypto_util.display_id())\n    db_session.add(source)\n    try:\n        db_session.commit()\n    except IntegrityError as e:\n        app.logger.error(\"Attempt to create a source with duplicate codename: %s\" % (e,))\n    else:\n        os.mkdir(store.path(sid))\n\n    session['logged_in'] = True\n    return redirect(url_for('lookup'))\n\n\ndef async(f):\n    def wrapper(*args, **kwargs):\n        thread = Thread(target=f, args=args, kwargs=kwargs)\n        thread.start()\n    return wrapper\n\n@async\ndef async_genkey(sid, codename):\n    crypto_util.genkeypair(sid, codename)\n\n    # Register key generation as update to the source, so sources will\n    # filter to the top of the list in the document interface if a\n    # flagged source logs in and has a key generated for them. #789\n    try:\n        source = Source.query.filter(Source.filesystem_id == sid).one()\n        source.last_updated = datetime.utcnow()\n        db_session.commit()\n    except Exception as e:\n        app.logger.error(\"async_genkey for source (sid={}): {}\".format(sid, e))\n\n\n@app.route('/lookup', methods=('GET',))\n@login_required\ndef lookup():\n    replies = []\n    for fn in os.listdir(g.loc):\n        if fn.endswith('-reply.gpg'):\n            try:\n                msg = crypto_util.decrypt(g.codename,\n                        file(store.path(g.sid, fn)).read()).decode(\"utf-8\")\n            except UnicodeDecodeError:\n                app.logger.error(\"Could not decode reply %s\" % fn)\n            else:\n                date = datetime.utcfromtimestamp(os.stat(store.path(g.sid, fn)).st_mtime)\n                replies.append(dict(id=fn, date=date, msg=msg))\n\n    # Sort the replies by date\n    replies.sort(key=lambda reply: reply['date'], reverse=True)\n\n    # Generate a keypair to encrypt replies from the journalist\n    # Only do this if the journalist has flagged the source as one\n    # that they would like to reply to. (Issue #140.)\n    if not crypto_util.getkey(g.sid) and g.source.flagged:\n        async_genkey(g.sid, g.codename)\n\n    return render_template('lookup.html', codename=g.codename, replies=replies,\n            flagged=g.source.flagged, haskey=crypto_util.getkey(g.sid))\n\n\ndef normalize_timestamps(sid):\n    \"\"\"\n    Update the timestamps on all of the source's submissions to match that of\n    the latest submission. This minimizes metadata that could be useful to\n    investigators. See #301.\n    \"\"\"\n    sub_paths = [ store.path(sid, submission.filename)\n                  for submission in g.source.submissions ]\n    if len(sub_paths) > 1:\n        args = [\"touch\"]\n        args.extend(sub_paths[:-1])\n        rc = subprocess.call(args)\n        if rc != 0:\n            app.logger.warning(\"Couldn't normalize submission timestamps (touch exited with %d)\" % rc)\n\n\n@app.route('/submit', methods=('POST',))\n@login_required\ndef submit():\n    msg = request.form['msg']\n    fh = request.files['fh']\n\n    fnames = []\n    journalist_filename = g.source.journalist_filename()\n\n    if msg:\n        g.source.interaction_count += 1\n        fnames.append(store.save_message_submission(g.sid, g.source.interaction_count,\n            journalist_filename, msg))\n        flash(\"Thanks! We received your message.\", \"notification\")\n    if fh:\n        g.source.interaction_count += 1\n        fnames.append(store.save_file_submission(g.sid, g.source.interaction_count,\n            journalist_filename, fh.filename, fh.stream))\n        flash('{} \"{}\".'.format(\"Thanks! We received your document\",\n                                fh.filename or '[unnamed]'), \"notification\")\n    for fname in fnames:\n        submission = Submission(g.source, fname)\n        db_session.add(submission)\n\n    if g.source.pending:\n        g.source.pending = False\n\n        # Generate a keypair now, if there's enough entropy (issue #303)\n        entropy_avail = int(open('/proc/sys/kernel/random/entropy_avail').read())\n        if entropy_avail >= 2400:\n            async_genkey(g.sid, g.codename)\n\n    g.source.last_updated = datetime.utcnow()\n    db_session.commit()\n    normalize_timestamps(g.sid)\n\n    return redirect(url_for('lookup'))\n\n\n@app.route('/delete', methods=('POST',))\n@login_required\ndef delete():\n    msgid = request.form['msgid']\n    assert '/' not in msgid\n    potential_files = os.listdir(g.loc)\n    if msgid not in potential_files:\n        abort(404)  # TODO are the checks necessary?\n    store.secure_unlink(store.path(g.sid, msgid))\n    flash(\"Reply deleted\", \"notification\")\n\n    return redirect(url_for('lookup'))\n\n\ndef valid_codename(codename):\n    return os.path.exists(store.path(crypto_util.hash_codename(codename)))\n\n@app.route('/login', methods=('GET', 'POST'))\ndef login():\n    if request.method == 'POST':\n        codename = request.form['codename']\n        try:\n            valid = valid_codename(codename)\n        except crypto_util.CryptoException:\n            pass\n        else:\n            if valid:\n                session.update(codename=codename, logged_in=True)\n                return redirect(url_for('lookup', from_login='1'))\n        flash(\"Sorry, that is not a recognized codename.\", \"error\")\n    return render_template('login.html')\n\n\n@app.route('/howto-disable-js')\ndef howto_disable_js():\n    return render_template(\"howto-disable-js.html\")\n\n\n@app.route('/tor2web-warning')\ndef tor2web_warning():\n    return render_template(\"tor2web-warning.html\")\n\n\n@app.route('/journalist-key')\ndef download_journalist_pubkey():\n    journalist_pubkey = crypto_util.gpg.export_keys(config.JOURNALIST_KEY)\n    return send_file(StringIO(journalist_pubkey),\n                     mimetype=\"application/pgp-keys\",\n                     attachment_filename=config.JOURNALIST_KEY + \".asc\",\n                     as_attachment=True)\n\n\n@app.route('/why-journalist-key')\ndef why_download_journalist_pubkey():\n    return render_template(\"why-journalist-key.html\")\n\n\n@app.errorhandler(404)\ndef page_not_found(error):\n    return render_template('notfound.html'), 404\n\n@app.errorhandler(500)\ndef internal_error(error):\n    return render_template('error.html'), 500\n\ndef write_pidfile():\n    pid = str(os.getpid())\n    with open(config.SOURCE_PIDFILE, 'w') as fp:\n        fp.write(pid)\n\nif __name__ == \"__main__\":\n    write_pidfile()\n    # TODO make sure debug is not on in production\n    app.run(debug=True, host='0.0.0.0', port=8080)\n\n", "path": "securedrop/source.py"}, {"content": null, "path": "securedrop/background.py"}]}