├── .flake8 ├── .github └── workflows │ └── main.yml ├── .gitignore ├── .vscode ├── launch.json └── settings.json ├── LICENSE ├── README.md ├── etc ├── AddKey.png ├── AppDetail.png ├── AppView.png ├── KeyDetail.png ├── KeyView.png └── cURLExample.png ├── keyserv ├── __init__.py ├── auth.py ├── config.example.py ├── endpoints.py ├── forms.py ├── keymanager.py ├── models.py ├── static │ ├── favicon.ico │ └── icon.png ├── templates │ ├── add_modify.html │ ├── applications.html │ ├── detail_app.html │ ├── detail_key.html │ ├── index.html │ ├── keys.html │ ├── layout.html │ └── logs.html └── views.py ├── keyserver.py ├── requirements-dev.txt └── requirements.txt /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 100 3 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: CI 4 | 5 | # Controls when the action will run. 6 | on: 7 | # Triggers the workflow on push or pull request events but only for the master branch 8 | push: 9 | branches: [ beta ] 10 | pull_request: 11 | branches: [ beta ] 12 | 13 | # Allows you to run this workflow manually from the Actions tab 14 | workflow_dispatch: 15 | 16 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 17 | jobs: 18 | # This workflow contains a single job called "build" 19 | build: 20 | # The type of runner that the job will run on 21 | runs-on: ubuntu-latest 22 | 23 | # Steps represent a sequence of tasks that will be executed as part of the job 24 | steps: 25 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 26 | - uses: actions/checkout@v2 27 | 28 | - name: Setup Go environment 29 | uses: actions/setup-go@v2.1.3 30 | 31 | - name: Go Test 32 | run: go test -v -short ./... 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | 103 | # project specific 104 | keyserver/config.py 105 | 106 | # vs code 107 | .vscode/* 108 | !.vscode/settings.json 109 | !.vscode/tasks.json 110 | !.vscode/launch.json 111 | !.vscode/extensions.json 112 | 113 | keyserver.log* 114 | keyserv/config.py 115 | 116 | keyserv/keyserver.db 117 | app.yaml 118 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Flask", 9 | "type": "python", 10 | "request": "launch", 11 | "stopOnEntry": false, 12 | "pythonPath": "${config:python.pythonPath}", 13 | "program": "/Users/sam/git/mini-key-server/venv/bin/flask", 14 | "cwd": "${workspaceRoot}", 15 | "env": { 16 | "FLASK_APP": "${workspaceRoot}/keyserver.py" 17 | }, 18 | "args": [ 19 | "run", 20 | "--no-debugger", 21 | "--no-reload" 22 | ], 23 | "envFile": "${workspaceRoot}/.env", 24 | "debugOptions": [ 25 | "WaitOnAbnormalExit", 26 | "WaitOnNormalExit", 27 | "RedirectOutput" 28 | ] 29 | }, 30 | { 31 | "name": "Python", 32 | "type": "python", 33 | "request": "launch", 34 | "stopOnEntry": true, 35 | "pythonPath": "${config:python.pythonPath}", 36 | "program": "${file}", 37 | "cwd": "${workspaceRoot}", 38 | "env": {}, 39 | "envFile": "${workspaceRoot}/.env", 40 | "debugOptions": [ 41 | "WaitOnAbnormalExit", 42 | "WaitOnNormalExit", 43 | "RedirectOutput" 44 | ] 45 | }, 46 | { 47 | "name": "Python: Attach", 48 | "type": "python", 49 | "request": "attach", 50 | "localRoot": "${workspaceRoot}", 51 | "remoteRoot": "${workspaceRoot}", 52 | "port": 3000, 53 | "secret": "my_secret", 54 | "host": "localhost" 55 | }, 56 | { 57 | "name": "Python: Terminal (integrated)", 58 | "type": "python", 59 | "request": "launch", 60 | "stopOnEntry": true, 61 | "pythonPath": "${config:python.pythonPath}", 62 | "program": "${file}", 63 | "cwd": "", 64 | "console": "integratedTerminal", 65 | "env": {}, 66 | "envFile": "${workspaceRoot}/.env", 67 | "debugOptions": [ 68 | "WaitOnAbnormalExit", 69 | "WaitOnNormalExit" 70 | ] 71 | }, 72 | { 73 | "name": "Python: Terminal (external)", 74 | "type": "python", 75 | "request": "launch", 76 | "stopOnEntry": true, 77 | "pythonPath": "${config:python.pythonPath}", 78 | "program": "${file}", 79 | "cwd": "", 80 | "console": "externalTerminal", 81 | "env": {}, 82 | "envFile": "${workspaceRoot}/.env", 83 | "debugOptions": [ 84 | "WaitOnAbnormalExit", 85 | "WaitOnNormalExit" 86 | ] 87 | }, 88 | { 89 | "name": "Python: Django", 90 | "type": "python", 91 | "request": "launch", 92 | "stopOnEntry": true, 93 | "pythonPath": "${config:python.pythonPath}", 94 | "program": "${workspaceRoot}/manage.py", 95 | "cwd": "${workspaceRoot}", 96 | "args": [ 97 | "runserver", 98 | "--noreload", 99 | "--nothreading" 100 | ], 101 | "env": {}, 102 | "envFile": "${workspaceRoot}/.env", 103 | "debugOptions": [ 104 | "WaitOnAbnormalExit", 105 | "WaitOnNormalExit", 106 | "RedirectOutput", 107 | "DjangoDebugging" 108 | ] 109 | }, 110 | { 111 | "name": "Python: Flask (0.11.x or later)", 112 | "type": "python", 113 | "request": "launch", 114 | "stopOnEntry": false, 115 | "pythonPath": "${config:python.pythonPath}", 116 | "program": "fully qualified path fo 'flask' executable. Generally located along with python interpreter", 117 | "cwd": "${workspaceRoot}", 118 | "env": { 119 | "FLASK_APP": "${workspaceRoot}/quickstart/app.py" 120 | }, 121 | "args": [ 122 | "run", 123 | "--no-debugger", 124 | "--no-reload" 125 | ], 126 | "envFile": "${workspaceRoot}/.env", 127 | "debugOptions": [ 128 | "WaitOnAbnormalExit", 129 | "WaitOnNormalExit", 130 | "RedirectOutput" 131 | ] 132 | }, 133 | { 134 | "name": "Python: Flask (0.10.x or earlier)", 135 | "type": "python", 136 | "request": "launch", 137 | "stopOnEntry": false, 138 | "pythonPath": "${config:python.pythonPath}", 139 | "program": "${workspaceRoot}/run.py", 140 | "cwd": "${workspaceRoot}", 141 | "args": [], 142 | "env": {}, 143 | "envFile": "${workspaceRoot}/.env", 144 | "debugOptions": [ 145 | "WaitOnAbnormalExit", 146 | "WaitOnNormalExit", 147 | "RedirectOutput" 148 | ] 149 | }, 150 | { 151 | "name": "Python: PySpark", 152 | "type": "python", 153 | "request": "launch", 154 | "stopOnEntry": true, 155 | "osx": { 156 | "pythonPath": "${env:SPARK_HOME}/bin/spark-submit" 157 | }, 158 | "windows": { 159 | "pythonPath": "${env:SPARK_HOME}/bin/spark-submit.cmd" 160 | }, 161 | "linux": { 162 | "pythonPath": "${env:SPARK_HOME}/bin/spark-submit" 163 | }, 164 | "program": "${file}", 165 | "cwd": "${workspaceRoot}", 166 | "env": {}, 167 | "envFile": "${workspaceRoot}/.env", 168 | "debugOptions": [ 169 | "WaitOnAbnormalExit", 170 | "WaitOnNormalExit", 171 | "RedirectOutput" 172 | ] 173 | }, 174 | { 175 | "name": "Python: Module", 176 | "type": "python", 177 | "request": "launch", 178 | "stopOnEntry": true, 179 | "pythonPath": "${config:python.pythonPath}", 180 | "module": "module.name", 181 | "cwd": "${workspaceRoot}", 182 | "env": {}, 183 | "envFile": "${workspaceRoot}/.env", 184 | "debugOptions": [ 185 | "WaitOnAbnormalExit", 186 | "WaitOnNormalExit", 187 | "RedirectOutput" 188 | ] 189 | }, 190 | { 191 | "name": "Python: Pyramid", 192 | "type": "python", 193 | "request": "launch", 194 | "stopOnEntry": true, 195 | "pythonPath": "${config:python.pythonPath}", 196 | "cwd": "${workspaceRoot}", 197 | "env": {}, 198 | "envFile": "${workspaceRoot}/.env", 199 | "args": [ 200 | "${workspaceRoot}/development.ini" 201 | ], 202 | "debugOptions": [ 203 | "WaitOnAbnormalExit", 204 | "WaitOnNormalExit", 205 | "RedirectOutput", 206 | "Pyramid" 207 | ] 208 | }, 209 | { 210 | "name": "Python: Watson", 211 | "type": "python", 212 | "request": "launch", 213 | "stopOnEntry": true, 214 | "pythonPath": "${config:python.pythonPath}", 215 | "program": "${workspaceRoot}/console.py", 216 | "cwd": "${workspaceRoot}", 217 | "args": [ 218 | "dev", 219 | "runserver", 220 | "--noreload=True" 221 | ], 222 | "env": {}, 223 | "envFile": "${workspaceRoot}/.env", 224 | "debugOptions": [ 225 | "WaitOnAbnormalExit", 226 | "WaitOnNormalExit", 227 | "RedirectOutput" 228 | ] 229 | } 230 | ] 231 | } 232 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.pythonPath": "${workspaceFolder}/venv/bin/python3", 3 | "python.linting.enabled": true, 4 | "python.linting.pylintEnabled": false, 5 | "python.linting.flake8Enabled": true, 6 | "python.linting.mypyEnabled": true, 7 | "python.linting.pydocstyleEnabled": false, 8 | "editor.formatOnSave": false 9 | } 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Samuel Hoffman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mini-key-server 2 | 3 | > :star: This repo has not been maintained since 2019 but will being receiving updates again soon. :star: 4 | 5 | This web application provides a restful API for your desktop and other applications licensing needs. 6 | 7 | ### TODO 8 | 9 | - [ ] Backend re-write in Go 10 | - [ ] Frontend re-write with VueJS 11 | 12 | ### Key View 13 | 14 | ![key view](etc/KeyView.png) 15 | ![key detail](etc/KeyDetail.png) 16 | ![add key](etc/AddKey.png) 17 | 18 | ### Application View 19 | 20 | ![app view](etc/AppView.png) 21 | ![app detail](etc/AppDetail.png) 22 | 23 | ### API Example 24 | 25 | ![cURL](etc/cURLExample.png) 26 | 27 | ## Requirements 28 | 29 | Aside from the python module requirements listed in [requirements.txt](requirements.txt), the following is required: 30 | * Python 3.6 or later. 31 | * PostgreSQL (or other SQLAlchemy supported backend) 32 | 33 | 34 | ## Installation 35 | 36 | This software should be used from a [viritualenv](https://virtualenv.pypa.io/en/stable/) 37 | environment. 38 | 39 | ```sh 40 | virtualenv venv 41 | source venv/bin/activate 42 | pip3 install -U -r requirements.txt 43 | ``` 44 | 45 | Then edit the config: 46 | 47 | ```sh 48 | mv keyserv/config.example.py keyserv/config.py 49 | ``` 50 | 51 | Make sure you set `SECRET_KEY` to a randomly generated value, then change `SQLALCHEMY_DATABASE_URI` 52 | to the URI for the database you create below. 53 | 54 | ## Database Setup 55 | 56 | The following commands will create a suitable database for the keyserver to use. 57 | 58 | ```sh 59 | su - postgres 60 | createuser keyserver 61 | createdb -O keyserver keyserver 62 | ``` 63 | 64 | ## User Setup 65 | 66 | This creates a user and password on the command line. Currently there's no user creation available 67 | in the user interface. 68 | 69 | ```sh 70 | export FLASK_APP=keyserver.py 71 | flask create-user username password 72 | ``` 73 | 74 | ## Key Creation & Usage 75 | 76 | 1. Create an Application at the `/add/app` URL. 77 | 2. Create a Key at the `/add/key` URL. Activations set to `-1` means unlimited activations 78 | 79 | ### API Endpoints 80 | 81 | #### `/api/check` GET 82 | 83 | Used to check if a key is valid. Your application should exit if the response code is not `201`. 84 | A response of `404` means the key does not exist. This endpoint only accepts the GET method. 85 | 86 | 404 response: 87 | ```json 88 | {"result": "failure", "error": "invalid key"} 89 | ``` 90 | 91 | 201 OK response: 92 | ```json 93 | {"result": "ok"} 94 | ``` 95 | 96 | Arguments: 97 | - `token` - The token of the key to check for 98 | - `app_id` - Required ID of the application attempting to activate. An app-specific support message 99 | will be included in the response body if the response failed. 100 | - `machine` - The NetBIOS or domain name of the machine 101 | - `user` - The name of the currently logged in user 102 | - `hwid` - The same `hwid` provided during /api/activate (see below) 103 | 104 | #### `/api/activate` POST 105 | 106 | Used to activate the application. If successful, the number of remaining activations will decrement 107 | by one. After activation, your application should store the token in an obscure location and use the 108 | `/api/check` endpoint each time it starts up. This endpoint only supports the POST method. 109 | 110 | 404 Invalid Key response: 111 | ```json 112 | {"result": "failure", "error": "invalid activation token", "support_message": "call 555-555-5555 for support or email support@example.com"} 113 | ``` 114 | 115 | 410 Out of Activations response: 116 | ```json 117 | {"result": "failure", "error": "key is out of activations", "support_message": "visit https://example.com/ for support"} 118 | ``` 119 | 120 | 201 Activation Successful response: 121 | ```json 122 | {"result": "ok", "remainingActivations": 1} 123 | ``` 124 | The number of remaining activations will be returned in the JSON payload. `-1` indicates unlimited 125 | activations. 126 | 127 | Arguments: 128 | - `token` - The token of the key to check for 129 | - `app_id` - Required ID of the application attempting to activate. An app-specific support message 130 | will be included in the response body if the response failed. The ID is provided when an application is created 131 | - `machine` - The NetBIOS or domain name of the machine 132 | - `user` - The name of the currently logged in user 133 | - `hwid` - Something that identifies the machine this token is being activated on. This should not be stored on the client side but should be unique for each client and should be generated on the client machine (MAC address, etc.) 134 | 135 | Example: 136 | 137 | ```sh 138 | curl localhost:5001/api/activate -X POST -d token=2SZRHXZBNB3GUCHM375FTB8DJ -d machine=ICEBREAKER -d user=sam 139 | { 140 | "result": "ok", 141 | "remainingActivations": "9" 142 | } 143 | ``` 144 | 145 | ## Database Notice 146 | 147 | The database schema is likely to change as this software is still young. Appropriate `ALTER TABLE` queries will come with the commit message. 148 | 149 | ## Implications 150 | 151 | - Please run this software behind HTTPS, otherwise keys can be spoofed. Use [Qualys SSL Labs](https://www.ssllabs.com/) to verify. I recommend setting up HTTP Public Key Pinning - otherwise a bogus CA root can be issued to also spoof an instance of your domain. Setting up HPKP is not within the scope of this project. 152 | - Keys can be shared between machines, if disallowing this is important to you, use a different product. I am working on a way to seed activations via a mini-key-server client library. 153 | 154 | ## TODO 155 | 156 | - Client-side library (in progress) 157 | -------------------------------------------------------------------------------- /etc/AddKey.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/usrbinsam/jwt-key-server/f3a706b27b6ed840a891308cad1bc0e9e927f863/etc/AddKey.png -------------------------------------------------------------------------------- /etc/AppDetail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/usrbinsam/jwt-key-server/f3a706b27b6ed840a891308cad1bc0e9e927f863/etc/AppDetail.png -------------------------------------------------------------------------------- /etc/AppView.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/usrbinsam/jwt-key-server/f3a706b27b6ed840a891308cad1bc0e9e927f863/etc/AppView.png -------------------------------------------------------------------------------- /etc/KeyDetail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/usrbinsam/jwt-key-server/f3a706b27b6ed840a891308cad1bc0e9e927f863/etc/KeyDetail.png -------------------------------------------------------------------------------- /etc/KeyView.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/usrbinsam/jwt-key-server/f3a706b27b6ed840a891308cad1bc0e9e927f863/etc/KeyView.png -------------------------------------------------------------------------------- /etc/cURLExample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/usrbinsam/jwt-key-server/f3a706b27b6ed840a891308cad1bc0e9e927f863/etc/cURLExample.png -------------------------------------------------------------------------------- /keyserv/__init__.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | # Copyright(c) 2018 Samuel Hoffman 4 | 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files(the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and / or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | import click 24 | from flask import Flask 25 | from flask_bootstrap import Bootstrap 26 | 27 | from .auth import login_manager, add_user 28 | from .endpoints import api 29 | from .models import db, Event 30 | from .views import frontend 31 | 32 | 33 | def format_event(value): 34 | return Event(value) 35 | 36 | 37 | def format_datetime(value): 38 | if value is None: 39 | return "" 40 | 41 | try: 42 | return value.strftime("%Y-%m-%d %H:%M:%S") 43 | except ValueError: 44 | return "" 45 | 46 | 47 | def create_app(config): 48 | app = Flask(__name__) 49 | 50 | app.config.from_object(__name__) 51 | app.config.from_object("keyserv.config.{}".format(config)) 52 | app.jinja_env.filters["event"] = format_event 53 | app.jinja_env.filters["datetime"] = format_datetime 54 | 55 | Bootstrap(app) 56 | api.init_app(app) 57 | db.init_app(app) 58 | login_manager.init_app(app) 59 | 60 | app.register_blueprint(frontend) 61 | 62 | @app.cli.command("initdb") 63 | def initdb_command(): 64 | db.create_all() 65 | print("database initialized") 66 | 67 | @app.cli.command("create-user") 68 | @click.argument("username") 69 | @click.argument("password") 70 | def create_user_command(username: str, password: str): 71 | add_user(username, password.encode()) 72 | 73 | return app 74 | -------------------------------------------------------------------------------- /keyserv/auth.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | # Copyright(c) 2019 Samuel Hoffman 4 | 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files(the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and / or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | 22 | import secrets 23 | 24 | import argon2 25 | from flask_login import LoginManager, UserMixin 26 | 27 | from keyserv.models import db 28 | 29 | login_manager = LoginManager() 30 | login_manager.session_protection = "strong" 31 | 32 | 33 | def add_user(username: str, password: bytes, level=500): 34 | passwd = argon2.hash_password(password, secrets.token_bytes(None)) 35 | user = Users(username, passwd, level) 36 | db.session.add(user) 37 | db.session.commit() 38 | 39 | 40 | class Users(db.Model, UserMixin): 41 | id = db.Column(db.Integer(), primary_key=True) 42 | username = db.Column(db.String(), unique=True, nullable=False) 43 | passwd = db.Column(db.LargeBinary(), nullable=False) 44 | level = db.Column(db.Integer()) 45 | 46 | def __init__(self, username=None, passwd=None, level=0): 47 | self.username = username 48 | self.passwd = passwd 49 | self.level = level 50 | 51 | def get_id(self): 52 | return self.id 53 | 54 | def check_password(self, passwd): 55 | try: 56 | return argon2.verify_password(self.passwd, bytes(passwd, "UTF-8")) 57 | except argon2.exceptions.VerifyMismatchError: 58 | return False 59 | 60 | 61 | @login_manager.user_loader 62 | def user_loader(user_id) -> Users: 63 | return Users.query.get(user_id) 64 | -------------------------------------------------------------------------------- /keyserv/config.example.py: -------------------------------------------------------------------------------- 1 | # adjust the Config class below, then rename this file to config.py 2 | 3 | 4 | class DefaultConfig(object): 5 | # a decent way to generate a secret key is by running: python -c "import os; print(repr(os.urandom(24)))" 6 | # then pasting the output here. 7 | SECRET_KEY = __NOT_SET__ 8 | 9 | DEBUG = False 10 | TESTING = False 11 | 12 | SQLALCHEMY_TRACK_MODIFICATIONS = False 13 | SQLALCHEMY_DATABASE_URI = "sqlite:///:memory:" 14 | 15 | 16 | class ProductionConfig(DefaultConfig): 17 | 18 | SQLALCHEMY_DATABASE_URI = "postgres://localhost/keyserver" 19 | 20 | 21 | class DevelopmentConfig(ProductionConfig): 22 | DEBUG = True 23 | TESTING = True 24 | SQLALCHEMY_TRACK_MODIFICATIONS = True 25 | -------------------------------------------------------------------------------- /keyserv/endpoints.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | # Copyright (c) 2019 Samuel Hoffman 4 | 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | 24 | from flask import request 25 | from flask_restful import Api, Resource, reqparse 26 | 27 | from keyserv.keymanager import (Origin, activate_key_unsafe, key_exists_const, 28 | key_get_unsafe, key_valid_const) 29 | from keyserv.models import Application 30 | 31 | api = Api() 32 | 33 | 34 | class ActivateKey(Resource): 35 | """Endpoint used for key activation.""" 36 | 37 | def post(self): 38 | """ 39 | Activate a key 40 | 41 | Activates a live key; will either allow key activation or deny if there 42 | are no more key activations left. Function will log attempts to 43 | activate regardless of success or failure. 44 | """ 45 | parser = reqparse.RequestParser() 46 | parser.add_argument("token", required=True) 47 | parser.add_argument("machine", required=True) 48 | parser.add_argument("user", required=True) 49 | parser.add_argument("app_id", required=True, type=int) 50 | parser.add_argument("hwid", required=True) 51 | 52 | args = parser.parse_args() 53 | 54 | origin = Origin(request.remote_addr, args.machine, 55 | args.user, args.hwid) 56 | 57 | if not key_exists_const(args.app_id, args.token, origin): 58 | 59 | resp = {"result": "failure", "error": "invalid activation token", 60 | "support_message": None} 61 | if args.app_id: 62 | app = Application.query.get(args.app_id) 63 | if app and app.support_message: 64 | resp["support_message"] = app.support_message 65 | 66 | return resp, 404 67 | 68 | key = key_get_unsafe(args.app_id, args.token, origin) 69 | 70 | if key.remaining == 0: 71 | resp = {"result": "failure", "error": "key is out of activations", 72 | "support_message": key.app.support_message} 73 | 74 | return resp, 410 75 | 76 | activate_key_unsafe(args.app_id, args.token, origin) 77 | 78 | return {"result": "ok", 79 | "remainingActivations": str(key.remaining)}, 201 80 | 81 | 82 | class CheckKey(Resource): 83 | """Endpoint used for checking if a key is valid.""" 84 | 85 | def get(self): 86 | parser = reqparse.RequestParser() 87 | parser.add_argument("token", required=True) 88 | parser.add_argument("machine", required=True) 89 | parser.add_argument("user", required=True) 90 | parser.add_argument("hwid", required=True) 91 | parser.add_argument("app_id", required=True, type=int) 92 | 93 | args = parser.parse_args() 94 | 95 | origin = Origin(request.remote_addr, 96 | args.machine, args.user, args.hwid) 97 | 98 | if key_valid_const(args.app_id, args.token, origin): 99 | return {"result": "ok"}, 201 100 | 101 | return {"result": "failure", "error": "invalid key"}, 404 102 | 103 | 104 | api.add_resource(ActivateKey, "/api/activate") 105 | api.add_resource(CheckKey, "/api/check") 106 | -------------------------------------------------------------------------------- /keyserv/forms.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | # Copyright(c) 2018 Samuel Hoffman 4 | 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files(the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and / or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | 22 | from flask_wtf import FlaskForm 23 | from wtforms import (BooleanField, IntegerField, PasswordField, SelectField, 24 | StringField, SubmitField) 25 | from wtforms.validators import required 26 | 27 | 28 | class LoginForm(FlaskForm): 29 | username = StringField("Username", [required()]) 30 | password = PasswordField("Password", [required()]) 31 | submit = SubmitField("Log In") 32 | 33 | 34 | class KeyForm(FlaskForm): 35 | activations = IntegerField("Number of Activations", 36 | default=0, 37 | render_kw={"type": "number", "min": -1, 38 | "value": 0}) 39 | application = SelectField("Application", coerce=int) 40 | 41 | active = BooleanField("Active", default=True) 42 | memo = StringField("Memo") 43 | hwid = StringField("Hardware Id") 44 | submit = SubmitField("Submit") 45 | 46 | 47 | class AppForm(FlaskForm): 48 | name = StringField("Application Name") 49 | support = StringField("Support Message") 50 | submit = SubmitField("Submit") 51 | -------------------------------------------------------------------------------- /keyserv/keymanager.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | # Copyright (c) 2019 Samuel Hoffman 4 | 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | import secrets 24 | import string 25 | from datetime import datetime 26 | from hmac import compare_digest 27 | 28 | from flask import current_app, request 29 | from flask_login import current_user 30 | from sqlalchemy import exists 31 | 32 | from keyserv.models import AuditLog, Event, Key, db 33 | 34 | 35 | class ExhuastedActivations(Exception): 36 | """Raised when an activation attempt is made but the remaining activations 37 | is already at 0.""" 38 | pass 39 | 40 | 41 | class KeyNotFound(Exception): 42 | """Raised when an action is attempted on a non-existent key.""" 43 | pass 44 | 45 | 46 | class Origin: 47 | """Origin that identifies a key action.""" 48 | 49 | def __init__(self, ip, machine, user, hardware_id=None): 50 | self.ip = ip 51 | self.machine = machine 52 | self.user = user 53 | self.hwid = hardware_id 54 | 55 | def __str__(self): 56 | return f"IP: {self.ip}, Machine: {self.machine}, User: {self.user}" 57 | 58 | def __repr__(self): 59 | return f"" 60 | 61 | 62 | def rand_token(length: int = 25, 63 | chars: str = string.ascii_uppercase + string.digits) -> str: 64 | """ 65 | Generate a random token. Does not check for duplicates yet. 66 | 67 | A length of 25 should give us 8.082812775E38 keys. 68 | 69 | length: - length of token to generate 70 | chars: - characters used in seeding of token 71 | """ 72 | return "".join(secrets.choice(chars) for i in range(length)) 73 | 74 | 75 | def token_exists_unsafe(token: str, hwid: str = "") -> bool: 76 | """Check if `token` exists in the token database. Does NOT perform constant 77 | time comparison. Should not be used in APIs """ 78 | return db.session.query(exists().where(Key.token == token) 79 | .where(Key.hwid == hwid)).scalar() 80 | 81 | 82 | def token_matches_hwid(token: str, hwid: str) -> bool: 83 | """Check if the supplied hwid matches the hwid on a key""" 84 | k = Key.query(token=token) 85 | 86 | return bool(_compare(hwid, k.hwid)) 87 | 88 | 89 | def generate_token_unsafe() -> str: 90 | """ 91 | Generate a new token. 92 | 93 | Does not perform constant time comparison when checking if the generated 94 | token is a duplicate. 95 | """ 96 | key = rand_token() 97 | while token_exists_unsafe(key): 98 | key = rand_token() 99 | return key 100 | 101 | 102 | def cut_key_unsafe(activations: int, app_id: int, 103 | active: bool = True, memo: str = "") -> str: 104 | """ 105 | Cuts a new key and returns the activation token. 106 | 107 | Cuts a new key with # `activations` allowed activations. -1 is considered 108 | unlimited activations. 109 | """ 110 | token = generate_token_unsafe() 111 | key = Key(token, activations, app_id, active, memo) 112 | key.cutdate = datetime.utcnow() 113 | 114 | db.session.add(key) 115 | db.session.commit() 116 | 117 | current_app.logger.info( 118 | f"cut new key {key} with {activations} activation(s), memo: {memo}") 119 | AuditLog.from_key(key, 120 | f"new key cut by {current_user.username} " 121 | f"({request.remote_addr})", 122 | Event.KeyCreated) 123 | 124 | return token 125 | 126 | 127 | def disable_key_unsafe(token: str): 128 | """Disable a key by its token.""" 129 | key = Key.query.filter(Key.token == token).first() 130 | if not key: 131 | current_app.logger.error( 132 | f"failed to disable key by non-existent token {token}") 133 | raise KeyNotFound(f"no key found for token {token}") 134 | key.enabled = False 135 | current_app.logger.info(f"disabled key {key}") 136 | AuditLog.from_key(key, "key was disabled", Event.KeyModified) 137 | db.session.commit() 138 | 139 | 140 | def _compare(left: str, right: str) -> int: 141 | if len(left) != len(right): 142 | return 0 143 | res = 0 144 | for leftchr, rightchr in zip(left, right): 145 | res |= ord(leftchr) ^ ord(rightchr) 146 | return res % 1 147 | 148 | 149 | def key_exists_const(app_id: int, token: str, origin: Origin) -> bool: 150 | """Constant time check to see if `token` exists in the database. Compares 151 | against all keys even if a match is found.""" 152 | current_app.logger.info(f"key lookup by token {token}") 153 | found = False 154 | for key in Key.query.all(): 155 | if (compare_digest(token, key.token) and 156 | key.enabled and key.app_id == app_id): 157 | 158 | found = True 159 | key.last_check_ts = datetime.utcnow() 160 | key.last_check_ip = origin.ip 161 | key.total_checks += 1 162 | AuditLog.from_key(key, f"key check from {origin}", Event.KeyAccess) 163 | return found 164 | 165 | 166 | def key_valid_const(app_id: int, token: str, origin: Origin) -> bool: 167 | """Constant time check to see if `token` exists in the database. Compares 168 | against all keys even if a match is found. Validates against the app id 169 | and the hardware id provided.""" 170 | current_app.logger.info(f"key lookup by token {token} from {origin}") 171 | found = False 172 | for key in Key.query.all(): 173 | if (compare_digest(token, key.token) and 174 | key.enabled and key.app_id == app_id 175 | and compare_digest(origin.hwid, key.hwid)): 176 | 177 | found = True 178 | key.last_check_ts = datetime.utcnow() 179 | key.last_check_ip = origin.ip 180 | key.total_checks += 1 181 | AuditLog.from_key(key, f"key check from {origin}", Event.KeyAccess) 182 | return found 183 | 184 | def key_get_unsafe(app_id: int, token: str, origin) -> Key: 185 | """Get a key by its token using constant time comparison.""" 186 | 187 | current_app.logger.info(f"key retreival by token {token} from {origin}") 188 | 189 | key = Key.query.filter_by(app_id=app_id, token=token, enabled=True).first() 190 | if key: 191 | AuditLog.from_key(key, f"key retreival from {origin}", Event.KeyAccess) 192 | return key 193 | return None 194 | 195 | 196 | def activate_key_unsafe(app_id: int, token: str, origin: Origin): 197 | """Mark a key as activated by its token. Does not perform constant time 198 | comparisons. 199 | 200 | `ip`, `machine`, and `user` are of the originating activation attempt. 201 | """ 202 | key = Key.query.filter_by(token=token, app_id=app_id, enabled=True).first() 203 | 204 | if key.remaining == -1: 205 | key.hwid = origin.hwid 206 | current_app.logger.info( 207 | f"new unlimited activation: Key {key!r} from {origin}") 208 | AuditLog.from_key( 209 | key, f"new unlimited activation from from {origin}", 210 | Event.AppActivation) 211 | return 212 | 213 | if key.remaining == 0: 214 | current_app.logger.info( 215 | f"failed activation attempt: Key {key!r} from {origin}") 216 | AuditLog.from_key( 217 | key, f"failed activation attempt from {origin}", 218 | Event.FailedActivation) 219 | 220 | raise ExhuastedActivations( 221 | f"token {token} has exhausted all remaining activations") 222 | 223 | key.remaining -= 1 224 | 225 | current_app.logger.info(f"new activation: Key {key!r} from {origin}." 226 | f" remaining activations: {key.remaining}") 227 | 228 | key.total_activations += 1 229 | key.last_activation_ts = datetime.utcnow() 230 | key.last_activation_ip = origin.ip 231 | key.hwid = origin.hwid 232 | 233 | AuditLog.from_key( 234 | key, f"new activation from {origin}", Event.AppActivation) 235 | 236 | db.session.commit() 237 | -------------------------------------------------------------------------------- /keyserv/models.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | # Copyright (c) 2019 Samuel Hoffman 4 | 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | from datetime import datetime 24 | from enum import IntEnum 25 | from typing import Any # NOQA: F401 26 | 27 | from flask_sqlalchemy import SQLAlchemy 28 | 29 | db = SQLAlchemy() # type: Any 30 | 31 | 32 | class Application(db.Model): 33 | id = db.Column(db.Integer, primary_key=True) 34 | name = db.Column(db.String, nullable=False, unique=True) 35 | support_message = db.Column(db.String) 36 | 37 | 38 | class Key(db.Model): 39 | """ 40 | Database representation of a software key provided by MKS. 41 | 42 | Id: identifier for a kkey 43 | token: the license token fed to the program 44 | remaining: remaining activations for a key. -1 if unlimited 45 | enabled: if the license is able to 46 | """ 47 | id = db.Column(db.Integer, primary_key=True) 48 | app = db.relationship("Application", uselist=False, backref="keys") 49 | app_id = db.Column(db.Integer, 50 | db.ForeignKey("application.id"), nullable=False) 51 | cutdate = db.Column(db.DateTime(timezone=True)) 52 | enabled = db.Column(db.Boolean, default=True) 53 | memo = db.Column(db.String) 54 | hwid = db.Column(db.String, default="") 55 | remaining = db.Column(db.Integer) 56 | token = db.Column(db.String, unique=True) 57 | total_activations = db.Column(db.Integer, default=0) 58 | total_checks = db.Column(db.Integer, default=0) 59 | last_activation_ts = db.Column(db.DateTime) 60 | last_activation_ip = db.Column(db.String) 61 | last_check_ts = db.Column(db.DateTime) 62 | last_check_ip = db.Column(db.String) 63 | 64 | def __init__(self, token: str, remaining: int, app_id: int, 65 | enabled: bool = True, memo: str = "", hwid: str = "") -> None: 66 | self.token = token 67 | self.remaining = remaining 68 | self.enabled = enabled 69 | self.memo = memo 70 | self.app_id = app_id 71 | self.hwid = hwid 72 | 73 | def __str__(self): 74 | return f"" 75 | 76 | 77 | class Event(IntEnum): 78 | Info = 0 79 | Warn = 1 80 | Error = 2 81 | AppActivation = 3 82 | FailedActivation = 4 83 | KeyModified = 5 84 | KeyCreated = 6 85 | KeyAccess = 7 86 | AppCreated = 8 87 | AppModified = 9 88 | 89 | 90 | class AuditLog(db.Model): 91 | """ 92 | Database representation of an audit log. 93 | """ 94 | id = db.Column(db.Integer, primary_key=True) 95 | app = db.relationship("Application", backref="logs") 96 | app_id = db.Column(db.Integer, db.ForeignKey("application.id"), nullable=False) 97 | event_type = db.Column(db.Integer) 98 | key = db.relationship("Key", uselist=False, backref="logs") 99 | key_id = db.Column(db.Integer, db.ForeignKey("key.id"), nullable=False) 100 | message = db.Column(db.String) 101 | timestamp = db.Column(db.DateTime) 102 | 103 | def __init__(self, key_id: int, app_id: int, 104 | message: str, event_type: Event) -> None: 105 | self.key_id = key_id 106 | self.app_id = app_id 107 | self.message = message 108 | self.event_type = int(event_type) 109 | self.timestamp = datetime.now() 110 | 111 | @classmethod 112 | def from_key(cls, key: Key, message: str, event_type: Event): 113 | audit = cls(key.id, key.app.id, message, event_type) 114 | db.session.add(audit) 115 | db.session.commit() 116 | -------------------------------------------------------------------------------- /keyserv/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/usrbinsam/jwt-key-server/f3a706b27b6ed840a891308cad1bc0e9e927f863/keyserv/static/favicon.ico -------------------------------------------------------------------------------- /keyserv/static/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/usrbinsam/jwt-key-server/f3a706b27b6ed840a891308cad1bc0e9e927f863/keyserv/static/icon.png -------------------------------------------------------------------------------- /keyserv/templates/add_modify.html: -------------------------------------------------------------------------------- 1 | {# 2 | MIT License 3 | 4 | Copyright(c) 2018 Samuel Hoffman 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files(the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and / or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | #} 24 | 25 | {% import "bootstrap/wtf.html" as wtf %} 26 | {% import "bootstrap/utils.html" as utils %} 27 | {% extends "layout.html" %} 28 | 29 | {% block title %}Mini Key Server - {{ header }}{% endblock%} 30 | 31 | {% block container %} 32 |

