SHA256
homeserver: рендейм subserver→homeserver, документ деривации ключей, запрет пустого пароля
Основное (наша работа в этой сессии): - Переименование «subserver» → «homeserver» по всему проекту: основной ESP32-скетч (папка shine_subserver_ui → shine_homeserver_ui, .ino, flash-скрипт, режим burn.sh homeserver-ui), скетч lvgl_nav_minimal_test (ключ homeserver.key:<имя>), spec-доки reference/*, формат PDA (терминология session_type=100 «Homeserver пользователя»), константа SESSION_TYPE_HOMESERVER в JS и Rust (значение 100 не менялось, формат не затронут), pending/future доки, AGENTS.md, DAO-док. Сохранены отдельный lvgl_subserver_touch_test и историческая пометка о рендейме в DERIVATION.md. - Новый источник истины по деривации ключей: Dev_Docs/Keys/DERIVATION.md (Argon2id-секрет из пароля, формула Ed25519(SHA-256(base64(secret)|suffix)), суффиксы root/bch/dev/homeserver.key, Solana-ключ = dev.key). Уточнены роли root (главный/master) и dev (пополняемый кошелёк) в Dev_Docs/Keys/README.md. - UI: убран легаси-путь пустого пароля (derivePasswordSeed и др.), deriveMasterSecretFromPassword бросает ошибку на пустом пароле, register-view блокирует пустой пароль; экран пополнения переведён на канонический device-адрес из preGeneratedKeyBundle (удалён расходящийся deriveWalletFromPassword). Включены также параллельные правки Solana-аудита №3 (были в рабочем дереве, переплетены в lib.rs): - shine_users: defense-in-depth «строгий список аккаунтов» (require!(it.next().is_none())) в init/update economy config и create/update user PDA, плюс описание в doc/programs/shine_users.md; - Dev_Docs/audit/Solana-audit-3-by-Claude-12июня2026.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
cf6a2830c8
commit
42dcf6970d
@@ -176,6 +176,11 @@ export function render({ navigate }) {
|
||||
const prevPassword = String(state.registrationDraft.password || '');
|
||||
const nextLogin = String(loginInput.value.trim());
|
||||
const nextPassword = String(passwordInput.value || '');
|
||||
if (nextPassword.length === 0) {
|
||||
formError.textContent = 'Пустой пароль запрещён. Введите непустой пароль для регистрации.';
|
||||
formError.style.display = '';
|
||||
return;
|
||||
}
|
||||
const credsChanged = prevLogin !== nextLogin || prevPassword !== nextPassword;
|
||||
|
||||
state.registrationDraft.login = nextLogin;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { renderHeader } from '../components/header.js';
|
||||
import { state } from '../state.js';
|
||||
import {
|
||||
deriveWalletFromPassword,
|
||||
formatSol,
|
||||
getBalanceSol,
|
||||
getTopupSiteUrl,
|
||||
@@ -10,6 +9,21 @@ import {
|
||||
|
||||
export const pageMeta = { id: 'topup-view', title: 'Пополнение счета', showAppChrome: false };
|
||||
|
||||
// Канонический Solana-адрес пополнения = публичный device-ключ из сгенерированного набора ключей.
|
||||
// Тот же путь, что в registration-payment-view (deriveUserWalletAddress); не выводим адрес
|
||||
// напрямую из пароля, иначе он расходится с device-ключом регистрации.
|
||||
async function deviceWalletAddressFromBundle() {
|
||||
const keyBundle = state.registrationDraft.preGeneratedKeyBundle;
|
||||
if (!keyBundle || !keyBundle.devicePair) {
|
||||
throw new Error('Ключи ещё не сгенерированы. Вернитесь на экран регистрации.');
|
||||
}
|
||||
const raw = atob(keyBundle.devicePair.publicKeyB64);
|
||||
const bytes = new Uint8Array(raw.length);
|
||||
for (let i = 0; i < raw.length; i += 1) bytes[i] = raw.charCodeAt(i);
|
||||
const { PublicKey } = await import('https://esm.sh/@solana/web3.js@1.98.4');
|
||||
return new PublicKey(bytes).toBase58();
|
||||
}
|
||||
|
||||
export function render({ navigate }) {
|
||||
const screen = document.createElement('section');
|
||||
screen.className = 'stack';
|
||||
@@ -55,7 +69,7 @@ export function render({ navigate }) {
|
||||
</div>
|
||||
<a class="link-card" id="topup-site-link" href="${getTopupSiteUrl(state.registrationPayment.walletAddress || '')}" target="_blank" rel="noreferrer">Открыть сайт пополнения</a>
|
||||
<div class="card stack" style="padding:12px; max-width:320px;">
|
||||
<div class="field-label" style="margin-bottom:6px;">Кошелёк для пополнения (wallet.key)</div>
|
||||
<div class="field-label" style="margin-bottom:6px;">Кошелёк для пополнения (device key = Solana wallet)</div>
|
||||
</div>
|
||||
`;
|
||||
card.children[3].append(walletRow);
|
||||
@@ -103,9 +117,9 @@ export function render({ navigate }) {
|
||||
(async () => {
|
||||
try {
|
||||
if (!walletValue.value) {
|
||||
const wallet = await deriveWalletFromPassword(String(state.registrationDraft.password ?? ''));
|
||||
state.registrationPayment.walletAddress = wallet.address;
|
||||
walletValue.value = wallet.address;
|
||||
const address = await deviceWalletAddressFromBundle();
|
||||
state.registrationPayment.walletAddress = address;
|
||||
walletValue.value = address;
|
||||
}
|
||||
const topupSiteLink = card.querySelector('#topup-site-link');
|
||||
if (topupSiteLink instanceof HTMLAnchorElement) {
|
||||
|
||||
@@ -116,12 +116,6 @@ export async function sha256Text(text) {
|
||||
return sha256Bytes(utf8Bytes(text));
|
||||
}
|
||||
|
||||
export async function derivePasswordSeed(password, suffix) {
|
||||
const base = await sha256Text(password || '');
|
||||
const concat = `${bytesToBase64(base)}${suffix}`;
|
||||
return sha256Text(concat);
|
||||
}
|
||||
|
||||
function normalizeLoginForKdf(login) {
|
||||
return String(login || '').trim().toLowerCase();
|
||||
}
|
||||
@@ -134,21 +128,6 @@ async function makeArgon2Salt(login, suffix) {
|
||||
return digest.slice(0, 16);
|
||||
}
|
||||
|
||||
async function derivePasswordSeedArgon2id({ login, password, suffix }) {
|
||||
const normalizedLogin = normalizeLoginForKdf(login);
|
||||
const normalizedPassword = String(password ?? '');
|
||||
const normalizedSuffix = String(suffix || '').trim();
|
||||
const salt = await makeArgon2Salt(normalizedLogin, normalizedSuffix);
|
||||
const passBytes = utf8Bytes(`${normalizedLogin}\n${normalizedPassword}`);
|
||||
const out = await argon2idAsync(passBytes, salt, {
|
||||
t: 2,
|
||||
m: 65536,
|
||||
p: 1,
|
||||
dkLen: 32,
|
||||
});
|
||||
return new Uint8Array(out);
|
||||
}
|
||||
|
||||
async function deriveMasterSecretArgon2id({ login, password, onProgress }) {
|
||||
const normalizedLogin = normalizeLoginForKdf(login);
|
||||
const normalizedPassword = String(password ?? '');
|
||||
@@ -177,38 +156,13 @@ function ed25519Pkcs8FromSeed(seed32) {
|
||||
return out;
|
||||
}
|
||||
|
||||
export async function deriveEd25519FromPassword(password, suffix, options = {}) {
|
||||
const normalizedPassword = String(password ?? '');
|
||||
const normalizedLogin = String(options?.login ?? '');
|
||||
const useLegacyEmptyPassword = normalizedPassword.length === 0;
|
||||
const seed = useLegacyEmptyPassword
|
||||
? await derivePasswordSeed(normalizedPassword, suffix)
|
||||
: await derivePasswordSeedArgon2id({
|
||||
login: normalizedLogin,
|
||||
password: normalizedPassword,
|
||||
suffix,
|
||||
});
|
||||
const pkcs8 = ed25519Pkcs8FromSeed(seed);
|
||||
const subtle = getSubtleApi();
|
||||
const privateKey = await subtle.importKey('pkcs8', pkcs8, { name: 'Ed25519' }, true, ['sign']);
|
||||
const jwk = await subtle.exportKey('jwk', privateKey);
|
||||
if (!jwk.x) throw new Error('Не удалось получить публичный ключ Ed25519');
|
||||
|
||||
return {
|
||||
privateKey,
|
||||
publicKeyB64: bytesToBase64(base64ToBytes(base64UrlToBase64(jwk.x))),
|
||||
privatePkcs8B64: bytesToBase64(pkcs8),
|
||||
};
|
||||
}
|
||||
|
||||
export async function deriveMasterSecretFromPassword(password, options = {}) {
|
||||
const normalizedPassword = String(password ?? '');
|
||||
const normalizedLogin = String(options?.login ?? '');
|
||||
const onProgress = typeof options?.onProgress === 'function' ? options.onProgress : undefined;
|
||||
if (normalizedPassword.length === 0) {
|
||||
const legacy = await derivePasswordSeed(normalizedPassword, 'master.secret');
|
||||
if (onProgress) onProgress(1);
|
||||
return legacy;
|
||||
// Пустой пароль запрещён: упрощённый легаси-путь убран, регистрация/вход требуют непустой пароль.
|
||||
throw new Error('Пустой пароль запрещён: регистрация и вход требуют непустой пароль.');
|
||||
}
|
||||
return deriveMasterSecretArgon2id({
|
||||
login: normalizedLogin,
|
||||
|
||||
@@ -25,7 +25,7 @@ const BLOCK_TYPE_SESSIONS = 50;
|
||||
const BLOCK_TYPE_TRUSTED_STATE = 70;
|
||||
const SESSIONS_MODE_MIXED = 1;
|
||||
const SESSION_TYPE_USER = 1;
|
||||
const SESSION_TYPE_SUBSERVER = 100;
|
||||
const SESSION_TYPE_HOMESERVER = 100;
|
||||
|
||||
let solanaLibPromise = null;
|
||||
function loadSolanaLib() {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { deriveEd25519FromPassword } from './crypto-utils.js';
|
||||
import { extractDeviceKey32FromStoredValue } from './device-key-utils.js';
|
||||
import { loadEncryptedUserSecrets } from './key-vault.js';
|
||||
import { SOLANA_ENDPOINT_DEFAULT } from '../solana-programs.js';
|
||||
@@ -68,17 +67,6 @@ async function keypairFromPkcs8(pkcs8B64) {
|
||||
return solana.Keypair.fromSeed(seed32);
|
||||
}
|
||||
|
||||
export async function deriveWalletFromPassword(password) {
|
||||
const keyBundle = await deriveEd25519FromPassword(String(password ?? ''), 'dev.key');
|
||||
const keypair = await keypairFromPkcs8(keyBundle.privatePkcs8B64);
|
||||
return {
|
||||
address: keypair.publicKey.toBase58(),
|
||||
keypair,
|
||||
devicePublicKeyB64: keyBundle.publicKeyB64,
|
||||
devicePrivatePkcs8B64: keyBundle.privatePkcs8B64,
|
||||
};
|
||||
}
|
||||
|
||||
export async function createRandomSolanaWallet() {
|
||||
const solana = await loadSolanaLib();
|
||||
const keypair = solana.Keypair.generate();
|
||||
|
||||
Reference in New Issue
Block a user