Миграция PDA на client.key

This commit is contained in:
AidarKC
2026-06-22 21:57:09 +04:00
parent ba348dafb3
commit 5c92b6a734
133 changed files with 941 additions and 30531 deletions
+11 -11
View File
@@ -1,6 +1,6 @@
import { loadEncryptedUserSecrets, updateEncryptedUserSecrets } from './key-vault.js';
import { extractDeviceKey32FromStoredValue } from './device-key-utils.js';
import { deriveArweaveWalletFromDeviceKey32 } from './sawd-v1.js';
import { extractClientKey32FromStoredValue } from './client-key-utils.js';
import { deriveArweaveWalletFromClientKey32 } from './sawd-v1.js';
const DEFAULT_ARWEAVE_GATEWAY = 'https://arweave.net';
const AR_TOPUP_URL = 'https://changenow.io/exchange?from=usd&to=ar&amount=10&fiatMode=true';
@@ -102,11 +102,11 @@ function safeStatus(onStatus, text) {
}
}
export async function getArweaveWalletFromStoredDeviceKey({ login, storagePwd, onStatus } = {}) {
export async function getArweaveWalletFromStoredClientKey({ login, storagePwd, onStatus } = {}) {
const cleanLogin = String(login || '').trim();
const cleanPwd = String(storagePwd || '').trim();
if (!cleanLogin || !cleanPwd) {
throw new Error('Нет активной сессии для доступа к wallet.key');
throw new Error('Нет активной сессии для доступа к client.key');
}
const secrets = await loadEncryptedUserSecrets(cleanLogin, cleanPwd);
@@ -116,19 +116,19 @@ export async function getArweaveWalletFromStoredDeviceKey({ login, storagePwd, o
return cached;
}
safeStatus(onStatus, 'Сейчас мы впервые получаем Arweave-кошелёк из вашего device key. Это может занять немного времени.');
safeStatus(onStatus, 'Сейчас мы впервые получаем Arweave-кошелёк из вашего client key. Это может занять немного времени.');
const storedDeviceKey = String(secrets?.deviceKey || '').trim();
if (!storedDeviceKey) {
throw new Error('На устройстве не найден device.key (wallet.key)');
const storedClientKey = String(secrets?.clientKey || '').trim();
if (!storedClientKey) {
throw new Error('На устройстве не найден client.key');
}
const deviceKey32 = extractDeviceKey32FromStoredValue(storedDeviceKey);
const clientKey32 = extractClientKey32FromStoredValue(storedClientKey);
let wallet;
try {
wallet = await deriveArweaveWalletFromDeviceKey32(deviceKey32);
wallet = await deriveArweaveWalletFromClientKey32(clientKey32);
} finally {
deviceKey32.fill(0);
clientKey32.fill(0);
}
const cachedWallet = {
+48 -39
View File
@@ -809,22 +809,27 @@ export class AuthService {
});
if (isCancelled && isCancelled()) throw new Error('DERIVE_CANCELLED');
if (onProgress) onProgress({ percent: 94, stage: 'derive', message: 'Вычисление root key...' });
if (onProgress) onProgress({ percent: 93, stage: 'derive', message: 'Вычисление recovery key...' });
const recoveryPair = await deriveEd25519FromMasterSecret(masterSecret, 'recovery.key');
if (isCancelled && isCancelled()) throw new Error('DERIVE_CANCELLED');
if (onProgress) onProgress({ percent: 95, stage: 'derive', message: 'Вычисление root key...' });
const rootPair = await deriveEd25519FromMasterSecret(masterSecret, 'root.key');
if (isCancelled && isCancelled()) throw new Error('DERIVE_CANCELLED');
if (onProgress) onProgress({ percent: 96, stage: 'derive', message: 'Вычисление blockchain key...' });
const blockchainPair = await deriveEd25519FromMasterSecret(masterSecret, 'bch.key');
if (onProgress) onProgress({ percent: 97, stage: 'derive', message: 'Вычисление blockchain key...' });
const blockchainPair = await deriveEd25519FromMasterSecret(masterSecret, 'blockchain.key');
if (isCancelled && isCancelled()) throw new Error('DERIVE_CANCELLED');
if (onProgress) onProgress({ percent: 98, stage: 'derive', message: 'Вычисление device key...' });
const devicePair = await deriveEd25519FromMasterSecret(masterSecret, 'dev.key');
const result = {
masterSecretB64: bytesToBase64(masterSecret),
rootPair,
blockchainPair,
devicePair,
};
if (onProgress) onProgress({ percent: 99, stage: 'derive', message: 'Вычисление client key...' });
const clientPair = await deriveEd25519FromMasterSecret(masterSecret, 'client.key');
const result = {
masterSecretB64: bytesToBase64(masterSecret),
recoveryPair,
rootPair,
blockchainPair,
clientPair,
};
this.passwordKeyBundleCache.set(cacheKey, result);
if (onProgress) onProgress({ percent: 100, stage: 'done', message: 'Ключи сгенерированы.' });
return result;
@@ -839,6 +844,8 @@ export class AuthService {
async createAuthSession(login, keyBundle) {
const cleanLogin = (login || '').trim();
if (!cleanLogin) throw new Error('Введите логин');
const clientPair = keyBundle?.clientPair;
if (!clientPair) throw new Error('createAuthSession: не передан clientPair');
const sessionPair = await generateEd25519Pair();
const sessionKeyPub = await exportEd25519PublicKeyB64(sessionPair.publicKey);
@@ -853,7 +860,7 @@ export class AuthService {
const timeMs = Date.now();
const preimage = `AUTH_CREATE_SESSION:${cleanLogin}:${sessionKey}:${storagePwd}:${timeMs}:${authNonce}`;
const signatureB64 = await signBase64(keyBundle.devicePair.privateKey, preimage);
const signatureB64 = await signBase64(clientPair.privateKey, preimage);
const createResp = await this.ws.request('CreateAuthSession', {
login: cleanLogin,
@@ -861,7 +868,7 @@ export class AuthService {
sessionKey,
timeMs,
authNonce,
deviceKey: keyBundle.devicePair.publicKeyB64,
clientKey: clientPair.publicKeyB64,
signatureB64,
sessionType: SESSION_TYPE_CLIENT,
clientPlatform: makeClientPlatform(),
@@ -892,13 +899,14 @@ export class AuthService {
if (!isFree) throw new Error('Этот логин уже занят');
const keyBundle = await this.derivePasswordKeyBundle(cleanLogin, password);
const clientPair = keyBundle?.clientPair;
const addResp = await this.ws.request('AddUser', {
login: cleanLogin,
blockchainName: `${cleanLogin}-${BCH_SUFFIX}`,
solanaKey: keyBundle.devicePair.publicKeyB64,
solanaKey: clientPair.publicKeyB64,
blockchainKey: keyBundle.blockchainPair.publicKeyB64,
deviceKey: keyBundle.devicePair.publicKeyB64,
clientKey: clientPair.publicKeyB64,
bchLimit: 1000000,
});
if (addResp.status !== 200) throw opError('AddUser', addResp);
@@ -910,13 +918,14 @@ export class AuthService {
async registerUserWithKeyBundle(login, keyBundle) {
const cleanLogin = (login || '').trim();
if (!cleanLogin) throw new Error('Введите логин');
const clientPair = keyBundle?.clientPair;
const addResp = await this.ws.request('AddUser', {
login: cleanLogin,
blockchainName: `${cleanLogin}-${BCH_SUFFIX}`,
solanaKey: keyBundle.devicePair.publicKeyB64,
solanaKey: clientPair.publicKeyB64,
blockchainKey: keyBundle.blockchainPair.publicKeyB64,
deviceKey: keyBundle.devicePair.publicKeyB64,
clientKey: clientPair.publicKeyB64,
bchLimit: 1000000,
});
if (addResp.status !== 200) throw opError('AddUser', addResp);
@@ -937,13 +946,13 @@ export class AuthService {
async createSessionFromImportedSecrets(login, secrets) {
const cleanLogin = (login || '').trim();
if (!cleanLogin) throw new Error('В QR-коде нет логина');
const deviceKey = String(secrets?.deviceKey || '').trim();
if (!deviceKey) throw new Error('В QR-коде нет device key для входа');
const clientKey = String(secrets?.clientKey || secrets?.clientKey || '').trim();
if (!clientKey) throw new Error('В QR-коде нет client key для входа');
const privateKey = await importPkcs8Ed25519(deviceKey);
const publicKeyB64 = await publicKeyB64FromPkcs8Ed25519(deviceKey);
const privateKey = await importPkcs8Ed25519(clientKey);
const publicKeyB64 = await publicKeyB64FromPkcs8Ed25519(clientKey);
const session = await this.createAuthSession(cleanLogin, {
devicePair: {
clientPair: {
privateKey,
publicKeyB64,
},
@@ -951,9 +960,9 @@ export class AuthService {
return session;
}
async createDelegatedSessionWithDeviceKey({
async createDelegatedSessionWithClientKey({
login,
devicePrivPkcs8,
clientPrivPkcs8,
sessionKey,
sessionType = SESSION_TYPE_WALLET,
clientPlatform = 'Delegated session',
@@ -961,13 +970,13 @@ export class AuthService {
}) {
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 cleanClientPriv = String(clientPrivPkcs8 || '').trim();
if (!cleanLogin) throw new Error('createDelegatedSessionWithClientKey: пустой login');
if (!cleanSessionKey) throw new Error('createDelegatedSessionWithClientKey: пустой sessionKey');
if (!cleanClientPriv) throw new Error('createDelegatedSessionWithClientKey: пустой client private key');
const devicePrivateKey = await importPkcs8Ed25519(cleanDevicePriv);
const devicePublicKeyB64 = await publicKeyB64FromPkcs8Ed25519(cleanDevicePriv);
const clientPrivateKey = await importPkcs8Ed25519(cleanClientPriv);
const clientPublicKeyB64 = await publicKeyB64FromPkcs8Ed25519(cleanClientPriv);
const storagePwd = randomBase64(32);
const tempAuth = new AuthService(this.serverUrl);
@@ -980,7 +989,7 @@ export class AuthService {
const timeMs = Date.now();
const preimage = `AUTH_CREATE_SESSION:${cleanLogin}:${cleanSessionKey}:${storagePwd}:${timeMs}:${authNonce}`;
const signatureB64 = await signBase64(devicePrivateKey, preimage);
const signatureB64 = await signBase64(clientPrivateKey, preimage);
const createResp = await tempAuth.ws.request('CreateAuthSession', {
login: cleanLogin,
@@ -988,7 +997,7 @@ export class AuthService {
sessionKey: cleanSessionKey,
timeMs,
authNonce,
deviceKey: devicePublicKeyB64,
clientKey: clientPublicKeyB64,
signatureB64,
sessionType: Number(sessionType) || SESSION_TYPE_WALLET,
clientPlatform: String(clientPlatform || '').trim() || 'Delegated session',
@@ -1025,7 +1034,7 @@ export class AuthService {
const secrets = {
...currentSecrets,
deviceKey: keyBundle.devicePair.privatePkcs8B64,
clientKey: keyBundle.clientPair.privatePkcs8B64,
};
if (saveOptions.saveRoot) secrets.rootKey = keyBundle.rootPair.privatePkcs8B64;
if (saveOptions.saveBlockchain) secrets.blockchainKey = keyBundle.blockchainPair.privatePkcs8B64;
@@ -1898,9 +1907,9 @@ export class AuthService {
if (!storagePwd) throw new Error('Не передан storagePwd для подписи');
const secrets = await loadEncryptedUserSecrets(cleanFromLogin, storagePwd);
const devicePriv = secrets?.deviceKey;
if (!devicePriv) throw new Error('Не найден приватный deviceKey');
const privateKey = await importPkcs8Ed25519(devicePriv);
const clientPriv = secrets?.clientKey || secrets?.clientKey;
if (!clientPriv) throw new Error('Не найден приватный clientKey');
const privateKey = await importPkcs8Ed25519(clientPriv);
const toBytes = ensureAsciiBytes(cleanToLogin, 'toLogin');
const fromBytes = ensureAsciiBytes(cleanFromLogin, 'fromLogin');
@@ -1941,9 +1950,9 @@ export class AuthService {
}
const secrets = await loadEncryptedUserSecrets(cleanFromLogin, storagePwd);
const devicePriv = secrets?.deviceKey;
if (!devicePriv) throw new Error('Не найден приватный deviceKey');
const privateKey = await importPkcs8Ed25519(devicePriv);
const clientPriv = secrets?.clientKey || secrets?.clientKey;
if (!clientPriv) throw new Error('Не найден приватный clientKey');
const privateKey = await importPkcs8Ed25519(clientPriv);
const toBytes = ensureAsciiBytes(cleanToLogin, 'toLogin');
const fromBytes = ensureAsciiBytes(cleanFromLogin, 'fromLogin');
@@ -18,14 +18,14 @@ function parseKeypairJson64(raw) {
if (!isByteArrayLike(parsed)) return null;
const asArray = Array.from(parsed);
if (asArray.length < 32) {
throw new Error('Некорректный JSON ключ device.key: ожидалось минимум 32 байта');
throw new Error('Некорректный JSON ключ client.key: ожидалось минимум 32 байта');
}
const out = new Uint8Array(asArray.length);
for (let i = 0; i < asArray.length; i += 1) {
const n = Number(asArray[i]);
if (!Number.isInteger(n) || n < 0 || n > 255) {
throw new Error('Некорректный JSON ключ device.key: найдены не-байтовые значения');
throw new Error('Некорректный JSON ключ client.key: найдены не-байтовые значения');
}
out[i] = n;
}
@@ -34,13 +34,13 @@ function parseKeypairJson64(raw) {
export function extractSeed32FromPkcs8B64(pkcs8B64) {
const bytes = base64ToBytes(String(pkcs8B64 || '').trim());
if (bytes.length < 32) throw new Error('Некорректный PKCS8 ключ device.key');
if (bytes.length < 32) throw new Error('Некорректный PKCS8 ключ client.key');
return bytes.slice(bytes.length - 32);
}
export function extractDeviceKey32FromStoredValue(storedDeviceKey) {
const raw = String(storedDeviceKey || '').trim();
if (!raw) throw new Error('Пустой device.key');
export function extractClientKey32FromStoredValue(storedClientKey) {
const raw = String(storedClientKey || '').trim();
if (!raw) throw new Error('Пустой client.key');
const jsonBytes = parseKeypairJson64(raw);
if (jsonBytes) {
+14 -2
View File
@@ -1,6 +1,7 @@
const encoder = new TextEncoder();
const WEB_CRYPTO_REQUIRED_MESSAGE = 'Регистрация и подпись блоков требуют WebCrypto (crypto.subtle). Откройте приложение через HTTPS или localhost в современном браузере и повторите попытку.';
import { argon2idAsync } from 'https://esm.sh/@noble/hashes@1.8.0/argon2.js';
const SHINE_KEY_DERIVATION_PREFIX = 'SHiNE-key';
function getCryptoApi() {
const api = globalThis.crypto;
@@ -205,8 +206,19 @@ export async function deriveEd25519FromMasterSecret(masterSecret32, suffix) {
if (secretBytes.length !== 32) {
throw new Error('Master secret должен быть длиной 32 байта');
}
const material = `${bytesToBase64(secretBytes)}|${String(suffix || '')}`;
const seed = await sha256Text(material);
const suffixBytes = utf8Bytes(String(suffix || ''));
const material = new Uint8Array(
SHINE_KEY_DERIVATION_PREFIX.length + 1 + secretBytes.length + 1 + suffixBytes.length,
);
let offset = 0;
material.set(utf8Bytes(SHINE_KEY_DERIVATION_PREFIX), offset);
offset += SHINE_KEY_DERIVATION_PREFIX.length;
material[offset++] = 0;
material.set(secretBytes, offset);
offset += secretBytes.length;
material[offset++] = 0;
material.set(suffixBytes, offset);
const seed = await sha256Bytes(material);
const pkcs8 = ed25519Pkcs8FromSeed(seed);
const subtle = getSubtleApi();
const privateKey = await subtle.importKey('pkcs8', pkcs8, { name: 'Ed25519' }, true, ['sign']);
@@ -71,7 +71,7 @@ async function importAesKeyFromSharedSecret(sharedSecretBytes) {
function normalizeKeys(keys = {}) {
return {
deviceKey: String(keys?.deviceKey || '').trim(),
clientKey: String(keys?.clientKey || '').trim(),
blockchainKey: String(keys?.blockchainKey || '').trim(),
rootKey: String(keys?.rootKey || '').trim(),
};
@@ -30,7 +30,7 @@ export function keyLabel(id) {
export function describeTransferKeys(keys = {}) {
const out = [];
if (keys.deviceKey) out.push('device');
if (keys.clientKey) out.push('device');
if (keys.blockchainKey) out.push('blockchain');
if (keys.rootKey) out.push('root');
return out;
@@ -42,7 +42,7 @@ export function makeKeyTransferText({ login, keys }) {
type: 'shine-key-transfer',
login: String(login || '').trim(),
keys: {
deviceKey: String(keys?.deviceKey || ''),
clientKey: String(keys?.clientKey || ''),
blockchainKey: String(keys?.blockchainKey || ''),
rootKey: String(keys?.rootKey || ''),
},
@@ -65,13 +65,13 @@ export function parseKeyTransferText(text) {
const login = String(payload.login || '').trim();
if (!login) throw new Error('В QR-коде нет логина');
const keys = payload.keys && typeof payload.keys === 'object' ? payload.keys : {};
if (!keys.deviceKey && !keys.blockchainKey && !keys.rootKey) {
if (!keys.clientKey && !keys.blockchainKey && !keys.rootKey) {
throw new Error('В QR-коде нет ключей');
}
return {
login,
keys: {
deviceKey: String(keys.deviceKey || ''),
clientKey: String(keys.clientKey || ''),
blockchainKey: String(keys.blockchainKey || ''),
rootKey: String(keys.rootKey || ''),
},
+14 -14
View File
@@ -323,15 +323,15 @@ function toJwkB64(value) {
return base64UrlEncode(bigIntToUnsignedBytes(value));
}
async function deriveArweaveWalletParts(deviceKey32) {
if (!(deviceKey32 instanceof Uint8Array)) {
throw new Error('SAWD-v1: deviceKey32 должен быть Uint8Array');
async function deriveArweaveWalletParts(clientKey32) {
if (!(clientKey32 instanceof Uint8Array)) {
throw new Error('SAWD-v1: clientKey32 должен быть Uint8Array');
}
if (deviceKey32.length !== 32) {
throw new Error('SAWD-v1: deviceKey32 должен быть ровно 32 байта');
if (clientKey32.length !== 32) {
throw new Error('SAWD-v1: clientKey32 должен быть ровно 32 байта');
}
const masterSeed32 = await hmacSha256(MASTER_LABEL_UTF8, deviceKey32);
const masterSeed32 = await hmacSha256(MASTER_LABEL_UTF8, clientKey32);
const masterSeedKey = await importHmacKey(masterSeed32);
const pResult = await derivePrimeWithImportedKey(masterSeedKey, 'p');
@@ -381,8 +381,8 @@ async function deriveArweaveWalletParts(deviceKey32) {
};
}
export async function deriveArweaveWalletFromDeviceKey32(deviceKey32) {
const result = await deriveArweaveWalletParts(deviceKey32);
export async function deriveArweaveWalletFromClientKey32(clientKey32) {
const result = await deriveArweaveWalletParts(clientKey32);
return {
derivation: result.derivation,
jwk: result.jwk,
@@ -395,7 +395,7 @@ export async function selfTestSawdV1() {
const invalid = new Uint8Array(31);
let invalidFailed = false;
try {
await deriveArweaveWalletFromDeviceKey32(invalid);
await deriveArweaveWalletFromClientKey32(invalid);
} catch {
invalidFailed = true;
}
@@ -403,13 +403,13 @@ export async function selfTestSawdV1() {
throw new Error('SAWD-v1 self-test: длина != 32 должна приводить к ошибке');
}
const deviceKey = new Uint8Array(32);
for (let i = 0; i < deviceKey.length; i += 1) {
deviceKey[i] = i + 1;
const clientKey = new Uint8Array(32);
for (let i = 0; i < clientKey.length; i += 1) {
clientKey[i] = i + 1;
}
const first = await deriveArweaveWalletParts(deviceKey);
const second = await deriveArweaveWalletParts(deviceKey);
const first = await deriveArweaveWalletParts(clientKey);
const second = await deriveArweaveWalletParts(clientKey);
if (!first.address || !second.address) {
throw new Error('SAWD-v1 self-test: адрес пустой');
+54 -32
View File
@@ -1,5 +1,5 @@
import { base64ToBytes, importPkcs8Ed25519, sha256Bytes, signBytes } from './crypto-utils.js';
import { extractSeed32FromPkcs8B64 } from './device-key-utils.js';
import { extractSeed32FromPkcs8B64 } from './client-key-utils.js';
import {
SHINE_LOGIN_GUARD_PROGRAM_ID,
SHINE_PAYMENTS_PROGRAM_ID,
@@ -16,8 +16,9 @@ const BLOCKCHAIN_TYPE_MAIN_USER = 1;
const ED25519_PROGRAM_ID = 'Ed25519SigVerify111111111111111111111111111';
const SYSVAR_INSTRUCTIONS_ID = 'Sysvar1nstructions1111111111111111111111111';
const BLOCK_TYPE_RECOVERY_KEY = 0;
const BLOCK_TYPE_ROOT_KEY = 1;
const BLOCK_TYPE_DEVICE_KEY = 2;
const BLOCK_TYPE_CLIENT_KEY = 2;
const BLOCK_TYPE_BLOCKCHAIN_REGISTRY = 3;
const BLOCK_TYPE_SERVER_PROFILE = 30;
const BLOCK_TYPE_ACCESS_SERVERS = 40;
@@ -176,10 +177,11 @@ function serializeCreateUserPdaArgs(args) {
const buf = [];
buf.push(3);
pushStrU8(buf, args.login);
for (const x of args.recoveryKey32) buf.push(x);
for (const x of args.rootKey32) buf.push(x);
pushU64LE(buf, args.createdAtMs);
pushU64LE(buf, BigInt(args.additionalLimitBytes || 0n));
for (const x of args.deviceKey32) buf.push(x);
for (const x of args.clientKey32) buf.push(x);
for (const x of args.blockchainPublicKey32) buf.push(x);
pushStrU8(buf, args.blockchainName);
pushU64LE(buf, args.usedBytes);
@@ -218,13 +220,14 @@ function serializeUpdateUserPdaArgs(args) {
const buf = [];
buf.push(4);
pushStrU8(buf, args.login);
for (const x of args.recoveryKey32) buf.push(x);
for (const x of args.rootKey32) buf.push(x);
pushU64LE(buf, args.createdAtMs);
pushU64LE(buf, args.updatedAtMs);
pushU32LE(buf, args.version);
for (const x of args.prevHash32) buf.push(x);
pushU64LE(buf, args.additionalLimitBytes);
for (const x of args.deviceKey32) buf.push(x);
for (const x of args.clientKey32) buf.push(x);
for (const x of args.blockchainPublicKey32) buf.push(x);
pushStrU8(buf, args.blockchainName);
pushU64LE(buf, args.usedBytes);
@@ -288,8 +291,9 @@ function createPdaState({
updatedAtMs,
recordNumber,
prevRecordHash,
recoveryKey,
rootKey,
deviceKey,
clientKey,
blockchain,
isServer,
addressFormatType,
@@ -313,8 +317,10 @@ function createPdaState({
recordNumber,
prevRecordHash,
login,
recoveryKey,
rootKey,
deviceKey,
clientKey,
clientKey: clientKey,
blockchain,
isServer: Boolean(isServer),
serverProfile,
@@ -373,8 +379,9 @@ export function parseShineUserPda(dataBytes) {
const login = reader.readStrU8();
const blocksCount = reader.readU8();
let recoveryKey = null;
let rootKey = null;
let deviceKey = null;
let clientKey = null;
let blockchain = null;
let isServer = false;
let addressFormatType = 0;
@@ -390,12 +397,16 @@ export function parseShineUserPda(dataBytes) {
const blockType = reader.readU8();
reader.readU8();
if (blockType === BLOCK_TYPE_RECOVERY_KEY) {
recoveryKey = reader.readBytes(32);
continue;
}
if (blockType === BLOCK_TYPE_ROOT_KEY) {
rootKey = reader.readBytes(32);
continue;
}
if (blockType === BLOCK_TYPE_DEVICE_KEY) {
deviceKey = reader.readBytes(32);
if (blockType === BLOCK_TYPE_CLIENT_KEY) {
clientKey = reader.readBytes(32);
continue;
}
if (blockType === BLOCK_TYPE_BLOCKCHAIN_REGISTRY) {
@@ -466,7 +477,7 @@ export function parseShineUserPda(dataBytes) {
throw new Error(`Неизвестный блок PDA: ${blockType}`);
}
if (!rootKey || !deviceKey || !blockchain) {
if (!recoveryKey || !rootKey || !clientKey || !blockchain) {
throw new Error('В PDA отсутствуют обязательные блоки');
}
@@ -478,8 +489,9 @@ export function parseShineUserPda(dataBytes) {
updatedAtMs,
recordNumber,
prevRecordHash,
recoveryKey,
rootKey,
deviceKey,
clientKey,
blockchain,
isServer,
addressFormatType,
@@ -507,8 +519,9 @@ export function serializeUnsignedRecordFromState(stateLike) {
updatedAtMs: stateLike.updatedAtMs,
recordNumber: stateLike.recordNumber,
prevRecordHash: stateLike.prevRecordHash,
recoveryKey: stateLike.recoveryKey,
rootKey: stateLike.rootKey,
deviceKey: stateLike.deviceKey,
clientKey: stateLike.clientKey ?? stateLike.clientKey,
blockchain: stateLike.blockchain,
isServer: stateLike.isServer,
addressFormatType: stateLike.addressFormatType ?? stateLike.serverProfile?.addressFormatType,
@@ -527,13 +540,16 @@ export function serializeUnsignedRecordFromState(stateLike) {
pushU32LE(buf, state.recordNumber);
for (const x of state.prevRecordHash) buf.push(x);
pushStrU8(buf, state.login);
buf.push(state.isServer ? 7 : 6);
buf.push(state.isServer ? 8 : 7);
buf.push(BLOCK_TYPE_RECOVERY_KEY, 0);
for (const x of state.recoveryKey) buf.push(x);
buf.push(BLOCK_TYPE_ROOT_KEY, 0);
for (const x of state.rootKey) buf.push(x);
buf.push(BLOCK_TYPE_DEVICE_KEY, 0);
for (const x of state.deviceKey) buf.push(x);
buf.push(BLOCK_TYPE_CLIENT_KEY, 0);
for (const x of state.clientKey) buf.push(x);
buf.push(BLOCK_TYPE_BLOCKCHAIN_REGISTRY, 0, 1, state.blockchain.blockchainType);
pushStrU8(buf, state.blockchain.blockchainName);
@@ -656,14 +672,15 @@ async function buildCreateContext({ login, keyBundle, solanaEndpoint }) {
const [economyConfigPda] = solana.PublicKey.findProgramAddressSync([enc.encode(SHINE_USERS_ECONOMY_CONFIG_SEED)], usersProgram);
const [inflowVault] = solana.PublicKey.findProgramAddressSync([enc.encode(SHINE_PAYMENTS_INFLOW_VAULT_SEED)], paymentsProgram);
const recoveryKey32 = base64ToBytes(keyBundle.recoveryPair.publicKeyB64);
const rootKey32 = base64ToBytes(keyBundle.rootPair.publicKeyB64);
const blockchainKey32 = base64ToBytes(keyBundle.blockchainPair.publicKeyB64);
const deviceKey32 = base64ToBytes(keyBundle.devicePair.publicKeyB64);
const clientKey32 = base64ToBytes(keyBundle.clientPair.publicKeyB64);
const rootPrivKey = await importPkcs8Ed25519(keyBundle.rootPair.privatePkcs8B64);
const bchPrivKey = await importPkcs8Ed25519(keyBundle.blockchainPair.privatePkcs8B64);
const deviceSeed32 = extractSeed32FromPkcs8B64(keyBundle.devicePair.privatePkcs8B64);
const deviceKeypair = solana.Keypair.fromSeed(deviceSeed32);
const clientSeed32 = extractSeed32FromPkcs8B64(keyBundle.clientPair.privatePkcs8B64);
const clientKeypair = solana.Keypair.fromSeed(clientSeed32);
return {
cleanLogin,
@@ -678,12 +695,13 @@ async function buildCreateContext({ login, keyBundle, solanaEndpoint }) {
userPda,
economyConfigPda,
inflowVault,
recoveryKey32,
rootKey32,
blockchainKey32,
deviceKey32,
clientKey32,
rootPrivKey,
bchPrivKey,
deviceKeypair,
clientKeypair,
};
}
@@ -720,8 +738,9 @@ async function createShineUserPdaOnSolana({
updatedAtMs: createdAtMs,
recordNumber: 0,
prevRecordHash: zeroHash32,
recoveryKey: ctx.recoveryKey32,
rootKey: ctx.rootKey32,
deviceKey: ctx.deviceKey32,
clientKey: ctx.clientKey32,
blockchain: createBlockchainState({
blockchainName,
blockchainPublicKey: ctx.blockchainKey32,
@@ -749,10 +768,11 @@ async function createShineUserPdaOnSolana({
const ixData = serializeCreateUserPdaArgs({
login: cleanLogin,
recoveryKey32: ctx.recoveryKey32,
rootKey32: ctx.rootKey32,
createdAtMs,
additionalLimitBytes: 0n,
deviceKey32: ctx.deviceKey32,
clientKey32: ctx.clientKey32,
blockchainPublicKey32: ctx.blockchainKey32,
blockchainName,
usedBytes: 0n,
@@ -785,7 +805,7 @@ async function createShineUserPdaOnSolana({
const createIx = new ctx.solana.TransactionInstruction({
programId: ctx.usersProgram,
keys: [
{ pubkey: ctx.deviceKeypair.publicKey, isSigner: true, isWritable: true },
{ pubkey: ctx.clientKeypair.publicKey, isSigner: true, isWritable: true },
{ pubkey: ctx.userPda, isSigner: false, isWritable: true },
{ pubkey: ctx.solana.SystemProgram.programId, isSigner: false, isWritable: false },
{ pubkey: ctx.inflowVault, isSigner: false, isWritable: true },
@@ -799,7 +819,7 @@ async function createShineUserPdaOnSolana({
const signature = await ctx.solana.sendAndConfirmTransaction(
ctx.connection,
new ctx.solana.Transaction().add(ed25519RootIx, ed25519BchIx, createIx),
[ctx.deviceKeypair],
[ctx.clientKeypair],
{ commitment: 'confirmed' },
);
@@ -848,7 +868,7 @@ export async function updateShineUserPdaOnSolana({
login,
solanaEndpoint,
rootPrivatePkcs8B64,
devicePrivatePkcs8B64,
clientPrivatePkcs8B64,
blockchainPrivatePkcs8B64,
additionalLimitBytes = 0n,
nextUsedBytes,
@@ -882,8 +902,8 @@ export async function updateShineUserPdaOnSolana({
const [userPda] = solana.PublicKey.findProgramAddressSync([enc.encode(SHINE_USERS_USER_PDA_SEED_PREFIX), enc.encode(cleanLogin)], usersProgram);
const [economyPda] = solana.PublicKey.findProgramAddressSync([enc.encode(SHINE_USERS_ECONOMY_CONFIG_SEED)], usersProgram);
const [inflowVault] = solana.PublicKey.findProgramAddressSync([enc.encode(SHINE_PAYMENTS_INFLOW_VAULT_SEED)], paymentsProgram);
const deviceSeed32 = extractSeed32FromPkcs8B64(devicePrivatePkcs8B64);
const deviceKeypair = solana.Keypair.fromSeed(deviceSeed32);
const clientSeed32 = extractSeed32FromPkcs8B64(clientPrivatePkcs8B64);
const clientKeypair = solana.Keypair.fromSeed(clientSeed32);
const lastBlockStateBytes = buildLastBlockStateBytes(
cleanLogin,
@@ -928,8 +948,9 @@ export async function updateShineUserPdaOnSolana({
updatedAtMs,
recordNumber: newRecordNumber,
prevRecordHash: prevHash,
recoveryKey: current.recoveryKey,
rootKey: current.rootKey,
deviceKey: current.deviceKey,
clientKey: current.clientKey ?? current.clientKey,
blockchain: createBlockchainState({
blockchainName: currentBch.blockchainName,
blockchainPublicKey: currentBch.blockchainPublicKey,
@@ -957,13 +978,14 @@ export async function updateShineUserPdaOnSolana({
const ixData = serializeUpdateUserPdaArgs({
login: cleanLogin,
recoveryKey32: current.recoveryKey,
rootKey32: current.rootKey,
createdAtMs: current.createdAtMs,
updatedAtMs,
version: newRecordNumber,
prevHash32: prevHash,
additionalLimitBytes: addLimit,
deviceKey32: current.deviceKey,
clientKey32: current.clientKey,
blockchainPublicKey32: currentBch.blockchainPublicKey,
blockchainName: currentBch.blockchainName,
usedBytes: effectiveUsed,
@@ -996,7 +1018,7 @@ export async function updateShineUserPdaOnSolana({
const updateIx = new solana.TransactionInstruction({
programId: usersProgram,
keys: [
{ pubkey: deviceKeypair.publicKey, isSigner: true, isWritable: true },
{ pubkey: clientKeypair.publicKey, isSigner: true, isWritable: true },
{ pubkey: userPda, isSigner: false, isWritable: true },
{ pubkey: solana.SystemProgram.programId, isSigner: false, isWritable: false },
{ pubkey: inflowVault, isSigner: false, isWritable: true },
@@ -1011,7 +1033,7 @@ export async function updateShineUserPdaOnSolana({
const signature = await solana.sendAndConfirmTransaction(
connection,
new solana.Transaction().add(computeIx, heapIx, edIxRoot, edIxBch, updateIx),
[deviceKeypair],
[clientKeypair],
{ commitment: 'confirmed' },
);
@@ -1040,7 +1062,7 @@ export async function updateServerOnSolana({
login,
solanaEndpoint,
rootPrivatePkcs8B64: keyBundle.rootPair.privatePkcs8B64,
devicePrivatePkcs8B64: keyBundle.devicePair.privatePkcs8B64,
clientPrivatePkcs8B64: keyBundle.clientPair.privatePkcs8B64,
serverProfile: {
addressFormatType,
addressFormatVersion,
@@ -1,4 +1,4 @@
import { extractDeviceKey32FromStoredValue } from './device-key-utils.js';
import { extractClientKey32FromStoredValue } from './client-key-utils.js';
import { loadEncryptedUserSecrets } from './key-vault.js';
import { SOLANA_ENDPOINT_DEFAULT } from '../solana-programs.js';
@@ -63,7 +63,7 @@ function decodeBase58(input) {
async function keypairFromPkcs8(pkcs8B64) {
const solana = await loadSolanaLib();
const seed32 = extractDeviceKey32FromStoredValue(pkcs8B64);
const seed32 = extractClientKey32FromStoredValue(pkcs8B64);
return solana.Keypair.fromSeed(seed32);
}
@@ -95,22 +95,22 @@ export async function createSolanaWalletFromPrivateBase58(privateKey32Base58) {
};
}
export async function getWalletFromStoredDeviceKey({ login, storagePwd }) {
export async function getWalletFromStoredClientKey({ login, storagePwd }) {
const cleanLogin = String(login || '').trim();
const cleanPwd = String(storagePwd || '').trim();
if (!cleanLogin || !cleanPwd) {
throw new Error('Нет активной сессии для доступа к wallet.key');
throw new Error('Нет активной сессии для доступа к client.key');
}
const secrets = await loadEncryptedUserSecrets(cleanLogin, cleanPwd);
const devicePrivate = String(secrets?.deviceKey || '').trim();
if (!devicePrivate) {
throw new Error('На устройстве не найден device.key (wallet.key)');
const clientPrivate = String(secrets?.clientKey || '').trim();
if (!clientPrivate) {
throw new Error('На устройстве не найден client.key');
}
const keypair = await keypairFromPkcs8(devicePrivate);
const keypair = await keypairFromPkcs8(clientPrivate);
return {
address: keypair.publicKey.toBase58(),
keypair,
devicePrivatePkcs8B64: devicePrivate,
clientPrivatePkcs8B64: clientPrivate,
};
}