SHA256
Обновление архитектуры ключей и кошельков
This commit is contained in:
@@ -58,7 +58,6 @@ import * as entrySettingsView from './pages/entry-settings-view.js?v=20260926090
|
||||
import * as registerView from './pages/register-view.js?v=202609260900';
|
||||
import * as registrationFaqView from './pages/registration-faq-view.js?v=202609260900';
|
||||
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';
|
||||
import * as topupView from './pages/topup-view.js';
|
||||
import * as devnetTopupView from './pages/devnet-topup-view.js';
|
||||
@@ -66,7 +65,6 @@ import * as loginView from './pages/login-view.js?v=202609260900';
|
||||
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?v=202606201650';
|
||||
import * as keyStorageView from './pages/key-storage-view.js';
|
||||
import * as publicSupportQueueView from './pages/public-support-queue-view.js';
|
||||
|
||||
import * as profileView from './pages/profile-view.js?v=202609260900';
|
||||
@@ -130,7 +128,6 @@ const routes = {
|
||||
'register-view': registerView,
|
||||
'registration-faq-view': registrationFaqView,
|
||||
'registration-payment-view': registrationPaymentView,
|
||||
'registration-keys-view': registrationKeysView,
|
||||
'registration-draft-keys-view': registrationDraftKeysView,
|
||||
'topup-view': topupView,
|
||||
'devnet-topup-view': devnetTopupView,
|
||||
@@ -138,7 +135,6 @@ const routes = {
|
||||
'login-camera-view': loginCameraView,
|
||||
'login-other-device-view': loginOtherDeviceView,
|
||||
'login-password-view': loginPasswordView,
|
||||
'key-storage-view': keyStorageView,
|
||||
queue: publicSupportQueueView,
|
||||
'profile-view': profileView,
|
||||
'profile-edit-view': profileEditView,
|
||||
|
||||
@@ -21,7 +21,6 @@ import {
|
||||
|
||||
const HISTORY_KEY = 'shine-ui-arweave-attachment-history-v1';
|
||||
const LAST_WALLET_KEY_PREFIX = 'shine-ui-arweave-last-wallet-v1:';
|
||||
const LAST_TURBO_KEY_SOURCE_PREFIX = 'shine-ui-turbo-last-key-source-v1:';
|
||||
const MAX_HISTORY_ITEMS = 100;
|
||||
const RECENT_UPLOAD_MS = 20 * 60 * 1000;
|
||||
|
||||
@@ -171,10 +170,6 @@ function lastWalletStorageKey(login) {
|
||||
return `${LAST_WALLET_KEY_PREFIX}${String(login || '').trim().toLowerCase() || 'anonymous'}`;
|
||||
}
|
||||
|
||||
function lastTurboKeySourceStorageKey(login) {
|
||||
return `${LAST_TURBO_KEY_SOURCE_PREFIX}${String(login || '').trim().toLowerCase() || 'anonymous'}`;
|
||||
}
|
||||
|
||||
function readLastWalletId(login) {
|
||||
try {
|
||||
return sessionStorage.getItem(lastWalletStorageKey(login)) || '';
|
||||
@@ -192,23 +187,6 @@ function writeLastWalletId(login, walletId) {
|
||||
}
|
||||
}
|
||||
|
||||
function readLastTurboKeySource(login, fallback = 'client') {
|
||||
try {
|
||||
return sessionStorage.getItem(lastTurboKeySourceStorageKey(login)) || String(fallback || 'client');
|
||||
} catch {
|
||||
return String(fallback || 'client');
|
||||
}
|
||||
}
|
||||
|
||||
function writeLastTurboKeySource(login, keySource) {
|
||||
try {
|
||||
const clean = String(keySource || '').trim() || 'client';
|
||||
sessionStorage.setItem(lastTurboKeySourceStorageKey(login), clean);
|
||||
} catch {
|
||||
// ignore sessionStorage errors
|
||||
}
|
||||
}
|
||||
|
||||
async function resizeImageFileToAvatar(file) {
|
||||
const source = file instanceof File ? file : null;
|
||||
if (!source) throw new Error('Выберите изображение.');
|
||||
@@ -315,8 +293,8 @@ export function openArweaveAttachmentManager({
|
||||
let closed = false;
|
||||
let wallets = [];
|
||||
let turboKeyChoices = [];
|
||||
let selectedWalletId = readLastWalletId(cleanLogin) || 'derived-client-key';
|
||||
let selectedTurboKeySource = readLastTurboKeySource(cleanLogin, turboKeySource);
|
||||
let selectedWalletId = readLastWalletId(cleanLogin) || 'derived-blockchain-key';
|
||||
const selectedTurboKeySource = 'blockchain';
|
||||
let selectedUploadTransport = String(uploadTransport || '').trim().toLowerCase() === 'arweave' ? 'arweave' : 'turbo';
|
||||
let selectedFile = null;
|
||||
let selectedSha256 = '';
|
||||
@@ -394,7 +372,7 @@ export function openArweaveAttachmentManager({
|
||||
try {
|
||||
wallets = await getArweaveWalletChoices({ login: cleanLogin, storagePwd: cleanStoragePwd });
|
||||
if (!wallets.some((item) => String(item.id) === String(selectedWalletId))) {
|
||||
selectedWalletId = String(wallets[0]?.id || 'derived-client-key');
|
||||
selectedWalletId = String(wallets[0]?.id || 'derived-blockchain-key');
|
||||
}
|
||||
writeLastWalletId(cleanLogin, selectedWalletId);
|
||||
} catch (error) {
|
||||
@@ -408,12 +386,11 @@ export function openArweaveAttachmentManager({
|
||||
login: cleanLogin,
|
||||
storagePwd: cleanStoragePwd,
|
||||
});
|
||||
if (!turboKeyChoices.some((item) => String(item.keySource) === String(selectedTurboKeySource))) {
|
||||
selectedTurboKeySource = String(turboKeyChoices[0]?.keySource || 'blockchain');
|
||||
if (!turboKeyChoices.some((item) => String(item.keySource) === 'blockchain')) {
|
||||
throw new Error('На устройстве нет ключа блокчейна.');
|
||||
}
|
||||
writeLastTurboKeySource(cleanLogin, selectedTurboKeySource);
|
||||
} catch (error) {
|
||||
setText(errorEl, error?.message || 'Не удалось загрузить Solana-ключи для Turbo.');
|
||||
setText(errorEl, error?.message || 'Не удалось загрузить ключ блокчейна для Turbo.');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -633,8 +610,7 @@ export function openArweaveAttachmentManager({
|
||||
? `
|
||||
<p class="ar-attachment-note">Маленькие файлы и аватары через Turbo пока загружаются бесплатно. Для больших файлов пополните Turbo со своего SOL-кошелька.</p>
|
||||
<div class="form-field">
|
||||
<label for="turbo-key-source">Подписывать и пополнять через</label>
|
||||
<select class="select ar-attachment-wallet-select" id="turbo-key-source"></select>
|
||||
<span class="meta-muted">Подпись и пополнение: ключ блокчейна пользователя.</span>
|
||||
</div>
|
||||
`
|
||||
: `
|
||||
@@ -671,7 +647,6 @@ export function openArweaveAttachmentManager({
|
||||
`;
|
||||
bindBackdrop();
|
||||
const walletEl = root.querySelector('#ar-attach-wallet');
|
||||
const turboKeyEl = root.querySelector('#turbo-key-source');
|
||||
const fileEl = root.querySelector('#ar-attach-file');
|
||||
const mainMetaEl = root.querySelector('[data-meta-main="true"]');
|
||||
const previewMetaEl = root.querySelector('[data-meta-preview="true"]');
|
||||
@@ -705,17 +680,6 @@ export function openArweaveAttachmentManager({
|
||||
|
||||
if (turboMode) {
|
||||
await loadTurboKeyChoices(errorEl);
|
||||
if (turboKeyEl) {
|
||||
turboKeyEl.innerHTML = turboKeyChoices.map((item) => (
|
||||
`<option value="${escapeHtml(item.keySource)}">${escapeHtml(item.label)} · ${escapeHtml(shortAddress(item.address))}</option>`
|
||||
)).join('');
|
||||
turboKeyEl.value = selectedTurboKeySource;
|
||||
turboKeyEl.addEventListener('change', async () => {
|
||||
selectedTurboKeySource = String(turboKeyEl.value || 'client');
|
||||
writeLastTurboKeySource(cleanLogin, selectedTurboKeySource);
|
||||
await refreshTurboStateForCurrentFile(mainMetaEl, previewMetaEl, errorEl, uploadBtn);
|
||||
});
|
||||
}
|
||||
await refreshTurboStateForCurrentFile(mainMetaEl, previewMetaEl, errorEl, uploadBtn);
|
||||
} else {
|
||||
await loadWallets(errorEl);
|
||||
|
||||
@@ -174,17 +174,7 @@ function createPasswordModal() {
|
||||
<span class="field-label">Пароль аккаунта</span>
|
||||
<input class="input" id="access-servers-password-input" type="password" autocomplete="current-password" placeholder="Введите пароль" />
|
||||
</label>
|
||||
<div class="stack" style="gap:0.45rem;">
|
||||
<label class="checkbox-row">
|
||||
<input type="radio" name="access-servers-key-mode" value="once" checked />
|
||||
<span>Использовать blockchain key только сейчас</span>
|
||||
</label>
|
||||
<label class="checkbox-row">
|
||||
<input type="radio" name="access-servers-key-mode" value="save" />
|
||||
<span>Сохранить blockchain key на этом устройстве</span>
|
||||
</label>
|
||||
</div>
|
||||
<p class="meta-muted">client key при необходимости тоже будет временно восстановлен из пароля. Если выбрать сохранение, blockchain key попадёт в зашифрованный контейнер устройства.</p>
|
||||
<p class="meta-muted">После проверки пароля рабочие ключи будут восстановлены и сохранены на этом устройстве. Приватный ключ восстановления не сохраняется.</p>
|
||||
<div class="form-actions-grid">
|
||||
<button class="secondary-btn" type="button" id="access-servers-password-cancel">Отмена</button>
|
||||
<button class="primary-btn" type="button" id="access-servers-password-confirm">Продолжить</button>
|
||||
@@ -244,11 +234,7 @@ function createPasswordModal() {
|
||||
inputEl.focus();
|
||||
return;
|
||||
}
|
||||
const mode = root.querySelector('input[name="access-servers-key-mode"]:checked');
|
||||
close({
|
||||
password,
|
||||
saveBlockchain: mode instanceof HTMLInputElement && mode.value === 'save',
|
||||
});
|
||||
close({ password });
|
||||
});
|
||||
window.setTimeout(() => inputEl.focus(), 0);
|
||||
});
|
||||
@@ -494,10 +480,10 @@ export function render({navigate, chrome}) {
|
||||
|
||||
const passwordResult = await passwordModal?.open({
|
||||
title: 'Нужен пароль для обновления серверов доступа',
|
||||
text: 'Чтобы изменить сервер доступа, нужно подписать обновление PDA текущим blockchain key. Этот ключ не найден в локальном зашифрованном контейнере устройства.',
|
||||
text: 'Чтобы изменить сервер доступа, нужно подписать обновление PDA текущим blockchain key. Рабочие ключи не найдены в локальном зашифрованном контейнере устройства.',
|
||||
note: savedClient
|
||||
? 'Ключ устройства уже сохранён на устройстве. Из пароля будет восстановлен blockchain key.'
|
||||
: 'На устройстве не хватает blockchain key и/или ключа устройства. Они будут восстановлены из пароля аккаунта.',
|
||||
? 'Ключ доступа уже сохранён. Из пароля будет восстановлен ключ блокчейна.'
|
||||
: 'На устройстве не хватает ключа блокчейна и/или ключа доступа. Они будут восстановлены из пароля аккаунта.',
|
||||
});
|
||||
if (!passwordResult) {
|
||||
throw new Error('Операция отменена пользователем.');
|
||||
@@ -514,12 +500,7 @@ export function render({navigate, chrome}) {
|
||||
throw new Error('Пароль не подходит: ключ устройства не совпал с записью аккаунта.');
|
||||
}
|
||||
|
||||
if (passwordResult.saveBlockchain) {
|
||||
await authService.persistSelectedKeys(sessionLogin, storagePwd, keyBundle, {
|
||||
saveRoot: false,
|
||||
saveBlockchain: true,
|
||||
});
|
||||
}
|
||||
await authService.persistSelectedKeys(sessionLogin, storagePwd, keyBundle);
|
||||
|
||||
return {
|
||||
blockchainPrivatePkcs8B64: keyBundle.blockchainPair.privatePkcs8B64,
|
||||
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
getBalanceSol,
|
||||
getSolanaWalletFromStoredSecret,
|
||||
getWalletFromStoredBlockchainKey,
|
||||
getWalletFromStoredRootKey,
|
||||
solanaAddressFromPublicKeyBase64,
|
||||
transferSol,
|
||||
} from '../services/solana-wallet-service.js';
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
getBalanceSol,
|
||||
getSolanaWalletFromStoredSecret,
|
||||
getWalletFromStoredBlockchainKey,
|
||||
getWalletFromStoredRootKey,
|
||||
solanaAddressFromPublicKeyBase64,
|
||||
transferSol,
|
||||
} from '../services/solana-wallet-service.js';
|
||||
|
||||
@@ -9,17 +9,18 @@ export function render({navigate, chrome}) {
|
||||
screen.className = 'stack';
|
||||
|
||||
chrome?.setTopbar(createTopBar({
|
||||
title: 'Подключить устройство',
|
||||
back: { label: '←', onClick: () => navigate('device-view') },
|
||||
}));
|
||||
title: 'Подключить устройство',
|
||||
back: { label: '←', onClick: () => navigate('device-view') },
|
||||
}));
|
||||
|
||||
const card = document.createElement('div');
|
||||
card.className = 'card stack';
|
||||
card.innerHTML = `
|
||||
<p>Выберите, какие ключи передать на подключаемое устройство</p>
|
||||
<label class="checkbox-row"><input type="checkbox" id="connect-root" ${state.deviceConnect.root ? 'checked' : ''} /> root key</label>
|
||||
<label class="checkbox-row"><input type="checkbox" id="connect-blockchain" ${state.deviceConnect.blockchain ? 'checked' : ''} /> blockchain key</label>
|
||||
<label class="checkbox-row"><input type="checkbox" id="connect-device" checked disabled /> client key</label>
|
||||
<div class="stack" style="gap:6px;">
|
||||
<p class="field-label">Что будет передано</p>
|
||||
<p class="meta-muted">Новое устройство автоматически получит ключ доступа и ключ блокчейна.</p>
|
||||
<p class="meta-muted">Ключ восстановления никогда не передаётся и не сохраняется на устройствах.</p>
|
||||
</div>
|
||||
<p class="meta-muted" id="connect-keys-status">Проверяем ключи на этом устройстве...</p>
|
||||
<div class="row">
|
||||
<button class="icon-btn small-btn" type="button" id="tech-help">Техсправка</button>
|
||||
@@ -31,24 +32,9 @@ export function render({navigate, chrome}) {
|
||||
</div>
|
||||
`;
|
||||
|
||||
const rootToggle = card.querySelector('#connect-root');
|
||||
const blockchainToggle = card.querySelector('#connect-blockchain');
|
||||
const deviceToggle = card.querySelector('#connect-device');
|
||||
const statusEl = card.querySelector('#connect-keys-status');
|
||||
const openQrBtn = card.querySelector('#open-qr');
|
||||
const openPairBtn = card.querySelector('#open-pairing');
|
||||
deviceToggle.checked = true;
|
||||
|
||||
rootToggle.addEventListener('change', () => {
|
||||
state.deviceConnect.root = rootToggle.checked;
|
||||
});
|
||||
blockchainToggle.addEventListener('change', () => {
|
||||
state.deviceConnect.blockchain = blockchainToggle.checked;
|
||||
});
|
||||
deviceToggle.addEventListener('change', () => {
|
||||
state.deviceConnect.client = true;
|
||||
deviceToggle.checked = true;
|
||||
});
|
||||
|
||||
const helpModal = document.createElement('div');
|
||||
helpModal.className = 'modal-shell';
|
||||
@@ -61,15 +47,10 @@ export function render({navigate, chrome}) {
|
||||
<button class="icon-btn" type="button" data-close="true" aria-label="Закрыть">✕</button>
|
||||
</div>
|
||||
<div class="stack" style="gap:6px;">
|
||||
<p class="meta-muted">пользователь выбирает ключи для передачи</p>
|
||||
<p class="meta-muted">передать можно только существующие ключи</p>
|
||||
<p class="meta-muted">если ключа нет – он недоступен</p>
|
||||
<p class="meta-muted">blockchain key – можно передать или нет</p>
|
||||
<p class="meta-muted">root key – только если существует</p>
|
||||
<p class="meta-muted">client key передаётся всегда</p>
|
||||
<p class="meta-muted">подключение происходит напрямую через QR</p>
|
||||
<p class="meta-muted">сервер не используется</p>
|
||||
<p class="meta-muted">текущая логика: устройство 1 показывает QR, устройство 2 сканирует</p>
|
||||
<p class="meta-muted">client key (ключ доступа) передаётся всегда</p>
|
||||
<p class="meta-muted">blockchain key (ключ блокчейна) передаётся всегда</p>
|
||||
<p class="meta-muted">root/recovery private key никогда не передаётся</p>
|
||||
<p class="meta-muted">QR-сценарий передаёт рабочие ключи напрямую между устройствами</p>
|
||||
<p class="meta-muted">для сценария через сервер используйте кнопку «Подключить по коду»</p>
|
||||
</div>
|
||||
<button class="primary-btn" type="button" data-close="true">OK</button>
|
||||
@@ -78,12 +59,9 @@ export function render({navigate, chrome}) {
|
||||
|
||||
const openHelp = () => {
|
||||
helpModal.hidden = false;
|
||||
helpModal.querySelector('.modal-dialog').focus();
|
||||
};
|
||||
|
||||
const closeHelp = () => {
|
||||
helpModal.hidden = true;
|
||||
helpModal.querySelector('.modal-dialog')?.focus();
|
||||
};
|
||||
const closeHelp = () => { helpModal.hidden = true; };
|
||||
|
||||
card.querySelector('#tech-help').addEventListener('click', openHelp);
|
||||
card.querySelector('#open-qr').addEventListener('click', () => navigate('device-qr-view'));
|
||||
@@ -96,61 +74,32 @@ export function render({navigate, chrome}) {
|
||||
throw new Error('Нет активной сессии');
|
||||
}
|
||||
const savedKeys = await loadEncryptedUserSecrets(state.session.login, state.session.storagePwdInMemory);
|
||||
const hasRoot = Boolean(savedKeys.rootKey);
|
||||
const hasBlockchain = Boolean(savedKeys.blockchainKey);
|
||||
const hasClient = Boolean(savedKeys.clientKey);
|
||||
|
||||
rootToggle.disabled = !hasRoot;
|
||||
blockchainToggle.disabled = !hasBlockchain;
|
||||
deviceToggle.disabled = true;
|
||||
state.deviceConnect.root = hasRoot && rootToggle.checked;
|
||||
state.deviceConnect.blockchain = hasBlockchain && blockchainToggle.checked;
|
||||
state.deviceConnect.client = hasClient;
|
||||
rootToggle.checked = state.deviceConnect.root;
|
||||
blockchainToggle.checked = state.deviceConnect.blockchain;
|
||||
deviceToggle.checked = hasClient;
|
||||
openQrBtn.disabled = !hasClient;
|
||||
openPairBtn.disabled = !hasClient;
|
||||
|
||||
const available = [
|
||||
hasClient ? 'client' : '',
|
||||
hasBlockchain ? 'blockchain' : '',
|
||||
hasRoot ? 'root' : '',
|
||||
].filter(Boolean);
|
||||
statusEl.textContent = available.length
|
||||
? `На этом устройстве доступны: ${available.join(', ')}.`
|
||||
: 'На этом устройстве нет сохранённых ключей для передачи.';
|
||||
const ready = hasBlockchain && hasClient;
|
||||
openQrBtn.disabled = !ready;
|
||||
openPairBtn.disabled = !ready;
|
||||
statusEl.textContent = ready
|
||||
? 'Ключ доступа и ключ блокчейна готовы к безопасной передаче.'
|
||||
: 'На этом устройстве не хватает рабочих ключей. Выполните вход по паролю заново.';
|
||||
} catch {
|
||||
rootToggle.disabled = true;
|
||||
blockchainToggle.disabled = true;
|
||||
deviceToggle.checked = false;
|
||||
state.deviceConnect.root = false;
|
||||
state.deviceConnect.blockchain = false;
|
||||
state.deviceConnect.client = false;
|
||||
openQrBtn.disabled = true;
|
||||
openPairBtn.disabled = true;
|
||||
statusEl.textContent = 'Не удалось прочитать сохранённые ключи на этом устройстве.';
|
||||
statusEl.textContent = 'Не удалось прочитать рабочие ключи на этом устройстве.';
|
||||
}
|
||||
})();
|
||||
|
||||
helpModal.addEventListener('click', (event) => {
|
||||
const target = event.target;
|
||||
if (target instanceof HTMLElement && target.dataset.close === 'true') {
|
||||
closeHelp();
|
||||
}
|
||||
if (target instanceof HTMLElement && target.dataset.close === 'true') closeHelp();
|
||||
});
|
||||
|
||||
helpModal.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Escape') {
|
||||
closeHelp();
|
||||
}
|
||||
if (event.key === 'Escape') closeHelp();
|
||||
});
|
||||
|
||||
screen.append(card);
|
||||
const modalRoot = document.getElementById('modal-root');
|
||||
modalRoot?.append(helpModal);
|
||||
screen.cleanup = () => {
|
||||
helpModal.remove();
|
||||
};
|
||||
screen.cleanup = () => { helpModal.remove(); };
|
||||
return screen;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
promptPwaInstall,
|
||||
} from '../services/pwa-install-service.js';
|
||||
import { initPwaPush } from '../services/pwa-push-service.js';
|
||||
import { getArweaveWalletFromStoredClientKey } from '../services/arweave-wallet-service.js';
|
||||
import { getArweaveWalletFromStoredBlockchainKey } from '../services/arweave-wallet-service.js';
|
||||
import {
|
||||
prepareAvatarImageFile,
|
||||
uploadArweaveFile,
|
||||
@@ -144,7 +144,7 @@ function openDeveloperAvatarUploadModal({ walletLogin, storagePwd, gateway } = {
|
||||
+ `(${optimized.width}x${optimized.height}, ${optimized.contentType})`,
|
||||
);
|
||||
|
||||
walletCtx = await getArweaveWalletFromStoredClientKey({
|
||||
walletCtx = await getArweaveWalletFromStoredBlockchainKey({
|
||||
login: walletLogin,
|
||||
storagePwd,
|
||||
onStatus: (message) => setMeta(message),
|
||||
|
||||
@@ -37,24 +37,18 @@ function pairingSessionKindLabel(sessionType) {
|
||||
return Number(sessionType || 0) === SESSION_TYPE_WALLET ? 'Wallet session' : 'Client session';
|
||||
}
|
||||
|
||||
function buildTransferKeys(savedKeys, selection = {}) {
|
||||
function buildTransferKeys(savedKeys) {
|
||||
const keys = {
|
||||
clientKey: String(savedKeys?.clientKey || '').trim(),
|
||||
blockchainKey: '',
|
||||
rootKey: '',
|
||||
blockchainKey: String(savedKeys?.blockchainKey || '').trim(),
|
||||
};
|
||||
if (!keys.clientKey) {
|
||||
throw new Error('На этом устройстве нет сохранённого client key для передачи.');
|
||||
}
|
||||
if (selection.blockchain && savedKeys?.blockchainKey) {
|
||||
keys.blockchainKey = String(savedKeys.blockchainKey || '').trim();
|
||||
}
|
||||
if (selection.root && savedKeys?.rootKey) {
|
||||
keys.rootKey = String(savedKeys.rootKey || '').trim();
|
||||
if (!keys.clientKey || !keys.blockchainKey) {
|
||||
throw new Error('На этом устройстве нет полного набора рабочих ключей. Выполните вход по паролю заново.');
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
|
||||
function requestCardHtml(request) {
|
||||
const shortCode = formatPairingShortCode(request?.shortCode || '');
|
||||
const client = String(request?.requesterClientPlatform || 'unknown');
|
||||
@@ -166,45 +160,12 @@ export function render({navigate, chrome}) {
|
||||
let settingsBusy = false;
|
||||
let pairingPasswordConfigured = false;
|
||||
let dialogMode = '';
|
||||
let pendingTransferRequest = null;
|
||||
|
||||
chrome?.setTopbar(createTopBar({
|
||||
title: 'Подключить по коду',
|
||||
back: { label: '←', onClick: () => navigate('connect-device-view') },
|
||||
}));
|
||||
|
||||
const transferDialog = document.createElement('div');
|
||||
transferDialog.className = 'pairing-transfer-dialog';
|
||||
transferDialog.hidden = true;
|
||||
transferDialog.innerHTML = `
|
||||
<div class="pairing-transfer-dialog__backdrop" data-transfer-action="cancel"></div>
|
||||
<section class="card stack pairing-transfer-dialog__card" role="dialog" aria-modal="true" aria-labelledby="pairing-transfer-title">
|
||||
<div class="stack" style="gap:5px;">
|
||||
<h2 class="login-panel-title" id="pairing-transfer-title">Подключить и передать ключи</h2>
|
||||
<p class="meta-muted">Client key передаётся всегда. Дополнительные ключи можно передать только если они есть на этом устройстве.</p>
|
||||
</div>
|
||||
<div class="pairing-transfer-key-list">
|
||||
<label class="pairing-transfer-key is-required">
|
||||
<input type="checkbox" checked disabled />
|
||||
<span><strong>Client key</strong><small>Обязателен для клиентского устройства</small></span>
|
||||
</label>
|
||||
<label class="pairing-transfer-key" data-transfer-key="root">
|
||||
<input type="checkbox" id="pairing-transfer-root" checked />
|
||||
<span><strong>Root key</strong><small>Передать на новое устройство</small></span>
|
||||
</label>
|
||||
<label class="pairing-transfer-key" data-transfer-key="blockchain">
|
||||
<input type="checkbox" id="pairing-transfer-blockchain" checked />
|
||||
<span><strong>Blockchain key</strong><small>Передать на новое устройство</small></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="row pairing-transfer-dialog__actions">
|
||||
<button class="ghost-btn" type="button" data-transfer-action="cancel">Отмена</button>
|
||||
<button class="primary-btn pairing-approve-btn" type="button" data-transfer-action="confirm">Подключить</button>
|
||||
</div>
|
||||
</section>
|
||||
`;
|
||||
screen.append(transferDialog);
|
||||
|
||||
const settingsCard = document.createElement('div');
|
||||
settingsCard.className = 'card stack';
|
||||
const passwordIcons = makePasswordToggleIcons();
|
||||
@@ -212,8 +173,9 @@ export function render({navigate, chrome}) {
|
||||
const keySummaryCard = document.createElement('div');
|
||||
keySummaryCard.className = 'card stack';
|
||||
keySummaryCard.innerHTML = `
|
||||
<p class="field-label">Что передаётся при расширенном подключении</p>
|
||||
<p class="field-label">Что передаётся новому устройству</p>
|
||||
<p class="meta-muted" id="pairing-key-summary">Проверяем локальные ключи...</p>
|
||||
<p class="meta-muted">Ключ восстановления никогда не передаётся.</p>
|
||||
`;
|
||||
|
||||
const requestsCard = document.createElement('div');
|
||||
@@ -449,13 +411,11 @@ export function render({navigate, chrome}) {
|
||||
|
||||
const loadSavedKeys = async () => {
|
||||
savedKeys = await loadEncryptedUserSecrets(state.session.login, state.session.storagePwdInMemory);
|
||||
const available = [];
|
||||
if (savedKeys?.clientKey) available.push('client');
|
||||
if (savedKeys?.blockchainKey) available.push('blockchain');
|
||||
if (savedKeys?.rootKey) available.push('root');
|
||||
keySummaryEl.textContent = available.length > 1
|
||||
? `Доступны для подключения: ${available.join(', ')}.`
|
||||
: 'На этом устройстве доступен только client key.';
|
||||
const hasClient = !!String(savedKeys?.clientKey || '').trim();
|
||||
const hasBlockchain = !!String(savedKeys?.blockchainKey || '').trim();
|
||||
keySummaryEl.textContent = hasClient && hasBlockchain
|
||||
? 'Будут переданы: ключ доступа и ключ блокчейна.'
|
||||
: 'На устройстве не хватает рабочего набора ключей. Выполните вход по паролю заново.';
|
||||
};
|
||||
|
||||
const reloadRequests = async ({ silent = false } = {}) => {
|
||||
@@ -478,8 +438,7 @@ export function render({navigate, chrome}) {
|
||||
refreshBtn.disabled = flag;
|
||||
};
|
||||
|
||||
const approveRequest = async (request, selection = {}) => {
|
||||
const withExtras = !!selection.root || !!selection.blockchain;
|
||||
const approveRequest = async (request) => {
|
||||
let payload;
|
||||
if (Number(request?.requesterSessionType || 0) === SESSION_TYPE_WALLET) {
|
||||
const delegatedSession = await authService.createDelegatedSessionWithClientKey({
|
||||
@@ -495,81 +454,26 @@ export function render({navigate, chrome}) {
|
||||
session: delegatedSession,
|
||||
});
|
||||
} else {
|
||||
const keys = buildTransferKeys(savedKeys, selection);
|
||||
const keys = buildTransferKeys(savedKeys);
|
||||
payload = buildSecretsPayload({
|
||||
login: state.session.login,
|
||||
keys,
|
||||
mode: withExtras ? 'with-extras' : 'device-only',
|
||||
mode: 'full-device',
|
||||
});
|
||||
}
|
||||
const encryptedPayload = await encryptPairingPayloadForRequester(request?.requesterSessionKey, payload);
|
||||
await runPairingOpWithSessionRestore(() => authService.approveTrustedDeviceLogin(request?.pairingId, encryptedPayload));
|
||||
const sessionOnly = Number(request?.requesterSessionType || 0) === SESSION_TYPE_WALLET;
|
||||
showToast(
|
||||
withExtras
|
||||
? 'Ключи переданы на новое устройство'
|
||||
: sessionOnly
|
||||
? 'Wallet-session выпущена для нового устройства'
|
||||
: 'Новое устройство подключено',
|
||||
);
|
||||
setAuthInfo(
|
||||
withExtras
|
||||
? 'Заявка подтверждена, ключи переданы.'
|
||||
: sessionOnly
|
||||
? 'Заявка подтверждена, для нового устройства создана отдельная wallet-session без передачи постоянных ключей.'
|
||||
: 'Заявка подтверждена без передачи доп. ключей.',
|
||||
);
|
||||
showToast(sessionOnly
|
||||
? 'Wallet-session выпущена для нового устройства'
|
||||
: 'Ключ доступа и ключ блокчейна переданы на новое устройство');
|
||||
setAuthInfo(sessionOnly
|
||||
? 'Заявка подтверждена, для нового устройства создана отдельная wallet-session без передачи постоянных ключей.'
|
||||
: 'Заявка подтверждена. Рабочие ключи переданы, ключ восстановления не передавался.');
|
||||
await refreshSessions().catch(() => {});
|
||||
await reloadRequests({ silent: true });
|
||||
};
|
||||
|
||||
const closeTransferDialog = () => {
|
||||
pendingTransferRequest = null;
|
||||
transferDialog.hidden = true;
|
||||
};
|
||||
|
||||
const openTransferDialog = (request) => {
|
||||
pendingTransferRequest = request;
|
||||
const rootRow = transferDialog.querySelector('[data-transfer-key="root"]');
|
||||
const blockchainRow = transferDialog.querySelector('[data-transfer-key="blockchain"]');
|
||||
const rootInput = transferDialog.querySelector('#pairing-transfer-root');
|
||||
const blockchainInput = transferDialog.querySelector('#pairing-transfer-blockchain');
|
||||
const hasRoot = !!String(savedKeys?.rootKey || '').trim();
|
||||
const hasBlockchain = !!String(savedKeys?.blockchainKey || '').trim();
|
||||
rootRow.hidden = !hasRoot;
|
||||
blockchainRow.hidden = !hasBlockchain;
|
||||
rootInput.checked = hasRoot;
|
||||
blockchainInput.checked = hasBlockchain;
|
||||
transferDialog.hidden = false;
|
||||
};
|
||||
|
||||
transferDialog.addEventListener('click', async (event) => {
|
||||
const target = event.target instanceof Element ? event.target.closest('[data-transfer-action]') : null;
|
||||
const action = String(target?.dataset?.transferAction || '');
|
||||
if (!action) return;
|
||||
if (action === 'cancel') {
|
||||
closeTransferDialog();
|
||||
return;
|
||||
}
|
||||
if (action !== 'confirm' || !pendingTransferRequest) return;
|
||||
const request = pendingTransferRequest;
|
||||
const confirmBtn = transferDialog.querySelector('[data-transfer-action="confirm"]');
|
||||
confirmBtn.disabled = true;
|
||||
try {
|
||||
await approveRequest(request, {
|
||||
root: !!transferDialog.querySelector('#pairing-transfer-root')?.checked,
|
||||
blockchain: !!transferDialog.querySelector('#pairing-transfer-blockchain')?.checked,
|
||||
});
|
||||
closeTransferDialog();
|
||||
} catch (error) {
|
||||
const message = toUserMessage(error, 'Не удалось подключить устройство.');
|
||||
setAuthError(message);
|
||||
setStatus(status, message, 'error');
|
||||
} finally {
|
||||
confirmBtn.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
passwordDialog.addEventListener('click', (event) => {
|
||||
const target = event.target;
|
||||
if (!(target instanceof HTMLElement)) return;
|
||||
@@ -674,14 +578,7 @@ export function render({navigate, chrome}) {
|
||||
buttons.forEach((btn) => { btn.disabled = true; });
|
||||
try {
|
||||
if (action === 'approve-device') {
|
||||
const isWallet = Number(request?.requesterSessionType || 0) === SESSION_TYPE_WALLET;
|
||||
const hasExtras = !!String(savedKeys?.rootKey || '').trim() || !!String(savedKeys?.blockchainKey || '').trim();
|
||||
if (!isWallet && hasExtras) {
|
||||
openTransferDialog(request);
|
||||
buttons.forEach((btn) => { btn.disabled = false; });
|
||||
return;
|
||||
}
|
||||
await approveRequest(request, {});
|
||||
await approveRequest(request);
|
||||
} else if (action === 'reject') {
|
||||
await runPairingOpWithSessionRestore(() => authService.rejectTrustedDeviceLogin(pairingId, 'rejected_by_user'));
|
||||
showToast('Заявка отклонена', { kind: 'error' });
|
||||
|
||||
@@ -14,9 +14,9 @@ export function render({navigate, chrome}) {
|
||||
screen.className = 'stack';
|
||||
|
||||
chrome?.setTopbar(createTopBar({
|
||||
title: 'Показать QR-код',
|
||||
back: { label: '←', onClick: () => navigate('connect-device-view') },
|
||||
}));
|
||||
title: 'Показать QR-код',
|
||||
back: { label: '←', onClick: () => navigate('connect-device-view') },
|
||||
}));
|
||||
|
||||
const card = document.createElement('div');
|
||||
card.className = 'card stack qr-card';
|
||||
@@ -24,6 +24,7 @@ export function render({navigate, chrome}) {
|
||||
<div class="qr-image" id="device-transfer-qr" aria-label="QR-код для переноса ключей"></div>
|
||||
<p class="meta-muted" id="device-transfer-login">Логин: ...</p>
|
||||
<p class="meta-muted" id="device-transfer-keys">Ключи: ...</p>
|
||||
<p class="meta-muted">Ключ восстановления в QR-код не включается.</p>
|
||||
<p class="status-line is-unavailable" id="device-transfer-status" style="display:none;"></p>
|
||||
<button class="primary-btn" type="button" id="qr-ok">OK</button>
|
||||
`;
|
||||
@@ -40,11 +41,12 @@ export function render({navigate, chrome}) {
|
||||
}
|
||||
const savedKeys = await loadEncryptedUserSecrets(state.session.login, state.session.storagePwdInMemory);
|
||||
const keys = {
|
||||
clientKey: savedKeys.clientKey || '',
|
||||
blockchainKey: state.deviceConnect.blockchain ? (savedKeys.blockchainKey || '') : '',
|
||||
rootKey: state.deviceConnect.root ? (savedKeys.rootKey || '') : '',
|
||||
clientKey: String(savedKeys.clientKey || '').trim(),
|
||||
blockchainKey: String(savedKeys.blockchainKey || '').trim(),
|
||||
};
|
||||
if (!keys.clientKey) throw new Error('На этом устройстве нет client key');
|
||||
if (!keys.clientKey || !keys.blockchainKey) {
|
||||
throw new Error('На этом устройстве нет полного набора рабочих ключей');
|
||||
}
|
||||
|
||||
const qrText = makeKeyTransferText({ login: state.session.login, keys });
|
||||
qrEl.innerHTML = renderQrSvg(qrText);
|
||||
@@ -60,7 +62,6 @@ export function render({navigate, chrome}) {
|
||||
})();
|
||||
|
||||
card.querySelector('#qr-ok').addEventListener('click', () => navigate('connect-device-view'));
|
||||
|
||||
screen.append(card);
|
||||
return screen;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { authService, state } from '../state.js';
|
||||
import { bytesToBase64 } from '../services/crypto-utils.js';
|
||||
import { readShineUserPda } from '../services/shine-user-pda-service.js';
|
||||
import { KeyRotationClient, KEY_ROTATION_REASONS } from '../services/key-rotation-service.js';
|
||||
import { formatSol, getBalanceSol, getSolanaWalletFromStoredSecret, transferAllSol } from '../services/solana-wallet-service.js';
|
||||
|
||||
export const pageMeta = { id:'key-rotation-view', title:'Смена ключей', hideToolbar:true };
|
||||
|
||||
@@ -56,12 +57,66 @@ export function render({navigate,chrome}){
|
||||
|
||||
async function renderChainReady(s){
|
||||
const oldP=input('Текущий пароль');const newP=input('Новый пароль');const go=document.createElement('button');go.className='primary-btn';go.textContent='Изменить ключи в Solana';const abort=document.createElement('button');abort.className='secondary-btn';abort.textContent='Прервать смену ключей';const err=document.createElement('div');err.className='meta-muted';
|
||||
go.addEventListener('click',async()=>{go.disabled=true;try{cachedOldBundle=await derive(oldP.el.value);cachedNewBundle=await derive(newP.el.value);await client.rotatePda({login,solanaEndpoint,oldBundle:cachedOldBundle,newBundle:cachedNewBundle,storagePwd});await refresh();}catch(e){err.textContent=e?.message||String(e);go.disabled=false;}});abort.addEventListener('click',async()=>{await client.abort();await refresh();});setRoot(statusCard(s),note('Новая цепочка полностью готова. Следующий шаг — точка невозврата: старый root разрешит атомарную смену root + client + blockchain fork, а новое PDA подпишет новый blockchain key.'),oldP.wrap,newP.wrap,go,abort,err);
|
||||
go.addEventListener('click',async()=>{go.disabled=true;try{cachedOldBundle=await derive(oldP.el.value);cachedNewBundle=await derive(newP.el.value);await client.rotatePda({login,solanaEndpoint,oldBundle:cachedOldBundle,newBundle:cachedNewBundle,storagePwd});await refresh();}catch(e){err.textContent=e?.message||String(e);go.disabled=false;}});abort.addEventListener('click',async()=>{await client.abort();await refresh();});setRoot(statusCard(s),note('Новая цепочка полностью готова. Следующий шаг — точка невозврата: старый ключ восстановления временно используется для атомарной смены recovery + client + blockchain fork, а новое PDA подпишет новый blockchain key. Приватный ключ восстановления на устройстве не сохраняется.'),oldP.wrap,newP.wrap,go,abort,err);
|
||||
}
|
||||
|
||||
function pollView(s){setRoot(statusCard(s),note(s.rotationStatus==='REBUILDING_SERVER'?'Сервер перестраивает текущую рабочую историю по новому fork.':'Ожидаем подтверждение нового состояния PDA в Solana.'));timer=setTimeout(()=>void refresh(),POLL_MS);}
|
||||
function placeholderView(s,kind){const wallet=kind==='wallet';const text=wallet?'Перевод SOL со старого blockchain-wallet на новый пока не реализован. Этот этап сейчас будет отмечен как NOT_IMPLEMENTED и пропущен.':'Перешифрование старых личных сообщений новым client key пока не реализовано. Этот этап сейчас будет отмечен как NOT_IMPLEMENTED и пропущен.';const go=document.createElement('button');go.className='primary-btn';go.textContent='Продолжить';go.addEventListener('click',async()=>{go.disabled=true;try{const next=await client.continuePlaceholder();if(String(next?.rotationStatus||'')==='COMPLETE'){navigate('my-blockchain-view');return;}await refresh();}catch(e){go.disabled=false;root.append(note(e?.message||String(e)));}});setRoot(statusCard(s),note(text),go);}
|
||||
async function renderWalletMigration(s){
|
||||
const oldP=cachedOldBundle?null:input('Старый пароль');
|
||||
const newP=cachedNewBundle?null:input('Новый пароль');
|
||||
const info=note('После смены blockchain key меняется и Solana-кошелёк. На этом шаге можно перевести весь доступный SOL со старого кошелька на новый за вычетом сетевой комиссии.');
|
||||
const warning=note('Сейчас автоматически переносится только SOL. Миграцию AR/Turbo при смене blockchain key нужно доработать отдельно.');
|
||||
const details=document.createElement('div');details.className='card stack';details.innerHTML='<span class="meta-muted">Проверьте старый и новый кошельки.</span>';
|
||||
const prepare=document.createElement('button');prepare.className='secondary-btn';prepare.type='button';prepare.textContent='Проверить кошельки';
|
||||
const transfer=document.createElement('button');transfer.className='primary-btn';transfer.type='button';transfer.textContent='Перевести SOL и продолжить';transfer.disabled=true;
|
||||
const skip=document.createElement('button');skip.className='secondary-btn';skip.type='button';skip.textContent='Пропустить перенос';
|
||||
const err=document.createElement('div');err.className='meta-muted';
|
||||
let oldWallet=null,newWallet=null,oldBalance=null;
|
||||
|
||||
async function refresh(){if(disposed)return;if(timer){clearTimeout(timer);timer=null;}try{const s=await client.status();const st=String(s.rotationStatus||'NONE');if(st==='NONE'){await renderNone();return;}if(st==='COPYING_CHAIN'){if(cachedNewBundle)await runCopy(s);else await askNewPasswordAndCopy(s);return;}if(st==='CHAIN_READY'){await renderChainReady(s);return;}if(['ROTATING_PDA','PDA_ROTATED','REBUILDING_SERVER'].includes(st)){pollView(s);return;}if(st==='WALLET_MIGRATION'){placeholderView(s,'wallet');return;}if(st==='MESSAGE_MIGRATION'){placeholderView(s,'messages');return;}if(st==='FINALIZING'){const next=await client.continuePlaceholder();if(String(next?.rotationStatus||'')==='COMPLETE'){navigate('my-blockchain-view');return;}await refresh();return;}if(st==='COMPLETE'){setRoot(note('Смена ключей завершена.'));return;}setRoot(note(`Неизвестное состояние ротации: ${st}`));}catch(e){setRoot(note(`Не удалось прочитать состояние смены ключей: ${e?.message||e}`));}}
|
||||
const resolveBundles=async()=>{
|
||||
if(!cachedOldBundle){if(!oldP?.el.value)throw new Error('Введите старый пароль');cachedOldBundle=await derive(oldP.el.value);}
|
||||
if(!cachedNewBundle){if(!newP?.el.value)throw new Error('Введите новый пароль');cachedNewBundle=await derive(newP.el.value);}
|
||||
if(String(cachedOldBundle?.blockchainPair?.publicKeyB64||'')!==String(s.oldBlockchainKey||''))throw new Error('Старый пароль выводит другой blockchain key');
|
||||
if(String(cachedNewBundle?.blockchainPair?.publicKeyB64||'')!==String(s.newBlockchainKey||''))throw new Error('Новый пароль выводит другой blockchain key');
|
||||
};
|
||||
|
||||
const prepareWallets=async()=>{
|
||||
await resolveBundles();
|
||||
oldWallet=await getSolanaWalletFromStoredSecret(cachedOldBundle.blockchainPair.privatePkcs8B64);
|
||||
newWallet=await getSolanaWalletFromStoredSecret(cachedNewBundle.blockchainPair.privatePkcs8B64);
|
||||
const [oldB,newB]=await Promise.all([
|
||||
getBalanceSol({endpoint:solanaEndpoint,address:oldWallet.address}),
|
||||
getBalanceSol({endpoint:solanaEndpoint,address:newWallet.address}),
|
||||
]);
|
||||
oldBalance=oldB;
|
||||
details.innerHTML=`<strong>Старый кошелёк</strong><span class="meta-muted">${h(oldWallet.address)}</span><span>${h(formatSol(oldB.sol,9))} SOL</span><strong>Новый кошелёк</strong><span class="meta-muted">${h(newWallet.address)}</span><span>${h(formatSol(newB.sol,9))} SOL</span>`;
|
||||
transfer.textContent=Number(oldB.lamports||0)>0?'Перевести SOL и продолжить':'Средств нет — продолжить';
|
||||
transfer.disabled=false;
|
||||
};
|
||||
|
||||
prepare.addEventListener('click',async()=>{prepare.disabled=true;transfer.disabled=true;err.textContent='';try{await prepareWallets();}catch(e){err.textContent=e?.message||String(e);}finally{prepare.disabled=false;}});
|
||||
transfer.addEventListener('click',async()=>{transfer.disabled=true;prepare.disabled=true;skip.disabled=true;err.textContent='';try{
|
||||
if(!oldWallet||!newWallet||!oldBalance)await prepareWallets();
|
||||
if(Number(oldBalance?.lamports||0)>0){
|
||||
const result=await transferAllSol({endpoint:solanaEndpoint,fromKeypair:oldWallet.keypair,toAddress:newWallet.address});
|
||||
details.insertAdjacentHTML('beforeend',`<span class="meta-muted">${result.skipped?'Перевод не потребовался.':`Переведено ${h(formatSol(Number(result.lamports)/1_000_000_000,9))} SOL. Tx: ${h(result.signature)}`}</span>`);
|
||||
}
|
||||
const next=await client.continuePlaceholder();
|
||||
if(String(next?.rotationStatus||'')==='COMPLETE'){navigate('my-blockchain-view');return;}
|
||||
await refresh();
|
||||
}catch(e){err.textContent=e?.message||String(e);transfer.disabled=false;prepare.disabled=false;skip.disabled=false;}});
|
||||
skip.addEventListener('click',async()=>{skip.disabled=true;transfer.disabled=true;prepare.disabled=true;err.textContent='';try{
|
||||
const next=await client.continuePlaceholder();
|
||||
if(String(next?.rotationStatus||'')==='COMPLETE'){navigate('my-blockchain-view');return;}
|
||||
await refresh();
|
||||
}catch(e){err.textContent=e?.message||String(e);skip.disabled=false;transfer.disabled=!oldWallet;prepare.disabled=false;}});
|
||||
|
||||
const nodes=[statusCard(s),info,warning];if(oldP)nodes.push(oldP.wrap);if(newP)nodes.push(newP.wrap);nodes.push(details,prepare,transfer,skip,err);setRoot(...nodes);
|
||||
if(cachedOldBundle&&cachedNewBundle)void prepareWallets().catch(e=>{err.textContent=e?.message||String(e);});
|
||||
}
|
||||
|
||||
function placeholderView(s){const text='Перешифрование старых личных сообщений новым client key пока не реализовано. Этот этап сейчас будет отмечен как NOT_IMPLEMENTED и пропущен.';const go=document.createElement('button');go.className='primary-btn';go.textContent='Продолжить';go.addEventListener('click',async()=>{go.disabled=true;try{const next=await client.continuePlaceholder();if(String(next?.rotationStatus||'')==='COMPLETE'){navigate('my-blockchain-view');return;}await refresh();}catch(e){go.disabled=false;root.append(note(e?.message||String(e)));}});setRoot(statusCard(s),note(text),go);}
|
||||
|
||||
async function refresh(){if(disposed)return;if(timer){clearTimeout(timer);timer=null;}try{const s=await client.status();const st=String(s.rotationStatus||'NONE');if(st==='NONE'){await renderNone();return;}if(st==='COPYING_CHAIN'){if(cachedNewBundle)await runCopy(s);else await askNewPasswordAndCopy(s);return;}if(st==='CHAIN_READY'){await renderChainReady(s);return;}if(['ROTATING_PDA','PDA_ROTATED','REBUILDING_SERVER'].includes(st)){pollView(s);return;}if(st==='WALLET_MIGRATION'){await renderWalletMigration(s);return;}if(st==='MESSAGE_MIGRATION'){placeholderView(s);return;}if(st==='FINALIZING'){const next=await client.continuePlaceholder();if(String(next?.rotationStatus||'')==='COMPLETE'){navigate('my-blockchain-view');return;}await refresh();return;}if(st==='COMPLETE'){setRoot(note('Смена ключей завершена.'));return;}setRoot(note(`Неизвестное состояние ротации: ${st}`));}catch(e){setRoot(note(`Не удалось прочитать состояние смены ключей: ${e?.message||e}`));}}
|
||||
void refresh();screen.cleanup=()=>{disposed=true;if(timer)clearTimeout(timer);};return screen;
|
||||
}
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
import { createTopBar } from '../components/topbar.js';
|
||||
import { authorizeSession, state } from '../state.js';
|
||||
|
||||
export const pageMeta = { id: 'key-storage-view', title: 'Какие ключи сохранить', showAppChrome: false };
|
||||
|
||||
export function render({ navigate }) {
|
||||
const screen = document.createElement('section');
|
||||
screen.className = 'stack';
|
||||
|
||||
const card = document.createElement('div');
|
||||
card.className = 'card stack';
|
||||
|
||||
const rootToggle = document.createElement('input');
|
||||
rootToggle.type = 'checkbox';
|
||||
rootToggle.checked = state.keyStorage.saveRoot;
|
||||
rootToggle.addEventListener('change', () => {
|
||||
state.keyStorage.saveRoot = rootToggle.checked;
|
||||
if (rootToggle.checked) {
|
||||
window.alert('Мы советуем не сохранять главный ключ на устройстве, он используется только для смены паролей и основных настроек.');
|
||||
}
|
||||
});
|
||||
|
||||
const blockchainToggle = document.createElement('input');
|
||||
blockchainToggle.type = 'checkbox';
|
||||
blockchainToggle.checked = state.keyStorage.saveBlockchain;
|
||||
blockchainToggle.addEventListener('change', () => {
|
||||
state.keyStorage.saveBlockchain = blockchainToggle.checked;
|
||||
});
|
||||
|
||||
const deviceToggle = document.createElement('input');
|
||||
deviceToggle.type = 'checkbox';
|
||||
deviceToggle.checked = true;
|
||||
deviceToggle.disabled = true;
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="key-card stack">
|
||||
<label class="checkbox-row"><span class="field-label">Root Key</span></label>
|
||||
<p class="meta-muted key-storage-option__description">Главный ключ аккаунта. Нужен для смены пароля, восстановления доступа и важных основных настроек.</p>
|
||||
<input class="input" type="text" value="${state.keyStorage.rootKey}" />
|
||||
</div>
|
||||
<div class="key-card stack">
|
||||
<label class="checkbox-row"><span class="field-label">Blockchain Key</span></label>
|
||||
<p class="meta-muted key-storage-option__description">Используется для подписи ваших действий и записей в блокчейне Сияния.</p>
|
||||
<input class="input" type="text" value="${state.keyStorage.blockchainKey}" />
|
||||
</div>
|
||||
<div class="key-card stack">
|
||||
<label class="checkbox-row"><span class="field-label">Device Key</span></label>
|
||||
<p class="meta-muted key-storage-option__description">Ключ этого устройства. Нужен для обычного входа, авторизации сессии и работы приложения на телефоне.</p>
|
||||
<input class="input" type="text" value="${state.keyStorage.clientKey}" />
|
||||
</div>
|
||||
<p class="key-storage-note key-storage-note--strong">Если вы не особо понимаете, о чём идёт речь, и не хотите особо заморачиваться с ключами, можете просто сохранить все ключи на телефоне.</p>
|
||||
`;
|
||||
|
||||
card.children[0].querySelector('label').prepend(rootToggle);
|
||||
card.children[1].querySelector('label').prepend(blockchainToggle);
|
||||
card.children[2].querySelector('label').prepend(deviceToggle);
|
||||
|
||||
const rootInput = card.children[0].querySelector('.input');
|
||||
rootInput.addEventListener('input', () => {
|
||||
state.keyStorage.rootKey = rootInput.value;
|
||||
});
|
||||
|
||||
const blockchainInput = card.children[1].querySelector('.input');
|
||||
blockchainInput.addEventListener('input', () => {
|
||||
state.keyStorage.blockchainKey = blockchainInput.value;
|
||||
});
|
||||
|
||||
const deviceInput = card.children[2].querySelector('.input');
|
||||
deviceInput.addEventListener('input', () => {
|
||||
state.keyStorage.clientKey = deviceInput.value;
|
||||
});
|
||||
|
||||
const actions = document.createElement('div');
|
||||
actions.className = 'auth-footer-actions';
|
||||
|
||||
const cancelButton = document.createElement('button');
|
||||
cancelButton.className = 'ghost-btn';
|
||||
cancelButton.type = 'button';
|
||||
cancelButton.textContent = 'Отмена';
|
||||
cancelButton.addEventListener('click', () => navigate('login-password-view'));
|
||||
|
||||
const okButton = document.createElement('button');
|
||||
okButton.className = 'primary-btn';
|
||||
okButton.type = 'button';
|
||||
okButton.textContent = 'OK';
|
||||
okButton.addEventListener('click', () => {
|
||||
authorizeSession();
|
||||
navigate('profile-view');
|
||||
});
|
||||
|
||||
actions.append(cancelButton, okButton);
|
||||
|
||||
screen.append(
|
||||
createTopBar({
|
||||
title: 'Какие ключи сохранить',
|
||||
back: { label: '←', onClick: () => navigate('login-password-view') },
|
||||
}),
|
||||
card,
|
||||
actions,
|
||||
);
|
||||
|
||||
return screen;
|
||||
}
|
||||
@@ -143,8 +143,8 @@ export function render({ navigate }) {
|
||||
const parseTransferText = (text) => {
|
||||
try {
|
||||
const transfer = parseKeyTransferText(text);
|
||||
if (!transfer.keys.clientKey) {
|
||||
throw new Error('В QR-коде нет client key для входа');
|
||||
if (!transfer.keys.clientKey || !transfer.keys.blockchainKey) {
|
||||
throw new Error('В QR-коде нет полного набора рабочих ключей');
|
||||
}
|
||||
showTransfer(transfer);
|
||||
} catch (error) {
|
||||
|
||||
@@ -157,6 +157,9 @@ export function render({ navigate }) {
|
||||
};
|
||||
|
||||
const finalizeAuthorizedLogin = async (keys, login) => {
|
||||
if (!String(keys?.clientKey || '').trim() || !String(keys?.blockchainKey || '').trim()) {
|
||||
throw new Error('Новое устройство должно получить ключ доступа и ключ блокчейна.');
|
||||
}
|
||||
const session = await authService.createSessionFromImportedSecrets(login, keys);
|
||||
const addingProfile = isAddingProfileLogin();
|
||||
if (!addingProfile) {
|
||||
@@ -309,7 +312,7 @@ export function render({ navigate }) {
|
||||
login,
|
||||
passwordHash,
|
||||
requesterSessionKey: requesterMaterial.sessionKey,
|
||||
payloadType: 3,
|
||||
payloadType: 2,
|
||||
});
|
||||
|
||||
activePairingId = String(payload?.pairingId || '');
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import { createTopBar } from '../components/topbar.js';
|
||||
import {
|
||||
authService,
|
||||
authorizeSession,
|
||||
clearAuthMessages,
|
||||
consumeAuthReturnPage,
|
||||
refreshSessions,
|
||||
setAuthBusy,
|
||||
setAuthError,
|
||||
setAuthInfo,
|
||||
state,
|
||||
} from '../state.js';
|
||||
import { toUserMessage } from '../services/ui-error-texts.js';
|
||||
import { clearStoredMessages } from '../services/message-store.js';
|
||||
import { emptyPasswordWords, PASSWORD_MAX_LENGTH } from '../services/password-words.js';
|
||||
|
||||
export const pageMeta = { id: 'login-password-view', title: 'Введите пароль', showAppChrome: false };
|
||||
@@ -135,17 +140,33 @@ export function render({ navigate }) {
|
||||
enterButton.textContent = 'Проверяем пароль…';
|
||||
const result = await authService.createAuthSession(currentLogin, keyBundle);
|
||||
|
||||
// Существующую дальнейшую логику после успешного пароля сохраняем без изменений.
|
||||
state.registrationDraft.flowType = 'login';
|
||||
state.registrationDraft.login = result.login;
|
||||
state.registrationDraft.password = password;
|
||||
// В текущей модели оба рабочих ключа всегда сохраняются автоматически.
|
||||
// Recovery/root private key остаётся только в памяти этого шага и в key-vault не попадает.
|
||||
await authService.persistSelectedKeys(result.login, result.storagePwd, keyBundle);
|
||||
await authService.persistSessionMaterial(result.login, result.sessionMaterial);
|
||||
await clearStoredMessages(result.login).catch(() => {});
|
||||
|
||||
authorizeSession({
|
||||
login: result.login,
|
||||
sessionId: result.sessionId,
|
||||
storagePwd: result.storagePwd,
|
||||
});
|
||||
state.loginDraft.login = result.login;
|
||||
state.loginDraft.password = '';
|
||||
state.loginDraft.passwordMode = 'single';
|
||||
state.loginDraft.passwordWords = emptyPasswordWords();
|
||||
state.registrationDraft.flowType = '';
|
||||
state.registrationDraft.password = '';
|
||||
state.registrationDraft.passwordMode = 'single';
|
||||
state.registrationDraft.passwordWords = emptyPasswordWords();
|
||||
state.registrationDraft.sessionId = result.sessionId;
|
||||
state.registrationDraft.storagePwd = result.storagePwd;
|
||||
state.registrationDraft.pendingKeyBundle = keyBundle;
|
||||
state.registrationDraft.pendingSessionMaterial = result.sessionMaterial;
|
||||
navigate('registration-keys-view');
|
||||
state.registrationDraft.storagePwd = '';
|
||||
state.registrationDraft.sessionId = '';
|
||||
state.registrationDraft.pendingKeyBundle = null;
|
||||
state.registrationDraft.pendingSessionMaterial = null;
|
||||
|
||||
await refreshSessions();
|
||||
setAuthInfo(`Вы вошли как @${result.login}.`);
|
||||
navigate(consumeAuthReturnPage('profile-view'));
|
||||
} catch (error) {
|
||||
overlay.style.display = 'none';
|
||||
const message = toUserMessage(error, 'Не удалось выполнить вход.');
|
||||
|
||||
@@ -318,7 +318,7 @@ export function render({ navigate }) {
|
||||
|
||||
const subtitle = document.createElement('p');
|
||||
subtitle.className = 'meta-muted';
|
||||
subtitle.textContent = 'Процесс запускается сразу: из этого секрета будут вычислены root/recovery key, blockchain key и client key.';
|
||||
subtitle.textContent = 'Процесс запускается сразу: из этого секрета будут вычислены ключ восстановления, ключ блокчейна и ключ доступа.';
|
||||
|
||||
const details = document.createElement('p');
|
||||
details.className = 'meta-muted';
|
||||
|
||||
@@ -9,9 +9,9 @@ export const REGISTRATION_FAQ_TOPICS = [
|
||||
shortTitle: 'Где ключи',
|
||||
title: 'У кого хранятся ключи?',
|
||||
paragraphs: [
|
||||
'Ключи хранятся только у вас: на вашем устройстве, на доверенных устройствах или на отдельном внешнем устройстве, которое вы контролируете сами.',
|
||||
'Сияние не хранит ваши приватные ключи на сервере. Сервер помогает с доставкой и синхронизацией, но не владеет вашим секретом.',
|
||||
'Если захотите, ключи можно держать на отдельном полностью программируемом устройстве с открытым кодом, например на ESP32-контроллере.',
|
||||
'Рабочие приватные ключи хранятся только у вас: на вашем устройстве или на доверенных устройствах, которые вы контролируете сами.',
|
||||
'Ключ доступа и ключ блокчейна сохраняются на авторизованном устройстве в зашифрованном контейнере. Приватный ключ восстановления на устройстве не сохраняется: он вычисляется из пароля только на время операций, где действительно нужен.',
|
||||
'Сияние не хранит ваши приватные ключи на сервере. В будущем для повышенной защиты рабочие ключи можно будет вынести на отдельное программируемое устройство с открытым кодом, например ESP32-контроллер.',
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -30,7 +30,7 @@ export const REGISTRATION_FAQ_TOPICS = [
|
||||
title: 'Как генерируются ключи и что делает пароль?',
|
||||
paragraphs: [
|
||||
'Из вашего логина и пароля с помощью Argon2id вычисляется специальный секрет.',
|
||||
'Уже из этого секрета детерминированно строятся три основных ключа: root/recovery key, blockchain key и client key.',
|
||||
'Из этого секрета детерминированно строятся три ключа: ключ восстановления (Recovery), ключ блокчейна и ключ доступа.',
|
||||
'Это значит, что логин и пароль не просто проверяются на сервере, а реально участвуют в создании ваших ключей. У разных логинов даже с одинаковым паролем будут разные ключи.',
|
||||
],
|
||||
},
|
||||
@@ -39,10 +39,10 @@ export const REGISTRATION_FAQ_TOPICS = [
|
||||
shortTitle: 'Три ключа',
|
||||
title: 'Зачем нужны три ключа?',
|
||||
paragraphs: [
|
||||
'Главный ключ нужен для управления вашей основной публичной записью и важными изменениями личности, включая обновление главной публичной части в Solana.',
|
||||
'Ключ публикаций нужен для подписания действий и записей в блокчейне Сияния.',
|
||||
'Ключ устройства нужен для входов и работы конкретного устройства. Благодаря разделению ключей можно точнее выдавать права одним устройствам и не выдавать другим.',
|
||||
'Если не хочется в это вникать, обычно можно просто сохранить все ключи на своём устройстве. Для большинства обычных сценариев на iPhone, Android и Linux это вполне практично. Для больших сумм или повышенного риска лучше отдельное внешнее устройство.',
|
||||
'Ключ восстановления (Recovery key) нужен для восстановления и самых критичных операций с учётной записью. Его приватная часть никогда не сохраняется на устройстве и при необходимости временно вычисляется из пароля.',
|
||||
'Ключ блокчейна постоянно хранится на авторизованном устройстве. Он подписывает записи и действия в блокчейне Сияния и является исходным ключом для штатных пользовательских кошельков.',
|
||||
'Ключ доступа (client key) также постоянно хранится на устройстве. Он используется для входа на сервер, работы устройства и защищённых личных сообщений.',
|
||||
'Сейчас после обычного входа оба рабочих ключа — ключ блокчейна и ключ доступа — сохраняются автоматически. Отдельного ограниченного режима в текущей версии нет.',
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -93,7 +93,7 @@ export const REGISTRATION_FAQ_TOPICS = [
|
||||
paragraphs: [
|
||||
'На новом устройстве выберите вход через другое устройство и получите код подключения.',
|
||||
'На уже авторизованном устройстве откройте: Профиль → Настройки → Устройства → Подключить по коду. Введите или выберите соответствующий код и подтвердите подключение.',
|
||||
'Ключ устройства передаётся новому клиентскому устройству всегда. Если на доверенном устройстве доступны главный ключ и/или ключ публикаций, перед подтверждением можно отдельно выбрать, какие дополнительные ключи передать.',
|
||||
'При подтверждении нового устройства автоматически передаются два рабочих ключа: ключ доступа и ключ блокчейна. Приватный ключ восстановления не передаётся никогда.',
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,204 +0,0 @@
|
||||
import { createTopBar } from '../components/topbar.js';
|
||||
import {
|
||||
authService,
|
||||
cancelAddProfileLogin,
|
||||
consumeAuthReturnPage,
|
||||
authorizeSession,
|
||||
refreshSessions,
|
||||
resetRegistrationFlow,
|
||||
setAuthError,
|
||||
setAuthInfo,
|
||||
state,
|
||||
} from '../state.js';
|
||||
import { clearStoredMessages } from '../services/message-store.js';
|
||||
import { toUserMessage } from '../services/ui-error-texts.js';
|
||||
|
||||
export const pageMeta = { id: 'registration-keys-view', title: 'Сохранение ключей', showAppChrome: false };
|
||||
const EMPTY_PASSWORD_WORDS = Array.from({ length: 12 }, () => '');
|
||||
|
||||
export function render({ navigate }) {
|
||||
const screen = document.createElement('section');
|
||||
screen.className = 'stack keys-screen';
|
||||
|
||||
const isLoginFlow = state.registrationDraft.flowType === 'login';
|
||||
const normalizedLogin = (state.registrationDraft.login || '').trim();
|
||||
const displayLogin = normalizedLogin ? `@${normalizedLogin.replace(/^@/, '')}` : '@new.user';
|
||||
|
||||
const lead = document.createElement('div');
|
||||
lead.className = 'keys-lead';
|
||||
const title = document.createElement('h1');
|
||||
title.className = 'keys-lead__title';
|
||||
title.textContent = isLoginFlow ? `Вы вошли как ${displayLogin}` : `${displayLogin} зарегистрирован`;
|
||||
const question = document.createElement('p');
|
||||
question.className = 'keys-lead__text';
|
||||
question.textContent = 'Какие ключи хранить на этом устройстве?';
|
||||
const questionHint = document.createElement('p');
|
||||
questionHint.className = 'keys-lead__text';
|
||||
questionHint.textContent = 'Они хранятся в зашифрованном виде.';
|
||||
lead.append(title, question, questionHint);
|
||||
|
||||
const status = document.createElement('p');
|
||||
status.className = 'status-line is-unavailable';
|
||||
status.style.display = 'none';
|
||||
|
||||
const createKeyRow = (toggle, titleText, descriptionText) => {
|
||||
const row = document.createElement('label');
|
||||
row.className = 'keys-row';
|
||||
const text = document.createElement('span');
|
||||
text.className = 'keys-row__text';
|
||||
const name = document.createElement('span');
|
||||
name.className = 'keys-row__title';
|
||||
name.textContent = titleText;
|
||||
const description = document.createElement('span');
|
||||
description.className = 'keys-row__hint';
|
||||
description.textContent = descriptionText;
|
||||
text.append(name, description);
|
||||
toggle.className = 'switch';
|
||||
row.append(text, toggle);
|
||||
return row;
|
||||
};
|
||||
|
||||
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 list = document.createElement('div');
|
||||
list.className = 'keys-list';
|
||||
list.append(
|
||||
createKeyRow(rootToggle, 'Главный ключ', 'Смена пароля, восстановление доступа, важные настройки'),
|
||||
createKeyRow(blockchainToggle, 'Ключ публикаций', 'Подписывает ваши записи и действия'),
|
||||
createKeyRow(deviceToggle, 'Ключ устройства', 'Вход с этого устройства – нужен всегда'),
|
||||
);
|
||||
|
||||
const simpleNote = document.createElement('p');
|
||||
simpleNote.className = 'keys-note';
|
||||
simpleNote.textContent = 'Не уверены – оставьте все включёнными.';
|
||||
|
||||
const actions = document.createElement('div');
|
||||
actions.className = 'keys-actions';
|
||||
|
||||
const cancelButton = document.createElement('button');
|
||||
cancelButton.className = 'secondary-btn';
|
||||
cancelButton.type = 'button';
|
||||
cancelButton.textContent = 'Отмена';
|
||||
cancelButton.addEventListener('click', async () => {
|
||||
if (state.authReturnHash === '/profiles') {
|
||||
await cancelAddProfileLogin();
|
||||
navigate('profiles-view');
|
||||
return;
|
||||
}
|
||||
resetRegistrationFlow();
|
||||
navigate('start-view');
|
||||
});
|
||||
|
||||
const okButton = document.createElement('button');
|
||||
okButton.className = 'primary-btn';
|
||||
okButton.type = 'button';
|
||||
okButton.textContent = 'Готово';
|
||||
okButton.addEventListener('click', async () => {
|
||||
status.style.display = 'none';
|
||||
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,
|
||||
);
|
||||
|
||||
if (!state.keyStorage.saveRoot && state.registrationDraft.pendingKeyBundle) {
|
||||
state.registrationDraft.pendingKeyBundle.rootPair = null;
|
||||
}
|
||||
if (!state.keyStorage.saveBlockchain && state.registrationDraft.pendingKeyBundle) {
|
||||
state.registrationDraft.pendingKeyBundle.blockchainPair = null;
|
||||
}
|
||||
|
||||
await clearStoredMessages(state.registrationDraft.login).catch(() => {});
|
||||
|
||||
authorizeSession({
|
||||
login: state.registrationDraft.login,
|
||||
sessionId: state.registrationDraft.sessionId,
|
||||
storagePwd: state.registrationDraft.storagePwd,
|
||||
});
|
||||
|
||||
state.loginDraft.login = state.registrationDraft.login;
|
||||
state.loginDraft.password = '';
|
||||
state.loginDraft.passwordMode = 'single';
|
||||
state.loginDraft.passwordWords = EMPTY_PASSWORD_WORDS.slice();
|
||||
state.registrationDraft.flowType = '';
|
||||
state.registrationDraft.password = '';
|
||||
state.registrationDraft.passwordMode = 'single';
|
||||
state.registrationDraft.passwordWords = EMPTY_PASSWORD_WORDS.slice();
|
||||
state.registrationDraft.passwordWordsLinked = false;
|
||||
state.registrationDraft.usePromoCode = false;
|
||||
state.registrationDraft.promoCode = '';
|
||||
state.registrationDraft.storagePwd = '';
|
||||
state.registrationDraft.sessionId = '';
|
||||
state.registrationDraft.pendingKeyBundle = null;
|
||||
state.registrationDraft.pendingSessionMaterial = null;
|
||||
state.registrationDraft.preGeneratedKeyBundle = null;
|
||||
state.registrationPayment.walletAddress = '';
|
||||
state.registrationPayment.balanceSOL = '0.0000';
|
||||
|
||||
await refreshSessions();
|
||||
setAuthInfo(isLoginFlow
|
||||
? `Ключи сохранены. Вы вошли как @${state.registrationDraft.login}. Далее откройте вкладку «Каналы».`
|
||||
: `Ключи сохранены. Регистрация завершена для @${state.registrationDraft.login}. Далее откройте вкладку «Каналы».`);
|
||||
navigate(consumeAuthReturnPage('profile-view'));
|
||||
} catch (error) {
|
||||
const message = toUserMessage(error, 'Не удалось сохранить ключи на устройстве.');
|
||||
setAuthError(message);
|
||||
status.textContent = message;
|
||||
status.style.display = '';
|
||||
}
|
||||
});
|
||||
|
||||
actions.append(okButton, cancelButton);
|
||||
|
||||
screen.append(
|
||||
createTopBar({
|
||||
title: 'Сохранение ключей',
|
||||
back: {
|
||||
label: '←',
|
||||
onClick: async () => {
|
||||
if (state.authReturnHash === '/profiles') {
|
||||
await cancelAddProfileLogin();
|
||||
navigate('profiles-view');
|
||||
return;
|
||||
}
|
||||
resetRegistrationFlow();
|
||||
navigate('start-view');
|
||||
},
|
||||
},
|
||||
}),
|
||||
lead,
|
||||
list,
|
||||
simpleNote,
|
||||
status,
|
||||
actions,
|
||||
);
|
||||
|
||||
return screen;
|
||||
}
|
||||
@@ -95,18 +95,9 @@ async function completeRegistrationLogin({ navigate, keyBundle }) {
|
||||
state.registrationDraft.password,
|
||||
);
|
||||
|
||||
state.keyStorage.saveRoot = Boolean(state.keyStorage.saveRoot);
|
||||
state.keyStorage.saveBlockchain = Boolean(state.keyStorage.saveBlockchain);
|
||||
|
||||
await authService.persistSelectedKeys(
|
||||
result.login,
|
||||
result.storagePwd,
|
||||
keyBundle,
|
||||
{
|
||||
saveRoot: state.keyStorage.saveRoot,
|
||||
saveBlockchain: state.keyStorage.saveBlockchain,
|
||||
},
|
||||
);
|
||||
// После регистрации оба рабочих ключа сохраняются автоматически.
|
||||
// Приватный Recovery/root key не записывается на устройство.
|
||||
await authService.persistSelectedKeys(result.login, result.storagePwd, keyBundle);
|
||||
await authService.persistSessionMaterial(result.login, result.sessionMaterial);
|
||||
await clearStoredMessages(state.registrationDraft.login).catch(() => {});
|
||||
|
||||
|
||||
@@ -11,38 +11,41 @@ export function render({navigate, chrome}) {
|
||||
const screen = document.createElement('section');
|
||||
screen.className = 'stack';
|
||||
|
||||
const visible = {
|
||||
root: false,
|
||||
blockchain: false,
|
||||
device: false,
|
||||
};
|
||||
|
||||
const keys = {
|
||||
root: '',
|
||||
blockchain: '',
|
||||
device: '',
|
||||
};
|
||||
const visible = { blockchain: false, access: false };
|
||||
const keys = { blockchain: '', access: '' };
|
||||
|
||||
chrome?.setTopbar(createTopBar({
|
||||
title: 'Ключи этого устройства',
|
||||
back: { label: '←', onClick: () => navigate('device-view') },
|
||||
}));
|
||||
|
||||
const card = document.createElement('div');
|
||||
card.className = 'nav-list key-list';
|
||||
title: 'Ключи этого устройства',
|
||||
back: { label: '←', onClick: () => navigate('device-view') },
|
||||
}));
|
||||
|
||||
const status = document.createElement('p');
|
||||
status.className = 'meta-muted';
|
||||
status.textContent = 'Загружаем сохранённые ключи...';
|
||||
|
||||
const renderField = (id, label, hint = '') => {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'nav-list key-list';
|
||||
|
||||
const recovery = document.createElement('div');
|
||||
recovery.className = 'key-card stack';
|
||||
recovery.innerHTML = `
|
||||
<div class="row">
|
||||
<span class="field-label">Ключ восстановления
|
||||
<small class="key-card__hint">Управление учётной записью, восстановление и критическая смена ключей</small>
|
||||
</span>
|
||||
</div>
|
||||
<div class="key-value key-value--compact">Не хранится на устройстве</div>
|
||||
<p class="meta-muted">Приватный Recovery key создаётся из пароля только на время защищённых операций и не записывается в хранилище устройства.</p>
|
||||
`;
|
||||
|
||||
const renderStoredField = (id, label, hint) => {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'key-card stack';
|
||||
const eyeIcon = iconHtml('eye', false, 'key-toggle-icon');
|
||||
const eyeOffIcon = iconHtml('eyeOff', false, 'key-toggle-icon');
|
||||
row.innerHTML = `
|
||||
<div class="row">
|
||||
<span class="field-label">${label}${hint ? `<small class="key-card__hint">${hint}</small>` : ''}</span>
|
||||
<span class="field-label">${label}<small class="key-card__hint">${hint}</small></span>
|
||||
<button class="icon-btn key-toggle-btn" type="button" data-toggle="${id}" aria-label="Показать ключ" title="Показать ключ">${eyeIcon}</button>
|
||||
</div>
|
||||
<div class="key-value key-value--compact" data-value="${id}">*****</div>
|
||||
@@ -53,15 +56,16 @@ export function render({navigate, chrome}) {
|
||||
};
|
||||
|
||||
card.append(
|
||||
renderField('root', 'Главный ключ', 'Восстановление аккаунта и важные настройки'),
|
||||
renderField('blockchain', 'Ключ публикаций', 'Подпись ваших записей'),
|
||||
renderField('device', 'Ключ устройства', 'Вход с этого устройства'),
|
||||
recovery,
|
||||
renderStoredField('blockchain', 'Ключ блокчейна', 'Подпись записей в Сиянии и управление вашими кошельками'),
|
||||
renderStoredField('access', 'Ключ доступа', 'Вход на сервер, работа устройства и защищённые личные сообщения'),
|
||||
);
|
||||
|
||||
const setMissingState = (id) => {
|
||||
const valueEl = card.querySelector(`[data-value="${id}"]`);
|
||||
const btnEl = card.querySelector(`[data-toggle="${id}"]`);
|
||||
const field = btnEl?.closest('.key-card');
|
||||
if (!valueEl || !btnEl) return;
|
||||
valueEl.textContent = 'нет данных';
|
||||
btnEl.disabled = true;
|
||||
btnEl.innerHTML = field?._eyeOffIcon || '';
|
||||
@@ -73,59 +77,46 @@ export function render({navigate, chrome}) {
|
||||
const valueEl = card.querySelector(`[data-value="${id}"]`);
|
||||
const btnEl = card.querySelector(`[data-toggle="${id}"]`);
|
||||
const field = btnEl?.closest('.key-card');
|
||||
if (!valueEl || !btnEl) return;
|
||||
if (!keys[id]) {
|
||||
setMissingState(id);
|
||||
return;
|
||||
}
|
||||
valueEl.textContent = visible[id] ? keys[id] : '*****';
|
||||
btnEl.disabled = false;
|
||||
btnEl.innerHTML = visible[id]
|
||||
? field?._eyeOffIcon || ''
|
||||
: field?._eyeIcon || '';
|
||||
btnEl.innerHTML = visible[id] ? field?._eyeOffIcon || '' : field?._eyeIcon || '';
|
||||
btnEl.setAttribute('aria-label', visible[id] ? 'Скрыть ключ' : 'Показать ключ');
|
||||
btnEl.title = visible[id] ? 'Скрыть ключ' : 'Показать ключ';
|
||||
};
|
||||
|
||||
card.querySelectorAll('[data-toggle]').forEach((button) => {
|
||||
button.addEventListener('click', () => {
|
||||
const { toggle } = button.dataset;
|
||||
if (!keys[toggle]) return;
|
||||
visible[toggle] = !visible[toggle];
|
||||
updateField(toggle);
|
||||
const id = button.dataset.toggle;
|
||||
if (!keys[id]) return;
|
||||
visible[id] = !visible[id];
|
||||
updateField(id);
|
||||
});
|
||||
});
|
||||
|
||||
['root', 'blockchain', 'device'].forEach((id) => updateField(id));
|
||||
['blockchain', 'access'].forEach(updateField);
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
if (!state.session.login || !state.session.storagePwdInMemory) {
|
||||
throw new Error('Нет активной сессии для чтения ключей');
|
||||
}
|
||||
|
||||
const savedKeys = await loadEncryptedUserSecrets(
|
||||
state.session.login,
|
||||
state.session.storagePwdInMemory,
|
||||
);
|
||||
|
||||
const rootSeed32 = savedKeys.rootKey ? extractSeed32FromPkcs8B64(savedKeys.rootKey) : null;
|
||||
const savedKeys = await loadEncryptedUserSecrets(state.session.login, state.session.storagePwdInMemory);
|
||||
const blockchainSeed32 = savedKeys.blockchainKey ? extractSeed32FromPkcs8B64(savedKeys.blockchainKey) : null;
|
||||
const clientSeed32 = savedKeys.clientKey ? extractSeed32FromPkcs8B64(savedKeys.clientKey) : null;
|
||||
|
||||
keys.root = rootSeed32 ? bytesToBase58(rootSeed32) : '';
|
||||
keys.blockchain = blockchainSeed32 ? bytesToBase58(blockchainSeed32) : '';
|
||||
keys.device = clientSeed32 ? bytesToBase58(clientSeed32) : '';
|
||||
|
||||
if (keys.root || keys.blockchain || keys.device) {
|
||||
status.textContent = 'Показаны только ключи, сохранённые на этом устройстве.';
|
||||
} else {
|
||||
status.textContent = 'На этом устройстве нет сохранённых ключей.';
|
||||
}
|
||||
} catch (error) {
|
||||
status.textContent = 'На этом устройстве нет сохранённых ключей.';
|
||||
keys.access = clientSeed32 ? bytesToBase58(clientSeed32) : '';
|
||||
status.textContent = keys.blockchain && keys.access
|
||||
? 'На устройстве сохранены оба рабочих ключа. Ключ восстановления хранится только как публичная часть в учётной записи и приватно на устройстве не сохраняется.'
|
||||
: 'На устройстве отсутствует часть рабочих ключей. Выполните вход по паролю заново.';
|
||||
} catch {
|
||||
status.textContent = 'Не удалось прочитать рабочие ключи этого устройства.';
|
||||
}
|
||||
|
||||
['root', 'blockchain', 'device'].forEach((id) => updateField(id));
|
||||
['blockchain', 'access'].forEach(updateField);
|
||||
})();
|
||||
|
||||
screen.append(status, card);
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
formatAr,
|
||||
getArweaveBalance,
|
||||
getArweaveTopupSiteUrl,
|
||||
getArweaveWalletFromStoredClientKey,
|
||||
getArweaveWalletFromStoredBlockchainKey,
|
||||
transferAr,
|
||||
} from '../services/arweave-wallet-service.js';
|
||||
import { loadEncryptedUserSecrets } from '../services/key-vault.js';
|
||||
@@ -496,39 +496,38 @@ export function render({navigate, chrome}) {
|
||||
|
||||
async function resolveWalletSigningMaterial() {
|
||||
const { login, storagePwd } = sessionArgsOrThrow();
|
||||
let saved;
|
||||
let saved = null;
|
||||
try {
|
||||
saved = await loadEncryptedUserSecrets(login, storagePwd);
|
||||
} catch {
|
||||
saved = null;
|
||||
}
|
||||
let rootKey = String(saved?.rootKey || '').trim();
|
||||
let blockchainKey = String(saved?.blockchainKey || '').trim();
|
||||
|
||||
const blockchainKey = String(saved?.blockchainKey || '').trim();
|
||||
const clientKey = String(saved?.clientKey || '').trim();
|
||||
if (!clientKey) throw new Error('На устройстве нет client.key. Выполните вход заново.');
|
||||
if (rootKey && blockchainKey) {
|
||||
return { rootPrivatePkcs8B64: rootKey, blockchainPrivatePkcs8B64: blockchainKey, clientPrivatePkcs8B64: clientKey };
|
||||
if (blockchainKey && clientKey) {
|
||||
return {
|
||||
rootPrivatePkcs8B64: '',
|
||||
blockchainPrivatePkcs8B64: blockchainKey,
|
||||
clientPrivatePkcs8B64: clientKey,
|
||||
};
|
||||
}
|
||||
|
||||
const password = window.prompt(
|
||||
'Для операции нужен root key (и blockchain key), но они не сохранены на устройстве.\nВведите пароль аккаунта для временного восстановления ключей:',
|
||||
'На этом устройстве не найден полный набор рабочих ключей. Введите пароль аккаунта, чтобы восстановить ключ доступа и ключ блокчейна:',
|
||||
'',
|
||||
);
|
||||
if (password == null) throw new Error('Операция отменена пользователем');
|
||||
const keyBundle = await authService.derivePasswordKeyBundle(login, password);
|
||||
rootKey = keyBundle?.rootPair?.privatePkcs8B64 || '';
|
||||
blockchainKey = keyBundle?.blockchainPair?.privatePkcs8B64 || '';
|
||||
if (!rootKey || !blockchainKey) throw new Error('Не удалось восстановить root/blockchain key из пароля');
|
||||
|
||||
const shouldSave = window.confirm(
|
||||
'Сохранить root key и blockchain key в зашифрованном контейнере этого устройства?\nВнимание: хранить ключи на телефоне менее безопасно.',
|
||||
);
|
||||
if (shouldSave) {
|
||||
await authService.persistSelectedKeys(login, storagePwd, keyBundle, { saveRoot: true, saveBlockchain: true });
|
||||
}
|
||||
return { rootPrivatePkcs8B64: rootKey, blockchainPrivatePkcs8B64: blockchainKey, clientPrivatePkcs8B64: clientKey };
|
||||
await authService.persistSelectedKeys(login, storagePwd, keyBundle);
|
||||
return {
|
||||
rootPrivatePkcs8B64: '',
|
||||
blockchainPrivatePkcs8B64: keyBundle.blockchainPair.privatePkcs8B64,
|
||||
clientPrivatePkcs8B64: keyBundle.clientPair.privatePkcs8B64,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function styleSupportInputField(field) {
|
||||
if (!field) return;
|
||||
field.style.color = 'var(--text-primary)';
|
||||
@@ -2024,7 +2023,7 @@ export function render({navigate, chrome}) {
|
||||
helpCard.innerHTML = `
|
||||
<summary style="cursor:pointer; font-weight:600;">Как получен этот адрес?</summary>
|
||||
<p class="meta-muted" style="margin-top:8px;">
|
||||
Сияние берёт ваш локальный client.key и по стандарту SAWD-v1 получает из него нативный Arweave-кошелёк.
|
||||
Сияние берёт ваш локальный ключ блокчейна и по стандарту SAWD-v1 получает из него нативный Arweave-кошелёк.
|
||||
Приватный ключ не отправляется на сервер. После первого расчёта он хранится только в зашифрованном контейнере этого устройства.
|
||||
</p>
|
||||
`;
|
||||
@@ -2126,14 +2125,14 @@ export function render({navigate, chrome}) {
|
||||
|
||||
try {
|
||||
let wasFirstTimeGeneration = false;
|
||||
arweaveWalletCtx = await getArweaveWalletFromStoredClientKey({
|
||||
arweaveWalletCtx = await getArweaveWalletFromStoredBlockchainKey({
|
||||
...sessionArgsOrThrow(),
|
||||
onStatus: (message) => {
|
||||
const text = String(message || '').trim();
|
||||
if (!text) return;
|
||||
if (text.includes('впервые получаем Arweave-кошелёк')) {
|
||||
wasFirstTimeGeneration = true;
|
||||
setStatus('Подождите – ваш Arweave-ключ вычисляется из client key. Это происходит только один раз, потом будет мгновенно.');
|
||||
setStatus('Подождите – ваш Arweave-ключ вычисляется из ключа блокчейна. Это происходит только один раз, потом будет мгновенно.');
|
||||
return;
|
||||
}
|
||||
setStatus(text);
|
||||
|
||||
@@ -9,14 +9,12 @@ const PRETTY_PATHS = new Map([
|
||||
['registration-faq-view', 'register/faq'],
|
||||
['registration-payment-view', 'register/payment'],
|
||||
['registration-draft-keys-view', 'register/draft-keys'],
|
||||
['registration-keys-view', 'register/keys'],
|
||||
['topup-view', 'topup'],
|
||||
['devnet-topup-view', 'devnet-topup'],
|
||||
['login-view', 'login'],
|
||||
['login-camera-view', 'login/camera'],
|
||||
['login-other-device-view', 'login/device'],
|
||||
['login-password-view', 'login/password'],
|
||||
['key-storage-view', 'key-storage'],
|
||||
['profile-view', 'profile'],
|
||||
['profile-edit-view', 'profile/edit'],
|
||||
['profiles-view', 'profiles'],
|
||||
@@ -69,14 +67,12 @@ export const PRE_AUTH_PAGES = [
|
||||
'registration-faq-view',
|
||||
'registration-payment-view',
|
||||
'registration-draft-keys-view',
|
||||
'registration-keys-view',
|
||||
'topup-view',
|
||||
'devnet-topup-view',
|
||||
'login-view',
|
||||
'login-camera-view',
|
||||
'login-other-device-view',
|
||||
'login-password-view',
|
||||
'key-storage-view',
|
||||
];
|
||||
|
||||
export function parseRouteFromPath(pathname = '') {
|
||||
@@ -223,7 +219,7 @@ export function parseRouteFromPath(pathname = '') {
|
||||
if (sub === 'faq') return { pageId: 'registration-faq-view', params: {} };
|
||||
if (sub === 'payment') return { pageId: 'registration-payment-view', params: {} };
|
||||
if (sub === 'draft-keys') return { pageId: 'registration-draft-keys-view', params: {} };
|
||||
if (sub === 'keys') return { pageId: 'registration-keys-view', params: {} };
|
||||
if (sub === 'keys') return { pageId: 'register-view', params: {} };
|
||||
return { pageId: 'register-view', params: {} };
|
||||
}
|
||||
|
||||
@@ -244,7 +240,7 @@ export function parseRouteFromPath(pathname = '') {
|
||||
}
|
||||
|
||||
if (pageId === 'key-storage') {
|
||||
return { pageId: 'key-storage-view', params: {} };
|
||||
return { pageId: 'login-view', params: {} };
|
||||
}
|
||||
|
||||
if (pageId === 'profile') {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { loadEncryptedUserSecrets, updateEncryptedUserSecrets } from './key-vault.js';
|
||||
import { extractClientKey32FromStoredValue } from './client-key-utils.js';
|
||||
import { deriveArweaveWalletFromClientKey32 } from './sawd-v1.js';
|
||||
import { extractEd25519Seed32FromStoredValue } from './client-key-utils.js';
|
||||
import { deriveArweaveWalletFromBlockchainKey32 } from './sawd-v1.js';
|
||||
|
||||
const DEFAULT_ARWEAVE_GATEWAY = 'https://arweave.net';
|
||||
const AR_TOPUP_URL = 'https://changenow.io/exchange?from=usd&to=ar&amount=10&fiatMode=true';
|
||||
@@ -76,10 +76,11 @@ function pickCachedWallet(secrets) {
|
||||
const cached = secrets?.arweaveWallet;
|
||||
if (!cached || typeof cached !== 'object') return null;
|
||||
const derivation = String(cached.derivation || '').trim();
|
||||
const sourceKey = String(cached.sourceKey || '').trim();
|
||||
const address = String(cached.address || '').trim();
|
||||
const owner = String(cached.owner || '').trim();
|
||||
const jwk = cached.jwk;
|
||||
if (derivation !== 'SAWD-v1' || !address || !owner || !jwk || typeof jwk !== 'object') {
|
||||
if (derivation !== 'SAWD-v1' || sourceKey !== 'blockchain' || !address || !owner || !jwk || typeof jwk !== 'object') {
|
||||
return null;
|
||||
}
|
||||
if (!String(jwk.kty || '').trim() || !String(jwk.e || '').trim() || !String(jwk.n || '').trim() || !String(jwk.d || '').trim()) {
|
||||
@@ -87,6 +88,7 @@ function pickCachedWallet(secrets) {
|
||||
}
|
||||
return {
|
||||
derivation,
|
||||
sourceKey,
|
||||
address,
|
||||
owner,
|
||||
jwk,
|
||||
@@ -150,13 +152,13 @@ function safeStatus(onStatus, text) {
|
||||
}
|
||||
|
||||
export async function getArweaveWalletChoices({ login, storagePwd, onStatus } = {}) {
|
||||
const derived = await getArweaveWalletFromStoredClientKey({ login, storagePwd, onStatus });
|
||||
const derived = await getArweaveWalletFromStoredBlockchainKey({ login, storagePwd, onStatus });
|
||||
const secrets = await loadEncryptedUserSecrets(String(login || '').trim(), String(storagePwd || '').trim());
|
||||
const extras = normalizeExtraWallets(secrets);
|
||||
return [
|
||||
{
|
||||
id: 'derived-client-key',
|
||||
label: 'Стандартный из client key',
|
||||
id: 'derived-blockchain-key',
|
||||
label: 'Стандартный из ключа блокчейна',
|
||||
address: derived.address,
|
||||
owner: derived.owner,
|
||||
jwk: derived.jwk,
|
||||
@@ -206,11 +208,11 @@ export async function addArweaveWalletSecret({ login, storagePwd, secret, label
|
||||
return wallet;
|
||||
}
|
||||
|
||||
export async function getArweaveWalletFromStoredClientKey({ login, storagePwd, onStatus } = {}) {
|
||||
export async function getArweaveWalletFromStoredBlockchainKey({ login, storagePwd, onStatus } = {}) {
|
||||
const cleanLogin = String(login || '').trim();
|
||||
const cleanPwd = String(storagePwd || '').trim();
|
||||
if (!cleanLogin || !cleanPwd) {
|
||||
throw new Error('Нет активной сессии для доступа к client.key');
|
||||
throw new Error('Нет активной сессии для доступа к blockchain key');
|
||||
}
|
||||
|
||||
const secrets = await loadEncryptedUserSecrets(cleanLogin, cleanPwd);
|
||||
@@ -220,23 +222,24 @@ export async function getArweaveWalletFromStoredClientKey({ login, storagePwd, o
|
||||
return cached;
|
||||
}
|
||||
|
||||
safeStatus(onStatus, 'Сейчас мы впервые получаем Arweave-кошелёк из вашего client key. Это может занять немного времени.');
|
||||
safeStatus(onStatus, 'Сейчас мы впервые получаем Arweave-кошелёк из вашего ключа блокчейна. Это может занять немного времени.');
|
||||
|
||||
const storedClientKey = String(secrets?.clientKey || '').trim();
|
||||
if (!storedClientKey) {
|
||||
throw new Error('На устройстве не найден client.key');
|
||||
const storedBlockchainKey = String(secrets?.blockchainKey || '').trim();
|
||||
if (!storedBlockchainKey) {
|
||||
throw new Error('На устройстве не найден blockchain key');
|
||||
}
|
||||
|
||||
const clientKey32 = extractClientKey32FromStoredValue(storedClientKey);
|
||||
const blockchainKey32 = extractEd25519Seed32FromStoredValue(storedBlockchainKey);
|
||||
let wallet;
|
||||
try {
|
||||
wallet = await deriveArweaveWalletFromClientKey32(clientKey32);
|
||||
wallet = await deriveArweaveWalletFromBlockchainKey32(blockchainKey32);
|
||||
} finally {
|
||||
clientKey32.fill(0);
|
||||
blockchainKey32.fill(0);
|
||||
}
|
||||
|
||||
const cachedWallet = {
|
||||
derivation: wallet.derivation,
|
||||
sourceKey: 'blockchain',
|
||||
address: wallet.address,
|
||||
owner: wallet.owner,
|
||||
jwk: wallet.jwk,
|
||||
|
||||
@@ -674,7 +674,7 @@ function targetLoginBytes(value) {
|
||||
|
||||
function normalizeTargetForkNumber(value, actionName = 'target') {
|
||||
const n = Number(value);
|
||||
if (!Number.isInteger(n) || n < 0 || n > 999) throw new Error(`Invalid toForkNumber for ${actionName}`);
|
||||
if (!Number.isInteger(n) || n < 1 || n > 999) throw new Error(`Invalid toForkNumber for ${actionName}`);
|
||||
return n;
|
||||
}
|
||||
|
||||
@@ -1142,8 +1142,10 @@ export class AuthService {
|
||||
async createSessionFromImportedSecrets(login, secrets) {
|
||||
const cleanLogin = (login || '').trim();
|
||||
if (!cleanLogin) throw new Error('В QR-коде нет логина');
|
||||
const clientKey = String(secrets?.clientKey || secrets?.clientKey || '').trim();
|
||||
if (!clientKey) throw new Error('В QR-коде нет client key для входа');
|
||||
const clientKey = String(secrets?.clientKey || '').trim();
|
||||
const blockchainKey = String(secrets?.blockchainKey || '').trim();
|
||||
if (!clientKey) throw new Error('В QR-коде нет ключа доступа для входа');
|
||||
if (!blockchainKey) throw new Error('В QR-коде нет ключа блокчейна');
|
||||
|
||||
const privateKey = await importPkcs8Ed25519(clientKey);
|
||||
const publicKeyB64 = await publicKeyB64FromPkcs8Ed25519(clientKey);
|
||||
@@ -1219,7 +1221,7 @@ export class AuthService {
|
||||
}
|
||||
}
|
||||
|
||||
async persistSelectedKeys(login, storagePwd, keyBundle, saveOptions = { saveRoot: true, saveBlockchain: true }) {
|
||||
async persistSelectedKeys(login, storagePwd, keyBundle) {
|
||||
let currentSecrets = {};
|
||||
try {
|
||||
const loaded = await loadEncryptedUserSecrets(login, storagePwd);
|
||||
@@ -1230,12 +1232,23 @@ export class AuthService {
|
||||
// Если контейнера ещё нет или пароль новый для этого логина — создадим новый ниже.
|
||||
}
|
||||
|
||||
const clientKey = String(keyBundle?.clientPair?.privatePkcs8B64 || '').trim();
|
||||
const blockchainKey = String(keyBundle?.blockchainPair?.privatePkcs8B64 || '').trim();
|
||||
if (!clientKey || !blockchainKey) {
|
||||
throw new Error('Для входа нужно сохранить client key и blockchain key');
|
||||
}
|
||||
|
||||
const secrets = {
|
||||
...currentSecrets,
|
||||
clientKey: keyBundle.clientPair.privatePkcs8B64,
|
||||
clientKey,
|
||||
blockchainKey,
|
||||
};
|
||||
if (saveOptions.saveRoot) secrets.rootKey = keyBundle.rootPair.privatePkcs8B64;
|
||||
if (saveOptions.saveBlockchain) secrets.blockchainKey = keyBundle.blockchainPair.privatePkcs8B64;
|
||||
// SAWD-v1 wallet зависит от blockchain key. При смене ключа старый кэш
|
||||
// нельзя переносить в новую identity: он будет лениво выведен заново.
|
||||
if (String(currentSecrets?.blockchainKey || '').trim() !== blockchainKey) {
|
||||
delete secrets.arweaveWallet;
|
||||
}
|
||||
delete secrets.rootKey;
|
||||
await saveEncryptedUserSecrets(login, storagePwd, secrets);
|
||||
}
|
||||
|
||||
|
||||
@@ -18,14 +18,14 @@ function parseKeypairJson64(raw) {
|
||||
if (!isByteArrayLike(parsed)) return null;
|
||||
const asArray = Array.from(parsed);
|
||||
if (asArray.length < 32) {
|
||||
throw new Error('Некорректный JSON ключ client.key: ожидалось минимум 32 байта');
|
||||
throw new Error('Некорректный JSON Ed25519-ключ: ожидалось минимум 32 байта');
|
||||
}
|
||||
|
||||
const out = new Uint8Array(asArray.length);
|
||||
for (let i = 0; i < asArray.length; i += 1) {
|
||||
const n = Number(asArray[i]);
|
||||
if (!Number.isInteger(n) || n < 0 || n > 255) {
|
||||
throw new Error('Некорректный JSON ключ client.key: найдены не-байтовые значения');
|
||||
throw new Error('Некорректный JSON Ed25519-ключ: найдены не-байтовые значения');
|
||||
}
|
||||
out[i] = n;
|
||||
}
|
||||
@@ -34,13 +34,13 @@ function parseKeypairJson64(raw) {
|
||||
|
||||
export function extractSeed32FromPkcs8B64(pkcs8B64) {
|
||||
const bytes = base64ToBytes(String(pkcs8B64 || '').trim());
|
||||
if (bytes.length < 32) throw new Error('Некорректный PKCS8 ключ client.key');
|
||||
if (bytes.length < 32) throw new Error('Некорректный PKCS8 Ed25519-ключ');
|
||||
return bytes.slice(bytes.length - 32);
|
||||
}
|
||||
|
||||
export function extractClientKey32FromStoredValue(storedClientKey) {
|
||||
const raw = String(storedClientKey || '').trim();
|
||||
if (!raw) throw new Error('Пустой client.key');
|
||||
export function extractEd25519Seed32FromStoredValue(storedKey) {
|
||||
const raw = String(storedKey || '').trim();
|
||||
if (!raw) throw new Error('Пустой приватный Ed25519-ключ');
|
||||
|
||||
const jsonBytes = parseKeypairJson64(raw);
|
||||
if (jsonBytes) {
|
||||
@@ -49,3 +49,6 @@ export function extractClientKey32FromStoredValue(storedClientKey) {
|
||||
|
||||
return extractSeed32FromPkcs8B64(raw);
|
||||
}
|
||||
|
||||
// Legacy API name retained for callers outside the wallet subsystem.
|
||||
export const extractClientKey32FromStoredValue = extractEd25519Seed32FromStoredValue;
|
||||
|
||||
@@ -73,15 +73,15 @@ function normalizeKeys(keys = {}) {
|
||||
return {
|
||||
clientKey: String(keys?.clientKey || '').trim(),
|
||||
blockchainKey: String(keys?.blockchainKey || '').trim(),
|
||||
rootKey: String(keys?.rootKey || '').trim(),
|
||||
};
|
||||
}
|
||||
|
||||
export function detectPairingPayloadType(keys = {}) {
|
||||
const normalized = normalizeKeys(keys);
|
||||
if (normalized.rootKey) return 3;
|
||||
if (normalized.blockchainKey) return 2;
|
||||
return 1;
|
||||
if (!normalized.clientKey || !normalized.blockchainKey) {
|
||||
throw new Error('Для подключения устройства нужны ключ доступа и ключ блокчейна');
|
||||
}
|
||||
return 2;
|
||||
}
|
||||
|
||||
export async function deriveEspPairingPasswordHash(login, password) {
|
||||
@@ -159,13 +159,14 @@ export async function decryptPairingPayloadFromEnvelope(encryptedPayload, reques
|
||||
}
|
||||
|
||||
export function buildSecretsPayload({ login, keys, mode }) {
|
||||
const normalizedKeys = normalizeKeys(keys);
|
||||
return {
|
||||
v: 1,
|
||||
type: 'shine-esp-pairing-transfer',
|
||||
login: String(login || '').trim(),
|
||||
mode: String(mode || 'device-only').trim() || 'device-only',
|
||||
keys: normalizeKeys(keys),
|
||||
payloadType: detectPairingPayloadType(keys),
|
||||
mode: String(mode || 'full-device').trim() || 'full-device',
|
||||
keys: normalizedKeys,
|
||||
payloadType: detectPairingPayloadType(normalizedKeys),
|
||||
createdAtMs: Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -160,12 +160,17 @@ export class KeyRotationClient {
|
||||
}
|
||||
const state=await this.notifyPdaRotation(signature);
|
||||
if(storagePwd) {
|
||||
await updateEncryptedUserSecrets(login, storagePwd, (current)=>({
|
||||
...(current||{}),
|
||||
rootKey:newBundle.rootPair.privatePkcs8B64,
|
||||
blockchainKey:newBundle.blockchainPair.privatePkcs8B64,
|
||||
clientKey:newBundle.clientPair.privatePkcs8B64,
|
||||
}));
|
||||
await updateEncryptedUserSecrets(login, storagePwd, (current)=>{
|
||||
const next={
|
||||
...(current||{}),
|
||||
blockchainKey:newBundle.blockchainPair.privatePkcs8B64,
|
||||
clientKey:newBundle.clientPair.privatePkcs8B64,
|
||||
};
|
||||
// Arweave SAWD-v1 привязан к blockchain key: после ротации старый JWK-cache недействителен.
|
||||
delete next.arweaveWallet;
|
||||
delete next.rootKey;
|
||||
return next;
|
||||
});
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,15 @@ function normalizeLoginStorageKey(login) {
|
||||
return String(login || '').trim().toLowerCase();
|
||||
}
|
||||
|
||||
function sanitizePersistentSecrets(keys) {
|
||||
const source = keys && typeof keys === 'object' ? keys : {};
|
||||
const safe = { ...source };
|
||||
// Recovery/root private key is intentionally ephemeral: it may exist in RAM
|
||||
// during a protected operation, but must never be persisted on the device.
|
||||
delete safe.rootKey;
|
||||
return safe;
|
||||
}
|
||||
|
||||
function openDb() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
||||
@@ -53,7 +62,8 @@ async function get(storeName, key) {
|
||||
}
|
||||
|
||||
export async function saveEncryptedUserSecrets(login, storagePwd, keys) {
|
||||
const encrypted = await encryptJsonWithStoragePwd(keys, storagePwd);
|
||||
const safeKeys = sanitizePersistentSecrets(keys);
|
||||
const encrypted = await encryptJsonWithStoragePwd(safeKeys, storagePwd);
|
||||
const normalizedLogin = normalizeLoginStorageKey(login);
|
||||
await put(STORE_SECRETS, {
|
||||
login: normalizedLogin,
|
||||
@@ -71,7 +81,22 @@ export async function loadEncryptedUserSecrets(login, storagePwd) {
|
||||
if (!row?.encrypted) {
|
||||
throw new Error('На устройстве нет сохранённых ключей для этого логина');
|
||||
}
|
||||
return decryptJsonWithStoragePwd(row.encrypted, storagePwd);
|
||||
|
||||
const decrypted = await decryptJsonWithStoragePwd(row.encrypted, storagePwd);
|
||||
const safe = sanitizePersistentSecrets(decrypted);
|
||||
|
||||
// Одноразовая очистка контейнеров, созданных старыми версиями UI, где rootKey
|
||||
// мог быть сохранён. Перезаписываем тем же storagePwd уже без Recovery private key.
|
||||
if (decrypted && typeof decrypted === 'object' && Object.prototype.hasOwnProperty.call(decrypted, 'rootKey')) {
|
||||
const encrypted = await encryptJsonWithStoragePwd(safe, storagePwd);
|
||||
await put(STORE_SECRETS, {
|
||||
login: normalizedLogin,
|
||||
encrypted,
|
||||
updatedAtMs: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
return safe;
|
||||
}
|
||||
|
||||
export async function updateEncryptedUserSecrets(login, storagePwd, updater) {
|
||||
@@ -84,8 +109,9 @@ export async function updateEncryptedUserSecrets(login, storagePwd, updater) {
|
||||
if (!next || typeof next !== 'object') {
|
||||
throw new Error('updateEncryptedUserSecrets: updater должен вернуть объект secrets');
|
||||
}
|
||||
await saveEncryptedUserSecrets(login, storagePwd, next);
|
||||
return next;
|
||||
const safe = sanitizePersistentSecrets(next);
|
||||
await saveEncryptedUserSecrets(login, storagePwd, safe);
|
||||
return safe;
|
||||
}
|
||||
|
||||
export async function saveSessionMaterial(login, material) {
|
||||
|
||||
@@ -22,30 +22,29 @@ function base64UrlToBytes(value) {
|
||||
}
|
||||
|
||||
export function keyLabel(id) {
|
||||
if (id === 'root') return 'root';
|
||||
if (id === 'blockchain') return 'blockchain';
|
||||
if (id === 'device') return 'device';
|
||||
if (id === 'blockchain') return 'ключ блокчейна';
|
||||
if (id === 'device' || id === 'client') return 'ключ доступа';
|
||||
return id;
|
||||
}
|
||||
|
||||
export function describeTransferKeys(keys = {}) {
|
||||
const out = [];
|
||||
if (keys.clientKey) out.push('device');
|
||||
if (keys.blockchainKey) out.push('blockchain');
|
||||
if (keys.rootKey) out.push('root');
|
||||
if (keys.clientKey) out.push('ключ доступа');
|
||||
if (keys.blockchainKey) out.push('ключ блокчейна');
|
||||
return out;
|
||||
}
|
||||
|
||||
export function makeKeyTransferText({ login, keys }) {
|
||||
const clientKey = String(keys?.clientKey || '').trim();
|
||||
const blockchainKey = String(keys?.blockchainKey || '').trim();
|
||||
if (!clientKey || !blockchainKey) {
|
||||
throw new Error('Для подключения нужны ключ доступа и ключ блокчейна');
|
||||
}
|
||||
const payload = {
|
||||
v: 1,
|
||||
type: 'shine-key-transfer',
|
||||
login: String(login || '').trim(),
|
||||
keys: {
|
||||
clientKey: String(keys?.clientKey || ''),
|
||||
blockchainKey: String(keys?.blockchainKey || ''),
|
||||
rootKey: String(keys?.rootKey || ''),
|
||||
},
|
||||
keys: { clientKey, blockchainKey },
|
||||
createdAtMs: Date.now(),
|
||||
};
|
||||
const json = JSON.stringify(payload);
|
||||
@@ -65,17 +64,15 @@ export function parseKeyTransferText(text) {
|
||||
const login = String(payload.login || '').trim();
|
||||
if (!login) throw new Error('В QR-коде нет логина');
|
||||
const keys = payload.keys && typeof payload.keys === 'object' ? payload.keys : {};
|
||||
if (!keys.clientKey && !keys.blockchainKey && !keys.rootKey) {
|
||||
throw new Error('В QR-коде нет ключей');
|
||||
const clientKey = String(keys.clientKey || '').trim();
|
||||
const blockchainKey = String(keys.blockchainKey || '').trim();
|
||||
if (!clientKey || !blockchainKey) {
|
||||
throw new Error('В QR-коде нет полного набора рабочих ключей');
|
||||
}
|
||||
return {
|
||||
login,
|
||||
keys: {
|
||||
clientKey: String(keys.clientKey || ''),
|
||||
blockchainKey: String(keys.blockchainKey || ''),
|
||||
rootKey: String(keys.rootKey || ''),
|
||||
},
|
||||
keyTypes: describeTransferKeys(keys),
|
||||
keys: { clientKey, blockchainKey },
|
||||
keyTypes: describeTransferKeys({ clientKey, blockchainKey }),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -323,20 +323,26 @@ function toJwkB64(value) {
|
||||
return base64UrlEncode(bigIntToUnsignedBytes(value));
|
||||
}
|
||||
|
||||
async function deriveArweaveWalletParts(clientKey32) {
|
||||
if (!(clientKey32 instanceof Uint8Array)) {
|
||||
throw new Error('SAWD-v1: clientKey32 должен быть Uint8Array');
|
||||
async function deriveArweaveWalletParts(blockchainKey32) {
|
||||
if (!(blockchainKey32 instanceof Uint8Array)) {
|
||||
throw new Error('SAWD-v1: blockchainKey32 должен быть Uint8Array');
|
||||
}
|
||||
if (clientKey32.length !== 32) {
|
||||
throw new Error('SAWD-v1: clientKey32 должен быть ровно 32 байта');
|
||||
if (blockchainKey32.length !== 32) {
|
||||
throw new Error('SAWD-v1: blockchainKey32 должен быть ровно 32 байта');
|
||||
}
|
||||
|
||||
const masterSeed32 = await hmacSha256(MASTER_LABEL_UTF8, clientKey32);
|
||||
const masterSeed32 = await hmacSha256(MASTER_LABEL_UTF8, blockchainKey32);
|
||||
const masterSeedKey = await importHmacKey(masterSeed32);
|
||||
|
||||
const pResult = await derivePrimeWithImportedKey(masterSeedKey, 'p');
|
||||
let qResult = await derivePrimeWithImportedKey(masterSeedKey, 'q');
|
||||
while (qResult.prime === pResult.prime) {
|
||||
// Две 2048-битные простые не гарантируют 4096-битный product: n может иметь
|
||||
// 4095 бит. До публичного запуска legacy-output не фиксирован, поэтому SAWD-v1
|
||||
// детерминированно продолжает q-sequence, пока modulus не станет ровно RSA_BITS.
|
||||
while (
|
||||
qResult.prime === pResult.prime
|
||||
|| bitLength(pResult.prime * qResult.prime) !== RSA_BITS
|
||||
) {
|
||||
qResult = await derivePrimeWithImportedKey(masterSeedKey, 'q', qResult.index + 1n);
|
||||
}
|
||||
|
||||
@@ -381,8 +387,8 @@ async function deriveArweaveWalletParts(clientKey32) {
|
||||
};
|
||||
}
|
||||
|
||||
export async function deriveArweaveWalletFromClientKey32(clientKey32) {
|
||||
const result = await deriveArweaveWalletParts(clientKey32);
|
||||
export async function deriveArweaveWalletFromBlockchainKey32(blockchainKey32) {
|
||||
const result = await deriveArweaveWalletParts(blockchainKey32);
|
||||
return {
|
||||
derivation: result.derivation,
|
||||
jwk: result.jwk,
|
||||
@@ -395,7 +401,7 @@ export async function selfTestSawdV1() {
|
||||
const invalid = new Uint8Array(31);
|
||||
let invalidFailed = false;
|
||||
try {
|
||||
await deriveArweaveWalletFromClientKey32(invalid);
|
||||
await deriveArweaveWalletFromBlockchainKey32(invalid);
|
||||
} catch {
|
||||
invalidFailed = true;
|
||||
}
|
||||
@@ -403,13 +409,13 @@ export async function selfTestSawdV1() {
|
||||
throw new Error('SAWD-v1 self-test: длина != 32 должна приводить к ошибке');
|
||||
}
|
||||
|
||||
const clientKey = new Uint8Array(32);
|
||||
for (let i = 0; i < clientKey.length; i += 1) {
|
||||
clientKey[i] = i + 1;
|
||||
const blockchainKey = new Uint8Array(32);
|
||||
for (let i = 0; i < blockchainKey.length; i += 1) {
|
||||
blockchainKey[i] = i + 1;
|
||||
}
|
||||
|
||||
const first = await deriveArweaveWalletParts(clientKey);
|
||||
const second = await deriveArweaveWalletParts(clientKey);
|
||||
const first = await deriveArweaveWalletParts(blockchainKey);
|
||||
const second = await deriveArweaveWalletParts(blockchainKey);
|
||||
|
||||
if (!first.address || !second.address) {
|
||||
throw new Error('SAWD-v1 self-test: адрес пустой');
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { extractClientKey32FromStoredValue } from './client-key-utils.js';
|
||||
import { extractEd25519Seed32FromStoredValue } from './client-key-utils.js';
|
||||
import { base64ToBytes } from './crypto-utils.js';
|
||||
import { loadEncryptedUserSecrets } from './key-vault.js';
|
||||
import { SOLANA_ENDPOINT_DEFAULT } from '../solana-programs.js';
|
||||
@@ -104,7 +104,7 @@ function decodeBase58(input) {
|
||||
|
||||
async function keypairFromPkcs8(pkcs8B64) {
|
||||
const solana = await loadSolanaLib();
|
||||
const seed32 = extractClientKey32FromStoredValue(pkcs8B64);
|
||||
const seed32 = extractEd25519Seed32FromStoredValue(pkcs8B64);
|
||||
try {
|
||||
return solana.Keypair.fromSeed(seed32);
|
||||
} finally {
|
||||
@@ -114,7 +114,7 @@ async function keypairFromPkcs8(pkcs8B64) {
|
||||
|
||||
async function keypairFromStoredSecret(storedSecret) {
|
||||
const solana = await loadSolanaLib();
|
||||
const seed32 = extractClientKey32FromStoredValue(storedSecret);
|
||||
const seed32 = extractEd25519Seed32FromStoredValue(storedSecret);
|
||||
try {
|
||||
return solana.Keypair.fromSeed(seed32);
|
||||
} finally {
|
||||
@@ -161,7 +161,7 @@ export async function createSolanaWalletFromPrivateBase58(privateKey32Base58) {
|
||||
};
|
||||
}
|
||||
|
||||
export async function getWalletFromStoredClientKey({ login, storagePwd }) {
|
||||
) {
|
||||
const cleanLogin = String(login || '').trim();
|
||||
const cleanPwd = String(storagePwd || '').trim();
|
||||
if (!cleanLogin || !cleanPwd) {
|
||||
@@ -200,7 +200,7 @@ export async function getWalletFromStoredBlockchainKey({ login, storagePwd }) {
|
||||
};
|
||||
}
|
||||
|
||||
export async function getWalletFromStoredRootKey({ login, storagePwd }) {
|
||||
) {
|
||||
const cleanLogin = String(login || '').trim();
|
||||
const cleanPwd = String(storagePwd || '').trim();
|
||||
if (!cleanLogin || !cleanPwd) {
|
||||
@@ -227,7 +227,7 @@ export async function getStoredSolanaWalletChoices({ login, storagePwd } = {}) {
|
||||
return [{
|
||||
id: 'blockchain-key',
|
||||
keySource: 'blockchain',
|
||||
label: 'blockchain key',
|
||||
label: 'Ключ блокчейна',
|
||||
address: wallet.address,
|
||||
keypair: wallet.keypair,
|
||||
}];
|
||||
@@ -289,6 +289,70 @@ export async function transferSol({ endpoint, fromKeypair, toAddress, amountSol
|
||||
return { endpoint: rpc, signature, lamports };
|
||||
}
|
||||
|
||||
export async function transferAllSol({ endpoint, fromKeypair, toAddress }) {
|
||||
const solana = await loadSolanaLib();
|
||||
const rpc = normalizeEndpoint(endpoint);
|
||||
const cleanTo = String(toAddress || '').trim();
|
||||
if (!fromKeypair?.publicKey) throw new Error('Не передан старый Solana-кошелёк');
|
||||
if (!cleanTo) throw new Error('Не указан новый адрес кошелька');
|
||||
if (fromKeypair.publicKey.toBase58() === cleanTo) {
|
||||
throw new Error('Старый и новый кошельки совпадают');
|
||||
}
|
||||
|
||||
const conn = new solana.Connection(rpc, 'confirmed');
|
||||
const balanceLamports = await conn.getBalance(fromKeypair.publicKey, 'confirmed');
|
||||
if (balanceLamports <= 0) {
|
||||
return { endpoint: rpc, signature: '', lamports: 0, feeLamports: 0, balanceLamports, skipped: true };
|
||||
}
|
||||
|
||||
const latest = await conn.getLatestBlockhash('confirmed');
|
||||
const probeTx = new solana.Transaction({
|
||||
feePayer: fromKeypair.publicKey,
|
||||
recentBlockhash: latest.blockhash,
|
||||
}).add(solana.SystemProgram.transfer({
|
||||
fromPubkey: fromKeypair.publicKey,
|
||||
toPubkey: new solana.PublicKey(cleanTo),
|
||||
lamports: 1,
|
||||
}));
|
||||
const feeResult = await conn.getFeeForMessage(probeTx.compileMessage(), 'confirmed');
|
||||
const feeLamports = Number(feeResult?.value ?? 5000);
|
||||
const transferLamports = balanceLamports - feeLamports;
|
||||
if (transferLamports <= 0) {
|
||||
return {
|
||||
endpoint: rpc,
|
||||
signature: '',
|
||||
lamports: 0,
|
||||
feeLamports,
|
||||
balanceLamports,
|
||||
skipped: true,
|
||||
};
|
||||
}
|
||||
|
||||
const tx = new solana.Transaction({
|
||||
feePayer: fromKeypair.publicKey,
|
||||
recentBlockhash: latest.blockhash,
|
||||
}).add(solana.SystemProgram.transfer({
|
||||
fromPubkey: fromKeypair.publicKey,
|
||||
toPubkey: new solana.PublicKey(cleanTo),
|
||||
lamports: transferLamports,
|
||||
}));
|
||||
tx.sign(fromKeypair);
|
||||
const signature = await conn.sendRawTransaction(tx.serialize(), { skipPreflight: false });
|
||||
await conn.confirmTransaction({
|
||||
signature,
|
||||
blockhash: latest.blockhash,
|
||||
lastValidBlockHeight: latest.lastValidBlockHeight,
|
||||
}, 'confirmed');
|
||||
return {
|
||||
endpoint: rpc,
|
||||
signature,
|
||||
lamports: transferLamports,
|
||||
feeLamports,
|
||||
balanceLamports,
|
||||
skipped: false,
|
||||
};
|
||||
}
|
||||
|
||||
export function formatSol(value, digits = 6) {
|
||||
const n = Number(value);
|
||||
if (!Number.isFinite(n)) return '0';
|
||||
|
||||
@@ -146,7 +146,7 @@ export function toUserMessage(error, fallback = 'Действие не выпо
|
||||
}
|
||||
|
||||
if (code === 'UNSUPPORTED_KEY_ALGORITHM' || text.includes('unsupported key algorithm')) {
|
||||
return 'Ключ устройства не поддерживается сервером. Очистите локальные ключи и войдите заново.';
|
||||
return 'Ключ доступа не поддерживается сервером. Очистите локальные ключи и войдите заново.';
|
||||
}
|
||||
|
||||
if (!raw) return fallback;
|
||||
|
||||
@@ -437,19 +437,6 @@ function createInitialState({ withStoredSession = true } = {}) {
|
||||
walletAddress: '',
|
||||
balanceSOL: '0.0000',
|
||||
},
|
||||
keyStorage: {
|
||||
rootKey: 'Ключ root хранится в зашифрованном виде',
|
||||
blockchainKey: 'Ключ blockchain хранится в зашифрованном виде',
|
||||
clientKey: 'Client key хранится в зашифрованном виде',
|
||||
saveRoot: true,
|
||||
saveBlockchain: true,
|
||||
saveClient: true,
|
||||
},
|
||||
deviceConnect: {
|
||||
root: true,
|
||||
blockchain: true,
|
||||
client: true,
|
||||
},
|
||||
authUi: {
|
||||
busy: false,
|
||||
error: '',
|
||||
@@ -1113,7 +1100,6 @@ export function resetRegistrationFlow() {
|
||||
state.registrationDraft = next.registrationDraft;
|
||||
state.registrationHelp = next.registrationHelp;
|
||||
state.registrationPayment = next.registrationPayment;
|
||||
state.keyStorage = next.keyStorage;
|
||||
}
|
||||
|
||||
function resetStateForSignedOut() {
|
||||
@@ -1134,8 +1120,6 @@ function resetStateForSignedOut() {
|
||||
state.registrationDraft = next.registrationDraft;
|
||||
state.loginDraft = next.loginDraft;
|
||||
state.registrationPayment = next.registrationPayment;
|
||||
state.keyStorage = next.keyStorage;
|
||||
state.deviceConnect = next.deviceConnect;
|
||||
state.authUi = next.authUi;
|
||||
state.sessions = next.sessions;
|
||||
state.channelsFeed = next.channelsFeed;
|
||||
|
||||
@@ -245,7 +245,7 @@
|
||||
transform: translate(-50%, -50%);
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
background: url("../assets/glass_overlay_faithful.png") center / contain no-repeat;
|
||||
background: url("../../assets/glass_overlay_faithful.png") center / contain no-repeat;
|
||||
box-shadow: none;
|
||||
opacity: 1;
|
||||
pointer-events: none;
|
||||
|
||||
@@ -1,27 +1,5 @@
|
||||
/* Diagnostics/admin/developer UI styles. */
|
||||
|
||||
.key-storage-option {
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
|
||||
.key-storage-option__description {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
|
||||
.key-storage-note {
|
||||
margin: 0;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
|
||||
.key-storage-note--strong {
|
||||
font-size: 17px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
|
||||
.pwa-diag-list {
|
||||
gap: 8px;
|
||||
|
||||
Reference in New Issue
Block a user