Browse Source

initial commit

master
Fr3deric 6 years ago
commit
f7abd808a3
  1. 1
      journalmarks/__init__.py
  2. 194
      journalmarks/journalmarks.py
  3. 186
      journalmarks/static/journalmarks.js
  4. 7
      journalmarks/templates/base.html
  5. 27
      journalmarks/templates/get_journalmark.html
  6. 57
      journalmarks/templates/index.html
  7. 39
      journalmarks/templates/login.html
  8. 17
      journalmarks/templates/logout.html
  9. 68
      journalmarks/templates/overview.html

1
journalmarks/__init__.py

@ -0,0 +1 @@ @@ -0,0 +1 @@
from .journalmarks import app

194
journalmarks/journalmarks.py

@ -0,0 +1,194 @@ @@ -0,0 +1,194 @@
import re
import json
import random
import string
import hashlib
import datetime
import functools
from urllib.parse import urlparse
from flask import Flask, request, session, render_template, url_for, redirect
from flask import g as flask_g
from peewee import CharField, DateTimeField, ForeignKeyField, DoesNotExist
from playhouse.flask_utils import FlaskDB
from passlib.hash import pbkdf2_sha256
app = Flask(__name__)
app.config.from_envvar('JOURNALMARKS_SETTINGS', silent=True)
app.secret_key = app.config['SECRET_KEY']
db_wrapper = FlaskDB(app)
class User(db_wrapper.Model):
username = CharField(unique=True)
password = CharField()
class AccessToken(db_wrapper.Model):
token = CharField()
redeemed = DateTimeField(null=True, default=None)
class Journalmark(db_wrapper.Model):
user = ForeignKeyField(User, backref='journalmarks')
created = DateTimeField(default=datetime.datetime.now())
tag = CharField(unique=True)
content = CharField()
def login_required(f):
@functools.wraps(f)
def decorated_function(*args, **kwargs):
if 'username' not in session:
return redirect(url_for('login', next=request.path))
try:
u = User.select().where(User.username == session['username']).get()
except DoesNotExist:
return ('user not found', 400, None)
flask_g.user = u
return f(*args, **kwargs)
return decorated_function
@app.cli.command('initdb')
def initdb_command():
db_wrapper.database.create_tables([User, AccessToken, Journalmark])
@app.route('/register', methods=['POST'])
def register():
if len(request.form) != 3:
return ('wrong number of fields', 400, None)
try:
token = request.form['token']
username = request.form['username']
password_hash = request.form['password_hash']
except ValueError:
return ('invalid field names', 400, None)
if not re.match('^[a-zA-Z0-9]*$', token):
return ('invalid token', 400, None)
if not re.match('^[a-zA-Z0-9._]*$', username):
return ('invalid username', 400, None)
if not re.match('^[a-f0-9]{64}$', password_hash):
return ('invalid password hash', 400, None)
password_hash = pbkdf2_sha256.encrypt(password_hash)
try:
at = AccessToken.select().where(AccessToken.token == token).get()
except DoesNotExist:
return ('token does not exist', 400, None)
if at.redeemed is not None:
return ('token already used', 400, None)
if User.select().where(User.username == username).count() > 0:
return ('user already exists', 400, None)
with db_wrapper.database.atomic():
u = User(username=username, password=password_hash)
at.redeemed = datetime.datetime.now()
u.save()
at.save()
return json.dumps('ok')
@app.route('/login', methods=['GET'])
def show_login():
if 'next' in request.args and urlparse(request.args['next']).netloc == '':
next = request.args['next']
else:
next = url_for('index')
return render_template('login.html', next=next)
@app.route('/login', methods=['POST'])
def login():
print(request.json)
if len(request.json) != 2:
return ('wrong number of fields', 400, None)
try:
username = request.json['username']
password_hash = request.json['password_hash']
except ValueError:
return ('invalid field names', 400, None)
if not re.match('^[a-zA-Z0-9._]*$', username):
return ('invalid username', 400, None)
if not re.match('^[a-f0-9]{64}$', password_hash):
return ('invalid password hash', 400, None)
try:
u = User.select().where(User.username == username).get()
except DoesNotExist:
return ('invalid credentials', 400, None)
if not pbkdf2_sha256.verify(password_hash, u.password):
return ('invalid credentials', 400, None)
session['username'] = u.username
session.permanent = True
return json.dumps('ok')
@app.route('/logout')
@login_required
def logout():
del session['username']
return render_template('logout.html')
@app.route('/')
@login_required
def index():
return render_template('index.html')
@app.route('/create', methods=['POST'])
@login_required
def create():
if len(request.json) != 1 or 'content' not in reqjest.json:
return ('invalid fields', 400, None)
content = request.json['content']
tag = None
while tag is None:
tag = ''.join([ random.choice(string.ascii_lowercase + string.digits) \
for i in range(4) ])
if Journalmark.select().where(Journalmark.tag == tag).count() > 0:
tag = None
j = Journalmark(user=flask_g.user, tag=tag, content=json.dumps(content))
j.save()
return json.dumps(tag)
@app.route('/overview', methods=['GET'])
@login_required
def overview():
return render_template('overview.html')
@app.route('/overview', methods=['POST'])
@login_required
def overview_get_journalmarks():
jms = Journalmark.select().where(Journalmark.user == flask_g.user) \
.order_by(Journalmark.created.desc())
ret = []
for j in jms:
try:
content = json.loads(j.content)
except ValueError:
content = None
ret.append({'created': j.created.isoformat(), 'tag': j.tag, 'content': content})
return json.dumps(ret)
@app.route('/<tag>')
@login_required
def get_journalmark(tag):
print(tag)
try:
j = Journalmark.select().where(
(Journalmark.tag == tag) & (Journalmark.user == flask_g.user)
).get()
except DoesNotExist:
return ('tag not found', 404, None)
return render_template('get_journalmark.html', j=j)

