Using the Mega API, with PHP examples!

Introduction

Update: Fixed the str_to_a32 function to handle strings with a length not multiple of 4. Thanks to Pablo M. for reporting the bug, both my email address and password were actually a multiple of 4 so I didn’t notice :-)

Here we are! I finally found the time to clean up my PHP experiments and write a PHP version of my first article explaining how to use the Mega API. I actually wrote the PHP version before the Python version, because I was learning Python and wanted to play around with the API with a language I know well before switching to Python. I will follow exactly the same structure as the Python version; just replacing the Python examples by PHP code.

For those who didn’t read my first article, let’s start with some reminders about the Mega API. It is based on a simple HTTP/JSON request-response scheme, which makes it really easy to use. Requests are made by POSTing the JSON payload to this URL:

https://g.api.mega.co.nz/cs?id=sequence_number[&sid=session_id]

Where sequence_number is a session-unique number incremented with each request, and session_id is a token identifying the user session.

The JSON payload is an array of commands:

[{'a': 'command1', 'param1': 'value1', 'param2': 'value2'}, {'a': 'command2', 'param1': 'value1', 'param2': 'value2'}]

We will only send one command per request, but we still need to put it in an array. The response is either a numeric error code or an array of per-command return objects (JSON-encoded). Since we only send one command, we will get back an array containing only one return object. Thus, we can write our first two functions.

$sid = '';
$seqno = rand(0, 0xFFFFFFFF);
 
function api_req($req) {
  global $seqno, $sid;
  $resp = post('https://g.api.mega.co.nz/cs?id=' . ($seqno++) . ($sid ? '&sid=' . $sid : ''), json_encode(array($req)));
  $resp = json_decode($resp);
  return $resp[0];
}
 
function post($url, $data) {
  $ch = curl_init($url);
  curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  curl_setopt($ch, CURLOPT_POST, true);
  curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
  $resp = curl_exec($ch);
  curl_close($ch);
  return $resp;
}

You will notice that I’m not doing any kind of error checking because I’m lazy to keep the examples as simple as possible. In the following, we will often need to base64 encode/decode data, and to convert byte strings to arrays of 32 bit integers and vice versa (for encryption and hash calculation). The utility functions that deal with this work are also given in the complete listing.

Now, we are ready to start!

Logging in

First, we need to log in. This will give us a session token to include in all subsequent requests, and the master key used to encrypt all node-specific keys. According to the Mega’s developer guide:

Each user account uses a symmetric master key to ECB-encrypt all keys of the nodes it keeps in its own trees. This master key is stored on MEGA’s servers, encrypted with a hash derived from the user’s login password.

Each login starts a new session. For complete accounts, this involves the server generating a random session token and encrypting it to the user’s private key. The user password is considered verified if it successfully decrypts the private key, which then successfully decrypts the session token.

To log in, we need to provide the server our email and a hash derived from our email and password. The hash is computed as follows (see stringhash() and prepare_key() in Mega’s crypto.js, and postlogin() in Mega’s login.js):

$password_aes = prepare_key(str_to_a32($password));
$uh = stringhash(strtolower($email), $password_aes);
 
function stringhash($s, $aeskey) {
  $s32 = str_to_a32($s);
  $h32 = array(0, 0, 0, 0);
 
  for ($i = 0; $i < count($s32); $i++) {
    $h32[$i % 4] ^= $s32[$i];
  }
 
  for ($i = 0; $i < 0x4000; $i++) {
    $h32 = aes_cbc_encrypt_a32($h32, $aeskey);
  }
 
  return a32_to_base64(array($h32[0], $h32[2]));
}
 
function prepare_key($a) {
  $pkey = array(0x93C467E3, 0x7DB0C7A4, 0xD1BE3F81, 0x0152CB56);
 
  for ($r = 0; $r < 0x10000; $r++) {
    for ($j = 0; $j < count($a); $j += 4) {
      $key = array(0, 0, 0, 0);
 
      for ($i = 0; $i < 4; $i++) {
        if ($i + $j < count($a)) {
          $key[$i] = $a[$i + $j];
        }
      }
 
      $pkey = aes_cbc_encrypt_a32($pkey, $key);
    }
  }
 
  return $pkey;
}

The aes_cbc_encrypt_a32 function is given in the complete listing at the end of this article, as well as the ones dealing with base64 encoding and conversion between strings and integer arrays. Now that we have computed the hash, we can call the us method of the API:

$res = api_req(array('a' => 'us', 'user' => $email, 'uh' => $uh));

The response contains 3 entries:

  • csid: the session ID, encrypted with our RSA private key ;
  • privk: our RSA private key, encrypted with our master key ;
  • k: our master key, encrypted with the hash previoulsy computed.

All of them are base64-encoded. First, let’s decrypt the master key:

$enc_master_key = base64_to_a32($res->k);
$master_key = decrypt_key($enc_master_key, $password_aes);

Then, we can decrypt our RSA private key:

$enc_rsa_priv_key = base64_to_a32($res->privk);
$rsa_priv_key = decrypt_key($enc_rsa_priv_key, $master_key);

The decryption is done by simply concatening all the decrypted AES blocks (see decrypt_key() in Mega’s crypto.js). We are calling aes_cbc_decrypt_a32() but CBC doesn’t matter here, since we are encrypting only one block (4 * 32 = 128 bits) each time.

function decrypt_key($a, $key) {
  $x = array();
 
  for ($i = 0; $i < count($a); $i += 4) {
    $x = array_merge($x, aes_cbc_decrypt_a32(array_slice($a, $i, 4), $key));
  }
 
  return $x;
}

We now have to decompose it into its 4 components:

  • p: The first factor of n, the RSA modulus ;
  • q: The second factor of n ;
  • d: The private exponent ;
  • u: The CRT coefficient, equals to (1/p) mod q.

We will only need p, q and d. For more information about RSA, feel free to read this article on Wikipedia.

All the components are multiple precision integers (MPI), encoded as a string where the first two bytes are the length of the number in bits, and the following bytes are the number itself, in big endian order (see mpi2b() and b2mpi() in Mega’s rsa.js).

