TL;DR Link to heading
Library lending apps hand out .acsm files, which normally need Adobe Digital Editions to turn into a readable epub. ADE doesn’t run on Apple Silicon macOS. I built libgourou from source instead, activated it against my Adobe ID, and downloaded the loan with its acsmdownloader tool. Copying the encrypted epub straight to the Kobo didn’t work: newer Kobo firmware dropped Adobe DRM support entirely, so the device ignores ADEPT-encrypted files with no error and no log line. libgourou also ships adept_remove, a DRM-removal utility, for exactly this kind of dead end. Getting the book to look right in the library then needed a second fix, because Nickel never generates cover thumbnails for sideloaded books.
Starting point Link to heading
An .acsm file from a library app, a loan expiring in a few hours, and a Kobo on the desk over USB. The usual route is Adobe Digital Editions: open the .acsm, ADE talks to Adobe’s servers, fulfils the loan, and syncs the encrypted epub to any device it recognises. ADE 4.5 doesn’t install cleanly on Apple Silicon macOS, and I wasn’t going to spin up a VM for one library book.
Building an ADE replacement Link to heading
libgourou reimplements the ADEPT protocol in C++, without Adobe’s blessing but interoperably enough to talk to their activation servers. It’s not packaged for Homebrew, so:
git clone --recursive https://forge.soutade.fr/soutade/libgourou.git
brew install pugixml openssl@3 libzip
The bundled Makefile assumes GNU ar; macOS ships BSD ar, which doesn’t understand the --thin flag it passes. Skip the archive step and build the utility binaries directly against the compiled objects instead; that sidesteps it:
c++ -I./include -I$(brew --prefix pugixml)/include \
-I./lib/updfparser/include -I$(brew --prefix openssl@3)/include \
-I$(brew --prefix libzip)/include -I./utils -O2 \
utils/acsmdownloader.cpp utils/utils_common.cpp utils/drmprocessorclientimpl.cpp \
obj/*.o lib/updfparser/obj/*.o \
-L$(brew --prefix pugixml)/lib -lpugixml \
-L$(brew --prefix openssl@3)/lib -lssl -lcrypto \
-L$(brew --prefix libzip)/lib -lzip -lcurl -lz \
-o acsmdownloader
Same pattern for adept_activate and, later, adept_remove.
Activating and downloading Link to heading
./adept_activate -u <adobe-id-email> -p <password> -O ./.adept
./acsmdownloader -D ./.adept -O ./out URLLink.acsm
adept_activate registers a new virtual device against an Adobe ID, the same way ADE does on first run, and writes device.xml, activation.xml, and devicesalt into the output directory. acsmdownloader uses those to fulfil the loan and writes out an ADEPT-encrypted epub, indistinguishable in principle from one that ADE would have produced.
The device swap that didn’t work Link to heading
The Kobo’s own .adobe-digital-editions/device.xml only had a device identity, no activation.xml, meaning it had never been signed into an Adobe ID. The documented libgourou workflow: overwrite the Kobo’s ADE folder with the files from a matching activation, so the device’s own decryption engine has the right key material.
cp .adept/device.xml .adept/devicesalt .adept/activation.xml \
/Volumes/KOBOeReader/.adobe-digital-editions/
Copied the epub across, ejected, replugged. Nothing. No error dialog. No entry in KoboReader.sqlite. No log anywhere on the device. The file just sat there as if Nickel had never scanned it.
What was going on Link to heading
The device’s firmware was 4.45.23697, a 2024-era build. Kobo pulled native Adobe ADEPT decryption from firmware somewhere around version 4.32, moving library loans over to OverDrive’s own format instead. On that firmware, Nickel doesn’t attempt to decrypt an ADEPT epub and doesn’t record that it saw one either. The activation-file swap that used to work for sideloading library books has nothing left to swap into: the code path that would use it is gone.
There’s no config flag or file to restore that support. Downgrading firmware was the only other route, and not one worth the risk to the device for a single library book.
The other tool in the box Link to heading
adept_remove was already sitting in libgourou’s utils/ directory, just not built. It uses the same DRMProcessor::removeDRM API that acsmdownloader calls internally when it decrypts a book for the reader UI, exposed as a small standalone tool. Given the same activation files used for the download, it decrypts an epub and deletes META-INF/rights.xml and META-INF/encryption.xml:
./adept_remove -D ./.adept -o "book.epub" "book-encrypted.epub"
Worth knowing it exists once the firmware itself has stopped speaking ADEPT: a file with no DRM left in it doesn’t need a device that can decrypt DRM. It also means the loan’s automatic expiry goes with it, since Adobe’s licence server enforces the return date and the file itself carries no expiry of its own — the usual trade-off with any DRM-removal tool.
The cover problem Link to heading
A plain epub imports fine, but two cover-related things went wrong that have nothing to do with DRM.
First, the full-page cover rendered cropped at the edges. The cover page had an inline style of height:99vh; width:auto, and Kobo’s rendering engine handles vh units poorly enough that the image overflows the page width with nothing capping it. Replacing that with a plain fit works:
<img
src="images/cover.jpg"
style="max-width:100%; max-height:100%; width:auto; height:auto;"
/>
The source image was also small at 500×767, which doesn’t help on a 300ppi screen. Upscaling it before repackaging (sips -Z 1600 cover.jpg) made a visible difference.
Repack with the mimetype entry first and uncompressed, or the device rejects the file:
zip -X -q ../fixed.epub mimetype
zip -X -rq -D ../fixed.epub . -x mimetype -x "._*" -x ".DS_Store"
Nickel doesn’t make thumbnails for sideloaded books Link to heading
The second problem was the library thumbnail, which stayed stubbornly tiny. Deleting the row from KoboReader.sqlite to force a fresh import didn’t fix it, because the device never generates thumbnails for sideloaded books in the first place: it falls back to rendering something small on the fly.
Kobo caches covers in .kobo-images, sharded into two directory levels by a Qt3-era string hash of the book’s ImageId:
def qhash(data: bytes) -> int:
h = 0
for b in data:
h = (h << 4) + b
h ^= (h & 0xF0000000) >> 23
h &= 0x0FFFFFFF
return h
# .kobo-images/<h & 0xFF>/<(h & 0xFF00) >> 8>/<ImageId> - <SUFFIX>.parsed
The .parsed files are ordinary JPEGs. Three suffixes matter: N3_FULL, N3_LIBRARY_FULL, and N3_LIBRARY_GRID.
The sizes are the part worth getting right. Kobo blits these at their native size without upscaling, so an undersized cached image renders as a tiny cover no matter what the epub contains. Rather than guess, read them off the covers the device generated for its own store books:
find /Volumes/KOBOeReader/.kobo-images -name "*N3_LIBRARY_GRID.parsed" \
| head -3 | xargs -I{} sips -g pixelWidth -g pixelHeight {}
On a Clara Colour that gave 530px tall for N3_LIBRARY_FULL and 223px for N3_LIBRARY_GRID, with the width following the cover’s aspect ratio. My first attempt used 479 and 201, taken from a documented Clara-family convention, and those render visibly small. Generating at the measured sizes fixed it.
One warning if you go looking for the reference values: don’t clear .kobo-images wholesale to force a rebuild. Nickel doesn’t regenerate on rescan, and for store books the covers only come back after a full sync re-downloads them.
Further reading Link to heading
- libgourou, the ADEPT implementation this depended on