use anyhow::{anyhow, bail, Context, Result}; use fancy_regex::Regex; use log::debug; use once_cell::sync::Lazy; use reqwest::Client; use std::result::Result::Ok; use crate::cache::{Cache, DeobfData}; use crate::util; pub struct Deobfuscator { http: Client, cache: Cache, } impl Deobfuscator { #[must_use] pub fn new(http: Client, cache: Cache) -> Self { Self { http, cache } } async fn get_deobf_data(&self) -> Result { let http = self.http.clone(); self.cache .get_deobf_data(async move { let js_url = get_player_js_url(&http) .await .context("Failed to retrieve player.js URL")?; let player_js = get_response(&http, &js_url) .await .context("Failed to download player.js")?; debug!("Downloaded player.js from {}", js_url); let sig_fn = get_sig_fn(&player_js)?; let nsig_fn = get_nsig_fn(&player_js)?; let sts = get_sts(&player_js)?; Ok(DeobfData { js_url, nsig_fn, sig_fn, sts, }) }) .await } pub async fn deobfuscate_sig(&self, sig: &str) -> Result { let deobf_data = self.get_deobf_data().await?; deobfuscate_sig(sig, &deobf_data.sig_fn) } pub async fn deobfuscate_nsig(&self, nsig: &str) -> Result { let deobf_data = self.get_deobf_data().await?; deobfuscate_nsig(nsig, &deobf_data.nsig_fn) } pub async fn get_sts(&self) -> Result { let deobf_data = self.get_deobf_data().await?; Ok(deobf_data.sts) } } const DEOBFUSCATION_FUNC_NAME: &str = "deobfuscate"; fn get_sig_fn_name(player_js: &str) -> Result { static FUNCTION_PATTERNS: Lazy<[Regex; 6]> = Lazy::new(|| { [ Regex::new("(?:\\b|[^a-zA-Z0-9$])([a-zA-Z0-9$]{2,})\\s*=\\s*function\\(\\s*a\\s*\\)\\s*\\{\\s*a\\s*=\\s*a\\.split\\(\\s*\"\"\\s*\\)").unwrap(), Regex::new("\\bm=([a-zA-Z0-9$]{2,})\\(decodeURIComponent\\(h\\.s\\)\\)").unwrap(), Regex::new("\\bc&&\\(c=([a-zA-Z0-9$]{2,})\\(decodeURIComponent\\(c\\)\\)").unwrap(), Regex::new("([\\w$]+)\\s*=\\s*function\\((\\w+)\\)\\{\\s*\\2=\\s*\\2\\.split\\(\"\"\\)\\s*;").unwrap(), Regex::new("\\b([\\w$]{2,})\\s*=\\s*function\\((\\w+)\\)\\{\\s*\\2=\\s*\\2\\.split\\(\"\"\\)\\s*;").unwrap(), Regex::new("\\bc\\s*&&\\s*d\\.set\\([^,]+\\s*,\\s*(:encodeURIComponent\\s*\\()([a-zA-Z0-9$]+)\\(").unwrap(), ] }); util::get_cg_from_regexes(FUNCTION_PATTERNS.iter(), player_js, 1) .ok_or_else(|| anyhow!("could not find deobf function name")) } fn caller_function(fn_name: &str) -> String { "function ".to_owned() + DEOBFUSCATION_FUNC_NAME + "(a){return " + &fn_name + "(a);}" } fn get_sig_fn(player_js: &str) -> Result { let dfunc_name = get_sig_fn_name(player_js)?; let function_pattern_str = "(".to_owned() + &dfunc_name.replace('$', "\\$") + "=function\\([a-zA-Z0-9_]+\\)\\{.+?\\})"; let function_pattern = ok_or_bail!( Regex::new(&function_pattern_str), Err(anyhow!("could not parse function pattern regex")) ); let deobfuscate_function = "var ".to_owned() + some_or_bail!( function_pattern.captures(player_js).ok().flatten(), Err(anyhow!("could not find deobf function")) ) .get(1) .unwrap() .as_str() + ";"; let helper_object_name_pattern = Regex::new(";([A-Za-z0-9_\\$]{2})\\...\\(").unwrap(); let helper_object_name = some_or_bail!( helper_object_name_pattern .captures(&deobfuscate_function) .ok() .flatten(), Err(anyhow!("could not find helper object name")) ) .get(1) .unwrap() .as_str(); let helper_pattern_str = "(var ".to_owned() + &helper_object_name.replace('$', "\\$") + "=\\{.+?\\}\\};)"; let helper_pattern = ok_or_bail!( Regex::new(&helper_pattern_str), Err(anyhow!("could not parse helper pattern regex")) ); let player_js_nonl = player_js.replace('\n', ""); let helper_object = some_or_bail!( helper_pattern.captures(&player_js_nonl).ok().flatten(), Err(anyhow!("could not find helper object")) ) .get(1) .unwrap() .as_str(); Ok(helper_object.to_owned() + &deobfuscate_function + &caller_function(&dfunc_name)) } fn deobfuscate_sig(sig: &str, sig_fn: &str) -> Result { let context = quick_js::Context::new()?; context.eval(sig_fn)?; let res = context.call_function(DEOBFUSCATION_FUNC_NAME, vec![sig])?; match res.as_str() { Some(res) => Ok(res.to_owned()), None => bail!("deobfuscation func returned null"), } } fn get_nsig_fn_name(player_js: &str) -> Result { let function_name_pattern = Regex::new("\\.get\\(\"n\"\\)\\)&&\\(b=([a-zA-Z0-9$]+)(?:\\[(\\d+)])?\\([a-zA-Z0-9]\\)") .unwrap(); let fname_match = some_or_bail!( function_name_pattern.captures(player_js).ok().flatten(), Err(anyhow!("could not find n_deobf function")) ); let function_name = fname_match.get(1).unwrap().as_str(); if fname_match.len() == 1 { return Ok(function_name.to_owned()); } let array_num = fname_match.get(2).unwrap().as_str().parse::()?; let array_pattern_str = "var ".to_owned() + &fancy_regex::escape(function_name) + "\\s*=\\s*\\[(.+?)];"; let array_pattern = Regex::new(&array_pattern_str)?; let array_str = some_or_bail!( array_pattern.captures(player_js).ok().flatten(), Err(anyhow!("could not find n_deobf array_str")) ) .get(1) .unwrap() .as_str(); let mut names = array_str.split(','); let name = some_or_bail!( names.nth(array_num.try_into()?), Err(anyhow!( "could not get {}th item from {}", array_num, array_str )) ); Ok(name.to_owned()) } fn match_to_closing_parenthesis(string: &str, start: &str) -> Option { let mut start_index = string.find(start)?; start_index += start.len(); let mut visited_par = false; let mut open_par = 0; let mut res = String::new(); for c in string[start_index..].chars() { res.push(c); match c { '{' => { visited_par = true; open_par += 1; } '}' => { open_par -= 1; } _ => {} }; if visited_par && open_par == 0 { break; } } Some(res) } fn get_nsig_fn(player_js: &str) -> Result { let function_name = get_nsig_fn_name(player_js)?; // Find using parentheses let function_base = function_name.to_owned() + "=function"; let nsig_fn_code = match match_to_closing_parenthesis(player_js, &function_base) { Some(m) => function_base.clone() + &m + ";", None => { // Find using regex let player_js_nonl = player_js.replace('\n', ""); let function_pattern_str = function_name.to_owned() + "=function(.*?}};)\n"; let function_pattern = Regex::new(&function_pattern_str)?; let function = some_or_bail!( function_pattern.captures(&player_js_nonl)?, Err(anyhow!("could not find n_decode function")) ) .get(1) .unwrap() .as_str(); "function ".to_owned() + function } }; Ok(nsig_fn_code + &caller_function(&function_name)) } fn deobfuscate_nsig(sig: &str, nsig_fn: &str) -> Result { let context = quick_js::Context::new()?; context.eval(nsig_fn)?; let res = context.call_function(DEOBFUSCATION_FUNC_NAME, vec![sig])?; match res.as_str() { Some(res) => Ok(res.to_owned()), None => bail!("deobfuscation func returned null"), } } async fn get_player_js_url(http: &Client) -> Result { let resp = http .get("https://www.youtube.com/iframe_api") .send() .await? .error_for_status()?; let text = resp.text().await?; let player_hash_pattern = Regex::new(r#"https:\\\/\\\/www\.youtube\.com\\\/s\\\/player\\\/([a-z0-9]{8})\\\/"#) .unwrap(); let player_hash = some_or_bail!( player_hash_pattern.captures(&text)?, Err(anyhow!("could not find player hash")) ) .get(1) .unwrap() .as_str(); Ok(format!( "https://www.youtube.com/s/player/{}/player_ias.vflset/en_US/base.js", player_hash )) } async fn get_response(http: &Client, url: &str) -> Result { let resp = http.get(url).send().await?.error_for_status()?; Ok(resp.text().await?) } fn get_sts(player_js: &str) -> Result { let sts_pattern = Regex::new("signatureTimestamp[=:](\\d+)").unwrap(); Ok(some_or_bail!( sts_pattern.captures(&player_js)?, Err(anyhow!("could not find sts")) ) .get(1) .unwrap() .as_str() .to_owned()) } #[cfg(test)] mod tests { use super::*; use std::sync::Arc; use test_log::test; const TEST_JS: &str = include_str!("../notes/base.js"); const N_DEOBF_FUNC: &str = r#"Vo=function(a){var b=a.split(""),c=[function(d,e,f){var h=f.length;d.forEach(function(l,m,n){this.push(n[m]=f[(f.indexOf(l)-f.indexOf(this[m])+m+h--)%f.length])},e.split(""))}, 928409064,-595856984,1403221911,653089124,-168714481,-1883008765,158931990,1346921902,361518508,1403221911,-362174697,-233641452,function(){for(var d=64,e=[];++d-e.length-32;){switch(d){case 91:d=44;continue;case 123:d=65;break;case 65:d-=18;continue;case 58:d=96;continue;case 46:d=95}e.push(String.fromCharCode(d))}return e}, b,158931990,791141857,-907319795,-1776185924,1595027902,-829736173,function(d,e){e=(e%d.length+d.length)%d.length;d.splice(0,1,d.splice(e,1,d[0])[0])}, -1274951142,function(){for(var d=64,e=[];++d-e.length-32;){switch(d){case 91:d=44;continue;case 123:d=65;break;case 65:d-=18;continue;case 58:d=96;continue;case 46:d=95}e.push(String.fromCharCode(d))}return e}, 1758743891,function(d){d.reverse()}, -830417133,"AF43j",1942017693,function(d,e){e=(e%d.length+d.length)%d.length;d.splice(e,1)}, null,-959991459,-287691724,-1365731946,b,1250397544,-1883008765,-1912322658,b,1300441121,null,-1962382380,1954679120,function(d){for(var e=d.length;e;)d.push(d.splice(--e,1)[0])}, -985125467,function(d,e){for(e=(e%d.length+d.length)%d.length;e--;)d.unshift(d.pop())}, null,497372841,-1912651541,function(d,e){d.push(e)}, function(d,e){e=(e%d.length+d.length)%d.length;d.splice(-e).reverse().forEach(function(f){d.unshift(f)})}, function(d,e){e=(e%d.length+d.length)%d.length;var f=d[0];d[0]=d[e];d[e]=f}]; c[30]=c;c[40]=c;c[46]=c;try{c[43](c[34]),c[45](c[40],c[47]),c[46](c[51],c[33]),c[16](c[47],c[36]),c[38](c[31],c[49]),c[16](c[11],c[39]),c[0](c[11]),c[35](c[0],c[30]),c[35](c[4],c[17]),c[34](c[48],c[7],c[11]()),c[35](c[4],c[23]),c[35](c[4],c[9]),c[5](c[48],c[28]),c[36](c[46],c[16]),c[4](c[41],c[1]),c[4](c[16],c[28]),c[3](c[40],c[17]),c[9](c[8],c[23]),c[45](c[30],c[4]),c[50](c[3],c[28]),c[36](c[51],c[23]),c[14](c[0],c[24]),c[14](c[35],c[1]),c[20](c[51],c[41]),c[15](c[8],c[0]),c[31](c[35]),c[29](c[26]), c[36](c[8],c[32]),c[20](c[25],c[10]),c[2](c[22],c[8]),c[32](c[20],c[16]),c[32](c[47],c[49]),c[1](c[44],c[28]),c[39](c[16]),c[32](c[42],c[22]),c[46](c[14],c[48]),c[26](c[29],c[10]),c[46](c[9],c[3]),c[32](c[45])}catch(d){return"enhanced_except_85UBjOr-_w8_"+a}return b.join("")};function deobfuscate(a){return Vo(a);}"#; #[test] fn t_get_sig_fn_name() { let dfunc_name = get_sig_fn_name(TEST_JS).unwrap(); assert_eq!(dfunc_name, "Rva"); } #[test] fn t_get_sig_fn() { let dcode = get_sig_fn(TEST_JS).unwrap(); assert_eq!( dcode, r#"var qB={w8:function(a){a.reverse()},EC:function(a,b){var c=a[0];a[0]=a[b%a.length];a[b%a.length]=c},Np:function(a,b){a.splice(0,b)}};var Rva=function(a){a=a.split("");qB.Np(a,3);qB.w8(a,41);qB.EC(a,55);qB.Np(a,3);qB.w8(a,33);qB.Np(a,3);qB.EC(a,48);qB.EC(a,17);qB.EC(a,43);return a.join("")};function deobfuscate(a){return Rva(a);}"# ); } #[test] fn t_deobfuscate_sig() { let dcode = get_sig_fn(TEST_JS).unwrap(); let deobf = deobfuscate_sig("GOqGOqGOq0QJ8wRAIgaryQHfplJ9xJSKFywyaSMHuuwZYsoMTAvRvfm51qIGECIA5061zWeyfMPX9hEl_U6f9J0tr7GTJMKyPf5XNrJb5fb5i", &dcode).unwrap(); assert_eq!(deobf, "AOq0QJ8wRAIgaryQHmplJ9xJSKFywyaSMHuuwZYsoMTfvRviG51qIGECIA5061zWeyfMPX9hEl_U6f9J0tr7GTJMKyPf5XNrJb5f"); } #[test] fn t_get_nsig_fn_name() { let name = get_nsig_fn_name(TEST_JS).unwrap(); assert_eq!(name, "Vo"); } #[test] fn t_match_to_closing_parenthesis() { let res = match_to_closing_parenthesis("Kx Hello { Thx { Bye } } Wut {Tst {}}", "Hello").unwrap(); assert_eq!(res, " { Thx { Bye } }") } #[test] fn t_get_nsig_fn() { let res = get_nsig_fn(TEST_JS).unwrap(); assert_eq!(res, N_DEOBF_FUNC); } #[test] fn t_get_sts() { let res = get_sts(TEST_JS).unwrap(); assert_eq!(res, "19187") } #[test] fn t_deobfuscate_nsig() { let res = deobfuscate_nsig("BI_n4PxQ22is-KKajKUW", N_DEOBF_FUNC).unwrap(); assert_eq!(res, "nrkec0fwgTWolw"); } #[test(tokio::test)] async fn t_get_player_js_url() { let client = Client::new(); let url = get_player_js_url(&client).await.unwrap(); assert!(url.starts_with("https://www.youtube.com/s/player")); assert_eq!(url.len(), 73); } #[test(tokio::test)] async fn t_update() { let client = Client::new(); let cache = Cache::default(); let deobf = Deobfuscator::new(client, cache); let deobf_sig = deobf.deobfuscate_sig("GOqGOqGOq0QJ8wRAIgaryQHfplJ9xJSKFywyaSMHuuwZYsoMTAvRvfm51qIGECIA5061zWeyfMPX9hEl_U6f9J0tr7GTJMKyPf5XNrJb5fb5i").await.unwrap(); println!("{}", deobf_sig); } #[test(tokio::test)] async fn t_parallel() { let client = Client::new(); let cache = Cache::default(); let deobf = Deobfuscator::new(client, cache); let deobf_arc = Arc::new(deobf); let (deobf_sig, deobf_nsig) = tokio::join!( deobf_arc.deobfuscate_sig("GOqGOqGOq0QJ8wRAIgaryQHfplJ9xJSKFywyaSMHuuwZYsoMTAvRvfm51qIGECIA5061zWeyfMPX9hEl_U6f9J0tr7GTJMKyPf5XNrJb5fb5i"), deobf_arc.deobfuscate_nsig("BI_n4PxQ22is-KKajKUW"), ); println!("{}", deobf_sig.unwrap()); println!("{}", deobf_nsig.unwrap()); } }