Files
shadowsocks-android/mobile/src/main/java/com/github/shadowsocks/plugin/PluginManager.kt
T
Mygod d7a11eaa3b Implement direct boot awareness
To do direct boot, important files need to be moved to device storage (i.e. not as safe storage). The only thing sensitive is user credentials (server settings in each profile) and the config file passed to libss-*.so. In order to implement direct boot, at least the active profile has to be moved/copied to device storage.

This commit keeps profile information (and temporary data used by ProfileConfigFragment) and Shadowsocks config in credential storage and move everything else to device storage. In order to make direct boot work, active profile is also copied into device storage and Shadowsocks config file is created in device storage during direct boot. Cleanup of those information is also implemented.

Fixes #1457.

NB:

* `android:directBootAware` can't be used on application element. That seems to be used by system apps.
* Works fine with multi-user;
* Old GSM files are not migrated (see App.kt) to device storage.
2017-12-15 15:36:19 -08:00

183 lines
9.5 KiB
Kotlin

/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* 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 com.github.shadowsocks.plugin
import android.annotation.SuppressLint
import android.content.ContentResolver
import android.content.Intent
import android.content.pm.PackageManager
import android.content.pm.Signature
import android.net.Uri
import android.os.Bundle
import android.util.Base64
import android.util.Log
import com.github.shadowsocks.App.Companion.app
import com.github.shadowsocks.utils.Commandline
import eu.chainfire.libsuperuser.Shell
import java.io.File
import java.io.FileNotFoundException
object PluginManager {
/**
* Trusted signatures by the app. Third-party fork should add their public key to their fork if the developer wishes
* to publish or has published plugins for this app. You can obtain your public key by executing:
*
* $ keytool -export -alias key-alias -keystore /path/to/keystore.jks -rfc
*
* If you don't plan to publish any plugin but is developing/has developed some, it's not necessary to add your
* public key yet since it will also automatically trust packages signed by the same signatures, e.g. debug keys.
*/
val trustedSignatures by lazy {
app.info.signatures.toSet() +
Signature(Base64.decode( // @Mygod
"""
|MIIDWzCCAkOgAwIBAgIEUzfv8DANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJD
|TjEOMAwGA1UECBMFTXlnb2QxDjAMBgNVBAcTBU15Z29kMQ4wDAYDVQQKEwVNeWdv
|ZDEOMAwGA1UECxMFTXlnb2QxDjAMBgNVBAMTBU15Z29kMCAXDTE0MDUwMjA5MjQx
|OVoYDzMwMTMwOTAyMDkyNDE5WjBdMQswCQYDVQQGEwJDTjEOMAwGA1UECBMFTXln
|b2QxDjAMBgNVBAcTBU15Z29kMQ4wDAYDVQQKEwVNeWdvZDEOMAwGA1UECxMFTXln
|b2QxDjAMBgNVBAMTBU15Z29kMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
|AQEAjm5ikHoP3w6zavvZU5bRo6Birz41JL/nZidpdww21q/G9APA+IiJMUeeocy0
|L7/QY8MQZABVwNq79LXYWJBcmmFXM9xBPgDqQP4uh9JsvazCI9bvDiMn92mz9HiS
|Sg9V4KGg0AcY0r230KIFo7hz+2QBp1gwAAE97myBfA3pi3IzJM2kWsh4LWkKQMfL
|M6KDhpb4mdDQnHlgi4JWe3SYbLtpB6whnTqjHaOzvyiLspx1tmrb0KVxssry9KoX
|YQzl56scfE/QJX0jJ5qYmNAYRCb4PibMuNSGB2NObDabSOMAdT4JLueOcHZ/x9tw
|agGQ9UdymVZYzf8uqc+29ppKdQIDAQABoyEwHzAdBgNVHQ4EFgQUBK4uJ0cqmnho
|6I72VmOVQMvVCXowDQYJKoZIhvcNAQELBQADggEBABZQ3yNESQdgNJg+NRIcpF9l
|YSKZvrBZ51gyrC7/2ZKMpRIyXruUOIrjuTR5eaONs1E4HI/uA3xG1eeW2pjPxDnO
|zgM4t7EPH6QbzibihoHw1MAB/mzECzY8r11PBhDQlst0a2hp+zUNR8CLbpmPPqTY
|RSo6EooQ7+NBejOXysqIF1q0BJs8Y5s/CaTOmgbL7uPCkzArB6SS/hzXgDk5gw6v
|wkGeOtzcj1DlbUTvt1s5GlnwBTGUmkbLx+YUje+n+IBgMbohLUDYBtUHylRVgMsc
|1WS67kDqeJiiQZvrxvyW6CZZ/MIGI+uAkkj3DqJpaZirkwPgvpcOIrjZy0uFvQM=
""", Base64.DEFAULT)) +
Signature(Base64.decode( // @madeye
"""
|MIICQzCCAaygAwIBAgIETV9OhjANBgkqhkiG9w0BAQUFADBmMQswCQYDVQQGEwJjbjERMA8GA1UE
|CBMIU2hhbmdoYWkxDzANBgNVBAcTBlB1ZG9uZzEUMBIGA1UEChMLRnVkYW4gVW5pdi4xDDAKBgNV
|BAsTA1BQSTEPMA0GA1UEAxMGTWF4IEx2MB4XDTExMDIxOTA1MDA1NFoXDTM2MDIxMzA1MDA1NFow
|ZjELMAkGA1UEBhMCY24xETAPBgNVBAgTCFNoYW5naGFpMQ8wDQYDVQQHEwZQdWRvbmcxFDASBgNV
|BAoTC0Z1ZGFuIFVuaXYuMQwwCgYDVQQLEwNQUEkxDzANBgNVBAMTBk1heCBMdjCBnzANBgkqhkiG
|9w0BAQEFAAOBjQAwgYkCgYEAq6lA8LqdeEI+es9SDX85aIcx8LoL3cc//iRRi+2mFIWvzvZ+bLKr
|4Wd0rhu/iU7OeMm2GvySFyw/GdMh1bqh5nNPLiRxAlZxpaZxLOdRcxuvh5Nc5yzjM+QBv8ECmuvu
|AOvvT3UDmA0AMQjZqSCmxWIxc/cClZ/0DubreBo2st0CAwEAATANBgkqhkiG9w0BAQUFAAOBgQAQ
|Iqonxpwk2ay+Dm5RhFfZyG9SatM/JNFx2OdErU16WzuK1ItotXGVJaxCZv3u/tTwM5aaMACGED5n
|AvHaDGCWynY74oDAopM4liF/yLe1wmZDu6Zo/7fXrH+T03LBgj2fcIkUfN1AA4dvnBo8XWAm9VrI
|1iNuLIssdhDz3IL9Yg==
""", Base64.DEFAULT))
}
private val receiver by lazy { app.listenForPackageChanges { synchronized(this) { cachedPlugins = null } } }
private var cachedPlugins: Map<String, Plugin>? = null
fun fetchPlugins(): Map<String, Plugin> {
receiver
return synchronized(this) {
if (cachedPlugins == null) {
val pm = app.packageManager
cachedPlugins = (pm.queryIntentContentProviders(Intent(PluginContract.ACTION_NATIVE_PLUGIN),
PackageManager.GET_META_DATA).map { NativePlugin(it) } + NoPlugin).associate { it.id to it }
}
cachedPlugins!!
}
}
private fun buildUri(id: String) = Uri.Builder()
.scheme(PluginContract.SCHEME)
.authority(PluginContract.AUTHORITY)
.path('/' + id)
.build()
fun buildIntent(id: String, action: String): Intent = Intent(action, buildUri(id))
// the following parts are meant to be used by :bg
@Throws(Throwable::class)
fun init(options: PluginOptions): String? {
if (options.id.isEmpty()) return null
var throwable: Throwable? = null
try {
val path = initNative(options)
if (path != null) return path
} catch (t: Throwable) {
t.printStackTrace()
if (throwable == null) throwable = t
}
// add other plugin types here
throw if (throwable != null) throwable else
FileNotFoundException(app.getString(com.github.shadowsocks.R.string.plugin_unknown, options.id))
}
private fun initNative(options: PluginOptions): String? {
val providers = app.packageManager.queryIntentContentProviders(
Intent(PluginContract.ACTION_NATIVE_PLUGIN, buildUri(options.id)), 0)
assert(providers.size == 1)
val uri = Uri.Builder()
.scheme(ContentResolver.SCHEME_CONTENT)
.authority(providers[0].providerInfo.authority)
.build()
val cr = app.contentResolver
return try {
initNativeFast(cr, options, uri)
} catch (t: Throwable) {
t.printStackTrace()
Log.w("PluginManager", "Initializing native plugin fast mode failed. Falling back to slow mode.")
initNativeSlow(cr, options, uri)
}
}
private fun initNativeFast(cr: ContentResolver, options: PluginOptions, uri: Uri): String {
val out = Bundle()
out.putString(PluginContract.EXTRA_OPTIONS, options.id)
val result = cr.call(uri, PluginContract.METHOD_GET_EXECUTABLE, null, out)
.getString(PluginContract.EXTRA_ENTRY)
assert(File(result).canExecute())
return result
}
@SuppressLint("Recycle")
private fun initNativeSlow(cr: ContentResolver, options: PluginOptions, uri: Uri): String? {
var initialized = false
fun entryNotFound(): Nothing = throw IndexOutOfBoundsException("Plugin entry binary not found")
val list = ArrayList<String>()
val pluginDir = File(app.deviceContext.filesDir, "plugin")
(cr.query(uri, arrayOf(PluginContract.COLUMN_PATH, PluginContract.COLUMN_MODE), null, null, null)
?: return null).use { cursor ->
if (!cursor.moveToFirst()) entryNotFound()
pluginDir.deleteRecursively()
if (!pluginDir.mkdirs()) throw FileNotFoundException("Unable to create plugin directory")
val pluginDirPath = pluginDir.absolutePath + '/'
do {
val path = cursor.getString(0)
val file = File(pluginDir, path)
assert(file.absolutePath.startsWith(pluginDirPath))
cr.openInputStream(uri.buildUpon().path(path).build()).use { inStream ->
file.outputStream().use { outStream -> inStream.copyTo(outStream) }
}
list += Commandline.toString(arrayOf("chmod", cursor.getString(1), file.absolutePath))
if (path == options.id) initialized = true
} while (cursor.moveToNext())
}
if (!initialized) entryNotFound()
Shell.SH.run(list)
return File(pluginDir, options.id).absolutePath
}
}