diff --git a/README.md b/README.md index 6a86d46..664ca46 100644 --- a/README.md +++ b/README.md @@ -21,11 +21,6 @@ Requirements - CryptoJS (https://cryptojs.gitbook.io/docs/) - core.js - - cipher-core.js - - aes.js - - hmac.js - - sha256.js - - pad-nopadding.js - enc-utf16.js - enc-uint8array.js (Custom Uint8Array encoding) @@ -39,25 +34,22 @@ const aes = AesCrypt(); let fileSecret = document.getElementById("fileSecret").files[0]; -var password = "foopassword" +let password = "foopassword" // encryption/decryption -// **IMPORTANT** Only for workers! -let progress_callback = (current) => {let percent = current * 100;}; - // encrypt typed array (Uint8Array) -aes.encrypt(fileSecret, password, progress_callback).then((encrypted) => { +aes.encrypt(fileSecret, password).then((encrypted) => { console.log(encrypted); }); let fileEncrypted = document.getElementById("fileEncrypted").files[0]; // decrypt typed array (Uint8Array) -aes.decrypt(fileEncrypted, password, progress_callback).then((decrypted) => { +aes.decrypt(fileEncrypted, password).then((decrypted) => { // transform Uint8Array to Latin1 string - let secret = aes.utils.bytes_to_latin1(decrypted); + let secret = aes.utils.bytes2str(decrypted); console.log(secret); }); diff --git a/demo/index.html b/demo/index.html index 81c82b9..4a92e60 100644 --- a/demo/index.html +++ b/demo/index.html @@ -2,11 +2,6 @@ - - - - - diff --git a/demo/libs/aes_crypt.min.js b/demo/libs/aes_crypt.min.js index fe679db..d0096e8 100644 --- a/demo/libs/aes_crypt.min.js +++ b/demo/libs/aes_crypt.min.js @@ -1 +1 @@ -AesCrypt=function(){let e={version:"0.13a",bufferSize:32768,fileFormatVersion:2,maxPassLen:1024,AESBlockSize:16};var t=function(e){let t=0,r=e.size,o=new FileReader,i=e;async function a(e){return await function(e){return new Promise((n,r)=>{let a=i.slice(t,t+=e);o.onload=(()=>{n(new Uint8Array(o.result))}),o.onerror=r,o.readAsArrayBuffer(a)})}(e)}return{readByte:async function(){return(await a(1))[0]},readBytes:a,readBytesAsInt:async function(e){let t=await a(e);return n.arrToInt(t)},readBytesAsString:async function(e){let t=await a(e);return n.bytes_to_latin1(t)},getCurrentPosition:function(){return t},getLength:function(){return r}}},n={random_int:function(e,t){return Math.floor(Math.random()*(t-e)+e)},urandom:function(e){let t="";for(let n=0;n("00"+e.toString(16)).slice(-2)).join(""),16)},bytes_to_latin1:function(e){return CryptoJS.enc.Latin1.stringify(CryptoJS.enc.Uint8Arr.parse(e))},encode_to_words:function(e,t="Latin1"){return CryptoJS.enc[t].parse(e)}};let r=function(){let e=new Uint8Array([]);return{appendBytes:function(t){let n;if("number"==typeof t){let e=t.toString(16);n=new Uint8Array(e.match(/.{1,2}/g).map(e=>parseInt(e,16)))}else if("string"==typeof t){n=new Uint8Array(t.length);for(let e=0;eparseInt(e,16)))}else if("string"==typeof e){t=new Uint8Array(e.length);for(let n=0;n{})){if(s.length>e.maxPassLen)return console.warn("Password is too long."),!1;const p=n.urandom(e.AESBlockSize),c=a(s,p),d=n.urandom(e.AESBlockSize),y=n.urandom(32),u=o(y,d),S=CryptoJS.algo.HMAC.create(CryptoJS.algo.SHA256,n.encode_to_words(y)),f=o(c,p),g=f.process(n.encode_to_words(d+y)).toString(CryptoJS.enc.Latin1)+f.finalize().toString(CryptoJS.enc.Latin1),w=CryptoJS.algo.HMAC.create(CryptoJS.algo.SHA256,n.encode_to_words(c));return w.update(n.encode_to_words(g)),await async function(o,i,a,s,l,p,c){let d=r();d.appendBytes("AES"),d.appendBytes(e.fileFormatVersion),d.appendBytes(0);const y="jsAesCrypt "+e.version;d.appendBytes(0),d.appendBytes(1+("CREATED_BY"+y).length),d.appendBytes("CREATED_BY"),d.appendBytes(0),d.appendBytes(y),d.appendBytes([0,128]),d.appendBytes(n.fillArray(0,128)),d.appendBytes([0,0]),d.appendBytes(i),d.appendBytes(a),d.appendBytes(l.finalize().toString(CryptoJS.enc.Latin1));let u=String.fromCharCode(0),S=new t(o);Math.ceil(S.getLength()/e.bufferSize);let f=Math.ceil((S.getLength()-S.getCurrentPosition())/e.bufferSize),g=0;for(;S.getCurrentPosition(){})){if(s.length>e.maxPassLen)return console.warn("Password is too long."),!1;let p=t(o);if("AES"!==await p.readBytesAsString(3)||p.getLength()<136)return console.warn("File is corrupted or not an AES Crypt \n(or jsAesCrypt) file."),!1;if(await p.readByte()!==e.fileFormatVersion)return console.warn("jsAesCrypt is only compatible with version \n2 of the AES Crypt file format."),!1;for(await p.readByte();;){let e=await p.readBytes(2);if(e.length<2)return console.warn("File is corrupted."),!1;if(0==(e=+n.arrToInt(e)))break;await p.readBytes(e)}let c=await p.readBytesAsString(16);if(16!==c.length)return console.warn("File is corrupted."),!1;let d=a(s,c),y=await p.readBytesAsString(48);if(48!==y.length)return console.warn("File is corrupted."),!1;let u=await p.readBytesAsString(32);if(32!==u.length)return console.warn("File is corrupted."),!1;let S=CryptoJS.algo.HMAC.create(CryptoJS.algo.SHA256,n.encode_to_words(d));if(S.update(n.encode_to_words(y)),u!==CryptoJS.enc.Latin1.stringify(S.finalize()))return console.warn("Wrong password (or file is corrupted)."),!1;let f=i(d,c),g=f.process(n.encode_to_words(y)).toString(CryptoJS.enc.Latin1)+f.finalize().toString(CryptoJS.enc.Latin1),w=g.substr(0,e.AESBlockSize),C=g.substr(e.AESBlockSize,32),A=i(C,w),B=CryptoJS.algo.HMAC.create(CryptoJS.algo.SHA256,n.encode_to_words(C)),h=r(),_=Math.ceil((p.getLength()-p.getCurrentPosition()-32-1-e.AESBlockSize)/e.bufferSize)+1,J=0;for(;p.getCurrentPosition()("00"+e.toString(16)).slice(-2)).join(""),16)},bytes2str:function(e){return CryptoJS.enc.Latin1.stringify(CryptoJS.enc.Uint8Arr.parse(e))},str2bytes:function(e,t="Latin1"){return CryptoJS.enc.Uint8Arr.decode(CryptoJS.enc[t].parse(e))}},n={_createKey:async function(e,t,n){return await crypto.subtle.importKey("raw",e.buffer,t,!1,n)},_webHashHMAC:async function(e,t){let n=await this._createKey(t,{name:"HMAC",hash:{name:"SHA-256"}},["sign","verify"]);return new Uint8Array(await crypto.subtle.sign("HMAC",n,e))},_webEncryptAes:async function(e,t,n,r=!0){let a=await this._createKey(t,"AES-CBC",["encrypt","decrypt"]),i=new Uint8Array(await crypto.subtle.encrypt({name:"AES-CBC",iv:n},a,e));return e.length%this.info.AESBlockSize==0&&1==r&&(i=i.slice(0,i.length-this.info.AESBlockSize)),i},_webHashSHA256:async function(e){return new Uint8Array(await crypto.subtle.digest("SHA-256",e.buffer))},_webDecryptAes:async function(e,t,n,r=0){let i=await this._createKey(t,"AES-CBC",["encrypt","decrypt"]),s=a(e);if(0==r){let r=new Uint8Array(this.info.AESBlockSize);for(let t=0;t{let s=a.slice(t,t+=e);r.onload=(()=>{n(new Uint8Array(r.result))}),r.onerror=i,r.readAsArrayBuffer(s)})}(e)}return{readByte:async function(){return(await i(1))[0]},readBytes:i,getCurrentPosition:function(){return t},getLength:function(){return n}}},a=function(e=[]){let t=new Uint8Array(e);return{appendBytes:function(e){let n;if("number"==typeof e){let t=e.toString(16);n=new Uint8Array(t.match(/.{1,2}/g).map(e=>parseInt(e,16)))}else if("string"==typeof e){n=new Uint8Array(e.length);for(let t=0;te.maxPassLen)return console.warn("Password is too long."),!1;const c=t.urandom(e.AESBlockSize),l=await i(o,c),y=t.urandom(e.AESBlockSize),p=t.urandom(32);let u=a();u.appendBytes(y),u.appendBytes(p);const f=await n._webEncryptAes(u.finalize(),l,c),w=await n._webHashHMAC(f,l);return await async function(t,i,s,o,c,l){let y=a();y.appendBytes("AES"),y.appendBytes(e.fileFormatVersion),y.appendBytes(0);const p="jsAesCrypt "+e.version;y.appendBytes(0),y.appendBytes(1+("CREATED_BY"+p).length),y.appendBytes("CREATED_BY"),y.appendBytes(0),y.appendBytes(p),y.appendBytes([0,128]),y.appendBytes("\0".repeat(128)),y.appendBytes([0,0]),y.appendBytes(i),y.appendBytes(s),y.appendBytes(c);let u=new r(t),f=u.getLength(),w=a(await u.readBytes(u.getLength())),d=String.fromCharCode(f%e.AESBlockSize);return cText=await n._webEncryptAes(w.finalize(),o,l),hmac0=await n._webHashHMAC(cText,o),y.appendBytes(cText),y.appendBytes(d),y.appendBytes(hmac0),await y.finalize()}(s,c,f,p,w,y)},decrypt:async function(s,o){if(o.length>e.maxPassLen)return console.warn("Password is too long."),!1;let c=r(s);if("AES"!==t.bytes2str(await c.readBytes(3))||c.getLength()<136)return console.warn("File is corrupted or not an AES Crypt \n(or jsAesCrypt) file."),!1;if(await c.readByte()!==e.fileFormatVersion)return console.warn("jsAesCrypt is only compatible with version \n2 of the AES Crypt file format."),!1;for(await c.readByte();;){let e=await c.readBytes(2);if(e.length<2)return console.warn("File is corrupted."),!1;if(0==(e=+t.arrToInt(e)))break;await c.readBytes(e)}let l=await c.readBytes(16);if(16!==l.length)return console.warn("File is corrupted."),!1;let y=await i(o,l),p=await c.readBytes(48);if(48!==p.length)return console.warn("File is corrupted."),!1;let u=t.bytes2str(await c.readBytes(32));if(32!==u.length)return console.warn("File is corrupted."),!1;let f=await n._webHashHMAC(p,y);if(u!==t.bytes2str(f))return console.warn("Wrong password (or file is corrupted)."),!1;let w,d=await n._webDecryptAes(p,y,l,0),A=d.slice(0,e.AESBlockSize),B=d.slice(e.AESBlockSize,e.AESBlockSize+32),h=a(),S=a(await c.readBytes(c.getLength()-c.getCurrentPosition()-32-1)),g=t.arrToInt(await c.readBytes(1));hmac0Act=await n._webHashHMAC(S.finalize(),B);try{w=await n._webDecryptAes(S.finalize(),B,A,g)}catch{w=await n._webDecryptAes(S.finalize(),B,A,0);let t=e.AESBlockSize-g;w=w.slice(0,w.length-t)}h.appendBytes(w);let b=t.bytes2str(await c.readBytes(32));return 32!==b.length?(console.warn("File is corrupted."),!1):b!==t.bytes2str(hmac0Act)?(console.warn("Bad HMAC (file is corrupted)."),!1):h.finalize()},utils:t,info:e}}; \ No newline at end of file diff --git a/demo/worker.js b/demo/worker.js index ffb7b66..7da0586 100644 --- a/demo/worker.js +++ b/demo/worker.js @@ -3,11 +3,6 @@ let base_libs = "libs/"; self.importScripts( base_cdn + "core.min.js", - base_cdn + "cipher-core.min.js", - base_cdn + "aes.min.js", - base_cdn + "hmac.min.js", - base_cdn + "sha256.min.js", - base_cdn + "pad-nopadding.min.js", base_cdn + "enc-utf16.min.js", base_libs + "enc-uint8array.min.js", base_libs + "aes_crypt.min.js", diff --git a/src/aes_crypt.js b/src/aes_crypt.js index 6eed124..b4d1f2d 100644 --- a/src/aes_crypt.js +++ b/src/aes_crypt.js @@ -1,7 +1,9 @@ +/** global: AesCrypt */ + AesCrypt = function () { let info = { // jsAesCrypt version - version: "0.13a", + version: "0.15", // encryption/decryption buffer size - 32K bufferSize: 32 * 1024, @@ -30,20 +32,19 @@ AesCrypt = function () { * * @param fileObj file element object * @param passw string password to decrypt - * @callback_progress callback function (current) => { 0 < current <= 1 } */ - async function decrypt(fileObj, passw, callback_progress = (c) => {}) { + async function decrypt(fileObj, passw) { if( passw.length > info.maxPassLen ) { console.warn("Password is too long."); return false; } // file bytes reader - let file = fileReader(fileObj); + let file = fileBytesReader(fileObj); // check if file is in AES Crypt format (also min length check) - if( await file.readBytesAsString(3) !== "AES" || file.getLength() < 136 ) { + if( utils.bytes2str( await file.readBytes(3) ) !== "AES" || file.getLength() < 136 ) { console.warn( "File is corrupted or not an AES Crypt \n" + "(or jsAesCrypt) file."); @@ -83,128 +84,79 @@ AesCrypt = function () { } // read external iv - let iv1 = await file.readBytesAsString(16); + let iv1 = await file.readBytes(16); if( iv1.length !== 16 ) { console.warn("File is corrupted."); return false; } // _stretch password and iv - let key = _stretch(passw, iv1); + let key = await _stretch(passw, iv1); // read encrypted main iv and key - let c_iv_key = await file.readBytesAsString(48); + let c_iv_key = await file.readBytes(48); if( c_iv_key.length !== 48 ) { console.warn("File is corrupted."); return false; } // read HMAC-SHA256 of the encrypted iv and key - let hmac1 = await file.readBytesAsString(32); + let hmac1 = utils.bytes2str( await file.readBytes(32) ); if( hmac1.length !== 32 ) { console.warn("File is corrupted."); return false; } - let hmac1Act = CryptoJS.algo.HMAC.create(CryptoJS.algo.SHA256, utils.encode_to_words(key)); - hmac1Act.update( - utils.encode_to_words(c_iv_key) - ); + let hmac1Act = await webCryptSubtle._webHashHMAC(c_iv_key, key); // HMAC check - if( hmac1 !== CryptoJS.enc.Latin1.stringify(hmac1Act.finalize()) ) { + if( hmac1 !== utils.bytes2str(hmac1Act) ) { console.warn("Wrong password (or file is corrupted)."); return false; } - // instantiate AES cipher - let decryptor1 = _createDecryptor(key, iv1); - - // decrypt main iv and key - let iv_key = decryptor1.process(utils.encode_to_words(c_iv_key)).toString(CryptoJS.enc.Latin1) + decryptor1.finalize().toString(CryptoJS.enc.Latin1); + let iv_key = await webCryptSubtle._webDecryptAes(c_iv_key, key, iv1, 0); // get internal iv and key - let iv0 = iv_key.substr(0, info.AESBlockSize); - let intKey = iv_key.substr(info.AESBlockSize, 32); - - // instantiate another AES cipher - let decryptor0 = _createDecryptor(intKey, iv0); - - // instantiate actual HMAC-SHA256 of the ciphertext - let hmac0Act = CryptoJS.algo.HMAC.create(CryptoJS.algo.SHA256, utils.encode_to_words(intKey)); - - let result = binaryArray(); - let total = Math.ceil( (file.getLength() - file.getCurrentPosition() - 32 - 1 - info.AESBlockSize) / info.bufferSize ) + 1; - let counter = 0; - - // decrypt blocks - while( file.getCurrentPosition() < file.getLength() - 32 - 1 - info.AESBlockSize ) { - // read data - let cText = utils.encode_to_words( - await file.readBytes( - Math.min( - info.bufferSize, - file.getLength() - file.getCurrentPosition() - 32 - 1 - info.AESBlockSize, - ) - ), - "Uint8Arr" - ) - - // update HMAC - hmac0Act.update(cText) - // decrypt data and write it to output file - result.appendBytes(decryptor0.process(cText).toString(CryptoJS.enc.Latin1)); - callback_progress(++counter / total); - } + let iv0 = iv_key.slice(0, info.AESBlockSize); + let intKey = iv_key.slice(info.AESBlockSize, info.AESBlockSize+32); - var cText; - - // last block reached, remove padding if needed - // read last block - if( file.getCurrentPosition() !== file.getLength() - 32 - 1 ) { - // read typed array - cText = await file.readBytes(info.AESBlockSize); - - if( cText.length < info.AESBlockSize ) { - console.warn("File is corrupted."); - return false; - } - } else { - cText = new Uint8Array([]); - } + let result = binaryStream(); - // encode to words for CryptoJS - cText = utils.encode_to_words(cText, "Uint8Arr"); + let cText = binaryStream( await file.readBytes( + file.getLength() - file.getCurrentPosition() - 32 - 1, + )); - // update HMAC - hmac0Act.update(cText); + let fs16 = utils.arrToInt(await file.readBytes(1)); - let fs16 = await file.readBytesAsInt(1); + hmac0Act = await webCryptSubtle._webHashHMAC(cText.finalize(), intKey); - let pText = decryptor0.process(cText).toString(CryptoJS.enc.Latin1) + decryptor0.finalize().toString(CryptoJS.enc.Latin1); + let pText; - // remove padding - let toremove = ((16 - fs16) % 16); - if( toremove !== 0 ) { - pText = pText.substr(0, pText.length - toremove); + try{ + pText = await webCryptSubtle._webDecryptAes(cText.finalize(), intKey, iv0, fs16); + } catch { + // AesCrypt on C# use PKCS7 in pad without full pad block + // webCrypt can't decrypt it without force cheat with fs16 = 0 + pText = await webCryptSubtle._webDecryptAes(cText.finalize(), intKey, iv0, 0); + let toremove = info.AESBlockSize - fs16; + pText = pText.slice(0, pText.length - toremove); } result.appendBytes(pText); - let hmac0 = await file.readBytesAsString(32); + let hmac0 = utils.bytes2str( await file.readBytes(32) ); if( hmac0.length !== 32 ) { console.warn("File is corrupted."); return false; } - if( hmac0 !== CryptoJS.enc.Latin1.stringify(hmac0Act.finalize()) ) { + if( hmac0 !== utils.bytes2str(hmac0Act) ) { console.warn("Bad HMAC (file is corrupted)."); return false; } - callback_progress(1); - return result.finalize(); } @@ -221,10 +173,9 @@ AesCrypt = function () { * * @param fileObj file element object * @param passw string password to encrypt - * @callback_progress callback function (current) => { 0 < current <= 1 } */ - async function encrypt(fileObj, passw, callback_progress = (c) => {}) { + async function encrypt(fileObj, passw) { if( passw.length > info.maxPassLen ) { console.warn("Password is too long."); return false; @@ -234,7 +185,7 @@ AesCrypt = function () { const iv1 = utils.urandom(info.AESBlockSize); // _stretch password and iv - const key = _stretch(passw, iv1); + const key = await _stretch(passw, iv1); // generate random main iv const iv0 = utils.urandom(info.AESBlockSize); @@ -242,106 +193,24 @@ AesCrypt = function () { // generate random internal key const intKey = utils.urandom(32); - const encryptor0 = _createEncryptor(intKey, iv0); - - // instantiate HMAC-SHA256 for the ciphertext - const hmac0 = CryptoJS.algo.HMAC.create(CryptoJS.algo.SHA256, utils.encode_to_words(intKey)); - - // instantiate another AES cipher - const encryptor1 = _createEncryptor(key, iv1); + let iv0_and_intkey = binaryStream(); + iv0_and_intkey.appendBytes(iv0); + iv0_and_intkey.appendBytes(intKey); // encrypt main iv and key - const c_iv_key = encryptor1.process(utils.encode_to_words(iv0 + intKey)).toString(CryptoJS.enc.Latin1) + encryptor1.finalize().toString(CryptoJS.enc.Latin1); + const c_iv_key = await webCryptSubtle._webEncryptAes(iv0_and_intkey.finalize(), key, iv1); - //# calculate HMAC-SHA256 of the encrypted iv and key - const hmac1 = CryptoJS.algo.HMAC.create(CryptoJS.algo.SHA256, utils.encode_to_words(key)); - hmac1.update(utils.encode_to_words(c_iv_key)); + const hmac1 = await webCryptSubtle._webHashHMAC(c_iv_key, key); - return await _createAesCryptFormat(fileObj, iv1, c_iv_key, hmac0, hmac1, encryptor0, callback_progress); + return await _createAesCryptFormat(fileObj, iv1, c_iv_key, intKey, hmac1, iv0); } /* PRIVATE START */ - // private class fileReader for read file by blocks - var fileReader = function(file) { - let _i = 0; - let _fileSize = file.size; - let _reader = new FileReader(); - let _file = file; - - function readChunk(length) { - return new Promise((resolve, reject) => { - let blob = _file.slice(_i, _i += length); - - _reader.onload = () => { - // return Uint8Array - resolve(new Uint8Array(_reader.result)); - }; - - _reader.onerror = reject; - - _reader.readAsArrayBuffer(blob); - }); - } - - async function readBytes(length) { - return await readChunk(length); - } - - async function readByte() { - let bytes = await readBytes(1); - return bytes[0]; - } - - async function readBytesAsString(length) { - let bytes = await readBytes(length); - return utils.bytes_to_latin1(bytes); - } - - async function readBytesAsInt(length) { - let bytes = await readBytes(length); - return utils.arrToInt(bytes); - } - - function getCurrentPosition() { - return _i; - } - - function getLength() { - return _fileSize; - } - - return { - readByte: readByte, - readBytes: readBytes, - readBytesAsInt: readBytesAsInt, - readBytesAsString: readBytesAsString, - getCurrentPosition: getCurrentPosition, - getLength: getLength, - }; - }; - - // private class utils for usefull functions - var utils = { - - random_int: function(min, max) { - return Math.floor(Math.random() * (max - min) + min); - }, + const utils = { urandom: function(length) { - let out = ""; - for (let i = 0; i < length; i ++) { - out += String.fromCharCode(this.random_int(0, 256)) - } - return out; - }, - - fillArray: function(value, len) { - let arr = []; - for (let i = 0; i < len; i ++) { - arr.push(value); - } - return arr; + return crypto.getRandomValues( new Uint8Array(length) ); }, arrToInt: function (arr) { // buffer is an UInt8Array @@ -349,89 +218,41 @@ AesCrypt = function () { }, // bytes is typed array - bytes_to_latin1: function(bytes) { + bytes2str: function(bytes) { return CryptoJS.enc.Latin1.stringify(CryptoJS.enc.Uint8Arr.parse(bytes)) }, - encode_to_words: function (input, enc = "Latin1") { - return CryptoJS.enc[enc].parse(input); + str2bytes: function(str, enc="Latin1") { + return CryptoJS.enc.Uint8Arr.decode(CryptoJS.enc[enc].parse(str)); }, } - // private class binaryArray for easy work with Uint8Array - let binaryArray = function() { - let _data = new Uint8Array([]); - - function appendBytes(input) { - let tmp; - - if (typeof (input) == "number") { - let hex_string = input.toString(16); - tmp = new Uint8Array(hex_string.match(/.{1,2}/g).map(byte => parseInt(byte, 16))); - } else if (typeof (input) == "string") { - tmp = new Uint8Array(input.length); - for (let i = 0; i < input.length; i ++) { - tmp[i] = input.charCodeAt(i); - } - } else { - tmp = new Uint8Array(input); - } - - let new_uint8_arr = new Uint8Array(_data.length + tmp.length); - - new_uint8_arr.set(_data); - new_uint8_arr.set(tmp, _data.length); - - _data = new_uint8_arr; - }; - - function finalize() { - return _data; - }; - - return { - appendBytes: appendBytes, - finalize: finalize, - } - } - - // create encryptor object with default options - function _createEncryptor(key, iv) { - return CryptoJS.algo.AES._createEncryptor(utils.encode_to_words(key), { - mode: CryptoJS.mode.CBC, - iv: CryptoJS.enc.Latin1.parse(iv), - padding: CryptoJS.pad.NoPadding, - }); - } - - // create decryptor object with default options - function _createDecryptor(key, iv) { - return CryptoJS.algo.AES._createDecryptor(utils.encode_to_words(key), { - mode: CryptoJS.mode.CBC, - iv: CryptoJS.enc.Latin1.parse(iv), - padding: CryptoJS.pad.NoPadding, - }); - } + // IMPORT webCryptSubtle THIS + // IMPORT fileBytesReader THIS + // IMPORT binaryStream THIS // stretch password and iv1 - function _stretch(passw, iv1) { + async function _stretch(passw, iv1) { // hash the external iv and the password 8192 times - let digest = utils.encode_to_words(iv1 + ("\x00".repeat(16))); + let digest_tmp = binaryStream(); + digest_tmp.appendBytes(iv1); + digest_tmp.appendBytes("\x00".repeat(16)); + + let digest = digest_tmp.finalize(); for (let i = 0; i < 8192; i ++) { - let passHash = CryptoJS.algo.SHA256.create(); - passHash.update(digest); - passHash.update(utils.encode_to_words(passw, "Utf16LE")); - digest = passHash.finalize(); + let passHash = binaryStream(digest); + passHash.appendBytes(utils.str2bytes(passw, "Utf16LE")); + digest = await webCryptSubtle._webHashSHA256(passHash.finalize(0)); } - return digest.toString(CryptoJS.enc.Latin1); + return digest; } // see https://www.aescrypt.com/aes_file_format.html - async function _createAesCryptFormat(fileObj, iv1, c_iv_key, hmac0, hmac1, encryptor0, callback_progress) { - let result = binaryArray(); + async function _createAesCryptFormat(fileObj, iv1, c_iv_key, intKey, hmac1, iv0) { + let result = binaryStream(); // header result.appendBytes("AES"); @@ -459,7 +280,7 @@ AesCrypt = function () { result.appendBytes([0x0, 0x80]); // "container" extension - result.appendBytes(utils.fillArray(0x0, 128)); + result.appendBytes("\x00".repeat(128)); // end-of-extensions tag result.appendBytes([0x0, 0x0]); @@ -472,61 +293,33 @@ AesCrypt = function () { result.appendBytes(c_iv_key); // HMAC-SHA256 of the encrypted iv and key - result.appendBytes(hmac1.finalize().toString(CryptoJS.enc.Latin1)); - - let fs16 = String.fromCharCode(0); - let file = new fileReader(fileObj); - - const blockLength = Math.ceil(file.getLength() / info.bufferSize); - - let total = Math.ceil( (file.getLength() - file.getCurrentPosition()) / info.bufferSize ); - let counter = 0; - - while( file.getCurrentPosition() < file.getLength() ) { + result.appendBytes(hmac1); - let fdata = await file.readBytes(info.bufferSize); - - let bytesRead = fdata.length; - - let cText = encryptor0.process(utils.encode_to_words(fdata, "Uint8Arr")).toString(CryptoJS.enc.Latin1); - - // check if EOF was reached - if (bytesRead < info.bufferSize) { - // file size mod 16, lsb positions - fs16 = String.fromCharCode(bytesRead % info.AESBlockSize); - // pad data (this is NOT PKCS#7!) - // ...unless no bytes or a multiple of a block size - // of bytes was read - let padLen; - if (bytesRead % info.AESBlockSize === 0) { - padLen = 0; - } else { - padLen = 16 - bytesRead % info.AESBlockSize; - } - - let padByte = fs16.repeat(padLen); + let file = new fileBytesReader(fileObj); + let bytesRead = file.getLength(); + let pText = binaryStream( + await file.readBytes( file.getLength() ) + ); - // encrypt data - cText += encryptor0.process(utils.encode_to_words(padByte)).toString(CryptoJS.enc.Latin1) - } + // file size mod 16, lsb positions + let fs16 = String.fromCharCode(bytesRead % info.AESBlockSize); - cText += encryptor0.finalize().toString(CryptoJS.enc.Latin1); + cText = await webCryptSubtle._webEncryptAes(pText.finalize(), intKey ,iv0); - hmac0.update(utils.encode_to_words(cText)); + hmac0 = await webCryptSubtle._webHashHMAC(cText, intKey); - result.appendBytes(cText); - - callback_progress(++counter / total); - } + result.appendBytes(cText); result.appendBytes(fs16); // HMAC-SHA256 of the encrypted file - result.appendBytes(hmac0.finalize().toString(CryptoJS.enc.Latin1)); + result.appendBytes(hmac0); return await result.finalize(); } + webCryptSubtle.info = info; + return { encrypt: encrypt, decrypt: decrypt, diff --git a/src/binary_stream.js b/src/binary_stream.js new file mode 100644 index 0000000..851c0e9 --- /dev/null +++ b/src/binary_stream.js @@ -0,0 +1,63 @@ +/** global: binaryStream */ + +/** + * binary array stream object + * + * + * @return The binaryStream object. + * + * @static + * + * @example + * var binaryStream = binaryStream([1,2,3]); + * binaryStream.appendBytes([4,5,6]); + * binaryStream.appendBytes("\x07\x08\x09"); + * console.log(binaryStream.finalize()) // returns Uint8Array [1,2,3,4,5,6,7,8,9] + * + * @param arr origin array + */ +const binaryStream = function(arr = []) { + let _data = new Uint8Array(arr); + + function appendBytes(input) { + let tmp; + + if (typeof (input) == "number") { + let hex_string = input.toString(16); + tmp = new Uint8Array(hex_string.match(/.{1,2}/g).map(byte => parseInt(byte, 16))); + } else if (typeof (input) == "string") { + tmp = new Uint8Array(input.length); + for (let i = 0; i < input.length; i ++) { + tmp[i] = input.charCodeAt(i); + } + } else { + tmp = new Uint8Array(input); + } + + let new_uint8_arr = new Uint8Array(_data.length + tmp.length); + + new_uint8_arr.set(_data); + new_uint8_arr.set(tmp, _data.length); + + _data = new_uint8_arr; + }; + + function get(i) { + return _data[i]; + } + + function finalize() { + return _data; + }; + + function getLength() { + return _data.length; + } + + return { + appendBytes: appendBytes, + finalize: finalize, + get: get, + getLength: getLength, + } +} \ No newline at end of file diff --git a/src/file_bytes_reader.js b/src/file_bytes_reader.js new file mode 100644 index 0000000..b275a0a --- /dev/null +++ b/src/file_bytes_reader.js @@ -0,0 +1,64 @@ +/** global: fileBytesReader */ +/** global: FileReader */ + +/** + * read file as string, bytes (uint8Array), int (1 byte) + * + * + * @return The fileBytesReader object. + * + * @static + * + * @example + * var fileElement = document.getElementById('file').files[0]; + * var file = fileBytesReader(fileElement); + * + * @param file a fileElement object + */ + + +const fileBytesReader = function(file) { + let _i = 0; + let _fileSize = file.size; + let _reader = new FileReader(); + let _file = file; + + function readChunk(length) { + return new Promise((resolve, reject) => { + let blob = _file.slice(_i, _i += length); + + _reader.onload = () => { + // return Uint8Array + resolve(new Uint8Array(_reader.result)); + }; + + _reader.onerror = reject; + + _reader.readAsArrayBuffer(blob); + }); + } + + async function readBytes(length) { + return await readChunk(length); + } + + async function readByte() { + let bytes = await readBytes(1); + return bytes[0]; + } + + function getCurrentPosition() { + return _i; + } + + function getLength() { + return _fileSize; + } + + return { + readByte: readByte, + readBytes: readBytes, + getCurrentPosition: getCurrentPosition, + getLength: getLength, + }; +}; diff --git a/src/web_crypto_subtle.js b/src/web_crypto_subtle.js new file mode 100644 index 0000000..b5f8d81 --- /dev/null +++ b/src/web_crypto_subtle.js @@ -0,0 +1,84 @@ +/** global: webCryptSubtle */ + +const webCryptSubtle = { + + _createKey: async function(intKeyArr, mode, functions) { + return await crypto.subtle.importKey( "raw", intKeyArr.buffer, mode , false, functions); + }, + + _webHashHMAC: async function(text, intKeyArr) { + let key_encoded = await this._createKey( + intKeyArr, + { // algorithm details + name: "HMAC", + hash: {name: "SHA-256"} + }, + ["sign", "verify"], + ); + + return new Uint8Array( await crypto.subtle.sign( + "HMAC", + key_encoded, + text + ) ); + }, + + _webEncryptAes :async function(pText, intKeyArr, iv0, stayLast=true) { + let key_encoded = await this._createKey(intKeyArr, "AES-CBC", ["encrypt", "decrypt"]); + + let encrypted = new Uint8Array( await crypto.subtle.encrypt( + { + name: "AES-CBC", + iv: iv0, + }, + key_encoded, + pText + ) ); + + if( pText.length % this.info.AESBlockSize === 0 && stayLast == true ) { + encrypted = encrypted.slice(0, encrypted.length - this.info.AESBlockSize); + } + + return encrypted; + }, + + _webHashSHA256: async function(text) { + return new Uint8Array(await crypto.subtle.digest("SHA-256", text.buffer)); + }, + + _webDecryptAes: async function(cText, intKeyArr, iv0, fs16 = 0) { + let key_encoded = await this._createKey(intKeyArr, "AES-CBC", ["encrypt", "decrypt"]); + let cTextArr = binaryStream(cText); + + // dirty cheat to add encrypted block pkcs7 padding if mod = 0 + // because WebCrypto subtle working only with pkcs7 pad + if( fs16 == 0 ) { + let modBlock = new Uint8Array(this.info.AESBlockSize); + + // xor padding with last block (see mode AES-CBC) + for( let i = 0; i < this.info.AESBlockSize;i++ ) { + modBlock[i] = 0x00 ^ cText[cText.length - this.info.AESBlockSize + i]; + } + + modBlockEncrypted = await this._webEncryptAes(modBlock, intKeyArr, iv0, false); + + cTextArr.appendBytes(modBlockEncrypted); + } + + let pText = new Uint8Array( await crypto.subtle.decrypt( + { + name: "AES-CBC", + iv: iv0, + }, + key_encoded, + cTextArr.finalize() + ) ); + + // clear empty data pkcs7 padding block + if( fs16 == 0 ) { + pText = pText.slice(0, pText.length - this.info.AESBlockSize); + } + + return pText; + }, +} \ No newline at end of file