Fei Yang

解密 YouTube 的 「Signature cipher」

在幾乎沒有一個 Invidious 實例能夠穩定 24 小時運作的情況下,我決定把我的自動獲取 YouTube 影片對應的音/畫質最高串流的 API 使用解析 YouTube 網頁的方式重寫,但我很快就發現部分疑似「受版權保護」的影片無法正常獲取串流 itag 對應的連接。不同於一般影片,這些影片只有一欄怪異的 signatureCipher 包含着對應的連接和其他資料。

初步分析

signatureCipher 是一串 URL 編碼過的查詢參數(但是沒有一般使用中最前面應該出現的 ?)。

第一對查詢參數的參數名是 s, 內容則是不標準的 URL 編碼的 URL 安全 base64,多試幾次後會發現字串中會出現 URL 編碼過的 =, 正常的 URL 安全 base64 並沒有最後的 = 。根據 signatureCipher 的名稱猜測這裡的 s 應該指的是 signature.

第二對查詢參數的參數名是 sp, 內容長期不變,爲 sig,這裡應該指的是 signature parameter.

第三對查詢參數的參數名是 url, 是被 URL 編碼過的連接。

將簽章直接拼接入被 URL 解碼後的連接得到了 403 錯誤,簽章似乎需要進一步處理。

逆向工程

搜尋 “YouTube signatureCipher” 後發現了這個 Stackoverflow 問題,雖然兩個回答都不完整,但是可以知道解密簽章的程式碼在 base.js 內。

搜尋 YouTube 網頁原始碼得到 https://www.youtube.com/s/player/c299662f/player_ias.vflset/en_US/base.js.

這裡我使用了 NewPipe 的 regex 以減少我尋找解密程式碼所需的時間(但是直接 Ctrl+F 搜尋 function(a){a=a.split(""); 也能找到,在我寫這篇文章的時候只有兩個滿足此條件的函式。)。
但是找到這個函式後並沒有結束,因爲找到的只是用於解密的入口:

1
2
3
4
5
6
7
8
9
10
11
12
xv = function (a) {
a = a.split("")
wv.ZB(a, 19)
wv.qq(a, 2)
wv.NP(a, 65)
wv.qq(a, 2)
wv.ZB(a, 55)
wv.qq(a, 3)
wv.ZB(a, 29)
wv.NP(a, 2)
return a.join("")
};

現在需要尋找名爲 wx 的一系列函式,這也算好找,只需要搜尋 var wv= 就能找到了:

1
2
3
4
5
6
7
8
9
var wv = {
qq: function (a, b) { a.splice(0, b) },
NP: function (a, b) {
var c = a[0]
a[0] = a[b % a.length]
a[b % a.length] = c
},
ZB: function (a) { a.reverse() }
};

解密字串

到此解密所需的所有的函式都已經找齊了,只需要把前面提到的查詢參數 s 的內容使用入口函式處理後使用 sp 的內容作爲參數名拼接入被 URL 解碼過的連接即可。

如果使用 NodeJS,到現在這個問題就已經解決了,但是如果使用其他程式語言,則需要手動實現一遍這兩個函式的所有功能。

值得注意的是,這些函式會隨着 YouTube 更新變動,在前面我提到的 Stackoverflow 問題中,兩個回答的解密函式都不相同,並且和我獲取到的函式也不相同。

因此我不太推薦自動化獲取解密函式:正則表達式非常可能無法在更新後正常工作,這時依然需要人工介入。

以下是 NodeJS 獲取完整連接的一個範例:

1
2
3
4
5
6
//將查詢參數初始化,方便後續調用
data = new URLSearchParams(`?${format['signatureCipher']}`)
//解密簽章字串
sig = xv(decodeURI(data.get('s')))
//拼接 URL
url = `${decodeURI(data.get('url'))}&${data.get('sp')}=${sig}`

後記

既然已經取得了解密方法,那就來嘗試一下吧:

本文使用 GNU 自由文檔許可證 1.3 授權條款進行授權。