Merge pull request #103 from simple-login/staging

Add support for PGP
This commit is contained in:
Son Nguyen Kim 2020-03-09 14:00:15 +01:00 committed by GitHub
commit 3266a5982a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 427 additions and 68 deletions

View File

@ -38,7 +38,7 @@ def auth_mfa():
s = Signer(FLASK_SECRET)
try:
user_id = int(s.unsign(mfa_key))
except BadSignature:
except Exception:
return jsonify(error="Invalid mfa_key"), 400
user = User.get(user_id)

View File

@ -1,5 +1,8 @@
import os
import random
import string
import subprocess
import tempfile
from uuid import uuid4
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")
)
# 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_CLIENT_ID = os.environ.get("GITHUB_CLIENT_ID")

View File

@ -41,6 +41,10 @@
🚫
</span>
{% 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 %}
<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

View File

@ -17,8 +17,19 @@
🚫
</span>
{% endif %}
{% if mailbox.pgp_finger_print %}
<span class="cursor" data-toggle="tooltip" data-original-title="PGP Enabled">🗝</span>
{% endif %}
</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 -->
<div class="card">
<form method="post" enctype="multipart/form-data">
@ -52,6 +63,37 @@
</div>
<!-- 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>
{% endblock %}

View File

@ -20,7 +20,7 @@ def billing():
if request.method == "POST":
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)
if success:

View File

@ -120,7 +120,11 @@ def mailbox_route():
"success",
)
return redirect(url_for("dashboard.mailbox_route"))
return redirect(
url_for(
"dashboard.mailbox_detail_route", mailbox_id=new_mailbox.id
)
)
return render_template(
"dashboard/mailbox.html",
@ -138,8 +142,9 @@ def mailbox_verify():
try:
r_id = int(s.unsign(mailbox_id))
except BadSignature:
except Exception:
flash("Invalid link. Please delete and re-add your mailbox", "error")
return redirect(url_for("dashboard.mailbox_route"))
else:
mailbox = Mailbox.get(r_id)
mailbox.verified = True
@ -150,4 +155,6 @@ def mailbox_verify():
f"The {mailbox.email} is now verified, you can start creating alias with it",
"success",
)
return redirect(url_for("dashboard.mailbox_route"))
return redirect(
url_for("dashboard.mailbox_detail_route", mailbox_id=mailbox.id)
)

View File

