Skip to content

Commit

Permalink
feat: Obfuscate shards (#1923)
Browse files Browse the repository at this point in the history
Related to #1818

## Test Plan
> How do we know the code works?

Unit tests pass.

## Checklist

- [x] Documented
- [x] Unit tested
  • Loading branch information
jan-goral authored May 14, 2021
1 parent c924773 commit edf2e77
Show file tree
Hide file tree
Showing 8 changed files with 362 additions and 4 deletions.
2 changes: 2 additions & 0 deletions corellium/shard/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ This module is specifying only a data structures for sharding.
## Nested modules

* [:shard:calculate](./calculate)
* [:shard:dump](./calculate)
* [:shard:obfuscate](./calculate)
144 changes: 144 additions & 0 deletions corellium/shard/obfuscate/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
# Shard obfuscation

Allows obfuscating test cases names for security reasons.

## Example

Obfuscation will change the test cases names as following:

```
app1.test1.Test1#case1 -> a.a.A#a
app1.test1.Test1#case2 -> a.a.A#b
app2.test1.Test2#case1 -> b.a.A#a
app2.test1.Test2#case2 -> b.a.A#b
```

So, for the example input structure:

```kotlin
val input: List<List<Shard.App>> =
listOf(
listOf(
Shard.App(
name = "app1",
tests = listOf(
Shard.Test(
name = "app1-test1",
cases = listOf(
Shard.Test.Case(
name = "app1.test1.Test1#case1",
duration = 10_000,
),
)
),
)
),
),
listOf(
Shard.App(
name = "app1",
tests = listOf(
Shard.Test(
name = "app1-test1",
cases = listOf(
Shard.Test.Case(
name = "app1.test1.Test1#case2",
duration = 2_000,
),
)
),
)
),
Shard.App(
name = "app2",
tests = listOf(
Shard.Test(
name = "app2-test1",
cases = listOf(
Shard.Test.Case(
name = "app2.test1.Test2#case1",
duration = 1_000,
),
)
),
Shard.Test(
name = "app2-test1",
cases = listOf(
Shard.Test.Case(
name = "app2.test1.Test2#case2",
),
)
),
)
),
)
)
```

The call of:

```kotlin
input.obfuscate()
```

Should return result equal to the following:

```kotlin
val output: List<List<Shard.App>> =
listOf(
listOf(
Shard.App(
name = "app1",
tests = listOf(
Shard.Test(
name = "app1-test1",
cases = listOf(
Shard.Test.Case(
name = "a.a.A#a",
duration = 10_000,
),
)
),
)
),
),
listOf(
Shard.App(
name = "app1",
tests = listOf(
Shard.Test(
name = "app1-test1",
cases = listOf(
Shard.Test.Case(
name = "a.a.A#b",
duration = 2_000,
),
)
),
)
),
Shard.App(
name = "app2",
tests = listOf(
Shard.Test(
name = "app2-test1",
cases = listOf(
Shard.Test.Case(
name = "b.a.A#a",
duration = 1_000,
),
)
),
Shard.Test(
name = "app2-test1",
cases = listOf(
Shard.Test.Case(
name = "b.a.A#b",
),
)
),
)
),
)
)
```
19 changes: 19 additions & 0 deletions corellium/shard/obfuscate/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
kotlin(Plugins.Kotlin.PLUGIN_JVM)
}

repositories {
jcenter()
mavenCentral()
maven(url = "https://kotlin.bintray.com/kotlinx")
}

tasks.withType<KotlinCompile> { kotlinOptions.jvmTarget = "1.8" }

dependencies {
api(project(":corellium:shard"))

testImplementation(Dependencies.JUNIT)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package flank.corellium.shard

private const val LOWER_CASE_CHARS = "abcdefghijklmnopqrstuvwxyz"
private const val UPPER_CASE_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
private const val ANDROID_TEST_METHOD_SEPARATOR = '#'
private const val ANDROID_PACKAGE_SEPARATOR = '.'
private const val IOS_TEST_METHOD_SEPARATOR = '/'

internal typealias ObfuscationMappings = MutableMap<String, MutableMap<String, String>>

internal fun ObfuscationMappings.obfuscateAndroidTestName(input: String): String {
val obfuscatedPackageNameWithClass =
obfuscateAndroidPackageAndClass(input.split(ANDROID_TEST_METHOD_SEPARATOR).first())

return obfuscatedPackageNameWithClass + obfuscateAndroidMethodIfPresent(input, obfuscatedPackageNameWithClass)
}

private fun ObfuscationMappings.obfuscateAndroidPackageAndClass(packageNameWithClass: String) =
packageNameWithClass
.split(ANDROID_PACKAGE_SEPARATOR)
.fold("") { previous, next ->
val classChunk = getOrPut(previous) { linkedMapOf() }
val obfuscatedPart = classChunk.getOrPut(next) { nextSymbol(next, classChunk) }
if (previous.isBlank()) obfuscatedPart else "$previous$ANDROID_PACKAGE_SEPARATOR$obfuscatedPart"
}

private fun ObfuscationMappings.obfuscateAndroidMethodIfPresent(
input: String,
obfuscatedPackageNameWithClass: String
) = if (input.contains(ANDROID_TEST_METHOD_SEPARATOR))
ANDROID_TEST_METHOD_SEPARATOR + obfuscateMethodName(
methodName = input.split(ANDROID_TEST_METHOD_SEPARATOR).last(),
context = getOrPut(obfuscatedPackageNameWithClass) { mutableMapOf() }
)
else ""

internal fun ObfuscationMappings.obfuscateIosTestName(input: String): String {
val className = input.split(IOS_TEST_METHOD_SEPARATOR).first()
val obfuscatedClassName = getOrPut("") { mutableMapOf() }.run {
getOrPut(className) { nextSymbol(className, this) }
}
return obfuscatedClassName +
IOS_TEST_METHOD_SEPARATOR +
obfuscateMethodName(
methodName = input.split(IOS_TEST_METHOD_SEPARATOR).last(),
context = getOrPut(obfuscatedClassName) { linkedMapOf() }
)
}

private fun nextSymbol(key: String, context: Map<String, String>): String {
val isLowerCaseKey = key.first().isLowerCase()
val possibleSymbols = if (isLowerCaseKey) LOWER_CASE_CHARS else UPPER_CASE_CHARS

val currentContextItemCount = context.values.count {
if (isLowerCaseKey) it.first().isLowerCase() else it.first().isUpperCase()
}
val repeatSymbol = currentContextItemCount / possibleSymbols.length + 1

return possibleSymbols[currentContextItemCount % possibleSymbols.length].toString().repeat(repeatSymbol)
}

private fun obfuscateMethodName(methodName: String, context: MutableMap<String, String>) =
context.getOrPut(methodName) { nextSymbol(methodName, context) }
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package flank.corellium.shard

/**
* Obfuscate each test cases names.
* Be aware that this function is not touching the structure,
* just only hashing each [Shard.Test.Case.name] using alphabetical letters.
* Use for security purpose.
*
* @receiver calculated shards
* @return obfuscated shards
*/
fun obfuscate(shards: Shards): Shards =
// Those nested mappings looks fearfully, but there are just a bunch of iterations where only the last one is important.
shards.map { shard: List<Shard.App> ->
shard.map { app: Shard.App ->
app.copy(
tests = app.tests.map { test: Shard.Test ->
test.copy(
cases = test.cases.map { case: Shard.Test.Case ->
// The only crucial operation which is making the result different than the input.
case.copy(name = obfuscationMappings.obfuscateAndroidTestName(case.name))
}
)
}
)
}
}

internal val obfuscationMappings: ObfuscationMappings = mutableMapOf()
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package flank.corellium.shard

import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotEquals
import org.junit.Test

class ObfuscateTest {

private val shards: List<List<Shard.App>> =
listOf(
listOf(
Shard.App(
name = "app1",
tests = listOf(
Shard.Test(
name = "app1-test1",
cases = listOf(
Shard.Test.Case(
name = "app1.test1.Test1#case1",
duration = 10_000,
),
)
),
)
),
),
listOf(
Shard.App(
name = "app1",
tests = listOf(
Shard.Test(
name = "app1-test1",
cases = listOf(
Shard.Test.Case(
name = "app1.test1.Test1#case2",
duration = 2_000,
),
)
),
)
),
Shard.App(
name = "app2",
tests = listOf(
Shard.Test(
name = "app2-test1",
cases = listOf(
Shard.Test.Case(
name = "app2.test1.Test2#case1",
duration = 1_000,
),
)
),
Shard.Test(
name = "app2-test1",
cases = listOf(
Shard.Test.Case(
name = "app2.test1.Test2#case2",
),
)
),
)
),
)
)

@Test
fun test() {
val obfuscatedShards = obfuscate(shards)

assertEquals(shards.size, obfuscatedShards.size)

shards.forEachIndexed { shardIndex, apps ->
val obfuscatedApps = obfuscatedShards[shardIndex]

assertEquals(apps.size, obfuscatedApps.size)

apps.forEachIndexed { appIndex, app ->
val obfuscatedApp = obfuscatedApps[appIndex]

assertEquals(app.tests.size, obfuscatedApp.tests.size)
assertEquals(app.name, obfuscatedApp.name)

app.tests.forEachIndexed { testIndex, test ->
val obfuscatedTest = obfuscatedApp.tests[testIndex]

assertEquals(test.cases.size, obfuscatedTest.cases.size)
assertEquals(test.name, obfuscatedTest.name)

test.cases.forEachIndexed { caseIndex, case ->
val obfuscatedCase = obfuscatedTest.cases[caseIndex]

println("${case.name} -> ${obfuscatedCase.name}")
assertNotEquals(case.name, obfuscatedCase.name)
}
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ object Shard {
* @property name An abstract name for identifying app.
* @property tests The list of tests modules related to app.
*/
class App(
data class App(
val name: String,
val tests: List<Test>
)
Expand All @@ -20,16 +20,16 @@ object Shard {
* @property name An abstract name for identifying test group.
* @property cases The list of test cases related to group.
*/
class Test(
data class Test(
val name: String,
val cases: List<Case>
) {
/**
* Abstract representation for test case (test method).
* @property name An abstract name for identifying test case.
* @property duration The duration of the test case run. Use default if no previous duration was recorded.
* @property duration The duration of the test case run in milliseconds. Use default if no previous duration was recorded.
*/
class Case(
data class Case(
val name: String,
val duration: Long = DEFAULT_DURATION
)
Expand Down
Loading

0 comments on commit edf2e77

Please sign in to comment.