diff --git a/Dev_Docs/API/03_Session_Management_API.md b/Dev_Docs/API/03_Session_Management_API.md index 0762e70..213752e 100644 --- a/Dev_Docs/API/03_Session_Management_API.md +++ b/Dev_Docs/API/03_Session_Management_API.md @@ -183,7 +183,7 @@ K9v3nQ4u8jYk0a2p7cD4mLx1zR0sT5wV6bN8eH3fQ1M "requestId": "esp-set-001", "payload": { "enabled": true, - "passwordHash": "argon2id$...", + "passwordHash": "sha256$0123abcd...", "ttlSeconds": 180 } } @@ -191,6 +191,12 @@ K9v3nQ4u8jYk0a2p7cD4mLx1zR0sT5wV6bN8eH3fQ1M Если pairing должен работать **без доп. пароля**, клиент может включить его с пустым `passwordHash`. +Формат непустого `passwordHash`: + +```text +sha256$ +``` + ### Успешный ответ ```json @@ -222,7 +228,7 @@ K9v3nQ4u8jYk0a2p7cD4mLx1zR0sT5wV6bN8eH3fQ1M "requestId": "esp-start-001", "payload": { "login": "alice", - "passwordHash": "argon2id$...", + "passwordHash": "sha256$0123abcd...", "requesterSessionKey": "ed25519/BASE64_PUBLIC_KEY", "requesterSessionType": 1, "requesterClientPlatform": "Android", diff --git a/Dev_Docs/Pending_Features/2026-06-15_1531_wallet_session_pairing_и_sha256_пароль.md b/Dev_Docs/Pending_Features/2026-06-15_1531_wallet_session_pairing_и_sha256_пароль.md new file mode 100644 index 0000000..f628e48 --- /dev/null +++ b/Dev_Docs/Pending_Features/2026-06-15_1531_wallet_session_pairing_и_sha256_пароль.md @@ -0,0 +1,22 @@ +# wallet-session pairing и SHA-256 пароль pairing + +- краткое описание: + - добавлен сценарий `session-only` подключения wallet-plugin через доверенное устройство без передачи постоянных ключей; + - для pairing-пароля убран `argon2id`, вместо него используется только формат `sha256$`; + - новый plugin `SHiNE-browser-plugin-wallet` получает и хранит только `wallet-session`. + +- что проверять: + - в `shine-UI` экран `Войти через другое устройство` создаёт заявку и получает `session-only` approve; + - на доверенном устройстве в `Подключить по коду` кнопка `Подключить wallet-session` действительно не передаёт `device/root/blockchain` ключи; + - новый plugin загружается как Chrome MV3 extension и получает wallet-session; + - pairing c доп. паролем работает только с форматом `sha256$`; + - pairing без доп. пароля продолжает работать. + +- ожидаемый результат: + - requester получает только `sessionId/sessionKey/sessionPriv/storagePwd`; + - доверенное устройство не пересылает постоянные ключи в `session-only` режиме; + - сервер принимает только новый формат pairing-пароля; + - логин по сохранённой wallet-session восстанавливается успешно. + +- статус: + - pending diff --git a/Dev_Docs/Протоколы/ESP_Pairing_и_режимы_подключения.md b/Dev_Docs/Протоколы/ESP_Pairing_и_режимы_подключения.md index df1ffa7..902ec85 100644 --- a/Dev_Docs/Протоколы/ESP_Pairing_и_режимы_подключения.md +++ b/Dev_Docs/Протоколы/ESP_Pairing_и_режимы_подключения.md @@ -58,7 +58,7 @@ ## 3. Что именно делает сервер -- хранит включённость pairing и optional opaque `passwordHash`; +- хранит включённость pairing и optional `passwordHash` в формате `sha256$`; - хранит pairing-заявки всех статусов, но в список активных для доверённого устройства отдаёт только pending `created`; - рассчитывает короткий код `shortCode` из `7` цифр; - рассчитывает длинный `fingerprintB58` из `SHA-256` заявки; @@ -104,3 +104,9 @@ - пароль на сервере, если он включён, только отсеивает лишних; - онлайн доверенная сессия решает, добавлять ли новую сессию; - сервер остаётся маршрутизатором и хранилищем состояния, а не владельцем секретов. + +Текущий формат pairing-пароля: + +```text +sha256$ +``` diff --git a/ESP32-wallet/settings.gradle b/ESP32-wallet/settings.gradle deleted file mode 100644 index 93873f1..0000000 --- a/ESP32-wallet/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = 'ESP-wallet' \ No newline at end of file diff --git a/ESP32-wallet/.gitignore b/SHiNE-browser-plugin-wallet/.gitignore similarity index 95% rename from ESP32-wallet/.gitignore rename to SHiNE-browser-plugin-wallet/.gitignore index 39eea9c..f290139 100644 --- a/ESP32-wallet/.gitignore +++ b/SHiNE-browser-plugin-wallet/.gitignore @@ -1,5 +1,6 @@ .gradle build/ +node_modules/ !gradle/wrapper/gradle-wrapper.jar !**/src/main/**/build/ !**/src/test/**/build/ @@ -40,4 +41,4 @@ bin/ .vscode/ ### Mac OS ### -.DS_Store \ No newline at end of file +.DS_Store diff --git a/SHiNE-browser-plugin-wallet/.idea/.gitignore b/SHiNE-browser-plugin-wallet/.idea/.gitignore new file mode 100644 index 0000000..5d01992 --- /dev/null +++ b/SHiNE-browser-plugin-wallet/.idea/.gitignore @@ -0,0 +1,10 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Ignored default folder with query files +/queries/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/SHiNE-browser-plugin-wallet/.idea/.name b/SHiNE-browser-plugin-wallet/.idea/.name new file mode 100644 index 0000000..5720598 --- /dev/null +++ b/SHiNE-browser-plugin-wallet/.idea/.name @@ -0,0 +1 @@ +ESP-wallet \ No newline at end of file diff --git a/SHiNE-browser-plugin-wallet/.idea/gradle.xml b/SHiNE-browser-plugin-wallet/.idea/gradle.xml new file mode 100644 index 0000000..b48f6be --- /dev/null +++ b/SHiNE-browser-plugin-wallet/.idea/gradle.xml @@ -0,0 +1,17 @@ + + + + + + + \ No newline at end of file diff --git a/SHiNE-browser-plugin-wallet/.idea/misc.xml b/SHiNE-browser-plugin-wallet/.idea/misc.xml new file mode 100644 index 0000000..a9ef8da --- /dev/null +++ b/SHiNE-browser-plugin-wallet/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/SHiNE-browser-plugin-wallet/.idea/vcs.xml b/SHiNE-browser-plugin-wallet/.idea/vcs.xml new file mode 100644 index 0000000..0faa797 --- /dev/null +++ b/SHiNE-browser-plugin-wallet/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/SHiNE-browser-plugin-wallet/README.md b/SHiNE-browser-plugin-wallet/README.md new file mode 100644 index 0000000..0353f0c --- /dev/null +++ b/SHiNE-browser-plugin-wallet/README.md @@ -0,0 +1,38 @@ +# SHiNE Browser Plugin Wallet + +Chrome-compatible Manifest V3 plugin for SHiNE wallet-session login. + +## Что уже умеет + +- создать `wallet-session` через `StartEspPairing`; +- показать код подключения; +- дождаться подтверждения на доверенном устройстве; +- принять `session-only` payload без передачи `deviceKey/rootKey/blockchainKey`; +- сохранить `sessionPriv/sessionKey/sessionId` в локальном хранилище plugin; +- восстанавливать session через `SessionChallenge -> SessionLogin`; +- держать wallet-state в `background service worker`, а popup использовать как UI. + +## Как загрузить локально + +1. Открой `chrome://extensions/` +2. Включи `Developer mode` +3. Нажми `Load unpacked` +4. Выбери папку `SHiNE-browser-plugin-wallet/` + +## Ограничения текущего этапа + +- plugin пока не держит постоянный фоновый WS-канал после закрытия popup, но хранит wallet-state в `background`; +- на этом этапе реализован только `session-only login`; +- запросы на подпись будут следующим этапом. +- pairing-пароль, если он используется, должен генерироваться в формате `sha256$` от строки `shine-pairing|loginLower|password`. + +## Сборка crypto bundle + +Для обычной загрузки plugin это не нужно: bundled crypto-файл уже лежит в репозитории. + +Если понадобится пересобрать локальный crypto bundle: + +```bash +npm install +npx esbuild js/lib/vendor/noble-ed25519-entry.js --bundle --format=esm --platform=browser --outfile=js/lib/vendor/noble-ed25519-bundle.js +``` diff --git a/SHiNE-browser-plugin-wallet/background.js b/SHiNE-browser-plugin-wallet/background.js new file mode 100644 index 0000000..cf32f62 --- /dev/null +++ b/SHiNE-browser-plugin-wallet/background.js @@ -0,0 +1,294 @@ +import { createRequesterPairingMaterial, decryptPairingPayloadFromEnvelope, deriveEspPairingPasswordHash } from './js/lib/device-pairing.js'; +import { loadPluginSettings, loadSessionMaterial, savePluginSettings, saveSessionMaterial, clearSessionMaterial } from './js/lib/session-store.js'; +import { ShineApiClient } from './js/lib/shine-api.js'; + +const state = { + api: null, + settings: { + serverUrl: 'wss://shineup.me/ws', + login: '', + }, + requesterMaterial: null, + pairingId: '', + expiresAtMs: 0, + shortCode: '', + trustedSessionOnline: false, + pollTimer: 0, + activeSession: null, + connectionOnline: false, + statusText: '', + statusKind: 'info', +}; + +function setStatus(message = '', kind = 'info') { + state.statusText = String(message || ''); + state.statusKind = kind === 'error' ? 'error' : 'info'; +} + +function stopPoll() { + if (state.pollTimer) { + clearTimeout(state.pollTimer); + state.pollTimer = 0; + } +} + +function clearPairingState() { + stopPoll(); + state.requesterMaterial = null; + state.pairingId = ''; + state.expiresAtMs = 0; + state.shortCode = ''; + state.trustedSessionOnline = false; +} + +function ensureApi(serverUrl = state.settings.serverUrl) { + const normalized = String(serverUrl || '').trim() || 'wss://shineup.me/ws'; + if (!state.api || state.api.serverUrl !== normalized) { + state.api?.close(); + state.api = new ShineApiClient(normalized); + } + return state.api; +} + +async function loadStateFromStorage() { + const settings = await loadPluginSettings(); + state.settings = { + serverUrl: String(settings?.serverUrl || 'wss://shineup.me/ws').trim() || 'wss://shineup.me/ws', + login: String(settings?.login || '').trim(), + }; + state.activeSession = await loadSessionMaterial(); +} + +async function persistSettings(nextSettings = {}) { + state.settings = { + ...state.settings, + ...nextSettings, + }; + await savePluginSettings(state.settings); + return state.settings; +} + +async function resumeActiveSession() { + const sessionRecord = await loadSessionMaterial(); + state.activeSession = sessionRecord; + if (!sessionRecord) { + state.connectionOnline = false; + setStatus('Wallet-session ещё не подключена.', 'info'); + return { ok: true, connected: false }; + } + + try { + await persistSettings({ + serverUrl: String(sessionRecord?.serverUrl || state.settings.serverUrl || 'wss://shineup.me/ws').trim(), + login: String(sessionRecord?.login || state.settings.login || '').trim(), + }); + const resumed = await ensureApi().resumeSession(sessionRecord); + state.connectionOnline = true; + setStatus(`Wallet-session активна для @${resumed.login}.`, 'info'); + return { ok: true, connected: true, login: resumed.login, sessionId: resumed.sessionId }; + } catch (error) { + state.connectionOnline = false; + setStatus(error.message || 'Не удалось восстановить wallet-session.', 'error'); + return { ok: false, connected: false, error: state.statusText }; + } +} + +async function attachApprovedSession(payload) { + if (String(payload?.type || '') !== 'shine-esp-session-attach') { + throw new Error('Доверенное устройство вернуло неподдерживаемый payload. Для plugin нужен session-only approve.'); + } + + const login = String(payload?.login || state.settings.login || '').trim(); + const approvedSession = payload?.session || {}; + const sessionRecord = { + login, + sessionId: String(approvedSession?.sessionId || '').trim(), + sessionKey: state.requesterMaterial?.sessionKey || '', + sessionPrivPkcs8: state.requesterMaterial?.sessionPrivPkcs8 || '', + sessionType: Number(approvedSession?.sessionType || 50) || 50, + serverUrl: state.settings.serverUrl, + }; + if (!sessionRecord.login || !sessionRecord.sessionId || !sessionRecord.sessionKey || !sessionRecord.sessionPrivPkcs8) { + throw new Error('Получен неполный session-only payload'); + } + + await clearSessionMaterial(); + await saveSessionMaterial(sessionRecord); + state.activeSession = sessionRecord; + await persistSettings({ + login: sessionRecord.login, + serverUrl: sessionRecord.serverUrl, + }); + await resumeActiveSession(); +} + +async function pollPairingStatus() { + if (!state.pairingId || !state.requesterMaterial) return; + try { + const payload = await ensureApi().getEspPairingStatus(state.pairingId); + const stateValue = String(payload?.state || ''); + if (stateValue === 'created') { + state.pollTimer = setTimeout(() => { + void pollPairingStatus(); + }, 2200); + return; + } + if (stateValue === 'approved') { + const decoded = await decryptPairingPayloadFromEnvelope(payload?.encryptedPayload, state.requesterMaterial); + await attachApprovedSession(decoded); + clearPairingState(); + setStatus('Wallet-session создана и сохранена.', 'info'); + return; + } + if (stateValue === 'rejected') { + clearPairingState(); + setStatus('Заявка отклонена на доверенном устройстве.', 'error'); + return; + } + if (stateValue === 'expired' || stateValue === 'canceled') { + clearPairingState(); + setStatus('Ожидание подключения завершено.', 'error'); + return; + } + state.pollTimer = setTimeout(() => { + void pollPairingStatus(); + }, 2200); + } catch (error) { + clearPairingState(); + setStatus(error.message || 'Не удалось проверить pairing-статус.', 'error'); + } +} + +async function startPairing({ login, usePassword, password, serverUrl }) { + const cleanLogin = String(login || '').trim(); + if (!cleanLogin) { + throw new Error('Введите логин.'); + } + + await persistSettings({ + serverUrl: String(serverUrl || state.settings.serverUrl || 'wss://shineup.me/ws').trim() || 'wss://shineup.me/ws', + login: cleanLogin, + }); + clearPairingState(); + setStatus('Проверяем пользователя и создаём wallet-session заявку...', 'info'); + + const api = ensureApi(); + const user = await api.getUser(cleanLogin); + if (user?.exists !== true) { + throw new Error('Пользователь не найден.'); + } + + state.requesterMaterial = await createRequesterPairingMaterial(); + const passwordHash = usePassword + ? await deriveEspPairingPasswordHash(cleanLogin, String(password || '')) + : ''; + const payload = await api.startEspPairing({ + login: cleanLogin, + passwordHash, + requesterSessionKey: state.requesterMaterial.sessionKey, + payloadType: 1, + }); + + state.pairingId = String(payload?.pairingId || '').trim(); + state.expiresAtMs = Number(payload?.expiresAtMs || 0); + state.shortCode = String(payload?.shortCode || '0000000'); + state.trustedSessionOnline = !!payload?.trustedSessionOnline; + if (!state.pairingId) { + throw new Error('Сервер не вернул pairingId.'); + } + + state.pollTimer = setTimeout(() => { + void pollPairingStatus(); + }, 1800); + + setStatus('Wallet-session заявка создана. Ожидаем подтверждение на доверенном устройстве.', 'info'); + return { + pairingId: state.pairingId, + shortCode: String(payload?.shortCode || '0000000'), + expiresAtMs: state.expiresAtMs, + trustedSessionOnline: !!payload?.trustedSessionOnline, + }; +} + +async function cancelPairing() { + if (!state.pairingId || !state.requesterMaterial?.sessionKey) { + clearPairingState(); + return { ok: true }; + } + await ensureApi().cancelEspPairing(state.pairingId, state.requesterMaterial.sessionKey); + clearPairingState(); + setStatus('Ожидание подключения отменено.', 'info'); + return { ok: true }; +} + +async function disconnectSession() { + await clearSessionMaterial(); + state.activeSession = null; + state.connectionOnline = false; + setStatus('Сохранённая wallet-session удалена из plugin.', 'info'); + return { ok: true }; +} + +function snapshot() { + return { + settings: { ...state.settings }, + pairing: { + active: !!state.pairingId, + pairingId: state.pairingId, + expiresAtMs: state.expiresAtMs, + shortCode: state.shortCode, + trustedSessionOnline: state.trustedSessionOnline, + }, + session: state.activeSession ? { ...state.activeSession } : null, + connectionOnline: state.connectionOnline, + status: { + text: state.statusText, + kind: state.statusKind, + }, + }; +} + +chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => { + (async () => { + const type = String(message?.type || ''); + if (type === 'wallet:getState') { + await loadStateFromStorage(); + sendResponse({ ok: true, state: snapshot() }); + return; + } + if (type === 'wallet:saveSettings') { + await persistSettings(message?.payload || {}); + sendResponse({ ok: true, state: snapshot() }); + return; + } + if (type === 'wallet:startPairing') { + const result = await startPairing(message?.payload || {}); + sendResponse({ ok: true, result, state: snapshot() }); + return; + } + if (type === 'wallet:cancelPairing') { + const result = await cancelPairing(); + sendResponse({ ok: true, result, state: snapshot() }); + return; + } + if (type === 'wallet:resumeSession') { + const result = await resumeActiveSession(); + sendResponse({ ok: true, result, state: snapshot() }); + return; + } + if (type === 'wallet:disconnectSession') { + const result = await disconnectSession(); + sendResponse({ ok: true, result, state: snapshot() }); + return; + } + sendResponse({ ok: false, error: 'UNKNOWN_MESSAGE' }); + })().catch((error) => { + setStatus(error?.message || 'Unknown error', 'error'); + sendResponse({ ok: false, error: error?.message || 'Unknown error', state: snapshot() }); + }); + return true; +}); + +void loadStateFromStorage().then(() => resumeActiveSession()).catch((error) => { + setStatus(error?.message || 'Не удалось инициализировать wallet plugin.', 'error'); +}); diff --git a/ESP32-wallet/build.gradle b/SHiNE-browser-plugin-wallet/build.gradle similarity index 100% rename from ESP32-wallet/build.gradle rename to SHiNE-browser-plugin-wallet/build.gradle diff --git a/ESP32-wallet/gradle/wrapper/gradle-wrapper.jar b/SHiNE-browser-plugin-wallet/gradle/wrapper/gradle-wrapper.jar similarity index 100% rename from ESP32-wallet/gradle/wrapper/gradle-wrapper.jar rename to SHiNE-browser-plugin-wallet/gradle/wrapper/gradle-wrapper.jar diff --git a/ESP32-wallet/gradle/wrapper/gradle-wrapper.properties b/SHiNE-browser-plugin-wallet/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from ESP32-wallet/gradle/wrapper/gradle-wrapper.properties rename to SHiNE-browser-plugin-wallet/gradle/wrapper/gradle-wrapper.properties diff --git a/ESP32-wallet/gradlew b/SHiNE-browser-plugin-wallet/gradlew similarity index 100% rename from ESP32-wallet/gradlew rename to SHiNE-browser-plugin-wallet/gradlew diff --git a/ESP32-wallet/gradlew.bat b/SHiNE-browser-plugin-wallet/gradlew.bat similarity index 100% rename from ESP32-wallet/gradlew.bat rename to SHiNE-browser-plugin-wallet/gradlew.bat diff --git a/SHiNE-browser-plugin-wallet/js/lib/crypto-utils.js b/SHiNE-browser-plugin-wallet/js/lib/crypto-utils.js new file mode 100644 index 0000000..4bc236a --- /dev/null +++ b/SHiNE-browser-plugin-wallet/js/lib/crypto-utils.js @@ -0,0 +1,78 @@ +function getCryptoApi() { + const api = globalThis.crypto; + if (!api?.subtle || typeof api.getRandomValues !== 'function') { + throw new Error('WebCrypto недоступен в текущем браузере.'); + } + return api; +} + +function getSubtleApi() { + return getCryptoApi().subtle; +} + +function base64UrlToBase64(value) { + const normalized = String(value || '').trim().replace(/-/g, '+').replace(/_/g, '/'); + return normalized + '='.repeat((4 - (normalized.length % 4)) % 4); +} + +export function utf8Bytes(value) { + return new TextEncoder().encode(String(value ?? '')); +} + +export function bytesToBase64(bytes) { + let binary = ''; + const chunk = 0x8000; + for (let i = 0; i < bytes.length; i += chunk) { + const slice = bytes.subarray(i, i + chunk); + binary += String.fromCharCode(...slice); + } + return btoa(binary); +} + +export function base64ToBytes(value) { + const binary = atob(base64UrlToBase64(value)); + const out = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i += 1) out[i] = binary.charCodeAt(i); + return out; +} + +export async function generateEd25519Pair() { + return getSubtleApi().generateKey({ name: 'Ed25519' }, true, ['sign', 'verify']); +} + +export async function exportEd25519PublicKeyB64(publicKey) { + const raw = await getSubtleApi().exportKey('raw', publicKey); + return bytesToBase64(new Uint8Array(raw)); +} + +export async function exportPkcs8B64(privateKey) { + const raw = await getSubtleApi().exportKey('pkcs8', privateKey); + return bytesToBase64(new Uint8Array(raw)); +} + +export async function importPkcs8Ed25519(pkcs8B64) { + return getSubtleApi().importKey('pkcs8', base64ToBytes(pkcs8B64), { name: 'Ed25519' }, false, ['sign']); +} + +export async function signBase64(privateKey, text) { + const signature = await getSubtleApi().sign({ name: 'Ed25519' }, privateKey, utf8Bytes(text)); + return bytesToBase64(new Uint8Array(signature)); +} + +export async function sha256Bytes(bytes) { + const digest = await getSubtleApi().digest('SHA-256', bytes); + return new Uint8Array(digest); +} + +export async function sha256Text(text) { + return sha256Bytes(utf8Bytes(text)); +} + +export function randomBase64(size) { + const bytes = getCryptoApi().getRandomValues(new Uint8Array(size)); + return bytesToBase64(bytes); +} + +export function bytesToHex(bytes) { + return [...bytes].map((byte) => byte.toString(16).padStart(2, '0')).join(''); +} diff --git a/SHiNE-browser-plugin-wallet/js/lib/device-pairing.js b/SHiNE-browser-plugin-wallet/js/lib/device-pairing.js new file mode 100644 index 0000000..f1b68e8 --- /dev/null +++ b/SHiNE-browser-plugin-wallet/js/lib/device-pairing.js @@ -0,0 +1,90 @@ +import { + base64ToBytes, + bytesToBase64, + bytesToHex, + exportEd25519PublicKeyB64, + exportPkcs8B64, + generateEd25519Pair, + sha256Bytes, + sha256Text, + utf8Bytes, +} from './crypto-utils.js'; +import { edwardsToMontgomeryPriv, x25519 } from './vendor/noble-ed25519-bundle.js'; + +const PAIRING_ENVELOPE_PREFIX = 'shine-esp-pairing-v1:'; +const PAIRING_HASH_PREFIX = 'sha256$'; +const PAIRING_HASH_VERSION = 'shine-pairing'; +const ED25519_PKCS8_PREFIX = new Uint8Array([ + 0x30, 0x2e, 0x02, 0x01, 0x00, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x04, 0x22, 0x04, 0x20, +]); + +function getCryptoApi() { + const api = globalThis.crypto; + if (!api?.subtle || typeof api.getRandomValues !== 'function') { + throw new Error('WebCrypto недоступен.'); + } + return api; +} + +async function importAesKeyFromSharedSecret(sharedSecretBytes) { + const digest = await sha256Bytes(sharedSecretBytes); + return getCryptoApi().subtle.importKey('raw', digest, { name: 'AES-GCM' }, false, ['decrypt']); +} + +function base64UrlToBytes(value) { + const normalized = String(value || '').trim().replace(/-/g, '+').replace(/_/g, '/'); + const padded = normalized + '='.repeat((4 - (normalized.length % 4)) % 4); + return base64ToBytes(padded); +} + +function extractSeedFromPkcs8(pkcs8B64) { + const raw = base64ToBytes(pkcs8B64); + if (raw.length !== ED25519_PKCS8_PREFIX.length + 32) { + throw new Error('Некорректный приватный Ed25519 ключ'); + } + for (let i = 0; i < ED25519_PKCS8_PREFIX.length; i += 1) { + if (raw[i] !== ED25519_PKCS8_PREFIX[i]) { + throw new Error('Неподдерживаемый формат приватного Ed25519 ключа'); + } + } + return raw.slice(ED25519_PKCS8_PREFIX.length); +} + +export async function createRequesterPairingMaterial() { + const sessionPair = await generateEd25519Pair(); + const sessionPublicB64 = await exportEd25519PublicKeyB64(sessionPair.publicKey); + return { + sessionKey: `ed25519/${sessionPublicB64}`, + sessionPrivPkcs8: await exportPkcs8B64(sessionPair.privateKey), + }; +} + +export async function deriveEspPairingPasswordHash(login, password) { + const loginLower = String(login || '').trim().toLowerCase(); + const preimage = `${PAIRING_HASH_VERSION}|${loginLower}|${String(password ?? '')}`; + const digest = await sha256Text(preimage); + return `${PAIRING_HASH_PREFIX}${bytesToHex(digest)}`; +} + +export async function decryptPairingPayloadFromEnvelope(encryptedPayload, requesterPairingMaterial) { + const raw = String(encryptedPayload || '').trim(); + if (!raw.startsWith(PAIRING_ENVELOPE_PREFIX)) { + throw new Error('Неподдерживаемый формат pairing payload'); + } + const jsonBytes = base64UrlToBytes(raw.slice(PAIRING_ENVELOPE_PREFIX.length)); + const envelope = JSON.parse(new TextDecoder().decode(jsonBytes)); + if (Number(envelope?.v) !== 1 || String(envelope?.alg || '') !== 'x25519-aes256-gcm') { + throw new Error('Неподдерживаемая версия pairing payload'); + } + + const requesterSeed = extractSeedFromPkcs8(String(requesterPairingMaterial?.sessionPrivPkcs8 || '')); + const requesterMontPriv = edwardsToMontgomeryPriv(requesterSeed); + const sharedSecret = x25519.getSharedSecret(requesterMontPriv, base64ToBytes(String(envelope?.ephPubB64 || ''))); + const aesKey = await importAesKeyFromSharedSecret(sharedSecret); + const plain = await getCryptoApi().subtle.decrypt( + { name: 'AES-GCM', iv: base64ToBytes(String(envelope?.ivB64 || '')) }, + aesKey, + base64ToBytes(String(envelope?.cipherB64 || '')), + ); + return JSON.parse(new TextDecoder().decode(plain)); +} diff --git a/SHiNE-browser-plugin-wallet/js/lib/session-store.js b/SHiNE-browser-plugin-wallet/js/lib/session-store.js new file mode 100644 index 0000000..d010d0b --- /dev/null +++ b/SHiNE-browser-plugin-wallet/js/lib/session-store.js @@ -0,0 +1,152 @@ +import { base64ToBytes, bytesToBase64 } from './crypto-utils.js'; + +const DB_NAME = 'shine-wallet-plugin'; +const DB_VERSION = 1; +const STORE_META = 'meta'; +const STORE_VAULT = 'vault'; +const SESSION_ENTRY_ID = 'active-session'; +const VAULT_KEY_ID = 'session-wrap-key'; + +function openDb() { + return new Promise((resolve, reject) => { + const request = indexedDB.open(DB_NAME, DB_VERSION); + request.onupgradeneeded = () => { + const db = request.result; + if (!db.objectStoreNames.contains(STORE_META)) { + db.createObjectStore(STORE_META, { keyPath: 'id' }); + } + if (!db.objectStoreNames.contains(STORE_VAULT)) { + db.createObjectStore(STORE_VAULT, { keyPath: 'id' }); + } + }; + request.onsuccess = () => resolve(request.result); + request.onerror = () => reject(request.error || new Error('IndexedDB недоступен')); + }); +} + +async function withStore(storeName, mode, run) { + const db = await openDb(); + try { + return await new Promise((resolve, reject) => { + const tx = db.transaction(storeName, mode); + const store = tx.objectStore(storeName); + let settled = false; + const done = (fn) => (value) => { + if (settled) return; + settled = true; + fn(value); + }; + tx.oncomplete = () => done(resolve)(undefined); + tx.onerror = () => done(reject)(tx.error || new Error('IndexedDB transaction failed')); + Promise.resolve(run(store, tx, done)).catch((error) => done(reject)(error)); + }); + } finally { + db.close(); + } +} + +async function put(storeName, value) { + return withStore(storeName, 'readwrite', (store) => { + store.put(value); + }); +} + +async function get(storeName, key) { + const db = await openDb(); + try { + return await new Promise((resolve, reject) => { + const tx = db.transaction(storeName, 'readonly'); + const req = tx.objectStore(storeName).get(key); + req.onsuccess = () => resolve(req.result || null); + req.onerror = () => reject(req.error || new Error('Ошибка чтения из IndexedDB')); + }); + } finally { + db.close(); + } +} + +async function deleteById(storeName, key) { + return withStore(storeName, 'readwrite', (store) => { + store.delete(key); + }); +} + +async function getOrCreateVaultKey() { + const current = await get(STORE_META, VAULT_KEY_ID); + if (current?.key) return current.key; + + const key = await crypto.subtle.generateKey( + { name: 'AES-GCM', length: 256 }, + false, + ['encrypt', 'decrypt'], + ); + await put(STORE_META, { id: VAULT_KEY_ID, key, createdAtMs: Date.now() }); + return key; +} + +async function encryptJson(value) { + const key = await getOrCreateVaultKey(); + const iv = crypto.getRandomValues(new Uint8Array(12)); + const plainBytes = new TextEncoder().encode(JSON.stringify(value)); + const cipher = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, plainBytes); + return { + ivB64: bytesToBase64(iv), + cipherB64: bytesToBase64(new Uint8Array(cipher)), + }; +} + +async function decryptJson(envelope) { + const key = await getOrCreateVaultKey(); + const plain = await crypto.subtle.decrypt( + { name: 'AES-GCM', iv: base64ToBytes(envelope.ivB64) }, + key, + base64ToBytes(envelope.cipherB64), + ); + return JSON.parse(new TextDecoder().decode(plain)); +} + +function storageApi() { + if (globalThis.chrome?.storage?.local) return globalThis.chrome.storage.local; + return null; +} + +export async function savePluginSettings(settings) { + const api = storageApi(); + if (api) { + await api.set({ shineWalletSettings: settings }); + return; + } + localStorage.setItem('shineWalletSettings', JSON.stringify(settings)); +} + +export async function loadPluginSettings() { + const api = storageApi(); + if (api) { + const row = await api.get('shineWalletSettings'); + return row?.shineWalletSettings || {}; + } + try { + return JSON.parse(localStorage.getItem('shineWalletSettings') || '{}'); + } catch { + return {}; + } +} + +export async function saveSessionMaterial(sessionRecord) { + const encrypted = await encryptJson(sessionRecord); + await put(STORE_VAULT, { + id: SESSION_ENTRY_ID, + encrypted, + updatedAtMs: Date.now(), + }); +} + +export async function loadSessionMaterial() { + const row = await get(STORE_VAULT, SESSION_ENTRY_ID); + if (!row?.encrypted) return null; + return decryptJson(row.encrypted); +} + +export async function clearSessionMaterial() { + await deleteById(STORE_VAULT, SESSION_ENTRY_ID); +} diff --git a/SHiNE-browser-plugin-wallet/js/lib/shine-api.js b/SHiNE-browser-plugin-wallet/js/lib/shine-api.js new file mode 100644 index 0000000..9a5c9a8 --- /dev/null +++ b/SHiNE-browser-plugin-wallet/js/lib/shine-api.js @@ -0,0 +1,111 @@ +import { importPkcs8Ed25519, signBase64 } from './crypto-utils.js'; +import { WsJsonClient } from './ws-client.js'; + +const SESSION_TYPE_WALLET = 50; + +function normalizeServerUrl(url) { + const value = String(url || '').trim(); + if (!value) return 'wss://shineup.me/ws'; + if (value.startsWith('ws://') || value.startsWith('wss://')) return value; + if (value.startsWith('http://') || value.startsWith('https://')) { + const parsed = new URL(value); + parsed.protocol = parsed.protocol === 'https:' ? 'wss:' : 'ws:'; + if (!parsed.pathname || parsed.pathname === '/') parsed.pathname = '/ws'; + return parsed.toString(); + } + return value; +} + +function opError(op, response) { + const payload = response?.payload || {}; + const message = payload?.message || response?.message || payload?.error || response?.error || 'Unknown server error'; + const code = String(payload?.code || response?.code || payload?.error || response?.error || 'UNKNOWN').toUpperCase(); + const error = new Error(`${op}: ${message} (${code})`); + error.op = op; + error.code = code; + error.status = response?.status || 0; + return error; +} + +export class ShineApiClient { + constructor(serverUrl) { + this.serverUrl = normalizeServerUrl(serverUrl); + this.ws = new WsJsonClient(this.serverUrl); + } + + async getUser(login) { + const response = await this.ws.request('GetUser', { login: String(login || '').trim() }); + if (response.status !== 200) throw opError('GetUser', response); + return response.payload || {}; + } + + async startEspPairing({ login, passwordHash, requesterSessionKey, payloadType = 1 }) { + const response = await this.ws.request('StartEspPairing', { + login: String(login || '').trim(), + passwordHash: String(passwordHash || '').trim(), + requesterSessionKey: String(requesterSessionKey || '').trim(), + requesterSessionType: SESSION_TYPE_WALLET, + requesterClientPlatform: 'Chrome Extension Wallet', + payloadType: Number(payloadType) || 1, + }); + if (response.status !== 200) throw opError('StartEspPairing', response); + return response.payload || {}; + } + + async getEspPairingStatus(pairingId) { + const response = await this.ws.request('GetEspPairingStatus', { + pairingId: String(pairingId || '').trim(), + }); + if (response.status !== 200) throw opError('GetEspPairingStatus', response); + return response.payload || {}; + } + + async cancelEspPairing(pairingId, requesterSessionKey) { + const response = await this.ws.request('CancelEspPairing', { + pairingId: String(pairingId || '').trim(), + requesterSessionKey: String(requesterSessionKey || '').trim(), + }); + if (response.status !== 200) throw opError('CancelEspPairing', response); + return response.payload || {}; + } + + async resumeSession(sessionRecord) { + const login = String(sessionRecord?.login || '').trim(); + const sessionId = String(sessionRecord?.sessionId || '').trim(); + const sessionKey = String(sessionRecord?.sessionKey || '').trim(); + const sessionPrivPkcs8 = String(sessionRecord?.sessionPrivPkcs8 || '').trim(); + if (!login || !sessionId || !sessionKey || !sessionPrivPkcs8) { + throw new Error('Сохранённая wallet-session неполная'); + } + + const privateKey = await importPkcs8Ed25519(sessionPrivPkcs8); + const challengeResp = await this.ws.request('SessionChallenge', { sessionId }); + if (challengeResp.status !== 200) throw opError('SessionChallenge', challengeResp); + const nonce = challengeResp?.payload?.nonce; + if (!nonce) throw new Error('SessionChallenge: сервер не вернул nonce'); + + const timeMs = Date.now(); + const preimage = `SESSION_LOGIN:${sessionId}:${timeMs}:${nonce}`; + const signatureB64 = await signBase64(privateKey, preimage); + const loginResp = await this.ws.request('SessionLogin', { + sessionId, + sessionKey, + timeMs, + signatureB64, + sessionType: Number(sessionRecord?.sessionType || SESSION_TYPE_WALLET) || SESSION_TYPE_WALLET, + clientPlatform: 'Chrome Extension Wallet', + clientInfo: 'SHiNE Browser Plugin Wallet', + }); + if (loginResp.status !== 200) throw opError('SessionLogin', loginResp); + + return { + login, + sessionId, + storagePwd: String(loginResp?.payload?.storagePwd || '').trim(), + }; + } + + close() { + this.ws.close(); + } +} diff --git a/SHiNE-browser-plugin-wallet/js/lib/vendor/noble-ed25519-bundle.js b/SHiNE-browser-plugin-wallet/js/lib/vendor/noble-ed25519-bundle.js new file mode 100644 index 0000000..4407070 --- /dev/null +++ b/SHiNE-browser-plugin-wallet/js/lib/vendor/noble-ed25519-bundle.js @@ -0,0 +1,995 @@ +// node_modules/@noble/curves/node_modules/@noble/hashes/esm/_assert.js +function isBytes(a) { + return a instanceof Uint8Array || a != null && typeof a === "object" && a.constructor.name === "Uint8Array"; +} +function bytes(b, ...lengths) { + if (!isBytes(b)) + throw new Error("Uint8Array expected"); + if (lengths.length > 0 && !lengths.includes(b.length)) + throw new Error(`Uint8Array expected of length ${lengths}, not of length=${b.length}`); +} +function exists(instance, checkFinished = true) { + if (instance.destroyed) + throw new Error("Hash instance has been destroyed"); + if (checkFinished && instance.finished) + throw new Error("Hash#digest() has already been called"); +} +function output(out, instance) { + bytes(out); + const min = instance.outputLen; + if (out.length < min) { + throw new Error(`digestInto() expects output buffer of length at least ${min}`); + } +} + +// node_modules/@noble/curves/node_modules/@noble/hashes/esm/crypto.js +var crypto = typeof globalThis === "object" && "crypto" in globalThis ? globalThis.crypto : void 0; + +// node_modules/@noble/curves/node_modules/@noble/hashes/esm/utils.js +var createView = (arr) => new DataView(arr.buffer, arr.byteOffset, arr.byteLength); +var isLE = new Uint8Array(new Uint32Array([287454020]).buffer)[0] === 68; +function utf8ToBytes(str) { + if (typeof str !== "string") + throw new Error(`utf8ToBytes expected string, got ${typeof str}`); + return new Uint8Array(new TextEncoder().encode(str)); +} +function toBytes(data) { + if (typeof data === "string") + data = utf8ToBytes(data); + bytes(data); + return data; +} +var Hash = class { + // Safe version that clones internal state + clone() { + return this._cloneInto(); + } +}; +var toStr = {}.toString; +function wrapConstructor(hashCons) { + const hashC = (msg) => hashCons().update(toBytes(msg)).digest(); + const tmp = hashCons(); + hashC.outputLen = tmp.outputLen; + hashC.blockLen = tmp.blockLen; + hashC.create = () => hashCons(); + return hashC; +} +function randomBytes(bytesLength = 32) { + if (crypto && typeof crypto.getRandomValues === "function") { + return crypto.getRandomValues(new Uint8Array(bytesLength)); + } + throw new Error("crypto.getRandomValues must be defined"); +} + +// node_modules/@noble/curves/node_modules/@noble/hashes/esm/_md.js +function setBigUint64(view, byteOffset, value, isLE2) { + if (typeof view.setBigUint64 === "function") + return view.setBigUint64(byteOffset, value, isLE2); + const _32n2 = BigInt(32); + const _u32_max = BigInt(4294967295); + const wh = Number(value >> _32n2 & _u32_max); + const wl = Number(value & _u32_max); + const h = isLE2 ? 4 : 0; + const l = isLE2 ? 0 : 4; + view.setUint32(byteOffset + h, wh, isLE2); + view.setUint32(byteOffset + l, wl, isLE2); +} +var HashMD = class extends Hash { + constructor(blockLen, outputLen, padOffset, isLE2) { + super(); + this.blockLen = blockLen; + this.outputLen = outputLen; + this.padOffset = padOffset; + this.isLE = isLE2; + this.finished = false; + this.length = 0; + this.pos = 0; + this.destroyed = false; + this.buffer = new Uint8Array(blockLen); + this.view = createView(this.buffer); + } + update(data) { + exists(this); + const { view, buffer, blockLen } = this; + data = toBytes(data); + const len = data.length; + for (let pos = 0; pos < len; ) { + const take = Math.min(blockLen - this.pos, len - pos); + if (take === blockLen) { + const dataView = createView(data); + for (; blockLen <= len - pos; pos += blockLen) + this.process(dataView, pos); + continue; + } + buffer.set(data.subarray(pos, pos + take), this.pos); + this.pos += take; + pos += take; + if (this.pos === blockLen) { + this.process(view, 0); + this.pos = 0; + } + } + this.length += data.length; + this.roundClean(); + return this; + } + digestInto(out) { + exists(this); + output(out, this); + this.finished = true; + const { buffer, view, blockLen, isLE: isLE2 } = this; + let { pos } = this; + buffer[pos++] = 128; + this.buffer.subarray(pos).fill(0); + if (this.padOffset > blockLen - pos) { + this.process(view, 0); + pos = 0; + } + for (let i = pos; i < blockLen; i++) + buffer[i] = 0; + setBigUint64(view, blockLen - 8, BigInt(this.length * 8), isLE2); + this.process(view, 0); + const oview = createView(out); + const len = this.outputLen; + if (len % 4) + throw new Error("_sha2: outputLen should be aligned to 32bit"); + const outLen = len / 4; + const state = this.get(); + if (outLen > state.length) + throw new Error("_sha2: outputLen bigger than state"); + for (let i = 0; i < outLen; i++) + oview.setUint32(4 * i, state[i], isLE2); + } + digest() { + const { buffer, outputLen } = this; + this.digestInto(buffer); + const res = buffer.slice(0, outputLen); + this.destroy(); + return res; + } + _cloneInto(to) { + to || (to = new this.constructor()); + to.set(...this.get()); + const { blockLen, buffer, length, finished, destroyed, pos } = this; + to.length = length; + to.pos = pos; + to.finished = finished; + to.destroyed = destroyed; + if (length % blockLen) + to.buffer.set(buffer); + return to; + } +}; + +// node_modules/@noble/curves/node_modules/@noble/hashes/esm/_u64.js +var U32_MASK64 = /* @__PURE__ */ BigInt(2 ** 32 - 1); +var _32n = /* @__PURE__ */ BigInt(32); +function fromBig(n, le = false) { + if (le) + return { h: Number(n & U32_MASK64), l: Number(n >> _32n & U32_MASK64) }; + return { h: Number(n >> _32n & U32_MASK64) | 0, l: Number(n & U32_MASK64) | 0 }; +} +function split(lst, le = false) { + let Ah = new Uint32Array(lst.length); + let Al = new Uint32Array(lst.length); + for (let i = 0; i < lst.length; i++) { + const { h, l } = fromBig(lst[i], le); + [Ah[i], Al[i]] = [h, l]; + } + return [Ah, Al]; +} +var toBig = (h, l) => BigInt(h >>> 0) << _32n | BigInt(l >>> 0); +var shrSH = (h, _l, s) => h >>> s; +var shrSL = (h, l, s) => h << 32 - s | l >>> s; +var rotrSH = (h, l, s) => h >>> s | l << 32 - s; +var rotrSL = (h, l, s) => h << 32 - s | l >>> s; +var rotrBH = (h, l, s) => h << 64 - s | l >>> s - 32; +var rotrBL = (h, l, s) => h >>> s - 32 | l << 64 - s; +var rotr32H = (_h, l) => l; +var rotr32L = (h, _l) => h; +var rotlSH = (h, l, s) => h << s | l >>> 32 - s; +var rotlSL = (h, l, s) => l << s | h >>> 32 - s; +var rotlBH = (h, l, s) => l << s - 32 | h >>> 64 - s; +var rotlBL = (h, l, s) => h << s - 32 | l >>> 64 - s; +function add(Ah, Al, Bh, Bl) { + const l = (Al >>> 0) + (Bl >>> 0); + return { h: Ah + Bh + (l / 2 ** 32 | 0) | 0, l: l | 0 }; +} +var add3L = (Al, Bl, Cl) => (Al >>> 0) + (Bl >>> 0) + (Cl >>> 0); +var add3H = (low, Ah, Bh, Ch) => Ah + Bh + Ch + (low / 2 ** 32 | 0) | 0; +var add4L = (Al, Bl, Cl, Dl) => (Al >>> 0) + (Bl >>> 0) + (Cl >>> 0) + (Dl >>> 0); +var add4H = (low, Ah, Bh, Ch, Dh) => Ah + Bh + Ch + Dh + (low / 2 ** 32 | 0) | 0; +var add5L = (Al, Bl, Cl, Dl, El) => (Al >>> 0) + (Bl >>> 0) + (Cl >>> 0) + (Dl >>> 0) + (El >>> 0); +var add5H = (low, Ah, Bh, Ch, Dh, Eh) => Ah + Bh + Ch + Dh + Eh + (low / 2 ** 32 | 0) | 0; +var u64 = { + fromBig, + split, + toBig, + shrSH, + shrSL, + rotrSH, + rotrSL, + rotrBH, + rotrBL, + rotr32H, + rotr32L, + rotlSH, + rotlSL, + rotlBH, + rotlBL, + add, + add3L, + add3H, + add4L, + add4H, + add5H, + add5L +}; +var u64_default = u64; + +// node_modules/@noble/curves/node_modules/@noble/hashes/esm/sha512.js +var [SHA512_Kh, SHA512_Kl] = /* @__PURE__ */ (() => u64_default.split([ + "0x428a2f98d728ae22", + "0x7137449123ef65cd", + "0xb5c0fbcfec4d3b2f", + "0xe9b5dba58189dbbc", + "0x3956c25bf348b538", + "0x59f111f1b605d019", + "0x923f82a4af194f9b", + "0xab1c5ed5da6d8118", + "0xd807aa98a3030242", + "0x12835b0145706fbe", + "0x243185be4ee4b28c", + "0x550c7dc3d5ffb4e2", + "0x72be5d74f27b896f", + "0x80deb1fe3b1696b1", + "0x9bdc06a725c71235", + "0xc19bf174cf692694", + "0xe49b69c19ef14ad2", + "0xefbe4786384f25e3", + "0x0fc19dc68b8cd5b5", + "0x240ca1cc77ac9c65", + "0x2de92c6f592b0275", + "0x4a7484aa6ea6e483", + "0x5cb0a9dcbd41fbd4", + "0x76f988da831153b5", + "0x983e5152ee66dfab", + "0xa831c66d2db43210", + "0xb00327c898fb213f", + "0xbf597fc7beef0ee4", + "0xc6e00bf33da88fc2", + "0xd5a79147930aa725", + "0x06ca6351e003826f", + "0x142929670a0e6e70", + "0x27b70a8546d22ffc", + "0x2e1b21385c26c926", + "0x4d2c6dfc5ac42aed", + "0x53380d139d95b3df", + "0x650a73548baf63de", + "0x766a0abb3c77b2a8", + "0x81c2c92e47edaee6", + "0x92722c851482353b", + "0xa2bfe8a14cf10364", + "0xa81a664bbc423001", + "0xc24b8b70d0f89791", + "0xc76c51a30654be30", + "0xd192e819d6ef5218", + "0xd69906245565a910", + "0xf40e35855771202a", + "0x106aa07032bbd1b8", + "0x19a4c116b8d2d0c8", + "0x1e376c085141ab53", + "0x2748774cdf8eeb99", + "0x34b0bcb5e19b48a8", + "0x391c0cb3c5c95a63", + "0x4ed8aa4ae3418acb", + "0x5b9cca4f7763e373", + "0x682e6ff3d6b2b8a3", + "0x748f82ee5defb2fc", + "0x78a5636f43172f60", + "0x84c87814a1f0ab72", + "0x8cc702081a6439ec", + "0x90befffa23631e28", + "0xa4506cebde82bde9", + "0xbef9a3f7b2c67915", + "0xc67178f2e372532b", + "0xca273eceea26619c", + "0xd186b8c721c0c207", + "0xeada7dd6cde0eb1e", + "0xf57d4f7fee6ed178", + "0x06f067aa72176fba", + "0x0a637dc5a2c898a6", + "0x113f9804bef90dae", + "0x1b710b35131c471b", + "0x28db77f523047d84", + "0x32caab7b40c72493", + "0x3c9ebe0a15c9bebc", + "0x431d67c49c100d4c", + "0x4cc5d4becb3e42b6", + "0x597f299cfc657e2a", + "0x5fcb6fab3ad6faec", + "0x6c44198c4a475817" +].map((n) => BigInt(n))))(); +var SHA512_W_H = /* @__PURE__ */ new Uint32Array(80); +var SHA512_W_L = /* @__PURE__ */ new Uint32Array(80); +var SHA512 = class extends HashMD { + constructor() { + super(128, 64, 16, false); + this.Ah = 1779033703 | 0; + this.Al = 4089235720 | 0; + this.Bh = 3144134277 | 0; + this.Bl = 2227873595 | 0; + this.Ch = 1013904242 | 0; + this.Cl = 4271175723 | 0; + this.Dh = 2773480762 | 0; + this.Dl = 1595750129 | 0; + this.Eh = 1359893119 | 0; + this.El = 2917565137 | 0; + this.Fh = 2600822924 | 0; + this.Fl = 725511199 | 0; + this.Gh = 528734635 | 0; + this.Gl = 4215389547 | 0; + this.Hh = 1541459225 | 0; + this.Hl = 327033209 | 0; + } + // prettier-ignore + get() { + const { Ah, Al, Bh, Bl, Ch, Cl, Dh, Dl, Eh, El, Fh, Fl, Gh, Gl, Hh, Hl } = this; + return [Ah, Al, Bh, Bl, Ch, Cl, Dh, Dl, Eh, El, Fh, Fl, Gh, Gl, Hh, Hl]; + } + // prettier-ignore + set(Ah, Al, Bh, Bl, Ch, Cl, Dh, Dl, Eh, El, Fh, Fl, Gh, Gl, Hh, Hl) { + this.Ah = Ah | 0; + this.Al = Al | 0; + this.Bh = Bh | 0; + this.Bl = Bl | 0; + this.Ch = Ch | 0; + this.Cl = Cl | 0; + this.Dh = Dh | 0; + this.Dl = Dl | 0; + this.Eh = Eh | 0; + this.El = El | 0; + this.Fh = Fh | 0; + this.Fl = Fl | 0; + this.Gh = Gh | 0; + this.Gl = Gl | 0; + this.Hh = Hh | 0; + this.Hl = Hl | 0; + } + process(view, offset) { + for (let i = 0; i < 16; i++, offset += 4) { + SHA512_W_H[i] = view.getUint32(offset); + SHA512_W_L[i] = view.getUint32(offset += 4); + } + for (let i = 16; i < 80; i++) { + const W15h = SHA512_W_H[i - 15] | 0; + const W15l = SHA512_W_L[i - 15] | 0; + const s0h = u64_default.rotrSH(W15h, W15l, 1) ^ u64_default.rotrSH(W15h, W15l, 8) ^ u64_default.shrSH(W15h, W15l, 7); + const s0l = u64_default.rotrSL(W15h, W15l, 1) ^ u64_default.rotrSL(W15h, W15l, 8) ^ u64_default.shrSL(W15h, W15l, 7); + const W2h = SHA512_W_H[i - 2] | 0; + const W2l = SHA512_W_L[i - 2] | 0; + const s1h = u64_default.rotrSH(W2h, W2l, 19) ^ u64_default.rotrBH(W2h, W2l, 61) ^ u64_default.shrSH(W2h, W2l, 6); + const s1l = u64_default.rotrSL(W2h, W2l, 19) ^ u64_default.rotrBL(W2h, W2l, 61) ^ u64_default.shrSL(W2h, W2l, 6); + const SUMl = u64_default.add4L(s0l, s1l, SHA512_W_L[i - 7], SHA512_W_L[i - 16]); + const SUMh = u64_default.add4H(SUMl, s0h, s1h, SHA512_W_H[i - 7], SHA512_W_H[i - 16]); + SHA512_W_H[i] = SUMh | 0; + SHA512_W_L[i] = SUMl | 0; + } + let { Ah, Al, Bh, Bl, Ch, Cl, Dh, Dl, Eh, El, Fh, Fl, Gh, Gl, Hh, Hl } = this; + for (let i = 0; i < 80; i++) { + const sigma1h = u64_default.rotrSH(Eh, El, 14) ^ u64_default.rotrSH(Eh, El, 18) ^ u64_default.rotrBH(Eh, El, 41); + const sigma1l = u64_default.rotrSL(Eh, El, 14) ^ u64_default.rotrSL(Eh, El, 18) ^ u64_default.rotrBL(Eh, El, 41); + const CHIh = Eh & Fh ^ ~Eh & Gh; + const CHIl = El & Fl ^ ~El & Gl; + const T1ll = u64_default.add5L(Hl, sigma1l, CHIl, SHA512_Kl[i], SHA512_W_L[i]); + const T1h = u64_default.add5H(T1ll, Hh, sigma1h, CHIh, SHA512_Kh[i], SHA512_W_H[i]); + const T1l = T1ll | 0; + const sigma0h = u64_default.rotrSH(Ah, Al, 28) ^ u64_default.rotrBH(Ah, Al, 34) ^ u64_default.rotrBH(Ah, Al, 39); + const sigma0l = u64_default.rotrSL(Ah, Al, 28) ^ u64_default.rotrBL(Ah, Al, 34) ^ u64_default.rotrBL(Ah, Al, 39); + const MAJh = Ah & Bh ^ Ah & Ch ^ Bh & Ch; + const MAJl = Al & Bl ^ Al & Cl ^ Bl & Cl; + Hh = Gh | 0; + Hl = Gl | 0; + Gh = Fh | 0; + Gl = Fl | 0; + Fh = Eh | 0; + Fl = El | 0; + ({ h: Eh, l: El } = u64_default.add(Dh | 0, Dl | 0, T1h | 0, T1l | 0)); + Dh = Ch | 0; + Dl = Cl | 0; + Ch = Bh | 0; + Cl = Bl | 0; + Bh = Ah | 0; + Bl = Al | 0; + const All = u64_default.add3L(T1l, sigma0l, MAJl); + Ah = u64_default.add3H(All, T1h, sigma0h, MAJh); + Al = All | 0; + } + ({ h: Ah, l: Al } = u64_default.add(this.Ah | 0, this.Al | 0, Ah | 0, Al | 0)); + ({ h: Bh, l: Bl } = u64_default.add(this.Bh | 0, this.Bl | 0, Bh | 0, Bl | 0)); + ({ h: Ch, l: Cl } = u64_default.add(this.Ch | 0, this.Cl | 0, Ch | 0, Cl | 0)); + ({ h: Dh, l: Dl } = u64_default.add(this.Dh | 0, this.Dl | 0, Dh | 0, Dl | 0)); + ({ h: Eh, l: El } = u64_default.add(this.Eh | 0, this.El | 0, Eh | 0, El | 0)); + ({ h: Fh, l: Fl } = u64_default.add(this.Fh | 0, this.Fl | 0, Fh | 0, Fl | 0)); + ({ h: Gh, l: Gl } = u64_default.add(this.Gh | 0, this.Gl | 0, Gh | 0, Gl | 0)); + ({ h: Hh, l: Hl } = u64_default.add(this.Hh | 0, this.Hl | 0, Hh | 0, Hl | 0)); + this.set(Ah, Al, Bh, Bl, Ch, Cl, Dh, Dl, Eh, El, Fh, Fl, Gh, Gl, Hh, Hl); + } + roundClean() { + SHA512_W_H.fill(0); + SHA512_W_L.fill(0); + } + destroy() { + this.buffer.fill(0); + this.set(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); + } +}; +var sha512 = /* @__PURE__ */ wrapConstructor(() => new SHA512()); + +// node_modules/@noble/curves/esm/abstract/utils.js +var _0n = /* @__PURE__ */ BigInt(0); +var _1n = /* @__PURE__ */ BigInt(1); +var _2n = /* @__PURE__ */ BigInt(2); +function isBytes2(a) { + return a instanceof Uint8Array || a != null && typeof a === "object" && a.constructor.name === "Uint8Array"; +} +function abytes(item) { + if (!isBytes2(item)) + throw new Error("Uint8Array expected"); +} +var hexes = /* @__PURE__ */ Array.from({ length: 256 }, (_, i) => i.toString(16).padStart(2, "0")); +function bytesToHex(bytes2) { + abytes(bytes2); + let hex = ""; + for (let i = 0; i < bytes2.length; i++) { + hex += hexes[bytes2[i]]; + } + return hex; +} +function hexToNumber(hex) { + if (typeof hex !== "string") + throw new Error("hex string expected, got " + typeof hex); + return BigInt(hex === "" ? "0" : `0x${hex}`); +} +var asciis = { _0: 48, _9: 57, _A: 65, _F: 70, _a: 97, _f: 102 }; +function asciiToBase16(char) { + if (char >= asciis._0 && char <= asciis._9) + return char - asciis._0; + if (char >= asciis._A && char <= asciis._F) + return char - (asciis._A - 10); + if (char >= asciis._a && char <= asciis._f) + return char - (asciis._a - 10); + return; +} +function hexToBytes(hex) { + if (typeof hex !== "string") + throw new Error("hex string expected, got " + typeof hex); + const hl = hex.length; + const al = hl / 2; + if (hl % 2) + throw new Error("padded hex string expected, got unpadded hex of length " + hl); + const array = new Uint8Array(al); + for (let ai = 0, hi = 0; ai < al; ai++, hi += 2) { + const n1 = asciiToBase16(hex.charCodeAt(hi)); + const n2 = asciiToBase16(hex.charCodeAt(hi + 1)); + if (n1 === void 0 || n2 === void 0) { + const char = hex[hi] + hex[hi + 1]; + throw new Error('hex string expected, got non-hex character "' + char + '" at index ' + hi); + } + array[ai] = n1 * 16 + n2; + } + return array; +} +function bytesToNumberBE(bytes2) { + return hexToNumber(bytesToHex(bytes2)); +} +function bytesToNumberLE(bytes2) { + abytes(bytes2); + return hexToNumber(bytesToHex(Uint8Array.from(bytes2).reverse())); +} +function numberToBytesBE(n, len) { + return hexToBytes(n.toString(16).padStart(len * 2, "0")); +} +function numberToBytesLE(n, len) { + return numberToBytesBE(n, len).reverse(); +} +function ensureBytes(title, hex, expectedLength) { + let res; + if (typeof hex === "string") { + try { + res = hexToBytes(hex); + } catch (e) { + throw new Error(`${title} must be valid hex string, got "${hex}". Cause: ${e}`); + } + } else if (isBytes2(hex)) { + res = Uint8Array.from(hex); + } else { + throw new Error(`${title} must be hex string or Uint8Array`); + } + const len = res.length; + if (typeof expectedLength === "number" && len !== expectedLength) + throw new Error(`${title} expected ${expectedLength} bytes, got ${len}`); + return res; +} +var isPosBig = (n) => typeof n === "bigint" && _0n <= n; +function inRange(n, min, max) { + return isPosBig(n) && isPosBig(min) && isPosBig(max) && min <= n && n < max; +} +function aInRange(title, n, min, max) { + if (!inRange(n, min, max)) + throw new Error(`expected valid ${title}: ${min} <= n < ${max}, got ${typeof n} ${n}`); +} +var bitMask = (n) => (_2n << BigInt(n - 1)) - _1n; +var validatorFns = { + bigint: (val) => typeof val === "bigint", + function: (val) => typeof val === "function", + boolean: (val) => typeof val === "boolean", + string: (val) => typeof val === "string", + stringOrUint8Array: (val) => typeof val === "string" || isBytes2(val), + isSafeInteger: (val) => Number.isSafeInteger(val), + array: (val) => Array.isArray(val), + field: (val, object) => object.Fp.isValid(val), + hash: (val) => typeof val === "function" && Number.isSafeInteger(val.outputLen) +}; +function validateObject(object, validators, optValidators = {}) { + const checkField = (fieldName, type, isOptional) => { + const checkVal = validatorFns[type]; + if (typeof checkVal !== "function") + throw new Error(`Invalid validator "${type}", expected function`); + const val = object[fieldName]; + if (isOptional && val === void 0) + return; + if (!checkVal(val, object)) { + throw new Error(`Invalid param ${String(fieldName)}=${val} (${typeof val}), expected ${type}`); + } + }; + for (const [fieldName, type] of Object.entries(validators)) + checkField(fieldName, type, false); + for (const [fieldName, type] of Object.entries(optValidators)) + checkField(fieldName, type, true); + return object; +} + +// node_modules/@noble/curves/esm/abstract/modular.js +var _0n2 = BigInt(0); +var _1n2 = BigInt(1); +var _2n2 = BigInt(2); +var _3n = BigInt(3); +var _4n = BigInt(4); +var _5n = BigInt(5); +var _8n = BigInt(8); +var _9n = BigInt(9); +var _16n = BigInt(16); +function mod(a, b) { + const result = a % b; + return result >= _0n2 ? result : b + result; +} +function pow(num, power, modulo) { + if (modulo <= _0n2 || power < _0n2) + throw new Error("Expected power/modulo > 0"); + if (modulo === _1n2) + return _0n2; + let res = _1n2; + while (power > _0n2) { + if (power & _1n2) + res = res * num % modulo; + num = num * num % modulo; + power >>= _1n2; + } + return res; +} +function pow2(x, power, modulo) { + let res = x; + while (power-- > _0n2) { + res *= res; + res %= modulo; + } + return res; +} +function invert(number, modulo) { + if (number === _0n2 || modulo <= _0n2) { + throw new Error(`invert: expected positive integers, got n=${number} mod=${modulo}`); + } + let a = mod(number, modulo); + let b = modulo; + let x = _0n2, y = _1n2, u = _1n2, v = _0n2; + while (a !== _0n2) { + const q = b / a; + const r = b % a; + const m = x - u * q; + const n = y - v * q; + b = a, a = r, x = u, y = v, u = m, v = n; + } + const gcd = b; + if (gcd !== _1n2) + throw new Error("invert: does not exist"); + return mod(x, modulo); +} +function tonelliShanks(P) { + const legendreC = (P - _1n2) / _2n2; + let Q, S, Z; + for (Q = P - _1n2, S = 0; Q % _2n2 === _0n2; Q /= _2n2, S++) + ; + for (Z = _2n2; Z < P && pow(Z, legendreC, P) !== P - _1n2; Z++) + ; + if (S === 1) { + const p1div4 = (P + _1n2) / _4n; + return function tonelliFast(Fp2, n) { + const root = Fp2.pow(n, p1div4); + if (!Fp2.eql(Fp2.sqr(root), n)) + throw new Error("Cannot find square root"); + return root; + }; + } + const Q1div2 = (Q + _1n2) / _2n2; + return function tonelliSlow(Fp2, n) { + if (Fp2.pow(n, legendreC) === Fp2.neg(Fp2.ONE)) + throw new Error("Cannot find square root"); + let r = S; + let g = Fp2.pow(Fp2.mul(Fp2.ONE, Z), Q); + let x = Fp2.pow(n, Q1div2); + let b = Fp2.pow(n, Q); + while (!Fp2.eql(b, Fp2.ONE)) { + if (Fp2.eql(b, Fp2.ZERO)) + return Fp2.ZERO; + let m = 1; + for (let t2 = Fp2.sqr(b); m < r; m++) { + if (Fp2.eql(t2, Fp2.ONE)) + break; + t2 = Fp2.sqr(t2); + } + const ge = Fp2.pow(g, _1n2 << BigInt(r - m - 1)); + g = Fp2.sqr(ge); + x = Fp2.mul(x, ge); + b = Fp2.mul(b, g); + r = m; + } + return x; + }; +} +function FpSqrt(P) { + if (P % _4n === _3n) { + const p1div4 = (P + _1n2) / _4n; + return function sqrt3mod4(Fp2, n) { + const root = Fp2.pow(n, p1div4); + if (!Fp2.eql(Fp2.sqr(root), n)) + throw new Error("Cannot find square root"); + return root; + }; + } + if (P % _8n === _5n) { + const c1 = (P - _5n) / _8n; + return function sqrt5mod8(Fp2, n) { + const n2 = Fp2.mul(n, _2n2); + const v = Fp2.pow(n2, c1); + const nv = Fp2.mul(n, v); + const i = Fp2.mul(Fp2.mul(nv, _2n2), v); + const root = Fp2.mul(nv, Fp2.sub(i, Fp2.ONE)); + if (!Fp2.eql(Fp2.sqr(root), n)) + throw new Error("Cannot find square root"); + return root; + }; + } + if (P % _16n === _9n) { + } + return tonelliShanks(P); +} +var isNegativeLE = (num, modulo) => (mod(num, modulo) & _1n2) === _1n2; +function FpPow(f, num, power) { + if (power < _0n2) + throw new Error("Expected power > 0"); + if (power === _0n2) + return f.ONE; + if (power === _1n2) + return num; + let p = f.ONE; + let d = num; + while (power > _0n2) { + if (power & _1n2) + p = f.mul(p, d); + d = f.sqr(d); + power >>= _1n2; + } + return p; +} +function FpInvertBatch(f, nums) { + const tmp = new Array(nums.length); + const lastMultiplied = nums.reduce((acc, num, i) => { + if (f.is0(num)) + return acc; + tmp[i] = acc; + return f.mul(acc, num); + }, f.ONE); + const inverted = f.inv(lastMultiplied); + nums.reduceRight((acc, num, i) => { + if (f.is0(num)) + return acc; + tmp[i] = f.mul(acc, tmp[i]); + return f.mul(acc, num); + }, inverted); + return tmp; +} +function nLength(n, nBitLength) { + const _nBitLength = nBitLength !== void 0 ? nBitLength : n.toString(2).length; + const nByteLength = Math.ceil(_nBitLength / 8); + return { nBitLength: _nBitLength, nByteLength }; +} +function Field(ORDER, bitLen, isLE2 = false, redef = {}) { + if (ORDER <= _0n2) + throw new Error(`Expected Field ORDER > 0, got ${ORDER}`); + const { nBitLength: BITS, nByteLength: BYTES } = nLength(ORDER, bitLen); + if (BYTES > 2048) + throw new Error("Field lengths over 2048 bytes are not supported"); + const sqrtP = FpSqrt(ORDER); + const f = Object.freeze({ + ORDER, + BITS, + BYTES, + MASK: bitMask(BITS), + ZERO: _0n2, + ONE: _1n2, + create: (num) => mod(num, ORDER), + isValid: (num) => { + if (typeof num !== "bigint") + throw new Error(`Invalid field element: expected bigint, got ${typeof num}`); + return _0n2 <= num && num < ORDER; + }, + is0: (num) => num === _0n2, + isOdd: (num) => (num & _1n2) === _1n2, + neg: (num) => mod(-num, ORDER), + eql: (lhs, rhs) => lhs === rhs, + sqr: (num) => mod(num * num, ORDER), + add: (lhs, rhs) => mod(lhs + rhs, ORDER), + sub: (lhs, rhs) => mod(lhs - rhs, ORDER), + mul: (lhs, rhs) => mod(lhs * rhs, ORDER), + pow: (num, power) => FpPow(f, num, power), + div: (lhs, rhs) => mod(lhs * invert(rhs, ORDER), ORDER), + // Same as above, but doesn't normalize + sqrN: (num) => num * num, + addN: (lhs, rhs) => lhs + rhs, + subN: (lhs, rhs) => lhs - rhs, + mulN: (lhs, rhs) => lhs * rhs, + inv: (num) => invert(num, ORDER), + sqrt: redef.sqrt || ((n) => sqrtP(f, n)), + invertBatch: (lst) => FpInvertBatch(f, lst), + // TODO: do we really need constant cmov? + // We don't have const-time bigints anyway, so probably will be not very useful + cmov: (a, b, c) => c ? b : a, + toBytes: (num) => isLE2 ? numberToBytesLE(num, BYTES) : numberToBytesBE(num, BYTES), + fromBytes: (bytes2) => { + if (bytes2.length !== BYTES) + throw new Error(`Fp.fromBytes: expected ${BYTES}, got ${bytes2.length}`); + return isLE2 ? bytesToNumberLE(bytes2) : bytesToNumberBE(bytes2); + } + }); + return Object.freeze(f); +} + +// node_modules/@noble/curves/esm/abstract/montgomery.js +var _0n3 = BigInt(0); +var _1n3 = BigInt(1); +function validateOpts(curve) { + validateObject(curve, { + a: "bigint" + }, { + montgomeryBits: "isSafeInteger", + nByteLength: "isSafeInteger", + adjustScalarBytes: "function", + domain: "function", + powPminus2: "function", + Gu: "bigint" + }); + return Object.freeze({ ...curve }); +} +function montgomery(curveDef) { + const CURVE = validateOpts(curveDef); + const { P } = CURVE; + const modP = (n) => mod(n, P); + const montgomeryBits = CURVE.montgomeryBits; + const montgomeryBytes = Math.ceil(montgomeryBits / 8); + const fieldLen = CURVE.nByteLength; + const adjustScalarBytes2 = CURVE.adjustScalarBytes || ((bytes2) => bytes2); + const powPminus2 = CURVE.powPminus2 || ((x) => pow(x, P - BigInt(2), P)); + function cswap(swap, x_2, x_3) { + const dummy = modP(swap * (x_2 - x_3)); + x_2 = modP(x_2 - dummy); + x_3 = modP(x_3 + dummy); + return [x_2, x_3]; + } + const a24 = (CURVE.a - BigInt(2)) / BigInt(4); + function montgomeryLadder(u, scalar) { + aInRange("u", u, _0n3, P); + aInRange("scalar", scalar, _0n3, P); + const k = scalar; + const x_1 = u; + let x_2 = _1n3; + let z_2 = _0n3; + let x_3 = u; + let z_3 = _1n3; + let swap = _0n3; + let sw; + for (let t = BigInt(montgomeryBits - 1); t >= _0n3; t--) { + const k_t = k >> t & _1n3; + swap ^= k_t; + sw = cswap(swap, x_2, x_3); + x_2 = sw[0]; + x_3 = sw[1]; + sw = cswap(swap, z_2, z_3); + z_2 = sw[0]; + z_3 = sw[1]; + swap = k_t; + const A = x_2 + z_2; + const AA = modP(A * A); + const B = x_2 - z_2; + const BB = modP(B * B); + const E = AA - BB; + const C = x_3 + z_3; + const D = x_3 - z_3; + const DA = modP(D * A); + const CB = modP(C * B); + const dacb = DA + CB; + const da_cb = DA - CB; + x_3 = modP(dacb * dacb); + z_3 = modP(x_1 * modP(da_cb * da_cb)); + x_2 = modP(AA * BB); + z_2 = modP(E * (AA + modP(a24 * E))); + } + sw = cswap(swap, x_2, x_3); + x_2 = sw[0]; + x_3 = sw[1]; + sw = cswap(swap, z_2, z_3); + z_2 = sw[0]; + z_3 = sw[1]; + const z2 = powPminus2(z_2); + return modP(x_2 * z2); + } + function encodeUCoordinate(u) { + return numberToBytesLE(modP(u), montgomeryBytes); + } + function decodeUCoordinate(uEnc) { + const u = ensureBytes("u coordinate", uEnc, montgomeryBytes); + if (fieldLen === 32) + u[31] &= 127; + return bytesToNumberLE(u); + } + function decodeScalar(n) { + const bytes2 = ensureBytes("scalar", n); + const len = bytes2.length; + if (len !== montgomeryBytes && len !== fieldLen) + throw new Error(`Expected ${montgomeryBytes} or ${fieldLen} bytes, got ${len}`); + return bytesToNumberLE(adjustScalarBytes2(bytes2)); + } + function scalarMult(scalar, u) { + const pointU = decodeUCoordinate(u); + const _scalar = decodeScalar(scalar); + const pu = montgomeryLadder(pointU, _scalar); + if (pu === _0n3) + throw new Error("Invalid private or public key received"); + return encodeUCoordinate(pu); + } + const GuBytes = encodeUCoordinate(CURVE.Gu); + function scalarMultBase(scalar) { + return scalarMult(scalar, GuBytes); + } + return { + scalarMult, + scalarMultBase, + getSharedSecret: (privateKey, publicKey) => scalarMult(privateKey, publicKey), + getPublicKey: (privateKey) => scalarMultBase(privateKey), + utils: { randomPrivateKey: () => CURVE.randomBytes(CURVE.nByteLength) }, + GuBytes + }; +} + +// node_modules/@noble/curves/esm/ed25519.js +var ED25519_P = BigInt("57896044618658097711785492504343953926634992332820282019728792003956564819949"); +var ED25519_SQRT_M1 = /* @__PURE__ */ BigInt("19681161376707505956807079304988542015446066515923890162744021073123829784752"); +var _0n4 = BigInt(0); +var _1n4 = BigInt(1); +var _2n3 = BigInt(2); +var _3n2 = BigInt(3); +var _5n2 = BigInt(5); +var _8n2 = BigInt(8); +function ed25519_pow_2_252_3(x) { + const _10n = BigInt(10), _20n = BigInt(20), _40n = BigInt(40), _80n = BigInt(80); + const P = ED25519_P; + const x2 = x * x % P; + const b2 = x2 * x % P; + const b4 = pow2(b2, _2n3, P) * b2 % P; + const b5 = pow2(b4, _1n4, P) * x % P; + const b10 = pow2(b5, _5n2, P) * b5 % P; + const b20 = pow2(b10, _10n, P) * b10 % P; + const b40 = pow2(b20, _20n, P) * b20 % P; + const b80 = pow2(b40, _40n, P) * b40 % P; + const b160 = pow2(b80, _80n, P) * b80 % P; + const b240 = pow2(b160, _80n, P) * b80 % P; + const b250 = pow2(b240, _10n, P) * b10 % P; + const pow_p_5_8 = pow2(b250, _2n3, P) * x % P; + return { pow_p_5_8, b2 }; +} +function adjustScalarBytes(bytes2) { + bytes2[0] &= 248; + bytes2[31] &= 127; + bytes2[31] |= 64; + return bytes2; +} +function uvRatio(u, v) { + const P = ED25519_P; + const v3 = mod(v * v * v, P); + const v7 = mod(v3 * v3 * v, P); + const pow3 = ed25519_pow_2_252_3(u * v7).pow_p_5_8; + let x = mod(u * v3 * pow3, P); + const vx2 = mod(v * x * x, P); + const root1 = x; + const root2 = mod(x * ED25519_SQRT_M1, P); + const useRoot1 = vx2 === u; + const useRoot2 = vx2 === mod(-u, P); + const noRoot = vx2 === mod(-u * ED25519_SQRT_M1, P); + if (useRoot1) + x = root1; + if (useRoot2 || noRoot) + x = root2; + if (isNegativeLE(x, P)) + x = mod(-x, P); + return { isValid: useRoot1 || useRoot2, value: x }; +} +var Fp = /* @__PURE__ */ (() => Field(ED25519_P, void 0, true))(); +var ed25519Defaults = /* @__PURE__ */ (() => ({ + // Param: a + a: BigInt(-1), + // Fp.create(-1) is proper; our way still works and is faster + // d is equal to -121665/121666 over finite field. + // Negative number is P - number, and division is invert(number, P) + d: BigInt("37095705934669439343138083508754565189542113879843219016388785533085940283555"), + // Finite field 𝔽p over which we'll do calculations; 2n**255n - 19n + Fp, + // Subgroup order: how many points curve has + // 2n**252n + 27742317777372353535851937790883648493n; + n: BigInt("7237005577332262213973186563042994240857116359379907606001950938285454250989"), + // Cofactor + h: _8n2, + // Base point (x, y) aka generator point + Gx: BigInt("15112221349535400772501151409588531511454012693041857206046113283949847762202"), + Gy: BigInt("46316835694926478169428394003475163141307993866256225615783033603165251855960"), + hash: sha512, + randomBytes, + adjustScalarBytes, + // dom2 + // Ratio of u to v. Allows us to combine inversion and square root. Uses algo from RFC8032 5.1.3. + // Constant-time, u/√v + uvRatio +}))(); +var x25519 = /* @__PURE__ */ (() => montgomery({ + P: ED25519_P, + a: BigInt(486662), + montgomeryBits: 255, + // n is 253 bits + nByteLength: 32, + Gu: BigInt(9), + powPminus2: (x) => { + const P = ED25519_P; + const { pow_p_5_8, b2 } = ed25519_pow_2_252_3(x); + return mod(pow2(pow_p_5_8, _3n2, P) * b2, P); + }, + adjustScalarBytes, + randomBytes +}))(); +function edwardsToMontgomeryPriv(edwardsPriv) { + const hashed = ed25519Defaults.hash(edwardsPriv.subarray(0, 32)); + return ed25519Defaults.adjustScalarBytes(hashed).subarray(0, 32); +} +export { + edwardsToMontgomeryPriv, + x25519 +}; +/*! Bundled license information: + +@noble/hashes/esm/utils.js: + (*! noble-hashes - MIT License (c) 2022 Paul Miller (paulmillr.com) *) + +@noble/curves/esm/abstract/utils.js: +@noble/curves/esm/abstract/modular.js: +@noble/curves/esm/abstract/montgomery.js: +@noble/curves/esm/ed25519.js: + (*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) *) +*/ diff --git a/SHiNE-browser-plugin-wallet/js/lib/vendor/noble-ed25519-entry.js b/SHiNE-browser-plugin-wallet/js/lib/vendor/noble-ed25519-entry.js new file mode 100644 index 0000000..e953ca8 --- /dev/null +++ b/SHiNE-browser-plugin-wallet/js/lib/vendor/noble-ed25519-entry.js @@ -0,0 +1,3 @@ +import { edwardsToMontgomeryPriv, x25519 } from '../../../node_modules/@noble/curves/esm/ed25519.js'; + +export { edwardsToMontgomeryPriv, x25519 }; diff --git a/SHiNE-browser-plugin-wallet/js/lib/ws-client.js b/SHiNE-browser-plugin-wallet/js/lib/ws-client.js new file mode 100644 index 0000000..e5f7de6 --- /dev/null +++ b/SHiNE-browser-plugin-wallet/js/lib/ws-client.js @@ -0,0 +1,101 @@ +const DEFAULT_TIMEOUT_MS = 12000; +const runtimeTimers = globalThis; + +function buildWsUrl(raw) { + const value = String(raw || '').trim(); + if (!value) return 'wss://shineup.me/ws'; + if (value.startsWith('ws://') || value.startsWith('wss://')) return value; + if (value.startsWith('http://') || value.startsWith('https://')) { + const parsed = new URL(value); + parsed.protocol = parsed.protocol === 'https:' ? 'wss:' : 'ws:'; + if (!parsed.pathname || parsed.pathname === '/') parsed.pathname = '/ws'; + return parsed.toString(); + } + return value; +} + +function createRequestId(op) { + return `${op}-${Date.now()}-${Math.random().toString(16).slice(2)}`; +} + +export class WsJsonClient { + constructor(url) { + this.url = buildWsUrl(url); + this.ws = null; + this.openPromise = null; + this.pending = new Map(); + } + + async open() { + if (this.ws && this.ws.readyState === WebSocket.OPEN) return; + if (this.openPromise) return this.openPromise; + + this.openPromise = new Promise((resolve, reject) => { + const ws = new WebSocket(this.url); + this.ws = ws; + + ws.addEventListener('open', () => resolve(), { once: true }); + ws.addEventListener('error', () => reject(new Error(`Не удалось подключиться к ${this.url}`)), { once: true }); + ws.addEventListener('close', () => this.failPending('WebSocket соединение закрыто')); + ws.addEventListener('message', (event) => this.handleMessage(event.data)); + }).finally(() => { + this.openPromise = null; + }); + + return this.openPromise; + } + + async request(op, payload = {}, timeoutMs = DEFAULT_TIMEOUT_MS) { + await this.open(); + const requestId = createRequestId(op); + const body = { op, requestId, payload }; + + const response = new Promise((resolve, reject) => { + const timer = runtimeTimers.setTimeout(() => { + this.pending.delete(requestId); + reject(new Error(`Таймаут ответа для операции ${op}`)); + }, timeoutMs); + this.pending.set(requestId, { + resolve: (value) => { + runtimeTimers.clearTimeout(timer); + resolve(value); + }, + reject: (error) => { + runtimeTimers.clearTimeout(timer); + reject(error); + }, + }); + }); + + this.ws.send(JSON.stringify(body)); + return response; + } + + handleMessage(raw) { + let data; + try { + data = JSON.parse(raw); + } catch { + return; + } + const requestId = data?.requestId; + if (!requestId) return; + const slot = this.pending.get(requestId); + if (!slot) return; + this.pending.delete(requestId); + slot.resolve(data); + } + + failPending(message) { + const error = new Error(message); + for (const slot of this.pending.values()) slot.reject(error); + this.pending.clear(); + } + + close() { + if (this.ws) { + this.ws.close(); + this.ws = null; + } + } +} diff --git a/SHiNE-browser-plugin-wallet/manifest.json b/SHiNE-browser-plugin-wallet/manifest.json new file mode 100644 index 0000000..fb5dd61 --- /dev/null +++ b/SHiNE-browser-plugin-wallet/manifest.json @@ -0,0 +1,20 @@ +{ + "manifest_version": 3, + "name": "SHiNE Browser Plugin Wallet", + "version": "0.1.0", + "description": "Wallet-session plugin for SHiNE with session-only login via trusted device.", + "permissions": [ + "storage" + ], + "host_permissions": [ + "" + ], + "background": { + "service_worker": "background.js", + "type": "module" + }, + "action": { + "default_title": "SHiNE Wallet", + "default_popup": "popup.html" + } +} diff --git a/SHiNE-browser-plugin-wallet/package-lock.json b/SHiNE-browser-plugin-wallet/package-lock.json new file mode 100644 index 0000000..4827a2a --- /dev/null +++ b/SHiNE-browser-plugin-wallet/package-lock.json @@ -0,0 +1,527 @@ +{ + "name": "shine-browser-plugin-wallet", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "shine-browser-plugin-wallet", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@noble/curves": "^1.5.0" + }, + "devDependencies": { + "esbuild": "^0.28.1" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@noble/curves": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.5.0.tgz", + "integrity": "sha512-J5EKamIHnKPyClwVrzmaf5wSdQXgdHcPZIZLu3bwnbeCx8/7NPK5q2ZBWF+5FvYGByjiQQsJYX6jfgB2wDPn3A==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.4.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/curves/node_modules/@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + } + } +} diff --git a/SHiNE-browser-plugin-wallet/package.json b/SHiNE-browser-plugin-wallet/package.json new file mode 100644 index 0000000..a0fad5b --- /dev/null +++ b/SHiNE-browser-plugin-wallet/package.json @@ -0,0 +1,18 @@ +{ + "name": "shine-browser-plugin-wallet", + "version": "1.0.0", + "description": "Chrome-compatible Manifest V3 plugin for SHiNE wallet-session login.", + "main": "popup.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "@noble/curves": "^1.5.0" + }, + "devDependencies": { + "esbuild": "^0.28.1" + } +} diff --git a/SHiNE-browser-plugin-wallet/popup.css b/SHiNE-browser-plugin-wallet/popup.css new file mode 100644 index 0000000..37ebfc3 --- /dev/null +++ b/SHiNE-browser-plugin-wallet/popup.css @@ -0,0 +1,180 @@ +* { + box-sizing: border-box; +} + +body { + margin: 0; + min-width: 360px; + background: #0f1720; + color: #e8eef6; + font: 14px/1.4 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; +} + +.layout { + padding: 12px; +} + +.panel { + display: flex; + flex-direction: column; + gap: 12px; +} + +.panel-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 12px; +} + +.panel-header h1 { + margin: 0; + font-size: 18px; +} + +.muted { + margin: 2px 0 0; + color: #9aabbd; +} + +.small { + font-size: 12px; +} + +.pill { + display: inline-flex; + align-items: center; + min-height: 28px; + padding: 0 10px; + border-radius: 999px; + font-size: 12px; + font-weight: 600; +} + +.pill-offline { + background: #4b1f28; + color: #ffc4cf; +} + +.pill-online { + background: #153926; + color: #b7f5ce; +} + +.card { + display: flex; + flex-direction: column; + gap: 10px; + padding: 12px; + border: 1px solid #253446; + border-radius: 8px; + background: #131d29; +} + +.card-title { + font-size: 13px; + font-weight: 700; +} + +.field { + display: flex; + flex-direction: column; + gap: 6px; +} + +.field span { + font-size: 12px; + color: #b8c4d1; +} + +input[type="text"], +input[type="password"] { + width: 100%; + padding: 10px 12px; + border: 1px solid #314459; + border-radius: 8px; + background: #0d141d; + color: #edf3fb; +} + +.checkbox-row { + display: flex; + align-items: center; + gap: 8px; +} + +.actions { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.btn { + min-height: 36px; + padding: 0 12px; + border: 0; + border-radius: 8px; + font-weight: 600; + cursor: pointer; +} + +.btn.primary { + background: #2f7df4; + color: #fff; +} + +.btn.secondary { + background: #243446; + color: #e8eef6; +} + +.btn.danger { + background: #6a2430; + color: #ffd6de; +} + +.btn:disabled { + opacity: 0.55; + cursor: default; +} + +.code { + font-size: 34px; + font-weight: 700; + letter-spacing: 0.18em; +} + +.summary-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.summary-row code { + max-width: 180px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + color: #bed5f5; +} + +.status { + padding: 10px 12px; + border-radius: 8px; + font-size: 13px; +} + +.status.info { + background: #172838; + color: #d8ebff; +} + +.status.error { + background: #4d1e26; + color: #ffd0d8; +} + +.hidden { + display: none; +} diff --git a/SHiNE-browser-plugin-wallet/popup.html b/SHiNE-browser-plugin-wallet/popup.html new file mode 100644 index 0000000..a2ea80e --- /dev/null +++ b/SHiNE-browser-plugin-wallet/popup.html @@ -0,0 +1,75 @@ + + + + + + SHiNE Wallet + + + +
+
+
+
+