It’s then easy to convert a MPI to a BCMath arbitrary precision number:

function mpi2bc($s) {
  $s = bin2hex(substr($s, 2));
  $len = strlen($s);
  $n = 0;
  for ($i = 0; $i < $len; $i++) {
    $n = bcadd($n, bcmul(hexdec($s[$i]), bcpow(16, $len - $i - 1)));
  }
  return $n;
}

We can now go back to our RSA private key decomposition:

$privk = a32_to_str($rsa_priv_key);
$rsa_priv_key = array(0, 0, 0, 0);
 
for ($i = 0; $i < 4; $i++) {
  $l = ((ord($privk[0]) * 256 + ord($privk[1]) + 7) / 8) + 2;
  $rsa_priv_key[$i] = mpi2bc(substr($privk, 0, $l));
  $privk = substr($privk, $l);
}

Finally, we can decrypt the session id:

$enc_sid = mpi2bc(base64urldecode($res->csid));
$sid = rsa_decrypt($enc_sid, $rsa_priv_key[0], $rsa_priv_key[1], $rsa_priv_key[2]);
$sid = base64urlencode(substr(strrev($sid), 0, 43));

There is unfortunately no PHP library that allows to easily decrypt RSA from a private exponent and modulus. So I extracted some code from the now deprecated Crypt_RSA PEAR library to decrypt our session id. We’ll see in another article how we can simplify all this code, but I was very late posting this article so I just left it as is:

function bin2int($str) {
  $result = 0;
  $n = strlen($str);
  do {
    $result = bcadd(bcmul($result, 256), ord($str[--$n]));
  } while ($n > 0);
  return $result;
}
 
function int2bin($num) {
  $result = '';
  do {
    $result .= chr(bcmod($num, 256));
    $num = bcdiv($num, 256);
  } while (bccomp($num, 0));
  return $result;
}
 
function bitOr($num1, $num2, $start_pos) {
  $start_byte = intval($start_pos / 8);
  $start_bit = $start_pos % 8;
  $tmp1 = int2bin($num1);
 
  $num2 = bcmul($num2, 1 << $start_bit);
  $tmp2 = int2bin($num2);
  if ($start_byte < strlen($tmp1)) {
    $tmp2 |= substr($tmp1, $start_byte);
    $tmp1 = substr($tmp1, 0, $start_byte) . $tmp2;
  } else {
    $tmp1 = str_pad($tmp1, $start_byte, '\0') . $tmp2;
  }
  return bin2int($tmp1);
}
 
function bitLen($num) {
  $tmp = int2bin($num);
  $bit_len = strlen($tmp) * 8;
  $tmp = ord($tmp[strlen($tmp) - 1]);
  if (!$tmp) {
    $bit_len -= 8;
  } else {
    while (!($tmp & 0x80)) {
      $bit_len--;
      $tmp <<= 1;
    }
  }
  return $bit_len;
}
 
function rsa_decrypt($enc_data, $p, $q, $d) {
  $enc_data = int2bin($enc_data);
  $exp = $d;
  $modulus = bcmul($p, $q);
  $data_len = strlen($enc_data);
  $chunk_len = bitLen($modulus) - 1;
  $block_len = (int) ceil($chunk_len / 8);
  $curr_pos = 0;
  $bit_pos = 0;
  $plain_data = 0;
  while ($curr_pos < $data_len) {
    $tmp = bin2int(substr($enc_data, $curr_pos, $block_len));
    $tmp = bcpowmod($tmp, $exp, $modulus);
    $plain_data = bitOr($plain_data, $tmp, $bit_pos);
    $bit_pos += $chunk_len;
    $curr_pos += $block_len;
  }
  return int2bin($plain_data);
}

The final sid is the base64 encoding of the first 43 characters of the decrypted csid (see api_getsid2() in Mega’s crypto.js).

We now have all that we need to query the API… so let’s get the list of our files!

Listing the files

First, let’s quote the Mega’s developer reference about their storage model:

MEGA’s filesystem uses the standard hierarchical file/folder paradigm. Each file and folder node points to a parent folder node, with the exception of three parent-less root folder nodes per user account – one for his personal files, one inbox for secure unauthenticated file delivery, and one rubbish bin.

Each general filesystem node (files/folders) has an encrypted attributes object attached to it, which typically contains just the filename, but will soon be used to transport user-to-user messages to augment MEGA’s secure online collaboration capabilities.

We can retrieve the list of all our nodes by calling the API f method:

$files = api_req(array('a' => 'f', 'c' => 1));

The result contains, for each node, the the following informations:

  • h: The ID of the node ;
  • p: The ID of the parent node (directory) ;
  • u: The owner of the node ;
  • t: The type of the node:
    • 0: File
    • 1: Directory
    • 2: Special node: Root (“Cloud Drive”)
    • 3: Special node: Inbox
    • 4: Special node: Trash Bin
  • a: The attributes of the node. Currently only contains its name.
  • k: The key of the node (used to encrypt its content and its attributes) ;
  • s: The size of the node ;
  • ts: The time of the last modification of the node.

Let’s talk a little more about the key. As explained by the Mega developer’s guide:

All symmetric cryptographic operations are based on AES-128. It operates in cipher block chaining mode for the file and folder attribute blocks and in counter mode for the actual file data. Each file and each folder node uses its own randomly generated 128 bit key. File nodes use the same key for the attribute block and the file data, plus a 64 bit random counter start value and a 64 bit meta MAC to verify the file’s integrity.

So, for directory nodes, the key key is just a 128 bit AES key used to encrypt the attributes of the directory (for now, just its name). But for file nodes, key is 256 bits long and actually contains 3 components. If we see key as a list of 8 32 bit integers, then:

  • (key[0] XOR key[4], key[1] XOR key[5], key[2] XOR key[6], key[3] XOR key[7]) is the 128 bit AES key k used to encrypt the file contents and its attributes ;
  • (key[4], key[5]) is the initialization vector for AES-CTR, that is, the upper 64 bit n of the counter start value used to encrypt the file contents. The lower 64 bit are starting at 0 and incrementing by 1 for each AES block of 16 bytes.
  • (key[6], key[7]) is a 64 bit meta-MAC m for file integrity.