186
journalmarks/static/journalmarks.js

@ -0,0 +1,186 @@ @@ -0,0 +1,186 @@
var enc = new TextEncoder("utf-8");
var dec = new TextDecoder("utf-8");
var userkey;
function computeKey(username, password) {
return new Promise(function(resolve, reject) {
window.crypto.subtle.importKey(
'raw',
password,
{name: 'PBKDF2'},
false,
['deriveBits', 'deriveKey']
).then(function(key) {
return window.crypto.subtle.deriveKey(
{name: 'PBKDF2', salt: username, iterations: 4096, hash: 'SHA-256'},
key,
{name: 'AES-GCM', length: 256},
true,
[ "encrypt", "decrypt" ]
);
}).then(function(aeskey) {
resolve(aeskey);
}).catch(function(error) {
reject(error);
});
});
}
function storeKey(aeskey) {
return new Promise(function(resolve, reject) {
window.crypto.subtle.exportKey('jwk', aeskey).then(function(jwk) {
window.localStorage['userkey'] = JSON.stringify(jwk);
resolve();
}).catch(function(error) {
reject(error);
});
});
}
function loadKey() {
return new Promise(function(resolve, reject) {
var jwk = JSON.parse(window.localStorage['userkey']);
window.crypto.subtle.importKey(
'jwk',
jwk,
{name: 'AES-GCM', length: 256},
true,
['encrypt', 'decrypt']
).then(function(aeskey) {
resolve(aeskey);
}).catch(function(error) {
reject(error);
});
});
}
function deleteKey() {
return new Promise(function(resolve, reject) {
window.localStorage.removeItem('userkey');
});
}
function encrypt(data, key) {
return new Promise(function(resolve, reject) {
const iv = crypto.getRandomValues(new Uint8Array(12));
window.crypto.subtle.encrypt({name: 'AES-GCM', iv: iv}, key, data).then(function(encr) {
resolve([
base64js.fromByteArray(new Uint8Array(encr)),
base64js.fromByteArray(iv)
]);
}).catch(function(error) {
reject(error);
});
});
}
function decrypt(edata, key) {
return new Promise(function(resolve, reject) {
const encr = base64js.toByteArray(edata[0]);
const iv = base64js.toByteArray(edata[1]);
window.crypto.subtle.decrypt({name: 'AES-GCM', iv: iv}, key, encr).then(function(decr) {
resolve(decr);
}).catch(function(error) {
reject(error);
});
});
}
function journalmarks_loadkey() {
return new Promise(function(resolve, reject) {
loadKey().then(function(aeskey) {
userkey = aeskey;
resolve();
}).catch(function(error) {
reject(error);
});
});
}
function journalmarks_initkey(username, password) {
return new Promise(function(resolve, reject) {
computeKey(enc.encode(username), enc.encode(password))
.then(storeKey).then(function() {
resolve();
}).catch(function(error) {
reject(error);
});
});
}
function journalmarks_encrypturl(url) {
return new Promise(function(resolve, reject) {
var bytes = enc.encode(JSON.stringify({url: url}));
encrypt(bytes, userkey).then(function(encurl) {
resolve(encurl);
}).catch(function(error) {
reject(error);
});
});
}
function journalmarks_decrypturl(encurl) {
return new Promise(function(resolve, reject) {
decrypt(encurl, userkey).then(function(bytes) {
resolve(JSON.parse(dec.decode(bytes)));
}).catch(function(error) {
reject(error);
});
});
}
function post_object(url, data) {
return new Promise(function(resolve, reject) {
var req = new XMLHttpRequest();
req.open('POST', url);
req.setRequestHeader('Content-type', 'application/json');
req.onload = function() {
if (req.status == 200)
resolve(JSON.parse(req.response));
else
reject(Error(req.statusText));
};
req.onerror = function() {
reject(Error("Network Error"));
};
req.send(JSON.stringify(data));
});
}
// https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest
function sha256(str) {
var buffer = new TextEncoder("utf-8").encode(str);
return crypto.subtle.digest("SHA-256", buffer).then(function (hash) {
return hex(hash);
});
}
function hex(buffer) {
var hexCodes = [];
var view = new DataView(buffer);
for (var i = 0; i < view.byteLength; i += 4) {
// Using getUint32 reduces the number of iterations needed (we process 4 bytes each time)
var value = view.getUint32(i)
// toString(16) will give the hex representation of the number without padding
var stringValue = value.toString(16)
// We use concatenation and slice for padding
var padding = '00000000'
var paddedValue = (padding + stringValue).slice(-padding.length)
hexCodes.push(paddedValue);
}
// Join all the hex strings into one
return hexCodes.join("");
}

