Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add rick roll support #3

Closed
azihassan opened this issue Mar 1, 2023 · 4 comments
Closed

Add rick roll support #3

azihassan opened this issue Mar 1, 2023 · 4 comments
Labels
bug Something isn't working

Comments

@azihassan
Copy link
Owner

The source code for https://www.youtube.com/watch?v=dQw4w9WgXcQ doesn't contain the "itag":18,url: string. Instead, video URLs are encoded in a signatureCipher field :

{
  "itag": 18,
  "mimeType": "video/mp4; codecs='avc1.42001E, mp4a.40.2'",
  "bitrate": 342654,
  "width": 640,
  "height": 360,
  "lastModified": "1674233743350828",
  "quality": "medium",
  "fps": 25,
  "qualityLabel": "360p",
  "projectionType": "RECTANGULAR",
  "audioQuality": "AUDIO_QUALITY_LOW",
  "approxDurationMs": "212091",
  "audioSampleRate": "44100",
  "audioChannels": 2,
  "signatureCipher": "s=f%3Dg7YDsIxx09c-gmZw0PPY5t%3DSIBNYUiFBmh8EGrFytQkDQICA69QOmNV3tcGsv5PHELGkzf5ojyL-mLb92UXKMAH3WHgIQRw8JQ0qOAFOAF&sp=sig&url=https://rr2---sn-f5o5-jhod.googlevideo.com/videoplayback%3Fexpire%3D1677629799%26ei%3DB0X-Y82bDqTt1wb4vrQQ%26ip%3D105.66.0.5%26id%3Do-AFY6sdlJJ1IBUJBDk2CDuTIan2WiYPp2YWaDhwdKkE-m%26itag%3D18%26source%3Dyoutube%26requiressl%3Dyes%26mh%3D7c%26mm%3D31%252C29%26mn%3Dsn-f5o5-jhod%252Csn-h5qzen7d%26ms%3Dau%252Crdu%26mv%3Dm%26mvi%3D2%26pl%3D24%26initcwndbps%3D276250%26vprv%3D1%26mime%3Dvideo%252Fmp4%26ns%3D861xLFuuouDdIvuzeF2DfT0L%26cnr%3D14%26ratebypass%3Dyes%26dur%3D212.091%26lmt%3D1674233743350828%26mt%3D1677607758%26fvip%3D4%26fexp%3D24007246%26c%3DWEB%26txp%3D4530434%26n%3DME4I0_ZBuCo2R7YhTZF%26sparams%3Dexpire%252Cei%252Cip%252Cid%252Citag%252Csource%252Crequiressl%252Cvprv%252Cmime%252Cns%252Ccnr%252Cratebypass%252Cdur%252Clmt%26lsparams%3Dmh%252Cmm%252Cmn%252Cms%252Cmv%252Cmvi%252Cpl%252Cinitcwndbps%26lsig%3DAG3C_xAwRQIhAOgRfSiKHz-i6errp0ktW0qs3vqHxHM-gmDG0B8WJgIeAiBZHYOKcZW1tFU-8quvvCSR19Hpxyz9uxfzjvHKWMAhHw%253D%253D"
}
@azihassan azihassan added the bug Something isn't working label Mar 1, 2023
@azihassan
Copy link
Owner Author

azihassan commented Mar 3, 2023

Found a mention of signatureCipher in base.js :

  RC = function (a) {
    var b = g.M(a, qta) || a.signatureCipher;
    a = { AT: false, zx: "", xz: "", s: "" };
    if (!b) return a;
    b = dw(b);
    a.AT = true;
    a.zx = b.url;
    a.xz = b.sp;
    a.s = b.s;
    return a;
  };

Which is then called here :

        e = RC(n);
        e = tG(e.zx || n.url || "", e.xz, e.s);

The definition of tG is as follows :

  tG = function (a, b, c) {
    b = void 0 === b ? "" : b;
    c = void 0 === c ? "" : c;
    a = new g.oD(a, true);
    a.set("alr", "yes");
    c && ((c = Gta(decodeURIComponent(c))), a.set(b, encodeURIComponent(c)));
    return a;
  };

a.set seems to be a prototype function of g.oD :

g.oD.prototype.set = function (a, b) {
    this.j[a] !== b && ((this.j[a] = b), (this.url = ""));
};

