forked from alacleaker/apple-music-alac-downloader
-
Notifications
You must be signed in to change notification settings - Fork 62
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
3 changed files
with
327 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,7 @@ | ||
* | ||
!.gitignore | ||
!agent.js | ||
!agent-arm64.js | ||
!go.mod | ||
!go.sum | ||
!main.go | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,313 @@ | ||
'use strict'; | ||
|
||
const fairplayCert = "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"; | ||
const kdContextMap = new Map(); | ||
let persistentKeyPtr = null; | ||
|
||
function newStdStringFromArrayBuffer(content) { | ||
return newStdString(String.fromCharCode(...new Uint8Array(content))); | ||
} | ||
|
||
function newStdString(content) { | ||
const size = content.length; | ||
if (size >= 0x17) { | ||
const capacity = 2 ** Math.ceil(Math.log2(size + 1)); | ||
const buffer = malloc(capacity); | ||
buffer.writeUtf8String(content); | ||
|
||
const str = malloc(Process.pointerSize * 3); | ||
str.writeULong(capacity | 0x1); | ||
str.add(Process.pointerSize).writeULong(size); | ||
str.add(Process.pointerSize * 2).writePointer(buffer); | ||
|
||
return { buffer, str }; | ||
} else { | ||
const str = malloc(size + 2); | ||
str.writeU8(size * 2); | ||
str.add(1).writeUtf8String(content); | ||
str.add(size + 1).writeU8(0); | ||
|
||
return { buffer: null, str }; | ||
} | ||
} | ||
|
||
function getStrFromStdString(content) { | ||
const mem = new NativePointer(content); | ||
const size = mem.readU8(); | ||
if ((size & 0x1) === 1) { | ||
const bufferSize = mem.add(Process.pointerSize).readULong(); | ||
const bufferPtr = mem.add(Process.pointerSize * 2).readPointer(); | ||
return bufferPtr.readUtf8String(bufferSize); | ||
} else { | ||
return mem.add(1).readUtf8String(size / 2); | ||
} | ||
} | ||
|
||
function getPersistentKeyASM( | ||
sessionCtrlInstance, | ||
adamIdStr, keyUriStr, | ||
keyFormatStr, keyFormatVerStr, | ||
serverUriStr, protocolTypeStr, | ||
fpsCertStr, persistentKeyPtr | ||
) { | ||
const impl = malloc(Process.pageSize); | ||
Memory.patchCode(impl, Process.pageSize, code => { | ||
const writer = new Arm64Writer(code, { pc: impl }); | ||
writer.putLdrRegAddress("x0", sessionCtrlInstance); | ||
writer.putLdrRegAddress("x1", adamIdStr.str); | ||
writer.putLdrRegAddress("x2", adamIdStr.str); | ||
writer.putLdrRegAddress("x3", keyUriStr.str); | ||
writer.putLdrRegAddress("x4", keyFormatStr.str); | ||
writer.putLdrRegAddress("x5", keyFormatVerStr.str); | ||
writer.putLdrRegAddress("x6", serverUriStr.str); | ||
writer.putLdrRegAddress("x7", protocolTypeStr.str); | ||
writer.putLdrRegAddress("x8", persistentKeyPtr); | ||
writer.putSubRegRegImm("sp", "sp", 0x10); | ||
writer.putLdrRegAddress("x9", fpsCertStr.str); | ||
writer.putStrRegRegOffset("x9", "sp", 0); | ||
writer.putCallAddressWithArguments(getPersistentKeyAddr, ["x0", "x1", "x2", "x3", "x4", "x5", "x6", "x7", "x8"]); | ||
writer.putAddRegRegImm("sp", "sp", 0x10); | ||
writer.flush(); | ||
}); | ||
|
||
const implFunc = new NativeFunction(impl, 'void', []); | ||
try { | ||
implFunc(); | ||
} catch (e) { } // Ignore errors | ||
dealloc(impl); | ||
} | ||
|
||
function decryptContextASM(sessionCtrlInstance, persistentKeyPtr, decryptedKeyPtr) { | ||
const impl = malloc(Process.pageSize); | ||
Memory.patchCode(impl, Process.pageSize, code => { | ||
const writer = new Arm64Writer(code, { pc: impl }); | ||
writer.putLdrRegAddress("x0", sessionCtrlInstance); | ||
writer.putLdrRegAddress("x1", persistentKeyPtr); | ||
writer.putLdrRegAddress("x8", decryptedKeyPtr); | ||
writer.putCallAddressWithArguments(decryptContextAddr, ["x0", "x1", "x8"]); | ||
writer.flush(); | ||
}); | ||
|
||
const implFunc = new NativeFunction(impl, 'void', []); | ||
try { | ||
implFunc(); | ||
} catch (e) { } // Ignore errors | ||
dealloc(impl); | ||
} | ||
|
||
function getKdContext(adamId, uri) { | ||
const uriStr = String.fromCharCode(...new Uint8Array(uri)); | ||
if (kdContextMap.has(uriStr)) { | ||
return kdContextMap.get(uriStr); | ||
} | ||
|
||
const adamIdStr = newStdStringFromArrayBuffer(adamId); | ||
const keyUriStr = newStdStringFromArrayBuffer(uri); | ||
const keyFormatStr = newStdString("com.apple.streamingkeydelivery"); | ||
const keyFormatVerStr = newStdString("1"); | ||
const serverUriStr = newStdString("https://play.itunes.apple.com/WebObjects/MZPlay.woa/music/fps"); | ||
const protocolTypeStr = newStdString("simplified"); | ||
const fpsCertStr = newStdString(fairplayCert); | ||
|
||
const persistentKeyPtr = malloc(Process.pointerSize * 2); | ||
getPersistentKeyASM( | ||
sessionCtrlInstance, | ||
adamIdStr, keyUriStr, | ||
keyFormatStr, keyFormatVerStr, | ||
serverUriStr, protocolTypeStr, | ||
fpsCertStr, persistentKeyPtr | ||
); | ||
|
||
const decryptedKeyPtr = malloc(Process.pointerSize * 2); | ||
decryptContextASM(sessionCtrlInstance, persistentKeyPtr.readPointer(), decryptedKeyPtr); | ||
const kdContext = decryptedKeyPtr.readPointer().add(0x18).readPointer(); | ||
This comment has been minimized.
Sorry, something went wrong. |
||
|
||
if (kdContext.isNull()) { | ||
console.error("kdContext is null"); | ||
return null; | ||
} | ||
|
||
kdContextMap.set(uriStr, kdContext); | ||
return kdContext; | ||
} | ||
|
||
async function getM3U8Url(adamId) { | ||
return new Promise((resolve) => { | ||
Java.perform(() => { | ||
try { | ||
const SVPlaybackLeaseManagerProxy = Java.use("com.apple.android.music.playback.SVPlaybackLeaseManagerProxy"); | ||
const MediaAssetInfo = SVPlaybackLeaseManagerProxy.requestAsset(parseInt(adamId), "", ["HLS"], false); | ||
resolve(MediaAssetInfo ? MediaAssetInfo.getDownloadUrl() : null); | ||
} catch (e) { | ||
console.error("Error calling requestAsset:", e); | ||
resolve(null); | ||
} | ||
}); | ||
}); | ||
} | ||
|
||
async function getM3U8UrlFromDownload(adamId) { | ||
return new Promise((resolve) => { | ||
Java.perform(() => { | ||
const N = Java.use("com.apple.android.storeservices.v2.N"); | ||
const data = N.a().j(); | ||
const PurchaseRequestPtr = Java.use("com.apple.android.storeservices.javanative.account.PurchaseRequest$PurchaseRequestPtr"); | ||
|
||
const request = Java.cast(data, Java.use("com.apple.android.storeservices.storeclient.g")); | ||
const create = PurchaseRequestPtr.create(request.n.value); | ||
create.get().setProcessDialogActions(true); | ||
create.get().setURLBagKey("subDownload"); | ||
create.get().setBuyParameters(`salableAdamId=${adamId}&price=0&pricingParameters=SUBS&productType=S`); | ||
try { | ||
create.get().run(); | ||
const response = create.get().getResponse(); | ||
if (response.get().getError().get() == null) { | ||
const item = response.get().getItems().get(0); | ||
const assets = item.get().getAssets(); | ||
resolve(assets.get(assets.size() - 1).get().getURL()); | ||
} else { | ||
console.error("Download failed", response.get().getError().get().errorCode()); | ||
resolve(null); | ||
} | ||
} catch (error) { | ||
console.error("Error during download", error); | ||
resolve(null); | ||
} | ||
}); | ||
}); | ||
} | ||
|
||
async function handleDecryptionConnection(socket) { | ||
try { | ||
while (true) { | ||
const adamIdSize = (await socket.input.readAll(1)).unwrap().readU8(); | ||
if (adamIdSize === 0) break; | ||
|
||
const adamId = await socket.input.readAll(adamIdSize); | ||
const uriSize = (await socket.input.readAll(1)).unwrap().readU8(); | ||
const uri = await socket.input.readAll(uriSize); | ||
|
||
const kdContext = getKdContext(adamId, uri); | ||
if (!kdContext) { | ||
console.error("Failed to get kdContext"); | ||
break; | ||
} | ||
|
||
|
||
while (true) { | ||
const sizeBuffer = await socket.input.readAll(4); | ||
if (sizeBuffer.byteLength === 0) break; | ||
const size = sizeBuffer.unwrap().readU32(); | ||
if (size === 0) break; | ||
|
||
const sample = await socket.input.readAll(size); | ||
const sampleUnwrapped = sample.unwrap(); | ||
decryptSample(kdContext.readPointer(), 5, sampleUnwrapped, sampleUnwrapped, sample.byteLength); | ||
await socket.output.writeAll(sample); | ||
} | ||
} | ||
} catch (e) { | ||
console.error("Connection handling error:", e); | ||
console.error(e.stack); | ||
} finally { | ||
await socket.close(); | ||
} | ||
} | ||
|
||
|
||
async function handleM3U8Connection(socket) { | ||
try { | ||
const adamIdSizeBuffer = await socket.input.readAll(1); | ||
if (adamIdSizeBuffer.byteLength === 0) { | ||
console.warn("Empty adamId size received"); | ||
return; | ||
} | ||
|
||
const adamIdSize = adamIdSizeBuffer.unwrap().readU8(); | ||
if (adamIdSize > 0) { | ||
const adamIdBuffer = await socket.input.readAll(adamIdSize); | ||
const adamId = String.fromCharCode(...new Uint8Array(adamIdBuffer)); | ||
|
||
let m3u8Url = await getM3U8Url(adamId); | ||
if (!m3u8Url) { | ||
m3u8Url = await getM3U8UrlFromDownload(adamId); | ||
} | ||
|
||
if (m3u8Url) { | ||
const m3u8Array = new TextEncoder().encode(m3u8Url + "\n"); | ||
await socket.output.writeAll(m3u8Array); | ||
} else { | ||
console.error("Failed to retrieve M3U8 URL"); | ||
} | ||
} | ||
} catch (e) { | ||
console.error("M3U8 connection error:", e); | ||
console.error(e.stack); | ||
} finally { | ||
await socket.close(); | ||
} | ||
} | ||
|
||
function initializeFridaFunctions(androidappmusic) { | ||
// Utility functions | ||
global.malloc = new NativeFunction( | ||
Module.findExportByName(null, "_Znwm"), | ||
'pointer', | ||
['ulong'] | ||
); | ||
global.dealloc = new NativeFunction( | ||
Module.findExportByName(null, "_ZdlPv"), | ||
'void', | ||
['pointer'] | ||
); | ||
|
||
// Apple Music specific functions | ||
global.sessionCtrlInstance = new NativeFunction( | ||
androidappmusic.getExportByName("_ZN21SVFootHillSessionCtrl8instanceEv"), | ||
'pointer', | ||
[] | ||
)(); | ||
|
||
|
||
global.getPersistentKeyAddr = androidappmusic.getExportByName("_ZN21SVFootHillSessionCtrl16getPersistentKeyERKNSt6__ndk112basic_stringIcNS0_11char_traitsIcEENS0_9allocatorIcEEEES8_S8_S8_S8_S8_S8_S8_"); | ||
global.decryptContextAddr = androidappmusic.getExportByName("_ZN21SVFootHillSessionCtrl14decryptContextERKNSt6__ndk112basic_stringIcNS0_11char_traitsIcEENS0_9allocatorIcEEEERKN11SVDecryptor15SVDecryptorTypeERKb"); | ||
|
||
global.decryptSample = new NativeFunction( | ||
androidappmusic.getExportByName("NfcRKVnxuKZy04KWbdFu71Ou"), | ||
'ulong', | ||
['pointer', 'uint', 'pointer', 'pointer', 'size_t'] | ||
); | ||
} | ||
|
||
function startListeners() { | ||
Socket.listen({ family: "ipv4", port: 20020 }) | ||
.then(async (listener) => { | ||
while (true) { | ||
await handleM3U8Connection(await listener.accept()); | ||
} | ||
}) | ||
.catch(console.error); | ||
|
||
Socket.listen({ family: "ipv4", port: 10020 }) | ||
.then(async (listener) => { | ||
while (true) { | ||
await handleDecryptionConnection(await listener.accept()); | ||
} | ||
}) | ||
.catch(console.error); | ||
} | ||
|
||
function waitForModule() { | ||
const moduleName = "libandroidappmusic.so"; | ||
try { | ||
const androidappmusic = Process.getModuleByName(moduleName); | ||
initializeFridaFunctions(androidappmusic); | ||
startListeners(); | ||
console.log(`Module ${moduleName} loaded successfully and listeners started`); | ||
} catch (e) { | ||
console.log(`Module ${moduleName} not loaded yet, waiting...`); | ||
setTimeout(waitForModule, 1000); | ||
} | ||
} | ||
|
||
waitForModule(); |
Hi @zhaarey
I got an error when it's decrypting files on macos silicon m1. Everything worked fine on Windows except mac arm chip. Could you please have a look? Thanks.
Repo steps:
Expect behavior:
Files decrypt successfully.
Actual behavior:
Throw exception in agent-arm64.js
Environment: