diff --git a/build.gradle b/build.gradle index 7c1edd996..6e06dfce9 100644 --- a/build.gradle +++ b/build.gradle @@ -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' ] } diff --git a/twidere/build.gradle b/twidere/build.gradle index b79f972c1..36f328467 100644 --- a/twidere/build.gradle +++ b/twidere/build.gradle @@ -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' diff --git a/twidere/src/debug/java/org/mariotaku/twidere/util/DebugModeUtils.java b/twidere/src/debug/java/org/mariotaku/twidere/util/DebugModeUtils.java index 64d7204be..7958fb4e9 100644 --- a/twidere/src/debug/java/org/mariotaku/twidere/util/DebugModeUtils.java +++ b/twidere/src/debug/java/org/mariotaku/twidere/util/DebugModeUtils.java @@ -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 get() { + return new Stetho.DefaultDumperPluginsBuilder(application) + .provide(new AccountsDumper(application)) + .finish(); + } + }) .enableWebKitInspector(Stetho.defaultInspectorModulesProvider(application)) .build()); initLeakCanary(application); diff --git a/twidere/src/debug/kotlin/org/mariotaku/twidere/util/stetho/AccountsDumper.kt b/twidere/src/debug/kotlin/org/mariotaku/twidere/util/stetho/AccountsDumper.kt new file mode 100644 index 000000000..ee64060ab --- /dev/null +++ b/twidere/src/debug/kotlin/org/mariotaku/twidere/util/stetho/AccountsDumper.kt @@ -0,0 +1,185 @@ +/* + * 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.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 ") + } + } + + } + + 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") + } + +} diff --git a/twidere/src/main/kotlin/org/mariotaku/ktextension/CollectionExtensions.kt b/twidere/src/main/kotlin/org/mariotaku/ktextension/CollectionExtensions.kt index 729aa3a68..d7e54a925 100644 --- a/twidere/src/main/kotlin/org/mariotaku/ktextension/CollectionExtensions.kt +++ b/twidere/src/main/kotlin/org/mariotaku/ktextension/CollectionExtensions.kt @@ -33,4 +33,10 @@ fun Collection.contentEquals(other: Collection): Boolean { if (this === other) return true if (this.size != other.size) return false return this.containsAll(other) && other.containsAll(this) +} + +inline fun List.subArray(range: IntRange): Array { + return Array(range.count()) { + this[range.start + it] + } } \ No newline at end of file diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/activity/SignInActivity.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/activity/SignInActivity.kt index bb02a53e8..abde1ac1b 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/activity/SignInActivity.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/activity/SignInActivity.kt @@ -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) }