Files
wifi-gps-mapper-public/cracklab/server/web/templates/method_create.html
T
2026-10-07 11:36:30 +03:00

2238 lines
54 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "base.html" %}
{% block title %}
CrackLab — Create Method
{% endblock %}
{% block content %}
<style>
.method-form {
width: 100%;
max-width: 1500px;
margin: 0 auto;
}
#method-form {
display: grid;
grid-template-columns:
minmax(320px, 0.8fr)
minmax(620px, 1.6fr);
gap: 24px;
align-items: start;
}
.method-form-section {
margin-bottom: 0;
padding: 24px;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 10px;
}
.method-form-section h2 {
margin: 0 0 20px;
font-size: 20px;
}
.method-field {
display: flex;
flex-direction: column;
gap: 7px;
}
.method-field-full {
grid-column: 1 / -1;
}
.method-field label {
color: var(--muted);
font-size: 13px;
font-weight: 700;
letter-spacing: 0.4px;
}
.method-field input,
.method-field select,
.method-field textarea {
width: 100%;
padding: 11px 12px;
border: 1px solid var(--border);
border-radius: 7px;
background: var(--terminal);
color: var(--text);
font: inherit;
outline: none;
}
.method-field input:focus,
.method-field select:focus,
.method-field textarea:focus {
border-color: var(--accent);
}
.method-field textarea {
min-height: 100px;
resize: vertical;
}
.method-help {
margin-top: 4px;
color: var(--muted);
font-size: 12px;
line-height: 1.5;
}
.method-step {
margin-bottom: 18px;
padding: 20px;
background: var(--surface-light);
border: 1px solid var(--border);
border-radius: 9px;
}
.method-step:last-child {
margin-bottom: 0;
}
.method-step.method-step-bulk-hidden {
display: none;
}
.method-step-bulk-group {
margin-bottom: 18px;
padding: 20px;
background: var(--surface-light);
border: 1px solid var(--border);
border-radius: 9px;
}
.method-step-bulk-group:last-child {
margin-bottom: 0;
}
.method-step-bulk-group-title {
font-size: 17px;
font-weight: 700;
}
.method-step-bulk-group-mode {
margin-top: 4px;
color: var(--muted);
font-size: 12px;
}
.method-step-bulk-group-summary {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-top: 18px;
padding-top: 14px;
border-top: 1px solid var(--border);
}
.method-step-bulk-group-list {
display: none;
margin-top: 14px;
padding-top: 14px;
border-top: 1px solid var(--border);
}
.method-step-bulk-group.is-expanded
.method-step-bulk-group-list {
display: block;
}
.method-step-bulk-group-list ol {
margin: 0;
padding-left: 24px;
}
.method-step-bulk-group-list li {
margin-bottom: 5px;
font-family: monospace;
word-break: break-all;
}
.method-step-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 15px;
margin-bottom: 18px;
}
.method-step-title {
font-size: 17px;
font-weight: 700;
}
.method-step-mode {
margin-top: 4px;
color: var(--muted);
font-size: 12px;
}
.method-step-fields {
display: grid;
grid-template-columns:
repeat(
auto-fit,
minmax(240px, 1fr)
);
gap: 18px;
}
.method-error {
margin-bottom: 24px;
padding: 14px 16px;
border: 1px solid var(--red);
border-radius: 8px;
background: rgba(239, 98, 98, 0.08);
color: var(--red);
}
.method-actions {
grid-column: 1 / -1;
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
margin-top: 0;
}
.method-remove-step {
min-height: 32px;
padding: 0 11px;
border: 1px solid var(--border);
border-radius: 6px;
background: transparent;
color: var(--muted);
font: inherit;
font-size: 12px;
cursor: pointer;
}
.method-remove-step:hover {
color: var(--red);
border-color: var(--red);
}
.method-mode-help {
grid-column: 1 / -1;
margin-top: -2px;
padding: 15px 16px;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--terminal);
color: var(--muted);
font-size: 13px;
line-height: 1.6;
}
.method-mode-help-title {
margin-bottom: 6px;
color: var(--text);
font-weight: 700;
}
.method-mode-help code {
padding: 2px 5px;
border-radius: 4px;
background: var(--surface-light);
color: var(--text);
font-family: monospace;
}
.method-keyspace {
grid-column: 1 / -1;
padding: 12px 14px;
border: 1px solid var(--border);
border-radius: 7px;
background: var(--terminal);
color: var(--muted);
font-size: 13px;
}
.method-keyspace strong {
color: var(--text);
}
.method-steps-section {
grid-column: 2;
grid-row: 1 / span 2;
}
.method-mask-bulk {
grid-column: 1;
grid-row: 2 / span 4;
align-self: start;
padding: 20px;
border: 1px solid var(--border);
border-radius: 9px;
background: var(--surface-light);
}
.method-mask-bulk-title {
margin-bottom: 7px;
color: var(--text);
font-size: 17px;
font-weight: 700;
}
.method-mask-bulk textarea {
display: block;
width: 100%;
min-height: 250px;
padding: 11px 12px;
border: 1px solid var(--border);
border-radius: 7px;
background: var(--terminal);
color: var(--text);
font: inherit;
font-family: monospace;
outline: none;
resize: none;
white-space: pre;
box-sizing: border-box;
}
.method-mask-bulk textarea:focus {
border-color: var(--accent);
}
.method-mask-bulk-actions {
display: flex;
align-items: flex-start;
gap: 12px;
margin-top: 10px;
}
.method-step-actions {
display: flex;
align-items: center;
gap: 12px;
margin-top: 18px;
}
.method-mask-bulk {
display: none;
}
.method-mask-bulk.is-visible {
display: block;
}
.method-mask-bulk-status {
color: var(--muted);
font-size: 12px;
line-height: 1.5;
}
.method-mask-bulk-status-error {
color: var(--red);
}
.method-mask-bulk-status-success {
color: var(--accent);
}
.method-mask-bulk-status ul {
margin: 6px 0 0;
padding-left: 20px;
}
.method-mask-bulk button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
@media (max-width: 1000px) {
#method-form {
grid-template-columns: 1fr;
}
.method-actions {
grid-column: auto;
}
}
@media (max-width: 700px) {
.method-actions {
flex-direction: column;
align-items: stretch;
}
.method-step-actions {
flex-direction: column;
align-items: stretch;
}
.method-mask-bulk-actions {
flex-direction: column;
align-items: stretch;
}
}
</style>
<div class="method-form">
<div class="methods-page-header">
<div>
<h1 class="page-title">
{% if edit_mode %}
Редактирование метода
{% else %}
Создание метода
{% endif %}
</h1>
<div class="page-description">
{% if edit_mode %}
Изменения будут сохранены в текущей версии метода.
{% else %}
Новый рабочий метод будет сохранён в базе данных.
{% endif %}
</div>
</div>
</div>
{% if error %}
<div class="method-error">
{{ error }}
</div>
{% endif %}
<form
method="post"
id="method-form"
>
<section class="method-form-section">
<h2>
Основная информация
</h2>
<div class="method-form-grid">
<div class="method-field">
<label for="name">
Название
</label>
<input
id="name"
name="name"
type="text"
required
autocomplete="off"
value="{{ method.name if method else '' }}"
>
</div>
<div class="method-field">
<label for="hash_mode">
Тип хэша
</label>
<select
id="hash_mode"
name="hash_mode"
required
>
<option value="22000">
WPA-PBKDF2-PMKID+EAPOL (22000)
</option>
</select>
<div class="method-help">
Сейчас CrackLab работает с WPA/WPA2 hash mode 22000.
Дополнительные Wi-Fi hash modes можно будет добавить
позже.
</div>
</div>
<div class="method-field method-field-full">
<label for="description">
Описание
</label>
<textarea
id="description"
name="description"
placeholder="Например: имя + четыре цифры"
>{{ method.description if method and method.description else '' }}</textarea>
</div>
</div>
</section>
<section class="method-form-section method-steps-section">
<h2>
Шаги метода
</h2>
<div id="steps"></div>
<div class="method-step-actions">
<button
type="button"
class="methods-button-secondary"
id="add-step"
>
+ Добавить шаг
</button>
<button
type="button"
class="methods-button-secondary"
id="add-mask-bulk"
>
+ Добавить списком
</button>
</div>
<input
type="hidden"
id="step_count"
name="step_count"
value="1"
>
</section>
<div
class="method-mask-bulk"
id="mask-bulk"
>
<div class="method-mask-bulk-title">
Список масок
</div>
<textarea
data-mask-bulk
placeholder="?d?d?d?dwest
?u?u?u-west
?u?uwest?l?l"
spellcheck="false"
></textarea>
<div class="method-help">
Только для attack mode 3.
Одна маска на строку.
Пустые строки игнорируются.
Дубликаты удаляются после нормализации.
Минимальная длина кандидата — 8 позиций.
</div>
<div class="method-mask-bulk-actions">
<button
type="button"
class="methods-button-secondary"
data-mask-bulk-add
disabled
>
Добавить маски в шаги
</button>
<div
class="method-mask-bulk-status"
data-mask-bulk-status
></div>
</div>
</div>
<div class="method-actions">
<button
type="submit"
class="methods-button"
>
{% if edit_mode %}
Сохранить изменения
{% else %}
Сохранить метод
{% endif %}
</button>
<a
class="methods-button-secondary"
href="{{ url_for('cracklab.methods_page') }}"
>
Отмена
</a>
</div>
</form>
</div>
<script>
const editMode =
{{ "true" if edit_mode else "false" }};
const initialSteps =
{{ definition.get("steps", []) | tojson if definition else "[]" }};
const stepsContainer =
document.getElementById("steps");
const stepCountInput =
document.getElementById("step_count");
const addStepButton =
document.getElementById("add-step");
const addMaskBulkButton =
document.getElementById("add-mask-bulk");
const methodForm =
document.getElementById("method-form");
let stepCount = 0;
const modeNames = {
"0": "Dictionary",
"1": "Combination",
"3": "Mask",
"6": "Dictionary + Mask",
"7": "Mask + Dictionary",
};
const maskCharsetSizes = {
"?l": 26n,
"?u": 26n,
"?d": 10n,
"?s": 33n,
"?a": 95n,
"?b": 256n,
};
function escapeHtml(value) {
return value
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#039;");
}
function dictionaryField(
name,
label,
help,
value = "",
) {
const displayValue =
value
? value.replace(
/^wordlist:/,
"",
)
: "";
return `
<div class="method-field">
<label>
${escapeHtml(label)}
</label>
<input
name="${escapeHtml(name)}"
type="text"
placeholder="insidepro-wpa"
required
autocomplete="off"
value="${escapeHtml(displayValue)}"
>
<div class="method-help">
${escapeHtml(help)}
</div>
</div>
`;
}
function maskField(
name,
value = "",
) {
return `
<div class="method-field">
<label>
Маска
</label>
<input
name="${escapeHtml(name)}"
type="text"
placeholder="?d?d?d?d?d?d?d?d"
required
autocomplete="off"
value="${escapeHtml(value)}"
>
<div class="method-help">
Charset:
<code>?l</code>,
<code>?u</code>,
<code>?d</code>,
<code>?s</code>,
<code>?a</code>,
<code>?b</code>.
Для буквального <code>?</code> используйте
<code>??</code>.
</div>
</div>
`;
}
function maskBulkField() {
return `
<div class="method-mask-bulk">
<div class="method-mask-bulk-title">
Список масок
</div>
<textarea
data-mask-bulk
placeholder="?d?d?d?dwest
?u?u?u-west
?u?uwest?l?l"
spellcheck="false"
></textarea>
<div class="method-help">
Только для attack mode 3.
Одна маска на строку.
Пустые строки игнорируются.
Дубликаты удаляются после нормализации.
Минимальная длина кандидата — 8 позиций.
</div>
<div class="method-mask-bulk-actions">
<button
type="button"
class="methods-button-secondary"
data-mask-bulk-add
disabled
>
Добавить маски в шаги
</button>
<div
class="method-mask-bulk-status"
data-mask-bulk-status
></div>
</div>
</div>
`;
}
function modeHelp(mode) {
const helps = {
"0": `
<div class="method-mode-help-title">
Как работает словарь
</div>
Hashcat последовательно проверяет каждую строку
словаря как отдельный кандидат пароля.
Например, словарь может содержать
<code>password</code>,
<code>qwerty</code>,
<code>welcome</code>.
`,
"1": `
<div class="method-mode-help-title">
Как работает комбинация
</div>
Каждое слово левого словаря объединяется
с каждым словом правого словаря.
Например:
<code>alex</code> + <code>1234</code>
→ <code>alex1234</code>.
<br>
<strong>Порядок важен:</strong>
левый словарь образует левую часть кандидата,
правый — правую.
`,
"3": `
<div class="method-mode-help-title">
Как работает маска
</div>
Маска задаёт шаблон кандидата.
Hashcat перебирает все варианты символов,
разрешённых в каждой позиции.
<br><br>
<code>?l</code> — строчная буква a-z<br>
<code>?u</code> — заглавная буква A-Z<br>
<code>?d</code> — цифра 0-9<br>
<code>?s</code> — специальный символ<br>
<code>?a</code> — буква, цифра или специальный символ<br>
<code>?b</code> — полный байтовый charset<br>
<code>??</code> — буквальный символ <code>?</code>
<br><br>
В режиме 3 кандидат должен содержать
минимум 8 позиций.
Например:
<code>?d?d?d?dwest</code>
→ 8 позиций.
<br><br>
Текст без charset является буквальной
частью кандидата.
Например:
<code>u?u?west?l?l</code>
→ 8 позиций.
`,
"6": `
<div class="method-mode-help-title">
Как работает Dictionary + Mask
</div>
К каждому слову словаря добавляются все варианты,
соответствующие маске.
<br>
Например, слово <code>alex</code> и маска
<code>?d?d</code> дают кандидатов от
<code>alex00</code> до <code>alex99</code>.
<br><br>
<strong>Порядок:</strong>
словарь слева, маска справа.
`,
"7": `
<div class="method-mode-help-title">
Как работает Mask + Dictionary
</div>
К каждой комбинации, созданной маской,
добавляется слово из словаря справа.
<br>
Например, маска <code>?d?d</code> и слово
<code>alex</code> дают
<code>00alex</code>,
<code>01alex</code>,
<code>02alex</code>
и так далее.
<br><br>
<strong>Порядок:</strong>
маска слева, словарь справа.
`,
};
return `
<div class="method-mode-help">
${helps[mode] || ""}
</div>
`;
}
function analyzeMask(mask) {
if (!mask) {
return {
length: 0,
keyspace: 1n,
error: "Маска не должна быть пустой.",
};
}
let length = 0;
let keyspace = 1n;
for (
let index = 0;
index < mask.length;
) {
if (mask[index] !== "?") {
length += 1;
index += 1;
continue;
}
if (index + 1 >= mask.length) {
return {
length,
keyspace,
error:
"Одиночный символ '?' в позиции " +
`${index + 1}. Для буквального ? используйте ??.`,
};
}
const token =
mask.slice(
index,
index + 2,
);
if (token === "??") {
length += 1;
index += 2;
continue;
}
if (
!Object.prototype.hasOwnProperty.call(
maskCharsetSizes,
token,
)
) {
return {
length,
keyspace,
error:
`Неизвестный charset ${token} ` +
`в позиции ${index + 1}.`,
};
}
length += 1;
keyspace *=
maskCharsetSizes[token];
index += 2;
}
return {
length,
keyspace,
error: null,
};
}
function calculateMaskKeyspace(mask) {
const analysis =
analyzeMask(mask);
if (analysis.error) {
return null;
}
return analysis.keyspace;
}
function validateBulkMasks(value) {
const masks = [];
const seen = new Set();
const errors = [];
value.split(/\r?\n/).forEach(
(rawLine, lineIndex) => {
const mask =
rawLine.trim();
if (!mask) {
return;
}
const analysis =
analyzeMask(mask);
if (analysis.error) {
errors.push(
`Строка ${lineIndex + 1}: ` +
analysis.error,
);
return;
}
if (analysis.length < 8) {
errors.push(
`Строка ${lineIndex + 1}: ` +
"маска даёт только " +
`${analysis.length} позиций, ` +
"минимум — 8.",
);
return;
}
if (seen.has(mask)) {
return;
}
seen.add(mask);
masks.push(mask);
},
);
return {
masks,
errors,
};
}
function formatNumber(value) {
return new Intl.NumberFormat(
"ru-RU",
).format(value);
}
function updateMaskKeyspace(
step,
mask,
) {
const keyspace =
step.querySelector(
"[data-keyspace]",
);
if (!keyspace) {
return;
}
const maskInput =
step.querySelector(
'input[name$="_mask"]',
);
const analysis =
analyzeMask(mask);
const modeSelect =
step.querySelector(
"[data-attack-mode]",
);
const minimumLengthRequired =
modeSelect &&
modeSelect.value === "3";
let error =
analysis.error;
if (
!error &&
minimumLengthRequired &&
analysis.length < 8
) {
error =
"Маска должна давать минимум " +
`8 позиций, сейчас ${analysis.length}.`;
}
if (maskInput) {
maskInput.setCustomValidity(
error || "",
);
}
if (error) {
keyspace.innerHTML =
"<strong>Ошибка маски:</strong> " +
escapeHtml(error);
return;
}
keyspace.innerHTML =
"<strong>Длина кандидата:</strong> " +
formatNumber(
analysis.length,
) +
"<br>" +
"<strong>Количество кандидатов:</strong> " +
formatNumber(
analysis.keyspace,
);
}
function renderStepFields(
stepNumber,
mode,
stepData = null,
) {
const prefix =
`step_${stepNumber}`;
let html = "";
if (mode === "0") {
html += dictionaryField(
`${prefix}_dictionary`,
"Словарь",
"Укажите логическое имя wordlist, которое настроено на клиенте.",
stepData?.dictionary?.resource || "",
);
}
if (mode === "1") {
html += dictionaryField(
`${prefix}_dictionary_1`,
"Словарь — Left",
"Этот словарь образует левую часть кандидата.",
stepData?.dictionaries?.[0]?.resource || "",
);
html += dictionaryField(
`${prefix}_dictionary_2`,
"Словарь — Right",
"Этот словарь образует правую часть кандидата.",
stepData?.dictionaries?.[1]?.resource || "",
);
}
if (mode === "3") {
html += maskField(
`${prefix}_mask`,
stepData?.mask || "",
);
html += `
<div
class="method-keyspace"
data-keyspace
>
<strong>Длина кандидата:</strong>
укажите маску
</div>
`;
}
if (mode === "6") {
html += dictionaryField(
`${prefix}_dictionary`,
"Словарь — Left",
"Словарь образует левую часть кандидата.",
stepData?.dictionary?.resource || "",
);
html += maskField(
`${prefix}_mask`,
stepData?.mask || "",
);
html += `
<div
class="method-keyspace"
data-keyspace
>
<strong>Количество кандидатов:</strong>
укажите маску
</div>
`;
}
if (mode === "7") {
html += maskField(
`${prefix}_mask`,
stepData?.mask || "",
);
html += `
<div
class="method-keyspace"
data-keyspace
>
<strong>Количество кандидатов:</strong>
укажите маску
</div>
`;
html += dictionaryField(
`${prefix}_dictionary`,
"Словарь — Right",
"Словарь образует правую часть кандидата.",
stepData?.dictionary?.resource || "",
);
}
return html;
}
function bindMaskInput(step) {
const mask =
step.querySelector(
'input[name$="_mask"]',
);
if (!mask) {
return;
}
mask.addEventListener(
"input",
() => {
updateMaskKeyspace(
step,
mask.value,
);
updateMaskBulkVisibility();
},
);
updateMaskKeyspace(
step,
mask.value,
);
}
function bindMaskBulk() {
const bulk =
document.getElementById("mask-bulk");
const textarea =
bulk.querySelector(
"[data-mask-bulk]",
);
const button =
bulk.querySelector(
"[data-mask-bulk-add]",
);
const status =
bulk.querySelector(
"[data-mask-bulk-status]",
);
const refresh = () => {
const result =
validateBulkMasks(
textarea.value,
);
button.disabled =
result.errors.length > 0 ||
result.masks.length === 0;
if (result.errors.length > 0) {
status.className =
"method-mask-bulk-status " +
"method-mask-bulk-status-error";
status.innerHTML =
"<ul>" +
result.errors
.map(
(error) =>
`<li>${escapeHtml(error)}</li>`,
)
.join("") +
"</ul>";
return;
}
if (result.masks.length > 0) {
status.className =
"method-mask-bulk-status " +
"method-mask-bulk-status-success";
status.textContent =
`Готово: ${result.masks.length} ` +
"уникальных масок.";
return;
}
status.className =
"method-mask-bulk-status";
status.textContent =
"Введите по одной маске на строку.";
};
textarea.addEventListener(
"input",
refresh,
);
button.addEventListener(
"click",
() => {
const result =
validateBulkMasks(
textarea.value,
);
if (
result.errors.length > 0 ||
result.masks.length === 0
) {
refresh();
return;
}
addBulkMasks(
result.masks,
);
textarea.value = "";
button.disabled = true;
status.className =
"method-mask-bulk-status " +
"method-mask-bulk-status-success";
status.textContent =
`Добавлено: ${result.masks.length} ` +
"масок.";
},
);
refresh();
}
function getMethodSteps() {
return Array.from(
stepsContainer.querySelectorAll(
"[data-method-step]",
),
);
}
function getBulkMaskSteps() {
return getMethodSteps().filter(
(step) =>
step.dataset.bulkMaskStep ===
"true",
);
}
function getStepMask(step) {
const maskInput =
step.querySelector(
'input[name$="_mask"]',
);
return maskInput
? maskInput.value.trim()
: "";
}
function getStepWorkloadProfile(step) {
const workload =
step.querySelector(
"[data-workload-profile]",
);
return workload
? workload.value
: "2";
}
function setStepWorkloadProfile(
step,
value,
) {
const workload =
step.querySelector(
"[data-workload-profile]",
);
if (workload) {
workload.value =
String(value || "2");
}
}
function refreshBulkMaskGroup() {
const group =
document.querySelector(
"[data-mask-bulk-group]",
);
const bulkSteps =
getBulkMaskSteps();
if (!group || bulkSteps.length === 0) {
if (group) {
group.remove();
}
return;
}
const firstStep =
bulkSteps[0];
const title =
group.querySelector(
"[data-mask-bulk-group-title]",
);
const count =
group.querySelector(
"[data-mask-bulk-group-count]",
);
const list =
group.querySelector(
"[data-mask-bulk-group-list]",
);
const toggle =
group.querySelector(
"[data-mask-bulk-group-toggle]",
);
const firstStepTitle =
firstStep.querySelector(
".method-step-title",
);
const stepNumber =
firstStepTitle
? firstStepTitle.textContent
.replace(
/^Step\s+/i,
"",
)
.trim()
: "1";
title.textContent =
`Step ${stepNumber} — Mask`;
count.textContent =
`Маски: ${bulkSteps.length}`;
list.innerHTML = "";
bulkSteps.forEach(
(step) => {
const item =
document.createElement("li");
item.textContent =
getStepMask(step);
list.appendChild(item);
},
);
toggle.textContent =
group.classList.contains(
"is-expanded",
)
? "Скрыть список"
: "Показать список";
}
function createBulkMaskGroup(
bulkSteps,
) {
const existing =
document.querySelector(
"[data-mask-bulk-group]",
);
if (existing) {
existing.remove();
}
if (bulkSteps.length === 0) {
return;
}
const group =
document.createElement("div");
group.className =
"method-step-bulk-group";
group.dataset.maskBulkGroup =
"true";
group.innerHTML = `
<div
class="method-step-bulk-group-title"
data-mask-bulk-group-title
></div>
<div
class="method-step-bulk-group-mode"
>
Mode 3
</div>
<div
class="method-step-bulk-group-summary"
>
<strong
data-mask-bulk-group-count
></strong>
<button
type="button"
class="methods-button-secondary"
data-mask-bulk-group-toggle
>
Показать список
</button>
</div>
<div
class="method-step-bulk-group-list"
data-mask-bulk-group-list
>
<ol></ol>
</div>
`;
group.querySelector(
"[data-mask-bulk-group-toggle]",
).addEventListener(
"click",
() => {
group.classList.toggle(
"is-expanded",
);
refreshBulkMaskGroup();
},
);
stepsContainer.insertBefore(
group,
bulkSteps[0],
);
bulkSteps.forEach(
(step) => {
step.dataset.bulkMaskStep =
"true";
step.classList.add(
"method-step-bulk-hidden",
);
},
);
refreshBulkMaskGroup();
}
function updateMaskBulkVisibility() {
const bulk =
document.getElementById("mask-bulk");
const bulkSteps =
getBulkMaskSteps();
if (bulkSteps.length > 0) {
addMaskBulkButton.disabled =
false;
return;
}
const mode3Steps =
getMethodSteps().filter(
(step) => {
const modeSelect =
step.querySelector(
"[data-attack-mode]",
);
return (
modeSelect &&
modeSelect.value === "3"
);
},
);
const manualMode3Steps =
mode3Steps.filter(
(step) =>
getStepMask(step) !== "",
);
if (manualMode3Steps.length > 0) {
addMaskBulkButton.disabled =
true;
bulk.classList.remove(
"is-visible",
);
return;
}
addMaskBulkButton.disabled =
mode3Steps.length === 0;
if (mode3Steps.length === 0) {
bulk.classList.remove(
"is-visible",
);
}
}
function addBulkMasks(
masks,
) {
const methodSteps =
getMethodSteps();
const sourceStep =
methodSteps.find(
(step) => {
const modeSelect =
step.querySelector(
"[data-attack-mode]",
);
return (
modeSelect &&
modeSelect.value === "3"
);
},
);
if (!sourceStep) {
return;
}
const workloadProfile =
getStepWorkloadProfile(
sourceStep,
);
const oldBulkSteps =
getBulkMaskSteps();
oldBulkSteps.forEach(
(step) => {
step.remove();
},
);
const firstStep =
sourceStep;
const bulkSteps = [
firstStep,
];
while (
bulkSteps.length <
masks.length
) {
bulkSteps.push(
addStep(),
);
}
masks.forEach(
(mask, index) => {
const step =
bulkSteps[index];
const modeSelect =
step.querySelector(
"[data-attack-mode]",
);
modeSelect.value = "3";
modeSelect.dispatchEvent(
new Event("change"),
);
setStepWorkloadProfile(
step,
workloadProfile,
);
const maskInput =
step.querySelector(
'input[name$="_mask"]',
);
maskInput.value =
mask;
maskInput.dispatchEvent(
new Event(
"input",
{ bubbles: true },
),
);
step.dataset.bulkMaskStep =
"true";
step.classList.add(
"method-step-bulk-hidden",
);
},
);
stepCountInput.value =
stepCount;
rebuildStepNumbers();
createBulkMaskGroup(
bulkSteps,
);
updateMaskBulkVisibility();
}
function addStep(
initialStep = null,
) {
stepCount += 1;
const currentStepNumber =
stepCount;
const step =
document.createElement("div");
step.className =
"method-step";
step.dataset.methodStep =
"true";
step.dataset.step =
currentStepNumber;
step.innerHTML = `
<div class="method-step-header">
<div>
<div class="method-step-title">
Step ${currentStepNumber}
</div>
<div
class="method-step-mode"
data-mode-label
>
Attack mode
</div>
</div>
<button
type="button"
class="method-remove-step"
data-remove-step
>
Удалить
</button>
</div>
<div class="method-step-fields">
<div class="method-field">
<label>
Режим атаки
</label>
<select
name="step_${currentStepNumber}_attack_mode"
data-attack-mode
required
>
<option value="">
Выберите режим
</option>
<option value="0">
0 — Dictionary
</option>
<option value="1">
1 — Combination
</option>
<option value="3">
3 — Mask
</option>
<option value="6">
6 — Dictionary + Mask
</option>
<option value="7">
7 — Mask + Dictionary
</option>
</select>
</div>
<div class="method-field">
<label>
Профиль нагрузки
</label>
<select
name="step_${currentStepNumber}_workload_profile"
data-workload-profile
>
<option value="1">
1 — Low
</option>
<option
value="2"
selected
>
2 — Default
</option>
<option value="3">
3 — High
</option>
<option value="4">
4 — Nightmare
</option>
</select>
<div class="method-help">
Определяет, насколько агрессивно Hashcat
использует ресурсы компьютера.
</div>
</div>
<div
class="method-field-full"
data-step-fields
></div>
</div>
`;
stepsContainer.appendChild(
step,
);
const modeSelect =
step.querySelector(
"[data-attack-mode]",
);
const fields =
step.querySelector(
"[data-step-fields]",
);
const modeLabel =
step.querySelector(
"[data-mode-label]",
);
function updateStep(
initialData = null,
) {
const mode =
modeSelect.value;
const number =
Number(
step.dataset.step,
);
fields.innerHTML =
renderStepFields(
number,
mode,
initialData,
);
modeLabel.textContent =
mode
? `${modeNames[mode]} (mode ${mode})`
: "Attack mode";
if (mode) {
fields.insertAdjacentHTML(
"beforeend",
modeHelp(mode),
);
}
bindMaskInput(step);
updateMaskBulkVisibility();
}
modeSelect.addEventListener(
"change",
() => {
updateStep();
},
);
step.querySelector(
"[data-remove-step]",
).addEventListener(
"click",
() => {
if (
getMethodSteps().length <= 1
) {
return;
}
step.remove();
rebuildStepNumbers();
updateMaskBulkVisibility();
},
);
if (initialStep) {
modeSelect.value =
String(
initialStep.attack_mode,
);
const workloadProfile =
step.querySelector(
"[data-workload-profile]",
);
if (
workloadProfile &&
initialStep.workload_profile
) {
workloadProfile.value =
String(
initialStep.workload_profile,
);
}
}
updateStep(
initialStep,
);
stepCountInput.value =
stepCount;
return step;
}
function rebuildStepNumbers() {
const stepElements =
getMethodSteps();
stepCount =
stepElements.length;
stepElements.forEach(
(step, index) => {
const number =
index + 1;
step.dataset.step =
number;
const title =
step.querySelector(
".method-step-title",
);
if (title) {
title.textContent =
`Step ${number}`;
}
const modeSelect =
step.querySelector(
"[data-attack-mode]",
);
if (modeSelect) {
modeSelect.name =
`step_${number}_attack_mode`;
}
const workload =
step.querySelector(
"[data-workload-profile]",
);
if (workload) {
workload.name =
`step_${number}_workload_profile`;
}
const namedFields =
step.querySelectorAll(
"[data-step-fields] [name]",
);
namedFields.forEach(
(field) => {
field.name =
field.name.replace(
/^step_\d+_/,
`step_${number}_`,
);
},
);
},
);
refreshBulkMaskGroup();
stepCountInput.value =
stepCount;
}
methodForm.addEventListener(
"submit",
(event) => {
const maskInputs =
stepsContainer.querySelectorAll(
'input[name$="_mask"]',
);
let firstInvalidMask = null;
maskInputs.forEach(
(maskInput) => {
const step =
maskInput.closest(
".method-step",
);
const analysis =
analyzeMask(
maskInput.value.trim(),
);
const modeSelect =
step.querySelector(
"[data-attack-mode]",
);
const mode =
modeSelect
? modeSelect.value
: "";
let error =
analysis.error;
if (
!error &&
mode === "3" &&
analysis.length < 8
) {
error =
"Маска должна давать минимум " +
`8 позиций, сейчас ${analysis.length}.`;
}
maskInput.setCustomValidity(
error || "",
);
if (
error &&
!firstInvalidMask
) {
firstInvalidMask =
maskInput;
}
},
);
if (firstInvalidMask) {
event.preventDefault();
firstInvalidMask.reportValidity();
}
},
);
addMaskBulkButton.addEventListener(
"click",
() => {
if (addMaskBulkButton.disabled) {
return;
}
const bulk =
document.getElementById(
"mask-bulk",
);
const textarea =
bulk.querySelector(
"[data-mask-bulk]",
);
const bulkSteps =
getBulkMaskSteps();
if (bulkSteps.length > 0) {
textarea.value =
bulkSteps
.map(
(step) =>
getStepMask(step),
)
.join("\n");
textarea.dispatchEvent(
new Event(
"input",
{ bubbles: true },
),
);
}
bulk.classList.add(
"is-visible",
);
textarea.focus();
},
);
addStepButton.addEventListener(
"click",
() => {
addStep();
},
);
bindMaskBulk();
if (initialSteps.length > 0) {
initialSteps.forEach(
(step) => {
addStep(step);
},
);
} else {
addStep();
}
const initialMode3Steps =
getMethodSteps().filter(
(step) => {
const modeSelect =
step.querySelector(
"[data-attack-mode]",
);
return (
modeSelect &&
modeSelect.value === "3" &&
getStepMask(step) !== ""
);
},
);
if (initialMode3Steps.length > 1) {
createBulkMaskGroup(
initialMode3Steps,
);
}
updateMaskBulkVisibility();
</script>
{% endblock %}