a423d3de86cc0ed95c094afb2cdbac572bc854f1 — Drew DeVault 4 years ago 35804df
Implement email encryption and signing
M .gitignore => .gitignore +1 -0
@@ 12,3 12,4 @@ storage/

M config.ini.example => config.ini.example +10 -1
@@ 54,8 54,17 @@ site-name=sr.ht
# The source code for your fork of sr.ht:
# If "yes", payment will be required for account creation.
# If "yes", paid features will be enabled.
# 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.
# Hardcoded for effeciency's sake:


A emails/test => emails/test +18 -0
@@ 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:


{{#user.pgp_key}}and is encrypted with this key:


You may control your PGP settings here:



M meta/blueprints/privacy.py => meta/blueprints/privacy.py +22 -2
@@ 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")

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"])
def privacy_POST():

@@ 30,6 39,17 @@ def privacy_POST():

    user = User.query.get(current_user.id)
    user.pgp_key = key
            "Set default PGP key to {}".format(key.key_id if key else None))

    return redirect("/privacy")

@privacy.route("/privacy/test-email", methods=["POST"])
def privacy_testemail_POST():
    user = User.query.get(current_user.id)
    send_email("test", user.email, "Test email",
            site_key=_cfg("sr.ht", "pgp-key-id"))
    return redirect("/privacy")

M meta/email.py => meta/email.py +46 -14
@@ 1,22 1,25 @@
import smtplib
import pystache
import html.parser
import os

from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.message import Message
from flask_login import current_user
from flask import url_for

from meta.config import _cfg, _cfgi
from flask import url_for
import html.parser
import smtplib
import pystache
import pgpy
import os

# 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") == "":
    smtp = smtplib.SMTP(_cfg("mail", "smtp-host"), _cfgi("mail", "smtp-port"))

@@ 24,7 27,7 @@ def send_email(template, to, subject, **kwargs):
    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')),
    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",
    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'
    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))
        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'
        wrapped = MIMEMultipart(_subtype="encrypted", protocol="application/pgp-encrypted")
        wrapped['Subject'] = subject
        wrapped['From'] = _cfg("mail", "smtp-user")
        wrapped['To'] = to
        smtp.sendmail(_cfg("mail", "smtp-user"), [to], wrapped.as_string(unixfrom=True))

M meta/types/auditlogentry.py => meta/types/auditlogentry.py +1 -0
@@ 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'

M templates/privacy.html => templates/privacy.html +4 -3
@@ 5,9 5,7 @@
      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 href="/privacy/pubkey">{{_cfg("sr.ht", "pgp-key-id")}}</a>.
    {% 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 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>
    {% else %}
    <p>If you <a href="/keys">add a PGP key</a> to your account, we can encrypt
    emails to you.</p>