7
journalmarks/templates/base.html

@ -0,0 +1,7 @@ @@ -0,0 +1,7 @@
<!doctype html>
<title>Journalmarks</title>
<link rel=stylesheet type=text/css href="{{ url_for('static', filename='style.css') }}">
<script src="{{ url_for('static', filename='journalmarks.js') }}"></script>
<script src="{{ url_for('static', filename='base64js.min.js') }}"></script>
{% block script %}{% endblock %}
{% block body %}{% endblock %}

27
journalmarks/templates/get_journalmark.html

@ -0,0 +1,27 @@ @@ -0,0 +1,27 @@
{% extends "base.html" %}
{% block script %}
<script>
function run() {
journalmarks_loadkey().then(function() {
var encurl = JSON.parse({{ j.content|tojson|safe }});
journalmarks_decrypturl(encurl).then(function (url) {
document.getElementById('url').href = url.url;
document.getElementById('url').innerText = url.url;
document.getElementById('url').click();
});
}).catch(function() {
deleteKey();
window.location.href = '{{ url_for('login') }}';
});
}
if (document.readyState!='loading') run();
else if (document.addEventListener) document.addEventListener('DOMContentLoaded', run);
else document.attachEvent('onreadystatechange', function(){
if (document.readyState=='complete') run();
});
</script>
{% endblock %}
{% block body %}
<a href="" id="url" rel="noreferrer"></a>
{% endblock %}

57
journalmarks/templates/index.html

@ -0,0 +1,57 @@ @@ -0,0 +1,57 @@
{% extends "base.html" %}
{% block script %}
<script>
function isURL(str) {
try {
new URL(str)
return true;
} catch(e) {
return false;
}
}
function run() {
journalmarks_loadkey().catch(function() {
deleteKey();
window.location.href = '{{ url_for('login') }}';
});
document.getElementById('create').addEventListener('click', function() {
var url = document.getElementById('url').value;
if(isURL(url)) {
journalmarks_encrypturl(url).then(function (encurl) {
return post_object('{{ url_for('create') }}', {content: encurl});
}).then(function(tag) {
console.log('ok', tag);
});
} else if(url.match(/^[a-z0-9]{4}$/)) {
window.location.href = '/' + url;
} else {
console.log('not a URL and not a tag');
}
});
document.getElementById('url').addEventListener('keyup', function(e) {
if(e.keyCode == 13)
document.getElementById('create').click();
});
}
if (document.readyState!='loading') run();
else if (document.addEventListener) document.addEventListener('DOMContentLoaded', run);
else document.attachEvent('onreadystatechange', function(){
if (document.readyState=='complete') run();
});
</script>
{% endblock %}
{% block body %}
Welcome!
<p>
<a href="{{ url_for('overview') }}">overview</a>
<a href="{{ url_for('logout') }}">logout</a>
</p>
<input id="url" type="text">
<button id="create">ok</button>
{% endblock %}