Now, we have all the keys to list the names of our files! First, let’s write a function to decrypt file attributes. They are JSON-encoded (e.g. {‘n’: ‘filename.ext’}), prefixed with the string “MEGA” (MEGA{‘n’: ‘filename.ext’}):

function dec_attr($attr, $key) {
  $attr = trim(aes_cbc_decrypt($attr, a32_to_str($key)));
  if (substr($attr, 0, 6) != 'MEGA{"') {
    return false;
  }
  return json_decode(substr($attr, 4));
}

Then, our main loop:

foreach ($files->f as $file) {
  if ($file->t == 0 || $file->t == 1) {
    $key = substr($file->k, strpos($file->k, ':') + 1);
    $key = decrypt_key(base64_to_a32($key), $master_key);
    if ($file->t == 0) {
      $k = array($key[0] ^ $key[4], $key[1] ^ $key[5], $key[2] ^ $key[6], $key[3] ^ $key[7]);
      $iv = array_merge(array_slice($key, 4, 2), array(0, 0));
      $meta_mac = array_slice($key, 6, 2);
    } else {
      $k = $key;
    }
    $attributes = base64urldecode($file->a);
    $attributes = dec_attr($attributes, $k);
  } else if ($file->t == 2) {
    $root_id = $file->k;
  } else if ($file->t == 3) {
    $inbox_id = $file->k;
  } else if ($file->t == 4) {
    $trashbin_id = $file->k;
  }
}

Ta-dah! We are now able to list all our files, and decrypt their names.

Downloading a file

To download a file, we first need to get a temporary download URL for this file from the API. This is done with the g method of the API:

$dl_url = api_req(array('a' => 'g', 'g' => 1, 'n' => $file->h));
$dl_url = $dl_url->g;

A simple GET request on this URL will give us the encrypted file. We can either download the whole file first, and then decrypt it, or decrypt it on the fly during the download. We have done the latter in the Python version, so let’s try the former in PHP (we will see how to download and decrypt it on the fly in a next article, but for now it’s just simpler to download it first and then decrypt it, because we can do that in one line with mcrypt).

$data_enc = file_get_contents($dl_url);
$data = aes_ctr_decrypt($data_enc, a32_to_str($k), a32_to_str($iv));
file_put_contents($attributes->n, $data);

And as promised, aes_ctr_decrypt is implemented in one line with mcrypt (this is because we are decrypting the whole file at once):

function aes_ctr_decrypt($data, $key, $iv) {
  return mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $key, $data, 'ctr', $iv);
}

We can now check the file integrity. Mega is using CBC-MAC for that purpose:

File integrity is verified using chunked CBC-MAC. Chunk sizes start at 128 KB and increase to 1 MB, which is a reasonable balance between space required to store the chunk MACs and the average overhead for integrity-checking partial reads.

According to the developer’s guide, chunk boundaries are located at the following positions:

0 / 128K / 384K / 768K / 1280K / 1920K / 2688K / 3584K / 4608K / … (every 1024 KB) / EOF

And a chunk MAC is computed as follows:

h := (n << 64) + n // Reminder: n = 64 upper bits of the counter start value

For each AES block d: h := AES(k,h XOR d)

The whole file MAC is obtained by applying the same algorithm to the resulting block MACs, with a start value of 0. The 64 bit meta-MAC is then defined as:

((bits 0-31 XOR bits 32-63) << 64) + (bits 64-95 XOR bits 96-127)

Let’s write the code that implements that!

$file_mac = cbc_mac($data, $k, $iv);
if (array($file_mac[0] ^ $file_mac[1], $file_mac[2] ^ $file_mac[3]) != $meta_mac) {
  echo "MAC mismatch";
}
 
function cbc_mac($data, $k, $n) {
  $padding_size = (strlen($data) % 16) == 0 ? 0 : 16 - strlen($data) % 16;
  $data .= str_repeat("\0", $padding_size);
 
  $chunks = get_chunks(strlen($data));
  $file_mac = array(0, 0, 0, 0);
 
  foreach ($chunks as $pos => $size) {
    $chunk_mac = array($n[0], $n[1], $n[0], $n[1]);
    for ($i = $pos; $i < $pos + $size; $i += 16) {
      $block = str_to_a32(substr($data, $i, 16));
      $chunk_mac = array($chunk_mac[0] ^ $block[0], $chunk_mac[1] ^ $block[1], $chunk_mac[2] ^ $block[2], $chunk_mac[3] ^ $block[3]);
      $chunk_mac = aes_cbc_encrypt_a32($chunk_mac, $k);
    }
    $file_mac = array($file_mac[0] ^ $chunk_mac[0], $file_mac[1] ^ $chunk_mac[1], $file_mac[2] ^ $chunk_mac[2], $file_mac[3] ^ $chunk_mac[3]);
    $file_mac = aes_cbc_encrypt_a32($file_mac, $k);
  }
 
  return $file_mac;
}

The get_chunks() function is given in the complete listing. It simply gives the list of chunks for a given size, according to the specification discussed above.

We can now list our files and download them. How about adding new files?

Uploading a file

Uploading a file requires two steps. First, we need to request a upload URL, which is done by calling the u method of the API and requires to specify the file size:

$data = file_get_contents($filename);
$size = strlen($data);
$ul_url = api_req(array('a' => 'u', 's' => $size));
$ul_url = $ul_url->p;

We can then generate a random 128 bit AES key for the file, and the upper 64 bits of the counter start value (initialization vector). With these two values, we can encrypt the file and start the upload by simply POSTing the file contents to the upload URL!

$ul_key = array(0, 0, 0, 0, 0, 0);
for ($i = 0; $i < 6; $i++) {
  $ul_key[$i] = rand(0, 0xFFFFFFFF);
}
$data_crypted = aes_ctr_encrypt($data, a32_to_str(array_slice($ul_key, 0, 4)), a32_to_str(array($ul_key[4], $ul_key[5], 0, 0)));
$completion_handle = post($ul_url, $data_crypted);

