decryption ke(зіц)ys are rειnaðarann
This commit is contained in:
parent
37729aa76e
commit
b10801bf29
10 changed files with 423 additions and 34 deletions
|
@ -18,8 +18,12 @@ export default class AppleMusicApi {
|
|||
this.http = axios.create({
|
||||
baseURL: ampApiUrl,
|
||||
headers: {
|
||||
"Origin": appleMusicHomepageUrl,
|
||||
"Media-User-Token": mediaUserToken,
|
||||
"Origin": appleMusicHomepageUrl
|
||||
// TODO: move somewhere else
|
||||
// this is only used for `getWidevineLicense`
|
||||
"x-apple-music-user-token": mediaUserToken,
|
||||
"x-apple-renewal": true // do i wanna know what this does?
|
||||
},
|
||||
params: {
|
||||
"l": language
|
||||
|
@ -28,7 +32,7 @@ export default class AppleMusicApi {
|
|||
}
|
||||
|
||||
public async login(): Promise<void> {
|
||||
this.http.defaults.headers["Authorization"] = `Bearer ${await getToken(appleMusicHomepageUrl)}`;
|
||||
this.http.defaults.headers.common["Authorization"] = `Bearer ${await getToken(appleMusicHomepageUrl)}`;
|
||||
}
|
||||
|
||||
async getSong<
|
||||
|
@ -57,16 +61,14 @@ export default class AppleMusicApi {
|
|||
trackId: string,
|
||||
trackUri: string,
|
||||
challenge: string
|
||||
): Promise<string> {
|
||||
): Promise<{ license: string | undefined }> { // dubious type, doesn't matter much
|
||||
return (await this.http.post(licenseApiUrl, {
|
||||
params: {
|
||||
challenge: challenge,
|
||||
"key-system": "com.widevine.alpha",
|
||||
uri: trackUri,
|
||||
adamId: trackId,
|
||||
isLibrary: false,
|
||||
"user-initiated": true
|
||||
}
|
||||
challenge: challenge,
|
||||
"key-system": "com.widevine.alpha",
|
||||
uri: trackUri,
|
||||
adamId: trackId,
|
||||
isLibrary: false,
|
||||
"user-initiated": true
|
||||
})).data;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,7 +22,8 @@ const configSchema = z.object({
|
|||
const envSchema = z.object({
|
||||
MEDIA_USER_TOKEN: z.string(),
|
||||
ITUA: z.string(),
|
||||
WIDEVINE_CLIENT_ID: z.string()
|
||||
WIDEVINE_CLIENT_ID: z.string(),
|
||||
WIDEVINE_PRIVATE_KEY: z.string()
|
||||
});
|
||||
|
||||
// check that `config.toml` actually exists
|
||||
|
|
48
src/downloader/keygen.ts
Normal file
48
src/downloader/keygen.ts
Normal file
|
@ -0,0 +1,48 @@
|
|||
import { LicenseType, Session } from "node-widevine";
|
||||
import { env } from "../config.js";
|
||||
import { appleMusicApi } from "../api/index.js";
|
||||
import { dataUriToBuffer } from "data-uri-to-buffer";
|
||||
import * as log from "../log.js";
|
||||
import fs from "node:fs";
|
||||
import * as psshTools from "pssh-tools";
|
||||
|
||||
export async function getWidevineDecryptionKey(psshDataUri: string, trackId: string): Promise<void> {
|
||||
let pssh = Buffer.from(dataUriToBuffer(psshDataUri).buffer);
|
||||
|
||||
const privateKey = Buffer.from(env.WIDEVINE_PRIVATE_KEY, "base64");
|
||||
const identifierBlob = Buffer.from(env.WIDEVINE_CLIENT_ID, "base64");
|
||||
let session = new Session({ privateKey, identifierBlob }, pssh);
|
||||
|
||||
let challenge: Buffer;
|
||||
try {
|
||||
challenge = session.createLicenseRequest(LicenseType.STREAMING);
|
||||
} catch (err) {
|
||||
// for some reason, if gotten from a webplayback manifest, the pssh is in a completely different format
|
||||
// well, somewhat. we have to rebuild the pssh
|
||||
const rebuiltPssh = psshTools.widevine.encodePssh({
|
||||
contentId: "Hiiii", // lol?? i don't know what this is, random slop go!!!!
|
||||
dataOnly: false,
|
||||
keyIds: [Buffer.from(dataUriToBuffer(psshDataUri).buffer).toString("hex")]
|
||||
});
|
||||
|
||||
log.warn("pssh was invalid, treating it as raw data");
|
||||
log.warn("this should not error, unless the pssh data is invalid, too");
|
||||
|
||||
pssh = Buffer.from(rebuiltPssh, "base64");
|
||||
session = new Session({ privateKey, identifierBlob }, pssh);
|
||||
challenge = session.createLicenseRequest(LicenseType.STREAMING);
|
||||
}
|
||||
|
||||
const response = await appleMusicApi.getWidevineLicense(
|
||||
trackId,
|
||||
psshDataUri,
|
||||
challenge.toString("base64")
|
||||
);
|
||||
|
||||
if (typeof response?.license !== "string") { throw "license is missing or not a string! sign that authentication failed (unsupported codec?)"; }
|
||||
const license = session.parseLicense(Buffer.from(response.license, "base64"));
|
||||
if (license.length === 0) { throw "license(s) failed to be parsed. this could be an error for invalid data! (e.x. pssh/challenge)"; }
|
||||
|
||||
log.info(license);
|
||||
fs.writeFileSync("license", response.license, { encoding: "utf-8" });
|
||||
}
|
|
@ -3,7 +3,7 @@ import * as log from "../log.js";
|
|||
import type { SongAttributes } from "../api/types/appleMusic/attributes.js";
|
||||
import hls, { Item } from "parse-hls";
|
||||
import axios from "axios";
|
||||
// TODO: remove
|
||||
import { getWidevineDecryptionKey } from "./keygen.js";
|
||||
import { select } from "@inquirer/prompts";
|
||||
|
||||
// ugliest type ever
|
||||
|
@ -21,12 +21,14 @@ type HLS = ReturnType<typeof hls.default.parse>;
|
|||
// havent had this issue with the small pool i tested before late 2024. what ????
|
||||
// i don't get it.
|
||||
// i just tried another thing from 2022 ro 2023 and it worked fine
|
||||
// SOLVED. widevine keys are not always present in the m3u8 manifest that is default (you can see that in link above, thats why it exists)
|
||||
// OH. it doesn't seem to give the keys you want anyway LOLLLLLLL????
|
||||
// i'm sure its used for *SOMETHING* so i'll keep it
|
||||
|
||||
// SUPER TODO: turn this all into a streaminfo class
|
||||
|
||||
// this typing is dubious...
|
||||
// TODO: possibly just stop using an array; use union type on generic
|
||||
// TODO: add "legacy" fallback
|
||||
// SUPER TODO: add "legacy", would use stuff from webplayback, default to this
|
||||
// TODO: make widevine/fairplay optional (esp for above)
|
||||
async function getStreamInfo(trackMetadata: SongAttributes<["extendedAssetUrls"]>): Promise<void> {
|
||||
const m3u8Url = trackMetadata.extendedAssetUrls.enhancedHls;
|
||||
const m3u8 = await axios.get(m3u8Url, { responseType: "text" });
|
||||
|
@ -43,9 +45,7 @@ async function getStreamInfo(trackMetadata: SongAttributes<["extendedAssetUrls"]
|
|||
const playreadyPssh = getPlayreadyPssh(drmInfos, drmIds);
|
||||
const fairplayKey = getFairplayKey(drmInfos, drmIds);
|
||||
|
||||
log.debug("widevine pssh", widevinePssh);
|
||||
log.debug("playready pssh", playreadyPssh);
|
||||
log.debug("fairplay key", fairplayKey);
|
||||
await getWidevineDecryptionKey(widevinePssh, "1615276490");
|
||||
}
|
||||
|
||||
// i don't think i wanna write all of the values we need. annoying !
|
||||
|
@ -123,6 +123,5 @@ const getPlayreadyPssh = (drmInfos: DrmInfos, drmIds: string[]): string => getDr
|
|||
const getFairplayKey = (drmInfos: DrmInfos, drmIds: string[]): string => getDrmData(drmInfos, drmIds, "com.apple.streamingkeydelivery");
|
||||
|
||||
// TODO: remove later, this is just for testing
|
||||
log.debug(await appleMusicApi.getWebplayback("1758429584"));
|
||||
await getStreamInfo((await appleMusicApi.getSong("1758429584")).data[0].attributes);
|
||||
|
||||
log.debug(await appleMusicApi.getWebplayback("1615276490"));
|
||||
await getStreamInfo((await appleMusicApi.getSong("1615276490")).data[0].attributes);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue