/* * Twidere - Twitter client for Android * * Copyright (C) 2012-2017 Mariotaku Lee * * 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 . */ 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)}> ") } subCommand.execute(dumpContext, argsAsList.subArray(1..argsAsList.lastIndex)) } internal class AsIsMappingProvider : MappingProvider { override fun map(source: Any?, type: Class, configuration: Configuration): T { @Suppress("UNCHECKED_CAST") return source as T } override fun map(source: Any?, type: TypeRef, configuration: Configuration): T { @Suppress("UNCHECKED_CAST") return source as T } } abstract class SubCommand(val name: String) { abstract fun execute(dumpContext: DumperContext, args: Array) } 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) { 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) { 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) { if (args.isEmpty()) { throw DumpException("Usage: accounts $name [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) { if (args.size != 3) { throw DumpException("Usage: accounts $name ") } 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) { if (args.size != 3) { throw DumpException("Usage: accounts $name ") } 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 { 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) { 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() } } }