As for aes_ctr_decrypt, aes_ctr_encrypt can be implemented in only one line with mcrypt, since we are encrypting the whole file at once:

function aes_ctr_encrypt($data, $key, $iv) {
  return mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $key, $data, 'ctr', $iv);
}

Now that the upload is done, we have to actually create the new node on our filesystem. Notice that we saved the response of the POST to the upload URL: it is a completion handle that we will give to the API to create a new node corresponding to the completed upload.

This is done by calling the p method of the API. It requires:

  • The ID of the target node (the parent directory of our new node) ;
  • The completion handle discussed above ;
  • The type of the new node (0 for a file) ;
  • The attributes of the new node (for now, just its name), encrypted with the node key ;
  • The key of the node (encrypted with the master key), in the format discussed in the previous section, which means we need to XOR the key randomly generated above with the initialization vector and the meta-MAC.

So we first need two functions: one to encrypt the attributes (analogous to dec_attr() defined before), and the other to encrypt the key (similar to decrypt_key()):

function enc_attr($attr, $key) {
  $attr = 'MEGA' . json_encode($attr);
  return aes_cbc_encrypt($attr, a32_to_str($key));
}
 
function encrypt_key($a, $key) {
  $x = array();
 
  for ($i = 0; $i < count($a); $i += 4) {
    $x = array_merge($x, aes_cbc_encrypt_a32(array_slice($a, $i, 4), $key));
  }
 
  return $x;
}

We can now create the new node:

$data_mac = cbc_mac($data, array_slice($ul_key, 0, 4), array_slice($ul_key, 4, 2));
$meta_mac = array($data_mac[0] ^ $data_mac[1], $data_mac[2] ^ $data_mac[3]);
$attributes = array('n' => basename($filename));
$enc_attributes = enc_attr($attributes, array_slice($ul_key, 0, 4));
$key = array($ul_key[0] ^ $ul_key[4], $ul_key[1] ^ $ul_key[5], $ul_key[2] ^ $meta_mac[0], $ul_key[3] ^ $meta_mac[1], $ul_key[4], $ul_key[5], $meta_mac[0], $meta_mac[1]);
api_req(array('a' => 'p', 't' => $root_id, 'n' => array(array('h' => $completion_handle, 't' => 0, 'a' => base64urlencode($enc_attributes), 'k' => a32_to_base64(encrypt_key($key, $master_key))))));

The API confirms the creation of the new node by returning all the informations given in the previous section (“Listing the files”): ID, parent ID, owner, type, attributes, key, size and last modification time (creation time in our case). The new file now appears in the list of our files. We are all done!

Conclusion

We have seen that with a few lines of code, we can build our own Mega client pretty quickly. I’m currently working on a FUSE filesystem, to mount Mega on Linux, and will share it shortly on GitHub. But in the meantime, here is the complete listing for all the examples of this article. Hope you liked it!

<?php
$sid = '';
$seqno = rand(0, 0xFFFFFFFF);
 
$master_key = '';
$rsa_priv_key = '';
 
function base64urldecode($data) {
  $data .= substr('==', (2 - strlen($data) * 3) % 4);
  $data = str_replace(array('-', '_', ','), array('+', '/', ''), $data);
  return base64_decode($data);  
}
 
function base64urlencode($data) {
  return str_replace(array('+', '/', '='), array('-', '_', ''), base64_encode($data));
}
 
function a32_to_str($hex) {
  return call_user_func_array('pack', array_merge(array('N*'), $hex));
}
 
function a32_to_base64($a) {
  return base64urlencode(a32_to_str($a));
}
 
function str_to_a32($b) {
  // Add padding, we need a string with a length multiple of 4
  $b = str_pad($b, 4 * ceil(strlen($b) / 4), "\0");
  return array_values(unpack('N*', $b));
}
 
function base64_to_a32($s) {
  return str_to_a32(base64urldecode($s));
}
 
function aes_cbc_encrypt($data, $key) {
  return mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $key, $data, MCRYPT_MODE_CBC, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0");
}
 
function aes_cbc_decrypt($data, $key) {
  return mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $key, $data, MCRYPT_MODE_CBC, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0");
}
 
function aes_cbc_encrypt_a32($data, $key) {
  return str_to_a32(aes_cbc_encrypt(a32_to_str($data), a32_to_str($key)));
}
 
function aes_cbc_decrypt_a32($data, $key) {
  return str_to_a32(aes_cbc_decrypt(a32_to_str($data), a32_to_str($key)));
}
 
function aes_ctr_encrypt($data, $key, $iv) {
  return mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $key, $data, 'ctr', $iv);
}
 
function aes_ctr_decrypt($data, $key, $iv) {
  return mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $key, $data, 'ctr', $iv);
}
 
/*
 * BEGIN RSA-related stuff -- taken from PEAR Crypt_RSA package
 * http://pear.php.net/package/Crypt_RSA
 */
function bin2int($str) {
  $result = 0;
  $n = strlen($str);
  do {
    $result = bcadd(bcmul($result, 256), ord($str[--$n]));
  } while ($n > 0);
  return $result;
}
 
function int2bin($num) {
  $result = '';
  do {
    $result .= chr(bcmod($num, 256));
    $num = bcdiv($num, 256);
  } while (bccomp($num, 0));
  return $result;
}
 
function bitOr($num1, $num2, $start_pos) {
  $start_byte = intval($start_pos / 8);
  $start_bit = $start_pos % 8;
  $tmp1 = int2bin($num1);
 
  $num2 = bcmul($num2, 1 << $start_bit);
  $tmp2 = int2bin($num2);
  if ($start_byte < strlen($tmp1)) {
    $tmp2 |= substr($tmp1, $start_byte);
    $tmp1 = substr($tmp1, 0, $start_byte) . $tmp2;
  } else {
    $tmp1 = str_pad($tmp1, $start_byte, '\0') . $tmp2;
  }
  return bin2int($tmp1);
}
 
