Skip to content

Commit

Permalink
Merge pull request #881 from scidsg/always-require-pgp
Browse files Browse the repository at this point in the history
Remove REQUIRE_PGP env var and instead always require PGP
  • Loading branch information
brassy-endomorph authored Jan 28, 2025
2 parents 33420c7 + 638cd2f commit 3e4eba6
Show file tree
Hide file tree
Showing 8 changed files with 61 additions and 82 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,20 +27,24 @@
## In The Media

### Newsweek

“Investing in technology that protects privacy—such as Hush Line and Signal—is also important in sharing information that is anonymous, and can't be subpoenaed.”<br>
https://www.newsweek.com/protecting-free-speech-about-more-letting-content-run-wild-opinion-2012746<br>
https://web.archive.org/web/20250111062609/https://www.newsweek.com/protecting-free-speech-about-more-letting-content-run-wild-opinion-2012746

### TIME

"Psst’s safe is based on Hush Line, a tool designed by the nonprofit Science & Design, Inc., as a simpler way for sources to reach out to journalists and lawyers. It’s a one-way conversation system, essentially functioning as a tip-line. Micah Lee, an engineer on Hush Line, says that the tool fills a gap in the market for an encrypted yet accessible central clearinghouse for sensitive information."<br>
https://time.com/7208911/psst-whistleblower-collective/<br>
https://web.archive.org/web/20250122105330/https://time.com/7208911/psst-whistleblower-collective/

### Substack

“New systems in development, such as Hush Line, developed by entrepreneur Glenn Sorrentino, are the brave new frontier in reporting. Hush Line is a software application that offers a more secure ability to report anonymously.”<br>
https://zacharyellison.substack.com/p/part-151-playing-the-whistleblower

### Podcasts

"I'm working with a a non-profit software company called Science and Design that's worked on a number of really interesting products that are kind of nerdy and more on the journalism space, but they're working on something called Hush Line, which is a one-way encrypted anonymizing platform so that whistleblowers can reach out to individual journalists while remaining anonymous... it provides a non-technical person a way and a path and information, should they find themselves in a whistleblowing position, to not mitigate the danger because it's never not going to be dangerous, but prepare them for the process and give them an easy-to-use modern tool to responsibly disclose information to trustworthy journalists..."<br>
_Around the Bend_<br>
https://www.youtube.com/watch?v=pO6q_t0wGGA&t=38m17s
Expand Down
7 changes: 0 additions & 7 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -295,13 +295,6 @@ These configs are needed for the web app.
<td><code>true</code></td>
<td>Whether or not new user registrations require invite codes.</td>
</tr>
<tr>
<td><code>REQUIRE_PGP</code></td>
<td>false</td>
<td>boolean</td>
<td><code>false</code></td>
<td>Whether users are required to use PGP to receive messages.</td>
</tr>
<tr>
<td><code>SECRET_KEY</code></td>
<td>true</td>
Expand Down
1 change: 0 additions & 1 deletion hushline/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,6 @@ def _load_hushline_misc(env: Mapping[str, str]) -> Mapping[str, Any]:
("DIRECTORY_VERIFIED_TAB_ENABLED", True),
("FILE_UPLOADS_ENABLED", False),
("REGISTRATION_CODES_REQUIRED", True),
("REQUIRE_PGP", False),
]
for key, default in bool_configs:
if value := env.get(key):
Expand Down
1 change: 0 additions & 1 deletion hushline/routes/profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,5 @@ def profile(username: str) -> Response | str:
display_name_or_username=uname.display_name or uname.username,
current_user_id=session.get("user_id"),
public_key=uname.user.pgp_key,
require_pgp=app.config["REQUIRE_PGP"],
math_problem=math_problem,
)
2 changes: 1 addition & 1 deletion hushline/routes/submit.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ def submit_message(username: str) -> Response | str:
return redirect(url_for("index"))

if form.validate_on_submit():
if not uname.user.pgp_key and app.config["REQUIRE_PGP"]:
if not uname.user.pgp_key:
flash("⛔️ You cannot submit messages to users who have not set a PGP key.", "error")
return redirect(url_for("profile", username=username))

Expand Down
93 changes: 35 additions & 58 deletions hushline/templates/profile.html
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
{% extends "base.html" %}

{% set pgp_required_but_not_set = require_pgp and not user.pgp_key %}

