Send encrypted images
This commit is contained in:
parent
43e876ba80
commit
a164571218
6 changed files with 195 additions and 87 deletions
11
package-lock.json
generated
11
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -65,10 +65,10 @@
|
|||
<v-divider></v-divider>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="primary" text @click="currentImageInput = null"
|
||||
<v-btn color="primary" text @click="cancelSendAttachment"
|
||||
>Cancel</v-btn
|
||||
>
|
||||
<v-btn color="primary" text @click="sendAttachment">Send</v-btn>
|
||||
<v-btn color="primary" text @click="sendAttachment" :disabled="currentSendOperation != null">Send</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@
|
|||
|
||||
<script>
|
||||
import messageMixin from "./messageMixin";
|
||||
//import axios from 'axios';
|
||||
import util from "../../plugins/utils";
|
||||
|
||||
export default {
|
||||
|
|
@ -34,7 +33,7 @@ export default {
|
|||
};
|
||||
},
|
||||
mounted() {
|
||||
console.log("Mounted with event:", JSON.stringify(this.event.getContent()));
|
||||
//console.log("Mounted with event:", JSON.stringify(this.event.getContent()));
|
||||
const width = this.$refs.image.$el.clientWidth;
|
||||
const height = (width * 9) / 16;
|
||||
util
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
|
||||
<script>
|
||||
import messageMixin from "./messageMixin";
|
||||
import util from "../../plugins/utils";
|
||||
|
||||
export default {
|
||||
mixins: [messageMixin],
|
||||
|
|
@ -26,7 +27,14 @@ export default {
|
|||
mounted() {
|
||||
const width = this.$refs.image.$el.clientWidth;
|
||||
const height = (width * 9) / 16;
|
||||
this.src = this.$matrix.matrixClient.mxcUrlToHttp(this.event.getContent().url, width, height, 'scale', false);
|
||||
util
|
||||
.getThumbnail(this.$matrix.matrixClient, this.event, width, height)
|
||||
.then((url) => {
|
||||
this.src = url;
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log("Failed to fetch thumbnail: ", err);
|
||||
});
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.src) {
|
||||
|
|
@ -34,7 +42,7 @@ export default {
|
|||
this.src = null;
|
||||
URL.revokeObjectURL(objectUrl);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue