SHA256
Аввив 2.0 - работает, заливка!!
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
const TE = new TextEncoder();
|
||||
export const ANS104_ED25519_SIGNATURE_TYPE = 2;
|
||||
|
||||
function concatBytes(...chunks) {
|
||||
const len = chunks.reduce((n, c) => n + c.length, 0);
|
||||
const out = new Uint8Array(len);
|
||||
let p = 0;
|
||||
for (const c of chunks) { out.set(c, p); p += c.length; }
|
||||
return out;
|
||||
}
|
||||
|
||||
function uint16LE(value) {
|
||||
const out = new Uint8Array(2);
|
||||
new DataView(out.buffer).setUint16(0, Number(value), true);
|
||||
return out;
|
||||
}
|
||||
|
||||
function uint64LE(value) {
|
||||
let v = BigInt(value);
|
||||
if (v < 0n) throw new Error('uint64LE: negative value');
|
||||
const out = new Uint8Array(8);
|
||||
for (let i = 0; i < 8; i++) { out[i] = Number(v & 0xffn); v >>= 8n; }
|
||||
return out;
|
||||
}
|
||||
|
||||
function encodeAvroLong(value) {
|
||||
let n = BigInt(value);
|
||||
// Avro zig-zag signed long.
|
||||
let u = n >= 0n ? (n << 1n) : ((-n << 1n) - 1n);
|
||||
const bytes = [];
|
||||
do {
|
||||
let b = Number(u & 0x7fn);
|
||||
u >>= 7n;
|
||||
if (u !== 0n) b |= 0x80;
|
||||
bytes.push(b);
|
||||
} while (u !== 0n);
|
||||
return Uint8Array.from(bytes);
|
||||
}
|
||||
|
||||
function encodeAvroString(value) {
|
||||
const bytes = TE.encode(String(value));
|
||||
return concatBytes(encodeAvroLong(bytes.length), bytes);
|
||||
}
|
||||
|
||||
/** Serialize ANS-104 tags as the Avro array used by arbundles/Turbo. */
|
||||
export function serializeAns104Tags(tags = []) {
|
||||
const normalized = tags.map(({ name, value }) => ({
|
||||
name: String(name ?? ''),
|
||||
value: String(value ?? ''),
|
||||
}));
|
||||
if (normalized.length > 128) throw new Error('ANS-104 supports at most 128 tags');
|
||||
for (const tag of normalized) {
|
||||
if (!tag.name || !tag.value) throw new Error('ANS-104 tag name/value must be non-empty');
|
||||
if (TE.encode(tag.name).length > 1024) throw new Error('ANS-104 tag name exceeds 1024 bytes');
|
||||
if (TE.encode(tag.value).length > 3072) throw new Error('ANS-104 tag value exceeds 3072 bytes');
|
||||
}
|
||||
if (normalized.length === 0) return Uint8Array.of(0);
|
||||
return concatBytes(
|
||||
encodeAvroLong(normalized.length),
|
||||
...normalized.flatMap((tag) => [encodeAvroString(tag.name), encodeAvroString(tag.value)]),
|
||||
Uint8Array.of(0),
|
||||
);
|
||||
}
|
||||
|
||||
async function digest(name, bytes) {
|
||||
return new Uint8Array(await crypto.subtle.digest(name, bytes));
|
||||
}
|
||||
|
||||
async function deepHashBlob(bytes) {
|
||||
const tag = TE.encode(`blob${bytes.length}`);
|
||||
return digest('SHA-384', concatBytes(await digest('SHA-384', tag), await digest('SHA-384', bytes)));
|
||||
}
|
||||
|
||||
async function deepHash(item) {
|
||||
if (item instanceof Uint8Array) return deepHashBlob(item);
|
||||
if (Array.isArray(item)) {
|
||||
let acc = await digest('SHA-384', TE.encode(`list${item.length}`));
|
||||
for (const child of item) {
|
||||
acc = await digest('SHA-384', concatBytes(acc, await deepHash(child)));
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
throw new Error('Unsupported ANS-104 deepHash item');
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the exact signing message used by current arbundles/Turbo DataItems.
|
||||
* SHiNE intentionally uses no target and no anchor.
|
||||
*/
|
||||
export async function ans104SigningMessage({ owner32, tags = [], data }) {
|
||||
if (!(owner32 instanceof Uint8Array) || owner32.length !== 32) throw new Error('ANS-104 owner must be 32 bytes');
|
||||
if (!(data instanceof Uint8Array)) throw new Error('ANS-104 data must be Uint8Array');
|
||||
const rawTags = serializeAns104Tags(tags);
|
||||
return deepHash([
|
||||
TE.encode('dataitem'),
|
||||
TE.encode('1'),
|
||||
TE.encode(String(ANS104_ED25519_SIGNATURE_TYPE)),
|
||||
owner32,
|
||||
new Uint8Array(0),
|
||||
new Uint8Array(0),
|
||||
rawTags,
|
||||
data,
|
||||
]);
|
||||
}
|
||||
|
||||
/** Create one complete serialized ANS-104 DataItem signed with Ed25519. */
|
||||
export async function createAns104DataItem({ owner32, privateKey, data, tags = [] }) {
|
||||
if (!(data instanceof Uint8Array)) throw new Error('ANS-104 data must be Uint8Array');
|
||||
const rawTags = serializeAns104Tags(tags);
|
||||
const signingMessage = await ans104SigningMessage({ owner32, tags, data });
|
||||
const signature = new Uint8Array(await crypto.subtle.sign('Ed25519', privateKey, signingMessage));
|
||||
if (signature.length !== 64) throw new Error(`Unexpected Ed25519 signature length: ${signature.length}`);
|
||||
|
||||
return concatBytes(
|
||||
uint16LE(ANS104_ED25519_SIGNATURE_TYPE),
|
||||
signature,
|
||||
owner32,
|
||||
Uint8Array.of(0), // target absent
|
||||
Uint8Array.of(0), // anchor absent
|
||||
uint64LE(tags.length),
|
||||
uint64LE(rawTags.length),
|
||||
rawTags,
|
||||
data,
|
||||
);
|
||||
}
|
||||
@@ -37,6 +37,7 @@ import {
|
||||
saveEncryptedUserSecrets,
|
||||
saveSessionMaterial,
|
||||
} from './key-vault.js';
|
||||
import { createAns104DataItem } from './ans104-data-item.js';
|
||||
import { defaultServerWs } from '../deploy-config.js';
|
||||
|
||||
const BCH_SUFFIX = '001';
|
||||
@@ -973,7 +974,7 @@ function buildBlockPreimage({ prevBlockHashHex, blockNumber, msgType, msgSubType
|
||||
const blockSize = 2 + 32 + 4 + 4 + 8 + 2 + 2 + 2 + body.length;
|
||||
|
||||
return concatBytes(
|
||||
int16Bytes(0),
|
||||
int16Bytes(1),
|
||||
prevHashBytes,
|
||||
int32Bytes(blockSize),
|
||||
int32Bytes(blockNumber),
|
||||
@@ -1077,14 +1078,6 @@ export class AuthService {
|
||||
return response.payload || {};
|
||||
}
|
||||
|
||||
async getArchiveBlockchainLocation(blockchainName) {
|
||||
const cleanBlockchainName = String(blockchainName || '').trim();
|
||||
if (!cleanBlockchainName) throw new Error('Не указано имя блокчейна');
|
||||
const response = await this.ws.request('GetArchiveBlockchainLocation', { blockchainName: cleanBlockchainName });
|
||||
if (response.status !== 200) throw opError('GetArchiveBlockchainLocation', response);
|
||||
return response.payload || response || {};
|
||||
}
|
||||
|
||||
async resolveLoginForAuth(login) {
|
||||
const cleanLogin = String(login || '').trim();
|
||||
if (!cleanLogin) throw new Error('Введите логин');
|
||||
@@ -1645,7 +1638,7 @@ export class AuthService {
|
||||
};
|
||||
}
|
||||
|
||||
async submitPreparedAddBlock({ login, storagePwd, blockchainName, blockNumber, prevBlockHash, preimage }) {
|
||||
async submitPreparedAddBlock({ login, storagePwd, blockchainName, blockNumber, prevBlockHash, preimage, ansTags }) {
|
||||
const cleanLogin = String(login || '').trim();
|
||||
const cleanBlockchainName = String(blockchainName || '').trim();
|
||||
const cleanPrevBlockHash = normalizeHex32(prevBlockHash, ZERO_HASH_HEX);
|
||||
@@ -1658,17 +1651,22 @@ export class AuthService {
|
||||
const blockchainPrivatePkcs8 = String(savedKeys?.blockchainKey || '').trim();
|
||||
if (blockchainPrivatePkcs8) {
|
||||
const privateKey = await importPkcs8Ed25519(blockchainPrivatePkcs8);
|
||||
const hash32 = await sha256Bytes(preimage);
|
||||
const signatureBytes = await signBytes(privateKey, hash32);
|
||||
const fullBlock = concatBytes(preimage, int16Bytes(0x0100), signatureBytes);
|
||||
const owner32 = base64ToBytes(await publicKeyB64FromPkcs8Ed25519(blockchainPrivatePkcs8));
|
||||
const dataItemBytes = await createAns104DataItem({
|
||||
owner32,
|
||||
privateKey,
|
||||
data: preimage,
|
||||
tags: Array.isArray(ansTags) && ansTags.length ? ansTags : [{ name: 'App', value: 'test5590' }],
|
||||
});
|
||||
return this.ws.request('AddBlock', {
|
||||
blockchainName: cleanBlockchainName,
|
||||
blockNumber: Number(blockNumber),
|
||||
prevBlockHash: cleanPrevBlockHash,
|
||||
blockBytesB64: bytesToBase64(fullBlock),
|
||||
blockBytesB64: bytesToBase64(dataItemBytes),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
const remoteSessionId = String(this.remoteAddBlockSessionId || '').trim();
|
||||
if (!remoteSessionId) {
|
||||
throw new Error('На устройстве нет blockchain key и не выбрана homeserver-сессия для remote AddBlock');
|
||||
@@ -1782,7 +1780,7 @@ export class AuthService {
|
||||
};
|
||||
}
|
||||
|
||||
async runAddBlockWithRetry({ login, storagePwd, resolveFreshState, buildPreimage }) {
|
||||
async runAddBlockWithRetry({ login, storagePwd, resolveFreshState, buildPreimage, ansTags }) {
|
||||
let freshState = await resolveFreshState();
|
||||
let blockchainName = String(freshState?.blockchainName || '').trim();
|
||||
if (!blockchainName) throw new Error('runAddBlockWithRetry: blockchainName is empty');
|
||||
@@ -1798,6 +1796,7 @@ export class AuthService {
|
||||
blockNumber,
|
||||
prevBlockHash,
|
||||
preimage,
|
||||
ansTags,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1824,7 +1823,7 @@ export class AuthService {
|
||||
};
|
||||
}
|
||||
|
||||
async addBlockSigned({ login, storagePwd, msgType, msgSubType, msgVersion = 1, bodyBytes }) {
|
||||
async addBlockSigned({ login, storagePwd, msgType, msgSubType, msgVersion = 1, bodyBytes, channelSlug = '' }) {
|
||||
const cleanLogin = (login || '').trim();
|
||||
if (!cleanLogin) throw new Error('Missing login for AddBlock');
|
||||
if (!storagePwd) throw new Error('Missing storagePwd for AddBlock signing');
|
||||
@@ -1844,10 +1843,15 @@ export class AuthService {
|
||||
return response.payload || {};
|
||||
}
|
||||
|
||||
const ansTags = [{ name: 'App', value: 'test5590' }];
|
||||
const cleanChannelSlug = String(channelSlug || '').trim();
|
||||
if (cleanChannelSlug) ansTags.push({ name: 'c', value: cleanChannelSlug });
|
||||
|
||||
const { response, blockchainName } = await this.runAddBlockWithRetry({
|
||||
login: cleanLogin,
|
||||
storagePwd,
|
||||
resolveFreshState: () => this.resolveFreshBlockchainCursor(cleanLogin),
|
||||
ansTags,
|
||||
buildPreimage: async ({ blockNumber, prevBlockHash }) => buildBlockPreimage({
|
||||
prevBlockHashHex: prevBlockHash,
|
||||
blockNumber,
|
||||
@@ -2039,12 +2043,14 @@ export class AuthService {
|
||||
if (!owner || !Number.isFinite(root) || root < 0) throw new Error('Invalid channel selector');
|
||||
if (owner !== blockchainName) throw new Error('Repost is allowed only to your own channels');
|
||||
|
||||
let channelSlug = toCanonicalChannelSlug(String(selector?.channelName || selector?.slug || ''));
|
||||
let rootHashHex = normalizeHex32(selector?.channelRootBlockHash, ZERO64);
|
||||
if (rootHashHex === ZERO64) {
|
||||
if (rootHashHex === ZERO64 || !channelSlug) {
|
||||
const ownChannels = await this.listOwnChannelsForBlockchain(cleanLogin, blockchainName);
|
||||
const rootChannel = ownChannels.find((item) => item.rootBlockNumber === root);
|
||||
if (!rootChannel) throw new Error('Channel root not found');
|
||||
rootHashHex = normalizeHex32(rootChannel.rootBlockHash, ZERO64);
|
||||
if (rootHashHex === ZERO64) rootHashHex = normalizeHex32(rootChannel.rootBlockHash, ZERO64);
|
||||
if (!channelSlug) channelSlug = toCanonicalChannelSlug(rootChannel.channelName);
|
||||
}
|
||||
|
||||
let prevLineNumber = root;
|
||||
@@ -2089,6 +2095,7 @@ export class AuthService {
|
||||
msgSubType: MSG_SUBTYPE_TEXT_REPOST,
|
||||
msgVersion: 1,
|
||||
bodyBytes,
|
||||
channelSlug,
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -2115,11 +2122,15 @@ export class AuthService {
|
||||
throw new Error('Invalid channel selector for edit');
|
||||
}
|
||||
|
||||
let channelSlug = toCanonicalChannelSlug(String(selector?.channelName || selector?.slug || ''));
|
||||
let rootHashHex = normalizeHex32(selector?.channelRootBlockHash, ZERO64);
|
||||
if (rootHashHex === ZERO64) {
|
||||
if (rootHashHex === ZERO64 || !channelSlug) {
|
||||
const ownChannels = await this.listOwnChannelsForBlockchain(cleanLogin, ownerBlockchainName);
|
||||
const rootChannel = ownChannels.find((item) => item.rootBlockNumber === lineCode);
|
||||
if (rootChannel) rootHashHex = normalizeHex32(rootChannel.rootBlockHash, ZERO64);
|
||||
if (rootChannel) {
|
||||
if (rootHashHex === ZERO64) rootHashHex = normalizeHex32(rootChannel.rootBlockHash, ZERO64);
|
||||
if (!channelSlug) channelSlug = toCanonicalChannelSlug(rootChannel.channelName);
|
||||
}
|
||||
}
|
||||
|
||||
let prevLineNumber = lineCode;
|
||||
@@ -2165,6 +2176,7 @@ export class AuthService {
|
||||
msgSubType: MSG_SUBTYPE_TEXT_EDIT_POST,
|
||||
msgVersion: 1,
|
||||
bodyBytes,
|
||||
channelSlug,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2375,6 +2387,7 @@ export class AuthService {
|
||||
msgType: MSG_TYPE_TECH,
|
||||
msgSubType: MSG_SUBTYPE_TECH_CREATE_CHANNEL,
|
||||
msgVersion: CREATE_CHANNEL_BODY_VERSION,
|
||||
channelSlug,
|
||||
bodyBytes: makeCreateChannelBodyBytes({
|
||||
lineCode: 0,
|
||||
prevLineNumber,
|
||||
@@ -2413,13 +2426,16 @@ export class AuthService {
|
||||
throw new Error('Posting is allowed only to your own channels');
|
||||
}
|
||||
|
||||
let channelSlug = toCanonicalChannelSlug(String(selector?.channelName || selector?.slug || ''));
|
||||
let rootHashHex = normalizeHex32(selector?.channelRootBlockHash, ZERO64);
|
||||
if (rootHashHex === ZERO64) {
|
||||
if (rootHashHex === ZERO64 || !channelSlug) {
|
||||
const ownChannels = await this.listOwnChannelsForBlockchain(login, blockchainName);
|
||||
const rootChannel = ownChannels.find((item) => item.rootBlockNumber === lineCode);
|
||||
if (!rootChannel) throw new Error('Channel root not found');
|
||||
rootHashHex = normalizeHex32(rootChannel.rootBlockHash, ZERO64);
|
||||
if (rootHashHex === ZERO64) rootHashHex = normalizeHex32(rootChannel.rootBlockHash, ZERO64);
|
||||
if (!channelSlug) channelSlug = toCanonicalChannelSlug(rootChannel.channelName);
|
||||
}
|
||||
if (!channelSlug) throw new Error('Cannot resolve canonical channel slug');
|
||||
|
||||
let prevLineNumber = lineCode;
|
||||
let prevLineHashHex = rootHashHex;
|
||||
@@ -2462,6 +2478,7 @@ export class AuthService {
|
||||
prevLineNumber,
|
||||
prevLineHashHex,
|
||||
thisLineNumber,
|
||||
channelSlug,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2506,6 +2523,7 @@ export class AuthService {
|
||||
msgSubType: cleanSubType,
|
||||
msgVersion: 1,
|
||||
bodyBytes,
|
||||
channelSlug: tail.channelSlug,
|
||||
});
|
||||
|
||||
return {
|
||||
@@ -2547,6 +2565,7 @@ export class AuthService {
|
||||
msgSubType: MSG_SUBTYPE_TEXT_CHANNEL_META,
|
||||
msgVersion: 1,
|
||||
bodyBytes,
|
||||
channelSlug: tail.channelSlug,
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user