Compare commits

...
Sign in to create a new pull request.

39 commits

Author SHA1 Message Date
David Duque
9d180f592e
Use the core enigma instead 2020-10-04 16:32:58 +01:00
David Duque
f746ef64dc
Roundcube: Enable enigma plugin 2020-09-20 20:19:04 +01:00
David Duque
ca807c65a7
Syntax warning correction 2020-09-11 22:23:20 +01:00
David Duque
0e2fd86568
Daemon now sends signed messages as an attachment 2020-09-11 19:51:14 +01:00
David Duque
be26603525
Key auto-renewal 2020-09-11 16:23:37 +01:00
David Duque
80e532f31e
PGP Keyring status checks 2020-09-09 18:39:28 +01:00
David Duque
934397fc35
Fix setup issue when we tried to symlink something that is already symlinked 2020-09-09 15:13:23 +01:00
David Duque
e1ff419d83
Pubkey algos - index via the relevant constants 2020-09-09 15:12:09 +01:00
David Duque
1d6e902935
Remove trust owner - not relevant for our use cases 2020-09-09 14:48:28 +01:00
David Duque
01ef02841e
Key deletion 2020-09-07 21:30:33 +01:00
David Duque
ed2b192f9c
Key removal front-end 2020-09-07 18:11:13 +01:00
David Duque
549e11a3ad
Add single-key querying, key deletion stub 2020-09-07 18:03:36 +01:00
David Duque
cbbcbc12c8
Code refactoring, return results as raw data 2020-09-07 18:02:35 +01:00
David Duque
01954748ca
Key removal confirmation modal 2020-09-07 17:35:53 +01:00
David Duque
9638f84573
Enable key importing via admin panel
Key highlights:
* Supports ascii armored imports
* Refuses to import secret keys
* Can import entire keyboxes (imports with more than one key, etc.)
* Can import revocations, etc.
2020-09-07 00:11:45 +01:00
David Duque
ccab40cbc4
Display revoked keys as such 2020-09-05 12:42:14 +01:00
David Duque
d134da66e7
Key export: Handle error cases 2020-09-03 23:15:49 +01:00
David Duque
8a5a5aa92a
Import key front-end 2020-09-03 23:09:55 +01:00
David Duque
e6c0af621b
Allow keys to be exported 2020-09-03 22:40:12 +01:00
David Duque
888b3794fc
Increase modal screen width 2020-09-03 22:39:54 +01:00
David Duque
c3db6e4749
Make venv use the distribution's gpgme bindings
The bindings provided via pip both require a compilation step (which we
do not want), and they're actually severely out-of-date (aka, missing
features)

The only way to make venv use these bindings (at least from my point of
view, at the moment) is to symlink the gpg package inside the venv lib
directory.
2020-09-03 22:29:43 +01:00
David Duque
dd54fc1b51
Key exports 2020-09-02 20:21:07 +01:00
David Duque
f1a9a9fe7f
Properly add buttons to export and delete. Functionality still pending. 2020-09-02 16:37:01 +01:00
David Duque
4af61247b8
Fix algorithm display width to 120pt 2020-09-02 16:21:53 +01:00
David Duque
9384064b9c
Representation of keys in webpage 2020-08-25 00:04:31 +01:00
David Duque
a9b2160b2b
Pass in friendly versions of expiration timestamps 2020-08-24 23:55:37 +01:00
David Duque
145ba32b5e
Begin templating work 2020-08-24 11:24:05 +01:00
David Duque
73f4647e65
Placeholder for showing keys 2020-08-24 00:58:14 +01:00
David Duque
1771d76be4
Properly recall pgp key from /etc/mailinabox.conf 2020-08-24 00:25:48 +01:00
David Duque
dea8cb1356
Use first private key when writing mailinabox.conf
In a scenario where somehow there are multiple private keys in the ring
(development purposes), gpg will output all their fingerprints, and that
will be written to /etc/mailinabox.conf, like this:

PGPKEY=FPR1
FPR2
FPR3
FPRn

