因为过程大差不差所以就不写太正式了,就当随手一写。
不公开完整代码,不公开最后程序,叙述结构混乱。
首先因为Sokmil解密有若干个网络请求,所以先直接写个通用header(如果某个请求需要不一样的header可以再另改)
登录一个Sokmil账号之后复制主页的请求,到curlconverter.com直接复制headers,稍微修改一下即可。
headers = {
"Accept": "*/*",
"Accept-Language": "ja",
"Connection": "keep-alive",
"Origin": "https://www.sokmil.com",
"Sec-Fetch-Dest": "empty",
"Sec-Fetch-Mode": "cors",
"Sec-Fetch-Site": "same-origin",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36",
"content-type": "application/x-www-form-urlencoded",
"sec-ch-ua": '"Chromium";v="122", "Not(A:Brand";v="24", "Google Chrome";v="122"',
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": '"Windows"',
}
有很多用不到的但是不重要,重要的其实主要是UA。
为了捋清解密的所有必要请求,可以直接倒着推。
最后的请求肯定就是widevine的license,
https://license.candl.jp/sokmil/widevine/这个链接的请求。
这个请求的链接本身是固定的,data是CDM信息,那只有可能请求解密的关键讯息在headers里。
headers里也只有authorization中是一大串明显有讯息的内容,是Bearer eyXXXXX很长一串,明显后面ey开头的是编码后的讯息。
而这个信息在请求
https://www.sokmil.com/purchase_auth/时出现,并且就是这个请求的内容,也就是先进行的这个(大概率为账号)认证再最后widevine解密。
而这个auth的headers中没什么疑似讯息的东西,重要的是data和cookies。
data里面是keyID,看名字就盲猜是widevine的KID,内容还是32长度的。
cookies肯定就是带账号信息的了,有非常多个字段,可以直接另写脚本模拟请求来排除不重要的字段,最后可以知道其中只有SCS是必要的。
SCS大概率是登录时返回的Set-Cookie,所以直接先试试模拟登录。
登录请求很简单就不说了,python里面设置请求时不允许重定向,然后获取响应的headers中的Set-Cookie就行。
——不过其实会获取到两个Set-Cookie,总之使用第二个就行。
然后是auth里的KID,大概率能从mpd文件中找到(说大概率是因为有些mpd文件可以不含KID),于是能找到唯一一个mpd请求。
里面能很容易找到default_KID="XXXX",去掉横线再转小写就是前面auth的keyID了。
另外pssh就可以直接自己生成,Sokmil的pssh有KID即可,在最后解密请求时用到。
那么mpd的链接是如何来的呢,再往前找可以看到有个
https://www.sokmil.com/streaminginfo/?pid=XXXX&rid=XXXX的请求。
这个请求的内容是json,可以看到video_sources里面第一个就是mpd链接(但其实这里面也有m3u8链接和第三个忘了啥加密的链接)。
再接着,这个streaminginfo的pid和rid是怎么来的呢,pid倒是跟详情页的数字一样。
直接对视频详情页查看源码搜索rid就能搜到,所以streaminginfo的链接可以直接get详情页获取。
登录:
data = {
"id": mail,
"pw": password,
"autologin": "on",
"act": "",
"repeat": "true",
"encpw": "",
}
logger.info("Logging in...")
response = requests.post(
"https://www.sokmil.com/member/login/",
headers=headers,
data=data,
allow_redirects=False,
)
headers = response.headers
cookies = headers.get("Set-Cookie")
scs = re.findall(r"SCS=([^;]+);", cookies)[1]
logger.info("Login successful.")
logger.info("Retrieved SCS.")
然后写个通用Cookies:
cookies = {
"AGEAUTH": "ok",
"SCS": scs,
}
获取PID和RID:
url = input("Url of the title: ")
logger.info("Fetching PID & RID...")
response = requests.get(url, headers=headers, cookies=cookies)
data = response.text
match = re.search(
r"https:\/\/www\.sokmil\.com\/streaminginfo\/\?pid=(\d+)&rid=(\d+)", data
)
pid = match.group(1)
rid = match.group(2)
logger.info("Retrieved PID & RID.")
获取mpd链接:
logger.info("Fetching mpd url...")
params = {
"pid": pid,
"rid": rid,
}
response = requests.get(
"https://www.sokmil.com/streaminginfo/",
params=params,
cookies=cookies,
headers=headers,
)
data = response.text
streaminginfo = json.loads(data)
try:
mpd = streaminginfo["data"]["video_sources"][0]["src"]
except:
response = requests.get(
"https://www.sokmil.com/streaminginfo/",
params=params,
cookies=cookies,
headers=headers,
)
data = response.text
streaminginfo = json.loads(data)
mpd = streaminginfo["data"]["video_sources"][0]["src"]
logger.info("Retrieved mpd url.")
关于为什么要写个try:因为有时候好像请求的结果有误,验证起来比较麻烦所以干脆错误之后再请求一次,第二次总之没错误过。
获取KID:
logger.info("Fetching KID...")
response = requests.get(mpd, headers=headers, cookies=cookies)
data = response.text
kid = re.search(r"default_KID=\"([^\"]+)\"", data).group(1).replace("-", "")
logger.info("Retrieved KID.")
获取token:
logger.info("Fetching token...")
data = {
"keyId": kid.lower(),
}
response = requests.post(
"https://www.sokmil.com/purchase_auth/", cookies=cookies, headers=headers, data=data
)
token = response.text
logger.info("Retrieved token.")
生成PSSH:
logger.info("Generating PSSH...")
kid = [base64.b16decode(kid.upper())]
boxes = []
pssh_data = pssh_box._generate_widevine_data(kid, None, None, None)
boxes.append(
pssh_box.Pssh(
0, base64.b16decode("EDEF8BA979D64ACEA3C827DCD51D21ED"), kid, pssh_data
)
)
box_data = b"".join([x.binary_string() for x in boxes])
pssh = base64.b64encode(box_data).decode()
logger.info("Got PSSH.")
获取key:
logger.info("Obtaining keys...")
lic_url = "https://license.candl.jp/sokmil/widevine/"
def WV_Function(pssh, lic_url, cert_b64=None):
wvdecrypt = WvDecrypt(
init_data_b64=pssh,
cert_data_b64=cert_b64,
device={
"name": "android_generic",
"description": "android studio cdm",
"security_level": 3,
"session_id_type": "android",
"private_key_available": True,
"vmp": False,
"send_key_control_nonce": True,
"device_client_id_blob_filename": os.path.join(
current_folder, "CDM", "device_client_id_blob"
),
"device_private_key_filename": os.path.join(
current_folder, "CDM", "device_private_key"
),
},
)
widevine_license = requests.post(
url=lic_url,
data=wvdecrypt.get_challenge(),
headers={
"authorization": f"Bearer {token}",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36",
},
)
license_b64 = json.loads(widevine_license.text)["license"]
wvdecrypt.update_license(license_b64)
Correct, keyswvdecrypt = wvdecrypt.start_process()
if Correct:
return Correct, keyswvdecrypt
_, _keys = WV_Function(pssh, lic_url)
if len(_keys) == 0:
logger.error("Error in requesting key! Press Enter to exit.")
input()
raise
keys = ""
for key in _keys:
keys = f"{keys}--key {key} "
logger.info("Got keys.")
这里最后模拟请求除了token之外还要设置一下UA。
获取到key就可以下载合并解密混流了。