SHA256
WIP: новая схема сообщений и push (не проверено)
This commit is contained in:
@@ -200,6 +200,114 @@ function uint8Bytes(value) {
|
||||
return new Uint8Array([Number(value) & 0xff]);
|
||||
}
|
||||
|
||||
const DM2_PREFIX = utf8Bytes('SHiNE_dm2');
|
||||
const DM2_TYPE_INCOMING = 1;
|
||||
const DM2_TYPE_OUTGOING_COPY = 2;
|
||||
const DM2_TYPE_READ_INCOMING = 3;
|
||||
const DM2_TYPE_READ_OUTGOING_COPY = 4;
|
||||
|
||||
function ensureAsciiBytes(value, field, min = 1, max = 60) {
|
||||
const text = String(value || '').trim();
|
||||
const bytes = utf8Bytes(text);
|
||||
if (bytes.length < min || bytes.length > max) {
|
||||
throw new Error(`${field} должен быть ${min}..${max} ASCII-символов`);
|
||||
}
|
||||
for (let i = 0; i < bytes.length; i += 1) {
|
||||
const code = bytes[i];
|
||||
if (code < 0x20 || code > 0x7e) throw new Error(`${field} должен быть ASCII`);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
function dm2BaseKey({ toLogin, fromLogin, timeMs, nonce }) {
|
||||
return `${fromLogin}|${toLogin}|${Number(timeMs)}|${Number(nonce)}`;
|
||||
}
|
||||
|
||||
function dm2MessageKey({ toLogin, fromLogin, timeMs, nonce, messageType }) {
|
||||
return `${dm2BaseKey({ toLogin, fromLogin, timeMs, nonce })}|${Number(messageType)}`;
|
||||
}
|
||||
|
||||
function buildReadReceiptPayloadBytes({ refToLogin, refFromLogin, refTimeMs, refNonce, refType }) {
|
||||
const toBytes = ensureAsciiBytes(refToLogin, 'receipt.refToLogin');
|
||||
const fromBytes = ensureAsciiBytes(refFromLogin, 'receipt.refFromLogin');
|
||||
return concatBytes(
|
||||
uint8Bytes(toBytes.length), toBytes,
|
||||
uint8Bytes(fromBytes.length), fromBytes,
|
||||
uint64Bytes(refTimeMs),
|
||||
uint32Bytes(refNonce),
|
||||
uint16Bytes(refType),
|
||||
);
|
||||
}
|
||||
|
||||
function parseSignedMessageBlockBytes(bytes) {
|
||||
if (!(bytes instanceof Uint8Array)) throw new Error('Expected Uint8Array');
|
||||
let o = 0;
|
||||
const read = (n) => {
|
||||
if (o + n > bytes.length) throw new Error('BAD_LEN');
|
||||
const out = bytes.slice(o, o + n);
|
||||
o += n;
|
||||
return out;
|
||||
};
|
||||
const readU8 = () => read(1)[0];
|
||||
const readU16 = () => {
|
||||
const part = read(2);
|
||||
const view = new DataView(part.buffer, part.byteOffset, 2);
|
||||
return view.getUint16(0, false);
|
||||
};
|
||||
const readU32 = () => {
|
||||
const part = read(4);
|
||||
const view = new DataView(part.buffer, part.byteOffset, 4);
|
||||
return view.getUint32(0, false);
|
||||
};
|
||||
const readU64 = () => {
|
||||
const part = read(8);
|
||||
const view = new DataView(part.buffer, part.byteOffset, 8);
|
||||
return Number(view.getBigUint64(0, false));
|
||||
};
|
||||
const readAscii = () => {
|
||||
const len = readU8();
|
||||
const part = read(len);
|
||||
const text = new TextDecoder().decode(part);
|
||||
for (let i = 0; i < part.length; i += 1) {
|
||||
const c = part[i];
|
||||
if (c < 0x20 || c > 0x7e) throw new Error('BAD_ASCII');
|
||||
}
|
||||
return text;
|
||||
};
|
||||
|
||||
const prefix = read(DM2_PREFIX.length);
|
||||
for (let i = 0; i < DM2_PREFIX.length; i += 1) {
|
||||
if (prefix[i] !== DM2_PREFIX[i]) throw new Error('BAD_PREFIX');
|
||||
}
|
||||
|
||||
const toLogin = readAscii();
|
||||
const fromLogin = readAscii();
|
||||
const timeMs = readU64();
|
||||
const nonce = readU32();
|
||||
const messageType = readU16();
|
||||
const payloadLen = readU16();
|
||||
const payloadBytes = read(payloadLen);
|
||||
const signatureBytes = read(64);
|
||||
if (o !== bytes.length) throw new Error('BAD_LEN');
|
||||
|
||||
const signedBody = bytes.slice(0, bytes.length - 64);
|
||||
const baseKey = dm2BaseKey({ toLogin, fromLogin, timeMs, nonce });
|
||||
const messageKey = dm2MessageKey({ toLogin, fromLogin, timeMs, nonce, messageType });
|
||||
return {
|
||||
toLogin,
|
||||
fromLogin,
|
||||
timeMs,
|
||||
nonce,
|
||||
messageType,
|
||||
payloadBytes,
|
||||
signatureBytes,
|
||||
signedBody,
|
||||
rawBytes: bytes,
|
||||
baseKey,
|
||||
messageKey,
|
||||
};
|
||||
}
|
||||
|
||||
function makeUserParamBodyBytes({ lineCode, prevLineNumber, prevLineHashHex, thisLineNumber, key, value }) {
|
||||
const keyBytes = utf8Bytes(String(key || ''));
|
||||
const valueBytes = utf8Bytes(String(value || ''));
|
||||
@@ -1118,11 +1226,18 @@ export class AuthService {
|
||||
return response.payload || {};
|
||||
}
|
||||
|
||||
async sendDirectMessage({ login, toLogin, text, storagePwd, targetSessionId = null, messageType = 1 }) {
|
||||
async buildSignedDm2Block({
|
||||
login,
|
||||
toLogin,
|
||||
storagePwd,
|
||||
timeMs,
|
||||
nonce,
|
||||
messageType,
|
||||
payloadBytes,
|
||||
}) {
|
||||
const cleanFromLogin = String(login || '').trim();
|
||||
const cleanToLogin = String(toLogin || '').trim();
|
||||
const cleanText = String(text || '');
|
||||
if (!cleanFromLogin || !cleanToLogin || !cleanText) throw new Error('Не передан login/toLogin/text');
|
||||
if (!cleanFromLogin || !cleanToLogin) throw new Error('Не передан login/toLogin');
|
||||
if (!storagePwd) throw new Error('Не передан storagePwd для подписи');
|
||||
|
||||
const secrets = await loadEncryptedUserSecrets(cleanFromLogin, storagePwd);
|
||||
@@ -1130,46 +1245,147 @@ export class AuthService {
|
||||
if (!devicePriv) throw new Error('Не найден приватный deviceKey');
|
||||
const privateKey = await importPkcs8Ed25519(devicePriv);
|
||||
|
||||
const prefix = utf8Bytes('SHiNE_msg');
|
||||
const version = uint8Bytes(1);
|
||||
const toBytes = utf8Bytes(cleanToLogin);
|
||||
const fromBytes = utf8Bytes(cleanFromLogin);
|
||||
if (toBytes.length < 1 || toBytes.length > 30) throw new Error('toLogin должен быть 1..30 ASCII-символов');
|
||||
if (fromBytes.length < 1 || fromBytes.length > 30) throw new Error('fromLogin должен быть 1..30 ASCII-символов');
|
||||
if (cleanText.length > 3000) throw new Error('Слишком длинное сообщение');
|
||||
|
||||
const mode = targetSessionId ? 1 : 0;
|
||||
const targetBytes = targetSessionId ? utf8Bytes(String(targetSessionId)) : new Uint8Array(0);
|
||||
if (mode === 1 && (targetBytes.length < 1 || targetBytes.length > 255)) {
|
||||
throw new Error('targetSessionId должен быть 1..255 символов');
|
||||
const toBytes = ensureAsciiBytes(cleanToLogin, 'toLogin');
|
||||
const fromBytes = ensureAsciiBytes(cleanFromLogin, 'fromLogin');
|
||||
if (!(payloadBytes instanceof Uint8Array) || payloadBytes.length < 1 || payloadBytes.length > 4096) {
|
||||
throw new Error('payload должен быть 1..4096 байт');
|
||||
}
|
||||
const bodyBytes = utf8Bytes(cleanText);
|
||||
|
||||
const preimage = concatBytes(
|
||||
prefix,
|
||||
version,
|
||||
DM2_PREFIX,
|
||||
uint8Bytes(toBytes.length), toBytes,
|
||||
uint8Bytes(fromBytes.length), fromBytes,
|
||||
uint64Bytes(Date.now()),
|
||||
uint32Bytes(Math.floor(Math.random() * 0x100000000)),
|
||||
uint64Bytes(timeMs),
|
||||
uint32Bytes(nonce),
|
||||
uint16Bytes(messageType),
|
||||
uint8Bytes(mode),
|
||||
mode === 1 ? concatBytes(uint8Bytes(targetBytes.length), targetBytes) : new Uint8Array(0),
|
||||
uint16Bytes(bodyBytes.length),
|
||||
bodyBytes,
|
||||
uint16Bytes(payloadBytes.length),
|
||||
payloadBytes,
|
||||
);
|
||||
const signature = await signBytes(privateKey, preimage);
|
||||
const packet = concatBytes(preimage, signature);
|
||||
const blobB64 = bytesToBase64(packet);
|
||||
return concatBytes(preimage, signature);
|
||||
}
|
||||
|
||||
const response = await this.ws.request('SendDirectMessage', { blobB64 });
|
||||
if (response.status !== 200) throw opError('SendDirectMessage', response);
|
||||
parseSignedMessageBlob(blobB64) {
|
||||
const bytes = base64ToBytes(String(blobB64 || '').trim());
|
||||
return parseSignedMessageBlockBytes(bytes);
|
||||
}
|
||||
|
||||
parseReadReceiptPayload(payloadBytes) {
|
||||
if (!(payloadBytes instanceof Uint8Array)) throw new Error('Expected Uint8Array');
|
||||
let o = 0;
|
||||
const read = (n) => {
|
||||
if (o + n > payloadBytes.length) throw new Error('BAD_RECEIPT_LEN');
|
||||
const out = payloadBytes.slice(o, o + n);
|
||||
o += n;
|
||||
return out;
|
||||
};
|
||||
const readU8 = () => read(1)[0];
|
||||
const readU16 = () => {
|
||||
const part = read(2);
|
||||
return new DataView(part.buffer, part.byteOffset, 2).getUint16(0, false);
|
||||
};
|
||||
const readU32 = () => {
|
||||
const part = read(4);
|
||||
return new DataView(part.buffer, part.byteOffset, 4).getUint32(0, false);
|
||||
};
|
||||
const readU64 = () => {
|
||||
const part = read(8);
|
||||
return Number(new DataView(part.buffer, part.byteOffset, 8).getBigUint64(0, false));
|
||||
};
|
||||
const readAscii = () => {
|
||||
const len = readU8();
|
||||
const part = read(len);
|
||||
return new TextDecoder().decode(part);
|
||||
};
|
||||
const refToLogin = readAscii();
|
||||
const refFromLogin = readAscii();
|
||||
const refTimeMs = readU64();
|
||||
const refNonce = readU32();
|
||||
const refType = readU16();
|
||||
if (o !== payloadBytes.length) throw new Error('BAD_RECEIPT_LEN');
|
||||
return { refToLogin, refFromLogin, refTimeMs, refNonce, refType };
|
||||
}
|
||||
|
||||
async sendMessagePair({ incomingBlobB64, outgoingBlobB64 }) {
|
||||
const response = await this.ws.request('SendMessagePair', { incomingBlobB64, outgoingBlobB64 });
|
||||
if (response.status !== 200) throw opError('SendMessagePair', response);
|
||||
return response.payload || {};
|
||||
}
|
||||
|
||||
async ackIncomingMessage(eventId, messageId) {
|
||||
const response = await this.ws.request('AckIncomingMessage', { eventId, messageId });
|
||||
if (response.status !== 200) throw opError('AckIncomingMessage', response);
|
||||
async sendDirectMessage({ login, toLogin, text, storagePwd }) {
|
||||
const cleanFromLogin = String(login || '').trim();
|
||||
const cleanToLogin = String(toLogin || '').trim();
|
||||
const cleanText = String(text || '');
|
||||
if (!cleanFromLogin || !cleanToLogin || !cleanText) throw new Error('Не передан login/toLogin/text');
|
||||
|
||||
const timeMs = Date.now();
|
||||
const nonce = Math.floor(Math.random() * 0x100000000);
|
||||
const incomingPayload = utf8Bytes(cleanText);
|
||||
const outgoingPayload = utf8Bytes(cleanText);
|
||||
|
||||
const incomingBlock = await this.buildSignedDm2Block({
|
||||
login: cleanFromLogin,
|
||||
toLogin: cleanToLogin,
|
||||
storagePwd,
|
||||
timeMs,
|
||||
nonce,
|
||||
messageType: DM2_TYPE_INCOMING,
|
||||
payloadBytes: incomingPayload,
|
||||
});
|
||||
const outgoingBlock = await this.buildSignedDm2Block({
|
||||
login: cleanFromLogin,
|
||||
toLogin: cleanToLogin,
|
||||
storagePwd,
|
||||
timeMs,
|
||||
nonce,
|
||||
messageType: DM2_TYPE_OUTGOING_COPY,
|
||||
payloadBytes: outgoingPayload,
|
||||
});
|
||||
|
||||
const payload = await this.sendMessagePair({
|
||||
incomingBlobB64: bytesToBase64(incomingBlock),
|
||||
outgoingBlobB64: bytesToBase64(outgoingBlock),
|
||||
});
|
||||
return {
|
||||
...payload,
|
||||
localIncomingBlobB64: bytesToBase64(incomingBlock),
|
||||
localOutgoingBlobB64: bytesToBase64(outgoingBlock),
|
||||
localBaseKey: dm2BaseKey({ toLogin: cleanToLogin, fromLogin: cleanFromLogin, timeMs, nonce }),
|
||||
};
|
||||
}
|
||||
|
||||
async sendReadReceipt({ login, toLogin, storagePwd, refToLogin, refFromLogin, refTimeMs, refNonce, refType = DM2_TYPE_INCOMING }) {
|
||||
const timeMs = Date.now();
|
||||
const nonce = Math.floor(Math.random() * 0x100000000);
|
||||
const payload = buildReadReceiptPayloadBytes({ refToLogin, refFromLogin, refTimeMs, refNonce, refType });
|
||||
|
||||
const type3 = await this.buildSignedDm2Block({
|
||||
login,
|
||||
toLogin,
|
||||
storagePwd,
|
||||
timeMs,
|
||||
nonce,
|
||||
messageType: DM2_TYPE_READ_INCOMING,
|
||||
payloadBytes: payload,
|
||||
});
|
||||
const type4 = await this.buildSignedDm2Block({
|
||||
login,
|
||||
toLogin,
|
||||
storagePwd,
|
||||
timeMs,
|
||||
nonce,
|
||||
messageType: DM2_TYPE_READ_OUTGOING_COPY,
|
||||
payloadBytes: payload,
|
||||
});
|
||||
return this.sendMessagePair({
|
||||
incomingBlobB64: bytesToBase64(type3),
|
||||
outgoingBlobB64: bytesToBase64(type4),
|
||||
});
|
||||
}
|
||||
|
||||
async ackSessionDelivery(messageKey) {
|
||||
const response = await this.ws.request('AckSessionDelivery', { messageKey });
|
||||
if (response.status !== 200) throw opError('AckSessionDelivery', response);
|
||||
return response.payload || {};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user