Обновление архитектуры ключей и кошельков

This commit is contained in:
AidarKC
2026-10-01 17:11:38 +03:00
parent f452ce1f1e
commit 8d22602761
51 changed files with 565 additions and 898 deletions
+22 -125
View File
@@ -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' });