Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove REQUIRE_PGP env var and instead always require PGP #881

Merged
merged 5 commits into from
Jan 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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