From d6651b743741d7072a68b751c439e1dce0be694d Mon Sep 17 00:00:00 2001 From: Sharon Kennedy Date: Sat, 25 Jul 2020 19:00:28 -0400 Subject: [PATCH] encrypt transcript attachment --- package.json | 1 + src/bot.js | 63 +++++++++++---- src/encrypt-attachment.js | 158 ++++++++++++++++++++++++++++++++++++++ yarn.lock | 87 ++++++++++++++++++++- 4 files changed, 291 insertions(+), 18 deletions(-) create mode 100644 src/encrypt-attachment.js diff --git a/package.json b/package.json index 29a3ea2..8698da9 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "dotenv": "^8.2.0", "matrix-js-sdk": "^6.2.1", "node-localstorage": "^2.1.5", + "node-webcrypto-ossl": "^2.1.0", "olm": "https://packages.matrix.org/npm/olm/olm-3.1.4.tgz", "uuidv4": "^6.0.2", "winston": "^3.2.1" diff --git a/src/bot.js b/src/bot.js index bb0806d..eaa1fcf 100644 --- a/src/bot.js +++ b/src/bot.js @@ -9,6 +9,7 @@ global.Olm = require("olm"); import * as matrix from "matrix-js-sdk"; import logger from "./logger"; +import encrypt from "./encrypt-attachment"; class OcrccBot { @@ -289,26 +290,54 @@ class OcrccBot { ); } - const filename = path.basename(transcriptFile) || "Transcript"; - const file = fs.readFileSync(transcriptFile); - const stats = fs.statSync(transcriptFile); + if (this.client.isRoomEncrypted(roomId)) { + let encryptInfo; + const filename = path.basename(transcriptFile) || "Transcript"; + const data = fs.readFileSync(transcriptFile); + const encryptResult = await encrypt.encryptAttachment(data) + const buffer = Buffer.from(encryptResult.data) + encryptInfo = encryptResult.info; + const url = await this.client.uploadContent(buffer, { rawResponse: false, name: filename }) + encryptInfo.url = url.content_uri; + encryptInfo.mimetype = 'text/plain' - const url = await this.client.uploadContent(file, { rawResponse: false, name: filename }) - logger.log('info', url) - - const content = { - msgtype: "m.file", - body: filename, - info: { - size: stats.size, + const content = { + msgtype: "m.file", + body: filename, + info: { + mimetype: 'text/plain' + }, + file: encryptInfo, + url: url.content_uri, + showToUser: senderId, mimetype: 'text/plain' - }, - url: url.content_uri, - showToUser: senderId, - mimetype: 'text/plain' - }; + }; + + this.sendMessage(roomId, content); + + } else { + const filename = path.basename(transcriptFile) || "Transcript"; + const file = fs.readFileSync(transcriptFile); + const stats = fs.statSync(transcriptFile); + + const url = await this.client.uploadContent(file, { rawResponse: false, name: filename }) + logger.log('info', url) + + const content = { + msgtype: "m.file", + body: filename, + info: { + size: stats.size, + mimetype: 'text/plain' + }, + url: url.content_uri, + showToUser: senderId, + mimetype: 'text/plain' + }; + + this.sendMessage(roomId, content); + } - this.sendMessage(roomId, content); } catch(err) { logger.log("error", `ERROR UPLOADING CONTENT: ${err}`); this.sendTextMessage( diff --git a/src/encrypt-attachment.js b/src/encrypt-attachment.js new file mode 100644 index 0000000..51e3543 --- /dev/null +++ b/src/encrypt-attachment.js @@ -0,0 +1,158 @@ +const { Crypto } = require("node-webcrypto-ossl"); + +const crypto = new Crypto(); + + +// this is from https://github.com/matrix-org/browser-encrypt-attachment +// which is the library used by matrix-reack-sdk to encrypt and decrypt attachments +// just dropped in node-webcrypto-ossl to replace window.crypto +// and Buffer for base64 encoding/decoding instead of window.btoa/window.atob + + + +/** + * Encrypt an attachment. + * @param {ArrayBuffer} plaintextBuffer The attachment data buffer. + * @return {Promise} A promise that resolves with an object when the attachment is encrypted. + * The object has a "data" key with an ArrayBuffer of encrypted data and an "info" key + * with an object containing the info needed to decrypt the data. + */ +function encryptAttachment(plaintextBuffer) { + var cryptoKey; // The AES key object. + var exportedKey; // The AES key exported as JWK. + var ciphertextBuffer; // ArrayBuffer of encrypted data. + var sha256Buffer; // ArrayBuffer of digest. + var ivArray; // Uint8Array of AES IV + // Generate an IV where the first 8 bytes are random and the high 8 bytes + // are zero. We set the counter low bits to 0 since it makes it unlikely + // that the 64 bit counter will overflow. + ivArray = new Uint8Array(16); + crypto.getRandomValues(ivArray.subarray(0,8)); + // Load the encryption key. + return crypto.subtle.generateKey( + {"name": "AES-CTR", length: 256}, true, ["encrypt", "decrypt"] + ).then(function(generateKeyResult) { + cryptoKey = generateKeyResult; + // Export the Key as JWK. + return crypto.subtle.exportKey("jwk", cryptoKey); + }).then(function(exportKeyResult) { + exportedKey = exportKeyResult; + // Encrypt the input ArrayBuffer. + // Use half of the iv as the counter by setting the "length" to 64. + return crypto.subtle.encrypt( + {name: "AES-CTR", counter: ivArray, length: 64}, cryptoKey, plaintextBuffer + ); + }).then(function(encryptResult) { + ciphertextBuffer = encryptResult; + // SHA-256 the encrypted data. + return crypto.subtle.digest("SHA-256", ciphertextBuffer); + }).then(function (digestResult) { + sha256Buffer = digestResult; + + return { + data: ciphertextBuffer, + info: { + v: "v2", + key: exportedKey, + iv: encodeBase64(ivArray), + hashes: { + sha256: encodeBase64(new Uint8Array(sha256Buffer)), + }, + }, + }; + }); +} + +/** + * Decrypt an attachment. + * @param {ArrayBuffer} ciphertextBuffer The encrypted attachment data buffer. + * @param {Object} info The information needed to decrypt the attachment. + * @param {Object} info.key AES-CTR JWK key object. + * @param {string} info.iv Base64 encoded 16 byte AES-CTR IV. + * @param {string} info.hashes.sha256 Base64 encoded SHA-256 hash of the ciphertext. + * @return {Promise} A promise that resolves with an ArrayBuffer when the attachment is decrypted. + */ +function decryptAttachment(ciphertextBuffer, info) { + + if (info === undefined || info.key === undefined || info.iv === undefined + || info.hashes === undefined || info.hashes.sha256 === undefined) { + throw new Error("Invalid info. Missing info.key, info.iv or info.hashes.sha256 key"); + } + + var cryptoKey; // The AES key object. + var ivArray = decodeBase64(info.iv); + var expectedSha256base64 = info.hashes.sha256; + // Load the AES from the "key" key of the info object. + return crypto.subtle.importKey( + "jwk", info.key, {"name": "AES-CTR"}, false, ["encrypt", "decrypt"] + ).then(function (importKeyResult) { + cryptoKey = importKeyResult; + // Check the sha256 hash + return crypto.subtle.digest("SHA-256", ciphertextBuffer); + }).then(function (digestResult) { + if (encodeBase64(new Uint8Array(digestResult)) != expectedSha256base64) { + throw new Error("Mismatched SHA-256 digest"); + } + var counterLength; + if (info.v == "v1" || info.v == "v2") { + // Version 1 and 2 use a 64 bit counter. + counterLength = 64; + } else { + // Version 0 uses a 128 bit counter. + counterLength = 128; + } + return crypto.subtle.decrypt( + {name: "AES-CTR", counter: ivArray, length: counterLength}, cryptoKey, ciphertextBuffer + ); + }); +} + +/** + * Encode a typed array of uint8 as base64. + * @param {Uint8Array} uint8Array The data to encode. + * @return {string} The base64 without padding. + */ +function encodeBase64(uint8Array) { + // Misinterpt the Uint8Array as Latin-1. + // window.btoa expects a unicode string with codepoints in the range 0-255. + var latin1String = String.fromCharCode.apply(null, uint8Array); + // Use the builtin base64 encoder. + // var paddedBase64 = window.btoa(latin1String); + var paddedBase64 = Buffer.from(latin1String, 'binary').toString('base64') + // Calculate the unpadded length. + var inputLength = uint8Array.length; + var outputLength = 4 * Math.floor((inputLength + 2) / 3) + (inputLength + 2) % 3 - 2; + // Return the unpadded base64. + return paddedBase64.slice(0, outputLength); +} + +/** + * Decode a base64 string to a typed array of uint8. + * This will decode unpadded base64, but will also accept base64 with padding. + * @param {string} base64 The unpadded base64 to decode. + * @return {Uint8Array} The decoded data. + */ +function decodeBase64(base64) { + // Pad the base64 up to the next multiple of 4. + var paddedBase64 = base64 + "===".slice(0, (4 - base64.length % 4) % 4); + // Decode the base64 as a misinterpreted Latin-1 string. + // window.atob returns a unicode string with codepoints in the range 0-255. + // var latin1String = window.atob(paddedBase64); + var latin1String = Buffer.from(paddedBase64, 'base64').toString('binary') + // Encode the string as a Uint8Array as Latin-1. + var uint8Array = new Uint8Array(latin1String.length); + for (var i = 0; i < latin1String.length; i++) { + uint8Array[i] = latin1String.charCodeAt(i); + } + return uint8Array; +} + +try { + exports.encryptAttachment = encryptAttachment; + exports.decryptAttachment = decryptAttachment; +} +catch (e) { + // Ignore unknown variable "exports" errors when this is loaded directly into a browser + // This means that we can test it without having to use browserify. + // The intention is that the library is used using browserify. +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 9f628b9..492f98c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1257,6 +1257,23 @@ "@types/yargs" "^15.0.0" chalk "^3.0.0" +"@peculiar/asn1-schema@^2.0.1": + version "2.0.8" + resolved "https://registry.yarnpkg.com/@peculiar/asn1-schema/-/asn1-schema-2.0.8.tgz#bafb74388590f6ec3d53d1b2a4fdbe66d44224a4" + integrity sha512-D8ZqT61DdzuXfrILNvtdf7MUcTY2o9WHwmF0WgTKPEGNY5SDxNAjBY3enuwV9SXcSuCAwWac9c9v0vsswB1NIw== + dependencies: + "@types/asn1js" "^0.0.1" + asn1js "^2.0.26" + pvtsutils "^1.0.10" + tslib "^1.11.1" + +"@peculiar/json-schema@^1.1.10": + version "1.1.12" + resolved "https://registry.yarnpkg.com/@peculiar/json-schema/-/json-schema-1.1.12.tgz#fe61e85259e3b5ba5ad566cb62ca75b3d3cd5339" + integrity sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w== + dependencies: + tslib "^2.0.0" + "@sinonjs/commons@^1.7.0": version "1.7.1" resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.7.1.tgz#da5fd19a5f71177a53778073978873964f49acf1" @@ -1264,6 +1281,13 @@ dependencies: type-detect "4.0.8" +"@types/asn1js@^0.0.1": + version "0.0.1" + resolved "https://registry.yarnpkg.com/@types/asn1js/-/asn1js-0.0.1.tgz#ef8b9f9708cb1632a1c3a9cd27717caabe793bc2" + integrity sha1-74uflwjLFjKhw6nNJ3F8qr55O8I= + dependencies: + "@types/pvutils" "*" + "@types/babel__core@^7.1.0": version "7.1.6" resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.6.tgz#16ff42a5ae203c9af1c6e190ed1f30f83207b610" @@ -1322,6 +1346,11 @@ "@types/istanbul-lib-coverage" "*" "@types/istanbul-lib-report" "*" +"@types/pvutils@*": + version "0.0.2" + resolved "https://registry.yarnpkg.com/@types/pvutils/-/pvutils-0.0.2.tgz#e21684962cfa58ac920fd576d90556032dc86009" + integrity sha512-CgQAm7pjyeF3Gnv78ty4RBVIfluB+Td+2DR8iPaU0prF18pkzptHHP+DoKPfpsJYknKsVZyVsJEu5AuGgAqQ5w== + "@types/stack-utils@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e" @@ -1486,6 +1515,13 @@ asn1@~0.2.3: dependencies: safer-buffer "~2.1.0" +asn1js@^2.0.26: + version "2.0.26" + resolved "https://registry.yarnpkg.com/asn1js/-/asn1js-2.0.26.tgz#0a6d435000f556a96c6012969d9704d981b71251" + integrity sha512-yG89F0j9B4B0MKIcFyWWxnpZPLaNTjCj4tkE3fjbAoo0qmpGw0PYYqSbX/4ebnd9Icn8ZgK4K1fvDyEtW1JYtQ== + dependencies: + pvutils latest + assert-plus@1.0.0, assert-plus@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" @@ -3863,6 +3899,11 @@ mkdirp@^0.5.1: dependencies: minimist "0.0.8" +mkdirp@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" + integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== + ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" @@ -3873,7 +3914,7 @@ ms@^2.1.1: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -nan@^2.12.1: +nan@^2.12.1, nan@^2.14.1: version "2.14.1" resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.1.tgz#d7be34dfa3105b91494c3147089315eff8874b01" integrity sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw== @@ -3953,6 +3994,17 @@ node-releases@^1.1.53: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.53.tgz#2d821bfa499ed7c5dffc5e2f28c88e78a08ee3f4" integrity sha512-wp8zyQVwef2hpZ/dJH7SfSrIPD6YoJz6BDQDpGEkcA0s3LpAQoxBIYmfIq6QAhC1DhwsyCgTaTTcONwX8qzCuQ== +node-webcrypto-ossl@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/node-webcrypto-ossl/-/node-webcrypto-ossl-2.1.0.tgz#cee0e22fa9d9325a19f260539d0854ba143595db" + integrity sha512-diC2LLQKKo41XxrgdT2MmH4mxWNoeCwjS0+uSLfui3rCtxai8cdLLs0TKT0z9Mq8LZ4eMAkuU4FFmEOHk6CbsQ== + dependencies: + mkdirp "^1.0.4" + nan "^2.14.1" + pvtsutils "^1.0.10" + tslib "^1.11.2" + webcrypto-core "^1.1.0" + nodemon@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-2.0.2.tgz#9c7efeaaf9b8259295a97e5d4585ba8f0cbe50b0" @@ -4354,6 +4406,18 @@ punycode@^2.1.0, punycode@^2.1.1: resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== +pvtsutils@^1.0.10: + version "1.0.10" + resolved "https://registry.yarnpkg.com/pvtsutils/-/pvtsutils-1.0.10.tgz#157d0fcb853f570d32e0f8788179f3057eacdf38" + integrity sha512-8ZKQcxnZKTn+fpDh7wL4yKax5fdl3UJzT8Jv49djZpB/dzPxacyN1Sez90b6YLdOmvIr9vaySJ5gw4aUA1EdSw== + dependencies: + tslib "^1.10.0" + +pvutils@latest: + version "1.0.17" + resolved "https://registry.yarnpkg.com/pvutils/-/pvutils-1.0.17.tgz#ade3c74dfe7178944fe44806626bd2e249d996bf" + integrity sha512-wLHYUQxWaXVQvKnwIDWFVKDJku9XDCvyhhxoq8dc5MFdIlRenyPI9eSfEtcvgHgD7FlvCyGAlWgOzRnZD99GZQ== + qs@^6.5.2: version "6.9.1" resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.1.tgz#20082c65cb78223635ab1a9eaca8875a29bf8ec9" @@ -5161,6 +5225,16 @@ triple-beam@^1.2.0, triple-beam@^1.3.0: resolved "https://registry.yarnpkg.com/triple-beam/-/triple-beam-1.3.0.tgz#a595214c7298db8339eeeee083e4d10bd8cb8dd9" integrity sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw== +tslib@^1.10.0, tslib@^1.11.1, tslib@^1.11.2: + version "1.13.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043" + integrity sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q== + +tslib@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.0.0.tgz#18d13fc2dce04051e20f074cc8387fd8089ce4f3" + integrity sha512-lTqkx847PI7xEDYJntxZH89L2/aXInsyF2luSafe/+0fHOMjlBNXdH6th7f70qxLDhul7KZK0zC8V5ZIyHl0/g== + tunnel-agent@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" @@ -5392,6 +5466,17 @@ walker@^1.0.7, walker@~1.0.5: dependencies: makeerror "1.0.x" +webcrypto-core@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/webcrypto-core/-/webcrypto-core-1.1.2.tgz#c522a9e5596688f2b6bb19e2d336f68efa8bdd57" + integrity sha512-LxM/dTcXr/ZnwwKLox0tGEOIqvP7KIJ4Hk/fFPX20tr1EgqTmpEFZinmu4FzoGVbs6e4jI1priQKCDrOBD3L6w== + dependencies: + "@peculiar/asn1-schema" "^2.0.1" + "@peculiar/json-schema" "^1.1.10" + asn1js "^2.0.26" + pvtsutils "^1.0.10" + tslib "^1.11.2" + webidl-conversions@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad"