diff --git a/background.html b/background.html index 04040201f..a2c80e74b 100644 --- a/background.html +++ b/background.html @@ -136,9 +136,9 @@
-
+
- +
diff --git a/js/modules/loki_app_dot_net_api.js b/js/modules/loki_app_dot_net_api.js index eb113d860..c0670aedd 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 @@ -11,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(); @@ -344,6 +349,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 { @@ -606,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; @@ -618,6 +666,10 @@ class LokiPublicChannelAPI { sigString += adnMessage.reply_to; } } + sigString += [...attachmentAnnotations, ...previewAnnotations] + .map(data => data.id || data.image.id) + .sort() + .join(); sigString += sigVer; return dcodeIO.ByteBuffer.wrap(sigString, 'utf8').toArrayBuffer(); } @@ -640,17 +692,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 ); @@ -688,6 +749,8 @@ class LokiPublicChannelAPI { return { timestamp, + attachments, + preview, quote, }; } @@ -747,7 +810,7 @@ class LokiPublicChannelAPI { return; } - const { timestamp, quote } = messengerData; + const { timestamp, quote, attachments, preview } = messengerData; if (!timestamp) { return; // Invalid message } @@ -793,8 +856,9 @@ class LokiPublicChannelAPI { receivedAt, isPublic: true, message: { - body: adnMessage.text, - attachments: [], + body: + adnMessage.text === timestamp.toString() ? '' : adnMessage.text, + attachments, group: { id: this.conversationId, type: textsecure.protobuf.GroupContext.Type.DELIVER, @@ -807,7 +871,7 @@ class LokiPublicChannelAPI { sent_at: timestamp, quote, contact: [], - preview: [], + preview, profile: { displayName: from, }, @@ -826,8 +890,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, + 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, + 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 || messageTimeStamp.toString(); + const attachmentAnnotations = attachments.map( + LokiPublicChannelAPI.getAnnotationFromAttachment + ); + const previewAnnotations = preview.map( + LokiPublicChannelAPI.getAnnotationFromPreview + ); + const payload = { text, annotations: [ @@ -837,6 +978,8 @@ class LokiPublicChannelAPI { timestamp: messageTimeStamp, }, }, + ...attachmentAnnotations, + ...previewAnnotations, ], }; if (quote && quote.id) { @@ -869,6 +1012,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 b70984ccc..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 { @@ -38,28 +36,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/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/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/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(); 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 { 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) => {