diff --git a/shine-UI/js/pages/login-password-view.js b/shine-UI/js/pages/login-password-view.js index fe0e334..c8c112e 100644 --- a/shine-UI/js/pages/login-password-view.js +++ b/shine-UI/js/pages/login-password-view.js @@ -74,7 +74,7 @@ export function render({ navigate }) { try { await authService.reconnect(state.entrySettings.shineServer); - const result = await authService.login(state.loginDraft.login, state.loginDraft.password); + const result = await authService.createSessionForExistingUser(state.loginDraft.login, state.loginDraft.password); authorizeSession(result); await refreshSessions(); setAuthInfo('Успешный вход выполнен.'); diff --git a/shine-UI/js/pages/register-view.js b/shine-UI/js/pages/register-view.js index 9211ea8..64cfa70 100644 --- a/shine-UI/js/pages/register-view.js +++ b/shine-UI/js/pages/register-view.js @@ -1,12 +1,5 @@ import { renderHeader } from '../components/header.js?v=20260327192619'; -import { - authService, - clearAuthMessages, - setAuthBusy, - setAuthError, - setAuthInfo, - state, -} from '../state.js?v=20260327192619'; +import { authService, state } from '../state.js?v=20260327192619'; export const pageMeta = { id: 'register-view', title: 'Зарегистрироваться', showAppChrome: false }; @@ -40,20 +33,6 @@ export function render({ navigate }) { checkButton.type = 'button'; checkButton.textContent = 'Проверить логин'; - const saveRootRow = document.createElement('label'); - saveRootRow.className = 'checkbox-row'; - saveRootRow.innerHTML = ` Сохранить root key`; - const saveRootInput = saveRootRow.querySelector('input'); - - const saveBchRow = document.createElement('label'); - saveBchRow.className = 'checkbox-row'; - saveBchRow.innerHTML = ` Сохранить blockchain key`; - const saveBchInput = saveBchRow.querySelector('input'); - - const saveDevRow = document.createElement('label'); - saveDevRow.className = 'checkbox-row'; - saveDevRow.innerHTML = ' device key сохраняется всегда'; - async function runAvailabilityCheck() { const login = loginInput.value.trim(); if (!login) { @@ -87,7 +66,7 @@ export function render({ navigate }) { `; form.children[0].append(loginInput); form.children[1].append(passwordInput); - form.append(checkButton, statusText, saveRootRow, saveBchRow, saveDevRow); + form.append(checkButton, statusText); const actions = document.createElement('div'); actions.className = 'auth-footer-actions'; @@ -105,49 +84,19 @@ export function render({ navigate }) { nextButton.addEventListener('click', async () => { const isFree = await runAvailabilityCheck(); if (!isFree) { - setAuthError('Выберите свободный логин'); + window.alert('Выберите свободный логин'); return; } state.registrationDraft.login = loginInput.value.trim(); state.registrationDraft.password = passwordInput.value; - state.keyStorage.saveRoot = saveRootInput.checked; - state.keyStorage.saveBlockchain = saveBchInput.checked; - state.keyStorage.saveDevice = true; if (!state.registrationDraft.password) { window.alert('Введите пароль'); return; } - setAuthBusy(true); - nextButton.disabled = true; - nextButton.textContent = 'Создание...'; - setAuthError(''); - - try { - await authService.reconnect(state.entrySettings.shineServer); - const result = await authService.register( - state.registrationDraft.login, - state.registrationDraft.password, - { - saveRoot: state.keyStorage.saveRoot, - saveBlockchain: state.keyStorage.saveBlockchain, - saveDevice: true, - }, - ); - state.registrationDraft.sessionId = result.sessionId; - state.registrationDraft.storagePwd = result.storagePwd; - setAuthInfo(`Пользователь ${result.login} зарегистрирован`); - navigate('registration-keys-view'); - } catch (error) { - setAuthError(error.message); - window.alert(error.message); - } finally { - setAuthBusy(false); - nextButton.disabled = false; - nextButton.textContent = 'Далее'; - } + navigate('registration-payment-view'); }); actions.append(backButton, nextButton); diff --git a/shine-UI/js/pages/registration-keys-view.js b/shine-UI/js/pages/registration-keys-view.js index 6afc9fe..efe9314 100644 --- a/shine-UI/js/pages/registration-keys-view.js +++ b/shine-UI/js/pages/registration-keys-view.js @@ -1,5 +1,6 @@ import { renderHeader } from '../components/header.js?v=20260327192619'; import { + authService, authorizeSession, refreshSessions, setAuthError, @@ -21,11 +22,24 @@ export function render({ navigate }) { const title = document.createElement('p'); title.className = 'auth-copy'; - title.textContent = `Поздравляю, логин ${displayLogin} зарегистрирован.`; + title.textContent = `Отлично, логин ${displayLogin} зарегистрирован.`; const question = document.createElement('p'); question.className = 'auth-copy'; - question.textContent = 'Ключи считаются из пароля (SHA-256 + суффиксы root.key/dev.key/bch.key). В IndexedDB сохраняются только выбранные ключи и всегда device key.'; + question.textContent = 'Какие ключи сохранить в зашифрованном контейнере IndexedDB?'; + + const rootToggle = document.createElement('input'); + rootToggle.type = 'checkbox'; + rootToggle.checked = state.keyStorage.saveRoot; + + const blockchainToggle = document.createElement('input'); + blockchainToggle.type = 'checkbox'; + blockchainToggle.checked = state.keyStorage.saveBlockchain; + + const deviceToggle = document.createElement('input'); + deviceToggle.type = 'checkbox'; + deviceToggle.checked = true; + deviceToggle.disabled = true; const rootRow = document.createElement('label'); rootRow.className = 'checkbox-row'; @@ -37,7 +51,7 @@ export function render({ navigate }) { const deviceRow = document.createElement('label'); deviceRow.className = 'checkbox-row'; - deviceRow.innerHTML = ' device key'; + deviceRow.append(deviceToggle, document.createTextNode('device key (всегда)')); card.append(title, question, rootRow, deviceRow, blockchainRow); @@ -56,13 +70,34 @@ export function render({ navigate }) { okButton.textContent = 'OK'; okButton.addEventListener('click', async () => { try { + if (!state.registrationDraft.pendingKeyBundle || !state.registrationDraft.pendingSessionMaterial) { + throw new Error('Сначала завершите шаг регистрации на предыдущем экране'); + } + + state.keyStorage.saveRoot = rootToggle.checked; + state.keyStorage.saveBlockchain = blockchainToggle.checked; + + await authService.persistSelectedKeys( + state.registrationDraft.login, + state.registrationDraft.storagePwd, + state.registrationDraft.pendingKeyBundle, + { + saveRoot: state.keyStorage.saveRoot, + saveBlockchain: state.keyStorage.saveBlockchain, + }, + ); + await authService.persistSessionMaterial( + state.registrationDraft.login, + state.registrationDraft.pendingSessionMaterial, + ); + authorizeSession({ login: state.registrationDraft.login, sessionId: state.registrationDraft.sessionId, storagePwd: state.registrationDraft.storagePwd, }); await refreshSessions(); - setAuthInfo('Регистрация завершена, список сессий загружен.'); + setAuthInfo('Ключи сохранены, регистрация завершена.'); navigate('profile-view'); } catch (error) { setAuthError(error.message); diff --git a/shine-UI/js/pages/registration-payment-view.js b/shine-UI/js/pages/registration-payment-view.js index fce0b1c..bd70c98 100644 --- a/shine-UI/js/pages/registration-payment-view.js +++ b/shine-UI/js/pages/registration-payment-view.js @@ -1,5 +1,11 @@ import { renderHeader } from '../components/header.js?v=20260327192619'; -import { refreshRegistrationBalance, state } from '../state.js?v=20260327192619'; +import { + authService, + refreshRegistrationBalance, + setAuthError, + setAuthInfo, + state, +} from '../state.js?v=20260327192619'; export const pageMeta = { id: 'registration-payment-view', title: 'Оплата регистрации', showAppChrome: false }; @@ -66,8 +72,28 @@ export function render({ navigate }) { submitButton.className = 'primary-btn'; submitButton.type = 'button'; submitButton.textContent = 'Зарегистрироваться'; - submitButton.addEventListener('click', () => { - navigate('registration-keys-view'); + submitButton.addEventListener('click', async () => { + try { + submitButton.disabled = true; + submitButton.textContent = 'Регистрация...'; + + await authService.reconnect(state.entrySettings.shineServer); + const result = await authService.registerUser(state.registrationDraft.login, state.registrationDraft.password); + state.registrationDraft.sessionId = result.sessionId; + state.registrationDraft.storagePwd = result.storagePwd; + state.registrationDraft.pendingKeyBundle = result.keyBundle; + state.registrationDraft.pendingSessionMaterial = result.sessionMaterial; + + setAuthInfo(`Отлично, вы зарегистрировались: ${result.login}`); + window.alert('Отлично, вы зарегистрировались'); + navigate('registration-keys-view'); + } catch (error) { + setAuthError(error.message); + window.alert(error.message); + } finally { + submitButton.disabled = false; + submitButton.textContent = 'Зарегистрироваться'; + } }); card.innerHTML = ` diff --git a/shine-UI/js/services/auth-service.js b/shine-UI/js/services/auth-service.js index 9fa82d0..b4c702f 100644 --- a/shine-UI/js/services/auth-service.js +++ b/shine-UI/js/services/auth-service.js @@ -4,16 +4,10 @@ import { exportEd25519PublicKeyB64, exportPkcs8B64, generateEd25519Pair, - importPkcs8Ed25519, randomBase64, signBase64, } from './crypto-utils.js?v=20260327192619'; -import { - loadEncryptedUserSecrets, - loadSessionMaterial, - saveEncryptedUserSecrets, - saveSessionMaterial, -} from './key-vault.js?v=20260327192619'; +import { saveEncryptedUserSecrets, saveSessionMaterial } from './key-vault.js?v=20260327192619'; const BCH_SUFFIX = '001'; @@ -63,33 +57,22 @@ export class AuthService { return payload.exists !== true; } - async register(login, password, saveOptions = { saveRoot: true, saveBlockchain: true, saveDevice: true }) { - const cleanLogin = (login || '').trim(); - if (!cleanLogin) throw new Error('Введите логин'); + async derivePasswordKeyBundle(password) { if (!password) throw new Error('Введите пароль'); - - const isFree = await this.ensureLoginFree(cleanLogin); - if (!isFree) throw new Error('Этот логин уже занят'); - - const storagePwd = randomBase64(32); - const rootPair = await deriveEd25519FromPassword(password, 'root.key'); const blockchainPair = await deriveEd25519FromPassword(password, 'bch.key'); const devicePair = await deriveEd25519FromPassword(password, 'dev.key'); + return { rootPair, blockchainPair, devicePair }; + } + + async createAuthSession(login, keyBundle) { + const cleanLogin = (login || '').trim(); + if (!cleanLogin) throw new Error('Введите логин'); const sessionPair = await generateEd25519Pair(); const sessionKeyPub = await exportEd25519PublicKeyB64(sessionPair.publicKey); const sessionKey = `ed25519/${sessionKeyPub}`; - - const addResp = await this.ws.request('AddUser', { - login: cleanLogin, - blockchainName: `${cleanLogin}-${BCH_SUFFIX}`, - solanaKey: rootPair.publicKeyB64, - blockchainKey: blockchainPair.publicKeyB64, - deviceKey: devicePair.publicKeyB64, - bchLimit: 1000000, - }); - if (addResp.status !== 200) throw opError('AddUser', addResp); + const storagePwd = randomBase64(32); const challengeResp = await this.ws.request('AuthChallenge', { login: cleanLogin }); if (challengeResp.status !== 200) throw opError('AuthChallenge', challengeResp); @@ -99,7 +82,7 @@ export class AuthService { const timeMs = Date.now(); const preimage = `AUTH_CREATE_SESSION:${cleanLogin}:${sessionKey}:${storagePwd}:${timeMs}:${authNonce}`; - const signatureB64 = await signBase64(devicePair.privateKey, preimage); + const signatureB64 = await signBase64(keyBundle.devicePair.privateKey, preimage); const createResp = await this.ws.request('CreateAuthSession', { login: cleanLogin, @@ -107,43 +90,52 @@ export class AuthService { sessionKey, timeMs, authNonce, - deviceKey: devicePair.publicKeyB64, + deviceKey: keyBundle.devicePair.publicKeyB64, signatureB64, clientInfo: makeClientInfo(), }); - if (createResp.status !== 200) throw opError('CreateAuthSession', createResp); const sessionId = createResp?.payload?.sessionId; if (!sessionId) throw new Error('CreateAuthSession: не вернулся sessionId'); - const secrets = { - deviceKey: devicePair.privatePkcs8B64, - }; - - if (saveOptions.saveRoot) { - secrets.rootKey = rootPair.privatePkcs8B64; - } - if (saveOptions.saveBlockchain) { - secrets.blockchainKey = blockchainPair.privatePkcs8B64; - } - - await saveEncryptedUserSecrets(cleanLogin, storagePwd, secrets); - - await saveSessionMaterial(cleanLogin, { - sessionId, - sessionKey, - sessionPrivPkcs8: await exportPkcs8B64(sessionPair.privateKey), - }); - return { login: cleanLogin, sessionId, storagePwd, + sessionMaterial: { + sessionId, + sessionKey, + sessionPrivPkcs8: await exportPkcs8B64(sessionPair.privateKey), + }, }; } - async login(login, password) { + async registerUser(login, password) { + const cleanLogin = (login || '').trim(); + if (!cleanLogin) throw new Error('Введите логин'); + if (!password) throw new Error('Введите пароль'); + + const isFree = await this.ensureLoginFree(cleanLogin); + if (!isFree) throw new Error('Этот логин уже занят'); + + const keyBundle = await this.derivePasswordKeyBundle(password); + + const addResp = await this.ws.request('AddUser', { + login: cleanLogin, + blockchainName: `${cleanLogin}-${BCH_SUFFIX}`, + solanaKey: keyBundle.rootPair.publicKeyB64, + blockchainKey: keyBundle.blockchainPair.publicKeyB64, + deviceKey: keyBundle.devicePair.publicKeyB64, + bchLimit: 1000000, + }); + if (addResp.status !== 200) throw opError('AddUser', addResp); + + const session = await this.createAuthSession(cleanLogin, keyBundle); + return { ...session, keyBundle }; + } + + async createSessionForExistingUser(login, password) { const cleanLogin = (login || '').trim(); if (!cleanLogin) throw new Error('Введите логин'); if (!password) throw new Error('Введите пароль'); @@ -151,45 +143,19 @@ export class AuthService { const user = await this.getUser(cleanLogin); if (!user.exists) throw new Error('Пользователь не найден'); - const sessionMaterial = await loadSessionMaterial(cleanLogin); - if (!sessionMaterial?.sessionId || !sessionMaterial?.sessionKey || !sessionMaterial?.sessionPrivPkcs8) { - throw new Error('На устройстве отсутствует ключ сессии. Нужна регистрация на этом устройстве.'); - } + const keyBundle = await this.derivePasswordKeyBundle(password); + return this.createAuthSession(cleanLogin, keyBundle); + } - await deriveEd25519FromPassword(password, 'root.key'); - await deriveEd25519FromPassword(password, 'bch.key'); - await deriveEd25519FromPassword(password, 'dev.key'); + async persistSelectedKeys(login, storagePwd, keyBundle, saveOptions = { saveRoot: true, saveBlockchain: true }) { + const secrets = { deviceKey: keyBundle.devicePair.privatePkcs8B64 }; + if (saveOptions.saveRoot) secrets.rootKey = keyBundle.rootPair.privatePkcs8B64; + if (saveOptions.saveBlockchain) secrets.blockchainKey = keyBundle.blockchainPair.privatePkcs8B64; + await saveEncryptedUserSecrets(login, storagePwd, secrets); + } - const privateKey = await importPkcs8Ed25519(sessionMaterial.sessionPrivPkcs8); - - const challengeResp = await this.ws.request('SessionChallenge', { sessionId: sessionMaterial.sessionId }); - if (challengeResp.status !== 200) throw opError('SessionChallenge', challengeResp); - const nonce = challengeResp?.payload?.nonce; - if (!nonce) throw new Error('SessionChallenge: не вернулся nonce'); - - const timeMs = Date.now(); - const preimage = `SESSION_LOGIN:${sessionMaterial.sessionId}:${timeMs}:${nonce}`; - const signatureB64 = await signBase64(privateKey, preimage); - - const loginResp = await this.ws.request('SessionLogin', { - sessionId: sessionMaterial.sessionId, - sessionKey: sessionMaterial.sessionKey, - timeMs, - signatureB64, - clientInfo: makeClientInfo(), - }); - - if (loginResp.status !== 200) throw opError('SessionLogin', loginResp); - const storagePwd = loginResp?.payload?.storagePwd; - if (!storagePwd) throw new Error('SessionLogin: не вернулся storagePwd'); - - await loadEncryptedUserSecrets(cleanLogin, storagePwd); - - return { - login: cleanLogin, - sessionId: sessionMaterial.sessionId, - storagePwd, - }; + async persistSessionMaterial(login, sessionMaterial) { + await saveSessionMaterial(login, sessionMaterial); } async listSessions() { diff --git a/shine-UI/js/state.js b/shine-UI/js/state.js index 80a3fa2..c905e60 100644 --- a/shine-UI/js/state.js +++ b/shine-UI/js/state.js @@ -37,7 +37,7 @@ export const state = { notificationsTab: 'replies', pageLabelCollapsed: false, session: { - isAuthorized: Boolean(storedSession?.isAuthorized), + isAuthorized: false, login: storedSession?.login || '', sessionId: storedSession?.sessionId || '', storagePwdInMemory: '', @@ -59,9 +59,11 @@ export const state = { password: '', sessionId: '', storagePwd: '', + pendingKeyBundle: null, + pendingSessionMaterial: null, }, loginDraft: { - login: '', + login: storedSession?.login || '', password: '', }, registrationPayment: {