From 5ae52299295158320e63cd8792855d73f8479612 Mon Sep 17 00:00:00 2001 From: 1aerostorm Date: Sat, 24 Aug 2024 00:36:16 +0300 Subject: [PATCH] JS - Private groups, draft #2 --- golos-lib-js/src/auth/messages.js | 178 ++++++++++++++++++++++++----- golos-lib-js/test/messages.test.js | 131 ++++++++++++--------- 2 files changed, 226 insertions(+), 83 deletions(-) diff --git a/golos-lib-js/src/auth/messages.js b/golos-lib-js/src/auth/messages.js index 98ee5e7..f6ab15a 100644 --- a/golos-lib-js/src/auth/messages.js +++ b/golos-lib-js/src/auth/messages.js @@ -6,6 +6,7 @@ import truncate from 'lodash/truncate'; import {Aes, PrivateKey, PublicKey} from './ecc' import {ops} from './serializer' import golosApi from '../api' +import auth from '.' import {fitImageToSize} from '../utils'; import { promisify, } from '../promisify' const {isInteger} = Number @@ -32,6 +33,8 @@ export const MAX_IMAGE_QUOTE_LENGTH = 2000; const toPrivateObj = o => (o ? o.d ? o : PrivateKey.fromWif(o) : o/*null or undefined*/) const toPublicObj = o => (o ? o.Q ? o : PublicKey.fromString(o) : o/*null or undefined*/) +export const emptyPublicKey = 'GLS1111111111111111111111111111111114T1Anm' + function validateAppVersion(app, version) { assert(typeof app === 'string' && app.length >= 1 && app.length <= 16, 'message.app should be a string, >= 1, <= 16'); @@ -243,6 +246,26 @@ function msgFromHex(hex, lengthPrefixed = false) { return msgFromBuf(buf, lengthPrefixed) } +const parseMsg = (message, rawMsg, raw_messages = false) => { + message.raw_message = rawMsg + if (!raw_messages) { + let msg = JSON.parse(message.raw_message) + msg.type = msg.type || 'text' + validateBody(msg.body) + if (msg.type === 'image') + validateImageMsg(msg) + validateAppVersion(msg.app, msg.version) + validateMsgWithQuote(msg) + message.message = msg + } +} + +const warnDeprecation = (oldMethod, newMethod) => { + console.warn(`golos.messages.${oldMethod} deprecated. ` + + `It is not async, not supports groups, etc. ` + + `Will be removed in future. Migrate to golos.messages.${newMethod}.`) +} + /** Decodes messages of format used by golos.messages.encode(), which are length-prefixed, and also messages sent by another way (not length-prefixed).
Also, parses (JSON) and validates each message (app, version...). (Invalid messages are also added to result, it is need to mark them as read. To change it, use on_error).
@@ -260,6 +283,8 @@ function msgFromHex(hex, lengthPrefixed = false) { @return {array} - result array of message_objects. Each object has "message" and "raw_message" fields. If message is invalid, it has only "raw_message" field. And if message cannot be decoded at all, it hasn't any of these fields. */ export function decode(private_memo_key, second_user_public_memo_key, message_objects, before_decode = undefined, for_each = undefined, on_error = undefined, begin_idx = undefined, end_idx = undefined, raw_messages = false) { + warnDeprecation('decode', 'decodeMsgs') + assert(private_memo_key, 'private_memo_key is required'); assert(second_user_public_memo_key, 'second_user_public_memo_key is required'); assert(message_objects, 'message_objects is required'); @@ -300,17 +325,7 @@ export function decode(private_memo_key, second_user_public_memo_key, message_ob const mbuf = ByteBuffer.fromBinary(decrypted.toString('binary'), ByteBuffer.DEFAULT_CAPACITY, ByteBuffer.LITTLE_ENDIAN) decrypted = msgFromBuf(mbuf, true) - message_object.raw_message = decrypted; - if (!raw_messages) { - let msg = JSON.parse(message_object.raw_message); - msg.type = msg.type || 'text'; - validateBody(msg.body); - if (msg.type === 'image') - validateImageMsg(msg); - validateAppVersion(msg.app, msg.version); - validateMsgWithQuote(msg); - message_object.message = msg; - } + parseMsg(message_object, decrypted, raw_messages) } catch (exception) { if (processOnError(exception)) return true; @@ -327,11 +342,20 @@ export function decode(private_memo_key, second_user_public_memo_key, message_ob return results; } -export async function decodeMsgs({ messages, for_each, on_error, raw_messages, +export async function decodeMsgs({ msgs, before_decode, for_each, on_error, + raw_messages, + private_memo, // chats + api, login, // groups begin_idx, end_idx, }) { + let private_key = private_memo && toPrivateObj(private_memo) + const myPublic = private_key.toPublic().toString() + let shareds = {} + let results = [] - forEachMessage(messages, begin_idx, end_idx, (message, i) => { + let entriesDec = {} + + forEachMessage(msgs, begin_idx, end_idx, (message, i) => { // Return true if for_each should not be called let processOnError = (exception) => { if (on_error) { @@ -340,30 +364,49 @@ export async function decodeMsgs({ messages, for_each, on_error, raw_messages, } return true } + console.error('golos.messages.decodeMsgs', i, exception) return false } try { - let rawMsg = msgFromHex(message.encrypted_message) - const isEncrypted = rawMsg.startsWith('{"t":"em"') + if (before_decode && before_decode(message, i, results)) { + return true + } + + if (!message.group) { + const pubKey = message.from_memo_key === myPublic ? + message.to_memo_key : message.from_memo_key + // Most "heavy" line (in private chats) + if (!shareds[pubKey]) { + console.error(pubKey) + console.error(message.from_memo_key) + console.error(message.to_memo_key) + shareds[pubKey] = private_key.get_shared_secret(toPublicObj(pubKey)) + } - if (isEncrypted) { - if (!message.decrypted || message.decrypt_date !== message.receive_date) { + let decrypted = Aes.decrypt(shareds[pubKey], null, + message.nonce.toString(), + Buffer.from(message.encrypted_message, 'hex'), + message.checksum) + const mbuf = ByteBuffer.fromBinary(decrypted.toString('binary'), ByteBuffer.DEFAULT_CAPACITY, ByteBuffer.LITTLE_ENDIAN) + decrypted = msgFromBuf(mbuf, true) + + parseMsg(message, decrypted, raw_messages) + } else { + let rawMsg = msgFromHex(message.encrypted_message) + const isEncrypted = rawMsg.startsWith('{"t":"em"') + + if (isEncrypted) { + if (!message.decrypted || message.decrypt_date !== message.receive_date) { + entriesDec[i] = { group: message.group, + encrypted_message: message.encrypted_message, } + return true + } + rawMsg = msgFromHex(message.decrypted) } - rawMsg = msgFromHex(message.decrypted) - } - message.raw_message = rawMsg - if (!raw_messages) { - let msg = JSON.parse(message.raw_message) - msg.type = msg.type || 'text' - validateBody(msg.body) - if (msg.type === 'image') - validateImageMsg(msg) - validateAppVersion(msg.app, msg.version) - validateMsgWithQuote(msg) - message.message = msg + parseMsg(message, rawMsg, raw_messages) } } catch (exception) { if (processOnError(exception)) @@ -380,6 +423,39 @@ export async function decodeMsgs({ messages, for_each, on_error, raw_messages, return true }) + + const entries = Object.values(entriesDec) + if (entries.length) { + if (!api) { + api = golosApi + } + const { dgp, account, keys, sessionName } = login + const decRes = await auth.withNodeLogin({ account, keys, sessionName, dgp, + call: async (loginData) => { + const decRes = await api.decryptMessagesAsync({ + entries + }) + return decRes + } + }) + const idxs = Object.keys(entriesDec) + if (decRes.status !== 'ok') { + + } else { + for (let i = 0; i < entries.length; ++i) { + const dec = decRes.results[i] + if (dec.body) { + const msg = results[idxs[i]] + dec.decrypted = dec.body // TODO: but `decrypted` should be hex + parseMsg(msg, dec.decrypted, raw_messages) + // for_each + } else { + // on_error, for_each if not handled + } + } + } + } + return results } @@ -392,6 +468,8 @@ export async function decodeMsgs({ messages, for_each, on_error, raw_messages, @return {object} - Object with fields: nonce, checksum and message. */ export function encode(from_private_memo_key, to_public_memo_key, message, nonce = undefined) { + warnDeprecation('encode', 'encodeMsg') + assert(from_private_memo_key, 'from_private_memo_key is required'); assert(to_public_memo_key, 'to_public_memo_key is required'); assert(message, 'message is required'); @@ -415,14 +493,50 @@ export function encode(from_private_memo_key, to_public_memo_key, message, nonce }; } -export async function encodeMsg({ group, message, nonce, api }) { +export async function encodeMsg({ group, + private_memo, to_public_memo, + msg, nonce, + api +}) { + assert(msg, 'msg is required') + const msgToBuffer = (msg) => { const mbuf = new ByteBuffer(ByteBuffer.DEFAULT_CAPACITY, ByteBuffer.LITTLE_ENDIAN) - mbuf.writeUTF8String(JSON.stringify(msg)); + if (group) { + mbuf.writeUTF8String(JSON.stringify(msg)) + } else { + mbuf.writeVString(JSON.stringify(msg)) + } msg = new Buffer(mbuf.copy(0, mbuf.offset).toBinary(), 'binary') return msg } + if (!group) { + assert(private_memo, 'private_memo is required in private chats') + assert(to_public_memo, 'to_public_memo is required in private chats') + + const fromKey = toPrivateObj(private_memo) + const toKey = toPublicObj(to_public_memo) + + msg = msgToBuffer(msg) + + let data = Aes.encrypt(fromKey, + toKey, + msg, + nonce) + + return { + nonce: data.nonce.toString(), + encrypted_message: data.message.toString('hex'), + checksum: data.checksum, + from_memo_key: fromKey.toPublic().toString(), + to_memo_key: to_public_memo, + } + } + + assert(!private_memo, 'private_memo is for private messages, not groups') + assert(!to_public_memo, 'to_public_memo is for private messages, not groups') + if (group.is_encrypted) { if (!api) { api = golosApi @@ -448,6 +562,8 @@ export async function encodeMsg({ group, message, nonce, api }) { nonce: Aes.uniqueNonce().toString(), encrypted_message, checksum: 0, + from_memo_key: emptyPublicKey, + to_memo_key: emptyPublicKey, } } diff --git a/golos-lib-js/test/messages.test.js b/golos-lib-js/test/messages.test.js index 0c8e12a..fbc2a1a 100644 --- a/golos-lib-js/test/messages.test.js +++ b/golos-lib-js/test/messages.test.js @@ -1,6 +1,6 @@ import { assert } from 'chai'; import cloneDeep from 'lodash/cloneDeep'; -import { encode, decode, newTextMsg, newImageMsgAsync, makeQuoteMsg, +import { encode, encodeMsg, decode, decodeMsgs, newTextMsg, newImageMsgAsync, makeQuoteMsg, DEFAULT_APP, DEFAULT_VERSION, MAX_PREVIEW_WIDTH, MAX_PREVIEW_HEIGHT, MAX_TEXT_QUOTE_LENGTH, MAX_IMAGE_QUOTE_LENGTH } from '../src/auth/messages'; import th from './test_helper'; @@ -22,17 +22,19 @@ describe('golos.messages: encode()', function() { await importNativeLib(); }); - it('input arguments', function() { - assert.throws(() => encode(), 'from_private_memo_key is required'); - assert.throws(() => encode(alice.memo), 'to_public_memo_key is required'); - assert.throws(() => encode(alice.memo, bob.memo_pub), 'message is required'); + it('input arguments', async function() { + await assert.isRejected(encodeMsg({}), 'msg is required'); + await assert.isRejected(encodeMsg({ msg: 1 }), 'private_memo is required in private chats'); + await assert.isRejected(encodeMsg({ msg: 1, private_memo: alice.memo }), + 'to_public_memo is required in private chats'); - assert.throws(() => encode(1, alice.memo, bob.memo_pub)); + await assert.isRejected(encodeMsg({ msg: 1, private_memo: 1, to_public_memo: alice.memo })) }) - it('normal case', function() { + it('normal case', async function() { var msg = newTextMsg('Привет'); - var res = encode(alice.memo, bob.memo_pub, msg); + var res = await encodeMsg({ private_memo: alice.memo, + to_public_memo: bob.memo_pub, msg }) assert.isString(res.nonce); assert.isNotEmpty(res.nonce); @@ -43,7 +45,7 @@ describe('golos.messages: encode()', function() { assert.isNotEmpty(res.encrypted_message); }) - it('cyrillic, emoji, etc', function() { + it('cyrillic, emoji, etc', async function() { var veryLong = 'Очень'; for (let i = 0; i < 100; ++i) { veryLong += ' длинный текст. Длинный текст.\nОчень'; @@ -63,7 +65,8 @@ describe('golos.messages: encode()', function() { veryLong, ]) { var msg = newTextMsg(str); - var enc = encode(alice.memo, bob.memo_pub, msg); + var enc = await encodeMsg({ private_memo: alice.memo, + to_public_memo: bob.memo_pub, msg }) msg.type = 'text'; var res = decode(bob.memo, alice.memo_pub, [enc]); @@ -72,13 +75,17 @@ describe('golos.messages: encode()', function() { } }) - it('alice -> alice, bob', function() { + it('alice -> alice, bob', async function() { var msg = newTextMsg('Привет'); - var enc = encode(alice.memo, bob.memo_pub, msg); + var enc = await encodeMsg({ private_memo: alice.memo, + to_public_memo: bob.memo_pub, msg }) msg.type = 'text'; - var res = decode(alice.memo, bob.memo_pub, [enc]); + var res = await decodeMsgs({ private_memo: alice.memo, + msgs: [enc] }); assert.lengthOf(res, 1); + console.error(res[0].raw_message) + console.error(res[0].message) assert.deepStrictEqual(res[0].message, msg); var res = decode(bob.memo, alice.memo_pub, [enc]); @@ -86,9 +93,10 @@ describe('golos.messages: encode()', function() { assert.deepStrictEqual(res[0].message, msg); }) - it('alice -> alice', function() { + it('alice -> alice', async function() { var msg = newTextMsg('Привет'); - var enc = encode(alice.memo, alice.memo_pub, msg); + var enc = await encodeMsg({ private_memo: alice.memo, + to_public_memo: alice.memo_pub, msg }) msg.type = 'text'; var res = decode(alice.memo, alice.memo_pub, [enc]); @@ -104,9 +112,10 @@ describe('golos.messages: encode()', function() { assert.isNull(res[0].message); }) - it('edit case', function() { + it('edit case', async function() { var msg = newTextMsg('Привет'); - var enc = encode(alice.memo, bob.memo_pub, msg); + var enc = await encodeMsg({ private_memo: alice.memo, + to_public_memo: bob.memo_pub, msg }) msg.type = 'text'; var res = decode(bob.memo, alice.memo_pub, [enc]); @@ -114,7 +123,8 @@ describe('golos.messages: encode()', function() { assert.deepStrictEqual(res[0].message, msg); var msg = newTextMsg('Приветик'); - var enc = encode(alice.memo, bob.memo_pub, msg, enc.nonce); + var enc = await encodeMsg({ private_memo: alice.memo, + to_public_memo: bob.memo_pub, msg, nonce: enc.nonce }) msg.type = 'text'; var res2 = decode(bob.memo, alice.memo_pub, [enc]); @@ -128,11 +138,13 @@ describe('golos.messages: encode()', function() { describe('golos.messages: decode()', function() { before(async function () { var msg = newTextMsg('Привет'); - var msgEnc = encode(alice.memo, bob.memo_pub, msg); + var msgEnc = await encodeMsg({ private_memo: alice.memo, + to_public_memo: bob.memo_pub, msg }) msg.type = 'text'; // for comparison var msg2 = await newImageMsgAsync(correctImageURL); - var msgEnc2 = encode(alice.memo, bob.memo_pub, msg2); + var msgEnc2 = await encodeMsg({ private_memo: alice.memo, + to_public_memo: bob.memo_pub, msg: msg2 }) this._msgs = [ msg, @@ -143,8 +155,10 @@ describe('golos.messages: decode()', function() { this._msgObjs = [ msgEnc, msgEnc2, - encode(alice.memo, alice.memo_pub, {}), - encode(alice.memo, bob.memo_pub, {}), + await encodeMsg({ private_memo: alice.memo, + to_public_memo: alice.memo_pub, msg: {} }), + await encodeMsg({ private_memo: alice.memo, + to_public_memo: bob.memo_pub, msg: {} }), ]; this._decoded = decode(bob.memo, alice.memo_pub, this._msgObjs); @@ -163,8 +177,7 @@ describe('golos.messages: decode()', function() { assert.throws(() => decode(alice.memo, null), 'second_user_public_memo_key is required'); assert.throws(() => decode(alice.memo, bob.memo_pub), 'message_objects is required'); - assert.throws(() => decode('wrong key', bob.memo_pub, []), 'Non-base58 character'); - assert.throws(() => decode(alice.memo, 'wrong key', [])); + assert.throws(() => decode('wrong key', bob.memo_pub, [{}]), 'Non-base58 character'); }) it('validation', async function() { @@ -178,7 +191,8 @@ describe('golos.messages: decode()', function() { // non-decodable { - let msg = encode(alice.memo, alice.memo_pub, normalText); + let msg = await encodeMsg({ private_memo: alice.memo, + to_public_memo: alice.memo_pub, msg: normalText }) messages.push({ nonce: msg.nonce, checksum: 1, @@ -191,16 +205,20 @@ describe('golos.messages: decode()', function() { return data; // as it is }); - messages.push(encode(alice.memo, bob.memo_pub, 'не json')); + messages.push(await encodeMsg({ private_memo: alice.memo, + to_public_memo: bob.memo_pub, msg: 'не json' })); JSON.stringify.restore(); } // JSON, but not object - messages.push(encode(alice.memo, bob.memo_pub, 'Привет')); + messages.push(await encodeMsg({ private_memo: alice.memo, + to_public_memo: bob.memo_pub, msg: 'Привет' })) // no body - messages.push(encode(alice.memo, bob.memo_pub, {})); + messages.push(await encodeMsg({ private_memo: alice.memo, + to_public_memo: bob.memo_pub, msg: {} })); // no app, version - messages.push(encode(alice.memo, bob.memo_pub, {body: 'Привет'})); + messages.push(await encodeMsg({ private_memo: alice.memo, + to_public_memo: bob.memo_pub, msg: {body: 'Привет'} })); // normal text msgs for (let i = 0; i < 50 - 5; ++i) { @@ -308,25 +326,26 @@ describe('golos.messages: decode()', function() { // normal messages.push(normalImageEnc); - var addMsg = ((breaker) => { + var addMsg = async (breaker) => { var msg = Object.assign({}, normalImage); breaker(msg); - msg = encode(alice.memo, bob.memo_pub, msg); + msg = await encodeMsg({ private_memo: alice.memo, + to_public_memo: bob.memo_pub, msg }) messages.push(msg); - }); + }; // no body - addMsg(msg => delete msg.body); + await addMsg(msg => delete msg.body); // no app - addMsg(msg => delete msg.app); + await addMsg(msg => delete msg.app); // previewWidth, previewHeight problems - addMsg(msg => delete msg.previewWidth); - addMsg(msg => msg.previewWidth = '12px'); - addMsg(msg => msg.previewWidth = 0); - addMsg(msg => msg.previewWidth = MAX_PREVIEW_WIDTH + 1); - addMsg(msg => delete msg.previewHeight); - addMsg(msg => msg.previewHeight = '12px'); - addMsg(msg => msg.previewHeight = 0); - addMsg(msg => msg.previewHeight = MAX_PREVIEW_HEIGHT + 1); + await addMsg(msg => delete msg.previewWidth); + await addMsg(msg => msg.previewWidth = '12px'); + await addMsg(msg => msg.previewWidth = 0); + await addMsg(msg => msg.previewWidth = MAX_PREVIEW_WIDTH + 1); + await addMsg(msg => delete msg.previewHeight); + await addMsg(msg => msg.previewHeight = '12px'); + await addMsg(msg => msg.previewHeight = 0); + await addMsg(msg => msg.previewHeight = MAX_PREVIEW_HEIGHT + 1); var on_error = sandbox.spy(); var res = decode(bob.memo, alice.memo_pub, messages, @@ -574,9 +593,10 @@ describe('golos.messages: decode()', function() { }) describe('golos.messages: decode() replies', function() { - it('message', function() { + it('message', async function() { var msg = newTextMsg('Привет'); - var enc = encode(alice.memo, bob.memo_pub, msg); + var enc = await encodeMsg({ private_memo: alice.memo, + to_public_memo: bob.memo_pub, msg }) const orig = Object.assign({ from: 'alice', }, enc); @@ -585,7 +605,8 @@ describe('golos.messages: decode() replies', function() { var msg2 = newTextMsg('Hi'); msg2 = makeQuoteMsg(msg2, origDecoded[0]); - var enc2 = encode(alice.memo, bob.memo_pub, msg2); + var enc2 = await encodeMsg({ private_memo: alice.memo, + to_public_memo: bob.memo_pub, msg: msg2 }) const reply = Object.assign({ from: 'bob', }, enc2); @@ -598,9 +619,10 @@ describe('golos.messages: decode() replies', function() { assert.strictEqual(bothDecoded[1].message.quote.type, bothDecoded[0].message.type); }) - it('too long message', function() { + it('too long message', async function() { var msg = newTextMsg('a'.repeat(MAX_TEXT_QUOTE_LENGTH + 1)); - var enc = encode(alice.memo, bob.memo_pub, msg); + var enc = await encodeMsg({ private_memo: alice.memo, + to_public_memo: bob.memo_pub, msg }) const orig = Object.assign({ from: 'alice', }, enc); @@ -611,7 +633,8 @@ describe('golos.messages: decode() replies', function() { msg2 = makeQuoteMsg(msg2, origDecoded[0]); // keep its original length to make message invalid msg2.quote.body = 'a'.repeat(MAX_TEXT_QUOTE_LENGTH + 1); - var enc2 = encode(alice.memo, bob.memo_pub, msg2); + var enc2 = await encodeMsg({ private_memo: alice.memo, + to_public_memo: bob.memo_pub, msg: msg2 }) const reply = Object.assign({ from: 'bob', }, enc2); @@ -626,7 +649,8 @@ describe('golos.messages: decode() replies', function() { assert.isTrue(correctImageURL.length > MAX_TEXT_QUOTE_LENGTH, 'too short correctImageURL for this test'); var msg = await newImageMsgAsync(correctImageURL); - var enc = encode(alice.memo, bob.memo_pub, msg); + var enc = await encodeMsg({ private_memo: alice.memo, + to_public_memo: bob.memo_pub, msg }) const orig = Object.assign({ from: 'alice', }, enc); @@ -635,7 +659,8 @@ describe('golos.messages: decode() replies', function() { var msg2 = newTextMsg('Hi'); msg2 = makeQuoteMsg(msg2, origDecoded[0]); - var enc2 = encode(alice.memo, bob.memo_pub, msg2); + var enc2 = await encodeMsg({ private_memo: alice.memo, + to_public_memo: bob.memo_pub, msg: msg2 }) const reply = Object.assign({ from: 'bob', }, enc2); @@ -657,7 +682,8 @@ describe('golos.messages: decode() replies', function() { previewWidth: MAX_PREVIEW_WIDTH, previewHeight: MAX_PREVIEW_HEIGHT, }; - var enc = encode(alice.memo, bob.memo_pub, msg); + var enc = await encodeMsg({ private_memo: alice.memo, + to_public_memo: bob.memo_pub, msg }) const orig = Object.assign({ from: 'alice', }, enc); @@ -666,7 +692,8 @@ describe('golos.messages: decode() replies', function() { var msg2 = newTextMsg('Hi'); msg2 = makeQuoteMsg(msg2, origDecoded[0]); - var enc2 = encode(alice.memo, bob.memo_pub, msg2); + var enc2 = await encodeMsg({ private_memo: alice.memo, + to_public_memo: bob.memo_pub, msg: msg2 }) const reply = Object.assign({ from: 'bob', }, enc2);