Compare commits
12 Commits
90641f10cc
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b49c7737c0 | ||
| 072181b459 | |||
|
|
62c8da94c9 | ||
|
|
c7808614b6 | ||
|
|
ced732876f | ||
|
|
8338bc3b73 | ||
|
|
3e6a8882d7 | ||
|
|
de40a534df | ||
|
|
2c71f4f68b | ||
| d9d93d8522 | |||
| cf1ceae94c | |||
| af7fa0b87e |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,3 +1,4 @@
|
|||||||
venv/
|
venv/
|
||||||
|
.venv/
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.pyc
|
*.pyc
|
||||||
|
|||||||
50
app.py
50
app.py
@@ -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)
|
||||||
|
|
||||||
|
|||||||
BIN
requirements.txt
BIN
requirements.txt
Binary file not shown.
@@ -1,65 +1,73 @@
|
|||||||
body {
|
body {
|
||||||
font-family: system-ui, Segoe UI, Roboto, Arial;
|
font-family: system-ui, Segoe UI, Roboto, Arial;
|
||||||
max-width: 800px;
|
max-width: 800px;
|
||||||
margin: 40px auto;
|
margin: 40px auto;
|
||||||
padding: 0 16px;
|
padding: 0 16px;
|
||||||
background: #494949;
|
background: #494949;
|
||||||
color: #ececec;
|
color: #ececec;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
padding: 12px
|
padding: 12px
|
||||||
}
|
}
|
||||||
|
|
||||||
label {
|
label {
|
||||||
display: block;
|
display: block;
|
||||||
margin-top: 12px
|
margin-top: 12px
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type=text],
|
input[type=text],
|
||||||
input[type=password],
|
input[type=password],
|
||||||
textarea,
|
textarea,
|
||||||
select {
|
select {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
margin-top: 6px;
|
margin-top: 6px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
border: 1px solid #ececec
|
border: 1px solid #ececec
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
border: 0;
|
border: 0;
|
||||||
background: #1f7be0;
|
background: #1f7be0;
|
||||||
color: white
|
color: white
|
||||||
}
|
}
|
||||||
|
|
||||||
button:hover {
|
button:hover {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.row {
|
button:disabled,
|
||||||
display: flex;
|
input:disabled {
|
||||||
gap: 8px
|
background: #666666;
|
||||||
}
|
color: #aaaaaa;
|
||||||
|
cursor: not-allowed;
|
||||||
.row>* {
|
filter: grayscale(1);
|
||||||
flex: 1
|
}
|
||||||
}
|
|
||||||
|
.row {
|
||||||
.small {
|
display: flex;
|
||||||
width: 140px
|
gap: 8px
|
||||||
}
|
}
|
||||||
|
|
||||||
.note {
|
.row>* {
|
||||||
font-size: 0.9em;
|
flex: 1
|
||||||
color: #ffffff
|
}
|
||||||
}
|
|
||||||
|
.small {
|
||||||
.controls {
|
width: 140px
|
||||||
display: flex;
|
}
|
||||||
gap: 8px;
|
|
||||||
margin-top: 12px
|
.note {
|
||||||
}
|
font-size: 0.9em;
|
||||||
|
color: #ffffff
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 12px
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 (s.length < MIN_SALT_LEN || s.length > MAX_SALT_LEN) {
|
if (alg !== 'argon2_copyparty') {
|
||||||
alert('Salt must be between ' + MIN_SALT_LEN + ' and ' + MAX_SALT_LEN + ' characters');
|
if (s.length < MIN_SALT_LEN || s.length > MAX_SALT_LEN) {
|
||||||
return;
|
alert('Salt must be between ' + MIN_SALT_LEN + ' and ' + MAX_SALT_LEN + ' characters');
|
||||||
|
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
62
static/js/main.js.save
Normal 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);
|
||||||
|
});
|
||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user