Wallet-session pairing и browser plugin wallet, оплаты пока не работают
This commit is contained in:
parent
5c155ef503
commit
3efa8bb7ee
@ -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$<hex( SHA-256("shine-pairing|" + lower(login.trim()) + "|" + password) )>
|
||||
```
|
||||
|
||||
### Успешный ответ
|
||||
|
||||
```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",
|
||||
|
||||
@ -0,0 +1,22 @@
|
||||
# wallet-session pairing и SHA-256 пароль pairing
|
||||
|
||||
- краткое описание:
|
||||
- добавлен сценарий `session-only` подключения wallet-plugin через доверенное устройство без передачи постоянных ключей;
|
||||
- для pairing-пароля убран `argon2id`, вместо него используется только формат `sha256$<hex>`;
|
||||
- новый 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$<hex>`;
|
||||
- pairing без доп. пароля продолжает работать.
|
||||
|
||||
- ожидаемый результат:
|
||||
- requester получает только `sessionId/sessionKey/sessionPriv/storagePwd`;
|
||||
- доверенное устройство не пересылает постоянные ключи в `session-only` режиме;
|
||||
- сервер принимает только новый формат pairing-пароля;
|
||||
- логин по сохранённой wallet-session восстанавливается успешно.
|
||||
|
||||
- статус:
|
||||
- pending
|
||||
@ -58,7 +58,7 @@
|
||||
|
||||
## 3. Что именно делает сервер
|
||||
|
||||
- хранит включённость pairing и optional opaque `passwordHash`;
|
||||
- хранит включённость pairing и optional `passwordHash` в формате `sha256$<hex>`;
|
||||
- хранит pairing-заявки всех статусов, но в список активных для доверённого устройства отдаёт только pending `created`;
|
||||
- рассчитывает короткий код `shortCode` из `7` цифр;
|
||||
- рассчитывает длинный `fingerprintB58` из `SHA-256` заявки;
|
||||
@ -104,3 +104,9 @@
|
||||
- пароль на сервере, если он включён, только отсеивает лишних;
|
||||
- онлайн доверенная сессия решает, добавлять ли новую сессию;
|
||||
- сервер остаётся маршрутизатором и хранилищем состояния, а не владельцем секретов.
|
||||
|
||||
Текущий формат pairing-пароля:
|
||||
|
||||
```text
|
||||
sha256$<hex( SHA-256("shine-pairing|" + lower(login.trim()) + "|" + password) )>
|
||||
```
|
||||
|
||||
@ -1 +0,0 @@
|
||||
rootProject.name = 'ESP-wallet'
|
||||
@ -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
|
||||
.DS_Store
|
||||
10
SHiNE-browser-plugin-wallet/.idea/.gitignore
generated
vendored
Normal file
10
SHiNE-browser-plugin-wallet/.idea/.gitignore
generated
vendored
Normal file
@ -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
|
||||
1
SHiNE-browser-plugin-wallet/.idea/.name
generated
Normal file
1
SHiNE-browser-plugin-wallet/.idea/.name
generated
Normal file
@ -0,0 +1 @@
|
||||
ESP-wallet
|
||||
17
SHiNE-browser-plugin-wallet/.idea/gradle.xml
generated
Normal file
17
SHiNE-browser-plugin-wallet/.idea/gradle.xml
generated
Normal file
@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GradleMigrationSettings" migrationVersion="1" />
|
||||
<component name="GradleSettings">
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="modules">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
</set>
|
||||
</option>
|
||||
<option name="myGradleHome" value="" />
|
||||
</GradleProjectSettings>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
7
SHiNE-browser-plugin-wallet/.idea/misc.xml
generated
Normal file
7
SHiNE-browser-plugin-wallet/.idea/misc.xml
generated
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="21" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/out" />
|
||||
</component>
|
||||
</project>
|
||||
6
SHiNE-browser-plugin-wallet/.idea/vcs.xml
generated
Normal file
6
SHiNE-browser-plugin-wallet/.idea/vcs.xml
generated
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
38
SHiNE-browser-plugin-wallet/README.md
Normal file
38
SHiNE-browser-plugin-wallet/README.md
Normal file
@ -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$<hex>` от строки `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
|
||||
```
|
||||
294
SHiNE-browser-plugin-wallet/background.js
Normal file
294
SHiNE-browser-plugin-wallet/background.js
Normal file
@ -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');
|
||||
});
|
||||
78
SHiNE-browser-plugin-wallet/js/lib/crypto-utils.js
Normal file
78
SHiNE-browser-plugin-wallet/js/lib/crypto-utils.js
Normal file
@ -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('');
|
||||
}
|
||||
90
SHiNE-browser-plugin-wallet/js/lib/device-pairing.js
Normal file
90
SHiNE-browser-plugin-wallet/js/lib/device-pairing.js
Normal file
@ -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));
|
||||
}
|
||||
152
SHiNE-browser-plugin-wallet/js/lib/session-store.js
Normal file
152
SHiNE-browser-plugin-wallet/js/lib/session-store.js
Normal file
@ -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);
|
||||
}
|
||||
111
SHiNE-browser-plugin-wallet/js/lib/shine-api.js
Normal file
111
SHiNE-browser-plugin-wallet/js/lib/shine-api.js
Normal file
@ -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();
|
||||
}
|
||||
}
|
||||
995
SHiNE-browser-plugin-wallet/js/lib/vendor/noble-ed25519-bundle.js
vendored
Normal file
995
SHiNE-browser-plugin-wallet/js/lib/vendor/noble-ed25519-bundle.js
vendored
Normal file
@ -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) *)
|
||||
*/
|
||||
3
SHiNE-browser-plugin-wallet/js/lib/vendor/noble-ed25519-entry.js
vendored
Normal file
3
SHiNE-browser-plugin-wallet/js/lib/vendor/noble-ed25519-entry.js
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
import { edwardsToMontgomeryPriv, x25519 } from '../../../node_modules/@noble/curves/esm/ed25519.js';
|
||||
|
||||
export { edwardsToMontgomeryPriv, x25519 };
|
||||
101
SHiNE-browser-plugin-wallet/js/lib/ws-client.js
Normal file
101
SHiNE-browser-plugin-wallet/js/lib/ws-client.js
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
20
SHiNE-browser-plugin-wallet/manifest.json
Normal file
20
SHiNE-browser-plugin-wallet/manifest.json
Normal file
@ -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": [
|
||||
"<all_urls>"
|
||||
],
|
||||
"background": {
|
||||
"service_worker": "background.js",
|
||||
"type": "module"
|
||||
},
|
||||
"action": {
|
||||
"default_title": "SHiNE Wallet",
|
||||
"default_popup": "popup.html"
|
||||
}
|
||||
}
|
||||
527
SHiNE-browser-plugin-wallet/package-lock.json
generated
Normal file
527
SHiNE-browser-plugin-wallet/package-lock.json
generated
Normal file
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
18
SHiNE-browser-plugin-wallet/package.json
Normal file
18
SHiNE-browser-plugin-wallet/package.json
Normal file
@ -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"
|
||||
}
|
||||
}
|
||||
180
SHiNE-browser-plugin-wallet/popup.css
Normal file
180
SHiNE-browser-plugin-wallet/popup.css
Normal file
@ -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;
|
||||
}
|
||||
75
SHiNE-browser-plugin-wallet/popup.html
Normal file
75
SHiNE-browser-plugin-wallet/popup.html
Normal file
@ -0,0 +1,75 @@
|
||||
<!doctype html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>SHiNE Wallet</title>
|
||||
<link rel="stylesheet" href="./popup.css" />
|
||||
</head>
|
||||
<body>
|
||||
<main class="layout">
|
||||
<section class="panel">
|
||||
<div class="panel-header">
|
||||
<div>
|
||||
<h1>SHiNE Wallet</h1>
|
||||
<p class="muted">Session-only wallet plugin</p>
|
||||
</div>
|
||||
<span id="connection-pill" class="pill pill-offline">offline</span>
|
||||
</div>
|
||||
|
||||
<label class="field">
|
||||
<span>Shine server</span>
|
||||
<input id="server-url" type="text" placeholder="wss://shineup.me/ws" />
|
||||
</label>
|
||||
|
||||
<div id="session-card" class="card hidden">
|
||||
<div class="card-title">Подключённая wallet-session</div>
|
||||
<div class="summary-row"><span>Логин</span><strong id="session-login">—</strong></div>
|
||||
<div class="summary-row"><span>Session ID</span><code id="session-id">—</code></div>
|
||||
<div class="summary-row"><span>Тип</span><strong id="session-type">wallet</strong></div>
|
||||
<div class="actions">
|
||||
<button id="resume-btn" class="btn secondary" type="button">Переподключить</button>
|
||||
<button id="disconnect-btn" class="btn danger" type="button">Отключить</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title">Войти через другое устройство</div>
|
||||
<label class="field">
|
||||
<span>Логин</span>
|
||||
<input id="login-input" type="text" autocomplete="username" />
|
||||
</label>
|
||||
<label class="checkbox-row">
|
||||
<input id="use-password" type="checkbox" />
|
||||
<span>Использовать доп. пароль</span>
|
||||
</label>
|
||||
<label id="password-field" class="field hidden">
|
||||
<span>Пароль подключения</span>
|
||||
<input id="password-input" type="password" autocomplete="current-password" />
|
||||
</label>
|
||||
<button id="start-btn" class="btn primary" type="button">Получить код</button>
|
||||
<p class="muted small">
|
||||
Wallet plugin создаёт временный requester keypair, ждёт подтверждение на доверенном устройстве
|
||||
и получает только wallet-session без передачи постоянных ключей.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div id="pairing-card" class="card hidden">
|
||||
<div class="card-title">Код подключения</div>
|
||||
<div id="short-code" class="code">0000000</div>
|
||||
<p id="pairing-hint" class="muted small">
|
||||
Покажите код на доверенном устройстве в разделе «Подключить по коду».
|
||||
</p>
|
||||
<p id="pairing-expire" class="muted small"></p>
|
||||
<div class="actions">
|
||||
<button id="cancel-btn" class="btn secondary" type="button">Отменить</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="status" class="status hidden"></div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script type="module" src="./popup.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
244
SHiNE-browser-plugin-wallet/popup.js
Normal file
244
SHiNE-browser-plugin-wallet/popup.js
Normal file
@ -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();
|
||||
1
SHiNE-browser-plugin-wallet/settings.gradle
Normal file
1
SHiNE-browser-plugin-wallet/settings.gradle
Normal file
@ -0,0 +1 @@
|
||||
rootProject.name = 'SHiNE-browser-plugin-wallet'
|
||||
@ -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) {}
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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) {}
|
||||
}
|
||||
|
||||
@ -1,2 +1,2 @@
|
||||
client.version=1.2.204
|
||||
server.version=1.2.193
|
||||
client.version=1.2.205
|
||||
server.version=1.2.194
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
<link rel="manifest" href="./manifest.webmanifest" />
|
||||
<title>Shine UI Demo</title>
|
||||
<script>
|
||||
window.__SHINE_BUILD_HASH__ = '20260530000700';
|
||||
window.__SHINE_BUILD_HASH__ = '20260616091500';
|
||||
window.__SHINE_CLIENT_VERSION__ = '1.2.8';
|
||||
</script>
|
||||
<script>
|
||||
|
||||
@ -42,7 +42,7 @@ import * as topupView from './pages/topup-view.js';
|
||||
import * as devnetTopupView from './pages/devnet-topup-view.js';
|
||||
import * as loginView from './pages/login-view.js?v=202606150110';
|
||||
import * as loginCameraView from './pages/login-camera-view.js';
|
||||
import * as loginOtherDeviceView from './pages/login-other-device-view.js?v=202606150215';
|
||||
import * as loginOtherDeviceView from './pages/login-other-device-view.js?v=202606160915';
|
||||
import * as loginPasswordView from './pages/login-password-view.js';
|
||||
import * as keyStorageView from './pages/key-storage-view.js';
|
||||
|
||||
@ -55,7 +55,7 @@ import * as serverSettingsView from './pages/server-settings-view.js';
|
||||
import * as toolsSettingsView from './pages/tools-settings-view.js';
|
||||
import * as deviceView from './pages/device-view.js?v=202606131435';
|
||||
import * as connectDeviceView from './pages/connect-device-view.js?v=202606142055';
|
||||
import * as devicePairingView from './pages/device-pairing-view.js?v=202606151050';
|
||||
import * as devicePairingView from './pages/device-pairing-view.js?v=202606160915';
|
||||
import * as deviceQrView from './pages/device-qr-view.js';
|
||||
import * as deviceCameraView from './pages/device-camera-view.js';
|
||||
import * as showKeysView from './pages/show-keys-view.js';
|
||||
|
||||
@ -1,14 +1,17 @@
|
||||
import { renderHeader } from '../components/header.js';
|
||||
import {
|
||||
authService,
|
||||
authorizeSession,
|
||||
refreshSessions,
|
||||
setAuthError,
|
||||
setAuthInfo,
|
||||
state,
|
||||
terminateCurrentSession,
|
||||
} from '../state.js';
|
||||
import { formatRelativeTime, showToast } from '../services/channels-ux.js';
|
||||
import {
|
||||
buildSecretsPayload,
|
||||
buildSessionAttachPayload,
|
||||
deriveEspPairingPasswordHash,
|
||||
encryptPairingPayloadForRequester,
|
||||
} from '../services/device-pairing-service.js';
|
||||
@ -53,26 +56,73 @@ function buildTransferKeys(savedKeys, { withExtras = false }) {
|
||||
function requestCardHtml(request) {
|
||||
const shortCode = String(request?.shortCode || '').trim() || '0000000';
|
||||
const client = String(request?.requesterClientPlatform || 'unknown');
|
||||
const requesterSessionType = Number(request?.requesterSessionType || 0);
|
||||
const expiresText = request?.expiresAtMs ? formatRelativeTime(request.expiresAtMs) : '—';
|
||||
const sessionOnly = requesterSessionType === 50;
|
||||
return `
|
||||
<div class="card stack" data-pairing-id="${String(request?.pairingId || '')}">
|
||||
<div class="row" style="align-items:flex-start;">
|
||||
<div class="stack" style="gap:4px;">
|
||||
<div style="font-size:28px; font-weight:700; letter-spacing:0.14em;">${shortCode}</div>
|
||||
<span class="meta-muted">Платформа: ${client}</span>
|
||||
<span class="meta-muted">Тип сессии: ${requesterSessionType || '—'}</span>
|
||||
<span class="meta-muted">Тип payload: ${Number(request?.payloadType || 0)}</span>
|
||||
<span class="meta-muted">Истекает: ${expiresText}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" style="flex-wrap:wrap;">
|
||||
<button class="ghost-btn" type="button" data-action="approve-device">Подключить без доп. ключей</button>
|
||||
<button class="primary-btn" type="button" data-action="approve-full">Подключить и передать ключи</button>
|
||||
<button class="ghost-btn" type="button" data-action="approve-device">${sessionOnly ? 'Подключить wallet-session' : 'Подключить без доп. ключей'}</button>
|
||||
${sessionOnly ? '' : '<button class="primary-btn" type="button" data-action="approve-full">Подключить и передать ключи</button>'}
|
||||
<button class="text-btn" type="button" data-action="reject">Отклонить</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
async function restoreAuthorizedSessionForPairing() {
|
||||
const login = String(state.session.login || '').trim();
|
||||
const sessionId = String(state.session.sessionId || '').trim();
|
||||
if (!login || !sessionId) {
|
||||
throw new Error('Нет активной сохранённой сессии для восстановления pairing-доступа.');
|
||||
}
|
||||
|
||||
const resumed = await authService.resumeSession(login, sessionId);
|
||||
authorizeSession({
|
||||
login: resumed.login || login,
|
||||
sessionId: resumed.sessionId || sessionId,
|
||||
storagePwd: resumed.storagePwd || state.session.storagePwdInMemory,
|
||||
});
|
||||
await refreshSessions().catch(() => {});
|
||||
return resumed;
|
||||
}
|
||||
|
||||
async function runPairingOpWithSessionRestore(runAction) {
|
||||
try {
|
||||
return await runAction();
|
||||
} catch (error) {
|
||||
const code = String(error?.code || '').trim().toUpperCase();
|
||||
if (code !== 'PAIRING_REQUIRES_AUTH_SESSION' && code !== 'NOT_AUTHENTICATED') {
|
||||
throw error;
|
||||
}
|
||||
|
||||
try {
|
||||
await restoreAuthorizedSessionForPairing();
|
||||
} catch (restoreError) {
|
||||
const restoreCode = String(restoreError?.code || '').trim().toUpperCase();
|
||||
if (restoreCode === 'SESSION_NOT_FOUND'
|
||||
|| restoreCode === 'SESSION_KEY_NOT_ACTUAL'
|
||||
|| restoreCode === 'SESSION_OF_ANOTHER_USER') {
|
||||
await terminateCurrentSession({
|
||||
infoMessage: 'Сохранённая сессия устарела. Выполните вход заново.',
|
||||
});
|
||||
}
|
||||
throw restoreError;
|
||||
}
|
||||
|
||||
return runAction();
|
||||
}
|
||||
}
|
||||
|
||||
function makePasswordToggleIcons() {
|
||||
return {
|
||||
eye: `
|
||||
@ -154,7 +204,7 @@ export function render({ navigate }) {
|
||||
</label>
|
||||
<button class="ghost-btn" type="button" id="refresh-pairing-requests">Обновить заявки</button>
|
||||
</div>
|
||||
<p class="meta-muted">Если код не введён, показываются все активные заявки. Для подключения без доп. ключей передаётся только device key.</p>
|
||||
<p class="meta-muted">Если код не введён, показываются все активные заявки. Для wallet-заявки можно выпустить отдельную session-only сессию без передачи постоянных ключей.</p>
|
||||
<div class="stack" id="pairing-requests-list"></div>
|
||||
`;
|
||||
|
||||
@ -301,11 +351,11 @@ export function render({ navigate }) {
|
||||
};
|
||||
|
||||
const removeAdditionalPassword = async () => {
|
||||
const payload = await authService.upsertEspPairingSettings({
|
||||
const payload = await runPairingOpWithSessionRestore(() => authService.upsertEspPairingSettings({
|
||||
enabled: true,
|
||||
passwordHash: '',
|
||||
ttlSeconds: 180,
|
||||
});
|
||||
}));
|
||||
pairingPasswordConfigured = false;
|
||||
saveLocalPairingPasswordState(state.session.login, state.entrySettings.shineServer, false);
|
||||
setAuthInfo(`Подключение по коду без дополнительного пароля включено. TTL: ${payload?.ttlSeconds || 180} сек.`);
|
||||
@ -417,7 +467,7 @@ export function render({ navigate }) {
|
||||
|
||||
const reloadRequests = async ({ silent = false } = {}) => {
|
||||
try {
|
||||
requests = await authService.listEspPairingRequests();
|
||||
requests = await runPairingOpWithSessionRestore(() => authService.listEspPairingRequests());
|
||||
renderRequests();
|
||||
if (!silent) {
|
||||
setStatus(status, 'Список pairing-заявок обновлён.', 'info');
|
||||
@ -435,16 +485,45 @@ export function render({ navigate }) {
|
||||
|
||||
const approveRequest = async (request, mode) => {
|
||||
const withExtras = mode === 'with-extras';
|
||||
const keys = buildTransferKeys(savedKeys, { withExtras });
|
||||
const payload = buildSecretsPayload({
|
||||
login: state.session.login,
|
||||
keys,
|
||||
mode: withExtras ? 'with-extras' : 'device-only',
|
||||
});
|
||||
let payload;
|
||||
if (!withExtras && Number(request?.requesterSessionType || 0) === 50) {
|
||||
const delegatedSession = await authService.createDelegatedSessionWithDeviceKey({
|
||||
login: state.session.login,
|
||||
devicePrivPkcs8: String(savedKeys?.deviceKey || '').trim(),
|
||||
sessionKey: String(request?.requesterSessionKey || '').trim(),
|
||||
sessionType: Number(request?.requesterSessionType || 50) || 50,
|
||||
clientPlatform: String(request?.requesterClientPlatform || '').trim() || 'Wallet plugin',
|
||||
clientInfo: 'Wallet session approved via device pairing',
|
||||
});
|
||||
payload = buildSessionAttachPayload({
|
||||
login: state.session.login,
|
||||
session: delegatedSession,
|
||||
});
|
||||
} else {
|
||||
const keys = buildTransferKeys(savedKeys, { withExtras });
|
||||
payload = buildSecretsPayload({
|
||||
login: state.session.login,
|
||||
keys,
|
||||
mode: withExtras ? 'with-extras' : 'device-only',
|
||||
});
|
||||
}
|
||||
const encryptedPayload = await encryptPairingPayloadForRequester(request?.requesterSessionKey, payload);
|
||||
await authService.approveEspPairing(request?.pairingId, encryptedPayload);
|
||||
showToast(withExtras ? 'Ключи переданы на новое устройство' : 'Новое устройство подключено');
|
||||
setAuthInfo(withExtras ? 'Заявка подтверждена, ключи переданы.' : 'Заявка подтверждена без передачи доп. ключей.');
|
||||
await runPairingOpWithSessionRestore(() => authService.approveEspPairing(request?.pairingId, encryptedPayload));
|
||||
const sessionOnly = !withExtras && Number(request?.requesterSessionType || 0) === 50;
|
||||
showToast(
|
||||
withExtras
|
||||
? 'Ключи переданы на новое устройство'
|
||||
: sessionOnly
|
||||
? 'Wallet-session выпущена для нового устройства'
|
||||
: 'Новое устройство подключено',
|
||||
);
|
||||
setAuthInfo(
|
||||
withExtras
|
||||
? 'Заявка подтверждена, ключи переданы.'
|
||||
: sessionOnly
|
||||
? 'Заявка подтверждена, для нового устройства создана отдельная wallet-session без передачи постоянных ключей.'
|
||||
: 'Заявка подтверждена без передачи доп. ключей.',
|
||||
);
|
||||
await refreshSessions().catch(() => {});
|
||||
await reloadRequests({ silent: true });
|
||||
};
|
||||
@ -513,11 +592,11 @@ export function render({ navigate }) {
|
||||
dialogSaveBtn.disabled = true;
|
||||
try {
|
||||
const passwordHash = await deriveEspPairingPasswordHash(state.session.login, password);
|
||||
const payload = await authService.upsertEspPairingSettings({
|
||||
const payload = await runPairingOpWithSessionRestore(() => authService.upsertEspPairingSettings({
|
||||
enabled: true,
|
||||
passwordHash,
|
||||
ttlSeconds: 180,
|
||||
});
|
||||
}));
|
||||
pairingPasswordConfigured = true;
|
||||
saveLocalPairingPasswordState(state.session.login, state.entrySettings.shineServer, true);
|
||||
closePasswordDialog();
|
||||
@ -562,7 +641,7 @@ export function render({ navigate }) {
|
||||
} else if (action === 'approve-full') {
|
||||
await approveRequest(request, 'with-extras');
|
||||
} else if (action === 'reject') {
|
||||
await authService.rejectEspPairing(pairingId, 'rejected_by_user');
|
||||
await runPairingOpWithSessionRestore(() => authService.rejectEspPairing(pairingId, 'rejected_by_user'));
|
||||
showToast('Заявка отклонена', { kind: 'error' });
|
||||
await reloadRequests({ silent: true });
|
||||
}
|
||||
|
||||
@ -54,6 +54,7 @@ const CHANNEL_TYPE_PERSONAL = 100;
|
||||
const CHANNEL_TYPE_GROUP = 200;
|
||||
const CHANNEL_TYPE_VERSION_DEFAULT = 1;
|
||||
const SESSION_TYPE_CLIENT = 1;
|
||||
const SESSION_TYPE_WALLET = 50;
|
||||
|
||||
const CONNECTION_SUBTYPES = Object.freeze({
|
||||
// Legacy alias: friend == close_friend. Оба ключа ведут на один и тот же код 10/11.
|
||||
@ -887,6 +888,67 @@ export class AuthService {
|
||||
return session;
|
||||
}
|
||||
|
||||
async createDelegatedSessionWithDeviceKey({
|
||||
login,
|
||||
devicePrivPkcs8,
|
||||
sessionKey,
|
||||
sessionType = SESSION_TYPE_WALLET,
|
||||
clientPlatform = 'Delegated session',
|
||||
clientInfo = 'Delegated session via pairing',
|
||||
}) {
|
||||
const cleanLogin = String(login || '').trim();
|
||||
const cleanSessionKey = String(sessionKey || '').trim();
|
||||
const cleanDevicePriv = String(devicePrivPkcs8 || '').trim();
|
||||
if (!cleanLogin) throw new Error('createDelegatedSessionWithDeviceKey: пустой login');
|
||||
if (!cleanSessionKey) throw new Error('createDelegatedSessionWithDeviceKey: пустой sessionKey');
|
||||
if (!cleanDevicePriv) throw new Error('createDelegatedSessionWithDeviceKey: пустой device private key');
|
||||
|
||||
const devicePrivateKey = await importPkcs8Ed25519(cleanDevicePriv);
|
||||
const devicePublicKeyB64 = await publicKeyB64FromPkcs8Ed25519(cleanDevicePriv);
|
||||
const storagePwd = randomBase64(32);
|
||||
const tempAuth = new AuthService(this.serverUrl);
|
||||
|
||||
try {
|
||||
const challengeResp = await tempAuth.ws.request('AuthChallenge', { login: cleanLogin });
|
||||
if (challengeResp.status !== 200) throw opError('AuthChallenge', challengeResp);
|
||||
|
||||
const authNonce = challengeResp?.payload?.authNonce;
|
||||
if (!authNonce) throw new Error('AuthChallenge: сервер не вернул authNonce');
|
||||
|
||||
const timeMs = Date.now();
|
||||
const preimage = `AUTH_CREATE_SESSION:${cleanLogin}:${cleanSessionKey}:${storagePwd}:${timeMs}:${authNonce}`;
|
||||
const signatureB64 = await signBase64(devicePrivateKey, preimage);
|
||||
|
||||
const createResp = await tempAuth.ws.request('CreateAuthSession', {
|
||||
login: cleanLogin,
|
||||
storagePwd,
|
||||
sessionKey: cleanSessionKey,
|
||||
timeMs,
|
||||
authNonce,
|
||||
deviceKey: devicePublicKeyB64,
|
||||
signatureB64,
|
||||
sessionType: Number(sessionType) || SESSION_TYPE_WALLET,
|
||||
clientPlatform: String(clientPlatform || '').trim() || 'Delegated session',
|
||||
clientInfo: String(clientInfo || '').trim() || 'Delegated session via pairing',
|
||||
});
|
||||
if (createResp.status !== 200) throw opError('CreateAuthSession', createResp);
|
||||
|
||||
const sessionId = createResp?.payload?.sessionId;
|
||||
if (!sessionId) throw new Error('CreateAuthSession: не вернулся sessionId');
|
||||
|
||||
return {
|
||||
login: cleanLogin,
|
||||
sessionId,
|
||||
storagePwd,
|
||||
sessionKey: cleanSessionKey,
|
||||
sessionType: Number(sessionType) || SESSION_TYPE_WALLET,
|
||||
clientPlatform: String(clientPlatform || '').trim() || 'Delegated session',
|
||||
};
|
||||
} finally {
|
||||
tempAuth.ws.close();
|
||||
}
|
||||
}
|
||||
|
||||
async persistSelectedKeys(login, storagePwd, keyBundle, saveOptions = { saveRoot: true, saveBlockchain: true }) {
|
||||
let currentSecrets = {};
|
||||
try {
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import {
|
||||
base64ToBytes,
|
||||
bytesToBase64,
|
||||
deriveOpaqueArgon2Hash,
|
||||
exportEd25519PublicKeyB64,
|
||||
exportPkcs8B64,
|
||||
generateEd25519Pair,
|
||||
sha256Bytes,
|
||||
sha256Text,
|
||||
utf8Bytes,
|
||||
} from './crypto-utils.js';
|
||||
import {
|
||||
@ -14,8 +14,9 @@ import {
|
||||
x25519,
|
||||
} from 'https://esm.sh/@noble/curves@1.5.0/ed25519';
|
||||
|
||||
const PAIRING_HASH_SUFFIX = 'esp.pairing.password';
|
||||
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,
|
||||
]);
|
||||
@ -84,10 +85,11 @@ export function detectPairingPayloadType(keys = {}) {
|
||||
}
|
||||
|
||||
export async function deriveEspPairingPasswordHash(login, password) {
|
||||
return deriveOpaqueArgon2Hash(password, {
|
||||
login,
|
||||
suffix: PAIRING_HASH_SUFFIX,
|
||||
});
|
||||
const loginLower = String(login || '').trim().toLowerCase();
|
||||
const preimage = `${PAIRING_HASH_VERSION}|${loginLower}|${String(password ?? '')}`;
|
||||
const digest = await sha256Text(preimage);
|
||||
const hex = [...digest].map((byte) => byte.toString(16).padStart(2, '0')).join('');
|
||||
return `${PAIRING_HASH_PREFIX}${hex}`;
|
||||
}
|
||||
|
||||
export async function createRequesterPairingMaterial() {
|
||||
@ -158,3 +160,19 @@ export function buildSecretsPayload({ login, keys, mode }) {
|
||||
createdAtMs: Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
export function buildSessionAttachPayload({ login, session }) {
|
||||
return {
|
||||
v: 1,
|
||||
type: 'shine-esp-session-attach',
|
||||
login: String(login || '').trim(),
|
||||
session: {
|
||||
sessionId: String(session?.sessionId || '').trim(),
|
||||
sessionKey: String(session?.sessionKey || '').trim(),
|
||||
storagePwd: String(session?.storagePwd || '').trim(),
|
||||
sessionType: Number(session?.sessionType || 50) || 50,
|
||||
clientPlatform: String(session?.clientPlatform || '').trim(),
|
||||
},
|
||||
createdAtMs: Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user