diff --git a/package-lock.json b/package-lock.json
index 154d5fc..a0e75f3 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,6 +11,7 @@
"axios": "^0.21.0",
"core-js": "^3.6.5",
"intersection-observer": "^0.11.0",
+ "js-sha256": "^0.9.0",
"json-web-key": "^0.4.0",
"material-design-icons-iconfont": "^5.0.1",
"matrix-js-sdk": "^9.0.1",
@@ -7903,6 +7904,11 @@
"node": ">=1.0.0"
}
},
+ "node_modules/js-sha256": {
+ "version": "0.9.0",
+ "resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.9.0.tgz",
+ "integrity": "sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA=="
+ },
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -21073,6 +21079,11 @@
"easy-stack": "^1.0.1"
}
},
+ "js-sha256": {
+ "version": "0.9.0",
+ "resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.9.0.tgz",
+ "integrity": "sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA=="
+ },
"js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
diff --git a/package.json b/package.json
index f17d1d4..0c68e04 100644
--- a/package.json
+++ b/package.json
@@ -12,6 +12,7 @@
"axios": "^0.21.0",
"core-js": "^3.6.5",
"intersection-observer": "^0.11.0",
+ "js-sha256": "^0.9.0",
"json-web-key": "^0.4.0",
"material-design-icons-iconfont": "^5.0.1",
"matrix-js-sdk": "^9.0.1",
diff --git a/src/components/Chat.vue b/src/components/Chat.vue
index 063291c..4b94fb0 100644
--- a/src/components/Chat.vue
+++ b/src/components/Chat.vue
@@ -65,10 +65,10 @@
- Cancel
- Send
+ Send
@@ -89,6 +89,7 @@ import RoomAvatarChanged from "./messages/RoomAvatarChanged.vue";
import DebugEvent from "./messages/DebugEvent.vue";
import MessageOutgoingImage from "./messages/MessageOutgoingImage.vue";
import MessageIncomingImage from "./messages/MessageIncomingImage.vue";
+import util from "../plugins/utils";
// from https://kirbysayshi.com/2013/08/19/maintaining-scroll-position-knockoutjs-list.html
function ScrollPosition(node) {
@@ -211,7 +212,9 @@ export default {
);
this.timelineWindow.load(null, 20).then(() => {
this.events = this.timelineWindow.getEvents();
- this.paginateBackIfNeeded();
+ this.$nextTick(() => {
+ this.paginateBackIfNeeded();
+ });
});
},
},
@@ -316,7 +319,13 @@ export default {
sendMessage() {
if (this.currentInput.length > 0) {
- this.sendMatrixMessage(this.currentInput);
+ util.sendTextMessage(this.$matrix.matrixClient, this.roomId, this.currentInput)
+ .then(() => {
+ console.log("Sent message");
+ })
+ .catch(err => {
+ console.log("Failed to send:", err);
+ })
this.currentInput = "";
}
},
@@ -346,87 +355,27 @@ export default {
sendAttachment() {
if (this.currentImageInputPath) {
- const opts = {
- progressHandler: this.onUploadProgress,
- };
- this.currentSendOperation = this.$matrix.uploadFile(
- this.currentImageInputPath,
- opts
- );
- var matrixUri;
+ this.currentSendProgress = 0;
+ this.currentSendOperation = util.sendEncyptedImage(this.$matrix.matrixClient, this.roomId, this.currentImageInputPath, this.onUploadProgress);
this.currentSendOperation
- .then((uri) => {
- matrixUri = uri;
- return this.$matrix.matrixClient.sendImageMessage(
- this.roomId,
- matrixUri,
- "",
- "Image",
- null
- );
- })
- .then((result) => {
- console.log("Image sent: ", result);
- })
- .catch((err) => {
- console.log("Image send error: ", err);
- if (err && err.name == "UnknownDeviceError") {
- console.log("Unknown devices. Mark as known before retrying.");
- var setAsKnownPromises = [];
- for (var user of Object.keys(err.devices)) {
- const userDevices = err.devices[user];
- for (var deviceId of Object.keys(userDevices)) {
- const deviceInfo = userDevices[deviceId];
- if (!deviceInfo.known) {
- setAsKnownPromises.push(
- this.$matrix.matrixClient.setDeviceKnown(
- user,
- deviceId,
- true
- )
- );
- }
- }
- }
- Promise.all(setAsKnownPromises)
- .then(() => {
- // All devices now marked as "known", try to resend
- return this.$matrix.matrixClient.sendImageMessage(
- this.roomId,
- matrixUri,
- "",
- "Image",
- null
- );
- })
- .then((result) => {
- console.log("Image sent: ", result);
- })
- .catch((err) => {
- // Still error, abort
- this.currentSendError = err.toLocaleString();
- });
- } else {
- this.currentSendError = err.toLocaleString();
- }
- });
+ .then(() => {
+ this.currentSendOperation = null;
+ this.currentImageInput = null;
+ this.currentSendProgress = 0;
+ })
+ .catch(err => {
+ this.currentSendError = err.toLocaleString();
+ this.currentSendOperation = null;
+ this.currentSendProgress = 0;
+ });
}
},
- sendMatrixMessage(body) {
- var content = {
- body: body,
- msgtype: "m.text",
- };
- this.$matrix.matrixClient.sendEvent(
- this.roomId,
- "m.room.message",
- content,
- "",
- (err, ignoredres) => {
- console.log(err);
- }
- );
+ cancelSendAttachment() {
+ if (this.currentSendOperation) {
+ this.currentSendOperation.reject("Canceled");
+ }
+ this.currentImageInput = null;
},
handleScrolledToTop() {
diff --git a/src/components/messages/MessageIncomingImage.vue b/src/components/messages/MessageIncomingImage.vue
index d04ea8a..d259ce3 100644
--- a/src/components/messages/MessageIncomingImage.vue
+++ b/src/components/messages/MessageIncomingImage.vue
@@ -23,7 +23,6 @@
diff --git a/src/plugins/utils.js b/src/plugins/utils.js
index c2d2640..96681af 100644
--- a/src/plugins/utils.js
+++ b/src/plugins/utils.js
@@ -1,9 +1,18 @@
import axios from 'axios';
+import * as ContentHelpers from "matrix-js-sdk/lib/content-helpers";
+var sha256 = require('js-sha256').sha256;
+var aesjs = require('aes-js');
+var base64Url = require('json-web-key/lib/base64url');
class Util {
getThumbnail(matrixClient, event, ignoredw, ignoredh) {
return new Promise((resolve, reject) => {
const content = event.getContent();
+ if (content.url != null) {
+ // Unencrypted, just return!
+ resolve(matrixClient.mxcUrlToHttp(content.url));
+ return;
+ }
var url = null;
var file = null;
if (
@@ -38,13 +47,20 @@ class Util {
axios.get(url, { responseType: 'arraybuffer' })
.then(response => {
return new Promise((resolve, ignoredReject) => {
- var aesjs = require('aes-js');
- var base64Url = require('json-web-key/lib/base64url');
var key = base64Url.decode(file.key.k);
var iv = base64Url.decode(file.iv);
var aesCtr = new aesjs.ModeOfOperation.ctr(key, new aesjs.Counter(iv));
const data = new Uint8Array(response.data);
+
+ // Calculate sha256 and compare hashes
+ var hash = new Uint8Array(sha256.create().update(data).arrayBuffer());
+ const originalHash = base64Url.decode(file.hashes.sha256);
+ if (Buffer.compare(Buffer.from(hash), Buffer.from(originalHash.buffer)) != 0) {
+ reject("Hashes don't match!");
+ return;
+ }
+
var decryptedBytes = aesCtr.decrypt(data);
resolve(decryptedBytes);
});
@@ -58,6 +74,130 @@ class Util {
});
});
}
+
+ sendTextMessage(matrixClient, roomId, text) {
+ return this.sendMessage(matrixClient, roomId, ContentHelpers.makeTextMessage(text));
+ }
+
+ sendMessage(matrixClient, roomId, content) {
+ return new Promise((resolve, reject) => {
+ matrixClient.sendMessage(roomId, content, undefined, undefined)
+ .then((result) => {
+ console.log("Message sent: ", result);
+ resolve(true);
+ })
+ .catch(err => {
+ console.log("Image send error: ", err);
+ if (err && err.name == "UnknownDeviceError") {
+ console.log("Unknown devices. Mark as known before retrying.");
+ var setAsKnownPromises = [];
+ for (var user of Object.keys(err.devices)) {
+ const userDevices = err.devices[user];
+ for (var deviceId of Object.keys(userDevices)) {
+ const deviceInfo = userDevices[deviceId];
+ if (!deviceInfo.known) {
+ setAsKnownPromises.push(
+ matrixClient.setDeviceKnown(
+ user,
+ deviceId,
+ true
+ )
+ );
+ }
+ }
+ }
+ Promise.all(setAsKnownPromises)
+ .then(() => {
+ // All devices now marked as "known", try to resend
+ matrixClient.resendEvent(err.event)
+ .then((result) => {
+ console.log("Message sent: ", result);
+ resolve(true);
+ })
+ .catch((err) => {
+ // Still error, abort
+ reject(err.toLocaleString());
+ });
+ });
+ }
+ else {
+ reject(err.toLocaleString());
+ }
+ });
+ });
+ }
+
+ sendEncyptedImage(matrixClient, roomId, file, onUploadProgress) {
+ return new Promise((resolve, reject) => {
+ var reader = new FileReader();
+ reader.onload = (e) => {
+ const fileContents = e.target.result;
+ const crypto = require('crypto');
+ let key = crypto.randomBytes(256 / 8);
+ let iv = Buffer.concat([crypto.randomBytes(8),Buffer.alloc(8)]); // Initialization vector.
+
+ // Encrypt
+ var aesCtr = new aesjs.ModeOfOperation.ctr(key, new aesjs.Counter(iv));
+ const data = new Uint8Array(fileContents);
+ var encryptedBytes = aesCtr.encrypt(data);
+
+ // Calculate sha256
+ var hash = new Uint8Array(sha256.create().update(encryptedBytes).arrayBuffer());
+
+ const jwk = {
+ kty: 'oct',
+ key_opts: ['encrypt', 'decrypt'],
+ alg: 'A256CTR',
+ k: base64Url.encode(key),
+ ext: true
+ };
+
+ const encryptedFile = {
+ mimetype: file.type,
+ key: jwk,
+ iv: Buffer.from(iv).toString('base64').replace( /=/g, '' ),
+ hashes: { sha256: Buffer.from(hash).toString('base64').replace( /=/g, '' )},
+ v: 'v2'
+ };
+ console.log("Encrypted file:", encryptedFile);
+
+ const info = {
+ mimetype: file.type,
+ size: file.size
+ };
+
+
+ const opts = {
+ type: file.type,
+ name: 'Image',
+ progressHandler: onUploadProgress,
+ };
+
+ const messageContent = {
+ body: 'Image',
+ file: encryptedFile,
+ info: info,
+ msgtype: 'm.image'
+ }
+
+ matrixClient.uploadContent(encryptedBytes, opts)
+ .then((uri) => {
+ encryptedFile.url = uri;
+ return this.sendMessage(matrixClient, roomId, messageContent)
+ })
+ .then(result => {
+ resolve(result);
+ })
+ .catch(err => {
+ reject(err);
+ });
+ }
+ reader.onerror = (err) => {
+ reject(err);
+ }
+ reader.readAsArrayBuffer(file);
+ });
+ }
}
export default new Util();