Twidere-App-Android-Twitter.../twidere/src/debug/kotlin/org/mariotaku/twidere/util/stetho/AccountsDumperPlugin.kt

327 lines
13 KiB
Kotlin

/*
* Twidere - Twitter client for Android
*
* Copyright (C) 2012-2017 Mariotaku Lee <mariotaku.lee@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.mariotaku.twidere.util.stetho
import android.accounts.AccountManager
import android.content.Context
import android.util.Base64
import android.util.Base64InputStream
import android.util.Base64OutputStream
import com.facebook.stetho.dumpapp.DumpException
import com.facebook.stetho.dumpapp.DumperContext
import com.facebook.stetho.dumpapp.DumperPlugin
import com.jayway.jsonpath.Configuration
import com.jayway.jsonpath.DocumentContext
import com.jayway.jsonpath.JsonPath
import com.jayway.jsonpath.TypeRef
import com.jayway.jsonpath.spi.json.JsonOrgJsonProvider
import com.jayway.jsonpath.spi.mapper.MappingProvider
import org.apache.commons.cli.*
import org.json.JSONArray
import org.json.JSONObject
import org.mariotaku.ktextension.subArray
import org.mariotaku.twidere.exception.NoAccountException
import org.mariotaku.twidere.extension.model.updateDetails
import org.mariotaku.twidere.model.AccountDetails
import org.mariotaku.twidere.model.UserKey
import org.mariotaku.twidere.model.util.AccountUtils
import org.mariotaku.twidere.util.DataStoreUtils
import org.mariotaku.twidere.util.JsonSerializer
import java.io.InputStream
import java.io.OutputStream
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.util.zip.GZIPInputStream
import java.util.zip.GZIPOutputStream
import javax.crypto.Cipher
import javax.crypto.CipherInputStream
import javax.crypto.CipherOutputStream
import javax.crypto.SecretKeyFactory
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.PBEKeySpec
import javax.crypto.spec.SecretKeySpec
/**
* Created by mariotaku on 2017/3/6.
*/
class AccountsDumperPlugin(val context: Context) : DumperPlugin {
override fun getName() = "accounts"
override fun dump(dumpContext: DumperContext) {
val argsAsList = dumpContext.argsAsList
val subCommands = listOf(ExportCommand(context), ImportCommand(context),
ListCommand(context), GetCommand(context), SetCommand(context),
SetJsonCommand(context))
val subCommandName = argsAsList.firstOrNull()
val subCommand = subCommands.find { it.name == subCommandName } ?: run {
throw DumpException("Usage: accounts <${subCommands.joinToString("|", transform = SubCommand::name)}> <args>")
}
subCommand.execute(dumpContext, argsAsList.subArray(1..argsAsList.lastIndex))
}
internal class AsIsMappingProvider : MappingProvider {
override fun <T : Any?> map(source: Any?, type: Class<T>, configuration: Configuration): T {
@Suppress("UNCHECKED_CAST")
return source as T
}
override fun <T : Any?> map(source: Any?, type: TypeRef<T>, configuration: Configuration): T {
@Suppress("UNCHECKED_CAST")
return source as T
}
}
abstract class SubCommand(val name: String) {
abstract fun execute(dumpContext: DumperContext, args: Array<String>)
}
class ExportCommand(val context: Context) : CmdLineSubCommand("export") {
override val options = Options().apply {
addRequiredOption("p", "password", true, "Account encryption password")
}
override val syntax: String = "[-p]"
override fun execute(dumpContext: DumperContext, commandLine: CommandLine) {
val am = AccountManager.get(context)
try {
val password = commandLine.getOptionValue("password")
am.exportAccounts(password, dumpContext.stdout)
} catch (e: Exception) {
e.printStackTrace(dumpContext.stderr)
}
}
}
abstract class CmdLineSubCommand(name: String) : SubCommand(name) {
protected abstract val options: Options
protected abstract val syntax: String
final override fun execute(dumpContext: DumperContext, args: Array<String>) {
val commandLine = try {
GnuParser().parse(options, args)
} catch (e: ParseException) {
val formatter = HelpFormatter()
formatter.printHelp(dumpContext.stderr, "$name $syntax", options)
return
}
execute(dumpContext, commandLine)
}
abstract fun execute(dumpContext: DumperContext, commandLine: CommandLine)
}
class ImportCommand(val context: Context) : CmdLineSubCommand("import") {
override val options: Options = Options().apply {
addRequiredOption("p", "password", true, "Account encryption password")
addOption("t", "test", false, "Dry-run without actual import")
}
override val syntax: String = "[-pt]"
override fun execute(dumpContext: DumperContext, commandLine: CommandLine) {
val am = AccountManager.get(context)
try {
val password = commandLine.getOptionValue("password")
val isTest = commandLine.hasOption("test")
val accounts = readAccounts(password, dumpContext.stdin)
if (isTest) {
accounts.forEach { dumpContext.stdout.println(it.key) }
} else {
am.importAccounts(accounts)
}
} catch (e: Exception) {
e.printStackTrace(dumpContext.stderr)
}
}
}
class ListCommand(val context: Context) : SubCommand("list") {
override fun execute(dumpContext: DumperContext, args: Array<String>) {
val keys = DataStoreUtils.getAccountKeys(context)
keys.forEach {
dumpContext.stdout.println(it)
}
}
}
class GetCommand(val context: Context) : SubCommand("get-value") {
override fun execute(dumpContext: DumperContext, args: Array<String>) {
if (args.isEmpty()) {
throw DumpException("Usage: accounts $name <account_key> [value1] [value2] ...")
}
val am = AccountManager.get(context)
val docContext = try {
am.docContext(args[0])
} catch (e: NoAccountException) {
throw DumpException("Account not found")
}
if (args.size == 1) {
val result = docContext.read("$", Object::class.java)
dumpContext.stdout.println(result?.prettyPrint())
} else for (i in 1..args.lastIndex) {
val result = docContext.read(args[i], Object::class.java)
dumpContext.stdout.println(result?.prettyPrint())
}
}
}
class SetCommand(val context: Context) : SubCommand("set-value") {
override fun execute(dumpContext: DumperContext, args: Array<String>) {
if (args.size != 3) {
throw DumpException("Usage: accounts $name <account_key> <field> <value>")
}
val am = AccountManager.get(context)
val docContext = try {
am.docContext(args[0])
} catch (e: NoAccountException) {
throw DumpException("Account not found")
}
val value = args[2]
val path = args[1]
docContext.set(path, value)
val details = docContext.read("$", Object::class.java)?.let {
JsonSerializer.parse(it.toString(), AccountDetails::class.java)
} ?: return
details.account.updateDetails(am, details)
dumpContext.stdout.println("$path = ${docContext.read(path, Object::class.java)?.prettyPrint()}")
}
}
class SetJsonCommand(val context: Context) : SubCommand("set-json") {
override fun execute(dumpContext: DumperContext, args: Array<String>) {
if (args.size != 3) {
throw DumpException("Usage: accounts $name <account_key> <field> <json>")
}
val am = AccountManager.get(context)
val docContext = try {
am.docContext(args[0])
} catch (e: NoAccountException) {
throw DumpException("Account not found")
}
val value = args[2]
val path = args[1]
if (value.startsWith("{")) {
docContext.set(path, JSONObject(value))
} else if (value.startsWith("[")) {
docContext.set(path, JSONArray(value))
}
val details = docContext.read("$", Object::class.java)?.let {
JsonSerializer.parse(it.toString(), AccountDetails::class.java)
} ?: return
details.account.updateDetails(am, details)
dumpContext.stdout.println("$path = ${docContext.read(path, Object::class.java)?.prettyPrint()}")
}
}
companion object {
private val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1")
private val salt = byteArrayOf(0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8)
private fun AccountManager.exportAccounts(password: String, output: OutputStream) {
val secret = generateSecret(password)
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
cipher.init(Cipher.ENCRYPT_MODE, secret)
val base64 = Base64OutputStream(output, Base64.NO_CLOSE)
val iv = cipher.parameters.getParameterSpec(IvParameterSpec::class.java).iv
// write IV size
base64.write(iv.size.toByteArray())
// write IV
base64.write(iv)
val gz = GZIPOutputStream(CipherOutputStream(base64, cipher))
// write accounts
val accounts = AccountUtils.getAllAccountDetails(this, true).toList()
JsonSerializer.serialize(accounts, gz, AccountDetails::class.java)
}
private fun readAccounts(password: String, input: InputStream): List<AccountDetails> {
val base64 = Base64InputStream(input, Base64.NO_CLOSE)
val ivSize = ByteArray(4).apply { base64.read(this) }.toInt()
val iv = ByteArray(ivSize).apply { base64.read(this) }
val secret = generateSecret(password)
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
cipher.init(Cipher.DECRYPT_MODE, secret, IvParameterSpec(iv))
val gz = GZIPInputStream(CipherInputStream(base64, cipher))
return JsonSerializer.parseList(gz, AccountDetails::class.java)
}
private fun AccountManager.importAccounts(allDetails: List<AccountDetails>) {
val usedAccounts = AccountUtils.getAccounts(this)
allDetails.forEach { details ->
val account = details.account
if (account !in usedAccounts) {
this.addAccountExplicitly(account, null, null)
}
account.updateDetails(this, details)
}
}
fun ByteArray.toInt(): Int {
val bb = ByteBuffer.wrap(this)
bb.order(ByteOrder.LITTLE_ENDIAN)
return bb.int
}
fun Int.toByteArray(): ByteArray {
val bb = ByteBuffer.allocate(Integer.SIZE / java.lang.Byte.SIZE)
bb.order(ByteOrder.LITTLE_ENDIAN)
bb.putInt(this)
return bb.array()
}
private fun generateSecret(password: String): SecretKeySpec {
val spec = PBEKeySpec(password.toCharArray(), salt, 65536, 256)
return SecretKeySpec(factory.generateSecret(spec).encoded, "AES")
}
private fun AccountManager.docContext(forKey: String): DocumentContext {
val accountKey = UserKey.valueOf(forKey)
val details = AccountUtils.getAccountDetails(this, accountKey, true) ?: throw NoAccountException()
val configuration = Configuration.builder()
.jsonProvider(JsonOrgJsonProvider())
.mappingProvider(AsIsMappingProvider())
.build()
return JsonPath.parse(JsonSerializer.serialize(details), configuration)
}
private fun Any.prettyPrint() = when {
this is JSONObject -> toString(4)
this is JSONArray -> toString(4)
else -> toString()
}
}
}