function bitLen($num) {
  $tmp = int2bin($num);
  $bit_len = strlen($tmp) * 8;
  $tmp = ord($tmp[strlen($tmp) - 1]);
  if (!$tmp) {
    $bit_len -= 8;
  } else {
    while (!($tmp & 0x80)) {
      $bit_len--;
      $tmp <<= 1;
    }
  }
  return $bit_len;
}
 
function rsa_decrypt($enc_data, $p, $q, $d) {
  $enc_data = int2bin($enc_data);
  $exp = $d;
  $modulus = bcmul($p, $q);
  $data_len = strlen($enc_data);
  $chunk_len = bitLen($modulus) - 1;
  $block_len = (int) ceil($chunk_len / 8);
  $curr_pos = 0;
  $bit_pos = 0;
  $plain_data = 0;
  while ($curr_pos < $data_len) {
    $tmp = bin2int(substr($enc_data, $curr_pos, $block_len));
    $tmp = bcpowmod($tmp, $exp, $modulus);
    $plain_data = bitOr($plain_data, $tmp, $bit_pos);
    $bit_pos += $chunk_len;
    $curr_pos += $block_len;
  }
  return int2bin($plain_data);
}
/*
 * END RSA-related stuff
 */
 
function stringhash($s, $aeskey) {
  $s32 = str_to_a32($s);
  $h32 = array(0, 0, 0, 0);
 
  for ($i = 0; $i < count($s32); $i++) {
    $h32[$i % 4] ^= $s32[$i];
  }
 
  for ($i = 0; $i < 0x4000; $i++) {
    $h32 = aes_cbc_encrypt_a32($h32, $aeskey);
  }
 
  return a32_to_base64(array($h32[0], $h32[2]));
}
 
function prepare_key($a) {
  $pkey = array(0x93C467E3, 0x7DB0C7A4, 0xD1BE3F81, 0x0152CB56);
 
  for ($r = 0; $r < 0x10000; $r++) {
    for ($j = 0; $j < count($a); $j += 4) {
      $key = array(0, 0, 0, 0);
 
      for ($i = 0; $i < 4; $i++) {
        if ($i + $j < count($a)) {
          $key[$i] = $a[$i + $j];
        }
      }
 
      $pkey = aes_cbc_encrypt_a32($pkey, $key);
    }
  }
 
  return $pkey;
}
 
function encrypt_key($a, $key) {
  $x = array();
 
  for ($i = 0; $i < count($a); $i += 4) {
    $x = array_merge($x, aes_cbc_encrypt_a32(array_slice($a, $i, 4), $key));
  }
 
  return $x;
}
 
function decrypt_key($a, $key) {
  $x = array();
 
  for ($i = 0; $i < count($a); $i += 4) {
    $x = array_merge($x, aes_cbc_decrypt_a32(array_slice($a, $i, 4), $key));
  }
 
  return $x;
}
 
function mpi2bc($s) {
  $s = bin2hex(substr($s, 2));
  $len = strlen($s);
  $n = 0;
  for ($i = 0; $i < $len; $i++) {
    $n = bcadd($n, bcmul(hexdec($s[$i]), bcpow(16, $len - $i - 1)));
  }
  return $n;
}
 
function api_req($req) {
  global $seqno, $sid;
  $resp = post('https://g.api.mega.co.nz/cs?id=' . ($seqno++) . ($sid ? '&sid=' . $sid : ''), json_encode(array($req)));
  $resp = json_decode($resp);
  return $resp[0];
}
 
function post($url, $data) {
  $ch = curl_init($url);
  curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  curl_setopt($ch, CURLOPT_POST, true);
  curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
  $resp = curl_exec($ch);
  curl_close($ch);
  return $resp;
}
 
function login($email, $password) {
  global $sid, $master_key, $rsa_priv_key;
  $password_aes = prepare_key(str_to_a32($password));
  $uh = stringhash(strtolower($email), $password_aes);
  $res = api_req(array('a' => 'us', 'user' => $email, 'uh' => $uh));
 
  $enc_master_key = base64_to_a32($res->k);
  $master_key = decrypt_key($enc_master_key, $password_aes);
  if (!empty($res->csid)) {
    $enc_rsa_priv_key = base64_to_a32($res->privk);
    $rsa_priv_key = decrypt_key($enc_rsa_priv_key, $master_key);
 
    $privk = a32_to_str($rsa_priv_key);
    $rsa_priv_key = array(0, 0, 0, 0);
 
    for ($i = 0; $i < 4; $i++) {
      $l = ((ord($privk[0]) * 256 + ord($privk[1]) + 7) / 8) + 2;
      $rsa_priv_key[$i] = mpi2bc(substr($privk, 0, $l));
      $privk = substr($privk, $l);
    }
 
    $enc_sid = mpi2bc(base64urldecode($res->csid));
    $sid = rsa_decrypt($enc_sid, $rsa_priv_key[0], $rsa_priv_key[1], $rsa_priv_key[2]);
    $sid = base64urlencode(substr(strrev($sid), 0, 43));
  }
}
 
function enc_attr($attr, $key) {
  $attr = 'MEGA' . json_encode($attr);
  return aes_cbc_encrypt($attr, a32_to_str($key));
}
 
function dec_attr($attr, $key) {
  $attr = trim(aes_cbc_decrypt($attr, a32_to_str($key)));
  if (substr($attr, 0, 6) != 'MEGA{"') {
    return false;
  }
  return json_decode(substr($attr, 4));
}
 
function get_chunks($size) {
  $chunks = array();
  $p = $pp = 0;
 
  for ($i = 1; $i <= 8 && $p < $size - $i * 0x20000; $i++) {
    $chunks[$p] = $i * 0x20000;
    $pp = $p;
    $p += $chunks[$p];
  }
 
  while ($p < $size) {
    $chunks[$p] = 0x100000;
    $pp = $p;
    $p += $chunks[$p];
  }
 
  $chunks[$pp] = ($size - $pp);
  if (!$chunks[$pp]) {
    unset($chunks[$pp]);
  }
 
  return $chunks;
}
 