{{ header }}

33 | {{ wtf.quick_form(form) }} 34 | {%- endblock %} 35 | -------------------------------------------------------------------------------- /keyserv/templates/applications.html: -------------------------------------------------------------------------------- 1 | {# 2 | MIT License 3 | 4 | Copyright(c) 2018 Samuel Hoffman 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files(the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and / or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | #} 24 | 25 | {% import "bootstrap/wtf.html" as wtf %} 26 | {% import "bootstrap/utils.html" as utils %} 27 | {% extends "layout.html" %} 28 | 29 | {% block title %}Mini Key Server - Keys{% endblock%} 30 | 31 | {% block container %} 32 |

Applications

33 | 34 | 35 | Add Application 36 | 37 | 38 | {% if apps %} 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | {% for app in apps %} 51 | 52 | 53 | 54 | 55 | 56 | 63 | 64 | {% endfor %} 65 | 66 |
IDNameKeysSupport MessageActions
{{ app.id }}{{ app.name }}{{ app.keys|length }}{{ app.support_message }} 58 | Modify 59 | 61 | Detail 62 |
67 | {% else %} 68 |

No applications added. 69 | Add 70 | one to get started.

71 | {% endif %} 72 | {%- endblock %} 73 | -------------------------------------------------------------------------------- /keyserv/templates/detail_app.html: -------------------------------------------------------------------------------- 1 | {# 2 | MIT License 3 | 4 | Copyright(c) 2018 Samuel Hoffman 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files(the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and / or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | #} 24 | {% extends "layout.html" %} 25 | 26 | {% block title %}Mini Key Server - Application Detail{% endblock%} 27 | 28 | {% block container %} 29 |

Detail for Application {{ app.name }}

30 | 31 | 32 | 33 |
34 | 35 |
{{ app.name }}
36 |
37 | 38 | Modify 39 | 40 | Add Key 41 |

42 |
    43 |
  • App ID: {{ app.id }}
  • 44 |
  • Number of Keys: {{ app.keys|length }}
  • 45 |
  • Support Message: {{ app.support_message }}
  • 46 |
47 |
48 |
49 | 50 |

Audit Log

51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | {% for log in app.logs|sort(attribute='id', reverse=True) %} 62 | 63 | 64 | 65 | 66 | 67 | 68 | {% endfor %} 69 | 70 |
KeyTime StampMessageEvent
{{ log.key_id }}{{ log.timestamp.strftime("%Y-%m-%d %H:%M:%S") }}{{ log.message }}{{ log.event_type|event }}
71 | {%- endblock %} 72 | -------------------------------------------------------------------------------- /keyserv/templates/detail_key.html: -------------------------------------------------------------------------------- 1 | {# 2 | MIT License 3 | 4 | Copyright(c) 2018 Samuel Hoffman 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files(the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and / or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | #} 24 | {% extends "layout.html" %} 25 | 26 | {% block title %}Mini Key Server - Key Detail{% endblock%} 27 | 28 | {% block container %} 29 |

Detail for Key {{ key.id }}

30 | 31 |
32 | 33 |
Key Info for {{ key.app.name }}
34 |
35 | 36 | Modify 37 | {% if key.enabled %} 38 | 39 | Deactivate 40 | 41 | {% else %} 42 | 43 | Activate 44 | {% endif %} 45 |

46 |
    47 |
  • Token: {{ key.token }}
  • 48 |
  • Active: {{ "Yes" if key.enabled else "No" }}
  • 49 |
  • Remaining Activations: 50 | {% if key.remaining == -1 %} 51 | Unlimited 52 | {% else %} 53 | {{ key.remaining }} 54 | {% endif %}
  • 55 |
  • Cut On: 56 | {{ key.cutdate|datetime }}
  • 57 | {% if key.memo %} 58 |
  • Memo: {{ key.memo }}
  • 59 | {% endif %} 60 |
  • Lifetime Activations: 61 | {{ key.total_activations }}
  • 62 | 63 | {% if key.last_activation_ts %} 64 |
  • Last Activation on 65 | {{ key.last_activation_ts|datetime }} from 66 | {{ key.last_activation_ip }}
  • 67 | {% endif %} 68 | 69 | 70 |
  • Hardware Id: 71 | {% if key.hwid %} 72 | {{ key.hwid }} 73 | {% else %} 74 | ⚠️ No Hardware Id ⚠️ 75 | {% endif %} 76 |
  • 77 | 78 |
  • Lifetime Checks: {{ key.total_checks }}
  • 79 | 80 | {% if key.last_check_ts %} 81 |
  • Last Check on 82 | {{ key.last_check_ts|datetime }} from 83 | {{ key.last_check_ip }}
  • 84 | {% endif %} 85 |
86 |
87 |
88 | 89 |

Audit Log

90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | {% for log in key.logs|sort(attribute='id', reverse=True) %} 100 | 101 | 102 | 103 | 104 | 105 | {% endfor %} 106 | 107 |
Time StampMessageEvent
{{ log.timestamp|datetime }}{{ log.message }}{{ log.event_type|event }}
108 | {%- endblock %} 109 | -------------------------------------------------------------------------------- /keyserv/templates/index.html: -------------------------------------------------------------------------------- 1 | {# 2 | MIT License 3 | 4 | Copyright(c) 2018 Samuel Hoffman 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files(the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and / or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | #} 24 | 25 | {% import "bootstrap/wtf.html" as wtf %} 26 | {% extends "layout.html" %} 27 | 28 | {% block title %}Mini Key Server{% endblock%} 29 | 30 | {% block container %} 31 | 32 | {% if current_user.is_authenticated %} 33 |

Currently signed in as 34 | {{ current_user.username }} 35 |

36 | {% else %} 37 |

Please login to continue.

38 | {{ wtf.quick_form(form) }} {% endif %} 39 | 40 | {%- endblock %} 41 | -------------------------------------------------------------------------------- /keyserv/templates/keys.html: -------------------------------------------------------------------------------- 1 | {# 2 | MIT License 3 | 4 | Copyright(c) 2018 Samuel Hoffman 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files(the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and / or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | #} 24 | 25 | {% import "bootstrap/wtf.html" as wtf %} 26 | {% import "bootstrap/utils.html" as utils %} 27 | {% extends "layout.html" %} 28 | 29 | {% block title %}Mini Key Server - Keys{% endblock%} 30 | 31 | {% block container %} 32 |

Keys

33 | 34 | Add Key 35 | 36 | {% if keys %} 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | {% for key in keys %} 52 | 53 | 54 | 55 | 56 | 57 | 62 | 63 | 64 | 71 | 72 | {% endfor %} 73 | 74 |
IDTokenApplicationActiveRemaining ActivationsCut DateMemoModify
{{ key.id }}{{ key.token }} {{ key.app.name }}{% if key.enabled %}Yes{% else %}No{% endif %}{% if key.remaining == -1 %} 58 | Unlimited 59 | {% else %} 60 | {{ key.remaining }} 61 | {% endif %}{{ key.cutdate.strftime('%Y-%m-%d %H:%M:%S') }}{{ key.memo }} 66 | Modify 67 | 68 | 70 | Detail
75 | {% else %} 76 |

No keys have been cut.

77 | {% endif %} 78 | {%- endblock %} 79 | -------------------------------------------------------------------------------- /keyserv/templates/layout.html: -------------------------------------------------------------------------------- 1 | {# 2 | MIT License 3 | 4 | Copyright(c) 2019 Samuel Hoffman 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files(the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and / or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | #} 24 | 25 | {% extends "bootstrap/base.html" %} 26 | {% import "bootstrap/utils.html" as utils %} 27 | 28 | {% block title %}Mini Key Server{% endblock %} 29 | 30 | {% block styles %} {{ super() }} 31 | 32 | {% endblock styles %} 33 | 34 | {% block content %} 35 | 36 | 37 |
38 |

Mini Key Server

39 |
40 |
41 | {{ utils.flashed_messages(messages) }} 42 |
43 |
44 | {% if current_user.is_authenticated %} 45 | 46 | Keys 47 | 48 | Applications 49 | 50 | Audit Log 51 | 52 | Log Out 53 |
54 | {% endif %} 55 | {% block container %}{% endblock container %} 56 |
57 |
58 |
59 |

Copyright © 2019, GliTch_ Is Mad Studios

60 |
61 |
62 | {% endblock %} 63 | -------------------------------------------------------------------------------- /keyserv/templates/logs.html: -------------------------------------------------------------------------------- 1 | {# 2 | MIT License 3 | 4 | Copyright(c) 2018 Samuel Hoffman 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files(the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and / or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | #} 24 | {% extends "layout.html" %} 25 | 26 | {% block title %}Mini Key Server - Audit Log{% endblock%} 27 | 28 | {% block container %} 29 |

Audit Log

30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | {% for log in logs|sort(attribute='id', reverse=True) %} 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | {% endfor %} 50 | 51 |
KeyApplicationTime StampMessageEvent
{{ log.key.id }}{{ log.app.name }}{{ log.timestamp.strftime("%Y-%m-%d %H:%M:%S") }}{{ log.message }}{{ log.event_type|event }}
52 | {%- endblock %} 53 | -------------------------------------------------------------------------------- /keyserv/views.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | # Copyright (c) 2019 Samuel Hoffman 4 | 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | import os 24 | 25 | from flask import (Blueprint, abort, current_app, flash, redirect, 26 | render_template, request, send_from_directory, url_for) 27 | from flask_login import current_user, login_required, login_user, logout_user 28 | 29 | from keyserv.auth import Users 30 | from keyserv.forms import AppForm, KeyForm, LoginForm 31 | from keyserv.keymanager import cut_key_unsafe 32 | from keyserv.models import Application, AuditLog, Event, Key, db 33 | 34 | frontend = Blueprint("frontend", __name__) 35 | 36 | 37 | @frontend.route("/favicon.ico") 38 | def favicon(): 39 | return send_from_directory(os.path.join(current_app.root_path, "static"), 40 | "favicon.ico", 41 | mimetype="image/vnd.microsoft.icon") 42 | 43 | 44 | @frontend.route("/", methods=["GET", "POST"]) 45 | def index(): 46 | 47 | form = LoginForm(request.form) 48 | 49 | if request.method == "POST" and form.validate(): 50 | current_app.logger.debug("login form was submitted") 51 | user = Users.query.filter_by(username=form.username.data).first() 52 | if user and user.check_password(form.password.data): 53 | if login_user(user): 54 | current_app.logger.debug(f"login for {user}") 55 | else: 56 | flash("Invalid username or password.", "error") 57 | return redirect(url_for("frontend.index")) 58 | 59 | return render_template("index.html", form=form, current_user=current_user) 60 | 61 | 62 | @frontend.route("/logout") 63 | def logout(): 64 | logout_user() 65 | return redirect(url_for("frontend.index")) 66 | 67 | 68 | @frontend.route("/keys") 69 | @login_required 70 | def keys(): 71 | return render_template("keys.html", keys=Key.query.all()) 72 | 73 | 74 | @frontend.route("/applications") 75 | @login_required 76 | def apps(): 77 | return render_template("applications.html", apps=Application.query.all()) 78 | 79 | 80 | @frontend.route("/logs") 81 | @login_required 82 | def logs(): 83 | return render_template("logs.html", logs=AuditLog.query.all()) 84 | 85 | 86 | @frontend.route("/modify/key/", methods=["GET", "POST"]) 87 | @login_required 88 | def modify_key(key_id: int): 89 | 90 | key = Key.query.get(key_id) 91 | if not key: 92 | abort(404) 93 | 94 | form = KeyForm(request.form) 95 | form.application.choices = [(app.id, app.name) 96 | for app in Application.query.all()] 97 | 98 | if request.method == "POST" and form.validate_on_submit(): 99 | changes = [] 100 | 101 | if key.remaining != form.activations.data: 102 | changes.append(f"activations changed from {key.remaining}" 103 | f" to {form.activations.data}") 104 | key.remaining = form.activations.data 105 | if key.memo != form.memo.data: 106 | changes.append(f"memo changed from {key.memo!r}" 107 | f" to {form.memo.data!r}") 108 | key.memo = form.memo.data 109 | if key.app_id != form.application.data: 110 | changes.append(f"app changed from {key.app} to" 111 | f" {form.application.data}") 112 | key.application = form.application.data 113 | if key.enabled != form.active.data: 114 | changes.append(f"active changed from {key.enabled}" 115 | f" to {form.active.data}") 116 | key.enabled = form.active.data 117 | if key.hwid != form.hwid.data: 118 | changes.append(f"hwid changed from {key.hwid!r} to " 119 | f"{form.hwid.data!r}") 120 | key.hwid = form.hwid.data 121 | 122 | AuditLog.from_key(key, f"edited by {current_user.username} " 123 | f"({request.remote_addr}):" 124 | f" {', '.join(changes)}", Event.KeyModified) 125 | 126 | try: 127 | db.session.commit() 128 | flash("Changes successful!") 129 | return redirect(url_for("frontend.detail_key", key_id=key.id)) 130 | except Exception as error: 131 | flash(f"Failed to update key: {error}") 132 | 133 | form.application.data = key.app_id 134 | form.active.data = key.enabled 135 | form.memo.data = key.memo 136 | form.activations.data = key.remaining 137 | form.hwid.data = key.hwid 138 | 139 | return render_template("add_modify.html", 140 | header=f"Modify Key {key.id}", form=form) 141 | 142 | 143 | @frontend.route("/add/key", methods=["GET", "POST"]) 144 | @frontend.route("/add/key/", methods=["GET", "POST"]) 145 | @login_required 146 | def add_key(app_id=None): 147 | form = KeyForm(request.form) 148 | form.application.choices = [(app.id, app.name) 149 | for app in Application.query.all()] 150 | 151 | if app_id: 152 | form.application.data = app_id 153 | 154 | if request.method == "POST" and form.validate_on_submit(): 155 | try: 156 | token = cut_key_unsafe(form.activations.data, 157 | form.application.data, 158 | form.active.data, form.memo.data) 159 | flash(f"Key added! Token: {token}", "success") 160 | except Exception as error: 161 | flash(f"Unable to add key: {error}", "error") 162 | 163 | return render_template("add_modify.html", header="Add Key", form=form) 164 | 165 | 166 | @frontend.route("/add/app", methods=["GET", "POST"]) 167 | @login_required 168 | def add_app(): 169 | form = AppForm(request.form) 170 | 171 | if request.method == "POST" and form.validate_on_submit(): 172 | app = Application() 173 | app.name = form.name.data 174 | app.support_message = form.support.data 175 | 176 | db.session.add(app) 177 | try: 178 | db.session.commit() 179 | flash("Success!") 180 | except Exception as error: 181 | flash(f"Failed to add application: {error}") 182 | 183 | return render_template("add_modify.html", 184 | form=form, header="Add Application") 185 | 186 | 187 | @frontend.route("/modify/app/", methods=["GET", "POST"]) 188 | @login_required 189 | def modify_app(app_id: int): 190 | app = Application.query.get(app_id) 191 | 192 | if not app: 193 | abort(404) 194 | 195 | form = AppForm(request.form) 196 | if request.method == "POST" and form.validate_on_submit(): 197 | 198 | app.name = form.name.data 199 | app.support_message = form.support.data 200 | try: 201 | db.session.commit() 202 | flash("Success.") 203 | return redirect(url_for("frontend.detail_app", app_id=app.id)) 204 | except Exception as error: 205 | flash(f"Failed to modify application: {error}", "error") 206 | 207 | form.name.data = app.name 208 | form.support.data = app.support_message 209 | 210 | return render_template("add_modify.html", form=form) 211 | 212 | 213 | @frontend.route("/detail/key/") 214 | @login_required 215 | def detail_key(key_id: int): 216 | 217 | key = Key.query.get(key_id) 218 | 219 | if not key: 220 | abort(404) 221 | 222 | return render_template("detail_key.html", key=key) 223 | 224 | 225 | @frontend.route("/detail/app/") 226 | @login_required 227 | def detail_app(app_id: int): 228 | 229 | app = Application.query.get(app_id) 230 | 231 | if not app: 232 | abort(404) 233 | 234 | return render_template("detail_app.html", app=app) 235 | 236 | 237 | @frontend.route("/keys/app/") 238 | @login_required 239 | def keys_for_app(app_id): 240 | 241 | app = Application.query.get(app_id) 242 | 243 | if not app: 244 | abort(404) 245 | 246 | return render_template("keys.html", keys=app.keys) 247 | 248 | 249 | @frontend.route("/keys/deactivate/") 250 | @login_required 251 | def disable_key(key_id): 252 | 253 | key = Key.query.get(key_id) 254 | 255 | if not key: 256 | abort(404) 257 | 258 | key.enabled = False 259 | db.session.commit() 260 | 261 | return redirect(url_for("frontend.detail_key", key_id=key_id)) 262 | 263 | 264 | @frontend.route("/keys/activate/") 265 | @login_required 266 | def enable_key(key_id): 267 | 268 | key = Key.query.get(key_id) 269 | 270 | if not key: 271 | abort(404) 272 | 273 | key.enabled = True 274 | db.session.commit() 275 | 276 | return redirect(url_for("frontend.detail_key", key_id=key_id)) 277 | -------------------------------------------------------------------------------- /keyserver.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from keyserv import create_app 4 | 5 | if os.environ.get("FLASK_DEBUG"): 6 | app = create_app("DevelopmentConfig") 7 | else: 8 | app = create_app("ProductionConfig") 9 | 10 | if __name__ == '__main__': 11 | app.run() 12 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | argon2_cffi 2 | flask 3 | flask_bootstrap 4 | flask_login 5 | flask_restful 6 | flask_sqlalchemy 7 | flask_wtf 8 | psycopg2-binary 9 | wtforms 10 | uwsgi 11 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aniso8601==7.0.0 2 | argon2-cffi==19.1.0 3 | cffi==1.12.3 4 | Click==7.0 5 | dominate==2.3.5 6 | Flask==1.1.1 7 | Flask-Bootstrap==3.3.7.1 8 | Flask-Login==0.4.1 9 | Flask-RESTful==0.3.7 10 | Flask-SQLAlchemy==2.4.0 11 | Flask-WTF==0.14.2 12 | itsdangerous==1.1.0 13 | Jinja2==2.10.1 14 | MarkupSafe==1.1.1 15 | psycopg2-binary==2.8.3 16 | pycparser==2.19 17 | pytz==2019.1 18 | six==1.12.0 19 | SQLAlchemy==1.3.5 20 | uWSGI==2.0.18 21 | visitor==0.1.3 22 | Werkzeug==0.15.4 23 | WTForms==2.2.1 24 | --------------------------------------------------------------------------------