From f0c198c7b74bf453515bd1e047910c60a7139943 Mon Sep 17 00:00:00 2001 From: Beaudan Brown Date: Mon, 21 Oct 2019 12:32:59 +1100 Subject: [PATCH 1/6] Split uploadData to public and private --- background.html | 2 +- js/modules/loki_app_dot_net_api.js | 40 +++++++++++++++++++++++++++++- js/modules/loki_file_server_api.js | 24 ++---------------- js/modules/web_api.js | 2 +- libtextsecure/message_receiver.js | 30 +++++++++++----------- 5 files changed, 59 insertions(+), 39 deletions(-) diff --git a/background.html b/background.html index 04040201f..d68fbb83a 100644 --- a/background.html +++ b/background.html @@ -138,7 +138,7 @@
- +
diff --git a/js/modules/loki_app_dot_net_api.js b/js/modules/loki_app_dot_net_api.js index eb113d860..17d40e285 100644 --- a/js/modules/loki_app_dot_net_api.js +++ b/js/modules/loki_app_dot_net_api.js @@ -1,8 +1,9 @@ /* global log, textsecure, libloki, Signal, Whisper, Headers, ConversationController, -clearTimeout, MessageController, libsignal, StringView, window, _, dcodeIO */ +clearTimeout, MessageController, libsignal, StringView, window, _, dcodeIO, Buffer */ const EventEmitter = require('events'); const nodeFetch = require('node-fetch'); const { URL, URLSearchParams } = require('url'); +const FormData = require('form-data'); // Can't be less than 1200 if we have unauth'd requests const PUBLICCHAT_MSG_POLL_EVERY = 1.5 * 1000; // 1.5s @@ -344,6 +345,43 @@ class LokiAppDotNetServerAPI { return false; } + + async uploadData(data) { + const endpoint = 'files'; + const options = { + method: 'POST', + rawBody: data, + }; + + const { statusCode, response } = await this.serverRequest( + endpoint, + options + ); + if (statusCode !== 200) { + log.warn('Failed to upload data to fileserver'); + return null; + } + + const url = response.data && response.data.url; + const id = response.data && response.data.id; + return { + url, + id, + }; + } + + putAttachment(attachmentBin) { + const formData = new FormData(); + const buffer = Buffer.from(attachmentBin); + formData.append('type', 'network.loki'); + formData.append('content', buffer, { + contentType: 'application/octet-stream', + name: 'content', + filename: 'attachment', + }); + + return this.uploadData(formData); + } } class LokiPublicChannelAPI { diff --git a/js/modules/loki_file_server_api.js b/js/modules/loki_file_server_api.js index b70984ccc..06e062aa5 100644 --- a/js/modules/loki_file_server_api.js +++ b/js/modules/loki_file_server_api.js @@ -38,28 +38,8 @@ class LokiFileServerAPI { ); } - async uploadData(data) { - const endpoint = 'files'; - const options = { - method: 'POST', - rawBody: data, - }; - - const { statusCode, response } = await this._server.serverRequest( - endpoint, - options - ); - if (statusCode !== 200) { - log.warn('Failed to upload data to fileserver'); - return null; - } - - const url = response.data && response.data.url; - const id = response.data && response.data.id; - return { - url, - id, - }; + uploadPrivateAttachment(data) { + return this._server.uploadData(data); } } diff --git a/js/modules/web_api.js b/js/modules/web_api.js index 92bdf937b..7de88a727 100644 --- a/js/modules/web_api.js +++ b/js/modules/web_api.js @@ -864,7 +864,7 @@ function initialize({ filename: 'attachment', }); - return lokiFileServerAPI.uploadData(formData); + return lokiFileServerAPI.uploadPrivateAttachment(formData); } // eslint-disable-next-line no-shadow diff --git a/libtextsecure/message_receiver.js b/libtextsecure/message_receiver.js index 4a987d6b5..ea2539c8c 100644 --- a/libtextsecure/message_receiver.js +++ b/libtextsecure/message_receiver.js @@ -1438,21 +1438,23 @@ MessageReceiver.prototype.extend({ }, async downloadAttachment(attachment) { // The attachment id is actually just the absolute url of the attachment - const encrypted = await this.server.getAttachment(attachment.url); - const { key, digest, size } = attachment; - - const data = await textsecure.crypto.decryptAttachment( - encrypted, - window.Signal.Crypto.base64ToArrayBuffer(key), - window.Signal.Crypto.base64ToArrayBuffer(digest) - ); - - if (!size || size !== data.byteLength) { - throw new Error( - `downloadAttachment: Size ${size} did not match downloaded attachment size ${ - data.byteLength - }` + let data = await this.server.getAttachment(attachment.url); + if (!attachment.isRaw) { + const { key, digest, size } = attachment; + + data = await textsecure.crypto.decryptAttachment( + data, + window.Signal.Crypto.base64ToArrayBuffer(key), + window.Signal.Crypto.base64ToArrayBuffer(digest) ); + + if (!size || size !== data.byteLength) { + throw new Error( + `downloadAttachment: Size ${size} did not match downloaded attachment size ${ + data.byteLength + }` + ); + } } return { From 9114a3bc0394d40e43e74e295f721231b55fed08 Mon Sep 17 00:00:00 2001 From: Beaudan Brown Date: Mon, 21 Oct 2019 12:52:17 +1100 Subject: [PATCH 2/6] Annotation conversations --- js/modules/loki_app_dot_net_api.js | 83 +++++++++++++++++++++++++++++- 1 file changed, 82 insertions(+), 1 deletion(-) diff --git a/js/modules/loki_app_dot_net_api.js b/js/modules/loki_app_dot_net_api.js index 17d40e285..4004aae91 100644 --- a/js/modules/loki_app_dot_net_api.js +++ b/js/modules/loki_app_dot_net_api.js @@ -12,6 +12,10 @@ const PUBLICCHAT_DELETION_POLL_EVERY = 5 * 1000; // 5s const PUBLICCHAT_MOD_POLL_EVERY = 30 * 1000; // 30s const PUBLICCHAT_MIN_TIME_BETWEEN_DUPLICATE_MESSAGES = 10 * 1000; // 10s +const ATTACHMENT_TYPE = 'net.app.core.oembed'; +const LOKI_ATTACHMENT_TYPE = 'attachment'; +const LOKI_PREVIEW_TYPE = 'preview'; + class LokiAppDotNetAPI extends EventEmitter { constructor(ourKey) { super(); @@ -864,8 +868,85 @@ class LokiPublicChannelAPI { } } + static getPreviewFromAnnotation(annotation) { + const preview = { + title: annotation.value.linkPreviewTitle, + url: annotation.value.linkPreviewUrl, + image: { + isRaw: true, + caption: annotation.value.caption, + contentType: annotation.value.contentType, + digest: annotation.value.digest, + fileName: annotation.value.fileName || '', + flags: annotation.value.flags || '0', + height: annotation.value.height, + id: annotation.value.id, + key: annotation.value.key, + size: annotation.value.size, + thumbnail: annotation.value.thumbnail, + url: annotation.value.url, + width: annotation.value.width, + }, + }; + return preview; + } + + static getAnnotationFromPreview(preview) { + const annotation = { + type: ATTACHMENT_TYPE, + value: { + // Mandatory ADN fields + version: '1.0', + lokiType: LOKI_PREVIEW_TYPE, + + // Signal stuff we actually care about + linkPreviewTitle: preview.title, + linkPreviewUrl: preview.url, + caption: preview.image.caption, + contentType: preview.image.contentType, + digest: preview.image.digest, + fileName: preview.image.fileName || '', + flags: preview.image.flags || '0', + height: preview.image.height, + id: preview.image.id, + key: preview.image.key, + size: preview.image.size, + thumbnail: preview.image.thumbnail, + url: preview.image.url, + width: preview.image.width, + }, + }; + return annotation; + } + + static getAnnotationFromAttachment(attachment) { + const type = attachment.contentType.match(/^image/) ? 'photo' : 'video'; + const annotation = { + type: ATTACHMENT_TYPE, + value: { + // Mandatory ADN fields + version: '1.0', + type, + lokiType: LOKI_ATTACHMENT_TYPE, + + // Signal stuff we actually care about + ...attachment, + }, + }; + return annotation; + } + // create a message in the channel - async sendMessage(text, quote, messageTimeStamp) { + async sendMessage(data, messageTimeStamp) { + const { quote, attachments, preview } = data; + const text = data.body; + const attachmentAnnotations = attachments.map( + LokiPublicChannelAPI.getAnnotationFromAttachment + ); + const previewAnnotations = preview.map( + LokiPublicChannelAPI.getAnnotationFromPreview + ); + const payload = { text, annotations: [ From 638f1c0e6c42df055828a0fcc78243c993958718 Mon Sep 17 00:00:00 2001 From: Beaudan Brown Date: Mon, 21 Oct 2019 12:52:56 +1100 Subject: [PATCH 3/6] Bringing together all the stuff for attachments and link previews --- js/modules/loki_app_dot_net_api.js | 35 ++++++++-- js/modules/loki_file_server_api.js | 2 - js/modules/loki_message_api.js | 6 +- libtextsecure/sendmessage.js | 101 ++++++++++++++++------------- 4 files changed, 88 insertions(+), 56 deletions(-) diff --git a/js/modules/loki_app_dot_net_api.js b/js/modules/loki_app_dot_net_api.js index 4004aae91..bbc934ec6 100644 --- a/js/modules/loki_app_dot_net_api.js +++ b/js/modules/loki_app_dot_net_api.js @@ -648,7 +648,13 @@ class LokiPublicChannelAPI { } } - static getSigData(sigVer, noteValue, adnMessage) { + static getSigData( + sigVer, + noteValue, + attachmentAnnotations, + previewAnnotations, + adnMessage + ) { let sigString = ''; sigString += adnMessage.text.trim(); sigString += noteValue.timestamp; @@ -660,6 +666,12 @@ class LokiPublicChannelAPI { sigString += adnMessage.reply_to; } } + attachmentAnnotations + .concat(previewAnnotations) + .map(data => data.id || data.image.id) + .sort() + // eslint-disable-next-line no-return-assign + .forEach(id => sigString += id); sigString += sigVer; return dcodeIO.ByteBuffer.wrap(sigString, 'utf8').toArrayBuffer(); } @@ -682,17 +694,26 @@ class LokiPublicChannelAPI { const { timestamp, quote } = noteValue; if (quote) { + // TODO: Enable quote attachments again using proper ADN style quote.attachments = []; } // try to verify signature const { sig, sigver } = noteValue; const annoCopy = [...adnMessage.annotations]; + const attachments = annoCopy + .filter(anno => anno.value.lokiType === LOKI_ATTACHMENT_TYPE) + .map(attachment => ({ isRaw: true, ...attachment.value })); + const preview = annoCopy + .filter(anno => anno.value.lokiType === LOKI_PREVIEW_TYPE) + .map(LokiPublicChannelAPI.getPreviewFromAnnotation); // strip out sig and sigver annoCopy[0] = _.omit(annoCopy[0], ['value.sig', 'value.sigver']); const sigData = LokiPublicChannelAPI.getSigData( sigver, noteValue, + attachments, + preview, adnMessage ); @@ -730,6 +751,8 @@ class LokiPublicChannelAPI { return { timestamp, + attachments, + preview, quote, }; } @@ -789,7 +812,7 @@ class LokiPublicChannelAPI { return; } - const { timestamp, quote } = messengerData; + const { timestamp, quote, attachments, preview } = messengerData; if (!timestamp) { return; // Invalid message } @@ -836,7 +859,7 @@ class LokiPublicChannelAPI { isPublic: true, message: { body: adnMessage.text, - attachments: [], + attachments, group: { id: this.conversationId, type: textsecure.protobuf.GroupContext.Type.DELIVER, @@ -849,7 +872,7 @@ class LokiPublicChannelAPI { sent_at: timestamp, quote, contact: [], - preview: [], + preview, profile: { displayName: from, }, @@ -956,6 +979,8 @@ class LokiPublicChannelAPI { timestamp: messageTimeStamp, }, }, + ...attachmentAnnotations, + ...previewAnnotations, ], }; if (quote && quote.id) { @@ -988,6 +1013,8 @@ class LokiPublicChannelAPI { const sigData = LokiPublicChannelAPI.getSigData( sigVer, payload.annotations[0].value, + attachmentAnnotations.map(anno => anno.value), + previewAnnotations.map(anno => anno.value), mockAdnMessage ); const sig = await libsignal.Curve.async.calculateSignature( diff --git a/js/modules/loki_file_server_api.js b/js/modules/loki_file_server_api.js index 06e062aa5..e6f1ec518 100644 --- a/js/modules/loki_file_server_api.js +++ b/js/modules/loki_file_server_api.js @@ -2,8 +2,6 @@ const LokiAppDotNetAPI = require('./loki_app_dot_net_api'); -/* global log */ - const DEVICE_MAPPING_ANNOTATION_KEY = 'network.loki.messenger.devicemapping'; class LokiFileServerAPI { diff --git a/js/modules/loki_message_api.js b/js/modules/loki_message_api.js index 2c52ea4a3..d3f10a9f1 100644 --- a/js/modules/loki_message_api.js +++ b/js/modules/loki_message_api.js @@ -93,11 +93,7 @@ class LokiMessageAPI { 'Missing public send data for public chat message' ); } - const res = await publicSendData.sendMessage( - data.body, - data.quote, - messageTimeStamp - ); + const res = await publicSendData.sendMessage(data, messageTimeStamp); if (res === false) { throw new window.textsecure.PublicChatError( 'Failed to send public chat message' diff --git a/libtextsecure/sendmessage.js b/libtextsecure/sendmessage.js index 4ce31886d..613954c56 100644 --- a/libtextsecure/sendmessage.js +++ b/libtextsecure/sendmessage.js @@ -168,7 +168,7 @@ MessageSender.prototype = { constructor: MessageSender, // makeAttachmentPointer :: Attachment -> Promise AttachmentPointerProto - makeAttachmentPointer(attachment) { + async makeAttachmentPointer(attachment, publicServer = null) { if (typeof attachment !== 'object' || attachment == null) { return Promise.resolve(undefined); } @@ -185,42 +185,49 @@ MessageSender.prototype = { } const proto = new textsecure.protobuf.AttachmentPointer(); - proto.key = libsignal.crypto.getRandomBytes(64); - - const iv = libsignal.crypto.getRandomBytes(16); - return textsecure.crypto - .encryptAttachment(attachment.data, proto.key, iv) - .then(result => - this.server - .putAttachment(result.ciphertext) - .then(async ({ url, id }) => { - proto.id = id; - proto.url = url; - proto.contentType = attachment.contentType; - proto.digest = result.digest; - - if (attachment.size) { - proto.size = attachment.size; - } - if (attachment.fileName) { - proto.fileName = attachment.fileName; - } - if (attachment.flags) { - proto.flags = attachment.flags; - } - if (attachment.width) { - proto.width = attachment.width; - } - if (attachment.height) { - proto.height = attachment.height; - } - if (attachment.caption) { - proto.caption = attachment.caption; - } - - return proto; - }) + let attachmentData; + let server; + if (publicServer) { + attachmentData = attachment.data; + server = publicServer; + } else { + proto.key = libsignal.crypto.getRandomBytes(64); + const iv = libsignal.crypto.getRandomBytes(16); + const result = await textsecure.crypto.encryptAttachment( + attachment.data, + proto.key, + iv ); + proto.digest = result.digest; + attachmentData = result.ciphertext; + ({ server } = this); + } + + const { url, id } = await server.putAttachment(attachmentData); + proto.id = id; + proto.url = url; + proto.contentType = attachment.contentType; + + if (attachment.size) { + proto.size = attachment.size; + } + if (attachment.fileName) { + proto.fileName = attachment.fileName; + } + if (attachment.flags) { + proto.flags = attachment.flags; + } + if (attachment.width) { + proto.width = attachment.width; + } + if (attachment.height) { + proto.height = attachment.height; + } + if (attachment.caption) { + proto.caption = attachment.caption; + } + + return proto; }, queueJobForNumber(number, runJob) { @@ -243,9 +250,11 @@ MessageSender.prototype = { }); }, - uploadAttachments(message) { + uploadAttachments(message, publicServer) { return Promise.all( - message.attachments.map(this.makeAttachmentPointer.bind(this)) + message.attachments.map(attachment => + this.makeAttachmentPointer(attachment, publicServer) + ) ) .then(attachmentPointers => { // eslint-disable-next-line no-param-reassign @@ -260,12 +269,12 @@ MessageSender.prototype = { }); }, - async uploadLinkPreviews(message) { + async uploadLinkPreviews(message, publicServer) { try { const preview = await Promise.all( (message.preview || []).map(async item => ({ ...item, - image: await this.makeAttachmentPointer(item.image), + image: await this.makeAttachmentPointer(item.image, publicServer), })) ); // eslint-disable-next-line no-param-reassign @@ -279,7 +288,7 @@ MessageSender.prototype = { } }, - uploadThumbnails(message) { + uploadThumbnails(message, publicServer) { const makePointer = this.makeAttachmentPointer.bind(this); const { quote } = message; @@ -294,7 +303,7 @@ MessageSender.prototype = { return null; } - return makePointer(thumbnail).then(pointer => { + return makePointer(thumbnail, publicServer).then(pointer => { // eslint-disable-next-line no-param-reassign attachment.attachmentPointer = pointer; }); @@ -311,11 +320,13 @@ MessageSender.prototype = { sendMessage(attrs, options) { const message = new Message(attrs); const silent = false; + const publicServer = + options.publicSendData && options.publicSendData.serverAPI; return Promise.all([ - this.uploadAttachments(message), - this.uploadThumbnails(message), - this.uploadLinkPreviews(message), + this.uploadAttachments(message, publicServer), + this.uploadThumbnails(message, publicServer), + this.uploadLinkPreviews(message, publicServer), ]).then( () => new Promise((resolve, reject) => { From 8cf90ae85d9224475deb0c57e434b10d78602733 Mon Sep 17 00:00:00 2001 From: Beaudan Brown Date: Mon, 21 Oct 2019 14:29:32 +1100 Subject: [PATCH 4/6] Use message timestamp for empty messages --- js/modules/loki_app_dot_net_api.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/js/modules/loki_app_dot_net_api.js b/js/modules/loki_app_dot_net_api.js index bbc934ec6..92ef192d5 100644 --- a/js/modules/loki_app_dot_net_api.js +++ b/js/modules/loki_app_dot_net_api.js @@ -858,7 +858,8 @@ class LokiPublicChannelAPI { receivedAt, isPublic: true, message: { - body: adnMessage.text, + // == to compare string and number + body: adnMessage.text == timestamp ? '' : adnMessage.text, attachments, group: { id: this.conversationId, @@ -962,7 +963,7 @@ class LokiPublicChannelAPI { // create a message in the channel async sendMessage(data, messageTimeStamp) { const { quote, attachments, preview } = data; - const text = data.body; + const text = data.body || messageTimeStamp.toString(); const attachmentAnnotations = attachments.map( LokiPublicChannelAPI.getAnnotationFromAttachment ); From 221c6b53b1113fbd9e18657546a07287d1216f35 Mon Sep 17 00:00:00 2001 From: Beaudan Brown Date: Mon, 21 Oct 2019 15:58:40 +1100 Subject: [PATCH 5/6] Clean up a bit --- js/modules/loki_app_dot_net_api.js | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/js/modules/loki_app_dot_net_api.js b/js/modules/loki_app_dot_net_api.js index 92ef192d5..c0670aedd 100644 --- a/js/modules/loki_app_dot_net_api.js +++ b/js/modules/loki_app_dot_net_api.js @@ -666,12 +666,10 @@ class LokiPublicChannelAPI { sigString += adnMessage.reply_to; } } - attachmentAnnotations - .concat(previewAnnotations) + sigString += [...attachmentAnnotations, ...previewAnnotations] .map(data => data.id || data.image.id) .sort() - // eslint-disable-next-line no-return-assign - .forEach(id => sigString += id); + .join(); sigString += sigVer; return dcodeIO.ByteBuffer.wrap(sigString, 'utf8').toArrayBuffer(); } @@ -858,8 +856,8 @@ class LokiPublicChannelAPI { receivedAt, isPublic: true, message: { - // == to compare string and number - body: adnMessage.text == timestamp ? '' : adnMessage.text, + body: + adnMessage.text === timestamp.toString() ? '' : adnMessage.text, attachments, group: { id: this.conversationId, @@ -901,8 +899,8 @@ class LokiPublicChannelAPI { caption: annotation.value.caption, contentType: annotation.value.contentType, digest: annotation.value.digest, - fileName: annotation.value.fileName || '', - flags: annotation.value.flags || '0', + fileName: annotation.value.fileName, + flags: annotation.value.flags, height: annotation.value.height, id: annotation.value.id, key: annotation.value.key, @@ -929,8 +927,8 @@ class LokiPublicChannelAPI { caption: preview.image.caption, contentType: preview.image.contentType, digest: preview.image.digest, - fileName: preview.image.fileName || '', - flags: preview.image.flags || '0', + fileName: preview.image.fileName, + flags: preview.image.flags, height: preview.image.height, id: preview.image.id, key: preview.image.key, From b66abca17d07571fc4529c9922be8633ba017131 Mon Sep 17 00:00:00 2001 From: Beaudan Brown Date: Tue, 22 Oct 2019 11:31:20 +1100 Subject: [PATCH 6/6] Disable files for friend requests --- background.html | 2 +- js/views/conversation_view.js | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/background.html b/background.html index d68fbb83a..a2c80e74b 100644 --- a/background.html +++ b/background.html @@ -136,7 +136,7 @@
-
+
diff --git a/js/views/conversation_view.js b/js/views/conversation_view.js index 8826a89c1..482e15e29 100644 --- a/js/views/conversation_view.js +++ b/js/views/conversation_view.js @@ -93,6 +93,7 @@ initialize(options) { this.listenTo(this.model, 'destroy', this.stopListening); this.listenTo(this.model, 'change:verified', this.onVerifiedChange); + this.listenTo(this.model, 'change:friendRequestStatus', this.updateFileLock); this.listenTo(this.model, 'newmessage', this.addMessage); this.listenTo(this.model, 'opened', this.onOpened); this.listenTo(this.model, 'prune', this.onPrune); @@ -160,6 +161,7 @@ ); this.render(); + this.updateFileLock(); this.model.updateTextInputState(); @@ -621,6 +623,14 @@ } }, + updateFileLock() { + if (this.model.isPrivate() && !this.model.isFriend()) { + this.$('#choose-file').hide() + } else { + this.$('#choose-file').show() + } + }, + toggleMicrophone() { // ALWAYS HIDE until we support audio this.$('.capture-audio').hide();