function cbc_mac($data, $k, $n) {
  $padding_size = (strlen($data) % 16) == 0 ? 0 : 16 - strlen($data) % 16;
  $data .= str_repeat("\0", $padding_size);
 
  $chunks = get_chunks(strlen($data));
  $file_mac = array(0, 0, 0, 0);
 
  foreach ($chunks as $pos => $size) {
    $chunk_mac = array($n[0], $n[1], $n[0], $n[1]);
    for ($i = $pos; $i < $pos + $size; $i += 16) {
      $block = str_to_a32(substr($data, $i, 16));
      $chunk_mac = array($chunk_mac[0] ^ $block[0], $chunk_mac[1] ^ $block[1], $chunk_mac[2] ^ $block[2], $chunk_mac[3] ^ $block[3]);
      $chunk_mac = aes_cbc_encrypt_a32($chunk_mac, $k);
    }
    $file_mac = array($file_mac[0] ^ $chunk_mac[0], $file_mac[1] ^ $chunk_mac[1], $file_mac[2] ^ $chunk_mac[2], $file_mac[3] ^ $chunk_mac[3]);
    $file_mac = aes_cbc_encrypt_a32($file_mac, $k);
  }
 
  return $file_mac;
}
 
function uploadfile($filename) {
  global $master_key, $root_id;
 
  $data = file_get_contents($filename);
  $size = strlen($data);
  $ul_url = api_req(array('a' => 'u', 's' => $size));
  $ul_url = $ul_url->p;
 
  $ul_key = array(0, 1, 2, 3, 4, 5);
  for ($i = 0; $i < 6; $i++) {
    $ul_key[$i] = rand(0, 0xFFFFFFFF);
  }
 
  $data_crypted = aes_ctr_encrypt($data, a32_to_str(array_slice($ul_key, 0, 4)), a32_to_str(array($ul_key[4], $ul_key[5], 0, 0)));
  $completion_handle = post($ul_url, $data_crypted);
 
  $data_mac = cbc_mac($data, array_slice($ul_key, 0, 4), array_slice($ul_key, 4, 2));
  $meta_mac = array($data_mac[0] ^ $data_mac[1], $data_mac[2] ^ $data_mac[3]);
  $attributes = array('n' => basename($filename));
  $enc_attributes = enc_attr($attributes, array_slice($ul_key, 0, 4));
  $key = array($ul_key[0] ^ $ul_key[4], $ul_key[1] ^ $ul_key[5], $ul_key[2] ^ $meta_mac[0], $ul_key[3] ^ $meta_mac[1], $ul_key[4], $ul_key[5], $meta_mac[0], $meta_mac[1]);
  return api_req(array('a' => 'p', 't' => $root_id, 'n' => array(array('h' => $completion_handle, 't' => 0, 'a' => base64urlencode($enc_attributes), 'k' => a32_to_base64(encrypt_key($key, $master_key))))));
}
 
function downloadfile($file, $attributes, $k, $iv, $meta_mac) {
  $dl_url = api_req(array('a' => 'g', 'g' => 1, 'n' => $file->h));
 
  $data_enc = file_get_contents($dl_url->g);
  $data = aes_ctr_decrypt($data_enc, a32_to_str($k), a32_to_str($iv));
  file_put_contents($attributes->n, $data);
 
  $file_mac = cbc_mac($data, $k, $iv);
  if (array($file_mac[0] ^ $file_mac[1], $file_mac[2] ^ $file_mac[3]) != $meta_mac) {
    echo "MAC mismatch";
  }
}
 
function getfiles() {
  global $master_key, $root_id, $inbox_id, $trashbin_id;
 
  $files = api_req(array('a' => 'f', 'c' => 1));
  foreach ($files->f as $file) {
    if ($file->t == 0 || $file->t == 1) {
      $key = substr($file->k, strpos($file->k, ':') + 1);
      $key = decrypt_key(base64_to_a32($key), $master_key);
      if ($file->t == 0) {
        $k = array($key[0] ^ $key[4], $key[1] ^ $key[5], $key[2] ^ $key[6], $key[3] ^ $key[7]);
        $iv = array_merge(array_slice($key, 4, 2), array(0, 0));
        $meta_mac = array_slice($key, 6, 2);
      } else {
        $k = $key;
      }
      $attributes = base64urldecode($file->a);
      $attributes = dec_attr($attributes, $k);
      if ($file->h == 'gldU3Tab') {
        downloadfile($file, $attributes, $k, $iv, $meta_mac);
      }
    } else if ($file->t == 2) {
      $root_id = $file->k;
    } else if ($file->t == 3) {
      $inbox_id = $file->k;
    } else if ($file->t == 4) {
      $trashbin_id = $file->k;
    }
  }
}