When imported by other shell scripts this will cause errors (causing
issues in the setup)
2020-08-23 23:35:48 +01:00
David Duque
2b95ecd5eb
Add route /admin/system/pgp/
Returns all keys in the keyring (daemon's and private)
2020-08-23 23:34:34 +01:00
David Duque
02eeb0bd41
Get keys in keyring 2020-08-23 02:08:12 +01:00
David Duque
f9c6c76b8d
Key representation function 2020-08-23 01:59:15 +01:00
David Duque
1b232f14bc
Keep daemon's key fingerprint in configuration; Replace if needed 2020-08-17 00:58:28 +01:00
David Duque
a8abae7703
Begin pgp.py tools 2020-08-16 01:48:36 +01:00
David Duque
d03b5ad595
Generate initial private key if one doesn't already exist 2020-08-15 02:34:21 +01:00
David Duque
12f35acb09
Front-end mockup 2020-08-12 19:06:11 +01:00
David Duque
1414a9b6c3
Use bootstrap-provided features (instead of messing up with style tags) 2020-08-10 03:13:41 +01:00
David Duque
2e18b9280e
Add PGP Keyring Management page 2020-08-06 19:18:05 +01:00
13 changed files with 634 additions and 4 deletions

View file

@ -580,6 +580,63 @@ def smtp_relay_set():
except Exception as e:
return (str(e), 500)
# PGP
@app.route('/system/pgp/', methods=["GET"])
@authorized_personnel_only
def get_keys():
from pgp import get_daemon_key, get_imported_keys, key_representation
return {
"daemon": key_representation(get_daemon_key()),
"imported": list(map(key_representation, get_imported_keys()))
}
@app.route('/system/pgp/<fpr>', methods=["GET"])
@authorized_personnel_only
def get_key(fpr):
from pgp import get_key, key_representation
k = get_key(fpr)
if k is None:
abort(404)
return key_representation(k)
@app.route('/system/pgp/<fpr>', methods=["DELETE"])
@authorized_personnel_only
def delete_key(fpr):
from pgp import delete_key
try:
if delete_key(fpr) is None:
abort(404)
return "OK"
except ValueError as e:
return (str(e), 400)
@app.route('/system/pgp/<fpr>/export', methods=["GET"])
@authorized_personnel_only
def export_key(fpr):
from pgp import export_key
exp = export_key(fpr)
if exp is None:
abort(404)
return exp
@app.route('/system/pgp/import', methods=["POST"])
@authorized_personnel_only
def import_key():
from pgp import import_key
k = request.form.get('key')
try:
result = import_key(k)
return {
"keys_read": result.considered,
"keys_added": result.imported,
"keys_unchanged": result.unchanged,
"uids_added": result.new_user_ids,
"sigs_added": result.new_signatures,
"revs_added": result.new_revocations
}
except ValueError as e:
return (str(e), 400)
# MUNIN

View file

@ -21,5 +21,8 @@ management/backup.py 2>&1 | management/email_administrator.py "Backup Status"
# Provision any new certificates for new domains or domains with expiring certificates.
management/ssl_certificates.py -q 2>&1 | management/email_administrator.py "TLS Certificate Provisioning Result"
# Renew the daemon's PGP key if about to expire
management/pgp.py 2>&1 | management/email_administrator.py "PGP Key Renewal Result"
# Run status checks and email the administrator if anything changed.
management/status_checks.py --show-changes 2>&1 | management/email_administrator.py "Status Checks Change Notice"

View file

@ -7,9 +7,12 @@ import sys
import html
import smtplib
from email.mime.application import MIMEApplication
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from pgp import create_signature
# In Python 3.6:
#from email.message import Message
@ -45,6 +48,7 @@ content_html = "<html><body><pre>{}</pre></body></html>".format(html.escape(cont
msg.attach(MIMEText(content, 'plain'))
msg.attach(MIMEText(content_html, 'html'))
msg.attach(MIMEApplication(create_signature(content.encode()), Name="signed.asc"))
# In Python 3.6:
#msg.set_content(content)

125
management/pgp.py Executable file
View file

@ -0,0 +1,125 @@
#!/usr/local/lib/mailinabox/env/bin/python
# Tools to manipulate PGP keys
import gpg, utils, datetime
env = utils.load_environment()
# Import daemon's keyring - usually in /home/user-data/.gnupg/
gpghome = env['GNUPGHOME']
daemon_key_fpr = env['PGPKEY']
context = gpg.Context(armor=True, home_dir=gpghome)
# Global auxiliary lookup tables
crpyt_algos = {
0: "Unknown",
gpg.constants.PK_RSA: "RSA",
gpg.constants.PK_RSA_E: "RSA-E",
gpg.constants.PK_RSA_S: "RSA-S",
gpg.constants.PK_ELG_E: "ELG-E",
gpg.constants.PK_DSA: "DSA",
gpg.constants.PK_ECC: "ECC",
gpg.constants.PK_ELG: "ELG",
gpg.constants.PK_ECDSA: "ECDSA",
gpg.constants.PK_ECDH: "ECDH",
gpg.constants.PK_EDDSA: "EDDSA"
}
# Auxiliary function to process the key in order to be read more conveniently
def key_representation(key):
if key is None:
return None
key_rep = {
"master_fpr": key.fpr,
"revoked": key.revoked != 0,
"ids": [],
"subkeys": []
}
now = datetime.datetime.utcnow()
key_rep["ids"] = [ id.uid for id in key.uids ]
key_rep["subkeys"] = [{
"master": skey.fpr == key.fpr,
"sign": skey.can_sign == 1,
"cert": skey.can_certify == 1,
"encr": skey.can_encrypt == 1,
"auth": skey.can_authenticate == 1,
"fpr": skey.fpr,
"expires": skey.expires if skey.expires != 0 else None,
"expires_date": datetime.datetime.utcfromtimestamp(skey.expires).strftime("%x") if skey.expires != 0 else None,
"expires_days": (datetime.datetime.utcfromtimestamp(skey.expires) - now).days if skey.expires != 0 else None,
"expired": skey.expired == 1,
"algorithm": crpyt_algos[skey.pubkey_algo] if skey.pubkey_algo in crpyt_algos.keys() else crpyt_algos[0],
"bits": skey.length
} for skey in key.subkeys ]
return key_rep
# Tests an import as for whether we have any sort of private key material in our import
def contains_private_keys(imports):
import tempfile
with tempfile.TemporaryDirectory() as tmpdir:
with gpg.Context(home_dir=tmpdir, armor=True) as tmp:
result = tmp.key_import(imports)
return result.secret_read != 0
def get_key(fingerprint):
try:
return context.get_key(fingerprint, secret=False)
except KeyError:
return None
def get_daemon_key():
if daemon_key_fpr is None or daemon_key_fpr == "":
return None
return context.get_key(daemon_key_fpr, secret=True)
def get_imported_keys():
# All the keys in the keyring, except for the daemon's key
return list(
filter(
lambda k: k.fpr != daemon_key_fpr,
context.keylist(secret=False)
)
)
def import_key(key):
data = str.encode(key)
if contains_private_keys(data):
raise ValueError("Import cannot contain private keys!")
return context.key_import(data)
def export_key(fingerprint):
if get_key(fingerprint) is None:
return None
return context.key_export(pattern=fingerprint) # Key does exist, export it!
def delete_key(fingerprint):
key = get_key(fingerprint)
if fingerprint == daemon_key_fpr:
raise ValueError("You cannot delete the daemon's key!")
elif key is None:
return None
context.op_delete_ext(key, gpg.constants.DELETE_ALLOW_SECRET | gpg.constants.DELETE_FORCE)
return True
# Key usage
# Uses the daemon key to sign the provided message. If 'detached' is True, only the signature will be returned
def create_signature(data, detached=False):
signed_data, _ = context.sign(data, mode=gpg.constants.SIG_MODE_DETACH if detached else gpg.constants.SIG_MODE_CLEAR)
return signed_data
if __name__ == "__main__":
import sys, utils
# Check if we should renew the key
daemon_key = get_daemon_key()
exp = daemon_key.subkeys[0].expires
now = datetime.datetime.utcnow()
days_left = (datetime.datetime.utcfromtimestamp(exp) - now).days
if days_left > 14:
sys.exit(0)
else:
utils.shell("check_output", ["management/pgp_renew.sh"])

12
management/pgp_renew.sh Executable file
View file

@ -0,0 +1,12 @@
#!/bin/bash
# Renews the daemon's PGP key, if needed.
source /etc/mailinabox.conf # load global vars
export GNUPGHOME # Dump into the environment so that gpg uses it as homedir
gpg --batch --command-fd=0 --edit-key "${PGPKEY-}" << EOF;
key 0
expire
180d
save
EOF

View file

@ -17,6 +17,7 @@ from dns_update import get_dns_zones, build_tlsa_record, get_custom_dns_config,
from web_update import get_web_domains, get_domains_with_a_records
from ssl_certificates import get_ssl_certificates, get_domain_ssl_files, check_certificate
from mailconfig import get_mail_domains, get_mail_aliases
from pgp import get_daemon_key, get_imported_keys
from utils import shell, sort_domains, load_env_vars_from_file, load_settings
@ -59,6 +60,7 @@ def run_checks(rounded_values, env, output, pool):
shell('check_call', ["/usr/sbin/rndc", "flush"], trap=True)
run_system_checks(rounded_values, env, output)
run_pgp_checks(env, output)
# perform other checks asynchronously
@ -266,6 +268,75 @@ def check_free_memory(rounded_values, env, output):
if rounded_values: memory_msg = "System free memory is below 10%."
output.print_error(memory_msg)
def run_pgp_checks(env, output):
now = datetime.datetime.utcnow()
output.add_heading("PGP Keyring")
# Check daemon key
k = None
sk = None
try:
k = get_daemon_key()
sk = k.subkeys[0]
except KeyError:
pass
if k is None:
output.print_error("The daemon's key does not exist!")
elif sk.expired == 1:
output.print_error(f"The daemon's key ({k.fpr}) expired.")
elif k.revoked == 1:
output.print_error(f"The daemon's key ({k.fpr}) has been revoked.")
else:
exp = datetime.datetime.utcfromtimestamp(sk.expires) # Our daemon key only has one subkey
if (exp - now).days < 10 and sk.expires != 0:
output.print_warning(f"The daemon's key ({k.fpr}) will expire soon, in {(exp - now).days} days on {exp.strftime('%x')}.")
else:
output.print_ok(f"The daemon's key ({k.fpr}) is good. It expires in {(exp - now).days} days on {exp.strftime('%x')}.")
# Check imported keys
keys = get_imported_keys()
if len(keys) == 0:
output.print_warning("There are no imported keys here.")
else:
about_to_expire = []
expired = []
revoked = []
for key in keys:
if key.revoked == 1:
revoked.append(key)
continue
else:
for skey in key.subkeys:
exp = datetime.datetime.utcfromtimestamp(skey.expires)
if skey.expired == 1:
expired.append((key, skey))
elif (exp - now).days < 10 and skey.expires != 0:
about_to_expire.append((key, skey))
all_good = True
def printpair(keytuple):
key, skey = keytuple
output.print_line(f"Key {key.fpr}, subkey {skey.keyid}")
if len(about_to_expire) != 0:
all_good = False
output.print_warning(f"There {'is 1 subkey' if len(about_to_expire) == 1 else f'are {len(about_to_expire)} subkeys'} about to expire.")
list(map(printpair, about_to_expire))
if len(expired) != 0:
all_good = False
output.print_error(f"There {'is 1 expired subkey' if len(expired) == 1 else f'are {len(expired)} expired subkeys'}.")
list(map(printpair, expired))
if len(revoked) != 0:
all_good = False
output.print_error(f"There {'is 1 revoked key' if len(revoked) == 1 else f'are {len(revoked)} revoked keys'}.")
list(map(lambda k: output.print_line(k.fpr), revoked))
if all_good:
output.print_ok("All imported keys are good.")
def run_network_checks(env, output):
# Also see setup/network-checks.sh.

View file

@ -105,6 +105,8 @@
DNS</a></li>
<li class="dropdown-item"><a href="#external_dns"
onclick="return show_panel(this);">External DNS</a></li>
<li class="dropdown-item"><a href="#pgp_keyring"
onclick="return show_panel(this);">PGP Keyring Management</a></li>
<li class="dropdown-item"><a href="/admin/munin" target="_blank">Munin Monitoring</a></li>
</ul>
</li>
@ -153,6 +155,10 @@
{% include "custom-dns.html" %}
</div>
<div id="panel_pgp_keyring" class="admin_panel">
{% include "pgp-keyring.html" %}
</div>
<div id="panel_login" class="admin_panel">
{% include "login.html" %}
</div>
@ -199,7 +205,7 @@
<div id="global_modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="errorModalTitle"
aria-hidden="true">
<div class="modal-dialog modal-sm">
<div class="modal-dialog modal-sm" style="max-width: 600px;">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title" id="errorModalTitle"> </h4>

View file

@ -0,0 +1,279 @@
<style>
#pgp_keyring_config .status-error {
color: rgb(140, 0, 0);
}
#pgp_keyring_config .status-warning {
color: rgb(170, 120, 0);
}
#pgp_keyring_config .status-ok {
color: rgb(0, 140, 0);
}
#pgp_keyring_config .status-none {
color: rgb(190, 190, 190);
}
#pgp_keyring_config #uids {
white-space: pre-line;
}
</style>
<h2>PGP Keyring Management</h2>
<template id="pgpkey-template">
<tr>
<td>
<div id="trustlevel" style="font-size: 14pt;"><b>Trust Level:</b> Ultimate</div>
<code id="uids">
🤖 Power Mail-in-a-Box Management Daemon &lt;administrator@mailinabox.lan&gt;
</code>
<h3 style="font-size: 12pt;">Subkeys</h3>
<table id="subkeys">
<tr id="subkey-template">
<td id="ismaster">🔑</td>
<td>
<b>
<a id="sign">S</a>
<a id="cert">C</a>
<a id="encr">E</a>
<a id="auth">A</a>
</b>
</td>
<td style="width: 120pt;">
<b id="algo">RSA, 3072 bit</b>
</td>
<td>
<pre id="fpr">1756 6B81 D8A4 24C7 0098 659E 6872 2633 F692 52C6</pre>
</td>
<td id="expiration">
12/12/20 (119 days)
</td>
</tr>
</table>
</td>
<td id="options" style="width: 140pt;">
</td>
</tr>
</template>
<div id="pgp_keyring_config">
<h3>Daemon's Private Key</h3>
<table id="privatekey" class="table container">
</table>
<h3>Imported Public Keys</h3>
<table id="pubkeys" class="table container">
</table>
<h3>Import Key</h3>
<p>
You can upload your <b>public</b> key/keychain here. Keys <b>must</b> be submitted in ASCII-armored format.
<br>
If you're using <code>gpg</code>, you can export your public key by following this example:
<pre>
# Get all the keys in the ring
<b>$ gpg --list-keys</b>
/home/you/.gnupg/pubring.kbx
----------------------------
pub rsa4096 1970-01-01 [SC]
247C3553B4B36107BA0490C3CAFCCF3B4965761A
uid [ full ] Someone That I Used to Know &lt;someone@example.com&gt;
sub rsa2048 2020-01-01 [E] [expires: 2021-01-01]
pub rsa4096 2020-05-24 [SC] [expires: 2021-02-12]
52661092E5CD9EEFD7796B19E85F540C9318B69F
uid [ultimate] Me, Myself and I &lt;me@mydomain.com&gt;
sub rsa2048 2020-05-24 [E] [expires: 2021-02-12]
# Say that we want to export our own key, this is - "Me, Myself and I". That's the fingerprint "52661..."
<b>$ gpg --export --armor 52661092E5CD9EEFD7796B19E85F540C9318B69F</b> # Replace with your key's fingerprint
<b>-----BEGIN PGP PUBLIC KEY BLOCK-----
copy and paste this block in the area below
-----END PGP PUBLIC KEY BLOCK-----</b>
</pre>
</p>
<p><textarea id="pgp_paste_key" class="form-control" style="max-width: 40em; height: 8em" placeholder="-----BEGIN PGP PUBLIC KEY BLOCK-----&#xA;stuff here&#xA;-----END PGP PUBLIC KEY BLOCK-----"></textarea></p>
<button class="btn btn-primary" onclick="importkey()">Import Key</button>
</div>
<script>
function pretty_fpr(fpr) {
let pfpr = ""
for (let n = 0; n < 2; ++n) {
for (let i = 0; i < 5; ++i) {
pfpr += `${fpr.substring(n * 20 + i * 4, n * 20 + (i + 1) * 4)} `
}
pfpr += " "
}
return pfpr.substring(0, pfpr.length - 2)
}
function key_html(key, darken_bg, daemon) {
let keyrep = $("#pgpkey-template").html()
keyrep = $(keyrep)
keyrep.attr("id", key.master_fpr)
// Main key config
if (darken_bg) {
keyrep.addClass("bg-light")
}
const tlevel = keyrep.find("#trustlevel")
if (key.revoked) {
tlevel.html("<b class='status-error'>This key was revoked by it's owner.</b>")
} else {
tlevel.html("<b>Key is not revoked.</b>")
}
let uidtxt = ""
if (daemon) {
key.ids.forEach(id => {
uidtxt += "🤖 " + id + "\n"
});
} else {
key.ids.forEach(id => {
uidtxt += "🕵 " + id + "\n"
});
}
keyrep.find("#uids").text(uidtxt.substring(0, uidtxt.length - 1))
// Subkeys
const keyflags = ["sign", "cert", "encr", "auth"]
let subkeys = keyrep.find("#subkeys")
let subkeytemplate = subkeys.html()
keyrep.find("#subkey-template").remove()
key.subkeys.forEach(skey => {
let skeyrep = $(subkeytemplate)
skeyrep.attr("id", `sub${skey.fpr}`)
// Master key?
if (skey.master) {
skeyrep.find("#ismaster").html("🔑")
} else {
skeyrep.find("#ismaster").html("")
}
// Usage flags
keyflags.forEach(flag => {
if (!skey[flag]) {
skeyrep.find(`#${flag}`).addClass("status-none")
}
});
// Algorithm and fingerprint
skeyrep.find("#algo").html(`${skey.algorithm}, ${skey.bits} bits`)
skeyrep.find("#fpr").html(pretty_fpr(skey.fpr))
let expiration = skeyrep.find("#expiration")
// Expiration
if (key.revoked) {
skeyrep.addClass("status-error")
expiration.html(`Revoked`)
} else if (skey.expired) {
skeyrep.addClass("status-error")
expiration.html(`${skey.expires_date} (expired)`)
} else if (skey.expires && skey.expires_days <= 14) {
skeyrep.addClass("status-warning")
expiration.html(`${skey.expires_date} (${skey.expires_days} days)`)
} else if (skey.expires) {
skeyrep.addClass("status-ok")
expiration.html(`${skey.expires_date} (${skey.expires_days} days)`)
} else {
skeyrep.addClass("status-ok")
expiration.html("Does not expire")
}
skeyrep.appendTo(subkeys)
});
// Options
if (daemon) {
keyrep.find("#options").html(`<button class="btn btn-primary btn-block" onclick="exportkey('${key.master_fpr}')">Export Public Key</button>`)
} else {
keyrep.find("#options").html(`<button class="btn btn-secondary btn-block" onclick="exportkey('${key.master_fpr}')">Export Public Key</button><button class="btn btn-danger btn-block" onclick="rmkey('${key.master_fpr}')">Remove Key</button>`)
}
return keyrep
}
function show_pgp_keyring() {
$('#privatekey').html("<tr><td class='text-muted'>Loading...</td></tr>")
$('#pubkeys').html("<tr><td class='text-muted'>Loading...</td></tr>")
api(
"/system/pgp/",
"GET",
{},
function(r) {
$('#privatekey').html("")
$('#pubkeys').html("")
key_html(r.daemon, true, true).appendTo("#privatekey")
let pendulum = 1
r.imported.forEach(k => {
key_html(k, pendulum > 0, false).appendTo("#pubkeys")
pendulum *= -1
});
}
)
}
function exportkey(fpr) {
api(
`/system/pgp/${fpr}/export`,
"GET",
{},
function(r) {
show_modal_error("PGP Key", `Key export for <b>${fpr}</b>:<br><br><pre>${r}</pre>`)
},
function(_ ,xhr) {
if (xhr.status == 404) {
show_modal_error("Error", `The key you asked for (<b>${fpr}</b>) does not exist!`)
} else {
// Fallback to the default error modal
show_modal_error("Error", "Something went wrong, sorry.")
}
}
)
}
function rmkey(fpr) {
show_modal_confirm("Delete key", `Are you sure you wish to remove the key with the fingerprint ${pretty_fpr(fpr)}?`, "Yes, remove it", () => {
api(
`/system/pgp/${fpr}`,
"DELETE",
{},
function(r) {
show_modal_error("Delete key", r, show_pgp_keyring)
},
function(r) {
show_modal_error("Key deletion error", r)
}
)
}, ()=>{})
}
function importkey() {
api(
"/system/pgp/import",
"POST",
{
key: $("#pgp_paste_key").val()
},
function(r) {
show_modal_error("Import Results", `<ul>
<li><b>Keys read:</b> ${r.keys_read}</li>
<li><b>Keys added:</b> ${r.keys_added}</li>
<li><b>Keys not changed:</b> ${r.keys_unchanged}</li>
<li><b>User id's added:</b> ${r.uids_added}</li>
<li><b>Signatures added:</b> ${r.sigs_added}</li>
<li><b>Revocations added:</b> ${r.revs_added}</li>
</ul>`, show_pgp_keyring)
},
function(r) {
show_modal_error("Import Error", r)
}
)
}
</script>

