Implement email encryption and signing

This commit is contained in:
Drew DeVault 2016-10-22 12:03:30 -04:00
parent 35804df168
commit a423d3de86
7 changed files with 102 additions and 20 deletions

1
.gitignore vendored
View File

@ -12,3 +12,4 @@ storage/
pip-selfcheck.json
.sass-cache/
overrides/
.pgp

View File

@ -54,8 +54,17 @@ site-name=sr.ht
# The source code for your fork of sr.ht:
source-url=https://git.sr.ht/~sircmpwn/sr.ht
#
# If "yes", payment will be required for account creation.
# If "yes", paid features will be enabled.
require-payment=yes
#
# Your PGP key information (DO NOT mix up pub and priv here)
# You must remove the password from your secret key, if present.
# You can do this with gpg --edit-key [key-id], then use the passwd
# command and do not enter a new password.
pgp-privkey=CHANGEME
pgp-pubkey=CHANGEME
# Hardcoded for effeciency's sake:
pgp-key-id=CHANGEME
[network]
files=https://sr.ht

18
emails/test Normal file
View File

@ -0,0 +1,18 @@
{{! vim: set ft=email }}
This is a test email sent from {{site-name}} to confirm that PGP is working as you
expect. This email is signed with this key:
{{site_key}}
{{#user.pgp_key}}and is encrypted with this key:
{{user.pgp_key.key_id}}
{{/user.pgp_key}}
You may control your PGP settings here:
{{root}}/privacy
--
{{owner-name}}
{{site-name}}

View File

@ -1,8 +1,11 @@
from flask import Blueprint, render_template, request, redirect
from flask import Blueprint, Response, render_template, request, redirect
from flask_login import current_user
from meta.audit import audit_log
from meta.validation import Validation
from meta.common import loginrequired
from meta.types import User, PGPKey
from meta.types import User, PGPKey, EventType
from meta.email import send_email
from meta.config import _cfg
from meta.db import db
privacy = Blueprint('privacy', __name__, template_folder='../../templates')
@ -12,6 +15,12 @@ privacy = Blueprint('privacy', __name__, template_folder='../../templates')
def privacy_GET():
return render_template("privacy.html")
@privacy.route("/privacy/pubkey")
def privacy_pubkey_GET():
with open(_cfg("sr.ht", "pgp-pubkey"), "r") as f:
pubkey = f.read()
return Response(pubkey, mimetype="text/plain")
@privacy.route("/privacy", methods=["POST"])
@loginrequired
def privacy_POST():
@ -30,6 +39,17 @@ def privacy_POST():
user = User.query.get(current_user.id)
user.pgp_key = key
audit_log(EventType.changed_pgp_key,
"Set default PGP key to {}".format(key.key_id if key else None))
db.commit()
return redirect("/privacy")
@privacy.route("/privacy/test-email", methods=["POST"])
@loginrequired
def privacy_testemail_POST():
user = User.query.get(current_user.id)
send_email("test", user.email, "Test email",
encrypt_key=user.pgp_key,
site_key=_cfg("sr.ht", "pgp-key-id"))
return redirect("/privacy")

View File

@ -1,22 +1,25 @@
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.message import Message
from flask_login import current_user
from meta.config import _cfg, _cfgi
from flask import url_for
import html.parser
import smtplib
import pystache
import html.parser
import pgpy
import os
from email.mime.text import MIMEText
from flask_login import current_user
from flask import url_for
from meta.config import _cfg, _cfgi
# TODO: move this into celery worker
site_key, _ = pgpy.PGPKey.from_file(_cfg("sr.ht", "pgp-privkey"))
def _url_for(ep, **kw):
return _cfg("server", "protocol") \
+ _cfg("server", "domain") \
+ url_for(ep, **kw)
def send_email(template, to, subject, **kwargs):
def send_email(template, to, subject, encrypt_key=None, **kwargs):
if _cfg("mail", "smtp-host") == "":
return
smtp = smtplib.SMTP(_cfg("mail", "smtp-host"), _cfgi("mail", "smtp-port"))
@ -24,7 +27,7 @@ def send_email(template, to, subject, **kwargs):
smtp.starttls()
smtp.login(_cfg("mail", "smtp-user"), _cfg("mail", "smtp-password"))
with open("emails/" + template) as f:
message = MIMEText(html.parser.HTMLParser().unescape(\
message = html.parser.HTMLParser().unescape(\
pystache.render(f.read(), {
'owner-name': _cfg('sr.ht', 'owner-name'),
'site-name': _cfg('sr.ht', 'site-name'),
@ -32,9 +35,38 @@ def send_email(template, to, subject, **kwargs):
'root': '{}://{}'.format(
_cfg('server', 'protocol'), _cfg('server', 'domain')),
**kwargs
})))
message['Subject'] = subject
message['From'] = _cfg("mail", "smtp-user")
message['To'] = to
smtp.sendmail(_cfg("mail", "smtp-user"), [to], message.as_string())
}))
multipart = MIMEMultipart(_subtype="signed", micalg="pgp-sha1",
protocol="application/pgp-signature")
text_part = MIMEText(message)
signature = str(site_key.sign(text_part.as_string().replace('\n', '\r\n')))
sig_part = Message()
sig_part['Content-Type'] = 'application/pgp-signature; name="signature.asc"'
sig_part['Content-Description'] = 'OpenPGP digital signature'
sig_part.set_payload(signature)
multipart.attach(text_part)
multipart.attach(sig_part)
if not encrypt_key:
multipart['Subject'] = subject
multipart['From'] = _cfg("mail", "smtp-user")
multipart['To'] = to
smtp.sendmail(_cfg("mail", "smtp-user"), [to], multipart.as_string(unixfrom=True))
else:
pubkey, _ = pgpy.PGPKey.from_blob(encrypt_key.key.replace('\r', '').encode('utf-8'))
pgp_msg = pgpy.PGPMessage.new(multipart.as_string(unixfrom=True))
encrypted = str(pubkey.encrypt(pgp_msg))
ver_part = Message()
ver_part['Content-Type'] = 'application/pgp-encrypted'
ver_part.set_payload("Version: 1")
enc_part = Message()
enc_part['Content-Type'] = 'application/octet-stream; name="message.asc"'
enc_part['Content-Description'] = 'OpenPGP encrypted message'
enc_part.set_payload(encrypted)
wrapped = MIMEMultipart(_subtype="encrypted", protocol="application/pgp-encrypted")
wrapped.attach(ver_part)
wrapped.attach(enc_part)
wrapped['Subject'] = subject
wrapped['From'] = _cfg("mail", "smtp-user")
wrapped['To'] = to
smtp.sendmail(_cfg("mail", "smtp-user"), [to], wrapped.as_string(unixfrom=True))
smtp.quit()