38 thoughts on “Using the Mega API, with PHP examples!

  1. Steven

    Thanks sooo much for the php version!
    Would it be possible if you can convert the anonymous upload into a php function here real quick?
    THANK YOU!!!

    Reply
    1. Steven

      I got this far, don’t know how to do the rest, not great with PHP.

      function login_anon() {
      global $sid, $master_key;
      $master_key = rand(0, 0xFFFFFFFF);
      $password_aes = rand(0, 0xFFFFFFFF);
      $session_self_challenge = rand(0, 0xFFFFFFFF);

      $uh = api_req(array('a' => 'up', 'k' => a32_to_base64(encrypt_key(master_key, password_aes)), 'ts' => base64urlencode(a32_to_str(session_self_challenge) + a32_to_str(encrypt_key(session_self_challenge, master_key))) ));

      echo "ephemeral user handle: $uh";
      $res = api_req(array('a' => 'us', 'user' => $uh));

      $enc_master_key = base64_to_a32($res->k);
      $master_key = decrypt_key(enc_master_key, password_aes);
      }
      }

      Reply
    2. Julien Marchand Post author

      Hey! Sorry for the late answer. I’m going to post the code for anonymous uploads & downloads in PHP very soon :)

      Reply
  2. Pablo M.

    Hi, Julien. First of all thanks a lot for share this with us.

    I am having troubles to login. As i can see the hash derived from my pass and e-mail differs from the one i see when debuggin in Chrome logging in directly into MEGA. I am developing in a Windows machines, is there any chance this is the problem? (my english is not good, sorry for that)

    Reply
    1. Pablo M.

      Julien, i found that the str_to_a32 function is given me always one element less than the same code in javascript:

      Example: string “mail@gmail.com”
      In JS i got this: [0]:1835100524 [1]:1080520033 [2]:1768697443 [3]:1869414400
      But in php i got this: Array ( [0] => 1835100524 [1] => 1080520033 [2] => 1768697443 )

      I am trying to find out what’s going on, but if you have any clue, i would appreciate.

      Regards,
      Pablo M.

      Reply
        1. Julien Marchand Post author

          It’s fixed now, thanks for reporting this and sorry for the inconvenience! The str_to_a32 function was broken for strings with a length not multiple of 4.

          Reply
    1. Julien Marchand Post author

      You’ll find an example in the complete code at the end of the article, in the getfiles method. The arguments are all the informations about the file (ID, attributes, encryption key, initialization vector for AES-CBC and meta-MAC) that you get in getfiles by calling the API f method.

      Reply
  3. skywink

    Merci pour ces codes,
    Mais j’aimerai savoir, comment est-ce que le nombre 0x93C467E3 qui est de la taille d’un Long peut être encodé sur 4 bits grace à la fonction a32_to_str. En tout cas, c’est le problème que j’ai en passant par Java.
    Si quelqu’un peut me donner sa réponse à la conversion a32_to_str($pkey) je veux bien l’avoir parce que c’est la seule fonction qui fait planter mon programme.

    Reply
    1. Julien Marchand Post author

      Je ne comprends pas vraiment ta question… a32_to_str transforme un tableau d’entiers 32 bits en une chaîne d’octets (byte string). Tu peux faire cela en concaténant simplement tous les octets qui composent les entiers de ton tableau (ordre big endian). Si tu codes en java, regarde du côté de ByteBuffer.

      Reply
  4. Pablo M.

    Julien, don’t know if you are reading. Slowly i am reaching some solution.

    Everything seems to work if the mod by 4 of lenght of the string passed to the str_to_a32 functions is zero. I don’t know if i did expressed ok, examples:

    If “mail” wich lenght is 4-> works in php the same that in js
    If “mailmail” wich lenght is 8-> works in php the same that in js
    If “mailmail@” wich lenght is 9-> DO NOT works because one element of the array is missing.

    Trying to solve this.

    Regards.

    Reply
  5. Pablo M.

    Julien, i rewrote this, now it seems to work fine with all lenghts in the php version.

    function str_to_a32($b){
    for ($i = 0; $i >2] |= ord(substr($b,$i,1)) << (24-($i & 3)*8) ;
    }
    return $a;
    }

    Reply
    1. Julien Marchand Post author

      Hey! Thanks for reporting this, and sorry for the late answer. Both my email address and password have a length multiple of 4, so I didn’t notice during my tests… I fixed the code to add padding before calling unpack :)

      Reply
  6. HALGAND

    Julien, thanks a lot for this PHP version!

    I’m exploring the “downloadfile($file, $attributes, $k, $iv, $meta_mac)” and it works fine to get the temporary url to get file content.

    But to decrypt it with “aes_ctr_decrypt” I need the $key and the $iv parameters.
    Acutally, I think to have these parameters as array using the following function :

    function getLinkInfos($file_hash, $file_key)
    {
    $sequence_number = mt_rand(1, 99999999999);
    $ch = curl_init(‘https://g.api.mega.co.nz/cs?id=’.$sequence_number);

    $data = array(array(‘a’ => ‘g’, ‘g’=>1, ‘p’ => $file_hash));
    $data_string = json_encode($data);

    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
    curl_setopt($ch, CURLOPT_HTTPHEADER, array(‘Content-Type: application/json’));
    curl_setopt($ch, CURLOPT_VERBOSE, 1);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, $data_string);

    $output = curl_exec($ch);
    $res = json_decode($output, true);

    if (!isset($res[0]['s']) || !isset($res[0]['at'])) {
    return false;
    } else {
    $key = base64_to_a32($file_key);
    $k = array($key[0] ^ $key[4], $key[1] ^ $key[5], $key[2] ^ $key[6], $key[3] ^ $key[7]);
    $iv = array_merge(array_slice($key, 4, 2), array(0, 0));
    $meta_mac = array_slice($key, 6, 2);
    $enc_attributes = base64urldecode($res[0]['at']);
    $attributes = dec_attr($enc_attributes, $k);

    return array(‘url’ => $res[0]['g'], ‘size’ => $res[0]['s'], ‘name’ => $attributes->n, ‘key’=>$key, ‘iv’=>$iv, ‘mac’=>$meta_mac);
    }
    }
    ============================================================
    ‘key’ =>
    array
    0 => int1
    1 => int2
    2 => int3
    3 => int4
    4 => int5
    5 => int6
    6 => int7
    7 => int8
    ‘iv’ =>
    array
    0 => int1
    1 => int2
    2 => int 0
    3 => int 0
    ‘mac’ =>
    array
    0 => int1
    1 => int2

    If I understand the “aes_ctr_decrypt” function needs strings for $key and $iv.
    How can I get them?

    In advance thanks for your response.

    Best regards,
    Pierrick

    Reply
  7. shinchiro

    I really want to learn Mega API from you but I don’t know php and python, only in c#. Can you write the example in c# codes too? :)

    Reply
    1. Julien Marchand Post author

      I don’t know if I’ll write examples in C#, but maybe in Java, which should be closer to C# than Python and PHP :-)

      Reply
  8. Pablo M.

    Julien, had you tested againsta the API itself? I am not receiving any answer from de login and everything seems to be ok. Regards.

    Reply
    1. Julien Marchand Post author

      Yeap, I ran the code copied and pasted at the end of the article (with a call to login(‘my email’, ‘my password’), getfiles() and uploadfile()), and everything works fine!

      Can you dump the response of the API in api_req, just before the call to json_decode?

      Reply
    1. Julien Marchand Post author

      Heya :) There was a bug in the str_to_a32 function that prevented you to authenticate if your email or password didn’t have a length multiple of 4. That’s fixed now, so try again with the new version of the code!

      Reply
  9. antmanx3

    Hi
    can you help me??
    if i use this script, it will not slow progress and file large than 500MB it will suck

    $data_enc = file_get_contents($dl_url->g);
    $data = aes_ctr_decrypt($data_enc, a32_to_str($k), a32_to_str($iv));
    file_put_contents($attributes->n, $data);

    can you help me make a function decrypt part of file??

    i try this script but it not working
    $file_input = “antmanx3.rar.encrypt”;
    $file_output = “antmanx3.rar”;
    $key = “DOho4y7GzVGi0IFaXvd_EQ5WxCUSCY9V_ZOoKHfok54″;
    $key = $Ant_megaco -> base64_to_a32($key);
    $k = array($key[0] ^ $key[4], $key[1] ^ $key[5], $key[2] ^ $key[6], $key[3] ^ $key[7]);
    $iv = array_merge(array_slice($key, 4, 2), array(0, 0));

    $chunkSizes = $Ant_megaco -> get_chunks(filesize($file_input));
    $f_input = fopen($file_input, “r”);
    $f_output = fopen($file_output, “a”);
    foreach($chunkSizes as $chuck_start => $check_size)
    {
    fseek($f_input, $chuck_start);
    $buffer = fread($f_input, $check_size);
    $buffer_output = aes_ctr_decrypt($buffer, a32_to_str($k), a32_to_str($iv));
    $fwrite = fwrite($f_output, $buffer_output);
    if ($fwrite === false) {
    die(“Decoding error”);
    }
    else
    echo @round(($chuck_start + $check_size)*100/$file ["size"],2).”%”;
    }
    fclose($f_input);
    fclose($f_output);

    Thank you so much

    Reply
    1. Julien Marchand Post author

      The current download strategy has the advantage of being very simple to implement and requires only a few lines of code, but the disadvantage of loading the whole file into memory. That’s not very scalable. I’ll post another solution soon (downloading chunk by chunk and having only one chunk at a time in memory).

      Reply
      1. Th3-822

        I have written a download plugin for rapidleech thanks to your explanation and functions (i added a link to this post in the code), i did had to write an stream filter for decrypting in chunks the file.
        It doesn’t check the CBC-MAC, but still works. :D

        I’m not speak english very well, so this may be hard to read, sorry about that.

        Thanks for all the great post.

        Reply
      2. dan

        Hello!
        the code to download chunk by chunk it’s “almost” this one:

        $r = fopen( $dl_url , "r");
        $w = fopen("test.mp4" , "w");

        foreach( $chunks as $start => $end )
        {
        $enc_data = stream_get_contents($r, $end);
        $dec_data = aes_ctr_decrypt($enc_data, a32_to_str($k), a32_to_str($iv));
        fwrite($w, $dec_data);
        }

        fclose($w);
        fclose($r);

        The problem I think it is on the line that decrypts the text… In python I see that you use a counter. Can you tell me what I’m doing wrong?

        Reply
  10. Lionel

    Super intéressant ton script par contre je ne sais absolument pas comment l’utiliser.

    En fait j’aimerai pouvoir lancer le téléchargement d’un fichier depuis une page de mon site en récupérant l’url MEGA depuis ma base de donnée.

    J’ai donc un $lienmega = “https://mega.co.nz/!fjgjgjgjjgjgjaaaaaaaaaaaaaaaaa”;

    Comment ensuite lancer le téléchargement depuis ma page PHP ?
    Je n’y connais rien en php pour ainsi dire…

    Par avance merci

    PS: pour lancer le téléchargement il faut obligatoirement spécifié un identifiant et mot de passe ou bien on peut faire un download anonyme ?

    Reply
  11. Christian Georg

    Hello Julien,
    thank you very much for your work with mega api. Good luck for the future. I’m following your blog.

    Reply
  12. devlop

    Thank you for this script, question ?

    I have successfuly get the url where the file is stored but i don’t how get decrypted file ??

    thank you for help.

    Reply
    1. tomber

      That’s correct, you will have to decrypt the file yourself. You can do this after you have retrieved the full file or chunk by chunk.

      Reply
  13. jose

    Hello, thanks for the script and very nice work with that
    I’ve a question, my english is very bad so i will be very direct

    Im not very good with php but i’m trying haha.
    i tried use api but nothing works always the result is blank. I use your final code but no works too.
    I just wanted to know if i’m doing something wrong, using your code (conclusion code) and add in final code:
    login(‘my email’, ‘my password’); returns: Notice: Trying to get property of non-object in C:\WEB2\www\mega\all.php on line 224

    You can help me?
    Sorry for my english.
    I’m using windows 7 and easy-php, i dont know if this change something, anyway thank you.

    Reply
  14. BB

    Hi Julien,
    I think there is a problem with the “prepare_key ()” is takes several seconds (~ 0.45) to complete, is normal?

    Reply
  15. gregor

    hey,

    do you think you could change the upload function to also upload files chunk-wise?
    otherwise we are limited to the memory_limit of php ;(

    cheers and thanks for all the hard work!

    Reply
  16. Seid

    Hi,
    i would like to ask where I have problem. I copied whole code from conclusion, added call for function login with my email and password. I tried to debugged it in netbeans. Because i want see the results of the functions. But in:
    login function -> api_req -> post -> $resp = curl_exec($ch); result is false. I dont know why. I tried to find where i made error for two hours. Can someone help me please?

    Sorry for my bad english.
    Really thank you for you help.

    Reply
    1. Seid

      Problem was that I tried to run it on my localhost (xampp) and i had some php settings that refuse connection to https. I had to add this line before curl_exec ..

      curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);

      and now its working. But can someone help me, when I trying to communicate with Mega through java client, i get error response -2. I really dont know where i made mistake. I read how to send jpos parameters and I sent right values and I still got error -2 in response.

      Thank you

      Reply

Leave a Reply to Th3-822 Cancel reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>