SHA256
Миграция PDA на client.key
This commit is contained in:
+4
-4
@@ -57,12 +57,12 @@ import * as serverSettingsView from './pages/server-settings-view.js?v=202606161
|
||||
import * as toolsSettingsView from './pages/tools-settings-view.js';
|
||||
import * as deviceView from './pages/device-view.js?v=202606131435';
|
||||
import * as connectDeviceView from './pages/connect-device-view.js?v=202606142055';
|
||||
import * as devicePairingView from './pages/device-pairing-view.js?v=202606180940';
|
||||
import * as clientPairingView from './pages/device-pairing-view.js?v=202606180940';
|
||||
import * as trustedDeviceLoginSettingsView from './pages/trusted-device-login-settings-view.js?v=202606180930';
|
||||
import * as deviceQrView from './pages/device-qr-view.js';
|
||||
import * as deviceCameraView from './pages/device-camera-view.js';
|
||||
import * as showKeysView from './pages/show-keys-view.js';
|
||||
import * as deviceSessionView from './pages/device-session-view.js?v=202606131435';
|
||||
import * as clientSessionView from './pages/device-session-view.js?v=202606131435';
|
||||
import * as languageView from './pages/language-view.js';
|
||||
import * as appLogView from './pages/app-log-view.js';
|
||||
import * as pwaDiagnosticsView from './pages/pwa-diagnostics-view.js';
|
||||
@@ -103,12 +103,12 @@ const routes = {
|
||||
'tools-settings-view': toolsSettingsView,
|
||||
'device-view': deviceView,
|
||||
'connect-device-view': connectDeviceView,
|
||||
'device-pairing-view': devicePairingView,
|
||||
'device-pairing-view': clientPairingView,
|
||||
'trusted-device-login-settings-view': trustedDeviceLoginSettingsView,
|
||||
'device-qr-view': deviceQrView,
|
||||
'device-camera-view': deviceCameraView,
|
||||
'show-keys-view': showKeysView,
|
||||
'device-session-view': deviceSessionView,
|
||||
'device-session-view': clientSessionView,
|
||||
'language-view': languageView,
|
||||
'app-log-view': appLogView,
|
||||
'pwa-diagnostics-view': pwaDiagnosticsView,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { authService } from '../state.js';
|
||||
import { getArweaveBalance, getArweaveWalletFromStoredDeviceKey } from '../services/arweave-wallet-service.js';
|
||||
import { getArweaveBalance, getArweaveWalletFromStoredClientKey } from '../services/arweave-wallet-service.js';
|
||||
import {
|
||||
buildArweaveDataUrl,
|
||||
getArweaveUploadPrice,
|
||||
@@ -596,7 +596,7 @@ export function openAvatarWizard({
|
||||
const errorEl = root.querySelector('[data-error="true"]');
|
||||
root.querySelector('[data-action="back"]')?.addEventListener('click', showStepChoice);
|
||||
try {
|
||||
walletCtx = await getArweaveWalletFromStoredDeviceKey({
|
||||
walletCtx = await getArweaveWalletFromStoredClientKey({
|
||||
login: cleanLogin,
|
||||
storagePwd: cleanStoragePwd,
|
||||
onStatus: (message) => {
|
||||
|
||||
@@ -15,7 +15,7 @@ export const wallet = {
|
||||
updatedAt: 'сегодня, 14:42',
|
||||
};
|
||||
|
||||
export const deviceSessions = [
|
||||
export const clientSessions = [
|
||||
{
|
||||
sessionId: 'sess_7c5e5c4b',
|
||||
clientInfoFromClient: 'Android 15; Pixel 9',
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)),
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -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 = '';
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -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 = 'Показаны только ключи, сохранённые на этом устройстве.';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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'}`);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { loadEncryptedUserSecrets, updateEncryptedUserSecrets } from './key-vault.js';
|
||||
import { extractDeviceKey32FromStoredValue } from './device-key-utils.js';
|
||||
import { deriveArweaveWalletFromDeviceKey32 } from './sawd-v1.js';
|
||||
import { extractClientKey32FromStoredValue } from './client-key-utils.js';
|
||||
import { deriveArweaveWalletFromClientKey32 } 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';
|
||||
@@ -102,11 +102,11 @@ function safeStatus(onStatus, text) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function getArweaveWalletFromStoredDeviceKey({ login, storagePwd, onStatus } = {}) {
|
||||
export async function getArweaveWalletFromStoredClientKey({ login, storagePwd, onStatus } = {}) {
|
||||
const cleanLogin = String(login || '').trim();
|
||||
const cleanPwd = String(storagePwd || '').trim();
|
||||
if (!cleanLogin || !cleanPwd) {
|
||||
throw new Error('Нет активной сессии для доступа к wallet.key');
|
||||
throw new Error('Нет активной сессии для доступа к client.key');
|
||||
}
|
||||
|
||||
const secrets = await loadEncryptedUserSecrets(cleanLogin, cleanPwd);
|
||||
@@ -116,19 +116,19 @@ export async function getArweaveWalletFromStoredDeviceKey({ login, storagePwd, o
|
||||
return cached;
|
||||
}
|
||||
|
||||
safeStatus(onStatus, 'Сейчас мы впервые получаем Arweave-кошелёк из вашего device key. Это может занять немного времени.');
|
||||
safeStatus(onStatus, 'Сейчас мы впервые получаем Arweave-кошелёк из вашего client key. Это может занять немного времени.');
|
||||
|
||||
const storedDeviceKey = String(secrets?.deviceKey || '').trim();
|
||||
if (!storedDeviceKey) {
|
||||
throw new Error('На устройстве не найден device.key (wallet.key)');
|
||||
const storedClientKey = String(secrets?.clientKey || '').trim();
|
||||
if (!storedClientKey) {
|
||||
throw new Error('На устройстве не найден client.key');
|
||||
}
|
||||
|
||||
const deviceKey32 = extractDeviceKey32FromStoredValue(storedDeviceKey);
|
||||
const clientKey32 = extractClientKey32FromStoredValue(storedClientKey);
|
||||
let wallet;
|
||||
try {
|
||||
wallet = await deriveArweaveWalletFromDeviceKey32(deviceKey32);
|
||||
wallet = await deriveArweaveWalletFromClientKey32(clientKey32);
|
||||
} finally {
|
||||
deviceKey32.fill(0);
|
||||
clientKey32.fill(0);
|
||||
}
|
||||
|
||||
const cachedWallet = {
|
||||
|
||||
@@ -809,22 +809,27 @@ export class AuthService {
|
||||
});
|
||||
if (isCancelled && isCancelled()) throw new Error('DERIVE_CANCELLED');
|
||||
|
||||
if (onProgress) onProgress({ percent: 94, stage: 'derive', message: 'Вычисление root key...' });
|
||||
if (onProgress) onProgress({ percent: 93, stage: 'derive', message: 'Вычисление recovery key...' });
|
||||
const recoveryPair = await deriveEd25519FromMasterSecret(masterSecret, 'recovery.key');
|
||||
if (isCancelled && isCancelled()) throw new Error('DERIVE_CANCELLED');
|
||||
|
||||
if (onProgress) onProgress({ percent: 95, stage: 'derive', message: 'Вычисление root key...' });
|
||||
const rootPair = await deriveEd25519FromMasterSecret(masterSecret, 'root.key');
|
||||
if (isCancelled && isCancelled()) throw new Error('DERIVE_CANCELLED');
|
||||
|
||||
if (onProgress) onProgress({ percent: 96, stage: 'derive', message: 'Вычисление blockchain key...' });
|
||||
const blockchainPair = await deriveEd25519FromMasterSecret(masterSecret, 'bch.key');
|
||||
if (onProgress) onProgress({ percent: 97, stage: 'derive', message: 'Вычисление blockchain key...' });
|
||||
const blockchainPair = await deriveEd25519FromMasterSecret(masterSecret, 'blockchain.key');
|
||||
if (isCancelled && isCancelled()) throw new Error('DERIVE_CANCELLED');
|
||||
|
||||
if (onProgress) onProgress({ percent: 98, stage: 'derive', message: 'Вычисление device key...' });
|
||||
const devicePair = await deriveEd25519FromMasterSecret(masterSecret, 'dev.key');
|
||||
const result = {
|
||||
masterSecretB64: bytesToBase64(masterSecret),
|
||||
rootPair,
|
||||
blockchainPair,
|
||||
devicePair,
|
||||
};
|
||||
if (onProgress) onProgress({ percent: 99, stage: 'derive', message: 'Вычисление client key...' });
|
||||
const clientPair = await deriveEd25519FromMasterSecret(masterSecret, 'client.key');
|
||||
const result = {
|
||||
masterSecretB64: bytesToBase64(masterSecret),
|
||||
recoveryPair,
|
||||
rootPair,
|
||||
blockchainPair,
|
||||
clientPair,
|
||||
};
|
||||
this.passwordKeyBundleCache.set(cacheKey, result);
|
||||
if (onProgress) onProgress({ percent: 100, stage: 'done', message: 'Ключи сгенерированы.' });
|
||||
return result;
|
||||
@@ -839,6 +844,8 @@ export class AuthService {
|
||||
async createAuthSession(login, keyBundle) {
|
||||
const cleanLogin = (login || '').trim();
|
||||
if (!cleanLogin) throw new Error('Введите логин');
|
||||
const clientPair = keyBundle?.clientPair;
|
||||
if (!clientPair) throw new Error('createAuthSession: не передан clientPair');
|
||||
|
||||
const sessionPair = await generateEd25519Pair();
|
||||
const sessionKeyPub = await exportEd25519PublicKeyB64(sessionPair.publicKey);
|
||||
@@ -853,7 +860,7 @@ export class AuthService {
|
||||
|
||||
const timeMs = Date.now();
|
||||
const preimage = `AUTH_CREATE_SESSION:${cleanLogin}:${sessionKey}:${storagePwd}:${timeMs}:${authNonce}`;
|
||||
const signatureB64 = await signBase64(keyBundle.devicePair.privateKey, preimage);
|
||||
const signatureB64 = await signBase64(clientPair.privateKey, preimage);
|
||||
|
||||
const createResp = await this.ws.request('CreateAuthSession', {
|
||||
login: cleanLogin,
|
||||
@@ -861,7 +868,7 @@ export class AuthService {
|
||||
sessionKey,
|
||||
timeMs,
|
||||
authNonce,
|
||||
deviceKey: keyBundle.devicePair.publicKeyB64,
|
||||
clientKey: clientPair.publicKeyB64,
|
||||
signatureB64,
|
||||
sessionType: SESSION_TYPE_CLIENT,
|
||||
clientPlatform: makeClientPlatform(),
|
||||
@@ -892,13 +899,14 @@ export class AuthService {
|
||||
if (!isFree) throw new Error('Этот логин уже занят');
|
||||
|
||||
const keyBundle = await this.derivePasswordKeyBundle(cleanLogin, password);
|
||||
const clientPair = keyBundle?.clientPair;
|
||||
|
||||
const addResp = await this.ws.request('AddUser', {
|
||||
login: cleanLogin,
|
||||
blockchainName: `${cleanLogin}-${BCH_SUFFIX}`,
|
||||
solanaKey: keyBundle.devicePair.publicKeyB64,
|
||||
solanaKey: clientPair.publicKeyB64,
|
||||
blockchainKey: keyBundle.blockchainPair.publicKeyB64,
|
||||
deviceKey: keyBundle.devicePair.publicKeyB64,
|
||||
clientKey: clientPair.publicKeyB64,
|
||||
bchLimit: 1000000,
|
||||
});
|
||||
if (addResp.status !== 200) throw opError('AddUser', addResp);
|
||||
@@ -910,13 +918,14 @@ export class AuthService {
|
||||
async registerUserWithKeyBundle(login, keyBundle) {
|
||||
const cleanLogin = (login || '').trim();
|
||||
if (!cleanLogin) throw new Error('Введите логин');
|
||||
const clientPair = keyBundle?.clientPair;
|
||||
|
||||
const addResp = await this.ws.request('AddUser', {
|
||||
login: cleanLogin,
|
||||
blockchainName: `${cleanLogin}-${BCH_SUFFIX}`,
|
||||
solanaKey: keyBundle.devicePair.publicKeyB64,
|
||||
solanaKey: clientPair.publicKeyB64,
|
||||
blockchainKey: keyBundle.blockchainPair.publicKeyB64,
|
||||
deviceKey: keyBundle.devicePair.publicKeyB64,
|
||||
clientKey: clientPair.publicKeyB64,
|
||||
bchLimit: 1000000,
|
||||
});
|
||||
if (addResp.status !== 200) throw opError('AddUser', addResp);
|
||||
@@ -937,13 +946,13 @@ export class AuthService {
|
||||
async createSessionFromImportedSecrets(login, secrets) {
|
||||
const cleanLogin = (login || '').trim();
|
||||
if (!cleanLogin) throw new Error('В QR-коде нет логина');
|
||||
const deviceKey = String(secrets?.deviceKey || '').trim();
|
||||
if (!deviceKey) throw new Error('В QR-коде нет device key для входа');
|
||||
const clientKey = String(secrets?.clientKey || secrets?.clientKey || '').trim();
|
||||
if (!clientKey) throw new Error('В QR-коде нет client key для входа');
|
||||
|
||||
const privateKey = await importPkcs8Ed25519(deviceKey);
|
||||
const publicKeyB64 = await publicKeyB64FromPkcs8Ed25519(deviceKey);
|
||||
const privateKey = await importPkcs8Ed25519(clientKey);
|
||||
const publicKeyB64 = await publicKeyB64FromPkcs8Ed25519(clientKey);
|
||||
const session = await this.createAuthSession(cleanLogin, {
|
||||
devicePair: {
|
||||
clientPair: {
|
||||
privateKey,
|
||||
publicKeyB64,
|
||||
},
|
||||
@@ -951,9 +960,9 @@ export class AuthService {
|
||||
return session;
|
||||
}
|
||||
|
||||
async createDelegatedSessionWithDeviceKey({
|
||||
async createDelegatedSessionWithClientKey({
|
||||
login,
|
||||
devicePrivPkcs8,
|
||||
clientPrivPkcs8,
|
||||
sessionKey,
|
||||
sessionType = SESSION_TYPE_WALLET,
|
||||
clientPlatform = 'Delegated session',
|
||||
@@ -961,13 +970,13 @@ export class AuthService {
|
||||
}) {
|
||||
const cleanLogin = String(login || '').trim();
|
||||
const cleanSessionKey = String(sessionKey || '').trim();
|
||||
const cleanDevicePriv = String(devicePrivPkcs8 || '').trim();
|
||||
if (!cleanLogin) throw new Error('createDelegatedSessionWithDeviceKey: пустой login');
|
||||
if (!cleanSessionKey) throw new Error('createDelegatedSessionWithDeviceKey: пустой sessionKey');
|
||||
if (!cleanDevicePriv) throw new Error('createDelegatedSessionWithDeviceKey: пустой device private key');
|
||||
const cleanClientPriv = String(clientPrivPkcs8 || '').trim();
|
||||
if (!cleanLogin) throw new Error('createDelegatedSessionWithClientKey: пустой login');
|
||||
if (!cleanSessionKey) throw new Error('createDelegatedSessionWithClientKey: пустой sessionKey');
|
||||
if (!cleanClientPriv) throw new Error('createDelegatedSessionWithClientKey: пустой client private key');
|
||||
|
||||
const devicePrivateKey = await importPkcs8Ed25519(cleanDevicePriv);
|
||||
const devicePublicKeyB64 = await publicKeyB64FromPkcs8Ed25519(cleanDevicePriv);
|
||||
const clientPrivateKey = await importPkcs8Ed25519(cleanClientPriv);
|
||||
const clientPublicKeyB64 = await publicKeyB64FromPkcs8Ed25519(cleanClientPriv);
|
||||
const storagePwd = randomBase64(32);
|
||||
const tempAuth = new AuthService(this.serverUrl);
|
||||
|
||||
@@ -980,7 +989,7 @@ export class AuthService {
|
||||
|
||||
const timeMs = Date.now();
|
||||
const preimage = `AUTH_CREATE_SESSION:${cleanLogin}:${cleanSessionKey}:${storagePwd}:${timeMs}:${authNonce}`;
|
||||
const signatureB64 = await signBase64(devicePrivateKey, preimage);
|
||||
const signatureB64 = await signBase64(clientPrivateKey, preimage);
|
||||
|
||||
const createResp = await tempAuth.ws.request('CreateAuthSession', {
|
||||
login: cleanLogin,
|
||||
@@ -988,7 +997,7 @@ export class AuthService {
|
||||
sessionKey: cleanSessionKey,
|
||||
timeMs,
|
||||
authNonce,
|
||||
deviceKey: devicePublicKeyB64,
|
||||
clientKey: clientPublicKeyB64,
|
||||
signatureB64,
|
||||
sessionType: Number(sessionType) || SESSION_TYPE_WALLET,
|
||||
clientPlatform: String(clientPlatform || '').trim() || 'Delegated session',
|
||||
@@ -1025,7 +1034,7 @@ export class AuthService {
|
||||
|
||||
const secrets = {
|
||||
...currentSecrets,
|
||||
deviceKey: keyBundle.devicePair.privatePkcs8B64,
|
||||
clientKey: keyBundle.clientPair.privatePkcs8B64,
|
||||
};
|
||||
if (saveOptions.saveRoot) secrets.rootKey = keyBundle.rootPair.privatePkcs8B64;
|
||||
if (saveOptions.saveBlockchain) secrets.blockchainKey = keyBundle.blockchainPair.privatePkcs8B64;
|
||||
@@ -1898,9 +1907,9 @@ export class AuthService {
|
||||
if (!storagePwd) throw new Error('Не передан storagePwd для подписи');
|
||||
|
||||
const secrets = await loadEncryptedUserSecrets(cleanFromLogin, storagePwd);
|
||||
const devicePriv = secrets?.deviceKey;
|
||||
if (!devicePriv) throw new Error('Не найден приватный deviceKey');
|
||||
const privateKey = await importPkcs8Ed25519(devicePriv);
|
||||
const clientPriv = secrets?.clientKey || secrets?.clientKey;
|
||||
if (!clientPriv) throw new Error('Не найден приватный clientKey');
|
||||
const privateKey = await importPkcs8Ed25519(clientPriv);
|
||||
|
||||
const toBytes = ensureAsciiBytes(cleanToLogin, 'toLogin');
|
||||
const fromBytes = ensureAsciiBytes(cleanFromLogin, 'fromLogin');
|
||||
@@ -1941,9 +1950,9 @@ export class AuthService {
|
||||
}
|
||||
|
||||
const secrets = await loadEncryptedUserSecrets(cleanFromLogin, storagePwd);
|
||||
const devicePriv = secrets?.deviceKey;
|
||||
if (!devicePriv) throw new Error('Не найден приватный deviceKey');
|
||||
const privateKey = await importPkcs8Ed25519(devicePriv);
|
||||
const clientPriv = secrets?.clientKey || secrets?.clientKey;
|
||||
if (!clientPriv) throw new Error('Не найден приватный clientKey');
|
||||
const privateKey = await importPkcs8Ed25519(clientPriv);
|
||||
|
||||
const toBytes = ensureAsciiBytes(cleanToLogin, 'toLogin');
|
||||
const fromBytes = ensureAsciiBytes(cleanFromLogin, 'fromLogin');
|
||||
|
||||
@@ -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 ключ device.key: ожидалось минимум 32 байта');
|
||||
throw new Error('Некорректный JSON ключ client.key: ожидалось минимум 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 ключ device.key: найдены не-байтовые значения');
|
||||
throw new Error('Некорректный JSON ключ client.key: найдены не-байтовые значения');
|
||||
}
|
||||
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 ключ device.key');
|
||||
if (bytes.length < 32) throw new Error('Некорректный PKCS8 ключ client.key');
|
||||
return bytes.slice(bytes.length - 32);
|
||||
}
|
||||
|
||||
export function extractDeviceKey32FromStoredValue(storedDeviceKey) {
|
||||
const raw = String(storedDeviceKey || '').trim();
|
||||
if (!raw) throw new Error('Пустой device.key');
|
||||
export function extractClientKey32FromStoredValue(storedClientKey) {
|
||||
const raw = String(storedClientKey || '').trim();
|
||||
if (!raw) throw new Error('Пустой client.key');
|
||||
|
||||
const jsonBytes = parseKeypairJson64(raw);
|
||||
if (jsonBytes) {
|
||||
@@ -1,6 +1,7 @@
|
||||
const encoder = new TextEncoder();
|
||||
const WEB_CRYPTO_REQUIRED_MESSAGE = 'Регистрация и подпись блоков требуют WebCrypto (crypto.subtle). Откройте приложение через HTTPS или localhost в современном браузере и повторите попытку.';
|
||||
import { argon2idAsync } from 'https://esm.sh/@noble/hashes@1.8.0/argon2.js';
|
||||
const SHINE_KEY_DERIVATION_PREFIX = 'SHiNE-key';
|
||||
|
||||
function getCryptoApi() {
|
||||
const api = globalThis.crypto;
|
||||
@@ -205,8 +206,19 @@ export async function deriveEd25519FromMasterSecret(masterSecret32, suffix) {
|
||||
if (secretBytes.length !== 32) {
|
||||
throw new Error('Master secret должен быть длиной 32 байта');
|
||||
}
|
||||
const material = `${bytesToBase64(secretBytes)}|${String(suffix || '')}`;
|
||||
const seed = await sha256Text(material);
|
||||
const suffixBytes = utf8Bytes(String(suffix || ''));
|
||||
const material = new Uint8Array(
|
||||
SHINE_KEY_DERIVATION_PREFIX.length + 1 + secretBytes.length + 1 + suffixBytes.length,
|
||||
);
|
||||
let offset = 0;
|
||||
material.set(utf8Bytes(SHINE_KEY_DERIVATION_PREFIX), offset);
|
||||
offset += SHINE_KEY_DERIVATION_PREFIX.length;
|
||||
material[offset++] = 0;
|
||||
material.set(secretBytes, offset);
|
||||
offset += secretBytes.length;
|
||||
material[offset++] = 0;
|
||||
material.set(suffixBytes, offset);
|
||||
const seed = await sha256Bytes(material);
|
||||
const pkcs8 = ed25519Pkcs8FromSeed(seed);
|
||||
const subtle = getSubtleApi();
|
||||
const privateKey = await subtle.importKey('pkcs8', pkcs8, { name: 'Ed25519' }, true, ['sign']);
|
||||
|
||||
@@ -71,7 +71,7 @@ async function importAesKeyFromSharedSecret(sharedSecretBytes) {
|
||||
|
||||
function normalizeKeys(keys = {}) {
|
||||
return {
|
||||
deviceKey: String(keys?.deviceKey || '').trim(),
|
||||
clientKey: String(keys?.clientKey || '').trim(),
|
||||
blockchainKey: String(keys?.blockchainKey || '').trim(),
|
||||
rootKey: String(keys?.rootKey || '').trim(),
|
||||
};
|
||||
|
||||
@@ -30,7 +30,7 @@ export function keyLabel(id) {
|
||||
|
||||
export function describeTransferKeys(keys = {}) {
|
||||
const out = [];
|
||||
if (keys.deviceKey) out.push('device');
|
||||
if (keys.clientKey) out.push('device');
|
||||
if (keys.blockchainKey) out.push('blockchain');
|
||||
if (keys.rootKey) out.push('root');
|
||||
return out;
|
||||
@@ -42,7 +42,7 @@ export function makeKeyTransferText({ login, keys }) {
|
||||
type: 'shine-key-transfer',
|
||||
login: String(login || '').trim(),
|
||||
keys: {
|
||||
deviceKey: String(keys?.deviceKey || ''),
|
||||
clientKey: String(keys?.clientKey || ''),
|
||||
blockchainKey: String(keys?.blockchainKey || ''),
|
||||
rootKey: String(keys?.rootKey || ''),
|
||||
},
|
||||
@@ -65,13 +65,13 @@ 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.deviceKey && !keys.blockchainKey && !keys.rootKey) {
|
||||
if (!keys.clientKey && !keys.blockchainKey && !keys.rootKey) {
|
||||
throw new Error('В QR-коде нет ключей');
|
||||
}
|
||||
return {
|
||||
login,
|
||||
keys: {
|
||||
deviceKey: String(keys.deviceKey || ''),
|
||||
clientKey: String(keys.clientKey || ''),
|
||||
blockchainKey: String(keys.blockchainKey || ''),
|
||||
rootKey: String(keys.rootKey || ''),
|
||||
},
|
||||
|
||||
@@ -323,15 +323,15 @@ function toJwkB64(value) {
|
||||
return base64UrlEncode(bigIntToUnsignedBytes(value));
|
||||
}
|
||||
|
||||
async function deriveArweaveWalletParts(deviceKey32) {
|
||||
if (!(deviceKey32 instanceof Uint8Array)) {
|
||||
throw new Error('SAWD-v1: deviceKey32 должен быть Uint8Array');
|
||||
async function deriveArweaveWalletParts(clientKey32) {
|
||||
if (!(clientKey32 instanceof Uint8Array)) {
|
||||
throw new Error('SAWD-v1: clientKey32 должен быть Uint8Array');
|
||||
}
|
||||
if (deviceKey32.length !== 32) {
|
||||
throw new Error('SAWD-v1: deviceKey32 должен быть ровно 32 байта');
|
||||
if (clientKey32.length !== 32) {
|
||||
throw new Error('SAWD-v1: clientKey32 должен быть ровно 32 байта');
|
||||
}
|
||||
|
||||
const masterSeed32 = await hmacSha256(MASTER_LABEL_UTF8, deviceKey32);
|
||||
const masterSeed32 = await hmacSha256(MASTER_LABEL_UTF8, clientKey32);
|
||||
const masterSeedKey = await importHmacKey(masterSeed32);
|
||||
|
||||
const pResult = await derivePrimeWithImportedKey(masterSeedKey, 'p');
|
||||
@@ -381,8 +381,8 @@ async function deriveArweaveWalletParts(deviceKey32) {
|
||||
};
|
||||
}
|
||||
|
||||
export async function deriveArweaveWalletFromDeviceKey32(deviceKey32) {
|
||||
const result = await deriveArweaveWalletParts(deviceKey32);
|
||||
export async function deriveArweaveWalletFromClientKey32(clientKey32) {
|
||||
const result = await deriveArweaveWalletParts(clientKey32);
|
||||
return {
|
||||
derivation: result.derivation,
|
||||
jwk: result.jwk,
|
||||
@@ -395,7 +395,7 @@ export async function selfTestSawdV1() {
|
||||
const invalid = new Uint8Array(31);
|
||||
let invalidFailed = false;
|
||||
try {
|
||||
await deriveArweaveWalletFromDeviceKey32(invalid);
|
||||
await deriveArweaveWalletFromClientKey32(invalid);
|
||||
} catch {
|
||||
invalidFailed = true;
|
||||
}
|
||||
@@ -403,13 +403,13 @@ export async function selfTestSawdV1() {
|
||||
throw new Error('SAWD-v1 self-test: длина != 32 должна приводить к ошибке');
|
||||
}
|
||||
|
||||
const deviceKey = new Uint8Array(32);
|
||||
for (let i = 0; i < deviceKey.length; i += 1) {
|
||||
deviceKey[i] = i + 1;
|
||||
const clientKey = new Uint8Array(32);
|
||||
for (let i = 0; i < clientKey.length; i += 1) {
|
||||
clientKey[i] = i + 1;
|
||||
}
|
||||
|
||||
const first = await deriveArweaveWalletParts(deviceKey);
|
||||
const second = await deriveArweaveWalletParts(deviceKey);
|
||||
const first = await deriveArweaveWalletParts(clientKey);
|
||||
const second = await deriveArweaveWalletParts(clientKey);
|
||||
|
||||
if (!first.address || !second.address) {
|
||||
throw new Error('SAWD-v1 self-test: адрес пустой');
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { base64ToBytes, importPkcs8Ed25519, sha256Bytes, signBytes } from './crypto-utils.js';
|
||||
import { extractSeed32FromPkcs8B64 } from './device-key-utils.js';
|
||||
import { extractSeed32FromPkcs8B64 } from './client-key-utils.js';
|
||||
import {
|
||||
SHINE_LOGIN_GUARD_PROGRAM_ID,
|
||||
SHINE_PAYMENTS_PROGRAM_ID,
|
||||
@@ -16,8 +16,9 @@ const BLOCKCHAIN_TYPE_MAIN_USER = 1;
|
||||
const ED25519_PROGRAM_ID = 'Ed25519SigVerify111111111111111111111111111';
|
||||
const SYSVAR_INSTRUCTIONS_ID = 'Sysvar1nstructions1111111111111111111111111';
|
||||
|
||||
const BLOCK_TYPE_RECOVERY_KEY = 0;
|
||||
const BLOCK_TYPE_ROOT_KEY = 1;
|
||||
const BLOCK_TYPE_DEVICE_KEY = 2;
|
||||
const BLOCK_TYPE_CLIENT_KEY = 2;
|
||||
const BLOCK_TYPE_BLOCKCHAIN_REGISTRY = 3;
|
||||
const BLOCK_TYPE_SERVER_PROFILE = 30;
|
||||
const BLOCK_TYPE_ACCESS_SERVERS = 40;
|
||||
@@ -176,10 +177,11 @@ function serializeCreateUserPdaArgs(args) {
|
||||
const buf = [];
|
||||
buf.push(3);
|
||||
pushStrU8(buf, args.login);
|
||||
for (const x of args.recoveryKey32) buf.push(x);
|
||||
for (const x of args.rootKey32) buf.push(x);
|
||||
pushU64LE(buf, args.createdAtMs);
|
||||
pushU64LE(buf, BigInt(args.additionalLimitBytes || 0n));
|
||||
for (const x of args.deviceKey32) buf.push(x);
|
||||
for (const x of args.clientKey32) buf.push(x);
|
||||
for (const x of args.blockchainPublicKey32) buf.push(x);
|
||||
pushStrU8(buf, args.blockchainName);
|
||||
pushU64LE(buf, args.usedBytes);
|
||||
@@ -218,13 +220,14 @@ function serializeUpdateUserPdaArgs(args) {
|
||||
const buf = [];
|
||||
buf.push(4);
|
||||
pushStrU8(buf, args.login);
|
||||
for (const x of args.recoveryKey32) buf.push(x);
|
||||
for (const x of args.rootKey32) buf.push(x);
|
||||
pushU64LE(buf, args.createdAtMs);
|
||||
pushU64LE(buf, args.updatedAtMs);
|
||||
pushU32LE(buf, args.version);
|
||||
for (const x of args.prevHash32) buf.push(x);
|
||||
pushU64LE(buf, args.additionalLimitBytes);
|
||||
for (const x of args.deviceKey32) buf.push(x);
|
||||
for (const x of args.clientKey32) buf.push(x);
|
||||
for (const x of args.blockchainPublicKey32) buf.push(x);
|
||||
pushStrU8(buf, args.blockchainName);
|
||||
pushU64LE(buf, args.usedBytes);
|
||||
@@ -288,8 +291,9 @@ function createPdaState({
|
||||
updatedAtMs,
|
||||
recordNumber,
|
||||
prevRecordHash,
|
||||
recoveryKey,
|
||||
rootKey,
|
||||
deviceKey,
|
||||
clientKey,
|
||||
blockchain,
|
||||
isServer,
|
||||
addressFormatType,
|
||||
@@ -313,8 +317,10 @@ function createPdaState({
|
||||
recordNumber,
|
||||
prevRecordHash,
|
||||
login,
|
||||
recoveryKey,
|
||||
rootKey,
|
||||
deviceKey,
|
||||
clientKey,
|
||||
clientKey: clientKey,
|
||||
blockchain,
|
||||
isServer: Boolean(isServer),
|
||||
serverProfile,
|
||||
@@ -373,8 +379,9 @@ export function parseShineUserPda(dataBytes) {
|
||||
const login = reader.readStrU8();
|
||||
const blocksCount = reader.readU8();
|
||||
|
||||
let recoveryKey = null;
|
||||
let rootKey = null;
|
||||
let deviceKey = null;
|
||||
let clientKey = null;
|
||||
let blockchain = null;
|
||||
let isServer = false;
|
||||
let addressFormatType = 0;
|
||||
@@ -390,12 +397,16 @@ export function parseShineUserPda(dataBytes) {
|
||||
const blockType = reader.readU8();
|
||||
reader.readU8();
|
||||
|
||||
if (blockType === BLOCK_TYPE_RECOVERY_KEY) {
|
||||
recoveryKey = reader.readBytes(32);
|
||||
continue;
|
||||
}
|
||||
if (blockType === BLOCK_TYPE_ROOT_KEY) {
|
||||
rootKey = reader.readBytes(32);
|
||||
continue;
|
||||
}
|
||||
if (blockType === BLOCK_TYPE_DEVICE_KEY) {
|
||||
deviceKey = reader.readBytes(32);
|
||||
if (blockType === BLOCK_TYPE_CLIENT_KEY) {
|
||||
clientKey = reader.readBytes(32);
|
||||
continue;
|
||||
}
|
||||
if (blockType === BLOCK_TYPE_BLOCKCHAIN_REGISTRY) {
|
||||
@@ -466,7 +477,7 @@ export function parseShineUserPda(dataBytes) {
|
||||
throw new Error(`Неизвестный блок PDA: ${blockType}`);
|
||||
}
|
||||
|
||||
if (!rootKey || !deviceKey || !blockchain) {
|
||||
if (!recoveryKey || !rootKey || !clientKey || !blockchain) {
|
||||
throw new Error('В PDA отсутствуют обязательные блоки');
|
||||
}
|
||||
|
||||
@@ -478,8 +489,9 @@ export function parseShineUserPda(dataBytes) {
|
||||
updatedAtMs,
|
||||
recordNumber,
|
||||
prevRecordHash,
|
||||
recoveryKey,
|
||||
rootKey,
|
||||
deviceKey,
|
||||
clientKey,
|
||||
blockchain,
|
||||
isServer,
|
||||
addressFormatType,
|
||||
@@ -507,8 +519,9 @@ export function serializeUnsignedRecordFromState(stateLike) {
|
||||
updatedAtMs: stateLike.updatedAtMs,
|
||||
recordNumber: stateLike.recordNumber,
|
||||
prevRecordHash: stateLike.prevRecordHash,
|
||||
recoveryKey: stateLike.recoveryKey,
|
||||
rootKey: stateLike.rootKey,
|
||||
deviceKey: stateLike.deviceKey,
|
||||
clientKey: stateLike.clientKey ?? stateLike.clientKey,
|
||||
blockchain: stateLike.blockchain,
|
||||
isServer: stateLike.isServer,
|
||||
addressFormatType: stateLike.addressFormatType ?? stateLike.serverProfile?.addressFormatType,
|
||||
@@ -527,13 +540,16 @@ export function serializeUnsignedRecordFromState(stateLike) {
|
||||
pushU32LE(buf, state.recordNumber);
|
||||
for (const x of state.prevRecordHash) buf.push(x);
|
||||
pushStrU8(buf, state.login);
|
||||
buf.push(state.isServer ? 7 : 6);
|
||||
buf.push(state.isServer ? 8 : 7);
|
||||
|
||||
buf.push(BLOCK_TYPE_RECOVERY_KEY, 0);
|
||||
for (const x of state.recoveryKey) buf.push(x);
|
||||
|
||||
buf.push(BLOCK_TYPE_ROOT_KEY, 0);
|
||||
for (const x of state.rootKey) buf.push(x);
|
||||
|
||||
buf.push(BLOCK_TYPE_DEVICE_KEY, 0);
|
||||
for (const x of state.deviceKey) buf.push(x);
|
||||
buf.push(BLOCK_TYPE_CLIENT_KEY, 0);
|
||||
for (const x of state.clientKey) buf.push(x);
|
||||
|
||||
buf.push(BLOCK_TYPE_BLOCKCHAIN_REGISTRY, 0, 1, state.blockchain.blockchainType);
|
||||
pushStrU8(buf, state.blockchain.blockchainName);
|
||||
@@ -656,14 +672,15 @@ async function buildCreateContext({ login, keyBundle, solanaEndpoint }) {
|
||||
const [economyConfigPda] = solana.PublicKey.findProgramAddressSync([enc.encode(SHINE_USERS_ECONOMY_CONFIG_SEED)], usersProgram);
|
||||
const [inflowVault] = solana.PublicKey.findProgramAddressSync([enc.encode(SHINE_PAYMENTS_INFLOW_VAULT_SEED)], paymentsProgram);
|
||||
|
||||
const recoveryKey32 = base64ToBytes(keyBundle.recoveryPair.publicKeyB64);
|
||||
const rootKey32 = base64ToBytes(keyBundle.rootPair.publicKeyB64);
|
||||
const blockchainKey32 = base64ToBytes(keyBundle.blockchainPair.publicKeyB64);
|
||||
const deviceKey32 = base64ToBytes(keyBundle.devicePair.publicKeyB64);
|
||||
const clientKey32 = base64ToBytes(keyBundle.clientPair.publicKeyB64);
|
||||
|
||||
const rootPrivKey = await importPkcs8Ed25519(keyBundle.rootPair.privatePkcs8B64);
|
||||
const bchPrivKey = await importPkcs8Ed25519(keyBundle.blockchainPair.privatePkcs8B64);
|
||||
const deviceSeed32 = extractSeed32FromPkcs8B64(keyBundle.devicePair.privatePkcs8B64);
|
||||
const deviceKeypair = solana.Keypair.fromSeed(deviceSeed32);
|
||||
const clientSeed32 = extractSeed32FromPkcs8B64(keyBundle.clientPair.privatePkcs8B64);
|
||||
const clientKeypair = solana.Keypair.fromSeed(clientSeed32);
|
||||
|
||||
return {
|
||||
cleanLogin,
|
||||
@@ -678,12 +695,13 @@ async function buildCreateContext({ login, keyBundle, solanaEndpoint }) {
|
||||
userPda,
|
||||
economyConfigPda,
|
||||
inflowVault,
|
||||
recoveryKey32,
|
||||
rootKey32,
|
||||
blockchainKey32,
|
||||
deviceKey32,
|
||||
clientKey32,
|
||||
rootPrivKey,
|
||||
bchPrivKey,
|
||||
deviceKeypair,
|
||||
clientKeypair,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -720,8 +738,9 @@ async function createShineUserPdaOnSolana({
|
||||
updatedAtMs: createdAtMs,
|
||||
recordNumber: 0,
|
||||
prevRecordHash: zeroHash32,
|
||||
recoveryKey: ctx.recoveryKey32,
|
||||
rootKey: ctx.rootKey32,
|
||||
deviceKey: ctx.deviceKey32,
|
||||
clientKey: ctx.clientKey32,
|
||||
blockchain: createBlockchainState({
|
||||
blockchainName,
|
||||
blockchainPublicKey: ctx.blockchainKey32,
|
||||
@@ -749,10 +768,11 @@ async function createShineUserPdaOnSolana({
|
||||
|
||||
const ixData = serializeCreateUserPdaArgs({
|
||||
login: cleanLogin,
|
||||
recoveryKey32: ctx.recoveryKey32,
|
||||
rootKey32: ctx.rootKey32,
|
||||
createdAtMs,
|
||||
additionalLimitBytes: 0n,
|
||||
deviceKey32: ctx.deviceKey32,
|
||||
clientKey32: ctx.clientKey32,
|
||||
blockchainPublicKey32: ctx.blockchainKey32,
|
||||
blockchainName,
|
||||
usedBytes: 0n,
|
||||
@@ -785,7 +805,7 @@ async function createShineUserPdaOnSolana({
|
||||
const createIx = new ctx.solana.TransactionInstruction({
|
||||
programId: ctx.usersProgram,
|
||||
keys: [
|
||||
{ pubkey: ctx.deviceKeypair.publicKey, isSigner: true, isWritable: true },
|
||||
{ pubkey: ctx.clientKeypair.publicKey, isSigner: true, isWritable: true },
|
||||
{ pubkey: ctx.userPda, isSigner: false, isWritable: true },
|
||||
{ pubkey: ctx.solana.SystemProgram.programId, isSigner: false, isWritable: false },
|
||||
{ pubkey: ctx.inflowVault, isSigner: false, isWritable: true },
|
||||
@@ -799,7 +819,7 @@ async function createShineUserPdaOnSolana({
|
||||
const signature = await ctx.solana.sendAndConfirmTransaction(
|
||||
ctx.connection,
|
||||
new ctx.solana.Transaction().add(ed25519RootIx, ed25519BchIx, createIx),
|
||||
[ctx.deviceKeypair],
|
||||
[ctx.clientKeypair],
|
||||
{ commitment: 'confirmed' },
|
||||
);
|
||||
|
||||
@@ -848,7 +868,7 @@ export async function updateShineUserPdaOnSolana({
|
||||
login,
|
||||
solanaEndpoint,
|
||||
rootPrivatePkcs8B64,
|
||||
devicePrivatePkcs8B64,
|
||||
clientPrivatePkcs8B64,
|
||||
blockchainPrivatePkcs8B64,
|
||||
additionalLimitBytes = 0n,
|
||||
nextUsedBytes,
|
||||
@@ -882,8 +902,8 @@ export async function updateShineUserPdaOnSolana({
|
||||
const [userPda] = solana.PublicKey.findProgramAddressSync([enc.encode(SHINE_USERS_USER_PDA_SEED_PREFIX), enc.encode(cleanLogin)], usersProgram);
|
||||
const [economyPda] = solana.PublicKey.findProgramAddressSync([enc.encode(SHINE_USERS_ECONOMY_CONFIG_SEED)], usersProgram);
|
||||
const [inflowVault] = solana.PublicKey.findProgramAddressSync([enc.encode(SHINE_PAYMENTS_INFLOW_VAULT_SEED)], paymentsProgram);
|
||||
const deviceSeed32 = extractSeed32FromPkcs8B64(devicePrivatePkcs8B64);
|
||||
const deviceKeypair = solana.Keypair.fromSeed(deviceSeed32);
|
||||
const clientSeed32 = extractSeed32FromPkcs8B64(clientPrivatePkcs8B64);
|
||||
const clientKeypair = solana.Keypair.fromSeed(clientSeed32);
|
||||
|
||||
const lastBlockStateBytes = buildLastBlockStateBytes(
|
||||
cleanLogin,
|
||||
@@ -928,8 +948,9 @@ export async function updateShineUserPdaOnSolana({
|
||||
updatedAtMs,
|
||||
recordNumber: newRecordNumber,
|
||||
prevRecordHash: prevHash,
|
||||
recoveryKey: current.recoveryKey,
|
||||
rootKey: current.rootKey,
|
||||
deviceKey: current.deviceKey,
|
||||
clientKey: current.clientKey ?? current.clientKey,
|
||||
blockchain: createBlockchainState({
|
||||
blockchainName: currentBch.blockchainName,
|
||||
blockchainPublicKey: currentBch.blockchainPublicKey,
|
||||
@@ -957,13 +978,14 @@ export async function updateShineUserPdaOnSolana({
|
||||
|
||||
const ixData = serializeUpdateUserPdaArgs({
|
||||
login: cleanLogin,
|
||||
recoveryKey32: current.recoveryKey,
|
||||
rootKey32: current.rootKey,
|
||||
createdAtMs: current.createdAtMs,
|
||||
updatedAtMs,
|
||||
version: newRecordNumber,
|
||||
prevHash32: prevHash,
|
||||
additionalLimitBytes: addLimit,
|
||||
deviceKey32: current.deviceKey,
|
||||
clientKey32: current.clientKey,
|
||||
blockchainPublicKey32: currentBch.blockchainPublicKey,
|
||||
blockchainName: currentBch.blockchainName,
|
||||
usedBytes: effectiveUsed,
|
||||
@@ -996,7 +1018,7 @@ export async function updateShineUserPdaOnSolana({
|
||||
const updateIx = new solana.TransactionInstruction({
|
||||
programId: usersProgram,
|
||||
keys: [
|
||||
{ pubkey: deviceKeypair.publicKey, isSigner: true, isWritable: true },
|
||||
{ pubkey: clientKeypair.publicKey, isSigner: true, isWritable: true },
|
||||
{ pubkey: userPda, isSigner: false, isWritable: true },
|
||||
{ pubkey: solana.SystemProgram.programId, isSigner: false, isWritable: false },
|
||||
{ pubkey: inflowVault, isSigner: false, isWritable: true },
|
||||
@@ -1011,7 +1033,7 @@ export async function updateShineUserPdaOnSolana({
|
||||
const signature = await solana.sendAndConfirmTransaction(
|
||||
connection,
|
||||
new solana.Transaction().add(computeIx, heapIx, edIxRoot, edIxBch, updateIx),
|
||||
[deviceKeypair],
|
||||
[clientKeypair],
|
||||
{ commitment: 'confirmed' },
|
||||
);
|
||||
|
||||
@@ -1040,7 +1062,7 @@ export async function updateServerOnSolana({
|
||||
login,
|
||||
solanaEndpoint,
|
||||
rootPrivatePkcs8B64: keyBundle.rootPair.privatePkcs8B64,
|
||||
devicePrivatePkcs8B64: keyBundle.devicePair.privatePkcs8B64,
|
||||
clientPrivatePkcs8B64: keyBundle.clientPair.privatePkcs8B64,
|
||||
serverProfile: {
|
||||
addressFormatType,
|
||||
addressFormatVersion,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { extractDeviceKey32FromStoredValue } from './device-key-utils.js';
|
||||
import { extractClientKey32FromStoredValue } from './client-key-utils.js';
|
||||
import { loadEncryptedUserSecrets } from './key-vault.js';
|
||||
import { SOLANA_ENDPOINT_DEFAULT } from '../solana-programs.js';
|
||||
|
||||
@@ -63,7 +63,7 @@ function decodeBase58(input) {
|
||||
|
||||
async function keypairFromPkcs8(pkcs8B64) {
|
||||
const solana = await loadSolanaLib();
|
||||
const seed32 = extractDeviceKey32FromStoredValue(pkcs8B64);
|
||||
const seed32 = extractClientKey32FromStoredValue(pkcs8B64);
|
||||
return solana.Keypair.fromSeed(seed32);
|
||||
}
|
||||
|
||||
@@ -95,22 +95,22 @@ export async function createSolanaWalletFromPrivateBase58(privateKey32Base58) {
|
||||
};
|
||||
}
|
||||
|
||||
export async function getWalletFromStoredDeviceKey({ login, storagePwd }) {
|
||||
export async function getWalletFromStoredClientKey({ login, storagePwd }) {
|
||||
const cleanLogin = String(login || '').trim();
|
||||
const cleanPwd = String(storagePwd || '').trim();
|
||||
if (!cleanLogin || !cleanPwd) {
|
||||
throw new Error('Нет активной сессии для доступа к wallet.key');
|
||||
throw new Error('Нет активной сессии для доступа к client.key');
|
||||
}
|
||||
const secrets = await loadEncryptedUserSecrets(cleanLogin, cleanPwd);
|
||||
const devicePrivate = String(secrets?.deviceKey || '').trim();
|
||||
if (!devicePrivate) {
|
||||
throw new Error('На устройстве не найден device.key (wallet.key)');
|
||||
const clientPrivate = String(secrets?.clientKey || '').trim();
|
||||
if (!clientPrivate) {
|
||||
throw new Error('На устройстве не найден client.key');
|
||||
}
|
||||
const keypair = await keypairFromPkcs8(devicePrivate);
|
||||
const keypair = await keypairFromPkcs8(clientPrivate);
|
||||
return {
|
||||
address: keypair.publicKey.toBase58(),
|
||||
keypair,
|
||||
devicePrivatePkcs8B64: devicePrivate,
|
||||
clientPrivatePkcs8B64: clientPrivate,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -289,7 +289,7 @@ function createInitialState({ withStoredSession = true } = {}) {
|
||||
keyStorage: {
|
||||
rootKey: 'Ключ root хранится в зашифрованном виде',
|
||||
blockchainKey: 'Ключ blockchain хранится в зашифрованном виде',
|
||||
deviceKey: 'Ключ device хранится в зашифрованном виде',
|
||||
clientKey: 'Ключ device хранится в зашифрованном виде',
|
||||
saveRoot: false,
|
||||
saveBlockchain: true,
|
||||
saveDevice: true,
|
||||
|
||||
@@ -47,10 +47,10 @@
|
||||
<div class="card">
|
||||
<h2>Что потребуется</h2>
|
||||
<p style="color:var(--text-muted);font-size:13px;line-height:1.7;">
|
||||
<strong>Для создания:</strong> полный keyBundle сервера (rootPair + devicePair + blockchainPair),
|
||||
<strong>Для создания:</strong> полный keyBundle сервера (rootPair + clientPair + blockchainPair),
|
||||
логин сервера (без точки, не более 20 символов), URL-адрес сервера, Solana-эндпоинт,
|
||||
достаточный баланс SOL на device-ключе для комиссии.<br/><br/>
|
||||
<strong>Для обновления:</strong> только rootPair + devicePair (blockchain-ключ не нужен).
|
||||
<strong>Для обновления:</strong> только rootPair + clientPair (blockchain-ключ не нужен).
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -111,6 +111,18 @@ async function pairFromSeedBase58(seedB58, explicitPubB58) {
|
||||
|
||||
export async function buildKeyBundleFromForm(fieldMap, options = {}) {
|
||||
const requireBlockchain = options.requireBlockchain !== false;
|
||||
let recovery = null;
|
||||
const masterSecretValue = fieldMap.masterSecret ? String($(fieldMap.masterSecret).value || '').trim() : '';
|
||||
if (masterSecretValue) {
|
||||
const masterSecret32 = ensure32Bytes(base58ToBytes(masterSecretValue));
|
||||
const recoveryPair = await deriveEd25519FromMasterSecret(masterSecret32, 'recovery.key');
|
||||
recovery = {
|
||||
publicKeyB64: recoveryPair.publicKeyB64,
|
||||
privatePkcs8B64: recoveryPair.privatePkcs8B64,
|
||||
publicKeyB58: bytesToBase58(base64ToBytes(recoveryPair.publicKeyB64)),
|
||||
privateSeedB58: bytesToBase58(base64ToBytes(recoveryPair.privatePkcs8B64).slice(-32)),
|
||||
};
|
||||
}
|
||||
const root = await pairFromSeedBase58($(fieldMap.rootPriv).value, $(fieldMap.rootPub).value);
|
||||
const device = await pairFromSeedBase58($(fieldMap.devPriv).value, $(fieldMap.devPub).value);
|
||||
const blockchainPriv = String($(fieldMap.bchPriv).value || '').trim();
|
||||
@@ -122,13 +134,18 @@ export async function buildKeyBundleFromForm(fieldMap, options = {}) {
|
||||
}
|
||||
return {
|
||||
keyBundle: {
|
||||
recoveryPair: recovery
|
||||
? { publicKeyB64: recovery.publicKeyB64, privatePkcs8B64: recovery.privatePkcs8B64 }
|
||||
: null,
|
||||
rootPair: { publicKeyB64: root.publicKeyB64, privatePkcs8B64: root.privatePkcs8B64 },
|
||||
blockchainPair: blockchain
|
||||
? { publicKeyB64: blockchain.publicKeyB64, privatePkcs8B64: blockchain.privatePkcs8B64 }
|
||||
: null,
|
||||
devicePair: { publicKeyB64: device.publicKeyB64, privatePkcs8B64: device.privatePkcs8B64 },
|
||||
clientPair: { publicKeyB64: device.publicKeyB64, privatePkcs8B64: device.privatePkcs8B64 },
|
||||
},
|
||||
normalized: {
|
||||
recoveryPubB58: recovery?.publicKeyB58 || '',
|
||||
recoveryPrivB58: recovery?.privateSeedB58 || '',
|
||||
rootPubB58: root.publicKeyB58,
|
||||
rootPrivB58: root.privateSeedB58,
|
||||
bchPubB58: blockchain?.publicKeyB58 || '',
|
||||
@@ -148,14 +165,20 @@ export async function deriveKeyBundleFromPassword({ login, password, onProgress
|
||||
login: cleanLogin,
|
||||
onProgress,
|
||||
});
|
||||
const [rootPair, blockchainPair, devicePair] = await Promise.all([
|
||||
const [recoveryPair, rootPair, blockchainPair, clientPair] = await Promise.all([
|
||||
deriveEd25519FromMasterSecret(masterSecret32, 'recovery.key'),
|
||||
deriveEd25519FromMasterSecret(masterSecret32, 'root.key'),
|
||||
deriveEd25519FromMasterSecret(masterSecret32, 'bch.key'),
|
||||
deriveEd25519FromMasterSecret(masterSecret32, 'dev.key'),
|
||||
deriveEd25519FromMasterSecret(masterSecret32, 'blockchain.key'),
|
||||
deriveEd25519FromMasterSecret(masterSecret32, 'client.key'),
|
||||
]);
|
||||
return {
|
||||
masterSecret32,
|
||||
keyBundle: { rootPair, blockchainPair, devicePair },
|
||||
keyBundle: {
|
||||
recoveryPair,
|
||||
rootPair,
|
||||
blockchainPair,
|
||||
clientPair,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -163,12 +186,16 @@ export function fillKeyFields(fieldMap, keyBundle, masterSecret32) {
|
||||
if (masterSecret32) {
|
||||
$(fieldMap.masterSecret).value = bytesToBase58(masterSecret32);
|
||||
}
|
||||
if (fieldMap.recoveryPub && fieldMap.recoveryPriv && keyBundle.recoveryPair) {
|
||||
$(fieldMap.recoveryPub).value = bytesToBase58(base64ToBytes(keyBundle.recoveryPair.publicKeyB64));
|
||||
$(fieldMap.recoveryPriv).value = bytesToBase58(base64ToBytes(keyBundle.recoveryPair.privatePkcs8B64).slice(-32));
|
||||
}
|
||||
$(fieldMap.rootPub).value = bytesToBase58(base64ToBytes(keyBundle.rootPair.publicKeyB64));
|
||||
$(fieldMap.rootPriv).value = bytesToBase58(base64ToBytes(keyBundle.rootPair.privatePkcs8B64).slice(-32));
|
||||
$(fieldMap.bchPub).value = bytesToBase58(base64ToBytes(keyBundle.blockchainPair.publicKeyB64));
|
||||
$(fieldMap.bchPriv).value = bytesToBase58(base64ToBytes(keyBundle.blockchainPair.privatePkcs8B64).slice(-32));
|
||||
$(fieldMap.devPub).value = bytesToBase58(base64ToBytes(keyBundle.devicePair.publicKeyB64));
|
||||
$(fieldMap.devPriv).value = bytesToBase58(base64ToBytes(keyBundle.devicePair.privatePkcs8B64).slice(-32));
|
||||
$(fieldMap.devPub).value = bytesToBase58(base64ToBytes(keyBundle.clientPair.publicKeyB64));
|
||||
$(fieldMap.devPriv).value = bytesToBase58(base64ToBytes(keyBundle.clientPair.privatePkcs8B64).slice(-32));
|
||||
}
|
||||
|
||||
export function updateSolAddress(fieldMap) {
|
||||
|
||||
@@ -50,7 +50,7 @@ function renderExpectedKeys(parsed) {
|
||||
$('expectedKeysBox').style.display = 'block';
|
||||
setText('expectedRootPub', publicKeyBytesToBase58(parsed.rootKey));
|
||||
setText('expectedBchPub', publicKeyBytesToBase58(parsed.blockchain.blockchainPublicKey));
|
||||
setText('expectedDevPub', publicKeyBytesToBase58(parsed.deviceKey));
|
||||
setText('expectedDevPub', publicKeyBytesToBase58(parsed.clientKey));
|
||||
setGenMessage($('expectedKeysStatus'), 'После генерации ключей этот блок покажет, совпадают ли они с уже записанной PDA.', 'warn');
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ function compareCurrentFormKeysWithPda() {
|
||||
blockchain: blockchainActual
|
||||
? compareExpectedPublicKeys(publicKeyBytesToBase58(currentPda.blockchain.blockchainPublicKey), blockchainActual)
|
||||
: { matches: true, expected: publicKeyBytesToBase58(currentPda.blockchain.blockchainPublicKey), actual: '' },
|
||||
device: compareExpectedPublicKeys(publicKeyBytesToBase58(currentPda.deviceKey), $('devPub').value),
|
||||
device: compareExpectedPublicKeys(publicKeyBytesToBase58(currentPda.clientKey), $('devPub').value),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user