unlinking keys
This commit is contained in:
parent
e6524dce00
commit
88474a5ca6
|
@ -1,4 +1,7 @@
|
|||
import sqlite3
|
||||
import logging
|
||||
|
||||
DEBUG = False
|
||||
|
||||
def dict_factory(cursor, row):
|
||||
fields = [column[0] for column in cursor.description]
|
||||
|
@ -13,12 +16,15 @@ def generateToken(length):
|
|||
|
||||
class auth:
|
||||
ENABLE_REGISTRATION = True
|
||||
LINK_EXPIRE = 100*60
|
||||
LINK_EXPIRE = 10*60
|
||||
ANTIC_EXPIRE = 60*60*24
|
||||
|
||||
hash = None
|
||||
username = None
|
||||
# If outdated, userInfo will fetch data from database. Instead, it can be updated directly by this library to reduce disk IO.
|
||||
user = {"outdated": True}
|
||||
# User row cache. It's enough to ask database once and update cache only when asked for outdated or missing columns.
|
||||
user = {}
|
||||
userOutdated = []
|
||||
anticsrf = False
|
||||
|
||||
def __init__(self, dbFile):
|
||||
"""
|
||||
|
@ -26,6 +32,8 @@ class auth:
|
|||
"""
|
||||
self.con = sqlite3.connect(dbFile)
|
||||
self.con.row_factory = dict_factory
|
||||
if (DEBUG):
|
||||
self.con.set_trace_callback(logging.warning)
|
||||
self.cur = self.con.cursor()
|
||||
|
||||
self.cur.execute("""
|
||||
|
@ -52,7 +60,7 @@ class auth:
|
|||
""")
|
||||
|
||||
self.garbageCollector()
|
||||
|
||||
|
||||
def garbageCollector(self):
|
||||
"""
|
||||
delete all unlinked keys
|
||||
|
@ -60,7 +68,7 @@ class auth:
|
|||
self.cur.execute("DELETE FROM keys WHERE user IS NULL")
|
||||
self.cur.execute("UPDATE users SET link_token = NULL, link_token_time = NULL WHERE link_token_time + ? - strftime('%s') <= 0", (self.LINK_EXPIRE, ))
|
||||
self.con.commit()
|
||||
|
||||
|
||||
def passKey(self, hash):
|
||||
"""
|
||||
pass given key to the object
|
||||
|
@ -71,46 +79,97 @@ class auth:
|
|||
if (not key):
|
||||
self.registerKey()
|
||||
return # if key is just registered, there is not username yet
|
||||
|
||||
|
||||
self.username = key['username']
|
||||
self.updateKey()
|
||||
# we do not need to update cache right now
|
||||
|
||||
|
||||
def fetchKey(self):
|
||||
"""
|
||||
get current key and username
|
||||
"""
|
||||
if (not self.hash):
|
||||
return None
|
||||
|
||||
|
||||
res = self.cur.execute("SELECT keys.*, users.name as username FROM users, keys WHERE users.id = keys.user AND keys.hash = ?", (self.hash, ))
|
||||
return res.fetchone()
|
||||
|
||||
|
||||
def getKeys(self):
|
||||
"""
|
||||
get all keys of current user
|
||||
"""
|
||||
if (not self.username):
|
||||
return None
|
||||
|
||||
|
||||
res = self.cur.execute("SELECT * FROM users, keys WHERE users.id = keys.user AND users.name = ?", (self.username, ))
|
||||
return res.fetchall()
|
||||
|
||||
def userInfo(self, key=None):
|
||||
|
||||
def userInfo(self, key):
|
||||
"""
|
||||
get user row from database/cache
|
||||
"""
|
||||
if (not self.username):
|
||||
return None
|
||||
|
||||
if (not self.user['outdated']):
|
||||
return self.user if not key else self.user[key]
|
||||
|
||||
|
||||
if (DEBUG):
|
||||
logging.warning({"user": self.user, "userOutdated": self.userOutdated, "key": key})
|
||||
|
||||
if (key in self.user and key not in self.userOutdated):
|
||||
return self.user[key]
|
||||
|
||||
res = self.cur.execute("SELECT * FROM users WHERE users.name = ?", (self.username, ))
|
||||
self.user = res.fetchone()
|
||||
self.user['outdated'] = False
|
||||
return self.user if not key else self.user[key]
|
||||
self.userOutdated.clear()
|
||||
return self.user[key]
|
||||
|
||||
def updateUserInfo(self, key, value):
|
||||
"""
|
||||
handy function for quick update self.user and self.userOutdated
|
||||
for outdating it's enough to self.userOutdated.append(key)
|
||||
"""
|
||||
self.user[key] = value
|
||||
while (key in self.userOutdated): # remove duplicates
|
||||
self.userOutdated.remove(key)
|
||||
|
||||
def genAnticSRF(self):
|
||||
"""
|
||||
generate antic cross-site request forgery token
|
||||
There's one token per session.
|
||||
"""
|
||||
if (not self.username):
|
||||
return None
|
||||
|
||||
# skip generating token if already generated for this session
|
||||
if (self.anticsrf):
|
||||
return self.userInfo('anticsrf')
|
||||
|
||||
token = generateToken(4)
|
||||
self.cur.execute("UPDATE users SET anticsrf = ?, anticsrf_time = strftime('%s') WHERE name = ?", (token, self.username))
|
||||
self.con.commit()
|
||||
self.anticsrf = True
|
||||
self.updateUserInfo('anticsrf', token)
|
||||
self.userOutdated.append('anticsrf_time')
|
||||
|
||||
return token
|
||||
|
||||
def checkAnticSRF(self, token):
|
||||
"""
|
||||
check antic cross-site request forgery token validity
|
||||
Remider: there's one token per session
|
||||
TODO: anticsrf expiration
|
||||
"""
|
||||
if (not self.username):
|
||||
return None
|
||||
|
||||
validity = token == self.userInfo('anticsrf')
|
||||
|
||||
self.cur.execute("UPDATE users SET anticsrf = NULL, anticsrf_time = NULL WHERE name = ?", (self.username, ))
|
||||
self.con.commit()
|
||||
self.updateUserInfo('anticsrf', None)
|
||||
self.updateUserInfo('anticsrf_time', None)
|
||||
|
||||
return validity
|
||||
|
||||
def registerKey(self):
|
||||
"""
|
||||
insert new key into database (will be deleted if not linked)
|
||||
|
@ -118,44 +177,41 @@ class auth:
|
|||
"""
|
||||
if (not self.hash):
|
||||
return None
|
||||
|
||||
|
||||
self.cur.execute("INSERT INTO keys (hash) VALUES (?)", (self.hash, ))
|
||||
# unlinked key is deleted anyway, so why don't wait with commit until linking and avoid unneccessary disk IO?
|
||||
|
||||
|
||||
def updateKey(self):
|
||||
"""
|
||||
touch current key timestamp
|
||||
"""
|
||||
if (not self.hash):
|
||||
return None
|
||||
|
||||
|
||||
self.cur.execute("UPDATE keys SET last_seen = strftime('%s') WHERE hash = ?", (self.hash, ))
|
||||
self.con.commit()
|
||||
|
||||
SUCCESS = 0
|
||||
NAME_IN_USE = 1
|
||||
ACTION_DISABLED = 2
|
||||
BAD_TOKEN = 1
|
||||
BAD_TOKEN = 3
|
||||
KEY_IN_USE = 4
|
||||
NOT_FOUND = 5
|
||||
|
||||
def registerUser(self, username):
|
||||
"""
|
||||
link new user to the current key
|
||||
response codes:
|
||||
0 - registered successfully
|
||||
1 - name in use
|
||||
2 - registration disabled
|
||||
3 - (future) captcha challenge
|
||||
"""
|
||||
if (not self.hash):
|
||||
return None
|
||||
|
||||
|
||||
if (not self.ENABLE_REGISTRATION):
|
||||
return self.ACTION_DISABLED
|
||||
|
||||
|
||||
res = self.cur.execute("SELECT * FROM users WHERE name = ?", (username, ))
|
||||
if (res.fetchone()):
|
||||
return self.NAME_IN_USE
|
||||
|
||||
|
||||
self.cur.execute("INSERT INTO users (name) VALUES (?)", (username, ))
|
||||
uid = self.cur.lastrowid
|
||||
self.cur.execute("UPDATE keys SET user = ? WHERE hash = ?", (uid, self.hash))
|
||||
|
@ -164,22 +220,19 @@ class auth:
|
|||
self.username = username
|
||||
|
||||
return self.SUCCESS
|
||||
|
||||
|
||||
def requestLink(self, cancel=False):
|
||||
"""
|
||||
generate link token
|
||||
"""
|
||||
if (not self.username):
|
||||
return None
|
||||
|
||||
|
||||
if (cancel):
|
||||
# self.cur.execute("UPDATE users SET link_token = NULL, link_token_time = NULL WHERE name = ?", (self.username, ))
|
||||
self.burnLink(self.username)
|
||||
self.con.commit()
|
||||
if (self.user):
|
||||
self.user['link_token'] = self.user['link_token_time'] = None
|
||||
return True
|
||||
|
||||
|
||||
trials = 3
|
||||
token = None
|
||||
|
||||
|
@ -187,14 +240,15 @@ class auth:
|
|||
token = generateToken(16)
|
||||
res = self.cur.execute("SELECT * FROM users WHERE link_token = ?", (token, ))
|
||||
if (res.fetchone()):
|
||||
tokenSet = None
|
||||
token = None
|
||||
trials -= 1
|
||||
if (trials < 0):
|
||||
return False
|
||||
|
||||
|
||||
self.cur.execute("UPDATE users SET link_token = ?, link_token_time = strftime('%s') WHERE name = ?", (token, self.username))
|
||||
self.con.commit()
|
||||
self.user['outdated'] = True
|
||||
self.updateUserInfo('link_token', token)
|
||||
self.userOutdated.append('link_token_time')
|
||||
|
||||
return token
|
||||
|
||||
|
@ -203,6 +257,8 @@ class auth:
|
|||
force link token expiration
|
||||
"""
|
||||
self.cur.execute("UPDATE users SET link_token = NULL, link_token_time = NULL WHERE name = ?", (username, ))
|
||||
self.updateUserInfo('link_token', None)
|
||||
self.updateUserInfo('link_token_time', None)
|
||||
|
||||
def link(self, token):
|
||||
"""
|
||||
|
@ -210,7 +266,7 @@ class auth:
|
|||
"""
|
||||
if (not self.hash):
|
||||
return None
|
||||
|
||||
|
||||
res = self.cur.execute("SELECT id, name FROM users WHERE link_token = ?", (token, ))
|
||||
res = res.fetchone()
|
||||
if (res):
|
||||
|
@ -218,9 +274,28 @@ class auth:
|
|||
self.burnLink(res['name'])
|
||||
self.con.commit()
|
||||
self.username = res['name']
|
||||
return auth.SUCCESS
|
||||
return self.SUCCESS
|
||||
else:
|
||||
return auth.BAD_TOKEN
|
||||
return self.BAD_TOKEN
|
||||
|
||||
def unlink(self, hash):
|
||||
"""
|
||||
unlink given key from current user (since there is no such thing, the key is just deleted)
|
||||
"""
|
||||
if (not self.username or not self.hash):
|
||||
return None
|
||||
|
||||
if (hash == self.hash):
|
||||
return self.KEY_IN_USE
|
||||
|
||||
myKeys = self.getKeys()
|
||||
for key in myKeys:
|
||||
if (key['hash'] == hash):
|
||||
self.cur.execute("DELETE FROM keys WHERE hash = ?", (hash, ))
|
||||
self.con.commit()
|
||||
return self.SUCCESS
|
||||
|
||||
return self.NOT_FOUND
|
||||
|
||||
|
||||
if (__name__ == '__main__'):
|
||||
|
|
|
@ -34,27 +34,24 @@ else:
|
|||
current = hash == auth.hash
|
||||
|
||||
name = key['name']
|
||||
if (name):
|
||||
name = '"' + name + '"'
|
||||
else:
|
||||
name = '[no name]'
|
||||
|
||||
txt = '* ' + name
|
||||
name = '"' + name + '"' if name else '[no name]'
|
||||
|
||||
label = '* ' + name
|
||||
if (current):
|
||||
txt += ' (currently used)'
|
||||
print(txt)
|
||||
|
||||
label += ' (currently used)'
|
||||
print(label)
|
||||
|
||||
print('hash:', hash)
|
||||
print('last seen:', lastSeen)
|
||||
if (not current):
|
||||
print('=> unlink.gmi?{} unlink'.format(auth.genAnticSRF() + hash))
|
||||
|
||||
linkToken = auth.userInfo('link_token')
|
||||
if(linkToken):
|
||||
from datetime import datetime
|
||||
linkTokenExpire = datetime.fromtimestamp(auth.userInfo('link_token_time') + auth.LINK_EXPIRE)
|
||||
delta = linkTokenExpire - datetime.now()
|
||||
seconds = delta.seconds
|
||||
minutes = seconds // 60
|
||||
seconds = seconds % 60
|
||||
expire = datetime.fromtimestamp(auth.userInfo('link_token_time') + auth.LINK_EXPIRE)
|
||||
delta = expire - datetime.now()
|
||||
minutes = delta.seconds // 60
|
||||
seconds = delta.seconds % 60
|
||||
# TODO: calibration - it displays 00:00 for one second
|
||||
def zero(n):
|
||||
return str(n) if n>10 else '0'+str(n)
|
||||
|
@ -62,4 +59,4 @@ else:
|
|||
print('Token {} will expire in {}:{}'.format(linkToken, zero(minutes), zero(seconds)))
|
||||
print('=> link.gmi?cancel cancel')
|
||||
else:
|
||||
print('=> link.gmi Link new key')
|
||||
print('=> link.gmi link new key')
|
|
@ -22,9 +22,13 @@ if (auth.username):
|
|||
if (not query):
|
||||
# empty
|
||||
from datetime import datetime
|
||||
print('20 text/gemini\r\n')
|
||||
|
||||
token = auth.requestLink()
|
||||
if (not token):
|
||||
print('40 Unknown error\r\n')
|
||||
exit()
|
||||
|
||||
print('20 text/gemini\r\n')
|
||||
print('Here\'s your token:', token)
|
||||
print('Open this page on new device and enter this code.')
|
||||
print('This token will expire in', int(auth.LINK_EXPIRE/60), 'minutes')
|
||||
|
@ -37,7 +41,7 @@ if (auth.username):
|
|||
print('20 text/gemini\r\n')
|
||||
print('Tip: open this link on new device in order to link new key to your account')
|
||||
else:
|
||||
print('50 What are you trying to do?\r\n')
|
||||
print('59 What are you trying to do?\r\n')
|
||||
else:
|
||||
# mismatch
|
||||
if (not query):
|
||||
|
@ -52,6 +56,6 @@ else:
|
|||
print('=> index.gmi back to home')
|
||||
elif (res == auth.BAD_TOKEN):
|
||||
print('20 text/gemini\r\n')
|
||||
print('It seems you entered invalid or expired token. Try to generate a new one.')
|
||||
print('It seems have you entered invalid or expired token. Try to generate a new one.')
|
||||
else:
|
||||
print('40 Unknown error\r\n')
|
|
@ -25,7 +25,7 @@ else:
|
|||
if (not auth.ENABLE_REGISTRATION):
|
||||
print('50 Registration is disabled\r\n')
|
||||
exit()
|
||||
|
||||
|
||||
username = os.environ.get('QUERY_STRING')
|
||||
if (not username):
|
||||
# empty
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
#!/usr/bin/python3
|
||||
import os
|
||||
import sys
|
||||
|
||||
os.chdir('..')
|
||||
sys.path.append('lib')
|
||||
|
||||
hash = os.environ.get('TLS_CLIENT_HASH')
|
||||
if (not hash):
|
||||
# no CC
|
||||
print('60 Authentication is required\r\n')
|
||||
exit()
|
||||
|
||||
from auth import auth
|
||||
auth = auth('data/data.db')
|
||||
auth.passKey(hash)
|
||||
|
||||
if (not auth.username):
|
||||
# mismatch
|
||||
print('61 Unknown key\r\n')
|
||||
else:
|
||||
# match
|
||||
query = os.environ.get('QUERY_STRING')
|
||||
|
||||
if (not query):
|
||||
# empty
|
||||
print('30 index.gmi\r\n')
|
||||
else:
|
||||
anticsrf = query[:4]
|
||||
hash = query[4:]
|
||||
|
||||
if (not anticsrf or not hash):
|
||||
print('59 What are you trying to do?\r\n')
|
||||
exit()
|
||||
|
||||
# anticsrf+hash
|
||||
if (auth.checkAnticSRF(anticsrf)):
|
||||
res = auth.unlink(hash)
|
||||
if (res == auth.SUCCESS):
|
||||
print('30 index.gmi\r\n')
|
||||
elif (res == auth.KEY_IN_USE):
|
||||
print('20 text/gemini\r\n')
|
||||
print('You have requested to delete the key, which is being used by you RIGHT NOW.')
|
||||
print('This could lead to the loss of your account access. If you want to proceed, authenticate with another key and try again.')
|
||||
print('=> index.gmi back to home')
|
||||
elif (res == auth.NOT_FOUND):
|
||||
print('20 text/gemini\r\n')
|
||||
print('Failed to delete non-existing key, or key which does not belong to you.')
|
||||
print('Maybe you\'re trying to delete already deleted key?')
|
||||
print('=> index.gmi back to home')
|
||||
else:
|
||||
print('40 Unknown error\r\n')
|
||||
else:
|
||||
print('50 Bad Antic SRF (security)\r\n')
|
|
@ -20,7 +20,7 @@ auth.passKey(hash)
|
|||
print('Your hash:', auth.hash)
|
||||
if (auth.username):
|
||||
print('Your username:', auth.username)
|
||||
print('=> account/index.gmi Manage your account')
|
||||
print('=> account/index.gmi manage your account')
|
||||
else:
|
||||
print('=> account/register.gmi register')
|
||||
print('=> link.gmi link existing account')
|
||||
print('=> account/link.gmi link existing account')
|
Reference in New Issue