eLibro
eLibro es una plataforma con un servicio que utilizan varias universidades que permite leer libros en formato digital. Se puede usar un navegador o también se puede descargar una aplicación para distintas plataformas, la misma esta hecha en Electron y como tal puede correr en casi cualquier cosa que tenga una pantalla. En este caso nos vamos a concentrar en la versión linux que esta empaquetada como AppImage.
Cuál es el problema?
La aplicación es tan mala que no se la puede calificar, solo por nombrar algunas características:
- Se cuelga al bajar archivos, obligando a “devolver” el libro y obliga a intentar descargalo nuevamente.
- Todas estas transacciones tienen limites diarios/semanales/mensuales.
- La interfaz hace lo imposible para arruinarte el día.
- 50/50 que al intentar leer un libro se cuelgue.
En general podemos describir a la aplicación como el DRM del servicio.
Cómo funciona?
Después de decompilar la AppImage y descomprimir el archivo .asar podemos empezar a leer el código, obviamente esta minificado y ofuscado pero increiblemente esta comentando en casi todas sus funciones (mayormente en espanglish). Al hacer el código un poco más legible podemos ver lo… pobre que es.
En términos generales su funcionamiento es el siguiente:
- Descarga el .pdf (protegido con contraseña) de un servidor AWS.
- Encripta el .pdf (con otra contraseña) utilizando una funcion en
main-es2015.e82f88d5d315bf0f96a3.jsy guarda una copia junto con otros datos. - Cada vez que se desea leer el libro lo descomprime/desencripta.
Analizando el código encontramos esto:
static decrypt(e, t) {
const n = atob(e).split(";"),
r = st.enc.Base64.parse(n[0]),
i = st.enc.Base64.parse(n[1]),
o = [...atob(n[2])];
i.sigBytes = 16, i.clamp(), r.words.splice(0, 4), r.sigBytes -= 16;
const s = st.AES.decrypt({
ciphertext: r
}, st.enc.Utf8.parse(t), {
iv: i
}).toString(st.enc.Utf8);
let a = "",
c = 0;
for (let l = 0; l < s.length; l++) a += s[l], (l + 1) % 3 == 0 && 0 !== l && (c >= o.length && (c = 0), a += o[c], c += 1);
return a
}
El valor de retorno de la función parece prometedor, podemos modificar el código para que nos devuelva las variables en el log:
static decrypt(e, t) {
const n = atob(e).split(";"),
r = st.enc.Base64.parse(n[0]),
i = st.enc.Base64.parse(n[1]),
o = [...atob(n[2])];
i.sigBytes = 16, i.clamp(), r.words.splice(0, 4), r.sigBytes -= 16;
const s = st.AES.decrypt({
ciphertext: r
}, st.enc.Utf8.parse(t), {
iv: i
}).toString(st.enc.Utf8);
let a = "",
c = 0;
for (let l = 0; l < s.length; l++) a += s[l], (l + 1) % 3 == 0 && 0 !== l && (c >= o.length && (c = 0), a += o[c], c += 1);
console.log("ΓΓΓ EXTENDED INFO ΓΓΓ (decrypt function) ciphertext: ", JSON.stringify(r));
console.log("ΓΓΓ EXTENDED INFO ΓΓΓ (decrypt function) iv value: ", JSON.stringify(i));
console.log("ΓΓΓ EXTENDED INFO ΓΓΓ (decrypt function) e value: ", e);
console.log("ΓΓΓ EXTENDED INFO ΓΓΓ (decrypt function) t value: ", t);
console.log("ΓΓΓ EXTENDED INFO ΓΓΓ (decrypt function) possible PDF password: ", a);
return a // <--- Interesante!
}
Después reconstruimos la AppImage pero ahora la ejecutamos con el comando --enable-logging para tratar de entender mejor que es lo que está pasando.
Ya usando la aplicación elegimos un libro a leer obtenemos esto en el log:
[10615:0922/122455.850167:INFO:CONSOLE(12449)] "ΓΓΓ EXTENDED INFO ΓΓΓ (decrypt function) possible PDF password: t8m1tdb9ng91w8c.ycy1p0k02xa3ozd.q4q1oma1sxt0uzq.sqe4k4b7s101lnb9lp31p6r.bwv170k0vfo3n", source: capacitor-electron://-/main-es2015.e82f88d5d315bf0f96a3.js (12449)
Interesante, veamos si funciona. Los archivos .pdf descargados al ser procesados son guardados en el cache de la app, el directorio en linux es: .config/elibro-desktop-reader/Cache Probamos la clave que nos dió el log:
Para eliminar la clave del archivo y facilitar futuras lecturas podemos descargar pdftk y usarlo de esta forma: pdftk input.pdf input_pw laclave output output.pdf pero algo quizás un poco más útil podría ser buscar en el código la dirección URL de descarga del archivo .pdf y agregarla al log junto con la clave del archivo y quizás ya dejar el comando de pdfk listo para usar. Veamos:
// al inicio de la clase donde editamos la función decrypt
--snip
var ot = n("nV9S"),
st = n("NFKh"),
at = n("NLsH"),
ct = n("rPY6"),
lt = n("lJxs"),
ut = n("pLZG"),
dt = n("vKcw"),
ht = n("6Wv3"),
ft = n("vzb7"),
pt = n("Mky/"),
pdfURL = "", // <-- declaramos nuestra variable globalmente
gt = n("JX05");
--snip
// avanzamos un poco y encontramos la función downloadElibFile
--snip
}).pipe(Object(lt.a)(t => {
if (t.type === s.e.DownloadProgress) {
const r = Math.round(100 * t.loaded / t.total);
e.downloadUrlProgressPerc = r, n.borrowingDownloadProgress$.next(e)
}
return t
}), Object(ut.a)(e => e.type === s.e.Response), Object(lt.a)(e => e.body)).toPromise().catch(t => Object(o.a)(this, void 0, void 0, function*() {
console.error("xxx ERROR: fail downloading .elib file", t), e.errorInDownloadFile = !0, e.errorInDownloadFileMsg = JSON.stringify(t), yield p.save(e)
}));
console.log("::: INFO: starting to download elib file", e.downloadUrl, f);
console.log("ΓΓΓ EXTENDED INFO ΓΓΓ AMAZON PDF URL: ", e.downloadUrl, " & local filename (id): ", a); // <-- agregado mientras analizabamos el código
pdfURL = e.downloadUrl; // <--- guardamos la url del pdf
const m = new FileReader;
return g && (m.readAsDataURL(g), m.onloadend = () => Object(o.a)(this, void 0, void 0, function*() {
yield c.writeFile({
path: l,
--snip
// la función que nos devuelve todo lo que necesitamos queda de esta forma:
static decrypt(e, t) {
const n = atob(e).split(";"),
r = st.enc.Base64.parse(n[0]),
i = st.enc.Base64.parse(n[1]),
o = [...atob(n[2])];
i.sigBytes = 16, i.clamp(), r.words.splice(0, 4), r.sigBytes -= 16;
const s = st.AES.decrypt({
ciphertext: r
}, st.enc.Utf8.parse(t), {
iv: i
}).toString(st.enc.Utf8);
let a = "",
c = 0;
for (let l = 0; l < s.length; l++) a += s[l], (l + 1) % 3 == 0 && 0 !== l && (c >= o.length && (c = 0), a += o[c], c += 1);
console.log("ΓΓΓ EXTENDED INFO ΓΓΓ (decrypt function) ciphertext: ", JSON.stringify(r));
console.log("ΓΓΓ EXTENDED INFO ΓΓΓ (decrypt function) iv value: ", JSON.stringify(i));
console.log("ΓΓΓ EXTENDED INFO ΓΓΓ (decrypt function) e value: ", e);
console.log("ΓΓΓ EXTENDED INFO ΓΓΓ (decrypt function) t value: ", t);
console.log("ΓΓΓ EXTENDED INFO ΓΓΓ (decrypt function) possible PDF password: ", a);
console.log("***************************************************");
// vemos si la variable que creamos antes tiene algo para ofrecer...
if (pdfURL) {
// usando la URL obtenida nos quedamos solo con el nombre del archivo sin la extension
let filename = pdfURL.split('/').pop().slice(0, -4);
// el comando listo para desencriptar el pdf que nos devuelve el log
console.log("pdftk " + filename + ".pdf input_pw " + a + " output " + filename + "-decrypted.pdf"); // lo utilizamos donde descargamos el archivo .pdf
console.log("***************************************************");
}
return a
}
A todo esto lo más molesto de utilizar eLibro en linux fué que el framework de ionic por algún motivo no tenía el marco de la venta ni los botones para minimizar, maximizar y cerrar. Revisando un poco más el código:
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some Electron APIs can only be used after this event occurs.
electron_2.app.on('ready', () => {
myCapacitorApp.init();
const MainWindow = myCapacitorApp.getMainWindow();
MainWindow.webContents.on('dom-ready', () => {
MainWindow.hide();
setTimeout(() => {
MainWindow.show();
}, 100);
});
electron_2.app.setAppUserModelId('net.elibro.com');
electron_2.Menu.setApplicationMenu(null);
});
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some Electron APIs can only be used after this event occurs.
electron_2.app.on('ready', () => {
myCapacitorApp.init();
const MainWindow = myCapacitorApp.getMainWindow();
MainWindow.webContents.on('dom-ready', () => {
// quizas si funciona bien en otros escritorios, en mate-desktop sin este cambio no
//MainWindow.hide();
//setTimeout(() => {
MainWindow.show();
//}, 100);
});
electron_2.app.setAppUserModelId('net.elibro.com');
electron_2.Menu.setApplicationMenu(null);
});
Y por último algunas cositas interesantes que encontré navegando entre todos los archivos:
--snip
// quién no estuvo en esta situación en algún momento?
static b64toBlob(e, t = "image/png", n = 512) {
t = t || "", console.log("------------ inicio");
const r = atob(e),
i = [];
console.log("------------antes del for");
for (let o = 0; o < r.length; o += n) {
const e = r.slice(o, o + n),
t = new Array(e.length);
for (let n = 0; n < e.length; n++) t[n] = e.charCodeAt(n);
const s = new Uint8Array(t);
i.push(s)
}
console.log("------------despues del for");
return new Blob(i, {
type: t
})
}
--snip
--snip
} catch (e) {
console.log("::: ERROR: cannot create the directorys: " + n), console.log(e) // no inglish
}
--snip
--snip // gracias a dios la licencia es MIT
}, arguments)
}, t.pluginName = "PhotoViewer", t.plugin = "com-sarriaroman-photoviewer", t.pluginRef = "PhotoViewer", t.repo = "https://github.com/sarriaroman/photoviewer", t.platforms = ["Android", "iOS"], t.\u0275fac = function(e) {
return n(e || t)
--snip
--snip
return console.log("DATA FOR ENCRIPTION =====> (secureKey, secureIV, b64 - first50Chars)", t, i, e.slice(0, 50)), this.aes256.encrypt(t, i, e) // ¯\_(ツ)_/¯
--snip