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:
AidarKC
2026-06-12 21:16:12 +04:00
co-authored by Claude Opus 4.8
parent cf6a2830c8
commit 42dcf6970d
32 changed files with 517 additions and 251 deletions
+2 -48
View File
@@ -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();