summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAssaf Gordon <assafgordon@gmail.com>2017-05-18 03:29:12 (GMT)
committerAssaf Gordon <assafgordon@gmail.com>2017-05-18 03:29:12 (GMT)
commitd82d2e5f1d6b9373234d39b557b9a7c14f1b0758 (patch)
tree460b5a68151bce2f58a806650be041184b7606d9
parent3f7b7e010ff562208c00f7b2dbe87fd9132561c7 (diff)
downloadvaranusex-d82d2e5f1d6b9373234d39b557b9a7c14f1b0758.zip
varanusex-d82d2e5f1d6b9373234d39b557b9a7c14f1b0758.tar.gz
varanusex-d82d2e5f1d6b9373234d39b557b9a7c14f1b0758.tar.bz2
account/change-gpg-keys: new page
-rw-r--r--varanusex/templates/account/change-gpg-key.html117
-rw-r--r--varanusex/templates/account/overview.html2
-rw-r--r--varanusex/utilities.py92
-rw-r--r--varanusex/views/account.py32
-rw-r--r--varanusex/views/account_web_forms.py6
5 files changed, 246 insertions, 3 deletions
diff --git a/varanusex/templates/account/change-gpg-key.html b/varanusex/templates/account/change-gpg-key.html
new file mode 100644
index 0000000..f77a70d
--- /dev/null
+++ b/varanusex/templates/account/change-gpg-key.html
@@ -0,0 +1,117 @@
+{#
+VaranusEx - Savannah Monitor
+
+Copyright (C) 2017 Assaf Gordon (assafgordon@gmail.com)
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+#}
+{% extends 'master-layout.html' %}
+
+{% block head %}
+<style>
+ div.formerror { color : red }
+
+ table.gpgkeys {
+ border-collapse: collapse;;
+ border-spacing: 1ex 1ex;
+ }
+ table.gpgkeys td {
+ padding-left: 1ex;
+ }
+ table.gpgkeys th {
+ padding-left: 1ex;
+ text-align: left;
+ }
+ table.gpgkeys tr:nth-child(odd) {
+ background-color: rgb(255, 251, 237);
+ }
+ table.gpgkeys tr:nth-child(even) {
+ background-color: rgb(255, 238, 204);
+ }
+ table.gpgkeys td:nth-child(1) {
+ }
+
+
+ textarea.fixedfont {
+ font-family:Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New, monospace;
+ }
+
+</style>
+{% endblock %}
+
+{% block content %}
+
+<div class="header">
+ <h1>Savannah Account Configuration</h1>
+</div>
+
+<div class="content">
+
+ <h2 class="content-subhead">Update GPG key</h2>
+ <p>
+ name: <b>{{current_user.realname}}</b>
+ <br/>
+ Email: <b>{{current_user.email}}</b>
+ <br/>
+ Username: <b>{{current_user.user_name}}</b>
+ <br/>
+ <br/>
+
+ {% if gpg_packets %}
+ <table class="gpgkeys">
+ <tr>
+ <th>Type</th>
+ <th>Key-Length</th>
+ <th>Key-ID</th>
+ <th>Created</th>
+ <th>Expires</th>
+ <th>User-ID</th>
+ </tr>
+ {% for g in gpg_packets %}
+ <tr>
+ <td>{{ g.type }}</td>
+ <td>{{ g.keylen }}</td>
+ <td>{{ g.keyid }}</td>
+ <td>{{ g.created }}</td>
+ <td>{{ g.expires }}</td>
+ <td>{{ g.userid }}</td>
+ </tr>
+ {% endfor %}
+ </table>
+ {% endif %}
+ <br/>
+ <br/>
+ <form class="pure-form pure-form-aligned" method="post">
+ {{ form.hidden_tag() }}
+
+ {{ form.new_key.label }}
+ <br/>
+ {{ form.new_key(cols=63,rows=10,class="fixedfont") }}
+
+ {% for message in form.new_key.errors %}
+ <div class="formerror">{{ message }}</div>
+ {% endfor %}
+ <br/>
+ <br/>
+ <input class="pure-button" type="submit" name="go" value="Update">
+
+ <a class="pure-button" href="{{ url_for('account_config') }}">Cancel</a>
+ </form>
+
+ </p>
+
+</div>
+
+
+{% endblock %}
diff --git a/varanusex/templates/account/overview.html b/varanusex/templates/account/overview.html
index 2b1870c..59c584f 100644
--- a/varanusex/templates/account/overview.html
+++ b/varanusex/templates/account/overview.html
@@ -88,7 +88,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<li>
<b>N</b> GPG public keys
<small>
- <a href="">add/remove keys</a>
+ <a href="{{ url_for('account_change_gpg_keys') }}">add/remove keys</a>
</small>
</li>
diff --git a/varanusex/utilities.py b/varanusex/utilities.py
index 909f272..6df8c8b 100644
--- a/varanusex/utilities.py
+++ b/varanusex/utilities.py
@@ -20,6 +20,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import re
import hashlib
import base64
+from subprocess import Popen,PIPE
+from tempfile import TemporaryFile
+from pprint import pprint
def get_cgi_item_id(args):
"""
@@ -96,3 +99,92 @@ def valid_ssh_pubkey(key):
"""Returns TRUE if 'key' is a valid SSH key"""
d = parse_ssh_pubkey(key)
return d['valid']
+
+
+gpg_line_re = re.compile('^[\w+/=]+$')
+gpg_ver_re = re.compile('^Version: [\w ]+$')
+def valid_gpg_ascii_line(l):
+ """Return TRUE if this line is valid in an
+ ASCII-armored GPG public key"""
+ if len(l)==0 or \
+ l == '-----END PGP PUBLIC KEY BLOCK-----' \
+ or \
+ l == '-----BEGIN PGP PUBLIC KEY BLOCK-----' \
+ or \
+ gpg_ver_re.match(l) \
+ or \
+ gpg_line_re.match(l):
+ return True
+ return False
+
+
+def decode_gpg_packets(key):
+ # raises OSError if 'gpg1' executable was not found in $PATH
+
+ tf = TemporaryFile()
+ tf.write(key.encode('ascii'))
+ tf.seek(0)
+
+ cmd = ['gpg1','--with-colons']
+ p = Popen(cmd,shell=False,stdin=tf,stdout=PIPE,stderr=PIPE)
+ (out,err) = p.communicate()
+
+ # Invalid key content (or other GPG error?)
+ if p.returncode != 0:
+ # TODO: log the error, for further diagnostics
+ # or user-support calls.
+ return None
+
+ # TODO: there could be non-UTF8 characters in the
+ # names of the GPG packages (e.g. person's name).
+ p = out.decode("utf-8","replace") # decode needed for python3's byte->string
+ p = p.split('\n')
+ p = [ x.strip() for x in p ]
+ p = [ x for x in p if x ]
+
+ # Parse gpg's "--with-colon" output.
+ # see: https://git.gnupg.org/cgi-bin/gitweb.cgi?p=gnupg.git;a=blob_plain;f=doc/DETAILS
+
+ g = []
+ for c in p:
+ flds = c.split(':')
+
+ x = {
+ "type" : flds[0],
+ "validity" : flds[1],
+ "keylen" : flds[2],
+ "keyid" : flds[4],
+ "created" : flds[5],
+ "expires" : flds[6],
+
+ # The user-ID fields sometimes contains colons,
+ # as in "[Expires: YYYY-MM-DD]". Probably GPG uses '[]'
+ # as 'quotes' in which colons are not field separators.
+ # Not going to handle it now...
+ "userid" : flds[9],
+ "fields" : flds,
+ "raw" : c
+ }
+ g.append(x)
+
+ return g
+
+
+def valid_gpg_packets(key):
+ p = decode_gpg_packets(key)
+ return p is not None
+
+
+def valid_gpg_pubkey(key):
+ """Returns TRUE if 'key' is a valid GPG key"""
+
+ # Step 1: Minimal content check, for invalid lines
+ key = key.strip().replace('\r','')
+ lines = key.split('\n')
+
+ l = [x for x in lines if not valid_gpg_ascii_line(x)]
+ if len(l)>0:
+ return False
+
+ # Step 2: Feed it to GPG
+ return valid_gpg_packets(key)
diff --git a/varanusex/views/account.py b/varanusex/views/account.py
index d11a850..dc67e59 100644
--- a/varanusex/views/account.py
+++ b/varanusex/views/account.py
@@ -25,11 +25,12 @@ from flask_login import login_required, login_user, \
import itsdangerous
from .account_web_forms import LoginForm, ChangeNameForm, ChangePasswordForm, \
ChangeEmailForm, ChangeSSHPubKeyForm, \
- LostPasswordForm, ResetPasswordForm
+ LostPasswordForm, ResetPasswordForm, \
+ ChangeGPGKeyForm
from .. import app, db
from ..models.users import User
-from ..utilities import valid_ssh_pubkey
+from ..utilities import valid_ssh_pubkey, valid_gpg_pubkey, decode_gpg_packets
@app.route('/login',methods=['GET','POST'])
@@ -298,3 +299,30 @@ def account_download_ssh_keys():
filename = current_user.user_name + "_authorized_keys_" + ts + ".txt"
response.headers['Content-Disposition'] = 'attachment; filename=' + filename
return response
+
+@login_required
+@app.route("/my/change_gpg_keys",methods=['GET','POST'])
+def account_change_gpg_keys():
+ f = ChangeGPGKeyForm()
+
+ gpg_packets = None
+ if request.method=="GET":
+ # Put the user's current GPG key.
+ f.new_key.data = current_user.gpg_key
+
+ # Try to parse it
+ if current_user.gpg_key:
+ gpg_packets = decode_gpg_packets(current_user.gpg_key)
+
+ if f.validate_on_submit():
+ k = f.new_key.data.strip()
+ if valid_gpg_pubkey(k):
+ current_user.gpg_key = k
+ db.session.add(current_user)
+ db.session.commit()
+ return redirect(url_for('account_config'))
+ else:
+ f.new_key.errors = ["Invalid GPG public key"]
+
+ return render_template('account/change-gpg-key.html',form=f,
+ gpg_packets=gpg_packets)
diff --git a/varanusex/views/account_web_forms.py b/varanusex/views/account_web_forms.py
index 6262688..bfea279 100644
--- a/varanusex/views/account_web_forms.py
+++ b/varanusex/views/account_web_forms.py
@@ -76,6 +76,12 @@ class ChangeSSHPubKeyForm(Form):
"""
new_key = TextAreaField('New SSH public key')
+class ChangeGPGKeyForm(Form):
+ """
+ Change GPG public key <Form>
+ """
+ new_key = TextAreaField('Update GPG Key')
+
class LostPasswordForm(Form):
"""