|
|
|
@ -20,290 +20,298 @@ import javax.crypto.spec.SecretKeySpec
|
|
|
|
|
|
|
|
|
|
|
|
@MangaSourceParser("BATOTO", "Bato.To")
|
|
|
|
@MangaSourceParser("BATOTO", "Bato.To")
|
|
|
|
internal class BatoToParser(context: MangaLoaderContext) : PagedMangaParser(
|
|
|
|
internal class BatoToParser(context: MangaLoaderContext) : PagedMangaParser(
|
|
|
|
context = context,
|
|
|
|
context = context,
|
|
|
|
source = MangaSource.BATOTO,
|
|
|
|
source = MangaSource.BATOTO,
|
|
|
|
pageSize = 60,
|
|
|
|
pageSize = 60,
|
|
|
|
searchPageSize = 20,
|
|
|
|
searchPageSize = 20,
|
|
|
|
) {
|
|
|
|
) {
|
|
|
|
|
|
|
|
|
|
|
|
override val sortOrders: Set<SortOrder> = EnumSet.of(
|
|
|
|
override val sortOrders: Set<SortOrder> = EnumSet.of(
|
|
|
|
SortOrder.NEWEST,
|
|
|
|
SortOrder.NEWEST,
|
|
|
|
SortOrder.UPDATED,
|
|
|
|
SortOrder.UPDATED,
|
|
|
|
SortOrder.POPULARITY,
|
|
|
|
SortOrder.POPULARITY,
|
|
|
|
SortOrder.ALPHABETICAL,
|
|
|
|
SortOrder.ALPHABETICAL,
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
override val configKeyDomain = ConfigKey.Domain(
|
|
|
|
override val configKeyDomain = ConfigKey.Domain(
|
|
|
|
"bato.to",
|
|
|
|
"bato.to",
|
|
|
|
arrayOf("bato.to", "mto.to", "hto.to", "mangatoto.com", "battwo.com", "batotwo.com", "comiko.net", "batotoo.com"),
|
|
|
|
arrayOf(
|
|
|
|
)
|
|
|
|
"bato.to",
|
|
|
|
|
|
|
|
"mto.to",
|
|
|
|
|
|
|
|
"hto.to",
|
|
|
|
|
|
|
|
"mangatoto.com",
|
|
|
|
|
|
|
|
"battwo.com",
|
|
|
|
|
|
|
|
"batotwo.com",
|
|
|
|
|
|
|
|
"comiko.net",
|
|
|
|
|
|
|
|
"batotoo.com",
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
override suspend fun getListPage(
|
|
|
|
override suspend fun getListPage(
|
|
|
|
page: Int,
|
|
|
|
page: Int,
|
|
|
|
query: String?,
|
|
|
|
query: String?,
|
|
|
|
tags: Set<MangaTag>?,
|
|
|
|
tags: Set<MangaTag>?,
|
|
|
|
sortOrder: SortOrder,
|
|
|
|
sortOrder: SortOrder,
|
|
|
|
): List<Manga> {
|
|
|
|
): List<Manga> {
|
|
|
|
if (!query.isNullOrEmpty()) {
|
|
|
|
if (!query.isNullOrEmpty()) {
|
|
|
|
return search(page, query)
|
|
|
|
return search(page, query)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@Suppress("NON_EXHAUSTIVE_WHEN_STATEMENT")
|
|
|
|
@Suppress("NON_EXHAUSTIVE_WHEN_STATEMENT")
|
|
|
|
val url = buildString {
|
|
|
|
val url = buildString {
|
|
|
|
append("https://")
|
|
|
|
append("https://")
|
|
|
|
append(domain)
|
|
|
|
append(domain)
|
|
|
|
append("/browse?sort=")
|
|
|
|
append("/browse?sort=")
|
|
|
|
when (sortOrder) {
|
|
|
|
when (sortOrder) {
|
|
|
|
SortOrder.UPDATED,
|
|
|
|
SortOrder.UPDATED,
|
|
|
|
-> append("update.za")
|
|
|
|
-> append("update.za")
|
|
|
|
|
|
|
|
|
|
|
|
SortOrder.POPULARITY -> append("views_a.za")
|
|
|
|
SortOrder.POPULARITY -> append("views_a.za")
|
|
|
|
SortOrder.NEWEST -> append("create.za")
|
|
|
|
SortOrder.NEWEST -> append("create.za")
|
|
|
|
SortOrder.ALPHABETICAL -> append("title.az")
|
|
|
|
SortOrder.ALPHABETICAL -> append("title.az")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (!tags.isNullOrEmpty()) {
|
|
|
|
if (!tags.isNullOrEmpty()) {
|
|
|
|
append("&genres=")
|
|
|
|
append("&genres=")
|
|
|
|
appendAll(tags, ",") { it.key }
|
|
|
|
appendAll(tags, ",") { it.key }
|
|
|
|
}
|
|
|
|
}
|
|
|
|
append("&page=")
|
|
|
|
append("&page=")
|
|
|
|
append(page)
|
|
|
|
append(page)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return parseList(url, page)
|
|
|
|
return parseList(url, page)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
override suspend fun getDetails(manga: Manga): Manga {
|
|
|
|
override suspend fun getDetails(manga: Manga): Manga {
|
|
|
|
val root = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
|
|
|
|
val root = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
|
|
|
|
.requireElementById("mainer")
|
|
|
|
.requireElementById("mainer")
|
|
|
|
val details = root.selectFirstOrThrow(".detail-set")
|
|
|
|
val details = root.selectFirstOrThrow(".detail-set")
|
|
|
|
val attrs = details.selectFirst(".attr-main")?.select(".attr-item")?.associate {
|
|
|
|
val attrs = details.selectFirst(".attr-main")?.select(".attr-item")?.associate {
|
|
|
|
it.child(0).text().trim() to it.child(1)
|
|
|
|
it.child(0).text().trim() to it.child(1)
|
|
|
|
}.orEmpty()
|
|
|
|
}.orEmpty()
|
|
|
|
return manga.copy(
|
|
|
|
return manga.copy(
|
|
|
|
title = root.selectFirst("h3.item-title")?.text() ?: manga.title,
|
|
|
|
title = root.selectFirst("h3.item-title")?.text() ?: manga.title,
|
|
|
|
isNsfw = !root.selectFirst("alert")?.getElementsContainingOwnText("NSFW").isNullOrEmpty(),
|
|
|
|
isNsfw = !root.selectFirst("alert")?.getElementsContainingOwnText("NSFW").isNullOrEmpty(),
|
|
|
|
largeCoverUrl = details.selectFirst("img[src]")?.absUrl("src"),
|
|
|
|
largeCoverUrl = details.selectFirst("img[src]")?.absUrl("src"),
|
|
|
|
description = details.getElementById("limit-height-body-summary")
|
|
|
|
description = details.getElementById("limit-height-body-summary")
|
|
|
|
?.selectFirst(".limit-html")
|
|
|
|
?.selectFirst(".limit-html")
|
|
|
|
?.html(),
|
|
|
|
?.html(),
|
|
|
|
tags = manga.tags + attrs["Genres:"]?.parseTags().orEmpty(),
|
|
|
|
tags = manga.tags + attrs["Genres:"]?.parseTags().orEmpty(),
|
|
|
|
state = when (attrs["Release status:"]?.text()) {
|
|
|
|
state = when (attrs["Release status:"]?.text()) {
|
|
|
|
"Ongoing" -> MangaState.ONGOING
|
|
|
|
"Ongoing" -> MangaState.ONGOING
|
|
|
|
"Completed" -> MangaState.FINISHED
|
|
|
|
"Completed" -> MangaState.FINISHED
|
|
|
|
else -> manga.state
|
|
|
|
else -> manga.state
|
|
|
|
},
|
|
|
|
},
|
|
|
|
author = attrs["Authors:"]?.text()?.trim() ?: manga.author,
|
|
|
|
author = attrs["Authors:"]?.text()?.trim() ?: manga.author,
|
|
|
|
chapters = root.selectFirst(".episode-list")
|
|
|
|
chapters = root.selectFirst(".episode-list")
|
|
|
|
?.selectFirst(".main")
|
|
|
|
?.selectFirst(".main")
|
|
|
|
?.children()
|
|
|
|
?.children()
|
|
|
|
?.reversed()
|
|
|
|
?.reversed()
|
|
|
|
?.mapChapters { i, div ->
|
|
|
|
?.mapChapters { i, div ->
|
|
|
|
div.parseChapter(i)
|
|
|
|
div.parseChapter(i)
|
|
|
|
}.orEmpty(),
|
|
|
|
}.orEmpty(),
|
|
|
|
)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
|
|
|
|
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
|
|
|
|
val fullUrl = chapter.url.toAbsoluteUrl(domain)
|
|
|
|
val fullUrl = chapter.url.toAbsoluteUrl(domain)
|
|
|
|
val scripts = webClient.httpGet(fullUrl).parseHtml().select("script")
|
|
|
|
val scripts = webClient.httpGet(fullUrl).parseHtml().select("script")
|
|
|
|
for (script in scripts) {
|
|
|
|
for (script in scripts) {
|
|
|
|
val scriptSrc = script.html()
|
|
|
|
val scriptSrc = script.html()
|
|
|
|
val p = scriptSrc.indexOf("const imgHttpLis =")
|
|
|
|
val p = scriptSrc.indexOf("const imgHttpLis =")
|
|
|
|
if (p == -1) continue
|
|
|
|
if (p == -1) continue
|
|
|
|
val start = scriptSrc.indexOf('[', p)
|
|
|
|
val start = scriptSrc.indexOf('[', p)
|
|
|
|
val end = scriptSrc.indexOf(';', start)
|
|
|
|
val end = scriptSrc.indexOf(';', start)
|
|
|
|
if (start == -1 || end == -1) {
|
|
|
|
if (start == -1 || end == -1) {
|
|
|
|
continue
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
}
|
|
|
|
val images = JSONArray(scriptSrc.substring(start, end))
|
|
|
|
val images = JSONArray(scriptSrc.substring(start, end))
|
|
|
|
val batoPass = scriptSrc.substringBetweenFirst("batoPass =", ";")?.trim(' ', '"', '\n')
|
|
|
|
val batoPass = scriptSrc.substringBetweenFirst("batoPass =", ";")?.trim(' ', '"', '\n')
|
|
|
|
?: script.parseFailed("Cannot find batoPass")
|
|
|
|
?: script.parseFailed("Cannot find batoPass")
|
|
|
|
val batoWord = scriptSrc.substringBetweenFirst("batoWord =", ";")?.trim(' ', '"', '\n')
|
|
|
|
val batoWord = scriptSrc.substringBetweenFirst("batoWord =", ";")?.trim(' ', '"', '\n')
|
|
|
|
?: script.parseFailed("Cannot find batoWord")
|
|
|
|
?: script.parseFailed("Cannot find batoWord")
|
|
|
|
val password = context.evaluateJs(batoPass)?.removeSurrounding('"')
|
|
|
|
val password = context.evaluateJs(batoPass)?.removeSurrounding('"')
|
|
|
|
?: script.parseFailed("Cannot evaluate batoPass")
|
|
|
|
?: script.parseFailed("Cannot evaluate batoPass")
|
|
|
|
val args = JSONArray(decryptAES(batoWord, password))
|
|
|
|
val args = JSONArray(decryptAES(batoWord, password))
|
|
|
|
val result = ArrayList<MangaPage>(images.length())
|
|
|
|
val result = ArrayList<MangaPage>(images.length())
|
|
|
|
repeat(images.length()) { i ->
|
|
|
|
repeat(images.length()) { i ->
|
|
|
|
val url = images.getString(i)
|
|
|
|
val url = images.getString(i)
|
|
|
|
result += MangaPage(
|
|
|
|
result += MangaPage(
|
|
|
|
id = generateUid(url),
|
|
|
|
id = generateUid(url),
|
|
|
|
url = url + "?" + args.getString(i),
|
|
|
|
url = url + "?" + args.getString(i),
|
|
|
|
referer = fullUrl,
|
|
|
|
preview = null,
|
|
|
|
preview = null,
|
|
|
|
source = source,
|
|
|
|
source = source,
|
|
|
|
)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return result
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
}
|
|
|
|
throw ParseException("Cannot find images list", fullUrl)
|
|
|
|
throw ParseException("Cannot find images list", fullUrl)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
override suspend fun getTags(): Set<MangaTag> {
|
|
|
|
override suspend fun getTags(): Set<MangaTag> {
|
|
|
|
val scripts = webClient.httpGet(
|
|
|
|
val scripts = webClient.httpGet(
|
|
|
|
"https://${domain}/browse",
|
|
|
|
"https://${domain}/browse",
|
|
|
|
).parseHtml().selectOrThrow("script")
|
|
|
|
).parseHtml().selectOrThrow("script")
|
|
|
|
for (script in scripts) {
|
|
|
|
for (script in scripts) {
|
|
|
|
val genres = script.html().substringBetweenFirst("const _genres =", ";") ?: continue
|
|
|
|
val genres = script.html().substringBetweenFirst("const _genres =", ";") ?: continue
|
|
|
|
val jo = JSONObject(genres)
|
|
|
|
val jo = JSONObject(genres)
|
|
|
|
val result = ArraySet<MangaTag>(jo.length())
|
|
|
|
val result = ArraySet<MangaTag>(jo.length())
|
|
|
|
jo.keys().forEach { key ->
|
|
|
|
jo.keys().forEach { key ->
|
|
|
|
val item = jo.getJSONObject(key)
|
|
|
|
val item = jo.getJSONObject(key)
|
|
|
|
result += MangaTag(
|
|
|
|
result += MangaTag(
|
|
|
|
title = item.getString("text").toTitleCase(),
|
|
|
|
title = item.getString("text").toTitleCase(),
|
|
|
|
key = item.getString("file"),
|
|
|
|
key = item.getString("file"),
|
|
|
|
source = source,
|
|
|
|
source = source,
|
|
|
|
)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return result
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
}
|
|
|
|
throw ParseException("Cannot find gernes list", scripts[0].baseUri())
|
|
|
|
throw ParseException("Cannot find gernes list", scripts[0].baseUri())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private suspend fun search(page: Int, query: String): List<Manga> {
|
|
|
|
private suspend fun search(page: Int, query: String): List<Manga> {
|
|
|
|
val url = buildString {
|
|
|
|
val url = buildString {
|
|
|
|
append("https://")
|
|
|
|
append("https://")
|
|
|
|
append(domain)
|
|
|
|
append(domain)
|
|
|
|
append("/search?word=")
|
|
|
|
append("/search?word=")
|
|
|
|
append(query.replace(' ', '+'))
|
|
|
|
append(query.replace(' ', '+'))
|
|
|
|
append("&page=")
|
|
|
|
append("&page=")
|
|
|
|
append(page)
|
|
|
|
append(page)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return parseList(url, page)
|
|
|
|
return parseList(url, page)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private fun getActivePage(body: Element): Int = body.select("nav ul.pagination > li.page-item.active")
|
|
|
|
private fun getActivePage(body: Element): Int = body.select("nav ul.pagination > li.page-item.active")
|
|
|
|
.lastOrNull()
|
|
|
|
.lastOrNull()
|
|
|
|
?.text()
|
|
|
|
?.text()
|
|
|
|
?.toIntOrNull() ?: body.parseFailed("Cannot determine current page")
|
|
|
|
?.toIntOrNull() ?: body.parseFailed("Cannot determine current page")
|
|
|
|
|
|
|
|
|
|
|
|
private suspend fun parseList(url: String, page: Int): List<Manga> {
|
|
|
|
private suspend fun parseList(url: String, page: Int): List<Manga> {
|
|
|
|
val body = webClient.httpGet(url).parseHtml().body()
|
|
|
|
val body = webClient.httpGet(url).parseHtml().body()
|
|
|
|
if (body.selectFirst(".browse-no-matches") != null) {
|
|
|
|
if (body.selectFirst(".browse-no-matches") != null) {
|
|
|
|
return emptyList()
|
|
|
|
return emptyList()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
val activePage = getActivePage(body)
|
|
|
|
val activePage = getActivePage(body)
|
|
|
|
if (activePage != page) {
|
|
|
|
if (activePage != page) {
|
|
|
|
return emptyList()
|
|
|
|
return emptyList()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
val root = body.requireElementById("series-list")
|
|
|
|
val root = body.requireElementById("series-list")
|
|
|
|
return root.children().map { div ->
|
|
|
|
return root.children().map { div ->
|
|
|
|
val a = div.selectFirstOrThrow("a")
|
|
|
|
val a = div.selectFirstOrThrow("a")
|
|
|
|
val href = a.attrAsRelativeUrl("href")
|
|
|
|
val href = a.attrAsRelativeUrl("href")
|
|
|
|
val title = div.selectFirstOrThrow(".item-title").text()
|
|
|
|
val title = div.selectFirstOrThrow(".item-title").text()
|
|
|
|
Manga(
|
|
|
|
Manga(
|
|
|
|
id = generateUid(href),
|
|
|
|
id = generateUid(href),
|
|
|
|
title = title,
|
|
|
|
title = title,
|
|
|
|
altTitle = div.selectFirst(".item-alias")?.text()?.takeUnless { it == title },
|
|
|
|
altTitle = div.selectFirst(".item-alias")?.text()?.takeUnless { it == title },
|
|
|
|
url = href,
|
|
|
|
url = href,
|
|
|
|
publicUrl = a.absUrl("href"),
|
|
|
|
publicUrl = a.absUrl("href"),
|
|
|
|
rating = RATING_UNKNOWN,
|
|
|
|
rating = RATING_UNKNOWN,
|
|
|
|
isNsfw = false,
|
|
|
|
isNsfw = false,
|
|
|
|
coverUrl = div.selectFirst("img[src]")?.absUrl("src").orEmpty(),
|
|
|
|
coverUrl = div.selectFirst("img[src]")?.absUrl("src").orEmpty(),
|
|
|
|
largeCoverUrl = null,
|
|
|
|
largeCoverUrl = null,
|
|
|
|
description = null,
|
|
|
|
description = null,
|
|
|
|
tags = div.selectFirst(".item-genre")?.parseTags().orEmpty(),
|
|
|
|
tags = div.selectFirst(".item-genre")?.parseTags().orEmpty(),
|
|
|
|
state = null,
|
|
|
|
state = null,
|
|
|
|
author = null,
|
|
|
|
author = null,
|
|
|
|
source = source,
|
|
|
|
source = source,
|
|
|
|
)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private fun Element.parseTags() = children().mapToSet { span ->
|
|
|
|
private fun Element.parseTags() = children().mapToSet { span ->
|
|
|
|
val text = span.ownText()
|
|
|
|
val text = span.ownText()
|
|
|
|
MangaTag(
|
|
|
|
MangaTag(
|
|
|
|
title = text.toTitleCase(),
|
|
|
|
title = text.toTitleCase(),
|
|
|
|
key = text.lowercase(Locale.ENGLISH).replace(' ', '_'),
|
|
|
|
key = text.lowercase(Locale.ENGLISH).replace(' ', '_'),
|
|
|
|
source = source,
|
|
|
|
source = source,
|
|
|
|
)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private fun Element.parseChapter(index: Int): MangaChapter? {
|
|
|
|
private fun Element.parseChapter(index: Int): MangaChapter? {
|
|
|
|
val a = selectFirst("a.chapt") ?: return null
|
|
|
|
val a = selectFirst("a.chapt") ?: return null
|
|
|
|
val extra = selectFirst(".extra")
|
|
|
|
val extra = selectFirst(".extra")
|
|
|
|
val href = a.attrAsRelativeUrl("href")
|
|
|
|
val href = a.attrAsRelativeUrl("href")
|
|
|
|
return MangaChapter(
|
|
|
|
return MangaChapter(
|
|
|
|
id = generateUid(href),
|
|
|
|
id = generateUid(href),
|
|
|
|
name = a.text(),
|
|
|
|
name = a.text(),
|
|
|
|
number = index + 1,
|
|
|
|
number = index + 1,
|
|
|
|
url = href,
|
|
|
|
url = href,
|
|
|
|
scanlator = extra?.getElementsByAttributeValueContaining("href", "/group/")?.text(),
|
|
|
|
scanlator = extra?.getElementsByAttributeValueContaining("href", "/group/")?.text(),
|
|
|
|
uploadDate = runCatching {
|
|
|
|
uploadDate = runCatching {
|
|
|
|
parseChapterDate(extra?.select("i")?.lastOrNull()?.ownText())
|
|
|
|
parseChapterDate(extra?.select("i")?.lastOrNull()?.ownText())
|
|
|
|
}.getOrDefault(0),
|
|
|
|
}.getOrDefault(0),
|
|
|
|
branch = null,
|
|
|
|
branch = null,
|
|
|
|
source = source,
|
|
|
|
source = source,
|
|
|
|
)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private fun parseChapterDate(date: String?): Long {
|
|
|
|
private fun parseChapterDate(date: String?): Long {
|
|
|
|
if (date.isNullOrEmpty()) {
|
|
|
|
if (date.isNullOrEmpty()) {
|
|
|
|
return 0
|
|
|
|
return 0
|
|
|
|
}
|
|
|
|
}
|
|
|
|
val value = date.substringBefore(' ').toInt()
|
|
|
|
val value = date.substringBefore(' ').toInt()
|
|
|
|
val field = when {
|
|
|
|
val field = when {
|
|
|
|
"sec" in date -> Calendar.SECOND
|
|
|
|
"sec" in date -> Calendar.SECOND
|
|
|
|
"min" in date -> Calendar.MINUTE
|
|
|
|
"min" in date -> Calendar.MINUTE
|
|
|
|
"hour" in date -> Calendar.HOUR
|
|
|
|
"hour" in date -> Calendar.HOUR
|
|
|
|
"day" in date -> Calendar.DAY_OF_MONTH
|
|
|
|
"day" in date -> Calendar.DAY_OF_MONTH
|
|
|
|
"week" in date -> Calendar.WEEK_OF_YEAR
|
|
|
|
"week" in date -> Calendar.WEEK_OF_YEAR
|
|
|
|
"month" in date -> Calendar.MONTH
|
|
|
|
"month" in date -> Calendar.MONTH
|
|
|
|
"year" in date -> Calendar.YEAR
|
|
|
|
"year" in date -> Calendar.YEAR
|
|
|
|
else -> return 0
|
|
|
|
else -> return 0
|
|
|
|
}
|
|
|
|
}
|
|
|
|
val calendar = Calendar.getInstance()
|
|
|
|
val calendar = Calendar.getInstance()
|
|
|
|
calendar.add(field, -value)
|
|
|
|
calendar.add(field, -value)
|
|
|
|
return calendar.timeInMillis
|
|
|
|
return calendar.timeInMillis
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private fun decryptAES(encrypted: String, password: String): String {
|
|
|
|
private fun decryptAES(encrypted: String, password: String): String {
|
|
|
|
val cipherData = context.decodeBase64(encrypted)
|
|
|
|
val cipherData = context.decodeBase64(encrypted)
|
|
|
|
val saltData = cipherData.copyOfRange(8, 16)
|
|
|
|
val saltData = cipherData.copyOfRange(8, 16)
|
|
|
|
val (key, iv) = generateKeyAndIV(
|
|
|
|
val (key, iv) = generateKeyAndIV(
|
|
|
|
keyLength = 32,
|
|
|
|
keyLength = 32,
|
|
|
|
ivLength = 16,
|
|
|
|
ivLength = 16,
|
|
|
|
iterations = 1,
|
|
|
|
iterations = 1,
|
|
|
|
salt = saltData,
|
|
|
|
salt = saltData,
|
|
|
|
password = password.toByteArray(StandardCharsets.UTF_8),
|
|
|
|
password = password.toByteArray(StandardCharsets.UTF_8),
|
|
|
|
md = MessageDigest.getInstance("MD5"),
|
|
|
|
md = MessageDigest.getInstance("MD5"),
|
|
|
|
)
|
|
|
|
)
|
|
|
|
val encryptedData = cipherData.copyOfRange(16, cipherData.size)
|
|
|
|
val encryptedData = cipherData.copyOfRange(16, cipherData.size)
|
|
|
|
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
|
|
|
|
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
|
|
|
|
cipher.init(Cipher.DECRYPT_MODE, key, iv)
|
|
|
|
cipher.init(Cipher.DECRYPT_MODE, key, iv)
|
|
|
|
return cipher.doFinal(encryptedData).toString(Charsets.UTF_8)
|
|
|
|
return cipher.doFinal(encryptedData).toString(Charsets.UTF_8)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Suppress("SameParameterValue")
|
|
|
|
@Suppress("SameParameterValue")
|
|
|
|
private fun generateKeyAndIV(
|
|
|
|
private fun generateKeyAndIV(
|
|
|
|
keyLength: Int,
|
|
|
|
keyLength: Int,
|
|
|
|
ivLength: Int,
|
|
|
|
ivLength: Int,
|
|
|
|
iterations: Int,
|
|
|
|
iterations: Int,
|
|
|
|
salt: ByteArray,
|
|
|
|
salt: ByteArray,
|
|
|
|
password: ByteArray,
|
|
|
|
password: ByteArray,
|
|
|
|
md: MessageDigest,
|
|
|
|
md: MessageDigest,
|
|
|
|
): Pair<SecretKeySpec, IvParameterSpec> {
|
|
|
|
): Pair<SecretKeySpec, IvParameterSpec> {
|
|
|
|
val digestLength = md.digestLength
|
|
|
|
val digestLength = md.digestLength
|
|
|
|
val requiredLength = (keyLength + ivLength + digestLength - 1) / digestLength * digestLength
|
|
|
|
val requiredLength = (keyLength + ivLength + digestLength - 1) / digestLength * digestLength
|
|
|
|
val generatedData = ByteArray(requiredLength)
|
|
|
|
val generatedData = ByteArray(requiredLength)
|
|
|
|
var generatedLength = 0
|
|
|
|
var generatedLength = 0
|
|
|
|
md.reset()
|
|
|
|
md.reset()
|
|
|
|
while (generatedLength < keyLength + ivLength) {
|
|
|
|
while (generatedLength < keyLength + ivLength) {
|
|
|
|
if (generatedLength > 0) {
|
|
|
|
if (generatedLength > 0) {
|
|
|
|
md.update(generatedData, generatedLength - digestLength, digestLength)
|
|
|
|
md.update(generatedData, generatedLength - digestLength, digestLength)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
md.update(password)
|
|
|
|
md.update(password)
|
|
|
|
md.update(salt, 0, 8)
|
|
|
|
md.update(salt, 0, 8)
|
|
|
|
md.digest(generatedData, generatedLength, digestLength)
|
|
|
|
md.digest(generatedData, generatedLength, digestLength)
|
|
|
|
repeat(iterations - 1) {
|
|
|
|
repeat(iterations - 1) {
|
|
|
|
md.update(generatedData, generatedLength, digestLength)
|
|
|
|
md.update(generatedData, generatedLength, digestLength)
|
|
|
|
md.digest(generatedData, generatedLength, digestLength)
|
|
|
|
md.digest(generatedData, generatedLength, digestLength)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
generatedLength += digestLength
|
|
|
|
generatedLength += digestLength
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return SecretKeySpec(generatedData.copyOfRange(0, keyLength), "AES") to IvParameterSpec(
|
|
|
|
return SecretKeySpec(generatedData.copyOfRange(0, keyLength), "AES") to IvParameterSpec(
|
|
|
|
if (ivLength > 0) {
|
|
|
|
if (ivLength > 0) {
|
|
|
|
generatedData.copyOfRange(keyLength, keyLength + ivLength)
|
|
|
|
generatedData.copyOfRange(keyLength, keyLength + ivLength)
|
|
|
|
} else byteArrayOf(),
|
|
|
|
} else byteArrayOf(),
|
|
|
|
)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|