init commit
Initial commit with all of the files.
This commit is contained in:
53
app.py
Normal file
53
app.py
Normal file
@@ -0,0 +1,53 @@
|
||||
from flask import Flask, request, jsonify, render_template, abort
|
||||
from passlib.hash import sha512_crypt, sha256_crypt, md5_crypt
|
||||
import secrets, os
|
||||
|
||||
app = Flask(__name__, static_folder='static', template_folder='templates')
|
||||
|
||||
SALT_CHARS = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
||||
MIN_LEN = 16
|
||||
MAX_SALT_LEN = 16
|
||||
|
||||
ALG_MAP = {
|
||||
'sha512': sha512_crypt,
|
||||
'sha256': sha256_crypt
|
||||
}
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
return render_template('index.html')
|
||||
|
||||
@app.route('/gensalt')
|
||||
def gensalt():
|
||||
salt = ''.join(secrets.choice(SALT_CHARS) for _ in range(MAX_SALT_LEN))
|
||||
return jsonify({'salt': salt})
|
||||
|
||||
@app.route('/hash', methods=['POST'])
|
||||
def do_hash():
|
||||
data = request.get_json() or {}
|
||||
password = data.get('password', '')
|
||||
salt = data.get('salt', '')
|
||||
algorithm = data.get('algorithm', 'sha512')
|
||||
|
||||
if not isinstance(password, str) or not isinstance(salt, str):
|
||||
abort(400, 'Invalid input')
|
||||
if len(password) < MIN_LEN:
|
||||
abort(400, f'Password must be at least {MIN_LEN} characters')
|
||||
if len(salt) < MIN_LEN or len(salt) > MAX_SALT_LEN:
|
||||
abort(400, f'Salt must be between {MIN_LEN} and {MAX_SALT_LEN} characters')
|
||||
|
||||
hash_class = ALG_MAP.get(algorithm)
|
||||
if hash_class is None:
|
||||
abort(400, 'Unsupported algorithm')
|
||||
|
||||
# truncate salt to MAX_SALT_LEN just in case
|
||||
salt_to_use = salt[:MAX_SALT_LEN]
|
||||
|
||||
hashed = hash_class.using(salt=salt_to_use).hash(password)
|
||||
return jsonify({'hash': hashed})
|
||||
|
||||
if __name__ == '__main__':
|
||||
host = os.environ.get('HOST', '127.0.0.1')
|
||||
port = int(os.environ.get('PORT', 4444))
|
||||
debug = os.environ.get('DEBUG', '1') == '1'
|
||||
app.run(host=host, port=port, debug=debug)
|
||||
47
index.html
Normal file
47
index.html
Normal file
@@ -0,0 +1,47 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>TNC's Hashing Tool</title>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<h2>TNC's Hashing Tool</h2>
|
||||
|
||||
<label for="algorithm">Algorithm</label>
|
||||
<select id="algorithm">
|
||||
<option value="sha512">sha512-crypt ($6$)</option>
|
||||
<option value="sha256">sha256-crypt ($5$)</option>
|
||||
</select>
|
||||
|
||||
|
||||
<label for="password">Password (minimum 16 characters)</label>
|
||||
<input id="password" type="password" placeholder="Enter password" minlength="16">
|
||||
|
||||
|
||||
<label for="salt">Salt (maximum 16 characters)</label>
|
||||
<div class="row">
|
||||
<input id="salt" type="text" placeholder="Enter salt or generate one">
|
||||
<button id="gensaltBtn" type="button" class="small">Generate salt</button>
|
||||
</div>
|
||||
<div class="note">Salt characters limited to <code>./0-9A-Za-z</code>.</div>
|
||||
|
||||
|
||||
<div class="controls">
|
||||
<button id="hashBtn">Compute hash</button>
|
||||
<button id="clearBtn" type="button">Clear</button>
|
||||
</div>
|
||||
|
||||
|
||||
<label for="result">Result</label>
|
||||
<textarea id="result" rows="4" readonly placeholder="Result will appear here"></textarea>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/main.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
49
main.js
Normal file
49
main.js
Normal file
@@ -0,0 +1,49 @@
|
||||
const gensaltBtn = document.getElementById('gensaltBtn');
|
||||
const algorithm = document.getElementById('algorithm');
|
||||
const password = document.getElementById('password');
|
||||
const salt = document.getElementById('salt');
|
||||
const hashBtn = document.getElementById('hashBtn');
|
||||
const result = document.getElementById('result');
|
||||
const clearBtn = document.getElementById('clearBtn');
|
||||
|
||||
const MIN_LEN = 16;
|
||||
|
||||
gensaltBtn.addEventListener('click', async () => {
|
||||
const len = Math.max(MIN_LEN, MIN_LEN);
|
||||
const res = await fetch('/gensalt?length=' + len);
|
||||
if (!res.ok) { alert('Could not generate salt'); return; }
|
||||
const data = await res.json();
|
||||
salt.value = data.salt;
|
||||
});
|
||||
|
||||
|
||||
hashBtn.addEventListener('click', async () => {
|
||||
const pass = password.value || '';
|
||||
const s = salt.value || '';
|
||||
const alg = algorithm.value;
|
||||
|
||||
if (pass.length < MIN_LEN) { alert('Password must be at least ' + MIN_LEN + ' characters'); return; }
|
||||
|
||||
if (s.length < MIN_LEN) { alert('Salt must be at least ' + MIN_LEN + ' characters'); return; }
|
||||
|
||||
const payload = { password: pass, salt: s, algorithm: alg };
|
||||
const res = await fetch('/hash', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const txt = await res.text();
|
||||
alert('Error: ' + txt);
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
result.value = data.hash;
|
||||
});
|
||||
|
||||
|
||||
clearBtn.addEventListener('click', () => {
|
||||
password.value = '';
|
||||
salt.value = '';
|
||||
result.value = '';
|
||||
});
|
||||
BIN
requirements.txt
Normal file
BIN
requirements.txt
Normal file
Binary file not shown.
61
style.css
Normal file
61
style.css
Normal file
@@ -0,0 +1,61 @@
|
||||
body {
|
||||
font-family: system-ui, Segoe UI, Roboto, Arial;
|
||||
max-width: 800px;
|
||||
margin: 40px auto;
|
||||
padding: 0 16px;
|
||||
background: #494949;
|
||||
color: #ececec;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 12px
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-top: 12px
|
||||
}
|
||||
|
||||
input[type=text],
|
||||
input[type=password],
|
||||
textarea,
|
||||
select {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
margin-top: 6px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #ececec
|
||||
}
|
||||
|
||||
button {
|
||||
margin-top: 12px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 8px;
|
||||
border: 0;
|
||||
background: #1f7be0;
|
||||
color: white
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
gap: 8px
|
||||
}
|
||||
|
||||
.row>* {
|
||||
flex: 1
|
||||
}
|
||||
|
||||
.small {
|
||||
width: 140px
|
||||
}
|
||||
|
||||
.note {
|
||||
font-size: 0.9em;
|
||||
color: #ffffff
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 12px
|
||||
}
|
||||
Reference in New Issue
Block a user