/home/ivoiecob/email.hirewise-va.com/modules/CoreParanoidEncryptionWebclientPlugin/js/CCrypto.js
'use strict';
var
$ = require('jquery'),
_ = require('underscore'),
ModulesManager = require('%PathToCoreWebclientModule%/js/ModulesManager.js'),
Screens = require('%PathToCoreWebclientModule%/js/Screens.js'),
TextUtils = require('%PathToCoreWebclientModule%/js/utils/Text.js'),
FileSaver = require('%PathToCoreWebclientModule%/js/vendors/FileSaver.js'),
JscryptoKey = require('modules/%ModuleName%/js/JscryptoKey.js'),
Utils = require('%PathToCoreWebclientModule%/js/utils/Common.js'),
HexUtils = require('modules/%ModuleName%/js/utils/Hex.js'),
Popups = require('%PathToCoreWebclientModule%/js/Popups.js'),
OutdatedEncryptionMethodPopup = require('modules/%ModuleName%/js/popups/OutdatedEncryptionMethodPopup.js'),
GetTemporaryKeyPopup = require('modules/%ModuleName%/js/popups/GetTemporaryKeyPopup.js'),
Settings = require('modules/%ModuleName%/js/Settings.js'),
OpenPgpEncryptor = ModulesManager.run('OpenPgpWebclient', 'getOpenPgpEncryptor')
;
/**
* @constructor
*/
function CCrypto()
{
this.iChunkNumber = 0;
this.iChunkSize = Settings.ChunkSizeMb * 1024 * 1024;
this.iCurrChunk = 0;
this.oChunk = null;
this.iv = null;
// Queue of files awaiting upload
this.oChunkQueue = {
isProcessed: false,
privateKeyPassword: '',
aFiles: []
};
this.aStopList = [];
this.fOnUploadCancelCallback = null;
this.oKey = null;
}
CCrypto.prototype.start = async function (oFileInfo, ParanoidKey = '')
{
this.oFileInfo = oFileInfo;
this.oFile = oFileInfo.File;
this.iChunkNumber = Math.ceil(oFileInfo.File.size/this.iChunkSize);
this.iCurrChunk = 0;
this.oChunk = null;
this.iv = window.crypto.getRandomValues(new Uint8Array(16));
this.oFileInfo.Hidden = { 'RangeType': 1, 'Overwrite': true };
this.oFileInfo.Hidden.ExtendedProps = {
'InitializationVector': HexUtils.Array2HexString(new Uint8Array(this.iv))
};
if (ParanoidKey)
{
this.oFileInfo.Hidden.ExtendedProps.ParanoidKey = ParanoidKey;
}
};
CCrypto.prototype.startUpload = async function (oFileInfo, sUid, fOnChunkEncryptCallback, fCancelCallback)
{
this.oChunkQueue.isProcessed = true;
this.oKey = await JscryptoKey.generateKey();
const sKeyData = await JscryptoKey.convertKeyToString(this.oKey);
const oCurrentUserPrivateKey = await OpenPgpEncryptor.getCurrentUserPrivateKey();
if (oCurrentUserPrivateKey && sKeyData)
{
const CurrentUserPublicKey = await OpenPgpEncryptor.getCurrentUserPublicKey();
if (CurrentUserPublicKey)
{
const sEncryptedKey = await this.encryptParanoidKey(sKeyData, [CurrentUserPublicKey]);
if (sEncryptedKey)
{
await this.start(oFileInfo, sEncryptedKey);
this.readChunk(sUid, fOnChunkEncryptCallback);
}
else if (_.isFunction(fCancelCallback))
{
fCancelCallback();
}
}
else if (_.isFunction(fCancelCallback))
{
fCancelCallback();
}
}
else if (_.isFunction(fCancelCallback))
{
fCancelCallback();
}
};
CCrypto.prototype.readChunk = function (sUid, fOnChunkEncryptCallback)
{
var
iStart = this.iChunkSize * this.iCurrChunk,
iEnd = (this.iCurrChunk < (this.iChunkNumber - 1)) ? this.iChunkSize * (this.iCurrChunk + 1) : this.oFile.size,
oReader = new FileReader(),
oBlob = null
;
if (this.aStopList.indexOf(sUid) !== -1)
{ // if user canceled uploading file with uid = sUid
this.aStopList.splice(this.aStopList.indexOf(sUid), 1);
this.checkQueue();
return;
}
else
{
// Get file chunk
if (this.oFile.slice)
{
oBlob = this.oFile.slice(iStart, iEnd);
}
else if (this.oFile.webkitSlice)
{
oBlob = this.oFile.webkitSlice(iStart, iEnd);
}
else if (this.oFile.mozSlice)
{
oBlob = this.oFile.mozSlice(iStart, iEnd);
}
if (oBlob)
{
try
{ //Encrypt file chunk
oReader.onloadend = _.bind(function(evt) {
if (evt.target.readyState === FileReader.DONE)
{
this.oChunk = evt.target.result;
this.iCurrChunk++;
this.encryptChunk(sUid, fOnChunkEncryptCallback);
}
}, this);
oReader.readAsArrayBuffer(oBlob);
}
catch(err)
{
Screens.showError(TextUtils.i18n('%MODULENAME%/ERROR_ENCRYPTION'));
}
}
}
};
CCrypto.prototype.encryptChunk = function (sUid, fOnChunkEncryptCallback)
{
crypto.subtle.encrypt({ name: 'AES-CBC', iv: this.iv }, this.oKey, this.oChunk)
.then(_.bind(function (oEncryptedContent) {
//delete padding for all chunks except last one
oEncryptedContent = (this.iChunkNumber > 1 && this.iCurrChunk !== this.iChunkNumber) ? oEncryptedContent.slice(0, oEncryptedContent.byteLength - 16) : oEncryptedContent;
var
oEncryptedFile = new Blob([oEncryptedContent], {type: "text/plain", lastModified: new Date()}),
//fProcessNextChunkCallback runs after previous chunk uploading
fProcessNextChunkCallback = _.bind(function (sUid, fOnChunkEncryptCallback) {
if (this.iCurrChunk < this.iChunkNumber)
{// if it was not last chunk - read another chunk
this.readChunk(sUid, fOnChunkEncryptCallback);
}
else
{// if it was last chunk - check Queue for files awaiting upload
this.oChunkQueue.isProcessed = false;
this.checkQueue();
}
}, this)
;
this.oFileInfo.File = oEncryptedFile;
//use last 16 byte of current chunk as initial vector for next chunk
this.iv = new Uint8Array(oEncryptedContent.slice(oEncryptedContent.byteLength - 16));
if (this.iCurrChunk === 1)
{ // for first chunk enable 'FirstChunk' attribute. This is necessary to solve the problem of simultaneous loading of files with the same name
this.oFileInfo.Hidden.ExtendedProps.FirstChunk = true;
}
else
{
this.oFileInfo.Hidden.ExtendedProps.FirstChunk = null;
}
if (this.iCurrChunk == this.iChunkNumber)
{ // unmark file as loading
this.oFileInfo.Hidden.ExtendedProps.Loading = null;
}
else
{ // mark file as loading until upload doesn't finish
this.oFileInfo.Hidden.ExtendedProps.Loading = true;
}
// call upload of encrypted chunk
fOnChunkEncryptCallback(sUid, this.oFileInfo, fProcessNextChunkCallback, this.iCurrChunk, this.iChunkNumber, (this.iCurrChunk - 1) * this.iChunkSize);
}, this))
.catch(function(err)
{
Screens.showError(TextUtils.i18n('%MODULENAME%/ERROR_ENCRYPTION'));
})
;
};
CCrypto.prototype.downloadDividedFile = async function (oFile, iv, fProcessBlobCallback, fProcessBlobErrorCallback, sParanoidEncryptedKey = '')
{
oFile.startDownloading();
const sKey = await this.prepareKey(
oFile,
sParanoidEncryptedKey
);
if (sKey !== false)
{
new CDownloadFile(oFile, iv, this.iChunkSize, fProcessBlobCallback, fProcessBlobErrorCallback, sKey);
}
else
{
oFile.stopDownloading()
}
};
CCrypto.prototype.prepareKey = async function (oFile, sParanoidEncryptedKey = '')
{
let sKey = '';
if (sParanoidEncryptedKey)
{
sKey = await this.decryptParanoidKey(sParanoidEncryptedKey);
if (!sKey)
{
return false;
}
}
else
{
if (!Settings.DontRemindMe())
{
//showing popup
const bContinue = await this.showOutdatedEncryptionMethodPopup(oFile.fileName());
if (!bContinue)
{
return false;
}
}
if (!this.isKeyInStorage())
{//ask user to enter key
sKey = await this.getTemporaryKeyAsString();
if (!sKey)
{
return false;
}
}
}
return sKey;
};
CCrypto.prototype.getTemporaryKeyAsString = async function ()
{
const oPromiseGetKey = new Promise( (resolve, reject) => {
const fOnKeyEnterCallback = sKey => {
resolve(sKey);
};
const fOnCancellCallback = () => {
resolve(false);
};
//showing popup
Popups.showPopup(GetTemporaryKeyPopup, [
fOnKeyEnterCallback,
fOnCancellCallback
]);
});
const sKey = await oPromiseGetKey;
return sKey;
};
CCrypto.prototype.showOutdatedEncryptionMethodPopup = async function (sFileName)
{
const oPromiseShowPopup = new Promise( (resolve, reject) => {
const fContinueCallback = () => {
resolve(true);
};
const fCancellCallback = () => {
resolve(false);
};
//showing popup
Popups.showPopup(OutdatedEncryptionMethodPopup, [
sFileName,
fContinueCallback,
fCancellCallback
]);
});
const bResult = await oPromiseShowPopup;
return bResult;
};
CCrypto.prototype.encryptParanoidKey = async function (sParanoidKey, aPublicKeys, sPassword = '')
{
if (sPassword === '') {
sPassword = this.oChunkQueue.privateKeyPassword
}
let sEncryptedKey = '';
const oPrivateKey = await OpenPgpEncryptor.getCurrentUserPrivateKey();
if (oPrivateKey)
{
const oPGPEncryptionResult = await OpenPgpEncryptor.encryptData(
sParanoidKey,
aPublicKeys,
[oPrivateKey],
false, /*bPasswordBasedEncryption*/
true, /*bSign*/
sPassword
);
if (oPGPEncryptionResult.result)
{
let { data, passphrase } = oPGPEncryptionResult.result;
this.oChunkQueue.privateKeyPassword = passphrase;
sEncryptedKey = data;
}
else if (oPGPEncryptionResult.hasErrors() || oPGPEncryptionResult.hasNotices())
{
OpenPgpEncryptor.showPgpErrorByCode(
oPGPEncryptionResult,
'',
TextUtils.i18n('%MODULENAME%/ERROR_LOAD_KEY')
);
}
}
return sEncryptedKey;
};
CCrypto.prototype.decryptParanoidKey = async function (sParanoidEncryptedKey, sPassword = '')
{
let sKey = '';
let oPGPDecryptionResult = await OpenPgpEncryptor.decryptData(
sParanoidEncryptedKey,
sPassword
);
if (oPGPDecryptionResult.result)
{
sKey = oPGPDecryptionResult.result;
if (oPGPDecryptionResult.validKeyNames
&& oPGPDecryptionResult.validKeyNames.length
)
{
const oCurrentUserPrivateKey = await OpenPgpEncryptor.getCurrentUserPrivateKey();
if (!oCurrentUserPrivateKey
|| !oPGPDecryptionResult.validKeyNames.includes(oCurrentUserPrivateKey.getUser())
)
{//Paranoid-key was signed with a foreign key
const sReport = TextUtils.i18n('%MODULENAME%/REPORT_SUCCESSFULL_SIGNATURE_VERIFICATION')
+ oPGPDecryptionResult.validKeyNames.join(', ').replace(/</g, "<").replace(/>/g, ">");
Screens.showReport(sReport)
}
}
else if (oPGPDecryptionResult.notices && _.indexOf(oPGPDecryptionResult.notices, Enums.OpenPgpErrors.VerifyErrorNotice) !== -1)
{
Screens.showError(TextUtils.i18n('%MODULENAME%/ERROR_SIGNATURE_NOT_VERIFIED'));
}
}
else if (oPGPDecryptionResult.hasErrors() || oPGPDecryptionResult.hasNotices())
{
//if errors or notices contains PrivateKeyNotFoundError
let aErrors = oPGPDecryptionResult.errors ? oPGPDecryptionResult.errors : [];
let aNotices = oPGPDecryptionResult.notices ? oPGPDecryptionResult.notices : []
if ([...aErrors, ...aNotices].some(
error => error.length && error[0] === Enums.OpenPgpErrors.PrivateKeyNotFoundError
))
{
//show error message customised for files
Screens.showError(TextUtils.i18n('%MODULENAME%/ERROR_NO_PRIVATE_KEY_FOUND_FOR_DECRYPT'));
}
else
{
OpenPgpEncryptor.showPgpErrorByCode(
oPGPDecryptionResult,
'',
TextUtils.i18n('%MODULENAME%/ERROR_LOAD_KEY')
);
}
}
return sKey;
};
/**
* Checking Queue for files awaiting upload
*/
CCrypto.prototype.checkQueue = function ()
{
var aNode = null;
if (this.oChunkQueue.aFiles.length > 0)
{
aNode = this.oChunkQueue.aFiles.shift();
aNode.fStartUploadCallback.apply(aNode.fStartUploadCallback, [aNode.oFileInfo, aNode.sUid, aNode.fOnChunkEncryptCallback]);
} else {
this.oChunkQueue.privateKeyPassword = '';
}
};
/**
* Stop file uploading
*
* @param {String} sUid
* @param {Function} fOnUploadCancelCallback
*/
CCrypto.prototype.stopUploading = function (sUid, fOnUploadCancelCallback, sFileName)
{
var bFileInQueue = false;
// If file await to be uploaded - delete it from queue
this.oChunkQueue.aFiles.forEach(function (oData, index, array) {
if (oData.sUid === sUid)
{
fOnUploadCancelCallback(sUid, oData.oFileInfo.FileName);
array.splice(index, 1);
bFileInQueue = true;
}
});
if (!bFileInQueue)
{
this.aStopList.push(sUid);
this.oChunkQueue.isProcessed = false;
fOnUploadCancelCallback(sUid, sFileName);
this.oKey = null;
// this.checkQueue();
}
};
CCrypto.prototype.viewEncryptedImage = async function (oFile, iv, sParanoidEncryptedKey = '')
{
oFile.startDownloading();
const sKey = await this.prepareKey(
oFile,
sParanoidEncryptedKey
);
if (sKey !== false)
{
new CViewImage(oFile, iv, this.iChunkSize, sKey);
}
else
{
oFile.stopDownloading()
}
};
CCrypto.prototype.isKeyInStorage = function ()
{
return !!JscryptoKey.loadKeyFromStorage();
};
function CDownloadFile(oFile, iv, iChunkSize, fProcessBlobCallback, fProcessBlobErrorCallback, sKey = '')
{
this.oWriter = new CWriter(oFile.fileName(), fProcessBlobCallback);
this.init(oFile, iv, iChunkSize, fProcessBlobErrorCallback, sKey);
}
CDownloadFile.prototype.init = async function (oFile, iv, iChunkSize, fProcessBlobErrorCallback, sKey)
{
this.sHash = Utils.getRandomHash();
this.oFile = oFile;
this.sFileName = oFile.fileName();
this.iFileSize = oFile.size();
this.sDownloadLink = oFile.getActionUrl('download');
this.iCurrChunk = 0;
this.iv = new Uint8Array(HexUtils.HexString2Array(iv));
this.key = null;
this.iChunkNumber = Math.ceil(this.iFileSize/iChunkSize);
this.iChunkSize = iChunkSize;
this.fProcessBlobErrorCallback = fProcessBlobErrorCallback;
//clear parameters after & if DownloadLink contains any
if (this.sDownloadLink.indexOf('&') > 0)
{
this.sDownloadLink = this.sDownloadLink.substring(0, this.sDownloadLink.indexOf('&'));
}
const fCancelCallback = () => {
if (_.isFunction(this.fProcessBlobErrorCallback))
{
this.fProcessBlobErrorCallback();
}
this.stopDownloading();
};
if (sKey)
{//the key was transferred from outside
let oKey = await JscryptoKey.getKeyFromString(sKey);
if (oKey)
{
this.key = oKey;
this.decryptChunk();
}
else
{
fCancelCallback();
}
}
else
{//read the key from local storage
JscryptoKey.getKey(
oKey => {
this.key = oKey;
this.decryptChunk();
},
fCancelCallback
);
}
};
CDownloadFile.prototype.writeChunk = function (oDecryptedUint8Array)
{
if (this.oFile.downloading() !== true)
{ // if download was canceled
return;
}
else
{
this.oWriter.write(oDecryptedUint8Array); //write decrypted chunk
if (this.iCurrChunk < this.iChunkNumber)
{ //if it was not last chunk - decrypting another chunk
this.decryptChunk();
}
else
{
this.stopDownloading();
this.oWriter.close();
}
}
};
CDownloadFile.prototype.decryptChunk = function ()
{
var oReq = new XMLHttpRequest();
oReq.open("GET", this.getChunkLink(), true);
oReq.responseType = 'arraybuffer';
oReq.onprogress = _.bind(function(oEvent) {
if (this.isDownloading())
{
this.oFile.onDownloadProgress(oEvent.loaded + (this.iCurrChunk-1) * this.iChunkSize, this.iFileSize);
}
else
{
oReq.abort();
}
}, this);
oReq.onload =_.bind(function (oEvent)
{
var
oArrayBuffer = oReq.response,
oDataWithPadding = {}
;
if (oReq.status === 200 && oArrayBuffer)
{
oDataWithPadding = new Uint8Array(oArrayBuffer.byteLength + 16);
oDataWithPadding.set( new Uint8Array(oArrayBuffer), 0);
if (this.iCurrChunk !== this.iChunkNumber)
{// for all chunk except last - add padding
crypto.subtle.encrypt(
{
name: 'AES-CBC',
iv: new Uint8Array(oArrayBuffer.slice(oArrayBuffer.byteLength - 16))
},
this.key,
(new Uint8Array([16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16])).buffer // generate padding for chunk
).then(_.bind(function(oEncryptedContent) {
// add generated padding to data
// oEncryptedContent.slice(0, 16) - use only first 16 bytes of generated padding, other data is padding for our padding
oDataWithPadding.set(new Uint8Array(new Uint8Array(oEncryptedContent.slice(0, 16))), oArrayBuffer.byteLength);
// decrypt data
crypto.subtle.decrypt({ name: 'AES-CBC', iv: this.iv }, this.key, oDataWithPadding.buffer)
.then(_.bind(function (oDecryptedArrayBuffer) {
var oDecryptedUint8Array = new Uint8Array(oDecryptedArrayBuffer);
// use last 16 byte of current chunk as initial vector for next chunk
this.iv = new Uint8Array(oArrayBuffer.slice(oArrayBuffer.byteLength - 16));
this.writeChunk(oDecryptedUint8Array);
}, this))
.catch(_.bind(function(err) {
this.stopDownloading();
if (_.isFunction(this.fProcessBlobErrorCallback))
{
this.fProcessBlobErrorCallback();
}
Screens.showError(TextUtils.i18n('%MODULENAME%/ERROR_DECRYPTION'));
}, this));
}, this)
);
}
else
{ //for last chunk just decrypt data
crypto.subtle.decrypt({ name: 'AES-CBC', iv: this.iv }, this.key, oArrayBuffer)
.then(_.bind(function (oDecryptedArrayBuffer) {
var oDecryptedUint8Array = new Uint8Array(oDecryptedArrayBuffer);
// use last 16 byte of current chunk as initial vector for next chunk
this.iv = new Uint8Array(oArrayBuffer.slice(oArrayBuffer.byteLength - 16));
this.writeChunk(oDecryptedUint8Array);
}, this))
.catch(_.bind(function(err) {
this.stopDownloading();
if (_.isFunction(this.fProcessBlobErrorCallback))
{
this.fProcessBlobErrorCallback();
}
Screens.showError(TextUtils.i18n('%MODULENAME%/ERROR_DECRYPTION'));
}, this))
;
}
}
}, this);
oReq.send(null);
};
CDownloadFile.prototype.stopDownloading = function ()
{
this.oFile.stopDownloading();
};
/**
* Generate link for downloading current chunk
*/
CDownloadFile.prototype.getChunkLink = function ()
{
return this.sDownloadLink + '/download/' + this.iCurrChunk++ + '/' + this.iChunkSize + '&' + this.sHash;
};
CDownloadFile.prototype.isDownloading = function ()
{
return this.oFile.downloading();
};
function CViewImage(oFile, iv, iChunkSize, sParanoidEncryptedKey = '')
{
this.oWriter = null;
this.init(oFile, iv, iChunkSize, /*fProcessBlobErrorCallback*/null, sParanoidEncryptedKey);
}
CViewImage.prototype = Object.create(CDownloadFile.prototype);
CViewImage.prototype.constructor = CViewImage;
CViewImage.prototype.writeChunk = function (oDecryptedUint8Array)
{
this.oWriter = this.oWriter === null ? new CBlobViewer(this.sFileName) : this.oWriter;
this.oWriter.write(oDecryptedUint8Array); //write decrypted chunk
if (this.iCurrChunk < this.iChunkNumber)
{ //if it was not last chunk - decrypting another chunk
this.decryptChunk();
}
else
{
this.stopDownloading();
this.oWriter.close();
}
};
/**
* Writing chunks in file
*
* @constructor
* @param {String} sFileName
* @param {Function} fProcessBlobCallback
*/
function CWriter(sFileName, fProcessBlobCallback)
{
this.sName = sFileName;
this.aBuffer = [];
if (_.isFunction(fProcessBlobCallback))
{
this.fProcessBlobCallback = fProcessBlobCallback;
}
}
CWriter.prototype.write = function (oDecryptedUint8Array)
{
this.aBuffer.push(oDecryptedUint8Array);
};
CWriter.prototype.close = function ()
{
let file = new Blob(this.aBuffer);
if (typeof this.fProcessBlobCallback !== 'undefined')
{
this.fProcessBlobCallback(file);
}
else
{
FileSaver.saveAs(file, this.sName);
}
file = null;
};
/**
* Writing chunks in blob for viewing
*
* @constructor
* @param {String} sFileName
*/
function CBlobViewer(sFileName) {
this.sName = sFileName;
this.aBuffer = [];
this.imgWindow = window.open("", "_blank", "height=auto, width=auto,toolbar=no,scrollbars=no,resizable=yes");
}
CBlobViewer.prototype = Object.create(CWriter.prototype);
CBlobViewer.prototype.constructor = CBlobViewer;
CBlobViewer.prototype.close = function ()
{
try
{
var
file = new Blob(this.aBuffer),
link = window.URL.createObjectURL(file),
img = null
;
this.imgWindow.document.write("<head><title>" + this.sName + '</title></head><body><img src="' + link + '" /></body>');
img = $(this.imgWindow.document.body).find('img');
img.on('load', function () {
//remove blob after showing image
window.URL.revokeObjectURL(link);
});
}
catch (err)
{
Screens.showError(TextUtils.i18n('%MODULENAME%/ERROR_POPUP_WINDOWS'));
}
};
module.exports = new CCrypto();