repack icon indicating copy to clipboard operation
repack copied to clipboard

Support bundle verification with multiple public keys

Open baka3k opened this issue 1 year ago • 5 comments

Description

The situation we're facing is as follows: We have multiple independent MiniApp development teams, and we want each team to use their own unique private key.

This means that when verifying a bundle, we need to pass in the public key of the respective team (which is fetched from the server).

However, when examining the repack source code, we found that the 'verify' function is currently ONLY allowing for a single public key that is hardcoded into the source code, as shown below."

fun verifyBundle(context: Context, token: String?, fileContent: ByteArray?) {
            if (token == null) {
                throw Exception("The bundle verification failed because no token for the bundle was found.")
            }

            val stringPublicKey = getPublicKeyFromStringsIfExist(context)
                    ?: throw Exception("The bundle verification failed because PublicKey was not found in the bundle. Make sure you've added the PublicKey to the res/values/strings.xml under RepackPublicKey key.")

            val publicKey = parsePublicKey(stringPublicKey)
                    ?: throw Exception("The bundle verification failed because the PublicKey is invalid.")

            val claims: Map<String, Any?> = verifyAndDecodeToken(token, publicKey)

            val contentHash = claims["hash"] as String?
                    ?: throw Exception("The bundle verification failed because the token is invalid.")

            val fileHash = computeHash(fileContent)

            if (contentHash != fileHash) {
                throw Exception("The bundle verification failed because the bundle hash is invalid.")
            }
        }

 private fun getPublicKeyFromStringsIfExist(
                context: Context
        ): String? {
            val packageName: String = context.packageName
            val resId: Int =
                    context.resources.getIdentifier("RepackPublicKey", "string", packageName)
            if (resId != 0) {
                return context.getString(resId).ifEmpty {
                    null
                }
            }
            return null
        }

Suggested solution

It would be much better if the verify bundle function could accept an additional public key as a parameter, instead of hardcoding it as it is now, such as:

