Compare commits

..

12 Commits

7 changed files with 245 additions and 80 deletions

1
.gitignore vendored
View File

@@ -1,3 +1,4 @@
venv/ venv/
.venv/
__pycache__/ __pycache__/
*.pyc *.pyc

48
app.py
View File

@@ -1,5 +1,12 @@
from flask import Flask, request, jsonify, render_template, abort from flask import Flask, request, jsonify, render_template, abort
import secrets, crypt, os from argon2 import PasswordHasher
from argon2.low_level import Type as ArgonType, hash_secret
import secrets, os
try:
import crypt
except ImportError:
import crypt_r as crypt
app = Flask(__name__, static_folder='static', template_folder='templates') app = Flask(__name__, static_folder='static', template_folder='templates')
@@ -8,10 +15,13 @@ MIN_LEN = 16
MIN_SALT_LEN = 8 MIN_SALT_LEN = 8
MAX_SALT_LEN = 16 MAX_SALT_LEN = 16
ph = PasswordHasher()
ALG_PREFIX = { ALG_PREFIX = {
'sha512': '$6$', 'sha512': '$6$',
'sha256': '$5$', 'sha256': '$5$',
'argon2_std': '$argon2id$',
'argon2_copyparty': '+'
} }
# The main route # The main route
@@ -30,6 +40,7 @@ def gensalt():
@app.route('/hash', methods=['POST']) @app.route('/hash', methods=['POST'])
def do_hash(): def do_hash():
data = request.get_json() or {} data = request.get_json() or {}
username = data.get('username', '')
password = data.get('password', '') password = data.get('password', '')
salt = data.get('salt', '') salt = data.get('salt', '')
algorithm = data.get('algorithm', 'sha512') algorithm = data.get('algorithm', 'sha512')
@@ -37,17 +48,49 @@ def do_hash():
if not isinstance(password, str) or not isinstance(salt, str): if not isinstance(password, str) or not isinstance(salt, str):
abort(400, 'Invalid input') abort(400, 'Invalid input')
if len(password) < MIN_LEN: if len(password) < MIN_LEN:
abort(400, f'Password must be at least {MIN_LEN} characters') abort(400, f'Password must be at least {MIN_LEN} characters')
if algorithm == 'argon2_copyparty':
if (username == '' or username == None):
abort(400, 'Please type your username.')
specified_salt = 'LVZ1TJMdAIdLyBla6nWDexFt'
full_block = f"{username}:{password}"
b_pass = full_block.encode('utf-8')
b_salt = specified_salt.encode('utf-8')
raw_hash_copyparty = hash_secret(
secret = b_pass,
salt = b_salt,
time_cost = 3,
memory_cost = 256 * 1024,
parallelism = 4,
hash_len = 24,
type = ArgonType.ID,
version = 19
)
hash_only = raw_hash_copyparty.split(b"$")[-1].decode('utf-8')
final_hash = "+" + hash_only.replace('/', "_").replace('+', '-')
return jsonify({'hash': final_hash})
if len(salt) < MIN_SALT_LEN or len(salt) > MAX_SALT_LEN: if len(salt) < MIN_SALT_LEN or len(salt) > MAX_SALT_LEN:
abort(400, f'Salt must be between {MIN_SALT_LEN} and {MAX_SALT_LEN} characters') abort(400, f'Salt must be between {MIN_SALT_LEN} and {MAX_SALT_LEN} characters')
if algorithm == 'argon2_std':
hashed = ph.hash(password, salt = salt.encode('utf-8'))
return jsonify({'hash': hashed})
prefix = ALG_PREFIX.get(algorithm) prefix = ALG_PREFIX.get(algorithm)
if prefix is None: if prefix is None:
abort(400, 'Unsupported algorithm') abort(400, 'Unsupported algorithm')
full_salt = f"{prefix}{salt}" full_salt = f"{prefix}{salt}"
hashed = crypt.crypt(password, full_salt) hashed = crypt.crypt(password, full_salt)
return jsonify({'hash': hashed}) return jsonify({'hash': hashed})
@@ -58,3 +101,4 @@ if __name__ == '__main__':
port = int(os.environ.get('PORT', 4444)) port = int(os.environ.get('PORT', 4444))
debug = os.environ.get('DEBUG', '1') == '1' debug = os.environ.get('DEBUG', '1') == '1'
app.run(host=host, port=port, debug=debug) app.run(host=host, port=port, debug=debug)

Binary file not shown.

View File