@ -14,6 +14,8 @@ from app.extensions import db
from app.log import LOG
from app.models import GenEmail, DeletedAlias
from app.models import Mailbox
from app.pgp_utils import PGPException, load_public_key
from smtplib import SMTPRecipientsRefused
class ChangeEmailForm(FlaskForm):
@ -37,53 +39,92 @@ def mailbox_detail_route(mailbox_id):
else:
pending_email = None
if change_email_form.validate_on_submit():
new_email = change_email_form.email.data
if new_email != mailbox.email and not pending_email:
# check if this email is not already used
if (
email_already_used(new_email)
or GenEmail.get_by(email=new_email)
or DeletedAlias.get_by(email=new_email)
):
flash(f"Email {new_email} already used", "error")
elif not can_be_used_as_personal_email(new_email):
flash("You cannot use this email address as your mailbox", "error")
else:
mailbox.new_email = new_email
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
if new_email != mailbox.email and not pending_email:
# check if this email is not already used
if (
email_already_used(new_email)
or GenEmail.get_by(email=new_email)
or DeletedAlias.get_by(email=new_email)
):
flash(f"Email {new_email} already used", "error")
elif not can_be_used_as_personal_email(new_email):
flash(
"You cannot use this email address as your mailbox", "error",
)
else:
mailbox.new_email = new_email
db.session.commit()
s = Signer(MAILBOX_SECRET)
mailbox_id_signed = s.sign(str(mailbox.id)).decode()
verification_url = (
URL
+ "/dashboard/mailbox/confirm_change"
+ f"?mailbox_id={mailbox_id_signed}"
)
try:
send_email(
new_email,
f"Confirm mailbox change on SimpleLogin",
render(
"transactional/verify-mailbox-change.txt",
user=current_user,
link=verification_url,
mailbox_email=mailbox.email,
mailbox_new_email=new_email,
),
render(
"transactional/verify-mailbox-change.html",
user=current_user,
link=verification_url,
mailbox_email=mailbox.email,
mailbox_new_email=new_email,
),
)
except SMTPRecipientsRefused:
flash(
f"Incorrect mailbox, please recheck {mailbox.email}",
"error",
)
else:
flash(
f"You are going to receive an email to confirm {new_email}.",
"success",
)
return redirect(
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()
s = Signer(MAILBOX_SECRET)
mailbox_id_signed = s.sign(str(mailbox.id)).decode()
verification_url = (
URL
+ "/dashboard/mailbox/confirm_change"
+ f"?mailbox_id={mailbox_id_signed}"
)
send_email(
new_email,
f"Confirm mailbox change on SimpleLogin",
render(
"transactional/verify-mailbox-change.txt",
user=current_user,
link=verification_url,
mailbox_email=mailbox.email,
mailbox_new_email=new_email,
),
render(
"transactional/verify-mailbox-change.html",
user=current_user,
link=verification_url,
mailbox_email=mailbox.email,
mailbox_new_email=new_email,
),
)
flash(
f"You are going to receive an email to confirm {new_email}.",
"success",
)
flash("Your PGP public key is removed successfully", "success")
return redirect(
url_for("dashboard.mailbox_detail_route", mailbox_id=mailbox_id)
)
@ -122,19 +163,26 @@ def mailbox_confirm_change_route():
try:
r_id = int(s.unsign(mailbox_id))
except BadSignature:
except Exception:
flash("Invalid link", "error")
return redirect(url_for("dashboard.index"))
else:
mailbox = Mailbox.get(r_id)
mailbox.email = mailbox.new_email
mailbox.new_email = None
# mark mailbox as verified if the change request is sent from an unverified mailbox
mailbox.verified = True
db.session.commit()
# new_email can be None if user cancels change in the meantime
if mailbox and mailbox.new_email:
mailbox.email = mailbox.new_email
mailbox.new_email = None
LOG.d("Mailbox change %s is verified", mailbox)
flash(f"The {mailbox.email} is updated", "success")
return redirect(
url_for("dashboard.mailbox_detail_route", mailbox_id=mailbox.id)
)
# mark mailbox as verified if the change request is sent from an unverified mailbox
mailbox.verified = True
db.session.commit()
LOG.d("Mailbox change %s is verified", mailbox)
flash(f"The {mailbox.email} is updated", "success")
return redirect(
url_for("dashboard.mailbox_detail_route", mailbox_id=mailbox.id)
)
else:
flash("Invalid link", "error")
return redirect(url_for("dashboard.index"))

View File

@ -143,6 +143,11 @@ class User(db.Model, ModelMixin, UserMixin):
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)
@classmethod
@ -920,6 +925,9 @@ class Mailbox(db.Model, ModelMixin):
# used when user wants to update mailbox email
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):
return GenEmail.filter_by(mailbox_id=self.id).count()

26
app/pgp_utils.py Normal file
View 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)

View File