fun verifyBundle(context: Context, 
                         token: String?, 
                         fileContent: ByteArray?,
                         publicKeyAsByteArray: ByteArray? // add this parameter
) {
  val publicKey = publicKeyFromByteArray(publicKeyAsByteArray)
// 1. verify bundle by public key
// 2. check sum bundle

With this approach, we can have multiple mini-app development teams, each with their own private key for signing their mini-apps. We can authenticate development teams using their keys, allowing them to join the ecosystem or rejecting teams without relying on the main app's private key. Furthermore, if the main app's private key is lost or compromised, mini-apps created by various development teams can still operate. We have the flexibility to re-sign them whenever needed,(similar Google's Play App Signing approach)

Would you please consider this proposal

Thanks & BestRegard,

Additional context

No response

baka3k avatar Dec 13 '24 03:12 baka3k

I have forked and and made some modifications to allow for public key injection at repository

https://github.com/baka3k/repackcodesigning

https://github.com/baka3k/repackcodesigning/blob/main/packages/repack/android/src/main/java/com/callstack/repack/ScriptManagerModule.kt

class ScriptManagerModule(
    reactContext: ReactApplicationContext,
    publicKey: PublicKey?,
    coroutineDispatcher: CoroutineDispatcher = Dispatchers.IO
) : ScriptManagerSpec(reactContext) {
    private val nativeLoader = NativeScriptLoader(reactApplicationContext)
    private val remoteLoader =
        RemoteScriptLoader(reactApplicationContext, nativeLoader, publicKey = publicKey)

https://github.com/baka3k/repackcodesigning/blob/main/packages/repack/android/src/main/java/com/callstack/repack/RemoteScriptLoader.kt

   if (config.verifyScriptSignature == "strict" || (config.verifyScriptSignature == "lax" && token != null)) {
                            CodeSigningUtils.verifyBundle(reactContext, token, bundle, publicKey)
                        }

https://github.com/baka3k/repackcodesigning/blob/main/packages/repack/android/src/main/java/com/callstack/repack/CodeSigningUtils.kt

fun verifyBundle(
            context: Context,
            token: String?,
            fileContent: ByteArray?,
            publickey: PublicKey?
        ) {
            if (token == null) {
                throw Exception("The bundle verification failed because no token for the bundle was found.")
            }
            var publicKey = publickey 
            if (publicKey == null) {
                // find default
                val stringPublicKey = getPublicKeyFromStringsIfExist(context)
                    ?: throw Exception("The bundle verification failed because PublicKey was not found in the bundle. Make sure you've added the PublicKey to the res/values/strings.xml under RepackPublicKey key.")
                publicKey = parsePublicKey(stringPublicKey)
                    ?: throw Exception("The bundle verification failed because the PublicKey is invalid.")
            }

If no public key is passed, the system will retrieve the old public key from the repack stored in res/values/strings.xml. However, if a public key is supplied, that specific public key will be utilized.

Beside that, There are a few points I want to notice about the existing issues related multithreading in the source code Repack I add log to get thread which use to run in background function Image And get results Image

You can see that all the processes inside "runInBackground" are executed with the thread outside "runInBackground" - mqt_v_native That means they are executed on the SAME thread. The reason is

  val handler = Handler()

If you don't pass a Looper to a Handler, it will default to using the Looper of the thread that created it. This means that all tasks, both inside and outside the 'runInBackground' function, will be executed on the same thread. therefore, the runInBackground function might not work as expected I've made a few changes:

class ScriptManagerModule(
    reactContext: ReactApplicationContext,
    publicKey: PublicKey?,
    coroutineDispatcher: CoroutineDispatcher = Dispatchers.IO // add dispatcher or default
) : ScriptManagerSpec(reactContext) {
    private val nativeLoader = NativeScriptLoader(reactApplicationContext)
    private val remoteLoader = RemoteScriptLoader(reactApplicationContext, nativeLoader, publicKey = publicKey)
    private val fileSystemLoader = FileSystemScriptLoader(reactApplicationContext, nativeLoader)
    private val coroutineExceptionHandler = CoroutineExceptionHandler { _, throwable ->
        Log.e("ScriptManagerModule", "CoroutineExceptionHandler ${throwable.message}", throwable)
    }
    private val job = SupervisorJob()
    private val coroutineScope = CoroutineScope(job + coroutineExceptionHandler + coroutineDispatcher)

    private fun runInBackground(fn: () -> Unit) {
        coroutineScope.launch { // run in worker thread
            fn()
        }
    }

Could we discuss this item further?

Thanks & BestRegards

baka3k avatar Dec 30 '24 08:12 baka3k

hi @baka3k, I've taken a look at the whole proposal and your implementation and it looks good to me - there are few nitpicks that we would have to address but let's address that within the actual PR (like the JS API which is missing currently). Sorry for the lack of attention for this feature - the load is very high currently and it's simply hard for me to find the time.

Regarding the threading issue - super glad you found it and investigated it 🎉 If you could open a separate PR for that I'll be happy to assist and get this merged!

cc @okwasniewski would love to know your take on this!

jbroma avatar Dec 31 '24 15:12 jbroma

@baka3k Thanks for your great work on this!

@jbroma Proposal looks good for me. This effort will touch many surfaces from introducing new Javascript API to native implementation and documentation so I think agreeing on the implementation and public API beforehand is crucial. @baka3k it would be great if you could add few missing pieces to your proposal before jumping in to implementation:

  • JavaScript API outline
  • "How we teach this" section, describing potential use cases and problems it solves

Thanks in advance!

okwasniewski avatar Jan 07 '25 12:01 okwasniewski

@jbroma @okwasniewski I apologize, I'm currently very busy and will return to this ticket shortly.

Thanks & Best Regards,

baka3k avatar Jan 09 '25 03:01 baka3k

I apologize for taking so long to get back to this topic. I'm considering issues related to anti-tampering, specifically for repackaged libraries.

You can refer to the following diagram for a visual representation

Image

As you can see, with local files, an attacker can completely forge the bundles in various ways (e.g., using adb commands: adb push index.bundle data/data/packagename....). Therefore, solely relying on checking remote bundles is insufficient. The current anti-tampering mechanism of Repack may not be sufficient

I'm considering a more comprehensive approach. What are your thoughts on this

Thanks & Best Regards,

baka3k avatar Jan 22 '25 07:01 baka3k