prompt: We are currently solving the following issue within our repository. Here is the issue text:
  --- BEGIN ISSUE ---
  Initial messages containing non-ascii characters fail if codename filtering is enabled.
  ## Description
  
  Codename filtering was introduced in 2.3.0, allowing admins to block initial submissions containing only the user's codename, as they should not be shared with journalists. The filter uses the `compare_digest()` function to ensure constant-time comparison, but this fn will throw a `TypeError` if any of the strings being compared  contain Unicode.
  
  ## Steps to Reproduce
  
  - start up `make dev` on 2.4.0
  - visit the JI and enable codename filtering under Admin > Instance Config
  - visit the SI, create a new source, and submit an initial message containing unicode, ie `Hallo! ö, ü, ä, or ß`
  
  ## Expected Behavior
  - Message is submitted
  
  ## Actual Behavior
  - 500 error, and (in dev) stack trace due to TypeError
  
  ## Comments
  
  Suggestions to fix, any other relevant information.
  
  
  --- 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_app/utils.py`
  Content:
  ```
  1 import json
  2 import re
  3 import subprocess
  4 import typing
  5 from hmac import compare_digest
  6 
  7 import werkzeug
  8 from flask import current_app, flash, redirect, render_template, url_for
  9 from flask.sessions import SessionMixin
  10 from flask_babel import gettext
  11 from markupsafe import Markup, escape
  12 from source_user import SourceUser
  13 from store import Storage
  14 
  15 if typing.TYPE_CHECKING:
  16     from typing import Optional
  17 
  18 
  19 def codename_detected(message: str, codename: str) -> bool:
  20     """
  21     Check for codenames in incoming messages. including case where user copy/pasted
  22     from /generate or the codename widget on the same page
  23     """
  24     message = message.strip()
  25 
  26     return compare_digest(message.strip(), codename)
  27 
  28 
  29 def flash_msg(
  30     category: str,
  31     declarative: "Optional[str]",
  32     *msg_contents: "str",
  33 ) -> None:
  34     """
  35     Render flash message with a (currently) optional declarative heading.
  36     """
  37     contents = Markup("<br>".join([escape(part) for part in msg_contents]))
  38 
  39     msg = render_template(
  40         "flash_message.html",
  41         declarative=declarative,
  42         msg_contents=contents,
  43     )
  44     flash(Markup(msg), category)
  45 
  46 
  47 def clear_session_and_redirect_to_logged_out_page(flask_session: SessionMixin) -> werkzeug.Response:
  48     msg = render_template(
  49         "flash_message.html",
  50         declarative=gettext("Important"),
  51         msg_contents=Markup(
  52             gettext(
  53                 'You were logged out due to inactivity. Click the <img src={icon} alt="" '
  54                 'width="16" height="16">&nbsp;<b>New Identity</b> button in your Tor Browser\'s '
  55                 "toolbar before moving on. This will clear your Tor Browser activity data on "
  56                 "this device."
  57             ).format(icon=url_for("static", filename="i/torbroom.png"))
  58         ),
  59     )
  60 
  61     # Clear the session after we render the message so it's localized
  62     flask_session.clear()
  63 
  64     flash(Markup(msg), "error")
  65     return redirect(url_for("main.index"))
  66 
  67 
  68 def normalize_timestamps(logged_in_source: SourceUser) -> None:
  69     """
  70     Update the timestamps on all of the source's submissions. This
  71     minimizes metadata that could be useful to investigators. See
  72     #301.
  73     """
  74     source_in_db = logged_in_source.get_db_record()
  75     sub_paths = [
  76         Storage.get_default().path(logged_in_source.filesystem_id, submission.filename)
  77         for submission in source_in_db.submissions
  78     ]
  79     if len(sub_paths) > 1:
  80         args = ["touch", "--no-create"]
  81         args.extend(sub_paths)
  82         rc = subprocess.call(args)
  83         if rc != 0:
  84             current_app.logger.warning(
  85                 "Couldn't normalize submission " "timestamps (touch exited with %d)" % rc
  86             )
  87 
  88 
  89 def check_url_file(path: str, regexp: str) -> "Optional[str]":
  90     """
  91     Check that a file exists at the path given and contains a single line
  92     matching the regexp. Used for checking the source interface address
  93     files in /var/lib/securedrop (as the Apache user can't read Tor config)
  94     """
  95     try:
  96         f = open(path, "r")
  97         contents = f.readline().strip()
  98         f.close()
  99         if re.match(regexp, contents):
  100             return contents
  101         else:
  102             return None
  103     except IOError:
  104         return None
  105 
  106 
  107 def get_sourcev3_url() -> "Optional[str]":
  108     return check_url_file("/var/lib/securedrop/source_v3_url", r"^[a-z0-9]{56}\.onion$")
  109 
  110 
  111 def fit_codenames_into_cookie(codenames: dict) -> dict:
  112     """
  113     If `codenames` will approach `werkzeug.Response.max_cookie_size` once
  114     serialized, incrementally pop off the oldest codename until the remaining
  115     (newer) ones will fit.
  116     """
  117 
  118     serialized = json.dumps(codenames).encode()
  119     if len(codenames) > 1 and len(serialized) > 4000:  # werkzeug.Response.max_cookie_size = 4093
  120         if current_app:
  121             current_app.logger.warn(
  122                 f"Popping oldest of {len(codenames)} "
  123                 f"codenames ({len(serialized)} bytes) to "
  124                 f"fit within maximum cookie size"
  125             )
  126         del codenames[list(codenames)[0]]  # FIFO
  127 
  128         return fit_codenames_into_cookie(codenames)
  129 
  130     return codenames
  131
  ```
  --- 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()
  
  ```
  