The definition of Gta is as follows :

  Gta = function (a) {
    a = a.split("");
    nD.uC(a, 61);
    nD.ei(a, 50);
    nD.Ua(a, 3);
    nD.uC(a, 53);
    nD.ei(a, 72);
    nD.uC(a, 24);
    nD.ei(a, 72);
    nD.uC(a, 17);
    nD.Ua(a, 1);
    return a.join("");
  };

Gta relies on the nD object :

  var nD = {
    Ua: function (a, b) {
      a.splice(0, b);
    },
    ei: function (a) {
      a.reverse();
    },
    uC: function (a, b) {
      var c = a[0];
      a[0] = a[b % a.length];
      a[b % a.length] = c;
    },
  };

@azihassan
Copy link
Owner Author

azihassan commented Mar 5, 2023

With all the above I wrote this script :

var g = {};
g.ne = function (a) {
    return encodeURIComponent(String(a));
};

Xh = function (a, b, c) {
    if (Array.isArray(b))
        for (var d = 0; d < b.length; d++) Xh(a, String(b[d]), c);
    else null != b && c.push(a + ("" === b ? "" : "=" + g.ne(b)));
};

g.$h = function (a) {
    var b = [],
        c;
    for (c in a) Xh(c, a[c], b);
    return b.join("&");
};

var oe = function (a) {
    return decodeURIComponent(a.replace(/\+/g, " "));
};

var Ana = /^[\w.]*$/,
    yna = { q: !0, search_query: !0 },
    xna = String(bw);

var wna = function (a) {
    return a && a.match(Ana) ? a : oe(a);
};

var bw = function (a, b) {
    b = a.split(b);
    for (var c = {}, d = 0, e = b.length; d < e; d++) {
        var f = b[d].split("=");
        if ((1 == f.length && f[0]) || 2 == f.length)
            try {
                var h = wna(f[0] || ""),
                    l = wna(f[1] || "");
                h in c
                    ? Array.isArray(c[h])
                    ? g.vb(c[h], l)
                    : (c[h] = [c[h], l])
                    : (c[h] = l);
            } catch (q) {
                var m = q,
                    n = f[0],
                    p = String(bw);
                m.args = [
                    {
                        key: n,
                        value: f[1],
                        query: a,
                        method: xna == p ? "unchanged" : p,
                    },
                ];
                yna.hasOwnProperty(n) || console.log(m);
                throw q;
            }
    }
    return c;
};

dw = function (a) {
    "?" == a.charAt(0) && (a = a.substr(1));
    return bw(a, "&");
};

RC = function (a) {
    var b = a.signatureCipher;
    a = { AT: !1, zx: "", xz: "", s: "" };
    if (!b) return a;
    b = dw(b);
    a.AT = !0;
    a.zx = b.url;
    a.xz = b.sp;
    a.s = b.s;
    return a;
};

var nD = {
    Ua: function (a, b) {
        a.splice(0, b);
    },
    ei: function (a) {
        a.reverse();
    },
    uC: function (a, b) {
        var c = a[0];
        a[0] = a[b % a.length];
        a[b % a.length] = c;
    },
};


Gta = function (a) {
    a = a.split("");
    nD.uC(a, 61);
    nD.ei(a, 50);
    nD.Ua(a, 3);
    nD.uC(a, 53);
    nD.ei(a, 72);
    nD.uC(a, 24);
    nD.ei(a, 72);
    nD.uC(a, 17);
    nD.Ua(a, 1);
    return a.join("");
};

var CD = {
    Qk: function (a, b) {
        var c = a[0];
        a[0] = a[b % a.length];
        a[b % a.length] = c;
    },
    BW: function (a) {
        a.reverse();
    },
    CJ: function (a, b) {
        a.splice(0, b);
    },
};


Nta = function (a) {
    a = a.split("");
    CD.Qk(a, 17);
    CD.CJ(a, 1);
    CD.BW(a, 68);
    CD.Qk(a, 65);
    return a.join("");
};

g.oD = function (a, b) {
    this.u = a;
    this.D = void 0 === b ? false : b;
    this.C = this.path = this.B = "";
    this.j = {};
    this.url = "";
    this.alr = "";
};

g.oD.prototype.set = function (a, b) {
    this.j[a] !== b && ((this.j[a] = b), (this.url = ""));
};

tG = function (a, b, c) {
    b = void 0 === b ? "" : b;
    c = void 0 === c ? "" : c;
    a = new g.oD(a, true);
    console.log({ a });
    a.set("alr", "yes");
    c && ((c = Nta(decodeURIComponent(c))), a.set(b, encodeURIComponent(c)));
    return a;
};

