SHA256
Синхронизировать регистр логинов и доработать UI каналов
This commit is contained in:
@@ -1022,6 +1022,18 @@ export class AuthService {
|
||||
return response.payload || {};
|
||||
}
|
||||
|
||||
async resolveCanonicalDisplayLogin(login) {
|
||||
const cleanLogin = String(login || '').trim();
|
||||
if (!cleanLogin) return '';
|
||||
try {
|
||||
const user = await this.getUser(cleanLogin);
|
||||
const canonicalLogin = String(user?.login || '').trim();
|
||||
return canonicalLogin || cleanLogin;
|
||||
} catch {
|
||||
return cleanLogin;
|
||||
}
|
||||
}
|
||||
|
||||
async ensureLoginFree(login) {
|
||||
const payload = await this.getUser(login);
|
||||
return payload.exists !== true;
|
||||
@@ -1127,8 +1139,10 @@ export class AuthService {
|
||||
const sessionId = createResp?.payload?.sessionId;
|
||||
if (!sessionId) throw new Error('CreateAuthSession: не вернулся sessionId');
|
||||
|
||||
const canonicalLogin = await this.resolveCanonicalDisplayLogin(cleanLogin);
|
||||
|
||||
return {
|
||||
login: cleanLogin,
|
||||
login: canonicalLogin,
|
||||
sessionId,
|
||||
storagePwd,
|
||||
sessionMaterial: {
|
||||
@@ -1213,8 +1227,10 @@ export class AuthService {
|
||||
const sessionId = createResp?.payload?.sessionId;
|
||||
if (!sessionId) throw new Error('CreateAuthSession: не вернулся sessionId');
|
||||
|
||||
const canonicalLogin = await tempAuth.resolveCanonicalDisplayLogin(cleanLogin);
|
||||
|
||||
return {
|
||||
login: cleanLogin,
|
||||
login: canonicalLogin,
|
||||
sessionId,
|
||||
storagePwd,
|
||||
sessionKey: cleanSessionKey,
|
||||
@@ -1287,8 +1303,10 @@ export class AuthService {
|
||||
const storagePwd = loginResp?.payload?.storagePwd;
|
||||
if (!storagePwd) throw new Error('SessionLogin: не вернулся storagePwd');
|
||||
|
||||
const canonicalLogin = await this.resolveCanonicalDisplayLogin(cleanLogin);
|
||||
|
||||
return {
|
||||
login: cleanLogin,
|
||||
login: canonicalLogin,
|
||||
sessionId: targetSessionId,
|
||||
storagePwd,
|
||||
};
|
||||
@@ -2476,6 +2494,10 @@ export class AuthService {
|
||||
return response.payload || {};
|
||||
}
|
||||
|
||||
normalizeDmLogin(login) {
|
||||
return String(login || '').trim().toLowerCase();
|
||||
}
|
||||
|
||||
async buildSignedDmBlock({
|
||||
signerLogin,
|
||||
fromLogin,
|
||||
@@ -2488,9 +2510,9 @@ export class AuthService {
|
||||
reencryptedAtMs = 0,
|
||||
bodyBytes = new Uint8Array(0),
|
||||
}) {
|
||||
const cleanSignerLogin = String(signerLogin || '').trim();
|
||||
const cleanFromLogin = String(fromLogin || '').trim();
|
||||
const cleanToLogin = String(toLogin || '').trim();
|
||||
const cleanSignerLogin = this.normalizeDmLogin(signerLogin);
|
||||
const cleanFromLogin = this.normalizeDmLogin(fromLogin);
|
||||
const cleanToLogin = this.normalizeDmLogin(toLogin);
|
||||
if (!cleanSignerLogin || !cleanFromLogin || !cleanToLogin) throw new Error('Не передан signerLogin/fromLogin/toLogin');
|
||||
if (!storagePwd) throw new Error('Не передан storagePwd для подписи');
|
||||
if (!(bodyBytes instanceof Uint8Array) || bodyBytes.length > DM_MAX_ENCRYPTED_BODY_BYTES) {
|
||||
@@ -2590,8 +2612,8 @@ export class AuthService {
|
||||
revisionTimeMs = 0,
|
||||
reencryptedAtMs = 0,
|
||||
}) {
|
||||
const cleanFromLogin = String(login || '').trim();
|
||||
const cleanToLogin = String(toLogin || '').trim();
|
||||
const cleanFromLogin = this.normalizeDmLogin(login);
|
||||
const cleanToLogin = this.normalizeDmLogin(toLogin);
|
||||
const cleanText = String(text || '');
|
||||
if (!cleanFromLogin || !cleanToLogin) throw new Error('Не передан login/toLogin');
|
||||
const normalizedTimeMs = Number(timeMs);
|
||||
@@ -2674,8 +2696,8 @@ export class AuthService {
|
||||
}
|
||||
|
||||
async deleteDirectMessage({ login, toLogin, storagePwd, timeMs, nonce, revisionTimeMs, deleteByRecipient = false }) {
|
||||
const cleanLogin = String(login || '').trim();
|
||||
const cleanPeerLogin = String(toLogin || '').trim();
|
||||
const cleanLogin = this.normalizeDmLogin(login);
|
||||
const cleanPeerLogin = this.normalizeDmLogin(toLogin);
|
||||
if (!cleanLogin || !cleanPeerLogin) throw new Error('Не передан login/toLogin');
|
||||
const normalizedTimeMs = Number(timeMs);
|
||||
const normalizedNonce = Number(nonce);
|
||||
@@ -2706,14 +2728,23 @@ export class AuthService {
|
||||
}
|
||||
|
||||
async sendReadReceipt({ login, toLogin, storagePwd, refToLogin, refFromLogin, refTimeMs, refNonce }) {
|
||||
const cleanLogin = this.normalizeDmLogin(login);
|
||||
const cleanToLogin = this.normalizeDmLogin(toLogin);
|
||||
const cleanRefToLogin = this.normalizeDmLogin(refToLogin);
|
||||
const cleanRefFromLogin = this.normalizeDmLogin(refFromLogin);
|
||||
const timeMs = Date.now();
|
||||
const nonce = Math.floor(Math.random() * 0x100000000);
|
||||
const payload = buildReadReceiptPayloadBytes({ refToLogin, refFromLogin, refTimeMs, refNonce });
|
||||
const payload = buildReadReceiptPayloadBytes({
|
||||
refToLogin: cleanRefToLogin,
|
||||
refFromLogin: cleanRefFromLogin,
|
||||
refTimeMs,
|
||||
refNonce,
|
||||
});
|
||||
|
||||
const type3 = await this.buildSignedDmBlock({
|
||||
signerLogin: login,
|
||||
fromLogin: login,
|
||||
toLogin,
|
||||
signerLogin: cleanLogin,
|
||||
fromLogin: cleanLogin,
|
||||
toLogin: cleanToLogin,
|
||||
storagePwd,
|
||||
timeMs,
|
||||
nonce,
|
||||
@@ -2721,9 +2752,9 @@ export class AuthService {
|
||||
bodyBytes: payload,
|
||||
});
|
||||
const type4 = await this.buildSignedDmBlock({
|
||||
signerLogin: login,
|
||||
fromLogin: login,
|
||||
toLogin,
|
||||
signerLogin: cleanLogin,
|
||||
fromLogin: cleanLogin,
|
||||
toLogin: cleanToLogin,
|
||||
storagePwd,
|
||||
timeMs,
|
||||
nonce,
|
||||
@@ -2737,8 +2768,8 @@ export class AuthService {
|
||||
}
|
||||
|
||||
async deleteConversation({ login, toLogin, storagePwd, deleteByRecipient = false, timeMs = Date.now(), nonce = Math.floor(Math.random() * 0x100000000) }) {
|
||||
const cleanLogin = String(login || '').trim();
|
||||
const cleanPeerLogin = String(toLogin || '').trim();
|
||||
const cleanLogin = this.normalizeDmLogin(login);
|
||||
const cleanPeerLogin = this.normalizeDmLogin(toLogin);
|
||||
if (!cleanLogin || !cleanPeerLogin) throw new Error('Не передан login/toLogin');
|
||||
const normalizedTimeMs = Number(timeMs);
|
||||
const normalizedNonce = Number(nonce);
|
||||
|
||||
@@ -8,6 +8,10 @@ const DB_VERSION = 1;
|
||||
const STORE_SECRETS = 'encrypted-secrets';
|
||||
const STORE_SESSIONS = 'session-keys';
|
||||
|
||||
function normalizeLoginStorageKey(login) {
|
||||
return String(login || '').trim().toLowerCase();
|
||||
}
|
||||
|
||||
function openDb() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
||||
@@ -50,15 +54,20 @@ async function get(storeName, key) {
|
||||
|
||||
export async function saveEncryptedUserSecrets(login, storagePwd, keys) {
|
||||
const encrypted = await encryptJsonWithStoragePwd(keys, storagePwd);
|
||||
const normalizedLogin = normalizeLoginStorageKey(login);
|
||||
await put(STORE_SECRETS, {
|
||||
login,
|
||||
login: normalizedLogin,
|
||||
encrypted,
|
||||
updatedAtMs: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
export async function loadEncryptedUserSecrets(login, storagePwd) {
|
||||
const row = await get(STORE_SECRETS, login);
|
||||
const normalizedLogin = normalizeLoginStorageKey(login);
|
||||
let row = await get(STORE_SECRETS, normalizedLogin);
|
||||
if (!row?.encrypted && normalizedLogin !== String(login || '').trim()) {
|
||||
row = await get(STORE_SECRETS, String(login || '').trim());
|
||||
}
|
||||
if (!row?.encrypted) {
|
||||
throw new Error('На устройстве нет сохранённых ключей для этого логина');
|
||||
}
|
||||
@@ -80,15 +89,19 @@ export async function updateEncryptedUserSecrets(login, storagePwd, updater) {
|
||||
}
|
||||
|
||||
export async function saveSessionMaterial(login, material) {
|
||||
const normalizedLogin = normalizeLoginStorageKey(login);
|
||||
await put(STORE_SESSIONS, {
|
||||
login,
|
||||
login: normalizedLogin,
|
||||
...material,
|
||||
updatedAtMs: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
export async function loadSessionMaterial(login) {
|
||||
return get(STORE_SESSIONS, login);
|
||||
const normalizedLogin = normalizeLoginStorageKey(login);
|
||||
const row = await get(STORE_SESSIONS, normalizedLogin);
|
||||
if (row || normalizedLogin === String(login || '').trim()) return row;
|
||||
return get(STORE_SESSIONS, String(login || '').trim());
|
||||
}
|
||||
|
||||
export async function clearClientAuthData() {
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
const SNAPSHOT_STORAGE_KEY = 'shine-ui-blockchain-snapshot-v1';
|
||||
|
||||
function normalizeLogin(login) {
|
||||
return String(login || '').trim().toLowerCase();
|
||||
}
|
||||
|
||||
function storageBucketKey(login) {
|
||||
return normalizeLogin(login) || 'anonymous';
|
||||
}
|
||||
|
||||
function decodeBase64ToBytes(base64) {
|
||||
const raw = String(base64 || '').trim();
|
||||
if (!raw) return new Uint8Array();
|
||||
const bin = atob(raw);
|
||||
const out = new Uint8Array(bin.length);
|
||||
for (let i = 0; i < bin.length; i += 1) out[i] = bin.charCodeAt(i);
|
||||
return out;
|
||||
}
|
||||
|
||||
function readStorageMap() {
|
||||
try {
|
||||
const raw = localStorage.getItem(SNAPSHOT_STORAGE_KEY);
|
||||
const parsed = raw ? JSON.parse(raw) : {};
|
||||
return parsed && typeof parsed === 'object' ? parsed : {};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function writeStorageMap(value) {
|
||||
try {
|
||||
localStorage.setItem(SNAPSHOT_STORAGE_KEY, JSON.stringify(value || {}));
|
||||
} catch {
|
||||
// ignore localStorage errors
|
||||
}
|
||||
}
|
||||
|
||||
export function readStoredBlockchainSnapshot(login) {
|
||||
const key = storageBucketKey(login);
|
||||
const map = readStorageMap();
|
||||
const value = map?.[key];
|
||||
return value && typeof value === 'object' ? value : null;
|
||||
}
|
||||
|
||||
export function saveStoredBlockchainSnapshot(login, snapshot) {
|
||||
const key = storageBucketKey(login);
|
||||
const map = readStorageMap();
|
||||
map[key] = {
|
||||
...snapshot,
|
||||
savedAtMs: Date.now(),
|
||||
};
|
||||
writeStorageMap(map);
|
||||
return map[key];
|
||||
}
|
||||
|
||||
export function clearStoredBlockchainSnapshot(login) {
|
||||
const key = storageBucketKey(login);
|
||||
const map = readStorageMap();
|
||||
delete map[key];
|
||||
writeStorageMap(map);
|
||||
}
|
||||
|
||||
export async function buildBlockchainSnapshotFile({
|
||||
authService,
|
||||
login,
|
||||
blockchainName,
|
||||
lastBlockNumber,
|
||||
} = {}) {
|
||||
const cleanLogin = normalizeLogin(login);
|
||||
const cleanBlockchainName = String(blockchainName || '').trim();
|
||||
const maxBlockNumber = Number(lastBlockNumber);
|
||||
if (!authService?.ws?.request) throw new Error('Сервис сервера недоступен.');
|
||||
if (!cleanLogin) throw new Error('Не указан логин.');
|
||||
if (!cleanBlockchainName) throw new Error('Не указано имя блокчейна.');
|
||||
if (!Number.isFinite(maxBlockNumber) || maxBlockNumber < 0) {
|
||||
throw new Error('На сервере нет блоков для слепка.');
|
||||
}
|
||||
|
||||
const parts = [];
|
||||
let totalBytes = 0;
|
||||
for (let blockNumber = 0; blockNumber <= maxBlockNumber; blockNumber += 1) {
|
||||
const response = await authService.ws.request('GetBlockchainBlock', {
|
||||
blockchainName: cleanBlockchainName,
|
||||
blockNumber,
|
||||
});
|
||||
if (response?.status !== 200) {
|
||||
const message = String(response?.payload?.message || response?.message || 'Не удалось получить блок.');
|
||||
throw new Error(`Не удалось скачать блок ${blockNumber}: ${message}`);
|
||||
}
|
||||
const blockBytes = decodeBase64ToBytes(response?.payload?.blockBytesB64 || '');
|
||||
parts.push(blockBytes);
|
||||
totalBytes += blockBytes.length;
|
||||
}
|
||||
|
||||
const file = new File(parts, `${cleanBlockchainName}.shine-blockchain`, {
|
||||
type: 'application/octet-stream',
|
||||
lastModified: Date.now(),
|
||||
});
|
||||
|
||||
return {
|
||||
file,
|
||||
blockCount: maxBlockNumber + 1,
|
||||
totalBytes,
|
||||
lastBlockNumber: maxBlockNumber,
|
||||
login: cleanLogin,
|
||||
blockchainName: cleanBlockchainName,
|
||||
};
|
||||
}
|
||||
@@ -721,6 +721,8 @@ export async function getShineBlockchainUsage({ login, solanaEndpoint }) {
|
||||
paidLimitBytes: bch.paidLimitBytes,
|
||||
usedBytes: bch.usedBytes,
|
||||
leftBytes,
|
||||
blockchainName: String(bch.blockchainName || ''),
|
||||
arweaveTxId: String(bch.arweaveTxId || ''),
|
||||
lastBlockNumber: bch.lastBlockNumber,
|
||||
lastBlockHashHex: Array.from(bch.lastBlockHash).map((x) => x.toString(16).padStart(2, '0')).join(''),
|
||||
};
|
||||
@@ -762,9 +764,10 @@ async function attachSolanaLogs(error, connection) {
|
||||
}
|
||||
|
||||
async function buildCreateContext({ login, keyBundle, solanaEndpoint }) {
|
||||
const rawLogin = String(login || '').trim();
|
||||
const cleanLogin = normalizeLogin(login);
|
||||
const endpoint = String(solanaEndpoint || '').trim();
|
||||
if (!cleanLogin) throw new Error('Не указан логин');
|
||||
if (!rawLogin || !cleanLogin) throw new Error('Не указан логин');
|
||||
if (!endpoint) throw new Error('Не указан Solana RPC endpoint');
|
||||
|
||||
const solana = await loadSolanaLib();
|
||||
@@ -791,6 +794,7 @@ async function buildCreateContext({ login, keyBundle, solanaEndpoint }) {
|
||||
const clientKeypair = solana.Keypair.fromSeed(clientSeed32);
|
||||
|
||||
return {
|
||||
rawLogin,
|
||||
cleanLogin,
|
||||
endpoint,
|
||||
solana,
|
||||
@@ -832,18 +836,19 @@ async function createShineUserPdaOnSolana({
|
||||
}
|
||||
|
||||
const cleanLogin = ctx.cleanLogin;
|
||||
const displayLogin = ctx.rawLogin;
|
||||
const cleanPromoCode = String(promoCode || '').trim();
|
||||
const blockchainName = `${cleanLogin}-001`;
|
||||
const zeroHash32 = new Uint8Array(32);
|
||||
const createdAtMs = BigInt(Date.now());
|
||||
const startBonusLimit = parseUsersEconomyConfig(ecoAccount.data).startBonusLimit;
|
||||
|
||||
const lastBlockStateBytes = buildLastBlockStateBytes(cleanLogin, blockchainName, 0, zeroHash32, 0n);
|
||||
const lastBlockStateBytes = buildLastBlockStateBytes(displayLogin, blockchainName, 0, zeroHash32, 0n);
|
||||
const lastBlockStateHash = await sha256Bytes(lastBlockStateBytes);
|
||||
const lastBlockSig64 = await signBytes(ctx.bchPrivKey, lastBlockStateHash);
|
||||
|
||||
const initialState = createPdaState({
|
||||
login: cleanLogin,
|
||||
login: displayLogin,
|
||||
createdAtMs,
|
||||
updatedAtMs: createdAtMs,
|
||||
recordNumber: 0,
|
||||
@@ -910,7 +915,7 @@ async function createShineUserPdaOnSolana({
|
||||
});
|
||||
}
|
||||
const ixData = serializeCreateUserPdaArgs({
|
||||
login: cleanLogin,
|
||||
login: displayLogin,
|
||||
recoveryKey32: ctx.recoveryKey32,
|
||||
rootKey32: ctx.rootKey32,
|
||||
createdAtMs,
|
||||
@@ -1028,6 +1033,7 @@ export async function updateShineUserPdaOnSolana({
|
||||
nextUsedBytes,
|
||||
nextLastBlockNumber,
|
||||
nextLastBlockHashHex,
|
||||
nextArweaveTxId,
|
||||
serverProfile,
|
||||
accessServers,
|
||||
trustedCount,
|
||||
@@ -1043,6 +1049,7 @@ export async function updateShineUserPdaOnSolana({
|
||||
const effectiveUsed = nextUsedBytes == null ? currentBch.usedBytes : BigInt(nextUsedBytes);
|
||||
const effectiveLastNum = nextLastBlockNumber == null ? currentBch.lastBlockNumber : Number(nextLastBlockNumber);
|
||||
const effectiveLastHash = parseHex32(nextLastBlockHashHex) || currentBch.lastBlockHash;
|
||||
const effectiveArweaveTxId = nextArweaveTxId == null ? currentBch.arweaveTxId : String(nextArweaveTxId || '').trim();
|
||||
if (effectiveLastHash.length !== 32) throw new Error('last block hash должен быть 32 байта');
|
||||
|
||||
const rootPriv = await importPkcs8Ed25519(rootPrivatePkcs8B64);
|
||||
@@ -1060,7 +1067,7 @@ export async function updateShineUserPdaOnSolana({
|
||||
const clientKeypair = solana.Keypair.fromSeed(clientSeed32);
|
||||
|
||||
const lastBlockStateBytes = buildLastBlockStateBytes(
|
||||
cleanLogin,
|
||||
current.login,
|
||||
currentBch.blockchainName,
|
||||
effectiveLastNum,
|
||||
effectiveLastHash,
|
||||
@@ -1085,7 +1092,10 @@ export async function updateShineUserPdaOnSolana({
|
||||
const updatedAtMs = BigInt(Date.now());
|
||||
const newPaid = currentBch.paidLimitBytes + addLimit;
|
||||
const newRecordNumber = current.recordNumber + 1;
|
||||
const prevHash = await sha256Bytes(serializeUnsignedRecordFromState(current));
|
||||
// Для prev_hash нужно хэшировать точную unsigned-часть текущей PDA,
|
||||
// а не пересобирать её из распарсенного состояния: иначе можно получить
|
||||
// несовпадение байт и InvalidPrevHash в on-chain программе.
|
||||
const prevHash = await sha256Bytes(current.unsignedBytes || serializeUnsignedRecordFromState(current));
|
||||
|
||||
const nextServerProfile = serverProfile
|
||||
? {
|
||||
@@ -1097,7 +1107,7 @@ export async function updateShineUserPdaOnSolana({
|
||||
: current.serverProfile;
|
||||
|
||||
const nextState = createPdaState({
|
||||
login: cleanLogin,
|
||||
login: current.login,
|
||||
createdAtMs: current.createdAtMs,
|
||||
updatedAtMs,
|
||||
recordNumber: newRecordNumber,
|
||||
@@ -1113,7 +1123,7 @@ export async function updateShineUserPdaOnSolana({
|
||||
lastBlockNumber: effectiveLastNum,
|
||||
lastBlockHash: effectiveLastHash,
|
||||
lastBlockSignature: lastBlockSig64,
|
||||
arweaveTxId: currentBch.arweaveTxId,
|
||||
arweaveTxId: effectiveArweaveTxId,
|
||||
}),
|
||||
isServer: Boolean(nextServerProfile),
|
||||
addressFormatType: nextServerProfile?.addressFormatType ?? 0,
|
||||
@@ -1131,7 +1141,7 @@ export async function updateShineUserPdaOnSolana({
|
||||
const rootSig64 = await signBytes(rootPriv, unsignedNextHash);
|
||||
|
||||
const ixData = serializeUpdateUserPdaArgs({
|
||||
login: cleanLogin,
|
||||
login: current.login,
|
||||
recoveryKey32: current.recoveryKey,
|
||||
rootKey32: current.rootKey,
|
||||
createdAtMs: current.createdAtMs,
|
||||
@@ -1146,7 +1156,7 @@ export async function updateShineUserPdaOnSolana({
|
||||
lastBlockNumber: effectiveLastNum,
|
||||
lastBlockHash32: effectiveLastHash,
|
||||
lastBlockSignature64: lastBlockSig64,
|
||||
arweaveTxId: currentBch.arweaveTxId,
|
||||
arweaveTxId: effectiveArweaveTxId,
|
||||
isServer: nextState.isServer,
|
||||
addressFormatType: nextState.addressFormatType,
|
||||
addressFormatVersion: nextState.addressFormatVersion,
|
||||
@@ -1205,6 +1215,7 @@ export async function updateShineUserPdaOnSolana({
|
||||
leftBytes: newPaid > effectiveUsed ? (newPaid - effectiveUsed) : 0n,
|
||||
lastBlockNumber: effectiveLastNum,
|
||||
lastBlockHashHex: Array.from(effectiveLastHash).map((x) => x.toString(16).padStart(2, '0')).join(''),
|
||||
arweaveTxId: effectiveArweaveTxId,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user