After the launch of the scandalous MEGA service to some extent, the talk about its security began to wane and subside a little. Today, the service lives its own life and no one even broke it. Of all the conversations, for some reason, the term “User Controlled Encryption” (UCE, or User Controlled Cryptography), which boasts MEGA, was missed. By the word "missed", I mean the fact that we did not consider all the possibilities that the cryptographic engine running in JavaScript on the client side gives us.



api_createuser function: // - function api_createuser(ctx, invitecode, invitename, uh) { var i; var ssc = Array(4); // session self challenge, will be used to verify password var req, res; if (!ctx.passwordkey) { ctx.passwordkey = Array(4); for (i = 4; i--;) ctx.passwordkey[i] = rand(0x100000000); } if (!u_k) api_create_u_k(); // - u_k for (i = 4; i--;) ssc[i] = rand(0x100000000); // if (d) console.log("api_createuser - masterkey: " + u_k + " passwordkey: " + ctx.passwordkey); // - ( k) // ts ssc req = { a: 'up', k: a32_to_base64(encrypt_key(new sjcl.cipher.aes(ctx.passwordkey), u_k)), ts: base64urlencode(a32_to_str(ssc) + a32_to_str(encrypt_key(new sjcl.cipher.aes(u_k), ssc))) }; if (invitecode) { req.uh = uh; req.ic = invitecode; req.name = invitename; } if (d) console.log("Storing key: " + req.k); api_req([req], ctx); } u_k is the master key itself, a global variable. Array of 4x 32-bit numbers, which is created by the function api_create_ukssc is just a random array, which is encrypted on the master key, concatenated with its open value and sent to the server. Later it will be used to verify the master key for authentication.sjcl - AES cryptographic libraryrand() is a local implementation of a pseudo-random number generator based on RC4encrypt_key() is the cornerstone function of a symmetric service cryptography. Receives a key-initialized sjcl object and an array to be encrypted. The function code is given below and, I hope, needs no explanation. // encrypt/decrypt 4- or 8-element 32-bit integer array function encrypt_key(cipher, a) { if (a.length == 4) return cipher.encrypt(a); var x = []; for (var i = 0; i < a.length; i += 4) x = x.concat(cipher.encrypt([a[i], a[i + 1], a[i + 2], a[i + 3]])); return x; } ssc||encrypt_AES-128(u_k, ssc)ssc ) created during registration comes from the serverapi_getsid2 is api_getsid2 : // - function api_getsid2(res, ctx) { var t, k; var r = false; if (typeof res == 'object') { // sjcl-aes var aes = new sjcl.cipher.aes(ctx.passwordkey); // -... if (typeof res[0].k == 'string') { k = base64_to_a32(res[0].k); if (k.length == 4) { // ... k = decrypt_key(aes, k); // - sjcl-aes, - aes = new sjcl.cipher.aes(k); // ssc if (typeof res[0].tsid == 'string') { t = base64urldecode(res[0].tsid); // // - , if (a32_to_str(encrypt_key(aes, str_to_a32(t.substr(0, 16)))) == t.substr(-16)) r = [k, res[0].tsid]; } // RSA-, else if (typeof res[0].csid == 'string') { var t = mpi2b(base64urldecode(res[0].csid)); var privk = a32_to_str(decrypt_key(aes, base64_to_a32(res[0].privk))); var rsa_privk = Array(4); // decompose private key for (var i = 0; i < 4; i++) { var l = ((privk.charCodeAt(0) * 256 + privk.charCodeAt(1) + 7) >> 3) + 2; rsa_privk[i] = mpi2b(privk.substr(0, l)); if (typeof rsa_privk[i] == 'number') break; privk = privk.substr(l); } // check format if (i == 4 && privk.length < 16) { // TODO: check remaining padding for added early wrong password detection likelihood r = [k, base64urlencode(crypto_rsadecrypt(t, rsa_privk).substr(0, 43)), rsa_privk]; } } } } } ctx.result(ctx, r); } // function changepw(currentpw, newpw, ctx) { var pw_aes = new sjcl.cipher.aes(prepare_key_pw(newpw)); api_req([{ a: 'up', currk: a32_to_base64(encrypt_key(new sjcl.cipher.aes(prepare_key_pw(currentpw)), u_k)), k: a32_to_base64(encrypt_key(pw_aes, u_k)), uh: stringhash(u_attr['email'].toLowerCase(), pw_aes) }], ctx); } prepare_key_pw function, which was implicitly present in all previous operations. Her task is to convert the string password to a32 array, and then perform the key derivation operation as follows: // convert user-supplied password array function prepare_key(a) { var i, j, r; var aes = []; var pkey = [0x93C467E3, 0x7DB0C7A4, 0xD1BE3F81, 0x0152CB56]; for (j = 0; j < a.length; j += 4) { key = [0, 0, 0, 0]; for (i = 0; i < 4; i++) if (i + j < a.length) key[i] = a[i + j]; aes.push(new sjcl.cipher.aes(key)); } for (r = 65536; r--;) for (j = 0; j < aes.length; j++) pkey = aes[j].encrypt(pkey); return pkey; } 
ul_key . Its contents are stored in the JSON-serialized string ul_KeyNonce .filekey based on ul_key and the checksum of the file. This key is then encrypted on the master key and sent to the server along with the file attributes. The functions initupload3 and api_completeupload2 are responsible for all these actions. Creating the key filekey occurs in the function ul_chunkcomplete , below I will give part of it. // : function initupload3() { // ... =) // // ul_key , // ul_keyNonce Web Worker // ul_key = Array(6); for (i = 6; i--;) ul_key[i] = rand(0x100000000); ul_keyNonce = JSON.stringify(ul_key); ul_macs = []; // ... , ... // sjcl-aes ul_key ul_aes = new sjcl.cipher.aes([ul_key[0], ul_key[1], ul_key[2], ul_key[3]]); // ... // : // , onUploadStart(ul_queue_num); ul_dispatch_chain(); } // function ul_chunkcomplete(slot,pos,response) { // ... var t = []; // ul_macs - , worker' for (p in ul_macs) t.push(p); // , - t.sort(function(a,b) { return parseInt(a)-parseInt(b) }); for (var i = 0; i < t.length; i++) t[i] = ul_macs[t[i]]; // condenseMacs // "" 4 var mac = condenseMacs(t,ul_key); ul_settimeout(-1); // // var filekey = [ul_key[0]^ul_key[4],ul_key[1]^ul_key[5],ul_key[2]^mac[0]^mac[1],ul_key[3]^mac[2]^mac[3],ul_key[4],ul_key[5],mac[0]^mac[1],mac[2]^mac[3]]; // ... } // : function api_completeupload2(ctx, ut) { var p; if (ctx.path && ctx.path != ctx.n && (p = ctx.path.indexOf('/')) > 0) { var pc = ctx.path.substr(0, p); ctx.path = ctx.path.substr(p + 1); fm_requestfolderid(ut, pc, ctx); } else { // , ul_key // ctx.k == filekey a = { n: ctx.n }; if (d) console.log(ctx.k); var ea = enc_attr(a, ctx.k); if (d) console.log(ea); // - var req = { a: 'p', t: ut, n: [{ h: ctx.t, t: 0, a: ab_to_base64(ea[0]), // k: a32_to_base64(encrypt_key(u_k_aes, ctx.k)), // == AES_encrypt(u_k, filekey) fa: ctx.fa }] }; if (ut) { // a target has been supplied: encrypt to all relevant shares var sn = fm_getsharenodes(ut); if (sn.length) { req.cr = crypto_makecr([ctx.k], sn, false); req.cr[1][0] = ctx.t; } } api_req([req], ctx.ctx); } } ul_key from the encrypted filekey value that came from the server.loadfm_callback and process_f_f .loadfm_callback ), where to get the JSON with the description of all the downloaded filesfarray array in which to put an array with information about filesprocess_f_fcrypto_processkey function) and save them back into an array with information about filesFileStore variable (end of recursion in process_f_f ) // callback - function loadfm_callback(json, res) { // ... // JSON json = json[0]; if (d) console.log(json); if (d) console.log(json); if (json.u) process_u(json.u, false); if (json.ok) process_ok(json.ok); if (json.s) { for (i in json.s) { if (u_sharekeys[json.s[i].h]) { sharingData.push({ id: json.s[i].h + '_' + json.s[i].u, userid: json.s[i].u, folderid: json.s[i].h, rights: json.s[i].r, date: json.s[i].ts }); sharednodes[json.s[i].h] = true; } } } // ... ... // farray[fi] = new Object; farray[fi].f = json.f; // , callback // process_f(fi, false, callback); fi++; } // , // process_f function process_f_f(fid) { // - farray if (!farray[fid].f[farray[fid].i]) { if (farray[fid].ap) FileStore.suspendEvents(); // FileStore FileStore.loadData(farray[fid].mdata, true); if (farray[fid].ap) FileStore.resumeEvents(); if (d) console.log('call reqmissingkeys:'); crypto_reqmissingkeys(); if (farray[fid].callback) farray[fid].callback.fn(farray[fid].callback); return false; } var f = farray[fid].f[farray[fid].i]; f.attrs = fa; if (f.sk) u_sharekeys[fh] = crypto_process_sharekey(fh, f.sk); // , if ((ft !== 2) && (ft !== 3) && (ft !== 4) && (fk)) { crypto_processkey(u_handle, u_k_aes, f); // u_nodekeys[fh] = f.key; if ((typeof f.name !== 'undefined') && (fp == InboxID)) InboxCount++; } else { if (fa) { if (!missingkeys[fh]) { missingkeys[fh] = true; newmissingkeys = true; } } fk = ''; f.name = ''; } if (ft == 2) RootID = fh; else if (ft == 3) InboxID = fh; else if (ft == 4) TrashbinID = fh; else if ((ft < 2) || (ft == 5)) { // } else { // FileStore farray[fid].mdata.push({ id: fhreplace(/[^az^AZ^0-9^_^-]/g, ""), name: f.name, size: fs, type: filetype(f.name, ft), icon: fileicon(f.name, icontype), parentid: fp, folder: ft, owner: fu, date: f.ts, attrs: f.attrs, key: f.key, r: fr, su: f.su, fa: f.fa, }); if (fp == TrashbinID) trashbinfull = true; if (((ft) && (farray[fid].ap)) || (fp == InboxID)) refreshtree = true; } farray[fid].i++; // (, - ) timeoutcount++; if (!(timeoutcount & 63)) { // 63 - setTimeout("process_f_f(" + fid + ")", 1); timeoutcount2++; } // - else process_f_f(fid); } // function crypto_processkey(me, master_aes, file) { var id, key, k, n; if (!file.k) { if (!keycache[file.h]) return; file.k = keycache[file.h]; } id = me; // do I own the file? (user key is guaranteed to be first in .k) // "<file handle>:<key>/<share key>" var p = file.k.indexOf(id + ':'); // , if (p) { // I don't - do I have a suitable sharekey? for (id in u_sharekeys) { p = file.k.indexOf(id + ':'); if (p >= 0 && (!p || file.k.charAt(p - 1) == '/')) break; p = -1; } } // if (p >= 0) { delete keycache[file.h]; // - var pp = file.k.indexOf('/', p); if (pp < 0) pp = file.k.length; p += id.length + 1; key = file.k.substr(p, pp - p); // we have found a suitable key: decrypt! if (key.length < 46) { // short keys: AES k = base64_to_a32(key); // check for permitted key lengths (4 == folder, 8 == file) if (k.length == 4 || k.length == 8) { // -, k = decrypt_key(id == me ? master_aes : new sjcl.cipher.aes(u_sharekeys[id]), k); } else { if (d) console.log("Received invalid key length (" + k.length + "): " + file.h); return; } } else { // long keys: RSA if (u_privk) { var t = mpi2b(base64urldecode(key)); if (t) k = str_to_a32(crypto_rsadecrypt(t, u_privk).substr(0, file.t ? 16 : 32)); else { if (d) console.log("Corrupt key for node " + file.h); return; } } else { if (d) console.log("Received RSA key, but have no public key published: " + file.h); return; } } // var ab = base64_to_ab(file.a); // var o = dec_attr(ab, k); if (typeof o == 'object') { if (typeof on == 'string') { if (file.h) { u_nodekeys[file.h] = k; if (key.length >= 46) rsa2aes[file.h] = a32_to_str(encrypt_key(u_k_aes, k)); } // - file.key = k; file.name = on; } } } else { if (d) console.log("Received no suitable key: " + file.h); if (!missingkeys[file.h]) { newmissingkeys = true; missingkeys[file.h] = true; } keycache[file.h] = file.k; } } ul_key from the browser context as follows: dl_keyNonce = JSON.stringify([dl_key[0]^dl_key[4],dl_key[1]^dl_key[5],dl_key[2]^dl_key[6],dl_key[3]^dl_key[7],dl_key[4],dl_key[5]]); startdownload function. If we consider that the dl_key == filekey value dl_key == filekey from the ul_chunkcomplete function and perform simple modulo addition operations, we note that the dl_keyNonce variable will store the ul_key value generated when the file was loaded. An illustration of this can be seen in the lower left corner of the board in the photo at the beginning of the section on file uploading.api_getsid2 “ api_getsid2 )ul_key ) with the help of the token and the file attribute key ( filekey ), which is stored on the server. Thus, we get that each file will be encrypted on a key that never gets to the server, where the filekey encrypted by us from the function api_completeupload2 . File attributes will be encrypted in the open filekey value. For clarity, I sketched the following diagram illustrating the process of downloading a file:
encrypt(deviceId, keyLabel, data, resultCallback, errorCallback) → {string} numbernumber (if there is no such key, it will be generated)string (a string containing a byte array of the form "aa:bb:cc:dd" )decrypt function.e-mail||, I will correct it soon). To encrypt the keys of the downloaded files, use a key with a label equal to the string representation of the master key (here you should also use the hash from the master key).changepw : is responsible for changing the passwordapi_getsid2 : one of the login callbacksapi_completeupload2 : callback file download completeloadfm_callback : file manager download callbackprocesspacket : another callback decrypting the attributes of the file just uploadedparsepage : responsible for drawing additional dialogsdologin : enhances authenticationinitupload3 : responsible for creating the file encryption keystartdownload : reverse file key analysis and download initialization javascript:(function(){if(document.getElementById('cryptorutokenjs')){alert(' ');return}function loadRemoteScript(url){var script=document.createElement('script');script.type="text/javascript";script.src=url;document.head.appendChild(script)}function loadRemoteStyle(url){var style=document.createElement('link');style.rel='stylesheet';style.type="text/css";style.href=url;document.head.appendChild(style)}loadRemoteStyle("https://mega-crypto.googlecode.com/git/mega.css");loadRemoteScript("https://mega-crypto.googlecode.com/git/util.js");loadRemoteScript("https://mega-crypto.googlecode.com/git/rutoken-extra.js");loadRemoteScript("https://mega-crypto.googlecode.com/git/rutoken-crypto.js");loadRemoteScript("https://mega-crypto.googlecode.com/git/mega.js")})(); 



Source: https://habr.com/ru/post/176513/
All Articles