g.t = function (a) {
    var b =
        "undefined" != typeof Symbol && Symbol.iterator && a[Symbol.iterator];
    if (b) return b.call(a);
    if ("number" == typeof a.length) return { next: baa(a) };
    throw Error(String(a) + " is not an iterable or ArrayLike");
};

baa = function (a) {
    var b = 0;
    return function () {
        return b < a.length ? { done: !1, value: a[b++] } : { done: !0 };
    };
};

var GP = RC({
    signatureCipher: 's=8%3DIoUisF9qiH88GKc%3DHbSju0aSjmCdE1iEd_YIoA1R8-0AEiA3fDP2ZjVpjlgxaPokvdgdU6EXMc97ULktYa3MxWjjKIAhIgRw8JQ0qOM&sp=sig&url=https://rr2---sn-f5o5-jhod.googlevideo.com/videoplayback%3Fexpire%3D1677998710%26ei%3DFuYDZOvcF5rA1wbqpY-QAg%26ip%3D105.66.0.249%26id%3Do-AL9i4R7ZWDbkqGFcpqCjR64ZM9jhfSeNUHS2aDbJObwp%26itag%3D396%26aitags%3D133%252C134%252C135%252C136%252C137%252C160%252C242%252C243%252C244%252C247%252C248%252C278%252C394%252C395%252C396%252C397%252C398%252C399%26source%3Dyoutube%26requiressl%3Dyes%26mh%3D7c%26mm%3D31%252C29%26mn%3Dsn-f5o5-jhod%252Csn-h5q7knes%26ms%3Dau%252Crdu%26mv%3Dm%26mvi%3D2%26pl%3D24%26initcwndbps%3D277500%26vprv%3D1%26mime%3Dvideo%252Fmp4%26ns%3DB5uSVdIbFLxYS_fhKnn8WxIL%26gir%3Dyes%26clen%3D5953258%26dur%3D212.040%26lmt%3D1674230525337110%26mt%3D1677976618%26fvip%3D4%26keepalive%3Dyes%26fexp%3D24007246%26c%3DWEB%26txp%3D4537434%26n%3DHGV37DW5jPmqkk_%26sparams%3Dexpire%252Cei%252Cip%252Cid%252Caitags%252Csource%252Crequiressl%252Cvprv%252Cmime%252Cns%252Cgir%252Cclen%252Cdur%252Clmt%26lsparams%3Dmh%252Cmm%252Cmn%252Cms%252Cmv%252Cmvi%252Cpl%252Cinitcwndbps%26lsig%3DAG3C_xAwRQIhAIAFJFr1v__Yzrvr6cZ5xvpqP3F2VJtTAFFi0CdOeapGAiBV65ILOFQ1nshXJqO0X0aj0y6XNyr7ke2rK_CreaoQMg%253D%253D'
});

let e = tG(GP.zx || "", GP.xz, GP.s);
console.log({ e });
console.log(GP.zx + '&sig=' + e.j.sig);

The resulting URL still produces a 403. After comparing it with a real URL, I realized that the sig and n parameters are different :

image

The real URL also includes extra parameters, but it successfully loaded the video after I removed them so the issue isn't there.

I also tested by removing n from the real URL and it worked without it, so it can be discarded for now.

@azihassan
Copy link
Owner Author

azihassan commented Mar 5, 2023

I compared these results with some of the articles out there and found that this is indeed the right approach, even though the resulting URL fails with 403.

As I was reading this I noticed that the decryption function not only has a different name, but the algorithm itself is different from mine. I did some tests and it turns out that Youtube changes it on each page load apparently. After grabbing the base.js file from the page where I found the above signature and trying the new algorithm (I updated the code above to use CD and Nta instead of nD and Gta respectively) on the corresponding signature cipher, it finally worked. So in short :

  • Download the HTML page
  • Parse itags and signatureCiphers out the streamingData object
  • Parse out the URL of base.js
  • Somehow parse out the decryption algorithm
  • Apply it on the signatureCipher parameter of the streamingData formats

The trickiest part is going to involve parsing the decryption sequence.

@azihassan
Copy link
Owner Author

Still not sure why Youtube does this for certain videos only. So far I noticed that it does it for songs, but maybe the rule extends to all copyrighted content.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

1 participant