Slide 1

Slide 1 text

Marcel Schnelle @marcelschnelle Introduction to Bytecode Instrumentation with AGP 8

Slide 2

Slide 2 text

// HelloWorld.kt fun main(args: Array) { val name = args.firstOrNull() ?: "Guest" println("Hello $name!") }

Slide 3

Slide 3 text

// ================HelloWorldKt.class ================= // class version 52.0 (52) // access flags 0x31 public final class HelloWorldKt { // access flags 0x19 public final static main([Ljava/lang/String;)V @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0 L0 ALOAD 0 LDC "args" INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullParameter (Ljava/lang/Object;Ljava/lang/String;)V L1 LINENUMBER 2 L1 ALOAD 0 INVOKESTATIC kotlin/collections/ArraysKt.firstOrNull ([Ljava/lang/Object;)Ljava/lang/Object; CHECKCAST java/lang/String DUP IFNONNULL L2 L3 LINENUMBER 2 L3 POP LDC “Guest" (…) // HelloWorld.kt fun main(args: Array) { val name = args.firstOrNull() ?: "Guest" println("Hello $name!") }

Slide 4

Slide 4 text

// ================HelloWorldKt.class ================= // class version 52.0 (52) // access flags 0x31 public final class HelloWorldKt { // access flags 0x19 public final static main([Ljava/lang/String;)V @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0 L0 ALOAD 0 LDC "args" INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullParameter (Ljava/lang/Object;Ljava/lang/String;)V L1 LINENUMBER 2 L1 ALOAD 0 INVOKESTATIC kotlin/collections/ArraysKt.firstOrNull ([Ljava/lang/Object;)Ljava/lang/Object; CHECKCAST java/lang/String DUP IFNONNULL L2 L3 LINENUMBER 2 L3 POP LDC “Guest" (…) // HelloWorld.kt fun main(args: Array) { val name = args.firstOrNull() ?: "Guest" println("Hello $name!") } Source code

Slide 5

Slide 5 text

// ================HelloWorldKt.class ================= // class version 52.0 (52) // access flags 0x31 public final class HelloWorldKt { // access flags 0x19 public final static main([Ljava/lang/String;)V @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0 L0 ALOAD 0 LDC "args" INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullParameter (Ljava/lang/Object;Ljava/lang/String;)V L1 LINENUMBER 2 L1 ALOAD 0 INVOKESTATIC kotlin/collections/ArraysKt.firstOrNull ([Ljava/lang/Object;)Ljava/lang/Object; CHECKCAST java/lang/String DUP IFNONNULL L2 L3 LINENUMBER 2 L3 POP LDC “Guest" (…) // HelloWorld.kt fun main(args: Array) { val name = args.firstOrNull() ?: "Guest" println("Hello $name!") } Java Bytecode

Slide 6

Slide 6 text

Bytecode Instrumentation?

Slide 7

Slide 7 text

// ================HelloWorldKt.class ================= // class version 52.0 (52) // access flags 0x31 public final class HelloWorldKt { // access flags 0x19 public final static main([Ljava/lang/String;)V @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0 L0 ALOAD 0 LDC "args" INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullParameter (Ljava/lang/Object;Ljava/ lang/String;)V L1 LINENUMBER 2 L1 ALOAD 0 INVOKESTATIC kotlin/collections/ArraysKt.firstOrNull ([Ljava/lang/Object;)Ljava/lang/Object; CHECKCAST java/lang/String DUP IFNONNULL L2 L3 LINENUMBER 2 L3 POP LDC “Guest" (…)

Slide 8

Slide 8 text

