diff --git a/shine-UI/js/app.js b/shine-UI/js/app.js index 3d0348c..ed368f8 100644 --- a/shine-UI/js/app.js +++ b/shine-UI/js/app.js @@ -34,8 +34,8 @@ import { import * as startView from './pages/start-view.js?v=202606142105'; import * as entrySettingsView from './pages/entry-settings-view.js?v=202606161240'; -import * as registerView from './pages/register-view.js'; -import * as registrationFaqView from './pages/registration-faq-view.js'; +import * as registerView from './pages/register-view.js?v=202606201650'; +import * as registrationFaqView from './pages/registration-faq-view.js?v=202606201650'; import * as registrationPaymentView from './pages/registration-payment-view.js?v=202606180940'; import * as registrationKeysView from './pages/registration-keys-view.js'; import * as registrationDraftKeysView from './pages/registration-draft-keys-view.js'; @@ -44,7 +44,7 @@ import * as devnetTopupView from './pages/devnet-topup-view.js'; import * as loginView from './pages/login-view.js?v=202606150110'; import * as loginCameraView from './pages/login-camera-view.js'; import * as loginOtherDeviceView from './pages/login-other-device-view.js?v=202606180940'; -import * as loginPasswordView from './pages/login-password-view.js'; +import * as loginPasswordView from './pages/login-password-view.js?v=202606201650'; import * as keyStorageView from './pages/key-storage-view.js'; import * as profileView from './pages/profile-view.js'; diff --git a/shine-UI/js/pages/login-password-view.js b/shine-UI/js/pages/login-password-view.js index eef0a47..c870886 100644 --- a/shine-UI/js/pages/login-password-view.js +++ b/shine-UI/js/pages/login-password-view.js @@ -53,7 +53,7 @@ function createWordsLayout({ words, onInput }) { const preview = document.createElement('p'); preview.className = 'status-line'; - section.append(grid, hint, preview); + section.append(grid, hint); return { section, inputs, preview }; } @@ -119,24 +119,13 @@ export function render({ navigate }) { hint.className = 'meta-muted'; hint.textContent = 'Введите логин. На следующем шаге сохраните ключи на устройстве.'; - const advanced = document.createElement('details'); - advanced.className = 'card stack'; - advanced.innerHTML = ` - Расширенные -

Схема деривации: логин нормализуется как trim().toLowerCase(). При непустом пароле используется Argon2id, затем из результата строится секрет.

-

Из секрета строятся root key, blockchain key и device key. Обычно можно не вникать в это подробно и просто хранить всё на своём устройстве.

-

Режим 12 слов ничего не меняет в протоколе: слова просто склеиваются в один обычный пароль длиной до 256 символов.

-

Если пароль пустой — используется прежний детерминированный режим совместимости.

-

Профиль Argon2id сейчас фиксированный: t=2, m=65536 KiB (64 MiB), p=1, dkLen=32.

- `; - const status = document.createElement('p'); status.className = 'status-line is-unavailable'; status.style.display = 'none'; - const testLoginsHint = document.createElement('p'); - testLoginsHint.className = 'meta-muted'; - testLoginsHint.textContent = 'Основные тестовые логины: 1, 2, 3 (вход без пароля).'; + let passwordField = null; + const passwordLengthText = document.createElement('p'); + passwordLengthText.className = 'status-line'; function getCurrentPassword() { return passwordMode === 'words' ? composePasswordFromWords(passwordWords) : String(passwordInput.value || ''); @@ -150,15 +139,17 @@ export function render({ navigate }) { } function updateWordsPreview() { - const password = composePasswordFromWords(passwordWords); - const nonEmptyCount = normalizePasswordWords(passwordWords).filter((word) => word.trim()).length; - wordsPreview.textContent = `Заполнено слов: ${nonEmptyCount} из 12 · итоговая длина пароля: ${password.length} символов.`; + const password = getCurrentPassword(); + const text = `Итоговая длина пароля: ${password.length} символов.`; + wordsPreview.textContent = text; + passwordLengthText.textContent = text; } function updatePasswordModeVisibility() { const wordsMode = passwordMode === 'words'; - wordsSection.hidden = !wordsMode; - passwordInput.parentElement.hidden = wordsMode; + wordsSection.style.display = wordsMode ? 'grid' : 'none'; + if (passwordField) passwordField.style.display = wordsMode ? 'none' : 'grid'; + passwordInput.style.display = wordsMode ? 'none' : ''; updateWordsPreview(); } @@ -167,8 +158,9 @@ export function render({ navigate }) { `; form.children[0].append(loginInput); - form.children[1].append(passwordInput); - form.append(passwordModeToggle, wordsSection, hint, advanced, status, testLoginsHint); + passwordField = form.children[1]; + passwordField.append(passwordInput); + form.append(passwordModeToggle, wordsSection, passwordLengthText, hint, status); updatePasswordModeVisibility(); syncDraftState(); diff --git a/shine-UI/js/pages/register-view.js b/shine-UI/js/pages/register-view.js index d808732..e86cf2f 100644 --- a/shine-UI/js/pages/register-view.js +++ b/shine-UI/js/pages/register-view.js @@ -13,7 +13,7 @@ import { PASSWORD_MAX_LENGTH, PASSWORD_WORDS_COUNT, } from '../services/password-words.js'; -import { openRegistrationFaq, REGISTRATION_FAQ_TOPICS } from './registration-faq-view.js'; +import { openRegistrationFaq } from './registration-faq-view.js'; export const pageMeta = { id: 'register-view', title: 'Зарегистрироваться', showAppChrome: false }; @@ -55,7 +55,7 @@ function createWordsLayout({ words, onInput }) { const preview = document.createElement('p'); preview.className = 'status-line'; - section.append(grid, hint, preview); + section.append(grid, hint); return { section, inputs, preview }; } @@ -137,35 +137,20 @@ export function render({ navigate }) { const faqText = document.createElement('p'); faqText.className = 'meta-muted'; - faqText.textContent = 'Нажмите на вопрос, чтобы открыть отдельный экран с кратким объяснением.'; + faqText.textContent = 'Если хотите подробнее понять схему деривации, ключи, первый сервер и формат 12 слов, откройте отдельный экран с вопросами.'; - const faqButtons = document.createElement('div'); - faqButtons.className = 'registration-faq-grid'; - REGISTRATION_FAQ_TOPICS.forEach((topic) => { - const button = document.createElement('button'); - button.className = 'ghost-btn'; - button.type = 'button'; - button.textContent = topic.shortTitle; - button.addEventListener('click', () => openRegistrationFaq(navigate, topic.id)); - faqButtons.append(button); - }); - faqCard.append(faqTitle, faqText, faqButtons); + const faqButton = document.createElement('button'); + faqButton.className = 'ghost-btn'; + faqButton.type = 'button'; + faqButton.textContent = 'Частые вопросы'; + faqButton.addEventListener('click', () => openRegistrationFaq(navigate, 'key-derivation')); + + faqCard.append(faqTitle, faqText, faqButton); const formError = document.createElement('p'); formError.className = 'status-line is-unavailable'; formError.style.display = 'none'; - const advanced = document.createElement('details'); - advanced.className = 'card stack'; - advanced.innerHTML = ` - Расширенные -

Схема деривации: логин и пароль проходят через Argon2id, после чего получается главный секрет.

-

Из этого секрета строятся три ключа: root key для основной публичной записи и важных изменений, blockchain key для подписания действий SHiNE в блокчейне, device key для входа и работы конкретного устройства.

-

Разделение нужно, чтобы можно было аккуратнее выдавать права устройствам. Но если у вас нет большой суммы на счёте и нет повышенного риска, обычно можно просто хранить всё на своём устройстве.

-

Режим 12 слов не меняет формат пароля и не меняет API: слова просто склеиваются в одну строку длиной до 256 символов.

-

Профиль Argon2id сейчас фиксированный: t=2, m=65536 KiB (64 MB), p=1, dkLen=32.

- `; - const checkButton = document.createElement('button'); checkButton.className = 'ghost-btn'; checkButton.type = 'button'; @@ -185,6 +170,9 @@ export function render({ navigate }) { nextButton.type = 'button'; nextButton.textContent = 'Далее'; + let passwordField = null; + const passwordLengthText = document.createElement('p'); + passwordLengthText.className = 'status-line'; let lastCheckedLogin = ''; let lastCheckedFree = false; let lastCheckedClassName = ''; @@ -195,15 +183,17 @@ export function render({ navigate }) { } function updateWordsPreview() { - const password = composePasswordFromWords(passwordWords); - const nonEmptyCount = normalizePasswordWords(passwordWords).filter((word) => word.trim()).length; - wordsPreview.textContent = `Заполнено слов: ${nonEmptyCount} из 12 · итоговая длина пароля: ${password.length} символов.`; + const password = getCurrentPassword(); + const text = `Итоговая длина пароля: ${password.length} символов.`; + wordsPreview.textContent = text; + passwordLengthText.textContent = text; } function updatePasswordModeVisibility() { const wordsMode = passwordMode === 'words'; - wordsSection.hidden = !wordsMode; - passwordInput.parentElement.hidden = wordsMode; + wordsSection.style.display = wordsMode ? 'grid' : 'none'; + if (passwordField) passwordField.style.display = wordsMode ? 'none' : 'grid'; + passwordInput.style.display = wordsMode ? 'none' : ''; updateWordsPreview(); } @@ -365,9 +355,11 @@ export function render({ navigate }) { `; - form.children[0].append(loginInput); - form.children[1].append(passwordInput); - form.append(passwordModeToggle, wordsSection, serverNotice, checkButton, statusText, faqCard, advanced, formError); + const loginField = form.children[0]; + passwordField = form.children[1]; + loginField.append(loginInput); + passwordField.append(passwordInput); + form.append(passwordModeToggle, wordsSection, passwordLengthText, serverNotice, checkButton, statusText, faqCard, formError); actions.innerHTML = ''; actions.append(backButton, nextButton); updatePasswordModeVisibility();