golden_diff: diff --git a/securedrop/source_app/utils.py b/securedrop/source_app/utils.py
  --- a/securedrop/source_app/utils.py
  +++ b/securedrop/source_app/utils.py
  @@ -23,7 +23,7 @@
       """
       message = message.strip()
   
  -    return compare_digest(message.strip(), codename)
  +    return compare_digest(message.strip().encode("utf-8"), codename.encode("utf-8"))
   
   
   def flash_msg(
  
verification_info: {"golden_diff": "diff --git a/securedrop/source_app/utils.py b/securedrop/source_app/utils.py\n--- a/securedrop/source_app/utils.py\n+++ b/securedrop/source_app/utils.py\n@@ -23,7 +23,7 @@\n     \"\"\"\n     message = message.strip()\n \n-    return compare_digest(message.strip(), codename)\n+    return compare_digest(message.strip().encode(\"utf-8\"), codename.encode(\"utf-8\"))\n \n \n def flash_msg(\n", "issue": "Initial messages containing non-ascii characters fail if codename filtering is enabled.\n## Description\r\n\r\nCodename filtering was introduced in 2.3.0, allowing admins to block initial submissions containing only the user's codename, as they should not be shared with journalists. The filter uses the `compare_digest()` function to ensure constant-time comparison, but this fn will throw a `TypeError` if any of the strings being compared  contain Unicode.\r\n\r\n## Steps to Reproduce\r\n\r\n- start up `make dev` on 2.4.0\r\n- visit the JI and enable codename filtering under Admin > Instance Config\r\n- visit the SI, create a new source, and submit an initial message containing unicode, ie `Hallo! \u00f6, \u00fc, \u00e4, or \u00df`\r\n\r\n## Expected Behavior\r\n- Message is submitted\r\n\r\n## Actual Behavior\r\n- 500 error, and (in dev) stack trace due to TypeError\r\n\r\n## Comments\r\n\r\nSuggestions to fix, any other relevant information.\r\n\n", "before_files": [{"content": "import json\nimport re\nimport subprocess\nimport typing\nfrom hmac import compare_digest\n\nimport werkzeug\nfrom flask import current_app, flash, redirect, render_template, url_for\nfrom flask.sessions import SessionMixin\nfrom flask_babel import gettext\nfrom markupsafe import Markup, escape\nfrom source_user import SourceUser\nfrom store import Storage\n\nif typing.TYPE_CHECKING:\n    from typing import Optional\n\n\ndef codename_detected(message: str, codename: str) -> bool:\n    \"\"\"\n    Check for codenames in incoming messages. including case where user copy/pasted\n    from /generate or the codename widget on the same page\n    \"\"\"\n    message = message.strip()\n\n    return compare_digest(message.strip(), codename)\n\n\ndef flash_msg(\n    category: str,\n    declarative: \"Optional[str]\",\n    *msg_contents: \"str\",\n) -> None:\n    \"\"\"\n    Render flash message with a (currently) optional declarative heading.\n    \"\"\"\n    contents = Markup(\"<br>\".join([escape(part) for part in msg_contents]))\n\n    msg = render_template(\n        \"flash_message.html\",\n        declarative=declarative,\n        msg_contents=contents,\n    )\n    flash(Markup(msg), category)\n\n\ndef clear_session_and_redirect_to_logged_out_page(flask_session: SessionMixin) -> werkzeug.Response:\n    msg = render_template(\n        \"flash_message.html\",\n        declarative=gettext(\"Important\"),\n        msg_contents=Markup(\n            gettext(\n                'You were logged out due to inactivity. Click the <img src={icon} alt=\"\" '\n                'width=\"16\" height=\"16\">&nbsp;<b>New Identity</b> button in your Tor Browser\\'s '\n                \"toolbar before moving on. This will clear your Tor Browser activity data on \"\n                \"this device.\"\n            ).format(icon=url_for(\"static\", filename=\"i/torbroom.png\"))\n        ),\n    )\n\n    # Clear the session after we render the message so it's localized\n    flask_session.clear()\n\n    flash(Markup(msg), \"error\")\n    return redirect(url_for(\"main.index\"))\n\n\ndef normalize_timestamps(logged_in_source: SourceUser) -> None:\n    \"\"\"\n    Update the timestamps on all of the source's submissions. This\n    minimizes metadata that could be useful to investigators. See\n    #301.\n    \"\"\"\n    source_in_db = logged_in_source.get_db_record()\n    sub_paths = [\n        Storage.get_default().path(logged_in_source.filesystem_id, submission.filename)\n        for submission in source_in_db.submissions\n    ]\n    if len(sub_paths) > 1:\n        args = [\"touch\", \"--no-create\"]\n        args.extend(sub_paths)\n        rc = subprocess.call(args)\n        if rc != 0:\n            current_app.logger.warning(\n                \"Couldn't normalize submission \" \"timestamps (touch exited with %d)\" % rc\n            )\n\n\ndef check_url_file(path: str, regexp: str) -> \"Optional[str]\":\n    \"\"\"\n    Check that a file exists at the path given and contains a single line\n    matching the regexp. Used for checking the source interface address\n    files in /var/lib/securedrop (as the Apache user can't read Tor config)\n    \"\"\"\n    try:\n        f = open(path, \"r\")\n        contents = f.readline().strip()\n        f.close()\n        if re.match(regexp, contents):\n            return contents\n        else:\n            return None\n    except IOError:\n        return None\n\n\ndef get_sourcev3_url() -> \"Optional[str]\":\n    return check_url_file(\"/var/lib/securedrop/source_v3_url\", r\"^[a-z0-9]{56}\\.onion$\")\n\n\ndef fit_codenames_into_cookie(codenames: dict) -> dict:\n    \"\"\"\n    If `codenames` will approach `werkzeug.Response.max_cookie_size` once\n    serialized, incrementally pop off the oldest codename until the remaining\n    (newer) ones will fit.\n    \"\"\"\n\n    serialized = json.dumps(codenames).encode()\n    if len(codenames) > 1 and len(serialized) > 4000:  # werkzeug.Response.max_cookie_size = 4093\n        if current_app:\n            current_app.logger.warn(\n                f\"Popping oldest of {len(codenames)} \"\n                f\"codenames ({len(serialized)} bytes) to \"\n                f\"fit within maximum cookie size\"\n            )\n        del codenames[list(codenames)[0]]  # FIFO\n\n        return fit_codenames_into_cookie(codenames)\n\n    return codenames\n", "path": "securedrop/source_app/utils.py"}], "after_files": [{"content": "import json\nimport re\nimport subprocess\nimport typing\nfrom hmac import compare_digest\n\nimport werkzeug\nfrom flask import current_app, flash, redirect, render_template, url_for\nfrom flask.sessions import SessionMixin\nfrom flask_babel import gettext\nfrom markupsafe import Markup, escape\nfrom source_user import SourceUser\nfrom store import Storage\n\nif typing.TYPE_CHECKING:\n    from typing import Optional\n\n\ndef codename_detected(message: str, codename: str) -> bool:\n    \"\"\"\n    Check for codenames in incoming messages. including case where user copy/pasted\n    from /generate or the codename widget on the same page\n    \"\"\"\n    message = message.strip()\n\n    return compare_digest(message.strip().encode(\"utf-8\"), codename.encode(\"utf-8\"))\n\n\ndef flash_msg(\n    category: str,\n    declarative: \"Optional[str]\",\n    *msg_contents: \"str\",\n) -> None:\n    \"\"\"\n    Render flash message with a (currently) optional declarative heading.\n    \"\"\"\n    contents = Markup(\"<br>\".join([escape(part) for part in msg_contents]))\n\n    msg = render_template(\n        \"flash_message.html\",\n        declarative=declarative,\n        msg_contents=contents,\n    )\n    flash(Markup(msg), category)\n\n\ndef clear_session_and_redirect_to_logged_out_page(flask_session: SessionMixin) -> werkzeug.Response:\n    msg = render_template(\n        \"flash_message.html\",\n        declarative=gettext(\"Important\"),\n        msg_contents=Markup(\n            gettext(\n                'You were logged out due to inactivity. Click the <img src={icon} alt=\"\" '\n                'width=\"16\" height=\"16\">&nbsp;<b>New Identity</b> button in your Tor Browser\\'s '\n                \"toolbar before moving on. This will clear your Tor Browser activity data on \"\n                \"this device.\"\n            ).format(icon=url_for(\"static\", filename=\"i/torbroom.png\"))\n        ),\n    )\n\n    # Clear the session after we render the message so it's localized\n    flask_session.clear()\n\n    flash(Markup(msg), \"error\")\n    return redirect(url_for(\"main.index\"))\n\n\ndef normalize_timestamps(logged_in_source: SourceUser) -> None:\n    \"\"\"\n    Update the timestamps on all of the source's submissions. This\n    minimizes metadata that could be useful to investigators. See\n    #301.\n    \"\"\"\n    source_in_db = logged_in_source.get_db_record()\n    sub_paths = [\n        Storage.get_default().path(logged_in_source.filesystem_id, submission.filename)\n        for submission in source_in_db.submissions\n    ]\n    if len(sub_paths) > 1:\n        args = [\"touch\", \"--no-create\"]\n        args.extend(sub_paths)\n        rc = subprocess.call(args)\n        if rc != 0:\n            current_app.logger.warning(\n                \"Couldn't normalize submission \" \"timestamps (touch exited with %d)\" % rc\n            )\n\n\ndef check_url_file(path: str, regexp: str) -> \"Optional[str]\":\n    \"\"\"\n    Check that a file exists at the path given and contains a single line\n    matching the regexp. Used for checking the source interface address\n    files in /var/lib/securedrop (as the Apache user can't read Tor config)\n    \"\"\"\n    try:\n        f = open(path, \"r\")\n        contents = f.readline().strip()\n        f.close()\n        if re.match(regexp, contents):\n            return contents\n        else:\n            return None\n    except IOError:\n        return None\n\n\ndef get_sourcev3_url() -> \"Optional[str]\":\n    return check_url_file(\"/var/lib/securedrop/source_v3_url\", r\"^[a-z0-9]{56}\\.onion$\")\n\n\ndef fit_codenames_into_cookie(codenames: dict) -> dict:\n    \"\"\"\n    If `codenames` will approach `werkzeug.Response.max_cookie_size` once\n    serialized, incrementally pop off the oldest codename until the remaining\n    (newer) ones will fit.\n    \"\"\"\n\n    serialized = json.dumps(codenames).encode()\n    if len(codenames) > 1 and len(serialized) > 4000:  # werkzeug.Response.max_cookie_size = 4093\n        if current_app:\n            current_app.logger.warn(\n                f\"Popping oldest of {len(codenames)} \"\n                f\"codenames ({len(serialized)} bytes) to \"\n                f\"fit within maximum cookie size\"\n            )\n        del codenames[list(codenames)[0]]  # FIFO\n\n        return fit_codenames_into_cookie(codenames)\n\n    return codenames\n", "path": "securedrop/source_app/utils.py"}]}