Comments (9)
Hi.
Thank you for your advice. I can implement this. But can you re-open this issue on the audible-cli repo?
Thank you very much!
from audible.
Sure, np. I really like what you've done.
P.S. I wrote my own aaxc decryp that doesn't use ffmpeg if you want.
from audible.
Decrypting aaxc without ffmpeg sounds really fantastic. Not everyone has a patched ffmpeg bin with aaxc patch applied. Have you realized that in pure Python. I would be very greatful if you send me your code.
P.S.: If you don’t want to make it public, you can send me a pn to [email protected]!!!
from audible.
I've got a working version in Java, I think I wrote a proof of concept in Python first but I'm having trouble finding it. Give me a few days to rewrite it in Python.
aax[c]? is at the file structure level mp4, you can feed it through any mp4 parser. The only difference is they change some tag names, encrypted the samples and put in some extra padding in the sample section. Crypto wise, it's decent, they've done everything right (including leaving themselves space to completely rework how the crypto is done). As to the complexity of the code, it's pretty simple. More code is spent doing tag renaming then anything else.
I wouldn't release my code but ffmpeg and yourself have let the cat out of the bag so... it's moot now.
from audible.
You are welcome to use this as you see fit as long as it isn't used for commercial use or violating the rights of others.
It doesn't meet Python naming conventions (I can't be bothered) and you are welcome to clean it up. You should have seen the Java code I translated this from. It's super ugly. I had a lot of fun writing both this and that.
How it works:
It walks the mp4 atom tree, replacing atom ids and decrypting samples without changing the file size or layout. It does this in a single pass and without saving any state as it goes. It's memory footprint consequently should be quite small.
Wait... what? Doesn't Audible add all this extra stuff to the file?
MP4 parsers don't care about extra data or unrecognizable atoms. Additionally the crypto used doesn't change the size of the samples.
If you want to learn more about the mp4 format I recommend getting Bento4, it's a really easy mp4 toolkit that isn't the mess that ffmpeg is. But I digress.
Don't you need to jump around the file from the sample tables into the mdat section to perform decryption? How is it that you don't have a sample table or file seeking?
Audible violated the mp4 standard and put extra metadata in the mdat section that makes it possible to find and decrypt the samples without needing the sample tables.
Areas for Improvement
- atom ids could be treated as bytearray literals. This would eliminate the need to use struct to extract them. The performance gain would be minimal.
- There is a status method for reporting status. It currently does nothing. Some status reporting to the user would be good.
Limitations
- There is no good way to change the chapter titles, as that requires replotting the mdat section and the sample tables. Might as well use ffmpeg for chapter title updates.
- If audible removes the extra metadata in the mdat section, this script will break. Fixing it would probably double or triple the amount of code.
- This will only decrypt books you have vouchers for and it cannot be made to decrypt books without the vouchers.
import struct
from Crypto.Cipher import AES
class Translator:
fshort = (">h", 2)
fint = (">i", 4)
flong = (">q", 8)
def __init__(self, size = None):
self.buf = bytearray(size if size != None else 4096)
self.pos = 0
self.wpos = 0
def reset(self) :
self.pos = 0
self.wpos = 0
def position(self): return self.pos
def getShort(self): return self.getOne(self.fshort)
def getInt(self) : return self.getOne(self.fint)
def getLong(self) : return self.getOne(self.flong)
def putInt(self, position, value) : self.putOne(self.fint, position, value)
def getOne(self, format) :
r = struct.unpack_from(format[0], self.buf, self.pos)[0]
self.pos = self.pos + format[1]
return r
def putOne(self, format, position, value) :
struct.pack_into(format[0], self.buf, position, value)
def readOne(self, inStream, format):
length = format[1]
self.buf[self.wpos : self.wpos + length] = inStream.read(length)
r = struct.unpack_from(format[0], self.buf, self.pos)[0]
self.wpos = self.wpos + length
self.pos = self.pos + length
return r
def readInto(self, inStream, length) -> int:
self.buf[self.wpos : self.wpos + length] = inStream.read(length)
self.wpos = self.wpos + length
return length
def readCount(self) -> int: return self.wpos
def write(self, *outs) -> int:
if self.wpos > 0:
data = self.buf if self.wpos == len(self.buf) else self.buf[0 : self.wpos]#fuck you python and your write function that can't sublist!
for out in outs:
out.write(data)
return self.wpos
return 0
def readInt(self, inStream):
return self.readOne(inStream, self.fint)
def readLong(self, inStream):
return self.readOne(inStream, self.flong)
def skipInt(self): self.skip(self.fint[1])
def skipLong(self): self.skip(self.flong[1])
def skip(self, length): self.pos = self.pos + length
def readAtomSize(self, inStream):
atomLength = self.readInt(inStream)
if(atomLength == 1): #64 bit atom!
atomLength = translator.readLong(inStream)
return atomLength
def zero(self, start = 0, end = None):
if end == None:
end = wpos
for i in range(start, end):
self.buf[i] = 0
def write_and_reset(self, *outs) -> int:
r = self.write(*outs)
self.reset()
return r
class AaxDecrypter:
filetypes = {6:"html", 7:"xml", 12:"gif", 13:"jpg", 14:"png", 15:"url", 27:"bmp"}
def __init__(self, session, key, iv, inpath, outpath):
self.session = session
self.key = bytes.fromhex(key)
self.iv = bytes.fromhex(iv)
self.source = inpath
self.dest = outpath
self.filesize = inpath.stat().st_size
def walk_ilst(self, translator, inStream, outStream, endPosition): #cover extractor
startPosition = inStream.tell()
while inStream.tell() < endPosition :
translator.reset()
self.status(inStream.tell(), self.filesize)
atomStart = inStream.tell()
atomLength = translator.readAtomSize(inStream)
atomEnd = atomStart + atomLength
atom = translator.readInt(inStream)
remaining = atomLength - translator.write_and_reset(outStream)
if(atom == 0x636F7672): #covr
#Going to assume ONE data atom per item.
dataLength = translator.readAtomSize(inStream)
translator.readInto(inStream, 12)
translator.skipInt() #data
type = translator.getInt() #type
translator.skipInt() #zero?
remaining = remaining - translator.write_and_reset(outStream)
if type in self.filetypes:
postfix = self.filetypes[type]
with self.dest.with_suffix(".embedded-cover." + postfix).open('wb') as cover:
remaining = remaining - self.copy(inStream, remaining, outStream, cover)
if(remaining > 0):
walked = False
self.copy(inStream, remaining, outStream)
self.checkPosition(inStream, outStream, atomEnd)
self.status(inStream.tell(), self.filesize)
return endPosition - startPosition
def walk_mdat(self, translator, inStream, outStream, endPosition):#samples
startPosition = inStream.tell()
#It's illegal for mdat to contain atoms... but that didn't stop Audible! Not that any parsers care.
while inStream.tell() < endPosition :
self.status(inStream.tell(), self.filesize)
#read an atom length.
atomStart = inStream.tell()
translator.reset()
atomLength = translator.readAtomSize(inStream)
atomTypePosition = translator.position()
atomType = translator.readInt(inStream)
#after the atom type comes 5 additional fields describing the data.
#We only care about the last two.
translator.readInto(inStream, 20)
translator.skipInt()#time in ms
translator.skipInt()#first block index
translator.skipInt()#trak number
bs = translator.getInt()#total size of all blocks
bc = translator.getInt()# number of blocks
atomEnd = atomStart + atomLength + bs
#next come the atom specific fields
if(atomType == 0x61617664) : #aavd has a list of sample sizes and then the samples.
translator.putInt(atomTypePosition, 0x6d703461) #mp4a
translator.readInto(inStream, bc * 4)
translator.write(outStream)
for i in range(bc):
self.status(inStream.tell(), self.filesize)
sampleLength = translator.getInt()
cipher = AES.new(self.key, AES.MODE_CBC, iv=self.iv)#has to be reset every go round.
remaining = sampleLength - outStream.write(cipher.decrypt(inStream.read(sampleLength & 0xFFFFFFF0)))
#fun fact, the last few bytes of each sample aren't encrypted!
if remaining > 0 : self.copy(inStream, remaining, outStream)
#there is no point in actually parsing this,
#we would need to rebuild the sample tables if we wanted to modify it.
#elif atomType == 0x74657874: #text
# translator.readInto(inStream, bc * 2)
# translator.write(outStream)
# for i in range(bc):
# sampleLength = translator.getShort()
# t2 = Translator(sampleLength * 2)
# t2.readInto(inStream, sampleLength)
# t2.getString(sampleLength)
# before = t2.readCount()
# encdSize = t2.readAtomSize(inStream)#encd atom size
# t2.readInto(inStream, encdSize + before - translator.readCount())
# t2.write(outStream)
# translator.reset()
else:
len = translator.write_and_reset(outStream)
self.copy(inStream, atomLength + bs - len, outStream)
translator.reset()
self.checkPosition(inStream, outStream, atomEnd)
return endPosition - startPosition
def walk_atoms(self, translator, inStream, outStream, endPosition):#everything
startPosition = inStream.tell()
while inStream.tell() < endPosition :
self.status(inStream.tell(), self.filesize)
#read an atom length.
translator.reset()
atomStart = inStream.tell()
atomLength = translator.readAtomSize(inStream);
atomEnd = atomStart + atomLength
ap = translator.position()
atom = translator.readInt(inStream)
remaining = atomLength
if atom == 0x66747970:#ftyp-none
remaining = remaining - translator.write_and_reset(outStream)
len = translator.readInto(inStream, remaining)
translator.putInt(0, 0x4D344120) #"M4A "
translator.putInt(4, 0x00000200) #version 2.0?
translator.putInt(8, 0x69736F32) #"iso2"
translator.putInt(12, 0x4D344220) #"M4B "
translator.putInt(16, 0x6D703432) #"mp42"
translator.putInt(20, 0x69736F6D) #"isom"
translator.zero(24, len)
remaining = remaining - translator.write_and_reset(outStream)
elif atom == 0x696C7374: #ilst-0
remaining = remaining - translator.write_and_reset(outStream)
remaining = remaining - self.walk_ilst(translator, inStream, outStream, atomEnd)
elif atom == 0x6d6f6f76 \
or atom == 0x7472616b \
or atom == 0x6d646961 \
or atom == 0x6d696e66 \
or atom == 0x7374626c \
or atom == 0x75647461: #moov-0, trak-0, mdia-0, minf-0, stbl-0, udta-0
remaining = remaining - translator.write_and_reset(outStream)
remaining = remaining - self.walk_atoms(translator, inStream, outStream, atomEnd)
elif atom == 0x6D657461: #meta-4
translator.readInto(inStream, 4)
remaining = remaining - translator.write_and_reset(outStream)
remaining = remaining - self.walk_atoms(translator, inStream, outStream, atomEnd)
elif atom == 0x73747364: #stsd-8
translator.readInto(inStream, 8)
remaining = remaining - translator.write_and_reset(outStream)
remaining = remaining - self.walk_atoms(translator, inStream, outStream, atomEnd)
elif atom == 0x6d646174: #mdat-none
remaining = remaining - translator.write_and_reset(outStream)
remaining = remaining - self.walk_mdat(translator, inStream, outStream, atomEnd)
elif atom == 0x61617664: #aavd-variable
translator.putInt(ap, 0x6d703461) #mp4a
remaining = remaining - translator.write_and_reset(outStream)
self.copy(inStream, remaining, outStream) #don't care about the children.
else:
remaining = remaining - translator.write_and_reset(outStream)
self.copy(inStream, remaining, outStream) #don't care about the children.
self.checkPosition(inStream, outStream, atomEnd)
self.status(inStream.tell(), self.filesize)
return endPosition - startPosition
def status(self, position, filesize):
None
def copy(self, inStream, length, *outs) -> int :
remaining = length
while remaining > 0:
remaining = remaining - self.write(inStream.read(min(remaining, 4096)), *outs)
return length
def write(self, buf, *outs) -> int :
for out in outs:
out.write(buf)
return len(buf)
def checkPosition(self, inStream, outStream, position):
ip = inStream.tell()
op = outStream.tell()
if ip != op or ip != position:
secho("IP: %d\tOP: %d\tP: %d" % (ip, op, position))
def decrypt_local(inpath, outpath, key, iv, session):
with inpath.open('rb') as infile:
with outpath.open('wb') as outfile:
decrypter = AaxDecrypter(session, key, iv, inpath, outpath)
decrypter.walk_atoms(Translator(), infile, outfile, decrypter.filesize)
from audible.
That’s really fantastic. Thank you for your hard work! Very clever! I will try it out as fast as possible and will report here.
P.S.: I‘m a single developer without any company in the background. I don’t want to make money with my projects, I only want to fill my sparetime. And writing code is my hobby.
from audible.
@BlindWanderer
Hi. Can you help me on this issue?
from audible.
This issue has not been updated for a while and will be closed soon.
from audible.
This issue has automatically been closed due to no activities.
from audible.
Related Issues (20)
- 错误提示 HOT 19
- Quality is set to `Extreme` even set to `high` HOT 1
- TEST: New device registration method HOT 4
- Extract Audible Bookmark Notes HOT 3
- Issue getting activation bytes from auth server HOT 9
- Switch to poetry, implement CI+CD, remove Python version <3.8
- Lift upper version restriction on `httpx`. HOT 3
- Is there any way to get a list of books in a series? HOT 1
- Amazon authentication code not handled properly HOT 2
- I need some help with metadata HOT 2
- Audible brazil HOT 11
- Brazil Account Can not login by external_login HOT 3
- RecursionError introduced in 0.9.0 in some cases HOT 3
- API doc: Only High and Normal is allowed as quality
- encryptedPwd HOT 5
- How does the BestSellers sort_by option in the APIs work? HOT 3
- Where does the file get stored with to_file() HOT 5
- Can't filter plus catalog books from 'catalog/products' HOT 9
- state token for library requests HOT 2
- response_callback no longer works properly with the endpoint: https://cde-ta-g7g.amazon.com/FionaCDEServiceEngine/sidecar HOT 5
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from audible.