View file

@ -225,3 +225,7 @@ function git_clone {
function php_version {
php --version | head -n 1 | cut -d " " -f 2 | cut -c 1-3
}
function python_version {
python3 --version | cut -d " " -f 2 | cut -c 1-3
}

View file

@ -29,7 +29,7 @@ done
#
# certbot installs EFF's certbot which we use to
# provision free TLS certificates.
apt_install duplicity python3-pip virtualenv certbot
apt_install duplicity python3-pip python3-gpg virtualenv certbot
hide_output pip3 install --upgrade boto
# Create a virtualenv for the installation of Python 3 packages
@ -52,6 +52,11 @@ hide_output $venv/bin/pip install --upgrade \
flask dnspython python-dateutil \
"idna>=2.0.0" "cryptography==2.2.2" boto psutil postfix-mta-sts-resolver
# Make the venv use the packaged gpgme bindings (the ones pip provides are severely out-of-date)
if [ ! -d $venv/lib/python$(python_version)/site-packages/gpg/ ]; then
ln -s /usr/lib/python3/dist-packages/gpg/ $venv/lib/python$(python_version)/site-packages/
fi
# CONFIGURATION
# Create a backup directory and a random key for encrypting backups.

32
setup/pgp.sh Executable file
View file

@ -0,0 +1,32 @@
#!/bin/bash
# Daemon PGP Keyring
# ------------------
#
# Initializes the PGP keyring at /home/user-data/.gnupg
# For this, we will generate a new PGP keypair (if one isn't already present)
source setup/functions.sh # load our functions
source /etc/mailinabox.conf # load global vars
export GNUPGHOME # Dump into the environment so that gpg uses it as homedir
# Install gnupg
apt_install gnupg
if [ "$(gpg --list-secret-keys 2> /dev/null)" = "" -o "${PGPKEY-}" = "" ]; then
echo "No keypair found. Generating daemon's PGP keypair..."
gpg --generate-key --batch << EOF;
%no-protection
Key-Type: RSA
Key-Length: 4096
Key-Usage: sign,encrypt,auth
Name-Real: Power Mail-in-a-Box Management Daemon
Name-Email: administrator@${PRIMARY_HOSTNAME}
Expire-Date: 180d
%commit
EOF
chown -R root:root $GNUPGHOME
# Remove the old key fingerprint if it exists, and add the new one
echo "$(cat /etc/mailinabox.conf | grep -v "PGPKEY")" > /etc/mailinabox.conf
echo "PGPKEY=$(gpg --list-secret-keys --with-colons | grep fpr | head -n 1 | sed 's/fpr//g' | sed 's/://g')" >> /etc/mailinabox.conf
fi

View file

@ -95,12 +95,15 @@ PUBLIC_IPV6=$PUBLIC_IPV6
PRIVATE_IP=$PRIVATE_IP
PRIVATE_IPV6=$PRIVATE_IPV6
MTA_STS_MODE=${MTA_STS_MODE-}
GNUPGHOME=${STORAGE_ROOT}/.gnupg/
PGPKEY=${DEFAULT_PGPKEY-}
EOF
# Start service configuration.
source setup/system.sh
source setup/ssl.sh
source setup/dns.sh
source setup/pgp.sh
source setup/mail-postfix.sh
source setup/mail-dovecot.sh
source setup/mail-users.sh

View file

@ -23,7 +23,7 @@ echo "Installing Roundcube (webmail)..."
apt_install \
dbconfig-common \
php-cli php-sqlite3 php-intl php-json php-common php-curl php-ldap \
php-gd php-pspell tinymce libjs-jquery libjs-jquery-mousewheel libmagic1 php-mbstring
php-gd php-pspell tinymce libjs-jquery libjs-jquery-mousewheel libmagic1 php-mbstring php-gnupg
# Install Roundcube from source if it is not already present or if it is out of date.
# Combine the Roundcube version number with the commit hash of plugins to track
@ -126,7 +126,7 @@ cat > $RCM_CONFIG <<EOF;
\$config['support_url'] = 'https://mailinabox.email/';
\$config['product_name'] = '$PRIMARY_HOSTNAME Webmail';
\$config['des_key'] = '$SECRET_KEY';
\$config['plugins'] = array('html5_notifier', 'archive', 'zipdownload', 'password', 'managesieve', 'jqueryui', 'persistent_login', 'carddav');
\$config['plugins'] = array('html5_notifier', 'archive', 'zipdownload', 'password', 'managesieve', 'jqueryui', 'persistent_login', 'carddav', 'enigma');
\$config['skin'] = 'elastic';
\$config['login_autocomplete'] = 2;
\$config['password_charset'] = 'UTF-8';
@ -134,6 +134,35 @@ cat > $RCM_CONFIG <<EOF;
?>
EOF
mkdir -p ${STORAGE_ROOT}/userkeys/
chmod 700 ${STORAGE_ROOT}/userkeys/
chown www-data:www-data ${STORAGE_ROOT}/userkeys/
# Configure Enigma
cat > ${RCM_PLUGIN_DIR}/enigma/config.inc.php <<EOF;
<?php
/* Do not edit. Written by Mail-in-a-Box. Regenerated on updates. */
\$config['enigma_pgp_driver'] = 'gnupg';
\$config['enigma_smime_driver'] = 'phpssl';
\$config['enigma_debug'] = false;
\$config['enigma_pgp_homedir'] = '${STORAGE_ROOT}/.enigma/';
\$config['enigma_pgp_binary'] = '';
\$config['enigma_pgp_agent'] = '';
\$config['enigma_pgp_gpgconf'] = '';
\$config['enigma_pgp_cipher_algo'] = null;
\$config['enigma_pgp_digest_algo'] = null;
\$config['enigma_multihost'] = false;
\$config['enigma_signatures'] = true;
\$config['enigma_decryption'] = true;
\$config['enigma_encryption'] = true;
\$config['enigma_sign_all'] = false;
\$config['enigma_encrypt_all'] = false;
\$config['enigma_attach_pubkey'] = false;
\$config['enigma_password_time'] = 5;
\$config['enigma_options_lock'] = array();
?>
EOF
# Configure CardDav
cat > ${RCM_PLUGIN_DIR}/carddav/config.inc.php <<EOF;
<?php