Add support for multiple keys and move config to config file

This commit is contained in:
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 {
border: 0;
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 ===
var api_base = "http://localhost:5000";
var app_token = null;
var app_key_id = null;
function do_request(method, url, data, onsuccess, onerror) {
var req = new XMLHttpRequest();
@ -26,9 +34,16 @@ function oninit() {
window.location.href = "#tracker"
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)
keyholder_onload();
update_key_name();
update_key_status();
load_saved_tracker_data();
@ -43,31 +58,31 @@ function oninit() {
}
function tracker_onsend(e) {
ui_tracker_noerror();
ui_tracker_error(false);
var name = document.getElementById("input-name").value;
var contact = document.getElementById("input-contact").value;
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")
return;
}
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;
}
document.getElementById("btn-tracker").classList.add("loading");
ui_tracker_send_loading(true);
do_request(
"POST",
"/claim",
"name="+window.encodeURI(name)+"&contact="+window.encodeURI(contact),
function(r) {
document.getElementById("btn-tracker").classList.remove("loading");
ui_tracker_send_loading(true);
if (r.status == 200) {
localStorage.setItem("last_cid", r.response);
localStorage.setItem(app_key_id + "_last_cid", r.response);
if (save_data) {
localStorage.setItem("name", name);
localStorage.setItem("contact", contact);
@ -83,7 +98,7 @@ function tracker_onsend(e) {
}
},
function(r) {
document.getElementById("btn-tracker").classList.remove("loading");
ui_tracker_send_loading(true);
ui_tracker_error("Fehlgeschlagen: Bitte überprüfe deine Internetverbindung");
console.log("Claim request failed");
console.log(r);
@ -92,14 +107,16 @@ function tracker_onsend(e) {
}
function keyholder_onload() {
ui_password_noerror();
ui_keyholder_noerror();
ui_password_error(false);
ui_keyholder_error(false);
ui_keyholder_refresh_loading(true);
do_request(
"GET",
"/keyholder",
null,
function(r) {
ui_keyholder_refresh_loading(false);
if (r.status == 200) {
ui_show_keyholder(JSON.parse(r.response));
} else if (r.status == 403) {
@ -111,6 +128,7 @@ function keyholder_onload() {
}
},
function(r) {
ui_keyholder_refresh_loading(false);
ui_keyholder_error("Fehlgeschlagen: Bitte überprüfe deine Internetverbindung");
console.log("Keyholder request failed");
console.log(r);
@ -119,8 +137,8 @@ function keyholder_onload() {
}
function password_submit() {
ui_password_noerror();
ui_keyholder_noerror();
ui_password_error(false);
ui_keyholder_error(false);
var pass = document.getElementById("input-pass").value;
var save_token = document.getElementById("input-pass-save").checked;
@ -133,17 +151,28 @@ function password_submit() {
function(r) {
document.getElementById("btn-send-pw").classList.remove("loading");
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;
if (save_token)
localStorage.setItem("token", app_token);
if (save_token) {
if (localStorage.getItem("token") !== null)
localStorage.setItem("token", localStorage.getItem("token") + "," + app_token);
else
localStorage.removeItem("token");
localStorage.setItem("token", app_token)
}
ui_keyholder_loading();
keyholder_onload();
} else if (r.status == 401) {
ui_password_error();
ui_password_error(true);
} else {
ui_keyholder_error("Fehlgeschlagen: Status code " + r.status);
console.log("Auth request failed (Status code)");
@ -162,10 +191,11 @@ function password_submit() {
// === Helper functions ===
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(
"GET",
"/status/" + localStorage.getItem("last_cid"),
"/status/" + last_cid,
null,
function(r) {
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() {
if (localStorage.getItem("name") !== null)
document.getElementById("input-name").value = localStorage.getItem("name");
@ -186,7 +242,8 @@ function load_saved_tracker_data() {
}
function _get_token() {
if (localStorage.getItem("token") === null) {
// A token in the URL is always required to know the key
var token = null;
// Very dirty parsing as of now
var params_str = window.location.search;
if (params_str.search("&") >= 0)
@ -196,9 +253,33 @@ function _get_token() {
if (parts.length != 2)
return null
else
return parts[1];
token = parts[1];
if (localStorage.getItem("token") === null) {
return token;
} else {
return localStorage.getItem("token");
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
else
return parts[0];
}
}
@ -222,6 +303,11 @@ function ui_set_has_not_key() {
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() {
document.getElementById("keyholder-loading").style.display = "block"
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"
}
function ui_tracker_error(msg) {
document.getElementById("tracker-error").innerHTML = msg;
document.getElementById("tracker-error").style.display = "block";
function ui_keyholder_refresh_loading(is_loading) {
if (is_loading)
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) {
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) {
if (msg === false) {
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) {
document.getElementById("keyholder-error").style.display = "none";
}
function ui_password_error() {
document.getElementById("input-pass").classList.add("is-error")
}
function ui_password_noerror() {
document.getElementById("input-pass").classList.remove("is-error")
function ui_password_error(is_error) {
if (is_error)
document.getElementById("input-pass").classList.add("is-error");
else
document.getElementById("input-pass").classList.remove("is-error");
}

View file

@ -17,6 +17,7 @@
<div class="card" id="tracker">
<div class="card-header">
<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 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>
@ -54,7 +55,10 @@
</label>
</div>
<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">
<thead>
<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.json import jsonify
DB_PATH = "history.db"
PASS_FILE = "pass"
PASS_SALT = b"IgTp9iQH"
CLAIM_TOKEN_FILE = "claim_token"
CORS_ORIGIN = "*"
import configparser
config = configparser.ConfigParser()
config.read("config")
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"])
def auth():
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"),
PASS_SALT,
10000)
with open(PASS_FILE, "rb") as f:
if hashed == f.read():
new_token = secrets.token_hex(16)
10000).hex()
if key_id in KEYS:
if hashed == KEYS[key_id]["pass_hash"]:
new_token = "{}:{}".format(key_id, secrets.token_hex(12))
_add_token(new_token, "claim,query")
return new_token
else:
abort(401)
else:
abort(400)
else:
abort(400)
@app.route("/claim", methods=["POST"])
def claim():
permissions = _check_token()
key_id, permissions = _check_token()
if "claim" not in permissions:
abort(403)
@ -47,10 +56,10 @@ def claim():
contact = request.form["contact"].strip()
# 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)
claim_id = _add_claim(name, contact)
claim_id = _add_claim(key_id, name, contact)
return claim_id
@ -59,13 +68,22 @@ def 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")
def keyholder():
permissions = _check_token()
key_id, permissions = _check_token()
if "query" not in permissions:
abort(403)
return jsonify(_get_keyholder())
return jsonify(_get_keyholder(key_id))
@app.after_request
@ -81,6 +99,7 @@ def _init_db():
c.execute("""
CREATE TABLE IF NOT EXISTS History (
Id INTEGER PRIMARY KEY,
KId TEXT NOT NULL,
CId TEXT NOT NULL,
Name TEXT NOT NULL,
Contact TEXT NOT NULL,
@ -95,11 +114,11 @@ def _init_db():
Timestamp INTEGER NOT NULL
)
""")
with open(CLAIM_TOKEN_FILE) as f:
for key_id, key_data in KEYS.items():
c.execute("""
INSERT OR IGNORE INTO Token (Token, Permissions, Timestamp)
VALUES (?,?,?)
""", (f.read(), "claim", datetime.now().timestamp()))
""", (f"{key_id}:{key_data['claim_token']}", "claim", datetime.now().timestamp()))
c.commit()
@ -119,25 +138,29 @@ def _check_token():
else:
abort(401)
parts = token.split(":")
if len(parts) != 2 or len(parts[0]) == 0:
abort(400)
c = sqlite3.connect(DB_PATH)
conn = c.cursor()
conn.execute("SELECT Permissions FROM Token WHERE Token=?", (token,))
row = conn.fetchone()
if row is None:
return set()
return None, set()
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)
c = sqlite3.connect(DB_PATH)
conn = c.cursor()
conn.execute("""
INSERT INTO History (CId, Name, Contact, Timestamp)
VALUES (?,?,?,?)
""", (claim_id, name, contact, datetime.now().timestamp()))
INSERT INTO History (CId, KId, Name, Contact, Timestamp)
VALUES (?,?,?,?,?)
""", (claim_id, key_id, name, contact, datetime.now().timestamp()))
c.commit()
return claim_id
@ -146,24 +169,28 @@ def _add_claim(name, contact):
def _get_claim_status(claim_id):
c = sqlite3.connect(DB_PATH)
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()
if row is None:
return "unknown"
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:
return "latest"
else:
return "outdated"
def _get_keyholder():
def _get_keyholder(key_id):
c = sqlite3.connect(DB_PATH)
conn = c.cursor()
conn.execute("""
SELECT Name, Contact, Timestamp
FROM History ORDER BY Timestamp DESC LIMIT 3
""")
SELECT Name, Contact, Timestamp FROM History
WHERE KId=?
ORDER BY Timestamp DESC LIMIT 3
""", (key_id,))
# Timestamp precision is one minute
keyholder = [{"name": row[0], "contact": row[1], "timestamp": int(row[2] // 60 * 60)}
for row in conn]
@ -171,5 +198,12 @@ def _get_keyholder():
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()
app.run()
app.run(port=PORT)