Add support for multiple keys and move config to config file

master
octycs 2020-03-18 18:15:06 +01:00
parent c3fd3beaea
commit d55fe53181
7 changed files with 263 additions and 76 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
server/config
server/history.db

View File

@ -1,3 +1,7 @@
a:visited {
color: #1b7340;
}
.card { .card {
border: 0; border: 0;
box-shadow: 0 .25rem 1rem rgba(48,55,66,.15); box-shadow: 0 .25rem 1rem rgba(48,55,66,.15);

View File

@ -1,6 +1,14 @@
// polyfills for older browsers
if (typeof String.prototype.startsWith === 'undefined') {
String.prototype.startsWith = function (needle) {
return this.indexOf(needle) === 0;
};
}
// === Core functions === // === Core functions ===
var api_base = "http://localhost:5000"; var api_base = "http://localhost:5000";
var app_token = null; var app_token = null;
var app_key_id = null;
function do_request(method, url, data, onsuccess, onerror) { function do_request(method, url, data, onsuccess, onerror) {
var req = new XMLHttpRequest(); var req = new XMLHttpRequest();
@ -26,9 +34,16 @@ function oninit() {
window.location.href = "#tracker" window.location.href = "#tracker"
app_token = _get_token(); app_token = _get_token();
app_key_id = _get_key_id(app_token);
if (app_token === null || app_key_id === null) {
ui_tracker_error("Interner Fehler: kein/ungültiges Token. Bitte stelle sicher, dass du diese Seite durch Scannen des QR Codes erreicht hast");
return;
}
if (window.location.href.search("#keyholder") > 0) if (window.location.href.search("#keyholder") > 0)
keyholder_onload(); keyholder_onload();
update_key_name();
update_key_status(); update_key_status();
load_saved_tracker_data(); load_saved_tracker_data();
@ -43,31 +58,31 @@ function oninit() {
} }
function tracker_onsend(e) { function tracker_onsend(e) {
ui_tracker_noerror(); ui_tracker_error(false);
var name = document.getElementById("input-name").value; var name = document.getElementById("input-name").value;
var contact = document.getElementById("input-contact").value; var contact = document.getElementById("input-contact").value;
var save_data = document.getElementById("input-tracker-save").checked; var save_data = document.getElementById("input-tracker-save").checked;
if (name.length < 3 || contact.length < 3) { if (name.length < 2 || contact.length < 2) {
ui_tracker_error("Name und Kontakt müssen mindestens 3 Zeichen lang sein") ui_tracker_error("Name und Kontakt müssen mindestens 3 Zeichen lang sein")
return; return;
} }
if (app_token === null) { if (app_token === null) {
ui_tracker_error("Interner Fehler: kein Token. Bitte stelle sicher, dass du diese Seite durch Scannen den QR Codes erreicht hast"); ui_tracker_error("Interner Fehler: kein Token. Bitte stelle sicher, dass du diese Seite durch Scannen des QR Codes erreicht hast");
return; return;
} }
document.getElementById("btn-tracker").classList.add("loading"); ui_tracker_send_loading(true);
do_request( do_request(
"POST", "POST",
"/claim", "/claim",
"name="+window.encodeURI(name)+"&contact="+window.encodeURI(contact), "name="+window.encodeURI(name)+"&contact="+window.encodeURI(contact),
function(r) { function(r) {
document.getElementById("btn-tracker").classList.remove("loading"); ui_tracker_send_loading(true);
if (r.status == 200) { if (r.status == 200) {
localStorage.setItem("last_cid", r.response); localStorage.setItem(app_key_id + "_last_cid", r.response);
if (save_data) { if (save_data) {
localStorage.setItem("name", name); localStorage.setItem("name", name);
localStorage.setItem("contact", contact); localStorage.setItem("contact", contact);
@ -83,7 +98,7 @@ function tracker_onsend(e) {
} }
}, },
function(r) { function(r) {
document.getElementById("btn-tracker").classList.remove("loading"); ui_tracker_send_loading(true);
ui_tracker_error("Fehlgeschlagen: Bitte überprüfe deine Internetverbindung"); ui_tracker_error("Fehlgeschlagen: Bitte überprüfe deine Internetverbindung");
console.log("Claim request failed"); console.log("Claim request failed");
console.log(r); console.log(r);
@ -92,14 +107,16 @@ function tracker_onsend(e) {
} }
function keyholder_onload() { function keyholder_onload() {
ui_password_noerror(); ui_password_error(false);
ui_keyholder_noerror(); ui_keyholder_error(false);
ui_keyholder_refresh_loading(true);
do_request( do_request(
"GET", "GET",
"/keyholder", "/keyholder",
null, null,
function(r) { function(r) {
ui_keyholder_refresh_loading(false);
if (r.status == 200) { if (r.status == 200) {
ui_show_keyholder(JSON.parse(r.response)); ui_show_keyholder(JSON.parse(r.response));
} else if (r.status == 403) { } else if (r.status == 403) {
@ -111,6 +128,7 @@ function keyholder_onload() {
} }
}, },
function(r) { function(r) {
ui_keyholder_refresh_loading(false);
ui_keyholder_error("Fehlgeschlagen: Bitte überprüfe deine Internetverbindung"); ui_keyholder_error("Fehlgeschlagen: Bitte überprüfe deine Internetverbindung");
console.log("Keyholder request failed"); console.log("Keyholder request failed");
console.log(r); console.log(r);
@ -119,8 +137,8 @@ function keyholder_onload() {
} }
function password_submit() { function password_submit() {
ui_password_noerror(); ui_password_error(false);
ui_keyholder_noerror(); ui_keyholder_error(false);
var pass = document.getElementById("input-pass").value; var pass = document.getElementById("input-pass").value;
var save_token = document.getElementById("input-pass-save").checked; var save_token = document.getElementById("input-pass-save").checked;
@ -133,17 +151,28 @@ function password_submit() {
function(r) { function(r) {
document.getElementById("btn-send-pw").classList.remove("loading"); document.getElementById("btn-send-pw").classList.remove("loading");
if (r.status == 200) { if (r.status == 200) {
// Delete previously used token
if (app_token !== null && localStorage.getItem("token") !== null) {
var tokenlist = localStorage.getItem("token").split(",");
if (tokenlist.indexOf(app_token) !== -1) {
tokenlist.splice(tokenlist.indexOf(app_token), 1);
localStorage.setItem("token", tokenlist.join(","));
}
}
app_token = r.response; app_token = r.response;
if (save_token) if (save_token) {
localStorage.setItem("token", app_token); if (localStorage.getItem("token") !== null)
else localStorage.setItem("token", localStorage.getItem("token") + "," + app_token);
localStorage.removeItem("token"); else
localStorage.setItem("token", app_token)
}
ui_keyholder_loading(); ui_keyholder_loading();
keyholder_onload(); keyholder_onload();
} else if (r.status == 401) { } else if (r.status == 401) {
ui_password_error(); ui_password_error(true);
} else { } else {
ui_keyholder_error("Fehlgeschlagen: Status code " + r.status); ui_keyholder_error("Fehlgeschlagen: Status code " + r.status);
console.log("Auth request failed (Status code)"); console.log("Auth request failed (Status code)");
@ -162,10 +191,11 @@ function password_submit() {
// === Helper functions === // === Helper functions ===
function update_key_status() { function update_key_status() {
if (localStorage.getItem("last_cid") !== null) { var last_cid = localStorage.getItem(app_key_id + "_last_cid");
if (last_cid !== null) {
do_request( do_request(
"GET", "GET",
"/status/" + localStorage.getItem("last_cid"), "/status/" + last_cid,
null, null,
function(r) { function(r) {
if (r.status == 200 && r.response == "latest") if (r.status == 200 && r.response == "latest")
@ -178,6 +208,32 @@ function update_key_status() {
} }
} }
function update_key_name() {
// For perception purposes, the last key name is cached but
// updated as soon as possible.
var last_key_name = localStorage.getItem(app_key_id + "_last_key_name");
if (last_key_name !== null)
ui_set_key_name(last_key_name);
do_request(
"GET",
"/keyname",
null,
function(r) {
if (r.status == 200) {
ui_set_key_name(r.response);
localStorage.setItem(app_key_id + "_last_key_name", r.response);
} else {
ui_set_key_name("?");
console.log("Keyname request failed (status code " + r.status + ")");
}
},
function(r) {
ui_set_key_name("?");
console.log("Keyname request failed"); console.log(r)
}
);
}
function load_saved_tracker_data() { function load_saved_tracker_data() {
if (localStorage.getItem("name") !== null) if (localStorage.getItem("name") !== null)
document.getElementById("input-name").value = localStorage.getItem("name"); document.getElementById("input-name").value = localStorage.getItem("name");
@ -186,19 +242,44 @@ function load_saved_tracker_data() {
} }
function _get_token() { function _get_token() {
if (localStorage.getItem("token") === null) { // A token in the URL is always required to know the key
// Very dirty parsing as of now var token = null;
var params_str = window.location.search; // Very dirty parsing as of now
if (params_str.search("&") >= 0) var params_str = window.location.search;
return null; if (params_str.search("&") >= 0)
return null;
var parts = params_str.split("token="); var parts = params_str.split("token=");
if (parts.length != 2) if (parts.length != 2)
return null
else
token = parts[1];
if (localStorage.getItem("token") === null) {
return token;
} else {
var key_id = token.split(":")[0];
var tokenlist = localStorage.getItem("token").split(",");
// This returns the last stored token for this key.
// Generally it should not happen (tm) that there is more than one token.
for (var i = tokenlist.length - 1; i >= 0; i--) {
if (tokenlist[i].indexOf(key_id + ":") === 0)
return tokenlist[i];
}
return token;
}
}
function _get_key_id(token) {
if (token === null) {
return null;
} else {
var parts = token.split(":");
if (parts.length !== 2)
return null return null
else else
return parts[1]; return parts[0];
} else {
return localStorage.getItem("token");
} }
} }
@ -222,6 +303,11 @@ function ui_set_has_not_key() {
document.getElementById("tracker-formgroup").style.opacity = 1 document.getElementById("tracker-formgroup").style.opacity = 1
} }
function ui_set_key_name(name) {
document.getElementById("tracker-key-name").textContent = name;
document.getElementById("keyholder-key-name").textContent = name;
}
function ui_keyholder_loading() { function ui_keyholder_loading() {
document.getElementById("keyholder-loading").style.display = "block" document.getElementById("keyholder-loading").style.display = "block"
document.getElementById("btn-send-pw").style.display = "none" document.getElementById("btn-send-pw").style.display = "none"
@ -265,28 +351,41 @@ function ui_show_keyholder(keyholder) {
document.getElementById("keyholder-table-container").style.display = "block" document.getElementById("keyholder-table-container").style.display = "block"
} }
function ui_tracker_error(msg) { function ui_keyholder_refresh_loading(is_loading) {
document.getElementById("tracker-error").innerHTML = msg; if (is_loading)
document.getElementById("tracker-error").style.display = "block"; document.getElementById("btn-update").classList.add("loading");
else
document.getElementById("btn-update").classList.remove("loading");
} }
function ui_tracker_noerror(msg) { function ui_tracker_send_loading(is_loading) {
document.getElementById("tracker-error").style.display = "none"; if (is_loading)
document.getElementById("btn-tracker").classList.add("loading");
else
document.getElementById("btn-tracker").classList.remove("loading");
}
function ui_tracker_error(msg) {
if (msg === false) {
document.getElementById("tracker-error").style.display = "none";
} else {
document.getElementById("tracker-error").innerHTML = msg;
document.getElementById("tracker-error").style.display = "block";
}
} }
function ui_keyholder_error(msg) { function ui_keyholder_error(msg) {
document.getElementById("keyholder-error").innerHTML = msg; if (msg === false) {
document.getElementById("keyholder-error").style.display = "block"; document.getElementById("keyholder-error").style.display = "none";
} else {
document.getElementById("keyholder-error").innerHTML = msg;
document.getElementById("keyholder-error").style.display = "block";
}
} }
function ui_keyholder_noerror(msg) { function ui_password_error(is_error) {
document.getElementById("keyholder-error").style.display = "none"; if (is_error)
} document.getElementById("input-pass").classList.add("is-error");
else
function ui_password_error() { document.getElementById("input-pass").classList.remove("is-error");
document.getElementById("input-pass").classList.add("is-error")
}
function ui_password_noerror() {
document.getElementById("input-pass").classList.remove("is-error")
} }

View File

@ -17,6 +17,7 @@
<div class="card" id="tracker"> <div class="card" id="tracker">
<div class="card-header"> <div class="card-header">
<div class="card-title h5">Schlüsseltracker</div> <div class="card-title h5">Schlüsseltracker</div>
<div class="card-subtitle text-gray">für: <span id="tracker-key-name">...</span></div>
</div> </div>
<div class="card-body"> <div class="card-body">
<span id="has-key" style="display: none"><i class="icon icon-check"></i> Der Schlüssel ist aktuell auf dich eingetragen</span> <span id="has-key" style="display: none"><i class="icon icon-check"></i> Der Schlüssel ist aktuell auf dich eingetragen</span>
@ -54,7 +55,10 @@
</label> </label>
</div> </div>
<div id="keyholder-table-container" style="display: none"> <div id="keyholder-table-container" style="display: none">
<p>Folgende Personen hatten zuletzt den Schlüssel</p> <p>
Folgende Personen hatten zuletzt den Schlüssel für:
<span id="keyholder-key-name">...</span>
</p>
<table class="table table-hover"> <table class="table table-hover">
<thead> <thead>
<tr> <tr>

19
server/example-config Normal file
View File

@ -0,0 +1,19 @@
[Default]
# The Port of the server when running in development mode
Port = 5000
# This is the path to the sqlite database.
# If it does not exist yet, it will be created on startup.
DbPath = history.db
# Optionally, the keytracker can provide a CORS Header in case
# the Website is deployed on a different (sub)domain than the API.
# Use CorsOrigin = * to make it publically available (not advised).
CorsOrigin = off
# In the following section you can configure the keys that can be tracked.
# Use the script generate_key.py to generate the configuration line
# (each line is one key).
[Keys]
8174875f7d85 = Chris Büro;49c5dbda74fe86eae0dd1ce6;247f16f579033a6a947b3be301407319cd9bfe14f11554d71ea3190e04f7cb91
7fc944c9e632 = Test=2;74fffaf6e463950fc6da3fd3;95313e37ff448b1a19b133fd8067c160f9f1c6d417f5d8dbec6f4f931097d389

25
server/generate_key.py Normal file
View File

@ -0,0 +1,25 @@
import hashlib
import secrets
from server import PASS_SALT
if __name__ == "__main__":
try:
name = input("Key for (the description of the key): ")
if ";" in name:
print("The character ';' is not allowed in the key name")
exit(1)
password = input("Password (no hidden input here): ")
hashed = hashlib.pbkdf2_hmac("sha256", password.encode("utf-8"), PASS_SALT, 10000).hex()
key_id = secrets.token_hex(6)
claim_token = secrets.token_hex(12)
print("The configuration line is:")
print("{} = {};{};{}".format(key_id, name, claim_token, hashed))
print("The claim token is:")
print("{}:{}".format(key_id, claim_token))
except KeyboardInterrupt:
print("Cancelled")

View File

@ -6,37 +6,46 @@ import secrets
from flask import Flask, request, abort from flask import Flask, request, abort
from flask.json import jsonify from flask.json import jsonify
DB_PATH = "history.db"
PASS_FILE = "pass" import configparser
PASS_SALT = b"IgTp9iQH" config = configparser.ConfigParser()
CLAIM_TOKEN_FILE = "claim_token" config.read("config")
CORS_ORIGIN = "*"
PORT = config["Default"]["Port"]
DB_PATH = config["Default"]["DbPath"]
CORS_ORIGIN = None if config["Default"]["CorsOrigin"].lower() == "off" else config["Default"]["CorsOrigin"]
KEYS = {}
PASS_SALT = b"IgTp9iQH" # Static for now
app = Flask("Schluesselverfolgung") app = Flask("Schluesseltracker")
@app.route("/auth", methods=["POST"]) @app.route("/auth", methods=["POST"])
def auth(): def auth():
if "pass" in request.form: if "pass" in request.form:
hashed = hashlib.pbkdf2_hmac("sha512", key_id, permissions = _check_token()
hashed = hashlib.pbkdf2_hmac("sha256",
request.form["pass"].encode("utf-8"), request.form["pass"].encode("utf-8"),
PASS_SALT, PASS_SALT,
10000) 10000).hex()
with open(PASS_FILE, "rb") as f:
if hashed == f.read(): if key_id in KEYS:
new_token = secrets.token_hex(16) if hashed == KEYS[key_id]["pass_hash"]:
new_token = "{}:{}".format(key_id, secrets.token_hex(12))
_add_token(new_token, "claim,query") _add_token(new_token, "claim,query")
return new_token return new_token
else: else:
abort(401) abort(401)
else:
abort(400)
else: else:
abort(400) abort(400)
@app.route("/claim", methods=["POST"]) @app.route("/claim", methods=["POST"])
def claim(): def claim():
permissions = _check_token() key_id, permissions = _check_token()
if "claim" not in permissions: if "claim" not in permissions:
abort(403) abort(403)
@ -47,10 +56,10 @@ def claim():
contact = request.form["contact"].strip() contact = request.form["contact"].strip()
# These are arbitrary values but there to prevent sending an empty form # These are arbitrary values but there to prevent sending an empty form
if len(name) < 3 or len(contact) < 3: if len(name) < 2 or len(contact) < 2:
abort(400) abort(400)
claim_id = _add_claim(name, contact) claim_id = _add_claim(key_id, name, contact)
return claim_id return claim_id
@ -59,13 +68,22 @@ def status(cid):
return _get_claim_status(cid) return _get_claim_status(cid)
@app.route("/keyname")
def keyname():
key_id, permissions = _check_token()
if key_id in KEYS:
return KEYS[key_id]["name"]
else:
return ""
@app.route("/keyholder") @app.route("/keyholder")
def keyholder(): def keyholder():
permissions = _check_token() key_id, permissions = _check_token()
if "query" not in permissions: if "query" not in permissions:
abort(403) abort(403)
return jsonify(_get_keyholder()) return jsonify(_get_keyholder(key_id))
@app.after_request @app.after_request
@ -81,6 +99,7 @@ def _init_db():
c.execute(""" c.execute("""
CREATE TABLE IF NOT EXISTS History ( CREATE TABLE IF NOT EXISTS History (
Id INTEGER PRIMARY KEY, Id INTEGER PRIMARY KEY,
KId TEXT NOT NULL,
CId TEXT NOT NULL, CId TEXT NOT NULL,
Name TEXT NOT NULL, Name TEXT NOT NULL,
Contact TEXT NOT NULL, Contact TEXT NOT NULL,
@ -95,11 +114,11 @@ def _init_db():
Timestamp INTEGER NOT NULL Timestamp INTEGER NOT NULL
) )
""") """)
with open(CLAIM_TOKEN_FILE) as f: for key_id, key_data in KEYS.items():
c.execute(""" c.execute("""
INSERT OR IGNORE INTO Token (Token, Permissions, Timestamp) INSERT OR IGNORE INTO Token (Token, Permissions, Timestamp)
VALUES (?,?,?) VALUES (?,?,?)
""", (f.read(), "claim", datetime.now().timestamp())) """, (f"{key_id}:{key_data['claim_token']}", "claim", datetime.now().timestamp()))
c.commit() c.commit()
@ -119,25 +138,29 @@ def _check_token():
else: else:
abort(401) abort(401)
parts = token.split(":")
if len(parts) != 2 or len(parts[0]) == 0:
abort(400)
c = sqlite3.connect(DB_PATH) c = sqlite3.connect(DB_PATH)
conn = c.cursor() conn = c.cursor()
conn.execute("SELECT Permissions FROM Token WHERE Token=?", (token,)) conn.execute("SELECT Permissions FROM Token WHERE Token=?", (token,))
row = conn.fetchone() row = conn.fetchone()
if row is None: if row is None:
return set() return None, set()
else: else:
return set(row[0].split(",")) return parts[0], set(row[0].split(","))
def _add_claim(name, contact): def _add_claim(key_id, name, contact):
claim_id = secrets.token_hex(8) claim_id = secrets.token_hex(8)
c = sqlite3.connect(DB_PATH) c = sqlite3.connect(DB_PATH)
conn = c.cursor() conn = c.cursor()
conn.execute(""" conn.execute("""
INSERT INTO History (CId, Name, Contact, Timestamp) INSERT INTO History (CId, KId, Name, Contact, Timestamp)
VALUES (?,?,?,?) VALUES (?,?,?,?,?)
""", (claim_id, name, contact, datetime.now().timestamp())) """, (claim_id, key_id, name, contact, datetime.now().timestamp()))
c.commit() c.commit()
return claim_id return claim_id
@ -146,24 +169,28 @@ def _add_claim(name, contact):
def _get_claim_status(claim_id): def _get_claim_status(claim_id):
c = sqlite3.connect(DB_PATH) c = sqlite3.connect(DB_PATH)
conn = c.cursor() conn = c.cursor()
conn.execute("SELECT CId FROM History ORDER BY Timestamp DESC") conn.execute("SELECT KId FROM History WHERE CId=?", (claim_id,))
row = conn.fetchone() row = conn.fetchone()
if row is None: if row is None:
return "unknown" return "unknown"
else: else:
key_id = row[0]
conn.execute("SELECT CId FROM History WHERE KId=? ORDER BY Timestamp DESC", (key_id,))
row = conn.fetchone()
if row[0] == claim_id: if row[0] == claim_id:
return "latest" return "latest"
else: else:
return "outdated" return "outdated"
def _get_keyholder(): def _get_keyholder(key_id):
c = sqlite3.connect(DB_PATH) c = sqlite3.connect(DB_PATH)
conn = c.cursor() conn = c.cursor()
conn.execute(""" conn.execute("""
SELECT Name, Contact, Timestamp SELECT Name, Contact, Timestamp FROM History
FROM History ORDER BY Timestamp DESC LIMIT 3 WHERE KId=?
""") ORDER BY Timestamp DESC LIMIT 3
""", (key_id,))
# Timestamp precision is one minute # Timestamp precision is one minute
keyholder = [{"name": row[0], "contact": row[1], "timestamp": int(row[2] // 60 * 60)} keyholder = [{"name": row[0], "contact": row[1], "timestamp": int(row[2] // 60 * 60)}
for row in conn] for row in conn]
@ -171,5 +198,12 @@ def _get_keyholder():
if __name__ == "__main__": if __name__ == "__main__":
for key_id in config["Keys"]:
parts = config["Keys"][key_id].split(";")
if len(parts) != 3:
print("Invalid key configuration")
exit(1)
KEYS[key_id] = {"name": parts[0], "claim_token": parts[1], "pass_hash": parts[2]}
_init_db() _init_db()
app.run() app.run(port=PORT)