View File

@ -26,6 +26,7 @@ class EventType(Enum):
linked_external_account = "linked external account"
updated_credit_card = "updated credit card"
updated_billing_address = "updated billing address"
changed_pgp_key = "changed pgp key"
class AuditLogEntry(Base):
__tablename__ = 'audit_log_entry'

View File

@ -5,9 +5,7 @@
<h3>Encryption</h3>
<p>
All emails sent from sr.ht to you are signed with
<a href="https://pgp.mit.edu/pks/lookup?search={{_cfg("sr.ht", "pgp-key-id")}}&op=index">
{{_cfg("sr.ht", "pgp-key-id")}}
</a>.
<a href="/privacy/pubkey">{{_cfg("sr.ht", "pgp-key-id")}}</a>.
</p>
{% if any(current_user.pgp_keys) %}
<form method="POST" action="/privacy">
@ -37,6 +35,9 @@
{% endfor %}
<button type="submit" class="pull-right btn btn-default">Save</button>
</form>
<form method="POST" action="/privacy/test-email" style="clear: both; padding-top: 0.5rem">
<button type="submit" class="pull-right btn btn-default">Send test email</button>
</form>
{% else %}
<p>If you <a href="/keys">add a PGP key</a> to your account, we can encrypt
emails to you.</p>