{% block title %}
{% if (not require_pgp) or (require_pgp and user.pgp_key) %}
{% if user.pgp_key %}
{{ profile_header }}
{% else %}
Profile: {{ display_name_or_username }}
Expand All @@ -21,11 +19,11 @@ <h2 class="submit">
✍️ Note to Self (Add a PGP Key for secure notes)
{% endif %}
{% else %}
{% if (not require_pgp) or (require_pgp and user.pgp_key) %}
{# Unauthenticated or other users who meet PGP requirements or don’t require PGP #}
{% if user.pgp_key %}
{# Unauthenticated or other users who meet PGP requirements #}
{{ profile_header }}
{% else %}
{# Unauthenticated or other users who dont meet PGP requirements #}
{# Unauthenticated or other users who don't meet PGP requirements #}
{{ display_name_or_username }}
{% endif %}
{% endif %}
Expand Down Expand Up @@ -81,7 +79,7 @@ <h2 class="submit">

{% if current_user_id == user.id %}
<p class="instr">
{% if pgp_required_but_not_set %}
{% if not user.pgp_key %}
<b>👁️ Only visible to you:</b> In order to protect your sources, you need
to add a PGP key to your account before anyone can send you tips.
Without a PGP key, your profile can still be listed in the Hush Line
Expand All @@ -106,7 +104,7 @@ <h2 class="submit">
action="{{ url_for('submit_message', username=username.username) }}"
id="messageForm"
>
{% if not pgp_required_but_not_set %}
{% if not user.pgp_key %}
{{ form.hidden_tag() }}
{% endif %}

Expand All @@ -116,7 +114,7 @@ <h2 class="submit">
id="contact_method"
name="contact_method"
value="{{ form.contact_method.data if form.contact_method.data is not none else '' }}"
{% if pgp_required_but_not_set %}disabled="disabled"{% endif %}
{% if not user.pgp_key %}disabled="disabled"{% endif %}
/>

<label for="content">Message</label>
Expand All @@ -126,56 +124,34 @@ <h2 class="submit">
name="content"
required=""
spellcheck="true"
{% if pgp_required_but_not_set %}disabled="disabled"{% endif %}
{% if not user.pgp_key %}disabled="disabled"{% endif %}
>{{ form.content.data if form.content.data is not none else '' }}</textarea>

{% if not pgp_required_but_not_set %}
<!-- Hidden field for public PGP key -->
<input type="hidden" id="publicKey" value="{{ user.pgp_key }}" />
<!-- Hidden field to indicate if the message was encrypted client-side -->
<input
type="hidden"
name="client_side_encrypted"
id="clientSideEncrypted"
value="false"
/>
<!-- Hidden field for public PGP key -->
<input type="hidden" id="publicKey" value="{{ user.pgp_key }}" />
<!-- Hidden field to indicate if the message was encrypted client-side -->
<input
type="hidden"
name="client_side_encrypted"
id="clientSideEncrypted"
value="false"
/>

{% if current_user_id == user.id %}
{% if user.pgp_key %}
<p class="helper meta">
🔐 Your message will be encrypted and only readable by you.
</p>
{% else %}
<p class="helper meta">
⚠️ Your messages will NOT be encrypted. If you expect messages to
contain sensitive information, please
<a
href="https://github.com/scidsg/hushline/blob/main/docs/1-getting-started.md"
target="_blank"
rel="noopener noreferrer"
>add a public PGP key</a
>.
</p>
{% endif %}
{% else %}
{% if user.pgp_key %}
<p class="helper meta">
🔐 Your message will be encrypted and only readable by
{{ display_name_or_username }}.
</p>
{% else %}
<p class="helper meta">
⚠️ Your message will NOT be encrypted. If this message is sensitive,
ask {{ display_name_or_username }} to add a public PGP key.
<a
href="https://github.com/scidsg/hushline/blob/main/docs/1-getting-started.md"
target="_blank"
rel="noopener noreferrer"
>Here's how they can do it</a
>.
</p>
{% endif %}
{% if current_user_id == user.id %}
{% if user.pgp_key %}
<p class="helper meta">
🔐 Your message will be encrypted and only readable by you.
</p>
{% endif %}
{% else %}
{% if user.pgp_key %}
<p class="helper meta">
🔐 Your message will be encrypted and only readable by
{{ display_name_or_username }}.
</p>
{% endif %}
{% endif %}
{% if user.pgp_key %}
<!-- Math CAPTCHA -->
<div class="captcha">
<p>🤖 Solve the math problem to submit your message.</p>
Expand All @@ -192,15 +168,16 @@ <h2 class="submit">
</div>
</div>
{% endif %}

<button
type="submit"
id="submitBtn"
{% if pgp_required_but_not_set %}disabled="disabled"{% endif %}
{% if not user.pgp_key %}disabled="disabled"{% endif %}
>
Send Message
</button>

{% if pgp_required_but_not_set %}
{% if not user.pgp_key %}
<div class="pgp-disabled-overlay">
<p>
🔒<br />
Expand All @@ -213,7 +190,7 @@ <h2 class="submit">
{% endblock %}

{% block scripts %}
{% if not pgp_required_but_not_set %}
{% if user.pgp_key %}
<script src="{{ url_for('static', filename='vendor/openpgp-5.11.1.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/client-side-encryption.js') }}"></script>
<script src="{{ url_for('static', filename='js/submit-message.js') }}"></script>
Expand Down
32 changes: 18 additions & 14 deletions tests/test_profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
from hushline.db import db
from hushline.model import Message, OrganizationSetting, User, Username

pgp_message_sig = "-----BEGIN PGP MESSAGE-----\n\n"


def get_captcha_from_session(client: FlaskClient, username: str) -> str:
# Simulate loading the profile page to generate and retrieve the CAPTCHA from the session
Expand All @@ -20,6 +22,7 @@ def get_captcha_from_session(client: FlaskClient, username: str) -> str:
return captcha_answer


@pytest.mark.usefixtures("_pgp_user")
def test_profile_header(client: FlaskClient, user: User) -> None:
assert (
db.session.scalars(
Expand Down Expand Up @@ -53,6 +56,7 @@ def test_profile_header(client: FlaskClient, user: User) -> None:


@pytest.mark.usefixtures("_authenticated_user")
@pytest.mark.usefixtures("_pgp_user")
def test_profile_submit_message(client: FlaskClient, user: User) -> None:
msg_content = "This is a test message."

Expand All @@ -71,16 +75,17 @@ def test_profile_submit_message(client: FlaskClient, user: User) -> None:
message = db.session.scalars(
db.select(Message).filter_by(username_id=user.primary_username.id)
).one()
assert message.content == msg_content
assert pgp_message_sig in message.content

response = client.get(
url_for("inbox", username=user.primary_username.username), follow_redirects=True
)
assert response.status_code == 200
assert msg_content in response.text, response.text
assert pgp_message_sig in response.text, response.text


@pytest.mark.usefixtures("_authenticated_user")
@pytest.mark.usefixtures("_pgp_user")
def test_profile_submit_message_to_alias(
client: FlaskClient, user: User, user_alias: Username
) -> None:
Expand All @@ -99,18 +104,18 @@ def test_profile_submit_message_to_alias(
assert "Message submitted successfully." in response.text

message = db.session.scalars(db.select(Message).filter_by(username_id=user_alias.id)).one()
assert message.content == msg_content
assert pgp_message_sig in message.content

response = client.get(url_for("inbox", username=user_alias.username), follow_redirects=True)
assert response.status_code == 200
assert msg_content in response.text, response.text
assert pgp_message_sig in response.text, response.text


@pytest.mark.usefixtures("_authenticated_user")
@pytest.mark.usefixtures("_pgp_user")
def test_profile_submit_message_with_contact_method(client: FlaskClient, user: User) -> None:
message_content = "This is a test message."
contact_method = "[email protected]"
expected_content = f"Contact Method: {contact_method}\n\n{message_content}"

response = client.post(
url_for("profile", username=user.primary_username.username),
Expand All @@ -128,31 +133,29 @@ def test_profile_submit_message_with_contact_method(client: FlaskClient, user: U
message = db.session.scalars(
db.select(Message).filter_by(username_id=user.primary_username.id)
).one()
assert message.content == expected_content
assert pgp_message_sig in message.content

response = client.get(
url_for("inbox", username=user.primary_username.username), follow_redirects=True
)
assert response.status_code == 200
assert expected_content in response.text
assert pgp_message_sig in response.text


@pytest.mark.usefixtures("_authenticated_user")
def test_profile_pgp_required(client: FlaskClient, app: Flask, user: User) -> None:
app.config["REQUIRE_PGP"] = True

response = client.get(url_for("profile", username=user.primary_username.username))
assert response.status_code == 200
assert "Sending messages is disabled" in response.text

user.pgp_key = "test_pgp_key"
assert 'id="messageForm"' in response.text
assert "You can't send encrypted messages to this user through Hush Line" not in response.text

user.pgp_key = None
db.session.commit()

response = client.get(url_for("profile", username=user.primary_username.username))
assert response.status_code == 200

assert 'id="messageForm"' in response.text
assert "You can't send encrypted messages to this user through Hush Line" not in response.text
assert "Sending messages is disabled" in response.text


@pytest.mark.usefixtures("_authenticated_user")
Expand Down Expand Up @@ -190,6 +193,7 @@ def test_profile_extra_fields(client: FlaskClient, app: Flask, user: User) -> No


@pytest.mark.usefixtures("_authenticated_user")
@pytest.mark.usefixtures("_pgp_user")
def test_profile_submit_message_with_invalid_captcha(client: FlaskClient, user: User) -> None:
message_content = "This is a test message."
contact_method = "[email protected]"
Expand Down
3 changes: 3 additions & 0 deletions tests/test_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,9 @@ def test_add_invalid_pgp_key(client: FlaskClient, user: User) -> None:
@pytest.mark.usefixtures("_authenticated_user")
@patch("hushline.email.smtplib.SMTP")
def test_update_smtp_settings_no_pgp(SMTP: MagicMock, client: FlaskClient, user: User) -> None:
user.pgp_key = None
db.session.commit()

response = client.post(
url_for("settings.email"),
# for some reason using the Form class doesn't work here. why? fuck if i know.
Expand Down

0 comments on commit 3e4eba6

Please sign in to comment.