39
journalmarks/templates/login.html

@ -0,0 +1,39 @@ @@ -0,0 +1,39 @@
{% extends "base.html" %}
{% block script %}
<script>
function run() {
document.getElementById('login').addEventListener('click', function() {
var username = document.getElementById('username').value;
var password = document.getElementById('password').value;
sha256(password).then(function(pwhash) {
var credentials = {username: username, password_hash: pwhash}
console.log(username, password, credentials);
post_object('{{ url_for('login') }}', credentials)
.then(journalmarks_initkey(username, password)).then(function() {
window.location.href = '{{ next }}';
}).catch(function(error) {
console.log('login error', error);
});
});
});
document.getElementById('password').addEventListener('keyup', function(e) {
if(e.keyCode == 13)
document.getElementById('login').click();
});
}
if (document.readyState!='loading') run();
else if (document.addEventListener) document.addEventListener('DOMContentLoaded', run);
else document.attachEvent('onreadystatechange', function(){
if (document.readyState=='complete') run();
});
</script>
{% endblock %}
{% block body %}
<h1>Login</h1>
<input type="text" id="username">
<input type="password" id="password">
<button id="login">Login</button>
{% endblock %}

17
journalmarks/templates/logout.html

@ -0,0 +1,17 @@ @@ -0,0 +1,17 @@
{% extends "base.html" %}
{% block script %}
<script>
function run() {
deleteKey();
}
if (document.readyState!='loading') run();
else if (document.addEventListener) document.addEventListener('DOMContentLoaded', run);
else document.attachEvent('onreadystatechange', function(){
if (document.readyState=='complete') run();
});
</script>
{% endblock %}
{% block body %}
Goodbye!
{% endblock %}

68
journalmarks/templates/overview.html

@ -0,0 +1,68 @@ @@ -0,0 +1,68 @@
{% extends "base.html" %}
{% block script %}
<script>
function run() {
/*
var encurl = JSON.parse();
journalmarks_decrypturl(encurl).then(function (url) {
document.getElementById('url').href = url.url;
document.getElementById('url').innerText = url.url;
document.getElementById('url').click();
});
*/
journalmarks_loadkey().then(function() {
return post_object('{{ url_for('overview_get_journalmarks') }}', {});
}).then(function(jms) {
console.log(jms);
var decrs = [];
jms.forEach(function(j) {
var p = document.getElementById('prototype');
var n = p.cloneNode(true);
n.id = 'journalmark_' + j.tag;
n.getElementsByClassName('date')[0].innerText = j.created;
n.getElementsByClassName('tag')[0].innerText = j.tag;
n.getElementsByClassName('tag')[0].href = '/' + j.tag;
n.getElementsByClassName('url')[0].innerText = 'decrypting...';
document.getElementById('journalmarks').appendChild(n);
decrs.push(journalmarks_decrypturl(j.content).then(function(url) {
n.getElementsByClassName('url')[0].href = url.url;
n.getElementsByClassName('url')[0].innerText = url.url;
}).catch(function(error) {
console.log(error);
n.getElementsByClassName('url')[0].innerText = 'unable to decrypt url';
}));
});
return Promise.all(decrs);
}).catch(function(error) {
console.log(error);
//deleteKey();
//window.location.href = '{{ url_for('login') }}';
});
}
if (document.readyState!='loading') run();
else if (document.addEventListener) document.addEventListener('DOMContentLoaded', run);
else document.attachEvent('onreadystatechange', function(){
if (document.readyState=='complete') run();
});
</script>
<style>
#prototype {
display: none;
}
#journalmarks p {
margin: 1em;
padding: 1em;
border: 1px solid grey;
}
</style>
{% endblock %}
{% block body %}
<div id="journalmarks">
<p id="prototype">
Tag: <a class="tag" href=""></a><br>
URL: <a class="url" rel="noreferrer" href=""></a><br>
Created: <span class="date"></span>
</p>
</div>
{% endblock %}
Loading…
Cancel
Save