// ================HelloWorldKt.class ================= // class version 52.0 (52) // access flags 0x31 public final class HelloWorldKt { // access flags 0x19 public final static main([Ljava/lang/String;)V @Lorg/jetbrains/annotations/Nullable;() // invisible, parameter 0 L0 ALOAD 0 LDC "args" INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullParameter (Ljava/lang/Object;Ljava/ lang/String;)V L1 LINENUMBER 2 L1 ALOAD 0 INVOKESTATIC kotlin/collections/ArraysKt.firstOrNull ([Ljava/lang/Object;)Ljava/lang/Object; CHECKCAST java/lang/String DUP IFNONNULL L2 L3 LINENUMBER 2 L3 POP LDC “Admin” (…)

Slide 9

Slide 9 text

Why though? • Desugaring (Dx) • Code optimization & shrinking (ProGuard) • Library-specific features (Sentry, Realm)

Slide 10

Slide 10 text

D8/R8 Instrumentation API Android compilation pipeline Source 
 (.kt) (simplified) Java bytecode 
 (.class) Dalvik executable 
 (.dex)

Slide 11

Slide 11 text

No content

Slide 12

Slide 12 text

@Deprecated (kind of)

Slide 13

Slide 13 text

• Introduced in AGP 7.0, “official” since AGP 8.0 • Bridge between AGP and ASM, a popular Java decomposition library • Analyze and modify Java bytecode with a low level API • Higher level abstractions exist, such as Javassist (→ DK’19 talk) New Bytecode Instrumentation API

Slide 14

Slide 14 text

androidComponents.onVariants { variant -> variant.instrumentation.transformClassesWith( classVisitorFactoryImplClass = MyClassVisitorFactory::class.java, scope = InstrumentationScope.PROJECT, instrumentationParamsConfig = {}, ) } Entry point for bytecode instrumentation // build.gradle.kts Android Components

Slide 15

Slide 15 text

androidComponents.onVariants { variant -> variant.instrumentation.transformClassesWith( classVisitorFactoryImplClass = MyClassVisitorFactory::class.java, scope = InstrumentationScope.PROJECT, instrumentationParamsConfig = {}, ) } • debug • release • build flavors // build.gradle.kts Android Components Entry point for bytecode instrumentation

Slide 16

Slide 16 text

androidComponents.onVariants { variant -> variant.instrumentation.transformClassesWith( classVisitorFactoryImplClass = MyClassVisitorFactory::class.java, scope = InstrumentationScope.PROJECT, instrumentationParamsConfig = {}, ) } • Unrelated to UI tests • Hook for transforms // build.gradle.kts Android Components Entry point for bytecode instrumentation

Slide 17

Slide 17 text

androidComponents.onVariants { variant -> variant.instrumentation.transformClassesWith( classVisitorFactoryImplClass = MyClassVisitorFactory::class.java, scope = InstrumentationScope.PROJECT, instrumentationParamsConfig = {}, ) } • Implementation of AsmClassVisitorFactory • Connection to ASM API // build.gradle.kts Android Components Entry point for bytecode instrumentation

Slide 18

Slide 18 text

androidComponents.onVariants { variant -> variant.instrumentation.transformClassesWith( classVisitorFactoryImplClass = MyClassVisitorFactory::class.java, scope = InstrumentationScope.PROJECT, instrumentationParamsConfig = {}, ) } • Which classes should the transform deal with? • PROJECT or ALL // build.gradle.kts Android Components Entry point for bytecode instrumentation

Slide 19

Slide 19 text

androidComponents.onVariants { variant -> variant.instrumentation.transformClassesWith( classVisitorFactoryImplClass = MyClassVisitorFactory::class.java, scope = InstrumentationScope.PROJECT, instrumentationParamsConfig = {}, ) } • Custom configuration settings • Provided via Gradle plugin • Optional // build.gradle.kts Android Components Entry point for bytecode instrumentation

Slide 20

Slide 20 text

• val parameters: Property • val instrumentationContext: InstrumentationContext • fun isInstrumentable(ClassData): Boolean • fun createClassVisitor(ClassContext, ClassVisitor): ClassVisitor AsmClassVisitorFactory Android Gradle Plugin ObjectWeb ASM

Slide 21

