SHA256
Миграция PDA на client.key
This commit is contained in:
@@ -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 = {
|
||||
|
||||
@@ -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) {
|
||||
@@ -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 || ''),
|
||||
},
|
||||
|
||||
@@ -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: адрес пустой');
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user