use base64::Engine as _;
use aes::Aes128;
use aes::cipher::{Array, BlockCipherDecrypt, BlockCipherEncrypt, KeyInit};
use crate::fetch::{download_to_file, FetchResult};
use std::path::Path;
pub fn looks_like_mega(url: &str) -> bool {
let l = url.to_lowercase();
l.contains("mega.nz") || l.contains("mega.co.nz")
}
pub fn mega_b64decode(s: &str) -> Option<Vec<u8>> {
let t: String = s.chars().map(|c| match c { '+' => '-', '/' => '_', _ => c }).collect();
let pad = (4 - t.len() % 4) % 4;
let padded = format!("{}{}", t, "=".repeat(pad));
base64::engine::general_purpose::URL_SAFE.decode(padded.as_bytes()).ok()
}
pub enum MegaKind { File, Folder }
pub struct MegaLink {
pub kind: MegaKind,
pub id: String,
pub key: Vec<u8>,
}
pub fn parse_mega_url(url: &str) -> Option<MegaLink> {
let (frag_id, frag_key, kind) = if let Some((_, rest)) = url.split_once("/folder/") {
let (id, key) = rest.split_once('#')?;
(id.to_string(), key.to_string(), MegaKind::Folder)
} else if let Some((_, rest)) = url.split_once("/file/") {
let (id, key) = rest.split_once('#')?;
(id.to_string(), key.to_string(), MegaKind::File)
} else if let Some((_, rest)) = url.split_once("#F!") {
let (id, key) = rest.split_once('!')?;
(id.to_string(), key.to_string(), MegaKind::Folder)
} else if let Some((_, rest)) = url.split_once("#!") {
let (id, key) = rest.split_once('!')?;
(id.to_string(), key.to_string(), MegaKind::File)
} else {
return None;
};
let id = frag_id.split(['/', '?']).next().unwrap_or(&frag_id).to_string();
let key = mega_b64decode(frag_key.trim())?;
let ok_len = matches!(kind, MegaKind::File) && key.len() == 32
|| matches!(kind, MegaKind::Folder) && key.len() == 16;
if id.is_empty() || !ok_len {
return None;
}
Some(MegaLink { kind, id, key })
}
fn ecb_decrypt(key: &[u8; 16], data: &[u8]) -> Vec<u8> {
let cipher = Aes128::new(&Array::from(*key));
let mut out = Vec::with_capacity(data.len());
for chunk in data.chunks(16) {
if chunk.len() < 16 { break; }
let mut b = Array::from(<[u8; 16]>::try_from(chunk).unwrap());
cipher.decrypt_block(&mut b);
out.extend_from_slice(&b);
}
out
}
fn cbc_decrypt_zero(key: &[u8; 16], data: &[u8]) -> Vec<u8> {
let cipher = Aes128::new(&Array::from(*key));
let mut out = Vec::with_capacity(data.len());
let mut prev = [0u8; 16];
for chunk in data.chunks(16) {
if chunk.len() < 16 { break; }
let mut b = Array::from(<[u8; 16]>::try_from(chunk).unwrap());
cipher.decrypt_block(&mut b);
for i in 0..16 { out.push(b[i] ^ prev[i]); }
prev.copy_from_slice(chunk);
}
out
}
fn ctr_xor(key: &[u8; 16], iv: &[u8; 16], data: &mut [u8]) {
let cipher = Aes128::new(&Array::from(*key));
let mut counter = *iv;
for chunk in data.chunks_mut(16) {
let mut ks = Array::from(counter);
cipher.encrypt_block(&mut ks);
for (b, k) in chunk.iter_mut().zip(ks.iter()) { *b ^= *k; }
for i in (0..16).rev() {
counter[i] = counter[i].wrapping_add(1);
if counter[i] != 0 { break; }
}
}
}
fn derive_file_key(key32: &[u8]) -> Option<([u8; 16], [u8; 16])> {
if key32.len() != 32 { return None; }
let mut aes_key = [0u8; 16];
for b in 0..16 { aes_key[b] = key32[b] ^ key32[b + 16]; }
let mut iv = [0u8; 16];
iv[0..8].copy_from_slice(&key32[16..24]);
Some((aes_key, iv))
}
fn mega_api_base(url: &str) -> String {
let lower = url.to_lowercase();
if lower.contains("mega.nz") || lower.contains("mega.co.nz") {
return "https://g.api.mega.co.nz".to_string();
}
let (scheme, rest) = url.split_once("://").unwrap_or(("https", url));
let host = rest.split('/').next().unwrap_or("");
format!("{scheme}://{host}")
}
async fn post_cs(
client: &reqwest::Client,
api_base: &str,
query: &str,
payload: serde_json::Value,
) -> std::io::Result<serde_json::Value> {
let url = format!("{api_base}/cs?id=0{query}");
let body = serde_json::to_string(&payload).unwrap();
let text = client.post(&url).header("content-type", "application/json").body(body)
.send().await.and_then(|r| r.error_for_status())
.map_err(|e| std::io::Error::other(format!("mega api failed: {e}")))?
.text().await.map_err(|e| std::io::Error::other(format!("mega api read: {e}")))?;
let v: serde_json::Value = serde_json::from_str(&text)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
v.as_array().and_then(|a| a.first().cloned())
.ok_or_else(|| std::io::Error::new(std::io::ErrorKind::InvalidData, "empty mega api response"))
}
fn sanitize_name(name: &str) -> String {
let base = name.rsplit(['/', '\\']).next().unwrap_or(name);
if base.is_empty() || base == "." || base == ".." { "download".to_string() } else { base.to_string() }
}
fn decode_attrs(node_key: &[u8; 16], at_b64: &str) -> Option<String> {
let ct = mega_b64decode(at_b64)?;
let pt = cbc_decrypt_zero(node_key, &ct);
let end = pt.iter().rposition(|&b| b != 0).map(|i| i + 1).unwrap_or(0);
let pt = &pt[..end];
let s = pt.strip_prefix(b"MEGA")?;
let v: serde_json::Value = serde_json::from_slice(s).ok()?;
v.get("n").and_then(|n| n.as_str()).map(|s| s.to_string())
}
fn decrypt_file(enc: &Path, out: &Path, aes_key: &[u8; 16], iv: &[u8; 16]) -> std::io::Result<u64> {
let mut data = std::fs::read(enc)?;
ctr_xor(aes_key, iv, &mut data);
std::fs::write(out, &data)?;
Ok(data.len() as u64)
}
pub async fn fetch_mega_folder(
client: &reqwest::Client,
url: &str,
link: &MegaLink,
dest: &Path,
max_bytes: u64,
) -> std::io::Result<FetchResult> {
let mut master = [0u8; 16];
if link.key.len() != 16 {
return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "bad mega folder key"));
}
master.copy_from_slice(&link.key);
let api = mega_api_base(url);
let nq = format!("&n={}", link.id);
let listing = post_cs(client, &api, &nq, serde_json::json!([{"a":"f","c":1,"r":1}])).await?;
let nodes = listing.get("f").and_then(|f| f.as_array()).cloned().unwrap_or_default();
let mut names: std::collections::HashMap<String, String> = std::collections::HashMap::new();
let mut parents: std::collections::HashMap<String, String> = std::collections::HashMap::new();
struct FileNode { handle: String, name: String, aes_key: [u8; 16], iv: [u8; 16] }
let mut files: Vec<FileNode> = Vec::new();
for node in &nodes {
let handle = node.get("h").and_then(|h| h.as_str()).unwrap_or("").to_string();
let parent = node.get("p").and_then(|p| p.as_str()).unwrap_or("").to_string();
let t = node.get("t").and_then(|t| t.as_i64()).unwrap_or(-1);
let kfield = node.get("k").and_then(|k| k.as_str()).unwrap_or("");
let wrapped_b64 = kfield.rsplit(':').next().unwrap_or("");
let Some(wrapped) = mega_b64decode(wrapped_b64) else { continue };
let unwrapped = ecb_decrypt(&master, &wrapped);
parents.insert(handle.clone(), parent);
if t == 0 && unwrapped.len() >= 32 {
if let Some((aes_key, iv)) = derive_file_key(&unwrapped[..32]) {
let name = node.get("a").and_then(|a| a.as_str())
.and_then(|at| decode_attrs(&aes_key, at))
.map(|n| sanitize_name(&n)).unwrap_or_else(|| sanitize_name(&handle));
files.push(FileNode { handle: handle.clone(), name, aes_key, iv });
}
} else if t == 1 {
let mut nk = [0u8; 16];
if unwrapped.len() >= 16 {
nk.copy_from_slice(&unwrapped[..16]);
let name = node.get("a").and_then(|a| a.as_str())
.and_then(|at| decode_attrs(&nk, at))
.map(|n| sanitize_name(&n)).unwrap_or_else(|| sanitize_name(&handle));
names.insert(handle.clone(), name);
}
}
}
let rel_dir = |start: &str| -> std::path::PathBuf {
let mut segs: Vec<String> = Vec::new();
let mut cur = start.to_string();
let mut guard = 0;
while let Some(name) = names.get(&cur) {
segs.push(name.clone());
match parents.get(&cur) { Some(p) => cur = p.clone(), None => break }
guard += 1; if guard > 64 { break; }
}
segs.reverse();
segs.iter().fold(std::path::PathBuf::new(), |acc, s| acc.join(s))
};
std::fs::create_dir_all(dest)?;
let mut total_files = 0; let mut total_bytes = 0u64;
for f in files {
let parent = parents.get(&f.handle).cloned().unwrap_or_default();
let rel = rel_dir(&parent).join(&f.name);
let out = dest.join(&rel);
if let Some(p) = out.parent() { std::fs::create_dir_all(p)?; }
let resp = match post_cs(client, &api, &nq, serde_json::json!([{"a":"g","g":1,"n": f.handle}])).await {
Ok(r) => r, Err(e) => { eprintln!("kibble: mega node {} failed: {e}", f.handle); continue }
};
let Some(dl) = resp.get("g").and_then(|g| g.as_str()) else { continue };
let enc = out.with_extension("enc");
if download_to_file(client, dl, &enc, max_bytes).await.is_err() { continue; }
match decrypt_file(&enc, &out, &f.aes_key, &f.iv) {
Ok(n) => { total_files += 1; total_bytes += n; }
Err(e) => eprintln!("kibble: decrypt {} failed: {e}", f.name),
}
let _ = std::fs::remove_file(&enc);
}
Ok(FetchResult { handler: "mega".to_string(), files: total_files, bytes: total_bytes })
}
pub async fn fetch_mega(
client: &reqwest::Client,
url: &str,
dest: &Path,
max_bytes: u64,
) -> std::io::Result<FetchResult> {
let link = parse_mega_url(url)
.ok_or_else(|| std::io::Error::new(std::io::ErrorKind::InvalidInput, format!("unparseable mega url: {url}")))?;
match link.kind {
MegaKind::File => fetch_mega_file(client, url, &link, dest, max_bytes).await,
MegaKind::Folder => fetch_mega_folder(client, url, &link, dest, max_bytes).await,
}
}
pub async fn fetch_mega_file(
client: &reqwest::Client,
url: &str,
link: &MegaLink,
dest: &Path,
max_bytes: u64,
) -> std::io::Result<FetchResult> {
let key32: [u8; 32] = link.key.as_slice().try_into()
.map_err(|_| std::io::Error::new(std::io::ErrorKind::InvalidData, "bad mega file key length"))?;
let (aes_key, iv) = derive_file_key(&key32)
.ok_or_else(|| std::io::Error::new(std::io::ErrorKind::InvalidData, "bad mega file key"))?;
let api = mega_api_base(url);
let resp = post_cs(client, &api, "", serde_json::json!([{"a":"g","g":1,"p": link.id}])).await?;
let dl = resp.get("g").and_then(|g| g.as_str())
.ok_or_else(|| std::io::Error::new(std::io::ErrorKind::InvalidData, "no mega download url"))?
.to_string();
let name = resp.get("at").and_then(|a| a.as_str())
.and_then(|at| decode_attrs(&aes_key, at))
.map(|n| sanitize_name(&n))
.unwrap_or_else(|| sanitize_name(&link.id));
std::fs::create_dir_all(dest)?;
let enc = dest.join(format!("{name}.enc"));
download_to_file(client, &dl, &enc, max_bytes).await?;
let out = dest.join(&name);
let bytes = decrypt_file(&enc, &out, &aes_key, &iv)?;
let _ = std::fs::remove_file(&enc);
Ok(FetchResult { handler: "mega".to_string(), files: 1, bytes })
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn b64decode_urlsafe_nopad() {
assert_eq!(mega_b64decode("AAAA"), Some(vec![0, 0, 0]));
assert!(mega_b64decode("AA").is_some()); }
#[test]
fn b64decode_nonmultiple_of_four() {
let original: Vec<u8> = (1u8..=17).collect();
let encoded = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(&original);
let decoded = mega_b64decode(&encoded).expect("should decode");
assert_eq!(decoded, original);
}
#[test]
fn parse_file_links_both_formats() {
let key32 = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode([7u8; 32]);
let new = format!("https://mega.nz/file/ABCID#{key32}");
let l = parse_mega_url(&new).unwrap();
assert!(matches!(l.kind, MegaKind::File));
assert_eq!(l.id, "ABCID");
assert_eq!(l.key.len(), 32);
let legacy = format!("https://mega.nz/#!ABCID!{key32}");
let l2 = parse_mega_url(&legacy).unwrap();
assert_eq!(l2.id, "ABCID");
assert_eq!(l2.key.len(), 32);
}
#[test]
fn parse_folder_links_both_formats() {
let key16 = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode([3u8; 16]);
let new = format!("https://mega.nz/folder/FID#{key16}");
let l = parse_mega_url(&new).unwrap();
assert!(matches!(l.kind, MegaKind::Folder));
assert_eq!(l.id, "FID");
assert_eq!(l.key.len(), 16);
let legacy = format!("https://mega.nz/#F!FID!{key16}");
assert!(matches!(parse_mega_url(&legacy).unwrap().kind, MegaKind::Folder));
}
#[test]
fn detects_mega_hosts() {
assert!(looks_like_mega("https://mega.nz/file/x#y"));
assert!(looks_like_mega("https://mega.co.nz/file/x#y"));
assert!(!looks_like_mega("https://example.com/x"));
}
use aes::Aes128;
use aes::cipher::{Array, BlockCipherEncrypt, KeyInit};
fn ecb_encrypt(key: &[u8; 16], data: &[u8]) -> Vec<u8> {
let cipher = Aes128::new(&Array::from(*key));
let mut out = Vec::new();
for chunk in data.chunks(16) {
let mut b = Array::from(<[u8; 16]>::try_from(chunk).unwrap());
cipher.encrypt_block(&mut b);
out.extend_from_slice(&b);
}
out
}
#[test]
fn ecb_roundtrip() {
let key = [9u8; 16];
let pt = [1u8; 32];
let ct = ecb_encrypt(&key, &pt);
assert_eq!(ecb_decrypt(&key, &ct), pt.to_vec());
}
#[test]
fn ctr_is_symmetric() {
let key = [5u8; 16];
let iv = [0u8; 16];
let mut buf = b"hello mega world, longer than one block!!".to_vec();
let orig = buf.clone();
ctr_xor(&key, &iv, &mut buf); assert_ne!(buf, orig);
ctr_xor(&key, &iv, &mut buf); assert_eq!(buf, orig);
}
#[test]
fn cbc_decrypt_matches_manual_encrypt() {
let key = [2u8; 16];
let pt = [7u8; 32];
let cipher = Aes128::new(&Array::from(key));
let mut ct = Vec::new();
let mut prev = [0u8; 16];
for chunk in pt.chunks(16) {
let mut x = [0u8; 16];
for i in 0..16 { x[i] = chunk[i] ^ prev[i]; }
let mut b = Array::from(x);
cipher.encrypt_block(&mut b);
ct.extend_from_slice(&b);
prev.copy_from_slice(&b);
}
assert_eq!(cbc_decrypt_zero(&key, &ct), pt.to_vec());
}
#[tokio::test]
async fn mega_folder_downloads_tree() {
use std::io::{Read, Write};
use std::net::TcpListener;
let master = [4u8; 16];
let fkey: [u8; 32] = std::array::from_fn(|i| (i as u8).wrapping_mul(3).wrapping_add(2));
let (faes, fiv) = derive_file_key(&fkey).unwrap();
let wrapped = ecb_encrypt(&master, &fkey); let wrapped_b64 = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(&wrapped);
let mut attr = b"MEGA{\"n\":\"inside.txt\"}".to_vec();
while !attr.len().is_multiple_of(16) { attr.push(0); }
let at_ct = {
let cipher = Aes128::new(&Array::from(faes));
let mut out = Vec::new(); let mut prev = [0u8; 16];
for chunk in attr.chunks(16) {
let mut x = [0u8; 16]; for i in 0..16 { x[i] = chunk[i] ^ prev[i]; }
let mut b = Array::from(x);
cipher.encrypt_block(&mut b); out.extend_from_slice(&b); prev.copy_from_slice(&b);
}
out
};
let at_b64 = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(&at_ct);
let plaintext = b"folder file body content here, two blocks!!".to_vec();
let mut ct = plaintext.clone(); ctr_xor(&faes, &fiv, &mut ct);
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let port = listener.local_addr().unwrap().port();
let (atc, wc, ctc) = (at_b64.clone(), wrapped_b64.clone(), ct.clone());
std::thread::spawn(move || {
for _ in 0..3 {
if let Ok((mut s, _)) = listener.accept() {
let mut b = [0u8; 2048]; let n = s.read(&mut b).unwrap_or(0);
let req = String::from_utf8_lossy(&b[..n]);
let body: Vec<u8> = if req.contains("\"a\":\"f\"") || req.contains("a=f") || (req.contains("/cs") && !req.contains("\"a\":\"g\"")) {
format!("[{{\"f\":[{{\"h\":\"h1\",\"p\":\"root\",\"t\":0,\"a\":\"{atc}\",\"k\":\"owner:{wc}\"}}]}}]").into_bytes()
} else if req.contains("/cs") {
format!("[{{\"g\":\"http://127.0.0.1:{port}/dl\",\"s\":{}}}]", ctc.len()).into_bytes()
} else {
ctc.clone()
};
let head = format!("HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n", body.len());
let _ = s.write_all(head.as_bytes()); let _ = s.write_all(&body);
}
}
});
let client = crate::net::build_client(None).unwrap();
let dir = std::env::temp_dir().join(format!("kibble_megaf_{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
let key_b64 = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(master);
let url = format!("http://127.0.0.1:{port}/folder/FOLDID#{key_b64}");
let link = parse_mega_url(&url).unwrap();
let r = fetch_mega_folder(&client, &url, &link, &dir, 100000).await.unwrap();
assert_eq!(r.handler, "mega");
assert!(r.files >= 1);
assert_eq!(std::fs::read(dir.join("inside.txt")).unwrap(), plaintext);
}
#[tokio::test]
async fn mega_file_downloads_and_decrypts() {
use std::io::{Read, Write};
use std::net::TcpListener;
let mut key32 = [0u8; 32];
for (i, b) in key32.iter_mut().enumerate() { *b = (i as u8).wrapping_mul(7).wrapping_add(1); }
let (aes_key, iv) = derive_file_key(&key32).unwrap();
let plaintext = b"the decrypted mega file body, multiple blocks long here!!".to_vec();
let mut ciphertext = plaintext.clone();
ctr_xor(&aes_key, &iv, &mut ciphertext);
let mut attr = b"MEGA{\"n\":\"secret.txt\"}".to_vec();
while !attr.len().is_multiple_of(16) { attr.push(0); }
let at_ct = {
let cipher = Aes128::new(&Array::from(aes_key));
let mut out = Vec::new();
let mut prev = [0u8; 16];
for chunk in attr.chunks(16) {
let mut x = [0u8; 16];
for i in 0..16 { x[i] = chunk[i] ^ prev[i]; }
let mut b = Array::from(x);
cipher.encrypt_block(&mut b);
out.extend_from_slice(&b);
prev.copy_from_slice(&b);
}
out
};
let at_b64 = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(&at_ct);
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let port = listener.local_addr().unwrap().port();
let ct_for_thread = ciphertext.clone();
let at_for_thread = at_b64.clone();
std::thread::spawn(move || {
for _ in 0..2 {
if let Ok((mut s, _)) = listener.accept() {
let mut b = [0u8; 2048];
let n = s.read(&mut b).unwrap_or(0);
let req = String::from_utf8_lossy(&b[..n]);
let body: Vec<u8> = if req.contains("/cs") {
format!("[{{\"g\":\"http://127.0.0.1:{port}/dl\",\"s\":{},\"at\":\"{at_for_thread}\"}}]", ct_for_thread.len()).into_bytes()
} else {
ct_for_thread.clone()
};
let head = format!("HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n", body.len());
let _ = s.write_all(head.as_bytes());
let _ = s.write_all(&body);
}
}
});
let client = crate::net::build_client(None).unwrap();
let dir = std::env::temp_dir().join(format!("kibble_mega_{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
let key_b64 = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(key32);
let url = format!("http://127.0.0.1:{port}/file/FILEID#{key_b64}");
let link = parse_mega_url(&url).unwrap();
let r = fetch_mega_file(&client, &url, &link, &dir, 100000).await.unwrap();
assert_eq!(r.handler, "mega");
assert_eq!(std::fs::read(dir.join("secret.txt")).unwrap(), plaintext);
}
#[test]
fn derive_file_key_xors_halves() {
let mut k = [0u8; 32];
for (i, b) in k.iter_mut().enumerate() { *b = i as u8; }
let (aes_key, iv) = derive_file_key(&k).unwrap();
for b in 0..16 { assert_eq!(aes_key[b], k[b] ^ k[b + 16]); }
assert_eq!(&iv[0..8], &k[16..24]);
assert_eq!(&iv[8..16], &[0u8; 8]);
}
}