Slide 21 text

• AnnotationVisitor visitAnnotation(String, boolean) • void visitOuterClass(String, String, String) • void visitInnerClass(String, String, String, int) • FieldVisitor visitField(int, String, String, String, Object) • MethodVisitor visitMethod(int, String, String, String, String[]) • ... • void visitEnd() ClassVisitor Android Gradle Plugin ObjectWeb ASM

Slide 22

Slide 22 text

ʮSensitive Data Redactorʯ An exemplary Transform implementation

Slide 23

Slide 23 text

The idea data class User( val name: String, val age: Int, val password: String, val luckyNumber: Int, ) User(name=Hoge, age=33, password=droidsaregreat, luckyNumber=1337) User.toString()

Slide 24

Slide 24 text

The idea data class User( val name: String, val age: Int, @Redacted val password: String, @Redacted val luckyNumber: Int, ) User(name=Hoge, age=33, password=********, luckyNumber=********) User.toString()

Slide 25

Slide 25 text

public toString()Ljava/lang/String; @Lorg/jetbrains/annotations/NotNull;() // invisible NEW java/lang/StringBuilder DUP INVOKESPECIAL java/lang/StringBuilder. ()V LDC "User(name=" INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringB ALOAD 0 GETFIELD de/mannodermaus/dk23/models/User.name : Ljava/lang/String; INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringB LDC ", age=" INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringB ALOAD 0 GETFIELD de/mannodermaus/dk23/models/User.age : I INVOKEVIRTUAL java/lang/StringBuilder.append (I)Ljava/lang/StringBuilder; LDC ", password=" INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringB ALOAD 0 GETFIELD de/mannodermaus/dk23/models/User.password : Ljava/lang/String; INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringB LDC ", luckyNumber=" INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringB ALOAD 0 GETFIELD de/mannodermaus/dk23/models/User.luckyNumber : I INVOKEVIRTUAL java/lang/StringBuilder.append (I)Ljava/lang/StringBuilder; LDC ")" The idea

Slide 26

Slide 26 text

public toString()Ljava/lang/String; @Lorg/jetbrains/annotations/NotNull;() // invisible NEW java/lang/StringBuilder DUP INVOKESPECIAL java/lang/StringBuilder. ()V LDC "User(name=" INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringB ALOAD 0 GETFIELD de/mannodermaus/dk23/models/User.name : Ljava/lang/String; INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringB LDC ", age=" INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringB ALOAD 0 GETFIELD de/mannodermaus/dk23/models/User.age : I INVOKEVIRTUAL java/lang/StringBuilder.append (I)Ljava/lang/StringBuilder; LDC ", password=" INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringB ALOAD 0 LDC “********” GETFIELD de/mannodermaus/dk23/models/User.password : Ljava/lang/String; INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringB LDC ", luckyNumber=" INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringB ALOAD 0 LDC “********” GETFIELD de/mannodermaus/dk23/models/User.luckyNumber : I INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringB LDC ")" The idea

Slide 27

Slide 27 text

• Determine if it is a Kotlin data class • Collect constructor parameters annotated with @Redacted • Rewrite toString() bytecode to redact info about those parameters • Annotate the class with @RedactedMarker afterwards Proposed Workflow “For each class,…”

Slide 28

Slide 28 text

Demonstration https://github.com/mannodermaus/dk23-bytecode

Slide 29

Slide 29 text

Sources & Resources • https://java-decompiler.github.io • https://github.com/romainguy/kotlin-explorer • https://github.com/getsentry/sentry-android-gradle-plugin • https://jakewharton.com/digging-into-d8-and-r8 • Title image by Michel Didier Joomun 
 (https://unsplash.com/photos/TXZV_xE9ZZI)

Slide 30

Slide 30 text

Marcel Schnelle @marcelschnelle Introduction to Bytecode Instrumentation with AGP 8