@ -31,13 +31,17 @@ It should contain the following info:
"""
import time
from email import encoders
from email.message import Message
from email.mime.application import MIMEApplication
from email.mime.multipart import MIMEMultipart
from email.parser import Parser
from email.policy import SMTPUTF8
from smtplib import SMTP
from typing import Optional
from aiosmtpd.controller import Controller
import gnupg
from app.config import (
EMAIL_DOMAIN,
@ -47,6 +51,7 @@ from app.config import (
ADMIN_EMAIL,
SUPPORT_EMAIL,
POSTFIX_SUBMISSION_TLS,
GNUPGHOME,
)
from app.email_utils import (
get_email_name,
@ -74,6 +79,7 @@ from app.models import (
)
from app.utils import random_string
from server import create_app
from app import pgp_utils
# fix the database connection leak issue
@ -255,6 +261,31 @@ def should_append_alias(msg, alias):
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:
"""return *status_code message*"""
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)
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_log = ForwardEmailLog.create(forward_id=forward_email.id)

View File

@ -109,4 +109,7 @@ FACEBOOK_CLIENT_SECRET=to_fill
# Flask profiler
# 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
View 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)

View 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 ###

View File

@ -36,4 +36,5 @@ pyotp
flask_profiler
facebook-sdk
google-api-python-client
google-auth-httplib2
google-auth-httplib2
python-gnupg

View File

@ -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
httplib2==0.17.0 # via google-api-python-client, google-auth-httplib2
humanfriendly==4.18 # via coloredlogs
idna-ssl==1.1.0 # via aiohttp
idna==2.8 # via idna-ssl, requests, yarl
idna==2.8 # via requests, yarl
importlib-metadata==0.18 # via pluggy, pytest
ipython-genutils==0.2.0 # via traitlets
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-dotenv==0.10.3 # via -r requirements.in
python-editor==1.0.4 # via alembic
python-gnupg==0.4.5 # via -r requirements.in
raven-aiohttp==0.7.0 # via yacron
raven==6.10.0 # via raven-aiohttp, yacron
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
strictyaml==1.0.2 # via yacron
traitlets==4.3.2 # via ipython
typing-extensions==3.7.4.1 # via aiohttp
unidecode==1.0.23 # via -r requirements.in
uritemplate==3.0.1 # via google-api-python-client
urllib3==1.25.3 # via botocore, requests, sentry-sdk

View File

@ -131,6 +131,7 @@ def fake_data():
activated=True,
is_admin=True,
otp_secret="base32secret3232",
can_use_pgp=True,
)
db.session.commit()

105
tests/test_pgp_utils.py Normal file
View 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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
U9jtqNTsHrr0XFzA2zno+zcopiZZOoPXcwxLf+pCetjN5EOkpMgqZTtV2nCppQRB
d3ZpsguOO4OVexEW6gWGOuas5+/qa846it9VMo+nlqtLyIAFbj2P02Zk/QrUnPF3
PEjKDJssrOEnZWlpAdEDfFhC1OrBVlG0lkD1qHDCNO9MTeT2dRMghbFGxlno9z2K
wnnB+Ep4UULuvbh08GsVflQPaA0a59IFDbOzYc7puS5kpJ5fQWwdXZvjNc/jOeQX
BHaLfQKmWYW3pCxs0BqKRhAnZ9E+kkIL6xU6MlJPs/NGO7aAykrFv8BdmBJQ8s00
9LGlgSUhdEdIsn5h3Kdn0f/7FXXWwsCDBBgBCgAPBQJeWo/3BQkPCZwAAhsuAKgJ
EK2vEbcULedInSAEGQEKAAYFAl5aj/cACgkQUmqSL6FkI0yAfgQA+JqZO57NG0wg
RamMSNgaOlEX7a6PlWZTo3YTr7k2HvcdQo0JVGMBZ5FqumXJ78KpnKHM+Ci0M3MA
/MTiyajQ3S+9zjOLq2ioyI7g4ONHfXd3TLS/iI/6fnvpDCl8bD912VMQvsi/bx3N
0okCuM1zb+NHF58esBhLOMS6eX8ZSr5OhAP+LNCY+jpJaGU4OwjLh5H+7c2kq1BB
yzjRdYtVyLozrqe3KXleYEaAUW7RYchh4M6dNFyovVbnIRhpomFoRuY0aTe1/bo2
z1xOX3VLZ7KU72dYY6eoWDnyM/rdNlVHureMirV7kMqCzMbZW/J75tsrqowqZohc
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 != ""