1
0
mirror of http://git.whoc.org.uk/git/password-manager.git synced 2024-11-14 18:49:03 +01:00

Add docstrings to flask backend

This commit is contained in:
jokajak 2015-08-02 14:01:23 -04:00
parent 3a1188a779
commit a141d2e887
3 changed files with 128 additions and 31 deletions

View File

@ -1,3 +1,4 @@
"""Clipperz API handler."""
import json
import random
import hashlib
@ -12,27 +13,36 @@ from .exceptions import InvalidUsage
from .models import User, Record, RecordVersion, OneTimePassword
#==============================================================================
# ==============================================================================
# Helpers
#==============================================================================
# ==============================================================================
def randomSeed():
"""Generate a random seed."""
return hex(random.getrandbits(32*8))[2:-1]
def clipperzHash(aString):
"""Calculate a clipperz hash.
sha256(sha256(aString))
"""
firstRound = hashlib.sha256()
firstRound.update(aString)
result = hashlib.sha256()
result.update(firstRound.digest())
return result.hexdigest()
#==============================================================================
# ==============================================================================
# Method handlers
#==============================================================================
# ==============================================================================
class HandlerMixin:
"""Mixin for handling requests."""
def handle_request(self, request):
"""Default method to handle a request."""
parameters = json.loads(request.form['parameters'])
app.logger.debug('raw parameters: %s', parameters)
parameters = parameters['parameters']
@ -50,7 +60,14 @@ class HandlerMixin:
class registration(HandlerMixin):
"""Registration handler."""
def completeRegistration(self, parameters, request):
"""Complete a registration.
Create a new user.
"""
credentials = parameters['credentials']
data = parameters['user']
user = User()
@ -63,11 +80,21 @@ class registration(HandlerMixin):
class handshake(HandlerMixin):
"""Handshake handler.
This handles the logon process.
"""
srp_n = '115b8b692e0e045692cf280b436735c77a5a9e8a9e7ed56c965f87db5b2a2ece3'
srp_g = 2
srp_n = long(srp_n, 16)
def connect(self, parameters, request):
"""Process a connect request.
Attempt to log in by processing the parameters.
"""
result = {}
session['C'] = parameters['parameters']['C']
session['A'] = parameters['parameters']['A']
@ -127,8 +154,12 @@ class handshake(HandlerMixin):
return jsonify({'result': result})
def credentialCheck(self, parameters, request):
"""Check credentials.
Handles the SRP process.
"""
country = 'US'
# hard-coded for development
# hard-coded for development/personal use.
result = {
'accountInfo': {
'features': [
@ -203,14 +234,15 @@ class handshake(HandlerMixin):
return jsonify({'result': result})
def oneTimePassword(self, parameters, request):
#"parameters": {
#"message": "oneTimePassword",
#"version": "0.2",
#"parameters": {
"""Handle one time password logins."""
# "parameters": {
# "message": "oneTimePassword",
# "version": "0.2",
# "parameters": {
# "oneTimePasswordKey": "03bd882...396082c",
# "oneTimePasswordKeyChecksum": "f73f629...041031d"
#}
#}
# }
# }
result = {}
try:
@ -237,8 +269,12 @@ class handshake(HandlerMixin):
class message(HandlerMixin):
"""Handle messages once logged in."""
@login_required
def getUserDetails(self, parameters, request):
"""Get a user's details."""
app.logger.debug(parameters)
if 'srpSharedSecret' not in parameters:
raise InvalidUsage(
@ -252,7 +288,7 @@ class message(HandlerMixin):
# Online results
# {"result":
# {
# "header": "{\"records\":{\"index\":{\"383036...eeefbe48\":\"0\"},\"data\":\"zrhb3/791SDdb48v3vXfPzeDrv0Jhs4rAaOKHx+jDF6pwm/qi9DGSR0JwrprOgwv3bjYJgU2xHA8cuA0bPvABHSHK6fnGwvhSlyYjskY2Cy/WbRJhcA4kw+VUsOjZPRxtM8bSJnSxViAXsghTcya6+5M3MdMJHE=\"},\"directLogins\":{\"index\":{},\"data\":\"s7KYzHwKISmjYufv9h0mpTiM\"},\"preferences\":{\"data\":\"mf8fWjpOQjlV18ukEO9FN3LP\"},\"oneTimePasswords\":{\"data\":\"8tV1yRHv30lsl3FadG9YnTOo\"},\"version\":\"0.1\"}",
# "header": "{\"records\":{\"index\":{\"383036...eeefbe48\":\"0\"},\"data\":\"zrhb3/791SDdb48v3vXfPzeDrv0Jhs4rAaOKHx+jDF6pwm/qi9DGSR0JwrprOgwv3bjYJgU2xHA8cuA0bPvABHSHK6fnGwvhSlyYjskY2Cy/WbRJhcA4kw+VUsOjZPRxtM8bSJnSxViAXsghTcya6+5M3MdMJHE=\"},\"directLogins\":{\"index\":{},\"data\":\"s7KYzHwKISmjYufv9h0mpTiM\"},\"preferences\":{\"data\":\"mf8fWjpOQjlV18ukEO9FN3LP\"},\"oneTimePasswords\":{\"data\":\"8tV1yRHv30lsl3FadG9YnTOo\"},\"version\":\"0.1\"}", # NOQA
# "lock": "3D4B4501-D7A9-6E4F-A487-9428C0B6E79D",
# "version": "0.4",
# "recordsStats": {
@ -266,8 +302,8 @@ class message(HandlerMixin):
# }
# }
# Dev results
#{"result":
# {"header": "{\"records\":{\"index\":{\"843a95d8...5f734b\":\"1\"},\"data\":\"fKgc5Jt9JH/CibCIpcRmwyLuLIvufWchNJga7GoFcWT9K8LR+ai0BvzWBUxcPccivE9zPv2Swe5E8wPEIc+Lv0U73NobJEct7WqBcCdLxszBE1SokxPEZDUVdWVQtAiwgOS219inCFmI5CaB\"},\"directLogins\":{\"index\":{},\"data\":\"rnMQBB81ezh6JKNGXkDCyY+q\"},\"preferences\":{\"data\":\"9jzR9Goo5PGpXbAdmsXHuQGp\"},\"oneTimePasswords\":{\"data\":\"iXEUuQGskZhMyHEwU+3tRGQM\"},\"version\":\"0.1\"}",
# {"result":
# {"header": "{\"records\":{\"index\":{\"843a95d8...5f734b\":\"1\"},\"data\":\"fKgc5Jt9JH/CibCIpcRmwyLuLIvufWchNJga7GoFcWT9K8LR+ai0BvzWBUxcPccivE9zPv2Swe5E8wPEIc+Lv0U73NobJEct7WqBcCdLxszBE1SokxPEZDUVdWVQtAiwgOS219inCFmI5CaB\"},\"directLogins\":{\"index\":{},\"data\":\"rnMQBB81ezh6JKNGXkDCyY+q\"},\"preferences\":{\"data\":\"9jzR9Goo5PGpXbAdmsXHuQGp\"},\"oneTimePasswords\":{\"data\":\"iXEUuQGskZhMyHEwU+3tRGQM\"},\"version\":\"0.1\"}", # NOQA
# "recordStats": {
# "843a95d8...5f734b": {
# "updateDate": "Sun, 12 Apr 2015 13:08:44 GMT"
@ -295,6 +331,7 @@ class message(HandlerMixin):
@login_required
def saveChanges(self, parameters, request):
"""Save changes to a user's settings."""
result = {}
parameters = parameters['parameters']
if ('user' not in parameters
@ -340,7 +377,8 @@ class message(HandlerMixin):
@login_required
def getRecordDetail(self, parameters, request):
#{
"""Get details about a record."""
# {
# "parameters": {
# "srpSharedSecret": "bf79ad3cf0c1...63462a9fb560",
# "message": "getRecordDetail",
@ -348,7 +386,7 @@ class message(HandlerMixin):
# "reference": "e3a5856...20e080fc97f13c14c"
# }
# }
#}
# }
app.logger.debug(parameters)
if 'srpSharedSecret' not in parameters:
raise InvalidUsage(
@ -404,13 +442,14 @@ class message(HandlerMixin):
@login_required
def getOneTimePasswordsDetails(self, parameters, request):
#{
"""Get details about a one time password."""
# {
# "parameters": {
# "srpSharedSecret": "bf79ad3cf0c1...63462a9fb560",
# "message": "getOneTimePasswordsDetails",
# "parameters": {}
# }
#}
# }
if 'srpSharedSecret' not in parameters:
raise InvalidUsage(
'Mal-formed message format.',
@ -425,20 +464,24 @@ class message(HandlerMixin):
otps = OneTimePassword().query.filter_by(user=current_user).all()
for otp in otps:
#{"e8541...af0c6b951":{"status":"ACTIVE"}}
# {"e8541...af0c6b951":{"status":"ACTIVE"}}
result[otp.reference] = {'status': otp.status}
return jsonify({'result': result})
@login_required
def getLoginHistory(self, parameters, request):
#{
"""Get login history.
Not currently fully implemented.
"""
# {
# "parameters": {
# "srpSharedSecret": "bf79ad3cf0c1...63462a9fb560",
# "message": "getOneTimePasswordsDetails",
# "parameters": {}
# }
#}
# }
if 'srpSharedSecret' not in parameters:
raise InvalidUsage(
'Mal-formed message format.',
@ -462,6 +505,7 @@ class message(HandlerMixin):
@login_required
def addNewOneTimePassword(self, parameters, request):
"""Add a new one time password."""
# "parameters": {
# "message": "addNewOneTimePassword",
# "srpSharedSecret": "1e8e037a8...85680f931d45dfc20472cf9d1",
@ -481,7 +525,7 @@ class message(HandlerMixin):
# }
# }
# }
#}
# }
if 'srpSharedSecret' not in parameters:
raise InvalidUsage(
'Mal-formed message format.',
@ -524,6 +568,7 @@ class message(HandlerMixin):
return jsonify({'result': result})
def echo(self, parameters, request):
"""Check the status of the session."""
result = {}
if 'srpSharedSecret' not in parameters:
raise InvalidUsage(
@ -549,6 +594,7 @@ class message(HandlerMixin):
@login_required
def deleteUser(self, parameters, request):
"""Delete a user and all of its records."""
result = {}
if 'srpSharedSecret' not in parameters:
raise InvalidUsage(
@ -573,7 +619,8 @@ class message(HandlerMixin):
@login_required
def upgradeUserCredentials(self, parameters, request):
#{"parameters":{"message":"upgradeUserCredentials","srpSharedSecret":"36...d6","parameters":{"credentials":{"C":"59d02038fdb47cee5b7837a697bc8ff41cc66d8844c8fce844cdf45b0b08b1e4","s":"fe40513b99fbaca9bfe51b8d6e9b3eb42b1e01ce8b0ae32461bec0294c1030ed","v":"300b92f4a3e34034d78cd5081f8db36dbf2a4c5f7a41db6954518815a3554278","version":"0.2"},"user":{"header":"{\"records\":{\"index\":{},\"data\":\"VIIDc5vFNoIflyXF8syb8fRS\"},\"directLogins\":{\"index\":{},\"data\":\"9elg3tu2UqsJ0zbUAdQkLE69\"},\"preferences\":{\"data\":\"Sbwar35Ynd/XobuAm4K66lqj\"},\"oneTimePasswords\":{\"data\":\"tAcTsWVTwALSfxXvCChHi4FD\"},\"version\":\"0.1\"}","statistics":"","version":"0.4","lock":null}}}}
"""Upgrade a user's credentials to a new password."""
# {"parameters":{"message":"upgradeUserCredentials","srpSharedSecret":"36...d6","parameters":{"credentials":{"C":"59d02038fdb47cee5b7837a697bc8ff41cc66d8844c8fce844cdf45b0b08b1e4","s":"fe40513b99fbaca9bfe51b8d6e9b3eb42b1e01ce8b0ae32461bec0294c1030ed","v":"300b92f4a3e34034d78cd5081f8db36dbf2a4c5f7a41db6954518815a3554278","version":"0.2"},"user":{"header":"{\"records\":{\"index\":{},\"data\":\"VIIDc5vFNoIflyXF8syb8fRS\"},\"directLogins\":{\"index\":{},\"data\":\"9elg3tu2UqsJ0zbUAdQkLE69\"},\"preferences\":{\"data\":\"Sbwar35Ynd/XobuAm4K66lqj\"},\"oneTimePasswords\":{\"data\":\"tAcTsWVTwALSfxXvCChHi4FD\"},\"version\":\"0.1\"}","statistics":"","version":"0.4","lock":null}}}} # NOQA
result = {}
if 'srpSharedSecret' not in parameters:
raise InvalidUsage(
@ -614,7 +661,11 @@ class message(HandlerMixin):
class logout(HandlerMixin):
"""Logout handler."""
def handle_request(self, request):
"""Handle a logout request."""
result = {}
logout_user()
session.clear()

View File

@ -1,3 +1,4 @@
"""Clipperz models."""
import datetime
from flask.ext.login import UserMixin
@ -6,6 +7,9 @@ from clipperz import app, db
class User(db.Model, UserMixin):
"""Clipperz User model."""
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(), unique=True, index=True)
srp_s = db.Column(db.String(128))
@ -29,12 +33,14 @@ class User(db.Model, UserMixin):
update_date = db.Column(db.DateTime(), nullable=True)
def updateCredentials(self, credentials):
"""Update user credentials."""
self.username = credentials['C']
self.srp_s = credentials['s']
self.srp_v = credentials['v']
self.auth_version = credentials['version']
def update(self, data):
"""Update user object."""
self.header = data['header']
self.statistics = data['statistics']
self.version = data['version']
@ -44,12 +50,21 @@ class User(db.Model, UserMixin):
self.offline_saved = False
def __repr__(self):
"""User representation."""
return '<User %r>' % (self.username)
#------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
class RecordVersion(db.Model):
"""
Model a RecordVersion.
RecordVersion store attributes associated with a specific version of a
record.
"""
id = db.Column(db.Integer(), primary_key=True)
reference = db.Column(db.String(), unique=True, index=True)
header = db.Column(db.Text())
@ -77,6 +92,7 @@ class RecordVersion(db.Model):
self.creation_date = datetime.datetime.utcnow()
def update(self, someData):
"""Update a record version."""
app.logger.debug(someData)
recordVersionData = someData['currentRecordVersion']
self.reference = recordVersionData['reference']
@ -87,10 +103,16 @@ class RecordVersion(db.Model):
self.update_date = datetime.datetime.utcnow()
self.record.update(someData['record'], self)
#------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
class Record(db.Model):
"""Model a record.
A Record has multiple record versions.
"""
id = db.Column(db.Integer(), primary_key=True)
user_id = db.Column(db.ForeignKey('user.id'))
reference = db.Column(db.String(), unique=True, index=True)
@ -111,6 +133,7 @@ class Record(db.Model):
self.creation_date = datetime.datetime.utcnow()
def update(self, data, record_version):
"""Update a record."""
self.reference = data['reference']
self.data = data['data']
self.api_version = data['version']
@ -121,10 +144,16 @@ class Record(db.Model):
else:
self.version = 1
#------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
class OneTimePassword(db.Model):
"""Model a OneTimePassword.
OneTimePasswords are used to log in to clipperz only once.
"""
id = db.Column(db.Integer(), primary_key=True)
user_id = db.Column(db.ForeignKey('user.id'))
status = db.Column(db.String())
@ -142,6 +171,7 @@ class OneTimePassword(db.Model):
self.creation_date = datetime.datetime.utcnow()
def update(self, someParameters, aStatus):
"""Update a one time password."""
self.reference = someParameters['reference']
self.key_value = someParameters['key']
self.key_checksum = someParameters['keyChecksum']
@ -150,14 +180,18 @@ class OneTimePassword(db.Model):
self.status = aStatus
def reset(self, aStatus):
"""Reset a one time password."""
self.data = ""
self.status = aStatus
return self
#------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
class Session(db.Model):
"""Model a session."""
id = db.Column(db.Integer(), primary_key=True)
sessionId = db.Column(db.String())
access_date = db.Column(db.DateTime())

View File

@ -1,23 +1,30 @@
from flask import session, request, g, jsonify
"""Clipperz views."""
from flask import session, request, g
from clipperz import app, db, lm
from .models import User
from .api import *
from .api import * # NOQA
from .exceptions import InvalidUsage
from flask.ext.login import login_required
@lm.user_loader
def load_user(id):
"""Load a user.
Converts a user id in to a User object.
"""
return User.query.get(int(id))
@app.before_request
def before_request():
"""Store the current user."""
g.user = current_user
@app.teardown_appcontext
def shutdown_session(exception=None):
"""Remove the session from the database."""
db.session.remove()
@ -26,6 +33,7 @@ def shutdown_session(exception=None):
@app.route('/delta/dump/<string:frontend_version>')
@login_required
def dump(frontend_version):
"""Return JSON for a user's data."""
user = User().query.filter_by(username=session['C']).one()
if (user != g.user):
@ -104,21 +112,25 @@ def dump(frontend_version):
@app.route('/beta/<path:path>')
def beta(path):
"""Fallback for serving beta version."""
return send_from_directory('beta', path)
@app.route('/gamma/<path:path>')
def gamma(path):
"""Fallback for serving gamma version."""
return send_from_directory('gamma', path)
@app.route('/delta/<path:path>')
def delta(path):
"""Fallback for serving delta version."""
return send_from_directory('delta', path)
@app.route('/pm', methods=['GET', 'OPTIONS', 'POST'])
def pm():
"""Main request handler."""
method = request.form['method']
if method not in globals():
raise InvalidUsage('This method is not yet implemented',