@@ -40,6 +40,14 @@ button:hover {
cursor: pointer; cursor: pointer;
} }
button:disabled,
input:disabled {
background: #666666;
color: #aaaaaa;
cursor: not-allowed;
filter: grayscale(1);
}
.row { .row {
display: flex; display: flex;
gap: 8px gap: 8px

View File

@@ -5,6 +5,8 @@ const salt = document.getElementById('salt');
const hashBtn = document.getElementById('hashBtn'); const hashBtn = document.getElementById('hashBtn');
const result = document.getElementById('result'); const result = document.getElementById('result');
const clearBtn = document.getElementById('clearBtn'); const clearBtn = document.getElementById('clearBtn');
const resultBtn = document.getElementById('resultBtn');
const username = document.getElementById('username');
const MIN_PASS_LEN = 16; const MIN_PASS_LEN = 16;
const MIN_SALT_LEN = 8; const MIN_SALT_LEN = 8;
@@ -18,22 +20,53 @@ gensaltBtn.addEventListener('click', async () => {
salt.value = data.salt; salt.value = data.salt;
}); });
const updateUI = () => {
const isCopyparty = algorithm.value === 'argon2_copyparty';
salt.disabled = isCopyparty;
gensaltBtn.disabled = isCopyparty;
username.disabled = !isCopyparty;
if (isCopyparty) {
salt.value = "LVZ1TJMdAIdLyBla6nWDexFt";
salt.style.opacity = "0.5";
} else {
salt.value = "";
salt.style.opacity = "";
username.value = "";
}
};
algorithm.addEventListener('change', updateUI);
window.addEventListener('DOMContentLoaded', updateUI);
hashBtn.addEventListener('click', async () => { hashBtn.addEventListener('click', async () => {
const pass = password.value || ''; const pass = password.value || '';
const s = salt.value || ''; const s = salt.value || '';
const alg = algorithm.value; const alg = algorithm.value;
const usr = username.value || '';
if (pass.length < MIN_PASS_LEN) { if (pass.length < MIN_PASS_LEN) {
alert('Password must be at least ' + MIN_PASS_LEN + ' characters'); alert('Password must be at least ' + MIN_PASS_LEN + ' characters');
return; return;
} }
if (alg !== 'argon2_copyparty') {
if (s.length < MIN_SALT_LEN || s.length > MAX_SALT_LEN) { if (s.length < MIN_SALT_LEN || s.length > MAX_SALT_LEN) {
alert('Salt must be between ' + MIN_SALT_LEN + ' and ' + MAX_SALT_LEN + ' characters'); alert('Salt must be between ' + MIN_SALT_LEN + ' and ' + MAX_SALT_LEN + ' characters');
return; return;
} }
}
const payload = { password: pass, salt: s, algorithm: alg }; if (alg == 'argon2_copyparty') {
if (usr === '') {
alert('Please type your username.');
return;
}
}
const payload = { username: usr, password: pass, salt: s, algorithm: alg };
const res = await fetch('/hash', { const res = await fetch('/hash', {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload)
}); });
@@ -45,12 +78,24 @@ hashBtn.addEventListener('click', async () => {
} }
const data = await res.json(); const data = await res.json();
result.value = data.hash; result.textContent = data.hash;
}); });
clearBtn.addEventListener('click', () => { clearBtn.addEventListener('click', () => {
password.value = ''; password.value = '';
salt.value = ''; salt.value = '';
result.value = ''; result.textContent = 'Result will appear here';
}); });
resultBtn.addEventListener('click', async () => {
if (!result.textContent || result.textContent === 'Result will appear here') {
alert('Nothing to copy.');
}
try {
await navigator.clipboard.writeText(result.textContent);
alert('Copied to clipboard.');
} catch (err) {
alert('Failed to copy: ' + err);
}
});

62
static/js/main.js.save Normal file
View File

@@ -0,0 +1,62 @@
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 resultBtn = document.getElementById('resultBtn');
const MIN_PASS_LEN = 16;
const MIN_SALT_LEN = 8;
const MAX_SALT_LEN = 16;
gensaltBtn.addEventListener('click', async () => {
const len = MAX_SALT_LEN; // can be adjusted or user-defined
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_PASS_LEN) {
alert('Password must be at least ' + MIN_PASS_LEN + ' characters');
return;
}
if (s.length < MIN_SALT_LEN || s.length > MAX_SALT_LEN) {
alert('Salt must be between ' + MIN_SALT_LEN + ' and ' + MAX_SALT_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 = '';
});
btnResult.addEventListener('click', () => {
navigator.clipboard.writeText(result.value);
alert("Copied text: " + result.value);
});

View File

@@ -4,22 +4,24 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1"> <meta name="viewport" content="width=device-width,initial-scale=1">
<title>TNC's Hashing Tool</title> <title>The Night Club's Hashing Tool</title>
<link rel="stylesheet" href="/static/css/style.css"> <link rel="stylesheet" href="/static/css/style.css">
</head> </head>
<body> <body>
<div class="container"> <div class="container">
<h2>TNC's Hashing Tool</h2> <h2>The Night Club's Hashing Tool</h2>
<label for="algorithm">Algorithm</label> <label for="algorithm">Algorithm</label>
<select id="algorithm"> <select id="algorithm" onchange="updateUI()">
<option value="sha512">sha512-crypt ($6$)</option> <option value="sha512">sha512-crypt ($6$)</option>
<option value="sha256">sha256-crypt ($5$)</option> <option value="sha256">sha256-crypt ($5$)</option>
<option value="argon2_std">argon2 ($argon2id$)</option>
<option value="argon2_copyparty">argon2 (copyparty)</option>
</select> </select>
<label for="password">Password (minimum 16 characters)</label> <label for="password">Password (you will use this to login, minimum 16 characters)</label>
<input id="password" type="password" placeholder="Enter password" minlength="16"> <input id="password" type="password" placeholder="Enter password" minlength="16">
@@ -30,6 +32,8 @@
</div> </div>
<div class="note">Salt characters limited to <code>./0-9A-Za-z</code>.</div> <div class="note">Salt characters limited to <code>./0-9A-Za-z</code>.</div>
<label for="username">Username (copyparty only)</label>
<input id="username" type="text" placeholder="Enter username">
<div class="controls"> <div class="controls">
<button id="hashBtn">Compute hash</button> <button id="hashBtn">Compute hash</button>
@@ -37,8 +41,9 @@
</div> </div>
<label for="result">Result</label> <label>Result</label>
<textarea id="result" rows="4" readonly placeholder="Result will appear here"></textarea> <pre id="result" class="result-box">Result will appear here</pre>
<button id="resultBtn" type="button">Copy result</button>
</div> </div>
<script src="/static/js/main.js"></script> <script src="/static/js/main.js"></script>