SHiNE Wallet

+

Session-only wallet plugin

+
+ offline +
+ + + + + +
+
Войти через другое устройство
+ + + + +

+ Wallet plugin создаёт временный requester keypair, ждёт подтверждение на доверенном устройстве + и получает только wallet-session без передачи постоянных ключей. +

+
+ + + + +
+
+ + + + diff --git a/SHiNE-browser-plugin-wallet/popup.js b/SHiNE-browser-plugin-wallet/popup.js new file mode 100644 index 0000000..e171e3e --- /dev/null +++ b/SHiNE-browser-plugin-wallet/popup.js @@ -0,0 +1,244 @@ +const els = { + serverUrl: document.querySelector('#server-url'), + loginInput: document.querySelector('#login-input'), + usePassword: document.querySelector('#use-password'), + passwordField: document.querySelector('#password-field'), + passwordInput: document.querySelector('#password-input'), + startBtn: document.querySelector('#start-btn'), + pairingCard: document.querySelector('#pairing-card'), + shortCode: document.querySelector('#short-code'), + pairingHint: document.querySelector('#pairing-hint'), + pairingExpire: document.querySelector('#pairing-expire'), + cancelBtn: document.querySelector('#cancel-btn'), + status: document.querySelector('#status'), + sessionCard: document.querySelector('#session-card'), + sessionLogin: document.querySelector('#session-login'), + sessionId: document.querySelector('#session-id'), + sessionType: document.querySelector('#session-type'), + resumeBtn: document.querySelector('#resume-btn'), + disconnectBtn: document.querySelector('#disconnect-btn'), + connectionPill: document.querySelector('#connection-pill'), +}; + +let state = { + settings: { + serverUrl: 'wss://shineup.me/ws', + login: '', + }, + pairing: { + active: false, + pairingId: '', + expiresAtMs: 0, + }, + session: null, + connectionOnline: false, + status: { + text: '', + kind: 'info', + }, +}; + +let refreshTimer = 0; +let saveSettingsTimer = 0; + +function setStatus(message, kind = 'info') { + els.status.textContent = String(message || ''); + els.status.className = `status ${kind === 'error' ? 'error' : 'info'}`; + els.status.classList.toggle('hidden', !message); +} + +function setConnectedPill(connected) { + els.connectionPill.textContent = connected ? 'online' : 'offline'; + els.connectionPill.className = connected ? 'pill pill-online' : 'pill pill-offline'; +} + +function formatRemaining(ms) { + const safe = Math.max(0, Math.floor(Number(ms || 0) / 1000)); + const minutes = Math.floor(safe / 60); + const seconds = safe % 60; + return `${minutes} мин ${seconds} сек`; +} + +function applyState(nextState) { + state = nextState || state; + const serverValue = String(state?.settings?.serverUrl || 'wss://shineup.me/ws'); + const loginValue = String(state?.settings?.login || ''); + if (document.activeElement !== els.serverUrl) { + els.serverUrl.value = serverValue; + } + if (document.activeElement !== els.loginInput) { + els.loginInput.value = loginValue; + } + setConnectedPill(!!state?.connectionOnline); + setStatus(state?.status?.text || '', state?.status?.kind || 'info'); + + const session = state?.session; + if (session) { + els.sessionCard.classList.remove('hidden'); + els.sessionLogin.textContent = session.login || '—'; + els.sessionId.textContent = session.sessionId || '—'; + els.sessionType.textContent = String(session.sessionType || 50) === '50' ? 'wallet' : String(session.sessionType || '—'); + } else { + els.sessionCard.classList.add('hidden'); + els.sessionLogin.textContent = '—'; + els.sessionId.textContent = '—'; + els.sessionType.textContent = 'wallet'; + } + + const pairing = state?.pairing || {}; + if (pairing.active) { + els.pairingCard.classList.remove('hidden'); + const shortCode = String(pairing.shortCode || els.shortCode.dataset.shortCode || els.shortCode.textContent || '0000000'); + els.shortCode.dataset.shortCode = shortCode; + els.shortCode.textContent = shortCode; + els.pairingHint.textContent = pairing.trustedSessionOnline + ? 'Покажите код на доверенном устройстве и подтвердите выпуск wallet-session.' + : 'Сейчас нет онлайн доверенной сессии. Откройте другое устройство и подтвердите заявку.'; + const leftMs = Number(pairing.expiresAtMs || 0) - Date.now(); + els.pairingExpire.textContent = leftMs > 0 ? `Код действителен ещё ${formatRemaining(leftMs)}.` : 'Время ожидания истекло.'; + els.startBtn.disabled = true; + } else { + els.pairingCard.classList.add('hidden'); + els.shortCode.textContent = '0000000'; + delete els.shortCode.dataset.shortCode; + els.pairingExpire.textContent = ''; + els.startBtn.disabled = false; + } +} + +function normalizeError(response, fallback) { + return response?.error || fallback || 'Unknown error'; +} + +function sendMessage(type, payload = {}) { + return new Promise((resolve, reject) => { + chrome.runtime.sendMessage({ type, payload }, (response) => { + if (chrome.runtime.lastError) { + reject(new Error(chrome.runtime.lastError.message || 'Runtime message failed')); + return; + } + if (!response?.ok) { + reject(new Error(normalizeError(response, 'Wallet operation failed'))); + return; + } + if (response?.state) applyState(response.state); + resolve(response); + }); + }); +} + +async function refreshState() { + const response = await sendMessage('wallet:getState'); + applyState(response.state); +} + +async function saveSettings() { + await sendMessage('wallet:saveSettings', { + serverUrl: String(els.serverUrl.value || '').trim(), + login: String(els.loginInput.value || '').trim(), + }); +} + +function scheduleSaveSettings() { + if (saveSettingsTimer) { + window.clearTimeout(saveSettingsTimer); + } + saveSettingsTimer = window.setTimeout(() => { + saveSettingsTimer = 0; + void saveSettings(); + }, 250); +} + +async function startPairing() { + const login = String(els.loginInput.value || '').trim(); + if (!login) { + setStatus('Введите логин.', 'error'); + return; + } + setStatus('Создаём wallet-session заявку...', 'info'); + els.startBtn.disabled = true; + try { + const response = await sendMessage('wallet:startPairing', { + login, + usePassword: !!els.usePassword.checked, + password: String(els.passwordInput.value || ''), + serverUrl: String(els.serverUrl.value || '').trim(), + }); + applyState(response.state); + } catch (error) { + els.startBtn.disabled = false; + setStatus(error.message || 'Не удалось начать pairing.', 'error'); + } +} + +async function cancelPairing() { + try { + await sendMessage('wallet:cancelPairing'); + } catch (error) { + setStatus(error.message || 'Не удалось отменить pairing.', 'error'); + } +} + +async function resumeSession() { + setStatus('Проверяем сохранённую wallet-session...', 'info'); + try { + await sendMessage('wallet:resumeSession'); + } catch (error) { + setStatus(error.message || 'Не удалось восстановить session.', 'error'); + } +} + +async function disconnectSession() { + try { + await sendMessage('wallet:disconnectSession'); + } catch (error) { + setStatus(error.message || 'Не удалось удалить session.', 'error'); + } +} + +function startUiRefreshLoop() { + stopUiRefreshLoop(); + refreshTimer = window.setInterval(() => { + void refreshState(); + }, 1000); +} + +function stopUiRefreshLoop() { + if (refreshTimer) { + window.clearInterval(refreshTimer); + refreshTimer = 0; + } +} + +function bindUi() { + els.usePassword.addEventListener('change', () => { + els.passwordField.classList.toggle('hidden', !els.usePassword.checked); + if (!els.usePassword.checked) { + els.passwordInput.value = ''; + } + }); + els.serverUrl.addEventListener('input', () => { scheduleSaveSettings(); }); + els.serverUrl.addEventListener('change', () => { void saveSettings(); }); + els.loginInput.addEventListener('input', () => { scheduleSaveSettings(); }); + els.loginInput.addEventListener('change', () => { void saveSettings(); }); + els.startBtn.addEventListener('click', () => { void startPairing(); }); + els.cancelBtn.addEventListener('click', () => { void cancelPairing(); }); + els.resumeBtn.addEventListener('click', () => { void resumeSession(); }); + els.disconnectBtn.addEventListener('click', () => { void disconnectSession(); }); +} + +async function init() { + bindUi(); + await refreshState(); + startUiRefreshLoop(); +} + +window.addEventListener('beforeunload', () => { + stopUiRefreshLoop(); + if (saveSettingsTimer) { + window.clearTimeout(saveSettingsTimer); + saveSettingsTimer = 0; + } +}); + +void init(); diff --git a/SHiNE-browser-plugin-wallet/settings.gradle b/SHiNE-browser-plugin-wallet/settings.gradle new file mode 100644 index 0000000..c9ec0cd --- /dev/null +++ b/SHiNE-browser-plugin-wallet/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'SHiNE-browser-plugin-wallet' diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/EspPairingSupport.java b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/EspPairingSupport.java index 6d9efb6..56b484d 100644 --- a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/EspPairingSupport.java +++ b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/EspPairingSupport.java @@ -4,6 +4,7 @@ import org.eclipse.jetty.websocket.api.Session; import server.logic.ws_protocol.JSON.ActiveConnectionsRegistry; import server.logic.ws_protocol.JSON.ConnectionContext; import shine.db.entities.ActiveSessionEntry; +import utils.crypto.HashSHA256Util; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; @@ -29,6 +30,8 @@ final class EspPairingSupport { static final String STATE_REJECTED = "rejected"; static final String STATE_CANCELED = "canceled"; static final String STATE_EXPIRED = "expired"; + static final String PASSWORD_HASH_PREFIX = "sha256$"; + static final String PASSWORD_HASH_VERSION = "shine-pairing"; private static final SecureRandom RANDOM = new SecureRandom(); private static final char[] BASE58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz".toCharArray(); @@ -77,6 +80,30 @@ final class EspPairingSupport { return value; } + static String normalizePasswordHash(String raw) { + String value = normalizeOpaqueHash(raw); + if (value == null) return null; + if (!value.regionMatches(true, 0, PASSWORD_HASH_PREFIX, 0, PASSWORD_HASH_PREFIX.length())) { + return null; + } + String hex = value.substring(PASSWORD_HASH_PREFIX.length()).trim().toLowerCase(Locale.ROOT); + if (hex.length() != 64) return null; + for (int i = 0; i < hex.length(); i++) { + char ch = hex.charAt(i); + boolean ok = (ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'f'); + if (!ok) return null; + } + return PASSWORD_HASH_PREFIX + hex; + } + + static String derivePasswordHash(String loginRaw, String passwordRaw) { + String login = loginRaw == null ? "" : loginRaw.trim().toLowerCase(Locale.ROOT); + String password = passwordRaw == null ? "" : passwordRaw; + String preimage = PASSWORD_HASH_VERSION + "|" + login + "|" + password; + byte[] digest = HashSHA256Util.sha256(preimage.getBytes(StandardCharsets.UTF_8)); + return PASSWORD_HASH_PREFIX + toHexLower(digest); + } + static String normalizeEncryptedPayload(String raw) { if (raw == null) return null; String value = raw.trim(); @@ -149,5 +176,14 @@ final class EspPairingSupport { return remainder; } + private static String toHexLower(byte[] bytes) { + StringBuilder sb = new StringBuilder(bytes.length * 2); + for (byte b : bytes) { + sb.append(Character.forDigit((b >>> 4) & 0x0F, 16)); + sb.append(Character.forDigit(b & 0x0F, 16)); + } + return sb.toString(); + } + record PairingFingerprint(String shortCode, String fingerprintB58) {} } diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_StartEspPairing_Handler.java b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_StartEspPairing_Handler.java index b9d1e7a..771279a 100644 --- a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_StartEspPairing_Handler.java +++ b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_StartEspPairing_Handler.java @@ -54,7 +54,11 @@ public class Net_StartEspPairing_Handler implements JsonMessageHandler { if (!EspPairingSupport.isSupportedPayloadType(payloadType)) { return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_PAYLOAD_TYPE", "payloadType должен быть 1, 2 или 3"); } - String passwordHash = EspPairingSupport.normalizeOpaqueHash(req.getPasswordHash()); + String rawPasswordHash = EspPairingSupport.normalizeOpaqueHash(req.getPasswordHash()); + String passwordHash = EspPairingSupport.normalizePasswordHash(rawPasswordHash); + if (rawPasswordHash != null && passwordHash == null) { + return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_PASSWORD_HASH_FORMAT", "passwordHash должен быть пустым или иметь формат sha256$<64 hex>"); + } SolanaUserEntry user = SolanaUsersDAO.getInstance().getByLogin(login); if (user == null) { diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_UpsertEspPairingSettings_Handler.java b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_UpsertEspPairingSettings_Handler.java index 0d40845..091082b 100644 --- a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_UpsertEspPairingSettings_Handler.java +++ b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_UpsertEspPairingSettings_Handler.java @@ -27,7 +27,16 @@ public class Net_UpsertEspPairingSettings_Handler implements JsonMessageHandler } boolean enabled = req.getEnabled() != null && req.getEnabled(); - String passwordHash = EspPairingSupport.normalizeOpaqueHash(req.getPasswordHash()); + String rawPasswordHash = EspPairingSupport.normalizeOpaqueHash(req.getPasswordHash()); + String passwordHash = EspPairingSupport.normalizePasswordHash(rawPasswordHash); + if (rawPasswordHash != null && passwordHash == null) { + return NetExceptionResponseFactory.error( + req, + WireCodes.Status.BAD_REQUEST, + "BAD_PASSWORD_HASH_FORMAT", + "passwordHash должен быть пустым или иметь формат sha256$<64 hex>" + ); + } int ttlSeconds = EspPairingSupport.normalizeTtlSeconds(req.getTtlSeconds()); long now = System.currentTimeMillis(); diff --git a/SHiNE-server/src/test/java/test/it/cases/IT_07_EspPairing.java b/SHiNE-server/src/test/java/test/it/cases/IT_07_EspPairing.java index 7c9fa45..8c77114 100644 --- a/SHiNE-server/src/test/java/test/it/cases/IT_07_EspPairing.java +++ b/SHiNE-server/src/test/java/test/it/cases/IT_07_EspPairing.java @@ -9,9 +9,11 @@ import test.it.utils.ws.WsSession; import utils.crypto.Ed25519Util; import shine.db.dao.SolanaUsersDAO; import shine.db.entities.SolanaUserEntry; +import utils.crypto.HashSHA256Util; import java.time.Duration; import java.util.Base64; +import java.nio.charset.StandardCharsets; import static org.junit.jupiter.api.Assertions.*; @@ -38,7 +40,7 @@ public class IT_07_EspPairing { sessionLogin2Steps(clientWs, clientSession, 1, "Web", t, r); - String passwordHash = "argon2id$v=19$m=65536,t=2,p=1$test$esp_pairing_hash"; + String passwordHash = derivePairingHash(LOGIN, "test-pairing-password"); String upsertResp = clientWs.call( "UpsertEspPairingSettings", JsonBuilders.upsertEspPairingSettings(true, passwordHash, 180), @@ -218,6 +220,17 @@ public class IT_07_EspPairing { SolanaUsersDAO.getInstance().insert(entry); } + private static String derivePairingHash(String login, String password) { + String preimage = "shine-pairing|" + login.trim().toLowerCase() + "|" + password; + byte[] digest = HashSHA256Util.sha256(preimage.getBytes(StandardCharsets.UTF_8)); + StringBuilder sb = new StringBuilder(64); + for (byte b : digest) { + sb.append(Character.forDigit((b >>> 4) & 0x0F, 16)); + sb.append(Character.forDigit(b & 0x0F, 16)); + } + return "sha256$" + sb; + } + private record Session(String sessionId, String sessionKey, byte[] sessionPrivKey, String storagePwd) {} private record SessionMaterial(String sessionKey, byte[] sessionPrivKey) {} } diff --git a/VERSION.properties b/VERSION.properties index 0d75cbd..237e7c3 100644 --- a/VERSION.properties +++ b/VERSION.properties @@ -1,2 +1,2 @@ -client.version=1.2.204 -server.version=1.2.193 +client.version=1.2.205 +server.version=1.2.194 diff --git a/shine-UI/index.html b/shine-UI/index.html index 6be3986..0e8fba5 100644 --- a/shine-UI/index.html +++ b/shine-UI/index.html @@ -7,7 +7,7 @@ Shine UI Demo