commit
3266a5982a
|
@ -38,7 +38,7 @@ def auth_mfa():
|
||||||
s = Signer(FLASK_SECRET)
|
s = Signer(FLASK_SECRET)
|
||||||
try:
|
try:
|
||||||
user_id = int(s.unsign(mfa_key))
|
user_id = int(s.unsign(mfa_key))
|
||||||
except BadSignature:
|
except Exception:
|
||||||
return jsonify(error="Invalid mfa_key"), 400
|
return jsonify(error="Invalid mfa_key"), 400
|
||||||
|
|
||||||
user = User.get(user_id)
|
user = User.get(user_id)
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
import os
|
import os
|
||||||
|
import random
|
||||||
|
import string
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import tempfile
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
@ -156,6 +159,17 @@ WORDS_FILE_PATH = get_abs_path(
|
||||||
os.environ.get("WORDS_FILE_PATH", "local_data/words_alpha.txt")
|
os.environ.get("WORDS_FILE_PATH", "local_data/words_alpha.txt")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Used to generate random email
|
||||||
|
if os.environ.get("GNUPGHOME"):
|
||||||
|
GNUPGHOME = get_abs_path(os.environ.get("GNUPGHOME"))
|
||||||
|
else:
|
||||||
|
letters = string.ascii_lowercase
|
||||||
|
random_dir_name = "".join(random.choice(letters) for _ in range(20))
|
||||||
|
GNUPGHOME = f"/tmp/{random_dir_name}"
|
||||||
|
if not os.path.exists(GNUPGHOME):
|
||||||
|
os.mkdir(GNUPGHOME, mode=0o700)
|
||||||
|
|
||||||
|
print("WARNING: Use a temp directory for GNUPGHOME", GNUPGHOME)
|
||||||
|
|
||||||
# Github, Google, Facebook client id and secrets
|
# Github, Google, Facebook client id and secrets
|
||||||
GITHUB_CLIENT_ID = os.environ.get("GITHUB_CLIENT_ID")
|
GITHUB_CLIENT_ID = os.environ.get("GITHUB_CLIENT_ID")
|
||||||
|
|
|
@ -41,6 +41,10 @@
|
||||||
🚫
|
🚫
|
||||||
</span>
|
</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if mailbox.pgp_finger_print %}
|
||||||
|
<span class="cursor" data-toggle="tooltip" data-original-title="PGP Enabled">🗝</span>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if mailbox.id == current_user.default_mailbox_id %}
|
{% if mailbox.id == current_user.default_mailbox_id %}
|
||||||
<div class="badge badge-primary float-right" data-toggle="tooltip"
|
<div class="badge badge-primary float-right" data-toggle="tooltip"
|
||||||
title="When a new random alias is created, it belongs to the default mailbox">Default Mailbox
|
title="When a new random alias is created, it belongs to the default mailbox">Default Mailbox
|
||||||
|
|
|
@ -17,8 +17,19 @@
|
||||||
🚫
|
🚫
|
||||||
</span>
|
</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if mailbox.pgp_finger_print %}
|
||||||
|
<span class="cursor" data-toggle="tooltip" data-original-title="PGP Enabled">🗝</span>
|
||||||
|
{% endif %}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
|
{% if not mailbox.verified %}
|
||||||
|
<div class="alert alert-info">
|
||||||
|
Mailbox not verified, please check your inbox/spam folder for the verification email.
|
||||||
|
<br>
|
||||||
|
To receive the verification email again, you can delete and re-add the mailbox.
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<!-- Change email -->
|
<!-- Change email -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<form method="post" enctype="multipart/form-data">
|
<form method="post" enctype="multipart/form-data">
|
||||||
|
@ -52,6 +63,37 @@
|
||||||
</div>
|
</div>
|
||||||
<!-- END Change email -->
|
<!-- END Change email -->
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Change PGP Public key -->
|
||||||
|
{% if current_user.can_use_pgp %}
|
||||||
|
<div class="card">
|
||||||
|
<form method="post">
|
||||||
|
<input type="hidden" name="form-name" value="pgp">
|
||||||
|
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="card-title">
|
||||||
|
Pretty Good Privacy (PGP)
|
||||||
|
<div class="small-text">
|
||||||
|
By importing your PGP Public Key into SimpleLogin, all emails sent to {{mailbox.email}} are <b>encrypted</b> with your key.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">PGP Public Key</label>
|
||||||
|
|
||||||
|
<textarea name="pgp" class="form-control" rows=10 placeholder="-----BEGIN PGP PUBLIC KEY BLOCK-----">{{mailbox.pgp_public_key or ""}}</textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="btn btn-primary" name="action" value="save">Save</button>
|
||||||
|
<button class="btn btn-danger float-right" name="action" value="remove">Remove</button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<!-- END PGP Public key -->
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,7 @@ def billing():
|
||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
if request.form.get("form-name") == "cancel":
|
if request.form.get("form-name") == "cancel":
|
||||||
LOG.error(f"User {current_user} cancels their subscription")
|
LOG.warning(f"User {current_user} cancels their subscription")
|
||||||
success = cancel_subscription(sub.subscription_id)
|
success = cancel_subscription(sub.subscription_id)
|
||||||
|
|
||||||
if success:
|
if success:
|
||||||
|
|
|
@ -120,7 +120,11 @@ def mailbox_route():
|
||||||
"success",
|
"success",
|
||||||
)
|
)
|
||||||
|
|
||||||
return redirect(url_for("dashboard.mailbox_route"))
|
return redirect(
|
||||||
|
url_for(
|
||||||
|
"dashboard.mailbox_detail_route", mailbox_id=new_mailbox.id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
"dashboard/mailbox.html",
|
"dashboard/mailbox.html",
|
||||||
|
@ -138,8 +142,9 @@ def mailbox_verify():
|
||||||
|
|
||||||
try:
|
try:
|
||||||
r_id = int(s.unsign(mailbox_id))
|
r_id = int(s.unsign(mailbox_id))
|
||||||
except BadSignature:
|
except Exception:
|
||||||
flash("Invalid link. Please delete and re-add your mailbox", "error")
|
flash("Invalid link. Please delete and re-add your mailbox", "error")
|
||||||
|
return redirect(url_for("dashboard.mailbox_route"))
|
||||||
else:
|
else:
|
||||||
mailbox = Mailbox.get(r_id)
|
mailbox = Mailbox.get(r_id)
|
||||||
mailbox.verified = True
|
mailbox.verified = True
|
||||||
|
@ -150,4 +155,6 @@ def mailbox_verify():
|
||||||
f"The {mailbox.email} is now verified, you can start creating alias with it",
|
f"The {mailbox.email} is now verified, you can start creating alias with it",
|
||||||
"success",
|
"success",
|
||||||
)
|
)
|
||||||
return redirect(url_for("dashboard.mailbox_route"))
|
return redirect(
|
||||||
|
url_for("dashboard.mailbox_detail_route", mailbox_id=mailbox.id)
|
||||||
|
)
|
||||||
|
|
|
@ -14,6 +14,8 @@ from app.extensions import db
|
||||||
from app.log import LOG
|
from app.log import LOG
|
||||||
from app.models import GenEmail, DeletedAlias
|
from app.models import GenEmail, DeletedAlias
|
||||||
from app.models import Mailbox
|
from app.models import Mailbox
|
||||||
|
from app.pgp_utils import PGPException, load_public_key
|
||||||
|
from smtplib import SMTPRecipientsRefused
|
||||||
|
|
||||||
|
|
||||||
class ChangeEmailForm(FlaskForm):
|
class ChangeEmailForm(FlaskForm):
|
||||||
|
@ -37,7 +39,11 @@ def mailbox_detail_route(mailbox_id):
|
||||||
else:
|
else:
|
||||||
pending_email = None
|
pending_email = None
|
||||||
|
|
||||||
if change_email_form.validate_on_submit():
|
if request.method == "POST":
|
||||||
|
if (
|
||||||
|
request.form.get("form-name") == "update-email"
|
||||||
|
and change_email_form.validate_on_submit()
|
||||||
|
):
|
||||||
new_email = change_email_form.email.data
|
new_email = change_email_form.email.data
|
||||||
if new_email != mailbox.email and not pending_email:
|
if new_email != mailbox.email and not pending_email:
|
||||||
# check if this email is not already used
|
# check if this email is not already used
|
||||||
|
@ -48,7 +54,9 @@ def mailbox_detail_route(mailbox_id):
|
||||||
):
|
):
|
||||||
flash(f"Email {new_email} already used", "error")
|
flash(f"Email {new_email} already used", "error")
|
||||||
elif not can_be_used_as_personal_email(new_email):
|
elif not can_be_used_as_personal_email(new_email):
|
||||||
flash("You cannot use this email address as your mailbox", "error")
|
flash(
|
||||||
|
"You cannot use this email address as your mailbox", "error",
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
mailbox.new_email = new_email
|
mailbox.new_email = new_email
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
@ -61,6 +69,7 @@ def mailbox_detail_route(mailbox_id):
|
||||||
+ f"?mailbox_id={mailbox_id_signed}"
|
+ f"?mailbox_id={mailbox_id_signed}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
send_email(
|
send_email(
|
||||||
new_email,
|
new_email,
|
||||||
f"Confirm mailbox change on SimpleLogin",
|
f"Confirm mailbox change on SimpleLogin",
|
||||||
|
@ -79,7 +88,12 @@ def mailbox_detail_route(mailbox_id):
|
||||||
mailbox_new_email=new_email,
|
mailbox_new_email=new_email,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
except SMTPRecipientsRefused:
|
||||||
|
flash(
|
||||||
|
f"Incorrect mailbox, please recheck {mailbox.email}",
|
||||||
|
"error",
|
||||||
|
)
|
||||||
|
else:
|
||||||
flash(
|
flash(
|
||||||
f"You are going to receive an email to confirm {new_email}.",
|
f"You are going to receive an email to confirm {new_email}.",
|
||||||
"success",
|
"success",
|
||||||
|
@ -87,6 +101,33 @@ def mailbox_detail_route(mailbox_id):
|
||||||
return redirect(
|
return redirect(
|
||||||
url_for("dashboard.mailbox_detail_route", mailbox_id=mailbox_id)
|
url_for("dashboard.mailbox_detail_route", mailbox_id=mailbox_id)
|
||||||
)
|
)
|
||||||
|
elif request.form.get("form-name") == "pgp":
|
||||||
|
if not current_user.can_use_pgp:
|
||||||
|
flash("You cannot use PGP", "error")
|
||||||
|
return redirect(
|
||||||
|
url_for("dashboard.mailbox_detail_route", mailbox_id=mailbox_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
if request.form.get("action") == "save":
|
||||||
|
mailbox.pgp_public_key = request.form.get("pgp")
|
||||||
|
try:
|
||||||
|
mailbox.pgp_finger_print = load_public_key(mailbox.pgp_public_key)
|
||||||
|
except PGPException:
|
||||||
|
flash("Cannot add the public key, please verify it", "error")
|
||||||
|
else:
|
||||||
|
db.session.commit()
|
||||||
|
flash("Your PGP public key is saved successfully", "success")
|
||||||
|
return redirect(
|
||||||
|
url_for("dashboard.mailbox_detail_route", mailbox_id=mailbox_id)
|
||||||
|
)
|
||||||
|
elif request.form.get("action") == "remove":
|
||||||
|
mailbox.pgp_public_key = None
|
||||||
|
mailbox.pgp_finger_print = None
|
||||||
|
db.session.commit()
|
||||||
|
flash("Your PGP public key is removed successfully", "success")
|
||||||
|
return redirect(
|
||||||
|
url_for("dashboard.mailbox_detail_route", mailbox_id=mailbox_id)
|
||||||
|
)
|
||||||
|
|
||||||
return render_template("dashboard/mailbox_detail.html", **locals())
|
return render_template("dashboard/mailbox_detail.html", **locals())
|
||||||
|
|
||||||
|
@ -122,10 +163,14 @@ def mailbox_confirm_change_route():
|
||||||
|
|
||||||
try:
|
try:
|
||||||
r_id = int(s.unsign(mailbox_id))
|
r_id = int(s.unsign(mailbox_id))
|
||||||
except BadSignature:
|
except Exception:
|
||||||
flash("Invalid link", "error")
|
flash("Invalid link", "error")
|
||||||
|
return redirect(url_for("dashboard.index"))
|
||||||
else:
|
else:
|
||||||
mailbox = Mailbox.get(r_id)
|
mailbox = Mailbox.get(r_id)
|
||||||
|
|
||||||
|
# new_email can be None if user cancels change in the meantime
|
||||||
|
if mailbox and mailbox.new_email:
|
||||||
mailbox.email = mailbox.new_email
|
mailbox.email = mailbox.new_email
|
||||||
mailbox.new_email = None
|
mailbox.new_email = None
|
||||||
|
|
||||||
|
@ -138,3 +183,6 @@ def mailbox_confirm_change_route():
|
||||||
return redirect(
|
return redirect(
|
||||||
url_for("dashboard.mailbox_detail_route", mailbox_id=mailbox.id)
|
url_for("dashboard.mailbox_detail_route", mailbox_id=mailbox.id)
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
flash("Invalid link", "error")
|
||||||
|
return redirect(url_for("dashboard.index"))
|
||||||
|
|
|
@ -143,6 +143,11 @@ class User(db.Model, ModelMixin, UserMixin):
|
||||||
db.ForeignKey("mailbox.id"), nullable=True, default=None
|
db.ForeignKey("mailbox.id"), nullable=True, default=None
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# feature flag
|
||||||
|
can_use_pgp = db.Column(
|
||||||
|
db.Boolean, default=False, nullable=False, server_default="0"
|
||||||
|
)
|
||||||
|
|
||||||
profile_picture = db.relationship(File)
|
profile_picture = db.relationship(File)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -920,6 +925,9 @@ class Mailbox(db.Model, ModelMixin):
|
||||||
# used when user wants to update mailbox email
|
# used when user wants to update mailbox email
|
||||||
new_email = db.Column(db.String(256), unique=True)
|
new_email = db.Column(db.String(256), unique=True)
|
||||||
|
|
||||||
|
pgp_public_key = db.Column(db.Text, nullable=True)
|
||||||
|
pgp_finger_print = db.Column(db.String(512), nullable=True)
|
||||||
|
|
||||||
def nb_alias(self):
|
def nb_alias(self):
|
||||||
return GenEmail.filter_by(mailbox_id=self.id).count()
|
return GenEmail.filter_by(mailbox_id=self.id).count()
|
||||||
|
|
||||||
|
|
26
app/pgp_utils.py
Normal file
26
app/pgp_utils.py
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import gnupg
|
||||||
|
|
||||||
|
from app.config import GNUPGHOME
|
||||||
|
|
||||||
|
gpg = gnupg.GPG(gnupghome=GNUPGHOME)
|
||||||
|
|
||||||
|
|
||||||
|
class PGPException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def load_public_key(public_key: str) -> str:
|
||||||
|
"""Load a public key into keyring and return the fingerprint. If error, raise Exception"""
|
||||||
|
import_result = gpg.import_keys(public_key)
|
||||||
|
try:
|
||||||
|
return import_result.fingerprints[0]
|
||||||
|
except Exception as e:
|
||||||
|
raise PGPException("Cannot load key") from e
|
||||||
|
|
||||||
|
|
||||||
|
def encrypt(data: str, fingerprint: str) -> str:
|
||||||
|
r = gpg.encrypt(data, fingerprint, always_trust=True)
|
||||||
|
if not r.ok:
|
||||||
|
raise PGPException("Cannot encrypt")
|
||||||
|
|
||||||
|
return str(r)
|
|
@ -31,13 +31,17 @@ It should contain the following info:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
import time
|
import time
|
||||||
|
from email import encoders
|
||||||
from email.message import Message
|
from email.message import Message
|
||||||
|
from email.mime.application import MIMEApplication
|
||||||
|
from email.mime.multipart import MIMEMultipart
|
||||||
from email.parser import Parser
|
from email.parser import Parser
|
||||||
from email.policy import SMTPUTF8
|
from email.policy import SMTPUTF8
|
||||||
from smtplib import SMTP
|
from smtplib import SMTP
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from aiosmtpd.controller import Controller
|
from aiosmtpd.controller import Controller
|
||||||
|
import gnupg
|
||||||
|
|
||||||
from app.config import (
|
from app.config import (
|
||||||
EMAIL_DOMAIN,
|
EMAIL_DOMAIN,
|
||||||
|
@ -47,6 +51,7 @@ from app.config import (
|
||||||
ADMIN_EMAIL,
|
ADMIN_EMAIL,
|
||||||
SUPPORT_EMAIL,
|
SUPPORT_EMAIL,
|
||||||
POSTFIX_SUBMISSION_TLS,
|
POSTFIX_SUBMISSION_TLS,
|
||||||
|
GNUPGHOME,
|
||||||
)
|
)
|
||||||
from app.email_utils import (
|
from app.email_utils import (
|
||||||
get_email_name,
|
get_email_name,
|
||||||
|
@ -74,6 +79,7 @@ from app.models import (
|
||||||
)
|
)
|
||||||
from app.utils import random_string
|
from app.utils import random_string
|
||||||
from server import create_app
|
from server import create_app
|
||||||
|
from app import pgp_utils
|
||||||
|
|
||||||
|
|
||||||
# fix the database connection leak issue
|
# fix the database connection leak issue
|
||||||
|
@ -255,6 +261,31 @@ def should_append_alias(msg, alias):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def prepare_pgp_message(orig_msg: Message, pgp_fingerprint: str):
|
||||||
|
msg = MIMEMultipart("encrypted", protocol="application/pgp-encrypted")
|
||||||
|
|
||||||
|
# copy all headers from original message except the "Content-Type"
|
||||||
|
for i in reversed(range(len(orig_msg._headers))):
|
||||||
|
header_name = orig_msg._headers[i][0].lower()
|
||||||
|
if header_name != "Content-Type".lower():
|
||||||
|
msg[header_name] = orig_msg._headers[i][1]
|
||||||
|
|
||||||
|
first = MIMEApplication(
|
||||||
|
_subtype="pgp-encrypted", _encoder=encoders.encode_7or8bit, _data=""
|
||||||
|
)
|
||||||
|
first.set_payload("Version: 1")
|
||||||
|
msg.attach(first)
|
||||||
|
|
||||||
|
second = MIMEApplication("octet-stream", _encoder=encoders.encode_7or8bit)
|
||||||
|
second.add_header("Content-Disposition", "inline")
|
||||||
|
# encrypt original message
|
||||||
|
encrypted_data = pgp_utils.encrypt(orig_msg.as_string(), pgp_fingerprint)
|
||||||
|
second.set_payload(encrypted_data)
|
||||||
|
msg.attach(second)
|
||||||
|
|
||||||
|
return msg
|
||||||
|
|
||||||
|
|
||||||
def handle_forward(envelope, smtp: SMTP, msg: Message, rcpt_to: str) -> str:
|
def handle_forward(envelope, smtp: SMTP, msg: Message, rcpt_to: str) -> str:
|
||||||
"""return *status_code message*"""
|
"""return *status_code message*"""
|
||||||
alias = rcpt_to.lower() # alias@SL
|
alias = rcpt_to.lower() # alias@SL
|
||||||
|
@ -267,7 +298,14 @@ def handle_forward(envelope, smtp: SMTP, msg: Message, rcpt_to: str) -> str:
|
||||||
LOG.d("alias %s cannot be created on-the-fly, return 510", alias)
|
LOG.d("alias %s cannot be created on-the-fly, return 510", alias)
|
||||||
return "510 Email not exist"
|
return "510 Email not exist"
|
||||||
|
|
||||||
mailbox_email = gen_email.mailbox_email()
|
mailbox = gen_email.mailbox
|
||||||
|
mailbox_email = mailbox.email
|
||||||
|
|
||||||
|
# create PGP email if needed
|
||||||
|
if mailbox.pgp_finger_print:
|
||||||
|
LOG.d("Encrypt message using mailbox %s", mailbox)
|
||||||
|
msg = prepare_pgp_message(msg, mailbox.pgp_finger_print)
|
||||||
|
|
||||||
forward_email = get_or_create_forward_email(msg["From"], gen_email)
|
forward_email = get_or_create_forward_email(msg["From"], gen_email)
|
||||||
forward_log = ForwardEmailLog.create(forward_id=forward_email.id)
|
forward_log = ForwardEmailLog.create(forward_id=forward_email.id)
|
||||||
|
|
||||||
|
|
|
@ -110,3 +110,6 @@ FACEBOOK_CLIENT_SECRET=to_fill
|
||||||
# Flask profiler
|
# Flask profiler
|
||||||
# FLASK_PROFILER_PATH=/tmp/flask-profiler.sql
|
# FLASK_PROFILER_PATH=/tmp/flask-profiler.sql
|
||||||
# FLASK_PROFILER_PASSWORD=password
|
# FLASK_PROFILER_PASSWORD=password
|
||||||
|
|
||||||
|
# Where to store GPG Keyring
|
||||||
|
# GNUPGHOME=/tmp/gnupg
|
||||||
|
|
30
init_app.py
Normal file
30
init_app.py
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
"""Initial loading script"""
|
||||||
|
from app.models import Mailbox
|
||||||
|
from app.log import LOG
|
||||||
|
from app.extensions import db
|
||||||
|
from app.pgp_utils import load_public_key
|
||||||
|
from server import create_app
|
||||||
|
|
||||||
|
|
||||||
|
def load_pgp_public_keys(app):
|
||||||
|
"""Load PGP public key to keyring"""
|
||||||
|
with app.app_context():
|
||||||
|
for mailbox in Mailbox.query.filter(Mailbox.pgp_public_key != None).all():
|
||||||
|
LOG.d("Load PGP key for mailbox %s", mailbox)
|
||||||
|
fingerprint = load_public_key(mailbox.pgp_public_key)
|
||||||
|
|
||||||
|
# sanity check
|
||||||
|
if fingerprint != mailbox.pgp_finger_print:
|
||||||
|
LOG.error(
|
||||||
|
"fingerprint %s different for mailbox %s", fingerprint, mailbox
|
||||||
|
)
|
||||||
|
mailbox.pgp_finger_print = fingerprint
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app = create_app()
|
||||||
|
|
||||||
|
with app.app_context():
|
||||||
|
load_pgp_public_keys(app)
|
33
migrations/versions/2020_030813_628a5438295c_.py
Normal file
33
migrations/versions/2020_030813_628a5438295c_.py
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
"""empty message
|
||||||
|
|
||||||
|
Revision ID: 628a5438295c
|
||||||
|
Revises: 235355381f53
|
||||||
|
Create Date: 2020-03-08 13:07:13.312858
|
||||||
|
|
||||||
|
"""
|
||||||
|
import sqlalchemy_utils
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '628a5438295c'
|
||||||
|
down_revision = '235355381f53'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('mailbox', sa.Column('pgp_finger_print', sa.String(length=512), nullable=True))
|
||||||
|
op.add_column('mailbox', sa.Column('pgp_public_key', sa.Text(), nullable=True))
|
||||||
|
op.add_column('users', sa.Column('can_use_pgp', sa.Boolean(), server_default='0', nullable=False))
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_column('users', 'can_use_pgp')
|
||||||
|
op.drop_column('mailbox', 'pgp_public_key')
|
||||||
|
op.drop_column('mailbox', 'pgp_finger_print')
|
||||||
|
# ### end Alembic commands ###
|
|
@ -37,3 +37,4 @@ flask_profiler
|
||||||
facebook-sdk
|
facebook-sdk
|
||||||
google-api-python-client
|
google-api-python-client
|
||||||
google-auth-httplib2
|
google-auth-httplib2
|
||||||
|
python-gnupg
|
|
@ -49,8 +49,7 @@ google-auth==1.11.2 # via google-api-python-client, google-auth-httplib2
|
||||||
gunicorn==19.9.0 # via -r requirements.in
|
gunicorn==19.9.0 # via -r requirements.in
|
||||||
httplib2==0.17.0 # via google-api-python-client, google-auth-httplib2
|
httplib2==0.17.0 # via google-api-python-client, google-auth-httplib2
|
||||||
humanfriendly==4.18 # via coloredlogs
|
humanfriendly==4.18 # via coloredlogs
|
||||||
idna-ssl==1.1.0 # via aiohttp
|
idna==2.8 # via requests, yarl
|
||||||
idna==2.8 # via idna-ssl, requests, yarl
|
|
||||||
importlib-metadata==0.18 # via pluggy, pytest
|
importlib-metadata==0.18 # via pluggy, pytest
|
||||||
ipython-genutils==0.2.0 # via traitlets
|
ipython-genutils==0.2.0 # via traitlets
|
||||||
ipython==7.5.0 # via -r requirements.in
|
ipython==7.5.0 # via -r requirements.in
|
||||||
|
@ -87,6 +86,7 @@ pytest==4.6.3 # via -r requirements.in
|
||||||
python-dateutil==2.8.0 # via alembic, arrow, botocore, strictyaml
|
python-dateutil==2.8.0 # via alembic, arrow, botocore, strictyaml
|
||||||
python-dotenv==0.10.3 # via -r requirements.in
|
python-dotenv==0.10.3 # via -r requirements.in
|
||||||
python-editor==1.0.4 # via alembic
|
python-editor==1.0.4 # via alembic
|
||||||
|
python-gnupg==0.4.5 # via -r requirements.in
|
||||||
raven-aiohttp==0.7.0 # via yacron
|
raven-aiohttp==0.7.0 # via yacron
|
||||||
raven==6.10.0 # via raven-aiohttp, yacron
|
raven==6.10.0 # via raven-aiohttp, yacron
|
||||||
requests-oauthlib==1.2.0 # via -r requirements.in
|
requests-oauthlib==1.2.0 # via -r requirements.in
|
||||||
|
@ -101,7 +101,6 @@ sqlalchemy-utils==0.36.1 # via -r requirements.in
|
||||||
sqlalchemy==1.3.12 # via alembic, flask-sqlalchemy, sqlalchemy-utils
|
sqlalchemy==1.3.12 # via alembic, flask-sqlalchemy, sqlalchemy-utils
|
||||||
strictyaml==1.0.2 # via yacron
|
strictyaml==1.0.2 # via yacron
|
||||||
traitlets==4.3.2 # via ipython
|
traitlets==4.3.2 # via ipython
|
||||||
typing-extensions==3.7.4.1 # via aiohttp
|
|
||||||
unidecode==1.0.23 # via -r requirements.in
|
unidecode==1.0.23 # via -r requirements.in
|
||||||
uritemplate==3.0.1 # via google-api-python-client
|
uritemplate==3.0.1 # via google-api-python-client
|
||||||
urllib3==1.25.3 # via botocore, requests, sentry-sdk
|
urllib3==1.25.3 # via botocore, requests, sentry-sdk
|
||||||
|
|
|
@ -131,6 +131,7 @@ def fake_data():
|
||||||
activated=True,
|
activated=True,
|
||||||
is_admin=True,
|
is_admin=True,
|
||||||
otp_secret="base32secret3232",
|
otp_secret="base32secret3232",
|
||||||
|
can_use_pgp=True,
|
||||||
)
|
)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
|
|
105
tests/test_pgp_utils.py
Normal file
105
tests/test_pgp_utils.py
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
from app.pgp_utils import load_public_key, gpg, encrypt
|
||||||
|
|
||||||
|
pubkey = """-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||||
|
Version: Keybase OpenPGP v1.0.0
|
||||||
|
Comment: https://keybase.io/crypto
|
||||||
|
|
||||||
|
xo0EXlqP9wEEALoJsLHZA5W4yGQf+TIlIuYjj72SGEXbZyvMJxDk89YE8SWHAP+L
|
||||||
|
+GkyNBfiPidJ1putLBOTDuxjDroDa6zMmjxCORUYdtq35RIDo/raamAaYg32X/TI
|
||||||
|
3WyL3lgVf7K+VhXntG2V3OfM1r5nt3C1sy8Rsvzbih3p+eHpE3xCImg7ABEBAAHN
|
||||||
|
FFRlc3QgPHRlc3RAc2wubG9jYWw+wq0EEwEKABcFAl5aj/cCGy8DCwkHAxUKCAIe
|
||||||
|
AQIXgAAKCRCtrxG3FC3nSGhDA/wMT4PM8pWCsbsGA32SMN0j0MRsmc6KT4BGX8qd
|
||||||
|
CwTv7s5DvZlkFL9uJQxcKFe+yYpjnrPvW0p81ispj7pVJqUTyx4brZHiWFi/vODz
|
||||||
|
YyzTXNJvWJOp27G4YzWPeEeSKuGjF1CQScmZJA5luay7mkI5gttw4q3iqJlcDDFq
|
||||||
|
1sz2486NBF5aj/cBBADA7KbOa8klxOC8Oact0zzc30SCGxtCLuFQCBI/dIrnv2KC
|
||||||
|
lIbUd+CDlmD+cKCIu7MlrYPhCLF24MYnUXVFDbT3fP8YVy2HZTfk4Q64tj0S17ve
|
||||||
|
E9H1G1W6FqdDUhMCU1EmJgd8sKOrNOFtz4+b3IHJhtJIoUILDkiMjfUCHmQaqQAR
|
||||||
|
AQABwsCDBBgBCgAPBQJeWo/3BQkPCZwAAhsuAKgJEK2vEbcULedInSAEGQEKAAYF
|
||||||
|
Al5aj/cACgkQDjygQt7BuGFtPQQAmzUJqXB4UWo9HPZfutqSU6GElSMwZq1Dlf8S
|
||||||
|
Stjq7cYK+HSfcyw4wSBMRxMtG2zmbyhWlYTqx3fAAjgE32dBI/Rq8ku60u6SGEiE
|
||||||
|
egKCcm0lyR1TVUTYEsfjiYD5AmGWng8tTavz1ANdEoE66wGApkETfmTM7hOuQrKm
|
||||||
|
BjXpmembUQP5AXln8rWuDkeXVXhBa5RR3NgoD/fos2QJ5NxkZdfPmM57EwQkEXKv
|
||||||
|
S3c5rlvvhIupElSyJkxOzfykNlJewVrLxCicj+JPSt7ly6YlkMQglyevntI46y1l
|
||||||
|
2Msf0oeQZ3uedURGQiGQalC7nzPFnOARbNffFEJI3cJhcLkr2UFdL0rOjQReWo/3
|
||||||
|
AQQA+MJeovqVVVrE1Vsc3M/BuG5ao7xyP1y7YhgmJg3gi8HR7b4/ySJtKnCYAmLg
|
||||||
|
wwjfCUWed/GZ+3bGw48x8Fmn+6QTPG04j8RUOMUgVt9jc+TxC8VWSvqH1Taho3MK
|
||||||
|
6ZQpCwXPO0FmWc5ybp0AJzqy2YS4eZwue1WH3zFzjXrOBd0AEQEAAcLAgwQYAQoA
|
||||||
|
DwUCXlqP9wUJDwmcAAIbLgCoCRCtrxG3FC3nSJ0gBBkBCgAGBQJeWo/3AAoJEFJq
|
||||||
|
ki+hZCNMgH4EAPiamTuezRtMIEWpjEjYGjpRF+2uj5VmU6N2E6+5Nh73HUKNCVRj
|
||||||
|
AWeRarplye/CqZyhzPgotDNzAPzE4smo0N0vvc4zi6toqMiO4ODjR313d0y0v4iP
|
||||||
|
+n576QwpfGw/ddlTEL7Iv28dzdKJArjNc2/jRxefHrAYSzjEunl/GUq+ToQD/izQ
|
||||||
|
mPo6SWhlODsIy4eR/u3NpKtQQcs40XWLVci6M66ntyl5XmBGgFFu0WHIYeDOnTRc
|
||||||
|
qL1W5yEYaaJhaEbmNGk3tf26Ns9cTl91S2eylO9nWGOnqFg58jP63TZVR7q3jIq1
|
||||||
|
e5DKgszG2Vvye+bbK6qMKmaIXEMhnjw9eZuW6MGf
|
||||||
|
=yDVI
|
||||||
|
-----END PGP PUBLIC KEY BLOCK-----
|
||||||
|
"""
|
||||||
|
|
||||||
|
private_key = """-----BEGIN PGP PRIVATE KEY BLOCK-----
|
||||||
|
Version: Keybase OpenPGP v1.0.0
|
||||||
|
Comment: https://keybase.io/crypto
|
||||||
|
|
||||||
|
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
|
||||||
|
QyGePD15m5bowZ8=
|
||||||
|
=4OSo
|
||||||
|
-----END PGP PRIVATE KEY BLOCK-----"""
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_public_key():
|
||||||
|
load_public_key(pubkey)
|
||||||
|
assert len(gpg.list_keys()) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_encrypt():
|
||||||
|
fingerprint = load_public_key(pubkey)
|
||||||
|
secret = encrypt("abcd", fingerprint)
|
||||||
|
assert secret != ""
|
Loading…
Reference in a new issue