那接下来的问题就是我们该怎么读取onlyfans的密钥了
首先需要解密头
直接逐步定位打到了
, Pe = e=>{
try {
const t = {
...(0,
F.A)(e)
};
t["app-token"] = te;
const s = V.A.getters["auth/authUserId"];
s && (t["user-id"] = s),
t["x-bc"] = fe(),
t["x-of-rev"] = "202406261341-9a802bb7ea";
const {hash: r} = V.A.state.hash;
return r && (t["x-hash"] = r),
t
} catch (t) {
console.error(t)
}
return {}
}
t初始化是sign和time
然后设置app-token,这里跟其他一致
user-id,设置用户id,等价于cookie的auth_id,也基本一致
t["x-bc"] = fe() 提取bcToken,老生常谈
t["x-of-rev"] = "202406261341-9a802bb7ea";固定值
const {hash: r} = V.A.state.hash; 从https://cdn2.onlyfans.com/hash/中读取
没难度啊我靠
x-hash: "mkVyQlWEXk/Vb0n/4iia1HdR+AeHJrzzR27MA+8="
x-of-rev:"202406261341-9a802bb7ea"
这两个参数要保持最新,如果旧版本的请求没补这两个或者hash过期
会导致请求正常但是DRM的Cookies是假的
好心机
但是直接往认证服务器发送包依然失败
这部分卡了我很久
只能去参考od-drm项目
https://github.com/sim0n00ps/OF-DRM/blob/089b7402dc1f6240993255b0492795bcd1114be8/OF DRM Video Downloader/Helpers/APIHelper.cs#L1199
找到了
var resp1 = PostData(licenceURL, drmHeaders, new byte[] { 0x08, 0x04 });
var certDataB64 = Convert.ToBase64String(resp1);
var cdm = new CDMApi();
var challenge = cdm.GetChallenge(pssh, certDataB64, false, false);
var resp2 = PostData(licenceURL, drmHeaders, challenge);
var licenseB64 = Convert.ToBase64String(resp2);
cdm.ProvideLicense(licenseB64);
List<ContentKey> keys = cdm.GetKeys();
可以看到先提交了一个0804,然后设置证书再处理解密部分
观察onlyfans的抓包也确实存在两次提交
因为我一直在搞解密部分大意了,没有闪!
关于设置服务器证书pywidevine也有函数,我们查看源码可以找到set_service_certificate
函数
Set a Service Privacy Certificate for Privacy Mode. (optional but recommended)
The Service Certificate is used to encrypt Client IDs in Licenses. This is also
known as Privacy Mode and may be required for some services or for some devices.
Chrome CDM requires it as of the enforcement of VMP (Verified Media Path).
We reject direct DrmCertificates as they do not have signature verification and
cannot be verified. You must provide a SignedDrmCertificate or a SignedMessage
containing a SignedDrmCertificate.
Parameters:
session_id: Session identifier.
certificate: SignedDrmCertificate (or SignedMessage containing one) in Base64
or Bytes form obtained from the Service. Some services have their own,
but most use the common privacy cert, (common_privacy_cert). If None, it
will remove the current certificate.
Raises:
InvalidSession: If the Session identifier is invalid.
DecodeError: If the certificate could not be parsed as a SignedDrmCertificate
nor a SignedMessage containing a SignedDrmCertificate.
SignatureMismatch: If the Signature of the SignedDrmCertificate does not
match the underlying DrmCertificate.
Returns the Service Provider ID of the verified DrmCertificate if successful.
If certificate is None, it will return the now-unset certificate's Provider ID,
or None if no certificate was set yet.
那我们就继续研究一下原网页的代码,看看0801哪里来的
之前我们分析getLicense我们知道了接受消息的在
u.addEventListener("message", (e=>{
c.trigger({
type: "keymessage",
messageEvent: e
}),
"license-request" !== e.messageType && "license-renewal" !== e.messageType || a(o, e.message, d).then((e=>{
r(u.update(e).then((()=>{
c.trigger({
type: "keysessionupdated",
keySession: u
})
}
)).catch((e=>{
const t = {
errorType: s.default.Error.EMEFailedToUpdateSessionWithReceivedLicenseKeys,
keySystem: m
};
l(e, t)
}
)))
}
)).catch((e=>{
p(e)
}
))
}
查找文档https://www.w3.org/TR/encrypted-media/
generateRequest
Generates a license request based on the initData. A message of type "license-request" or "individualization-request" will always be queued if the algorithm succeeds and the promise is resolved.
Parameter Type Nullable Optional Description
initDataType DOMString ✘ ✘ The Initialization Data Type of the initData.
initData BufferSource ✘ ✘ Initialization Data
根据文档的提示,生成我们可以定位到
u.generateRequest(n, i).catch((e=>{
const t = {
errorType: s.default.Error.EMEFailedToGenerateLicenseRequest,
keySystem: m
};
l(e, t),
p("Unable to create or initialize key session")
}
关于到底这两个是否有关联可以下断在u.generateRequest调用时将函数置为空函数()=>{},可以发现接收消息没有触发,证明了这两个函数没有关联
其中n是cenc,i是字节,我们一路往上堆栈回溯可以找到
if (i) {
e[i] = {
attributes: n
};
const r = Te(t, "cenc:pssh")[0];
if (r) {
const t = Ie(r);
e[i].pssh = t && p(t)
}
}
其中p是将字符串转为字节数组
function p(e) {
for (var t = m(e), n = new Uint8Array(t.length), i = 0; i < t.length; i++)
n[i] = t.charCodeAt(i);
return n
}
根据调试首先传入的是wpd中较短的pssh,然后得到0804提交再获取证书
但是问题来了
0804到底怎么生成的?
我研究了几天还是没有得到答案
于是到处找人寻味
直到在forum.videohelp.com论坛得到了larley大神的解答!
https://forum.videohelp.com/threads/415095-How-to-simulate-the-generateRequest-function-through-python
The long PSSH is used for Microsoft's PlayReady and then short one is for Google's Widevine (that's what you're going to want to use).
The '08 04' (or CAQ= is base64) (or '\x08\x04' in python) is a fixed data value that can be sent to the same server (even same URL and 99% of the time even the same headers) from which you will receive your license.
0804竟然是generateRequest返回的固定值!
我在https://integration.widevine.com/diagnostics的生成widevine pssh试了几组
都返回了0804!
那么一切就通顺了
首先根据较短的pssh获得0804
然后将0804上传得到certData证书
再设置certData证书
然后上传pssh得到正确key
理论建立完毕
实践开始!