added accounts export/import in command line (debug only)

This commit is contained in:
Mariotaku Lee 2017-03-07 14:24:28 +08:00
parent 06ab10188a
commit d7e107c126
No known key found for this signature in database
GPG Key ID: 15C10F89D7C33535
6 changed files with 208 additions and 6 deletions

View File

@ -48,7 +48,8 @@ subprojects {
Toro : '2.1.0',
LoganSquare : '1.3.7',
IABv3 : '1.0.38',
Mime4J : '0.7.2'
Mime4J : '0.7.2',
Stetho : '1.4.2'
]
}

View File

@ -113,9 +113,9 @@ dependencies {
fdroidCompile 'org.osmdroid:osmdroid-android:5.6.4'
debugCompile 'com.facebook.stetho:stetho:1.4.2'
debugCompile 'com.facebook.stetho:stetho-okhttp3:1.4.2'
debugCompile 'com.facebook.stetho:stetho-js-rhino:1.4.2'
debugCompile "com.facebook.stetho:stetho:${libVersions['Stetho']}"
debugCompile "com.facebook.stetho:stetho-okhttp3:${libVersions['Stetho']}"
debugCompile "com.facebook.stetho:stetho-js-rhino:${libVersions['Stetho']}"
debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5'
provided 'javax.annotation:jsr250-api:1.0'

View File

@ -21,13 +21,16 @@ package org.mariotaku.twidere.util;
import android.app.Application;
import com.facebook.stetho.DumperPluginsProvider;
import com.facebook.stetho.Stetho;
import com.facebook.stetho.dumpapp.DumperPlugin;
import com.facebook.stetho.okhttp3.StethoInterceptor;
import com.squareup.leakcanary.LeakCanary;
import com.squareup.leakcanary.RefWatcher;
import org.mariotaku.twidere.BuildConfig;
import org.mariotaku.twidere.util.net.NoIntercept;
import org.mariotaku.twidere.util.stetho.AccountsDumper;
import java.io.IOException;
@ -61,7 +64,14 @@ public class DebugModeUtils {
public static void initForApplication(final Application application) {
Stetho.initialize(Stetho.newInitializerBuilder(application)
.enableDumpapp(Stetho.defaultDumperPluginsProvider(application))
.enableDumpapp(new DumperPluginsProvider() {
@Override
public Iterable<DumperPlugin> get() {
return new Stetho.DefaultDumperPluginsBuilder(application)
.provide(new AccountsDumper(application))
.finish();
}
})
.enableWebKitInspector(Stetho.defaultInspectorModulesProvider(application))
.build());
initLeakCanary(application);

View File

@ -0,0 +1,185 @@
/*
* 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.bluelinelabs.logansquare.LoganSquare
import com.facebook.stetho.dumpapp.DumperContext
import com.facebook.stetho.dumpapp.DumperPlugin
import org.apache.commons.cli.GnuParser
import org.apache.commons.cli.Option
import org.apache.commons.cli.Options
import org.mariotaku.ktextension.HexColorFormat
import org.mariotaku.ktextension.subArray
import org.mariotaku.ktextension.toHexColor
import org.mariotaku.twidere.TwidereConstants.*
import org.mariotaku.twidere.model.AccountDetails
import org.mariotaku.twidere.model.util.AccountUtils
import java.io.File
import java.io.InputStream
import java.io.OutputStream
import java.io.PrintStream
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 AccountsDumper(val context: Context) : DumperPlugin {
private val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1")
private val salt = byteArrayOf(0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8)
override fun getName() = "accounts"
override fun dump(dumpContext: DumperContext) {
val parser = GnuParser()
val argsAsList = dumpContext.argsAsList
when (argsAsList.firstOrNull()) {
"import" -> {
val subCommandArgs = argsAsList.subArray(1..argsAsList.lastIndex)
val options = Options().apply {
addRequiredOption(Option("p", "password", true, "Account encryption password"))
addRequiredOption(Option("i", "input", true, "Accounts data file"))
}
val commandLine = parser.parse(options, subCommandArgs)
try {
val password = commandLine.getOptionValue("password")
File(commandLine.getOptionValue("input")).inputStream().use { input ->
importAccounts(password, input, dumpContext.stdout)
}
} catch (e: Exception) {
e.printStackTrace(dumpContext.stderr)
}
}
"export" -> {
val subCommandArgs = argsAsList.subArray(1..argsAsList.lastIndex)
val options = Options().apply {
addRequiredOption(Option("p", "password", true, "Account encryption password"))
addRequiredOption(Option("o", "output", true, "Accounts data file"))
}
val commandLine = parser.parse(options, subCommandArgs)
try {
val password = commandLine.getOptionValue("password")
File(commandLine.getOptionValue("output")).outputStream().use { output ->
exportAccounts(password, output)
}
} catch (e: Exception) {
e.printStackTrace(dumpContext.stderr)
}
}
else -> {
dumpContext.stderr.println("Usage: accounts [import|export] -p <password>")
}
}
}
private fun 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 am = AccountManager.get(context)
val accounts = AccountUtils.getAllAccountDetails(am, true).toList()
LoganSquare.serialize(accounts, gz, AccountDetails::class.java)
}
private fun importAccounts(password: String, input: InputStream, output: PrintStream) {
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))
val am = AccountManager.get(context)
val usedAccounts = AccountUtils.getAccounts(am)
val allDetails = LoganSquare.parseList(gz, AccountDetails::class.java)
allDetails.forEach { details ->
val account = details.account
if (account !in usedAccounts) {
am.addAccountExplicitly(account, null, null)
}
am.setUserData(account, ACCOUNT_USER_DATA_KEY, details.key.toString())
am.setUserData(account, ACCOUNT_USER_DATA_TYPE, details.type)
am.setUserData(account, ACCOUNT_USER_DATA_CREDS_TYPE, details.credentials_type)
am.setUserData(account, ACCOUNT_USER_DATA_ACTIVATED, true.toString())
am.setUserData(account, ACCOUNT_USER_DATA_COLOR, toHexColor(details.color, format = HexColorFormat.RGB))
am.setUserData(account, ACCOUNT_USER_DATA_USER, LoganSquare.serialize(details.user))
am.setUserData(account, ACCOUNT_USER_DATA_EXTRAS, details.extras?.let { LoganSquare.serialize(it) })
am.setAuthToken(account, ACCOUNT_AUTH_TOKEN_TYPE, LoganSquare.serialize(details.credentials))
}
output.println("Done.")
}
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 Options.addRequiredOption(option: Option) {
option.isRequired = true
addOption(option)
}
private fun generateSecret(password: String): SecretKeySpec {
val spec = PBEKeySpec(password.toCharArray(), salt, 65536, 256)
return SecretKeySpec(factory.generateSecret(spec).encoded, "AES")
}
}

View File

@ -33,4 +33,10 @@ fun <E> Collection<E>.contentEquals(other: Collection<E>): Boolean {
if (this === other) return true
if (this.size != other.size) return false
return this.containsAll(other) && other.containsAll(this)
}
inline fun <reified T> List<T>.subArray(range: IntRange): Array<T> {
return Array(range.count()) {
this[range.start + it]
}
}

View File

@ -797,7 +797,7 @@ class SignInActivity : BaseActivity(), OnClickListener, TextWatcher, APIEditorDi
AccountUtils.getAccounts(am).mapTo(usedNames, Account::name)
do {
accountName = UUID.randomUUID().toString()
} while (usedNames.contains(accountName))
} while (accountName in usedNames)
} else {
accountName = generateAccountName(user.screen_name, user.key.host)
}