Fr3deric
7 years ago
commit
f7abd808a3
9 changed files with 596 additions and 0 deletions
@ -0,0 +1 @@
@@ -0,0 +1 @@
|
||||
from .journalmarks import app |
@ -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) |
@ -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(""); |
||||
} |
@ -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 %} |
@ -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 %} |
@ -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 %} |
@ -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 %} |
@ -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 %} |
@ -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…
Reference in new issue