Thanks, Carl. When I posted, I had never used RabbitMQ and didn’t fully grasp how DocsCoServer and FileConverter interacted. I actually wound up getting this to work with a little hackery. Here is the basic process:
- Create a NC app that encrypts the user’s session cookies with the JWT passphrase and sets this data as an insecure cookie.
- Modify
sdkjs/common/docscoapi.js::DocsCoApi.prototype.auth
to add a 'sessionData': document.cookie
field to the payload. (OnlyOffice uses websockets, but cookies are not sent with websockets. This sends the encrypted session cookies from step 1 to the server as part of the websocket body).
- Add the following object and a variable to store the session data to
utils.js
:
https://github.com/brainfoolong/cryptojs-aes-php/blob/master/dist/cryptojs-aes-format.js
- Modify the following functions to set, get, receive, and pass the encrypted cookie data throughout the system as necessary:
DocsCoServer.js::addTask
DocsCoServer.js::exports.install
(under conn.on
)
FileConverter.js::receiveTask
utils.js::downloadUrlPromise
utils.js::postRequestPromise
taskqueueRabbitMQ.js::TaskQueueRabbitMQ.prototype.addTask
I have a working system that integrates perfectly with Keycloak and NC with per-user encryption enabled (occ encryption:enable-master-key disable
). It is a unique but extremely secure configuration. I would be happy to share my code if your team is interested.
Thanks!
Edit: here’s a really hacky bash script that should patch the OnlyOffice sources before build. I could have used a patch file, but I wanted to write it in a way that would keep things immune from updates to the files down the road.
sed "s#\( \+\)\('indexUser': this._indexUser,\)#\1\2\n\1'sessionData': document.cookie#g" sdkjs/common/docscoapi.js
sed "s#\( \+\)\(\"content-disposition\": \"^0.5.3\",\)#\1\2\n\1\"crypto-js\": \"^4.1.1\",#g" server/Common/package.jsonsed "s#\(function\* addTask(data, priority, opt_queue, opt_expiration) {\)#\1\n data.sessionData = utils.SessionData.get(1);#g" server/DocService/sources/DocsCoServer.js
sed "s#\( \+\)\(logger.info('data.type = ' + data.type + ' id = ' + docId);\)#\n\1if (typeof data.user !== 'undefined') {\n\1 utils.SessionData.set(data.user.sessionData);\n\1}\n\n\1\2#g" server/DocService/sources/DocsCoServer.js
sed "s#\( \+\)\(task = new commonDefines.TaskQueueData(JSON.parse(data));\)#\1var payload = JSON.parse(data);\n\1if (payload.sessionData != null) {\n\1 utils.SessionData.set(payload.sessionData);\n\1}\n\n\1\2#g" server/FileConverter/sources/converter.js
sed "s#\(TaskQueueRabbitMQ.prototype.addTask = function (task, priority, opt_expiration, opt_headers) {\)#\1\n task.sessionData = utils.SessionData.get(1);#g" server/Common/sources/taskqueueRabbitMQ.js
echo "exports.SessionData = SessionData;" >> server/Common/source/utils.js
read -d '' block << EOF
if (! opt_headers) {
var opt_headers = {};
}
opt_headers['Cookie'] = this.SessionData.get();
if (opt_Authorization) {
opt_headers[cfgTokenOutboxHeader] = cfgTokenOutboxPrefix + opt_Authorization;
}
EOF
block=$(echo "$block" | sed ':a $!{N; ba}; s/\n/\\n/g')
sed "s#\(function downloadUrlPromise(uri, optTimeout, optLimit, opt_Authorization, opt_filterPrivate, opt_headers) {\)#\1\n $block\n#g" server/Common/source/utils.js
read -d '' block << EOF
var cookies = "";
if (this.SessionData.get() != null) {
cookies = this.SessionData.get();
}
EOF
block=$(echo "$block" | sed ':a $!{N; ba}; s/\n/\\n/g')
sed "s#\(function postRequestPromise(uri, postData, postDataStream, optTimeout, opt_Authorization, opt_header) {\)#\1\n $block\n#g" server/Common/source/utils.js
read -d '' block << EOF
var CryptoJS = require('crypto-js');
/**
* Adapted from:
* AES JSON formatter for CryptoJS
* @link https://github.com/brainfoolong/cryptojs-aes-php
* @version 2.1.1
*/
var SessionData = {
sessionData: null,
secret: null,
cookieName: null,
/**
* Encrypt any value
* @param {*} value
* @param {string} password
* @return {string}
*/
'encrypt': function (value, password = this.secret) {
const ciphertext = CryptoJS.AES.encrypt(JSON.stringify(value), password, { format: SessionData }).toString();
const buff = Buffer.from(ciphertext, 'utf-8');
const base64 = buff.toString('base64');
return base64;
},
/**
* Decrypt a previously encrypted value
* @param {string} jsonStr
* @param {string} password
* @return {*}
*/
'decrypt': function (data, password = this.secret) {
this.cookieName = data.split('=')[0];
const buff = Buffer.from(data.split(';')[0].split('=')[1], 'base64');
var jsonStr = buff.toString('utf-8');
try {
return JSON.parse(CryptoJS.AES.decrypt(jsonStr, password, { format: SessionData }).toString(CryptoJS.enc.Utf8))
} catch (err) {
jsonStr = jsonStr.substr(0, jsonStr.lastIndexOf("\}") + 1);
return JSON.parse(CryptoJS.AES.decrypt(jsonStr, password, { format: SessionData }).toString(CryptoJS.enc.Utf8))
}
},
/**
* Stringify cryptojs data
* @param {Object} cipherParams
* @return {string}
*/
'stringify': function (cipherParams) {
var j = { ct: cipherParams.ciphertext.toString(CryptoJS.enc.Base64) }
if (cipherParams.iv) j.iv = cipherParams.iv.toString()
if (cipherParams.salt) j.s = cipherParams.salt.toString()
return JSON.stringify(j).replace(/\s/g, '')
},
/**
* Parse cryptojs data
* @param {string} jsonStr
* @return {*}
*/
'parse': function (jsonStr) {
var j = JSON.parse(jsonStr)
var cipherParams = CryptoJS.lib.CipherParams.create({ ciphertext: CryptoJS.enc.Base64.parse(j.ct) })
if (j.iv) cipherParams.iv = CryptoJS.enc.Hex.parse(j.iv)
if (j.s) cipherParams.salt = CryptoJS.enc.Hex.parse(j.s)
return cipherParams
},
'get': function(encrypted = 0) {
if (encrypted) {
return this.cookieName + '=' + this.encrypt(this.sessionData);
//return this.encrypt(this.sessionData);
}
return this.sessionData;
},
'set': function(str) {
this.sessionData = decodeURIComponent(this.decrypt(str));
}
}
SessionData.secret = config.get('services.CoAuthoring.secret.session.string');
EOF
block=$(echo "$block" | sed ':a $!{N; ba}; s/\n/\\n/g')
sed "s#\(var g_oIpFilterRules = function() {\)#$block\n\n\1#" server/Common/source/utils.js