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: {