Миграция PDA на client.key

This commit is contained in:
AidarKC
2026-06-22 21:57:09 +04:00
parent ba348dafb3
commit 5c92b6a734
133 changed files with 941 additions and 30531 deletions
+3 -3
View File
@@ -21,7 +21,7 @@ export function render({ navigate }) {
<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 /> device key</label>
<label class="checkbox-row"><input type="checkbox" id="connect-device" checked disabled /> client key</label>
<p class="meta-muted" id="connect-keys-status">Проверяем ключи на этом устройстве...</p>
<div class="row">
<button class="icon-btn small-btn" type="button" id="tech-help">Техсправка</button>
@@ -68,7 +68,7 @@ export function render({ navigate }) {
<p class="meta-muted">если ключа нет — он недоступен</p>
<p class="meta-muted">blockchain key — можно передать или нет</p>
<p class="meta-muted">root key — только если существует</p>
<p class="meta-muted">device 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>
@@ -100,7 +100,7 @@ export function render({ navigate }) {
const savedKeys = await loadEncryptedUserSecrets(state.session.login, state.session.storagePwdInMemory);
const hasRoot = Boolean(savedKeys.rootKey);
const hasBlockchain = Boolean(savedKeys.blockchainKey);
const hasDevice = Boolean(savedKeys.deviceKey);
const hasDevice = Boolean(savedKeys.clientKey);
rootToggle.disabled = !hasRoot;
blockchainToggle.disabled = !hasBlockchain;
+2 -2
View File
@@ -11,7 +11,7 @@ import {
promptPwaInstall,
} from '../services/pwa-install-service.js';
import { initPwaPush } from '../services/pwa-push-service.js';
import { getArweaveWalletFromStoredDeviceKey } from '../services/arweave-wallet-service.js';
import { getArweaveWalletFromStoredClientKey } from '../services/arweave-wallet-service.js';
import {
prepareAvatarImageFile,
uploadArweaveFile,
@@ -143,7 +143,7 @@ function openDeveloperAvatarUploadModal({ walletLogin, storagePwd, gateway } = {
+ `(${optimized.width}x${optimized.height}, ${optimized.contentType})`,
);
walletCtx = await getArweaveWalletFromStoredDeviceKey({
walletCtx = await getArweaveWalletFromStoredClientKey({
login: walletLogin,
storagePwd,
onStatus: (message) => setMeta(message),
+7 -7
View File
@@ -38,12 +38,12 @@ function pairingSessionKindLabel(sessionType) {
function buildTransferKeys(savedKeys, { withExtras = false }) {
const keys = {
deviceKey: String(savedKeys?.deviceKey || '').trim(),
clientKey: String(savedKeys?.clientKey || savedKeys?.clientKey || '').trim(),
blockchainKey: '',
rootKey: '',
};
if (!keys.deviceKey) {
throw new Error('На этом устройстве нет сохранённого device key для передачи.');
if (!keys.clientKey) {
throw new Error('На этом устройстве нет сохранённого client key для передачи.');
}
if (withExtras) {
if (state.deviceConnect.blockchain && savedKeys?.blockchainKey) {
@@ -432,12 +432,12 @@ export function render({ navigate }) {
const loadSavedKeys = async () => {
savedKeys = await loadEncryptedUserSecrets(state.session.login, state.session.storagePwdInMemory);
const available = [];
if (savedKeys?.deviceKey) available.push('device');
if (savedKeys?.clientKey || savedKeys?.clientKey) available.push('client');
if (savedKeys?.blockchainKey && state.deviceConnect.blockchain) available.push('blockchain');
if (savedKeys?.rootKey && state.deviceConnect.root) available.push('root');
keySummaryEl.textContent = available.length
? `При расширенном подключении будут переданы: ${available.join(', ')}.`
: 'На этом устройстве доступен только device key.';
: 'На этом устройстве доступен только client key.';
};
const reloadRequests = async ({ silent = false } = {}) => {
@@ -464,9 +464,9 @@ export function render({ navigate }) {
const withExtras = mode === 'with-extras';
let payload;
if (!withExtras && Number(request?.requesterSessionType || 0) === SESSION_TYPE_WALLET) {
const delegatedSession = await authService.createDelegatedSessionWithDeviceKey({
const delegatedSession = await authService.createDelegatedSessionWithClientKey({
login: state.session.login,
devicePrivPkcs8: String(savedKeys?.deviceKey || '').trim(),
clientPrivPkcs8: String(savedKeys?.clientKey || savedKeys?.clientKey || '').trim(),
sessionKey: String(request?.requesterSessionKey || '').trim(),
sessionType: Number(request?.requesterSessionType || SESSION_TYPE_WALLET) || SESSION_TYPE_WALLET,
clientPlatform: String(request?.requesterClientPlatform || '').trim() || 'Wallet plugin',
+2 -2
View File
@@ -42,11 +42,11 @@ export function render({ navigate }) {
}
const savedKeys = await loadEncryptedUserSecrets(state.session.login, state.session.storagePwdInMemory);
const keys = {
deviceKey: savedKeys.deviceKey || '',
clientKey: savedKeys.clientKey || '',
blockchainKey: state.deviceConnect.blockchain ? (savedKeys.blockchainKey || '') : '',
rootKey: state.deviceConnect.root ? (savedKeys.rootKey || '') : '',
};
if (!keys.deviceKey) throw new Error('На этом устройстве нет device key');
if (!keys.clientKey) throw new Error('На этом устройстве нет client key');
const qrText = makeKeyTransferText({ login: state.session.login, keys });
qrEl.innerHTML = renderQrSvg(qrText);
+2 -2
View File
@@ -43,7 +43,7 @@ export function render({ navigate }) {
</div>
<div class="key-card stack">
<label class="checkbox-row"><span class="field-label">Device Key</span></label>
<input class="input" type="text" value="${state.keyStorage.deviceKey}" />
<input class="input" type="text" value="${state.keyStorage.clientKey}" />
</div>
`;
@@ -63,7 +63,7 @@ export function render({ navigate }) {
const deviceInput = card.children[2].querySelector('.input');
deviceInput.addEventListener('input', () => {
state.keyStorage.deviceKey = deviceInput.value;
state.keyStorage.clientKey = deviceInput.value;
});
const actions = document.createElement('div');
+2 -2
View File
@@ -141,8 +141,8 @@ export function render({ navigate }) {
const parseTransferText = (text) => {
try {
const transfer = parseKeyTransferText(text);
if (!transfer.keys.deviceKey) {
throw new Error('В QR-коде нет device key для входа');
if (!transfer.keys.clientKey) {
throw new Error('В QR-коде нет client key для входа');
}
showTransfer(transfer);
} catch (error) {
+1 -1
View File
@@ -379,7 +379,7 @@ export function render({ navigate }) {
const details2 = document.createElement('p');
details2.className = 'meta-muted';
details2.textContent = 'Из этого секрета строятся root key, blockchain key и device key. Это может занять некоторое время.';
details2.textContent = 'Из этого секрета строятся recovery key, root key, blockchain key и client key. Это может занять некоторое время.';
const details3 = document.createElement('p');
details3.className = 'meta-muted';
@@ -1,7 +1,7 @@
import { renderHeader } from '../components/header.js';
import { state } from '../state.js';
import { base64ToBytes, bytesToBase58 } from '../services/crypto-utils.js';
import { extractSeed32FromPkcs8B64 } from '../services/device-key-utils.js';
import { extractSeed32FromPkcs8B64 } from '../services/client-key-utils.js';
export const pageMeta = { id: 'registration-draft-keys-view', title: 'Сгенерированные ключи', showAppChrome: false };
@@ -109,6 +109,20 @@ export function render({ navigate }) {
}
card.append(makeSecretField({ label: 'Главный секрет (master secret, base58, 32 байта)', value: secretB58 }));
// Recovery key
const recoverySep = document.createElement('p');
recoverySep.className = 'field-label';
recoverySep.textContent = 'Recovery key';
card.append(recoverySep);
card.append(makePublicField({
label: 'Recovery — публичный (base58)',
value: bytesToBase58(base64ToBytes(keyBundle.recoveryPair.publicKeyB64)),
}));
card.append(makeSecretField({
label: 'Recovery — приватный (seed base58, 32 байта)',
value: bytesToBase58(extractSeed32FromPkcs8B64(keyBundle.recoveryPair.privatePkcs8B64)),
}));
// Root key
const rootSep = document.createElement('p');
rootSep.className = 'field-label';
@@ -137,18 +151,18 @@ export function render({ navigate }) {
value: bytesToBase58(extractSeed32FromPkcs8B64(keyBundle.blockchainPair.privatePkcs8B64)),
}));
// Device key
// Client key
const devSep = document.createElement('p');
devSep.className = 'field-label';
devSep.textContent = 'Device key (= Solana wallet)';
devSep.textContent = 'Client key (= Solana wallet)';
card.append(devSep);
card.append(makePublicField({
label: 'Device — публичный (base58)',
value: bytesToBase58(base64ToBytes(keyBundle.devicePair.publicKeyB64)),
label: 'Client — публичный (base58)',
value: bytesToBase58(base64ToBytes(keyBundle.clientPair.publicKeyB64)),
}));
card.append(makeSecretField({
label: 'Device — приватный (seed base58, 32 байта)',
value: bytesToBase58(extractSeed32FromPkcs8B64(keyBundle.devicePair.privatePkcs8B64)),
label: 'Client — приватный (seed base58, 32 байта)',
value: bytesToBase58(extractSeed32FromPkcs8B64(keyBundle.clientPair.privatePkcs8B64)),
}));
}
+2 -2
View File
@@ -30,7 +30,7 @@ export const REGISTRATION_FAQ_TOPICS = [
title: 'Как генерируются ключи и что делает пароль?',
paragraphs: [
'Из вашего логина и пароля с помощью Argon2id вычисляется специальный секрет.',
'Уже из этого секрета детерминированно строятся три основных ключа: root key, blockchain key и device key.',
'Уже из этого секрета детерминированно строятся четыре основных ключа: recovery key, root key, blockchain key и client key.',
'Это значит, что логин и пароль не просто проверяются на сервере, а реально участвуют в создании ваших ключей. У разных логинов даже с одинаковым паролем будут разные ключи.',
],
},
@@ -41,7 +41,7 @@ export const REGISTRATION_FAQ_TOPICS = [
paragraphs: [
'Root key нужен для управления вашей основной публичной записью и важными изменениями личности, включая обновление главной публичной части в Solana.',
'Blockchain key нужен для подписания действий и записей в блокчейне SHiNE.',
'Device key нужен для входов и работы конкретного устройства. Благодаря разделению ключей можно точнее выдавать права одним устройствам и не выдавать другим.',
'Client key нужен для входов и работы конкретного устройства. Благодаря разделению ключей можно точнее выдавать права одним устройствам и не выдавать другим.',
'Если не хочется в это вникать, обычно можно просто сохранить все ключи на своём устройстве. Для большинства обычных сценариев на iPhone, Android и Linux это вполне практично. Для больших сумм или повышенного риска лучше отдельное внешнее устройство.',
],
},
@@ -113,7 +113,7 @@ export function render({ navigate }) {
const deriveUserWalletAddress = async () => {
const keyBundle = state.registrationDraft.preGeneratedKeyBundle;
if (!keyBundle) throw new Error('Ключи ещё не сгенерированы. Вернитесь на предыдущий шаг.');
const { publicKeyB64 } = keyBundle.devicePair;
const { publicKeyB64 } = keyBundle.clientPair;
const raw = atob(publicKeyB64);
const bytes = new Uint8Array(raw.length);
for (let i = 0; i < raw.length; i++) bytes[i] = raw.charCodeAt(i);
@@ -219,7 +219,7 @@ export function render({ navigate }) {
card.innerHTML = `
<p class="auth-copy">Для регистрации в тестовой Solana нужно минимум 0,01 SOL на вашем кошельке.</p>
<label class="stack"><span class="field-label">Номер кошелька (wallet.key = device.key)</span></label>
<label class="stack"><span class="field-label">Номер кошелька (client.key)</span></label>
<div class="stack">
<span class="field-label">Баланс (Solana)</span>
</div>
@@ -242,7 +242,7 @@ export function render({ navigate }) {
await refreshBalance({ addressOverride: walletAddress });
} catch (error) {
status.className = 'status-line is-unavailable';
status.textContent = `Не удалось подготовить wallet.key: ${error?.message || 'unknown'}`;
status.textContent = `Не удалось подготовить client.key: ${error?.message || 'unknown'}`;
status.style.display = '';
}
})();
+4 -4
View File
@@ -1,7 +1,7 @@
import { renderHeader } from '../components/header.js';
import { state } from '../state.js';
import { bytesToBase58 } from '../services/crypto-utils.js';
import { extractSeed32FromPkcs8B64 } from '../services/device-key-utils.js';
import { extractSeed32FromPkcs8B64 } from '../services/client-key-utils.js';
import { loadEncryptedUserSecrets } from '../services/key-vault.js';
export const pageMeta = { id: 'show-keys-view', title: 'Показать ключи' };
@@ -68,7 +68,7 @@ export function render({ navigate }) {
card.append(
renderField('root', 'root key (base58)'),
renderField('blockchain', 'blockchain.key (base58)'),
renderField('device', 'device key (base58)'),
renderField('device', 'client key (base58)'),
);
const setMissingState = (id) => {
@@ -133,11 +133,11 @@ export function render({ navigate }) {
const rootSeed32 = savedKeys.rootKey ? extractSeed32FromPkcs8B64(savedKeys.rootKey) : null;
const blockchainSeed32 = savedKeys.blockchainKey ? extractSeed32FromPkcs8B64(savedKeys.blockchainKey) : null;
const deviceSeed32 = savedKeys.deviceKey ? extractSeed32FromPkcs8B64(savedKeys.deviceKey) : null;
const clientSeed32 = savedKeys.clientKey ? extractSeed32FromPkcs8B64(savedKeys.clientKey) : null;
keys.root = rootSeed32 ? bytesToBase58(rootSeed32) : '';
keys.blockchain = blockchainSeed32 ? bytesToBase58(blockchainSeed32) : '';
keys.device = deviceSeed32 ? bytesToBase58(deviceSeed32) : '';
keys.device = clientSeed32 ? bytesToBase58(clientSeed32) : '';
if (keys.root || keys.blockchain || keys.device) {
status.textContent = 'Показаны только ключи, сохранённые на этом устройстве.';
+5 -5
View File
@@ -12,12 +12,12 @@ export const pageMeta = { id: 'topup-view', title: 'Пополнение сче
// Канонический Solana-адрес пополнения = публичный device-ключ из сгенерированного набора ключей.
// Тот же путь, что в registration-payment-view (deriveUserWalletAddress); не выводим адрес
// напрямую из пароля, иначе он расходится с device-ключом регистрации.
async function deviceWalletAddressFromBundle() {
async function clientWalletAddressFromBundle() {
const keyBundle = state.registrationDraft.preGeneratedKeyBundle;
if (!keyBundle || !keyBundle.devicePair) {
if (!keyBundle || !keyBundle.clientPair) {
throw new Error('Ключи ещё не сгенерированы. Вернитесь на экран регистрации.');
}
const raw = atob(keyBundle.devicePair.publicKeyB64);
const raw = atob(keyBundle.clientPair.publicKeyB64);
const bytes = new Uint8Array(raw.length);
for (let i = 0; i < raw.length; i += 1) bytes[i] = raw.charCodeAt(i);
const { PublicKey } = await import('https://esm.sh/@solana/web3.js@1.98.4');
@@ -69,7 +69,7 @@ export function render({ navigate }) {
</div>
<a class="link-card" id="topup-site-link" href="${getTopupSiteUrl(state.registrationPayment.walletAddress || '')}" target="_blank" rel="noreferrer">Открыть сайт пополнения</a>
<div class="card stack" style="padding:12px; max-width:320px;">
<div class="field-label" style="margin-bottom:6px;">Кошелёк для пополнения (device key = Solana wallet)</div>
<div class="field-label" style="margin-bottom:6px;">Кошелёк для пополнения (client key = Solana wallet)</div>
</div>
`;
card.children[3].append(walletRow);
@@ -117,7 +117,7 @@ export function render({ navigate }) {
(async () => {
try {
if (!walletValue.value) {
const address = await deviceWalletAddressFromBundle();
const address = await clientWalletAddressFromBundle();
state.registrationPayment.walletAddress = address;
walletValue.value = address;
}
+17 -17
View File
@@ -6,14 +6,14 @@ import {
formatSol,
getBalanceSol,
getTopupSiteUrl,
getWalletFromStoredDeviceKey,
getWalletFromStoredClientKey,
transferSol,
} from '../services/solana-wallet-service.js';
import {
formatAr,
getArweaveBalance,
getArweaveTopupSiteUrl,
getArweaveWalletFromStoredDeviceKey,
getArweaveWalletFromStoredClientKey,
transferAr,
} from '../services/arweave-wallet-service.js';
import { loadEncryptedUserSecrets } from '../services/key-vault.js';
@@ -262,10 +262,10 @@ export function render({ navigate }) {
}
let rootKey = String(saved?.rootKey || '').trim();
let blockchainKey = String(saved?.blockchainKey || '').trim();
const deviceKey = String(saved?.deviceKey || '').trim();
if (!deviceKey) throw new Error('На устройстве нет device.key. Выполните вход заново.');
const clientKey = String(saved?.clientKey || '').trim();
if (!clientKey) throw new Error('На устройстве нет client.key. Выполните вход заново.');
if (rootKey && blockchainKey) {
return { rootPrivatePkcs8B64: rootKey, blockchainPrivatePkcs8B64: blockchainKey, devicePrivatePkcs8B64: deviceKey };
return { rootPrivatePkcs8B64: rootKey, blockchainPrivatePkcs8B64: blockchainKey, clientPrivatePkcs8B64: clientKey };
}
const password = window.prompt(
@@ -284,7 +284,7 @@ export function render({ navigate }) {
if (shouldSave) {
await authService.persistSelectedKeys(login, storagePwd, keyBundle, { saveRoot: true, saveBlockchain: true });
}
return { rootPrivatePkcs8B64: rootKey, blockchainPrivatePkcs8B64: blockchainKey, devicePrivatePkcs8B64: deviceKey };
return { rootPrivatePkcs8B64: rootKey, blockchainPrivatePkcs8B64: blockchainKey, clientPrivatePkcs8B64: clientKey };
};
const setButtonsDisabled = (disabled) => {
@@ -341,7 +341,7 @@ export function render({ navigate }) {
solanaEndpoint: state.entrySettings.solanaServer,
rootPrivatePkcs8B64: signing.rootPrivatePkcs8B64,
blockchainPrivatePkcs8B64: signing.blockchainPrivatePkcs8B64,
devicePrivatePkcs8B64: signing.devicePrivatePkcs8B64,
clientPrivatePkcs8B64: signing.clientPrivatePkcs8B64,
additionalLimitBytes: 0n,
nextUsedBytes: BigInt(Math.max(0, serverState.sizeBytes)),
nextLastBlockNumber: serverState.lastNumber,
@@ -388,7 +388,7 @@ export function render({ navigate }) {
solanaEndpoint: state.entrySettings.solanaServer,
rootPrivatePkcs8B64: signing.rootPrivatePkcs8B64,
blockchainPrivatePkcs8B64: signing.blockchainPrivatePkcs8B64,
devicePrivatePkcs8B64: signing.devicePrivatePkcs8B64,
clientPrivatePkcs8B64: signing.clientPrivatePkcs8B64,
additionalLimitBytes: addBytes,
});
if (modeToken !== activeModeToken) return;
@@ -443,7 +443,7 @@ export function render({ navigate }) {
addressCard.className = 'card';
addressCard.style.padding = '10px';
addressCard.innerHTML = `
<p class="meta-muted" style="margin-bottom:6px;">Публичный адрес (wallet.key = device.key)</p>
<p class="meta-muted" style="margin-bottom:6px;">Публичный адрес (client.key)</p>
<p style="font-size:13px; line-height:1.4; word-break:break-all;" id="wallet-address-value">—</p>
`;
const addressEl = addressCard.querySelector('#wallet-address-value');
@@ -705,7 +705,7 @@ export function render({ navigate }) {
sendBtn.addEventListener('click', async () => {
if (!walletCtx?.keypair) {
setStatus('Перевод недоступен: wallet.key не загружен.');
setStatus('Перевод недоступен: client.key не загружен.');
return;
}
const toAddress = window.prompt('Введите адрес получателя (Solana):', '');
@@ -741,17 +741,17 @@ export function render({ navigate }) {
});
content.append(backBtn, card, actions, generatedCard);
setStatus('Инициализация wallet.key...');
setStatus('Инициализация client.key...');
try {
walletCtx = await getWalletFromStoredDeviceKey(sessionArgsOrThrow());
walletCtx = await getWalletFromStoredClientKey(sessionArgsOrThrow());
if (modeToken !== activeModeToken) return;
walletAddress = walletCtx.address;
addressEl.textContent = walletAddress;
await refreshBalance();
} catch (error) {
if (modeToken !== activeModeToken) return;
addressEl.textContent = 'wallet.key недоступен';
addressEl.textContent = 'client.key недоступен';
setStatus(`Не удалось инициализировать кошелёк: ${error?.message || 'unknown'}`);
}
}
@@ -797,7 +797,7 @@ export function render({ navigate }) {
helpCard.innerHTML = `
<summary style="cursor:pointer; font-weight:600;">Как получен этот адрес?</summary>
<p class="meta-muted" style="margin-top:8px;">
SHiNE берёт ваш локальный device.key и по стандарту SAWD-v1 получает из него нативный Arweave-кошелёк.
SHiNE берёт ваш локальный client.key и по стандарту SAWD-v1 получает из него нативный Arweave-кошелёк.
Приватный ключ не отправляется на сервер. После первого расчёта он хранится только в зашифрованном контейнере этого устройства.
</p>
`;
@@ -899,14 +899,14 @@ export function render({ navigate }) {
try {
let wasFirstTimeGeneration = false;
arweaveWalletCtx = await getArweaveWalletFromStoredDeviceKey({
arweaveWalletCtx = await getArweaveWalletFromStoredClientKey({
...sessionArgsOrThrow(),
onStatus: (message) => {
const text = String(message || '').trim();
if (!text) return;
if (text.includes('впервые получаем Arweave-кошелёк')) {
wasFirstTimeGeneration = true;
setStatus('Подождите — ваш Arweave-ключ вычисляется из device key. Это происходит только один раз, потом будет мгновенно.');
setStatus('Подождите — ваш Arweave-ключ вычисляется из client key. Это происходит только один раз, потом будет мгновенно.');
return;
}
setStatus(text);
@@ -919,7 +919,7 @@ export function render({ navigate }) {
await refreshBalance();
} catch (error) {
if (modeToken !== activeModeToken) return;
addressEl.textContent = 'wallet.key недоступен';
addressEl.textContent = 'client.key недоступен';
clearArweaveSecretsInMemory();
setStatus(`Не удалось инициализировать Arweave-кошелёк: ${error?.message || 'unknown'}`);
}