Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Three ways to use AI on Android: The Good, the Bad and the Ugly

Marcel
April 28, 2024

Three ways to use AI on Android: The Good, the Bad and the Ugly

Session at Android Makers 2024:

Artificial Intelligence is revolutionizing the world, more and more apps are integrating AI, but are we doing it the right way?

In this talk, I will explore practical strategies for using AI responsibly in your Android apps. We will start with the “Ugly”, by showcasing a basic implementation and exploring the consequences. We will move forwards by improving our strategy but exposing the “Bad” pitfalls and leaks we could face. To finish, I will walk you through the “Good” strategy to craft secure prompts, leakproof API keys, and build robust architectures for cost-optimized AI integration in your Android apps.

Check my latest app at https://namewith.ai

Marcel

April 28, 2024
Tweet

More Decks by Marcel

Other Decks in Programming

Transcript

  1. Three ways to use AI on Android Marcel Pintó Biescas

    - Android Dev and Indie-Maker The Good, the Bad and the Ugly
  2. What is Generative AI then? Generative AI Foundation Models Not

    limited to language tasks (image recognition, sound processing, etc…)
  3. What is Generative AI then? Generative AI Foundation Models Large

    Language Models (LLM) Not limited to language tasks (image recognition, sound processing, etc…) GPT 3.5 / 4, LLaMa, Mistral, etc…
  4. What is Generative AI then? Generative AI Foundation Models Large

    Language Models (LLM) ChatGPT Gemini … Not limited to language tasks (image recognition, sound processing, etc…) GPT 3.5 / 4, LLaMa, Mistral, etc… User interface, chatbots
  5. Peak of Inflated Expectations Innovation Trigger Trough of Disillusionment Plateau

    of Productivity Slope of Enlightenm ent Let’s use AI! Time Expectations
  6. • Share text with the app • Create a reply

    Hey, how are you? I have an idea!
  7. • Share text with the app • Create a reply

    • Copy to clipboard Hey, how are you? I have an idea!
  8. • Share text with the app • Create a reply

    • Copy to clipboard • I will make millions! Hey, how are you? I have an idea!
  9. Let’s check the docs 1. Get the API Key (easy!)

    2. Secure API key Using Gemini SDK
  10. Let’s check the docs 1. Get the API Key (easy!)

    2. Secure API key Hardcode Key Using Gemini SDK
  11. Let’s check the docs 1. Get the API Key (easy!)

    2. Secure API key Hardcode Key Using Gemini SDK 3. Create Client
  12. Let’s check the docs 1. Get the API Key (easy!)

    2. Secure API key Hardcode Key Using Gemini SDK 3. Create Client 4. Generate response
  13. @Composable fun ShareScreen(sourceText: String) { val context = LocalContext.current val

    clipboardManager = remember { context.getSystemService<ClipboardManager>()!! } val gemini = remember { GenerativeModel( modelName = "gemini-pro", apiKey = BuildConfig.apiKey ) } var result by remember { mutableStateOf("") } LaunchedEffect(Unit) { try { val prompt = "Create a funny reply for the given text: $sourceText"
  14. } val gemini = remember { GenerativeModel( modelName = "gemini-pro",

    apiKey = BuildConfig.apiKey ) } var result by remember { mutableStateOf("") } LaunchedEffect(Unit) { try { val prompt = "Create a funny reply for the given text: $sourceText" result = gemini.generateContent(prompt).text!! clipboardManager.setPrimaryClip( ClipData.newPlainText("result", result) ) } catch (e: Exception) { result = "Something went wrong" e.printStackTrace() } } // ... }
  15. } val gemini = remember { GenerativeModel( modelName = "gemini-pro",

    apiKey = BuildConfig.apiKey ) } var result by remember { mutableStateOf("") } LaunchedEffect(Unit) { try { val prompt = "Create a funny reply for the given text: $sourceText" result = gemini.generateContent(prompt).text!! clipboardManager.setPrimaryClip( ClipData.newPlainText("result", result) ) } catch (e: Exception) { result = "Something went wrong" e.printStackTrace() } } // ... }
  16. } val gemini = remember { GenerativeModel( modelName = "gemini-pro",

    apiKey = BuildConfig.apiKey ) } var result by remember { mutableStateOf("") } LaunchedEffect(Unit) { try { val prompt = "Create a funny reply for the given text: $sourceText" result = gemini.generateContent(prompt).text!! clipboardManager.setPrimaryClip( ClipData.newPlainText("result", result) ) } catch (e: Exception) { result = "Something went wrong" e.printStackTrace() } } // ... }
  17. MVP is ready! 1. Share any text with the app

    2. Generate an answer 3. Copy to the clipboard 4. Reply!
  18. MVP is ready! 1. Share any text with the app

    2. Generate an answer 3. Copy to the clipboard 4. Reply!
  19. What happened? • Download APK • Use Android Studio (or

    others) • Analyse the APK • Key is right there! API Key was stolen!
  20. Let’s improve this! Obfuscation • R8 & ProGuard • Base64

    or custom logic • Native code • DexGuard
  21. What happened? API Key was stolen! again… Get Public Key

    Get Encrypted Key Locally decrypt API Key
  22. Flow 1. Encrypt API Key 2. Obfuscate public key in

    code 3. Authenticate app/user 4. Return encrypted API Key
  23. Flow 1. Encrypt API Key 2. Obfuscate public key in

    code 3. Authenticate app/user 4. Return encrypted API Key 5. Decrypt with Public Key
  24. A way to do it R8 / ProGuard Jetpack security-crypto

    Firebase Authentication Firebase Remote Config or Firestore
  25. Firebase.appCheck.installAppCheckProviderFactory( PlayIntegrityAppCheckProviderFactory.getInstance(), ) private suspend fun generateResponse(content: String): String {

    val result = Firebase.functions .getHttpsCallable("generate") .call(mapOf("content" to content)) .await() return result.data as String }
  26. Firebase.appCheck.installAppCheckProviderFactory( PlayIntegrityAppCheckProviderFactory.getInstance(), ) private suspend fun generateResponse(content: String): String {

    val result = Firebase.functions .getHttpsCallable("generate") .call(mapOf("content" to content)) .await() return result.data as String }
  27. rateResponse(content: String): String { e.functions e("generate") tent" to content)) s

    String @https_fn.on_call(enforce_app_check=True) def generate(req: https_fn.CallableRequest) -> https_fn.Response: """Given a text, generate a response""" if req.auth is None: raise https_fn.HttpsError( code=https_fn.FunctionsErrorCode.UNAUTHENTICATED, message="User not found.", ) content = req.data.get("content") if not content: raise https_fn. HttpsError( code=https_fn.FunctionsErrorCode.INVALID_ARGUMENT, message="Missing content", ) return generate_response(content)
  28. rateResponse(content: String): String { e.functions e("generate") tent" to content)) s

    String @https_fn.on_call(enforce_app_check=True) def generate(req: https_fn.CallableRequest) -> https_fn.Response: """Given a text, generate a response""" if req.auth is None: raise https_fn.HttpsError( code=https_fn.FunctionsErrorCode.UNAUTHENTICATED, message="User not found.", ) content = req.data.get("content") if not content: raise https_fn. HttpsError( code=https_fn.FunctionsErrorCode.INVALID_ARGUMENT, message="Missing content", ) return generate_response(content)
  29. rateResponse(content: String): String { e.functions e("generate") tent" to content)) s

    String @https_fn.on_call(enforce_app_check=True) def generate(req: https_fn.CallableRequest) -> https_fn.Response: """Given a text, generate a response""" if req.auth is None: raise https_fn.HttpsError( code=https_fn.FunctionsErrorCode.UNAUTHENTICATED, message="User not found.", ) content = req.data.get("content") if not content: raise https_fn. HttpsError( code=https_fn.FunctionsErrorCode.INVALID_ARGUMENT, message="Missing content", ) return generate_response(content)
  30. @https_fn.on_call(enforce_app_check=True) def generate(req: https_fn.CallableRequest) -> https_fn.Response: """Given a text, generate

    a response""" if req.auth is None: raise https_fn.HttpsError( code=https_fn.FunctionsErrorCode.UNAUTHENTICATED, message="User not found.", ) content = req.data.get("content") if not content: raise https_fn. HttpsError( code=https_fn.FunctionsErrorCode.INVALID_ARGUMENT, message="Missing content", ) return generate_response(content)
  31. @https_fn.on_call(enforce_app_check=True) def generate(req: https_fn.CallableRequest) -> https_fn.Response: """Given a text, generate

    a response""" if req.auth is None: raise https_fn.HttpsError( code=https_fn.FunctionsErrorCode.UNAUTHENTICATED, message="User not found.", ) content = req.data.get("content") if not content: raise https_fn. HttpsError( code=https_fn.FunctionsErrorCode.INVALID_ARGUMENT, message="Missing content", ) return generate_response(content)
  32. @https_fn.on_call(enforce_app_check=True) def generate(req: https_fn.CallableRequest) -> https_fn.Response: """Given a text, generate

    a response""" if req.auth is None: raise https_fn.HttpsError( code=https_fn.FunctionsErrorCode.UNAUTHENTICATED, message="User not found.", ) content = req.data.get("content") if not content: raise https_fn. HttpsError( code=https_fn.FunctionsErrorCode.INVALID_ARGUMENT, message="Missing content", ) return generate_response(content)
  33. @https_fn.on_call(enforce_app_check=True) def generate(req: https_fn.CallableRequest) -> https_fn.Response: """Given a text, generate

    a response""" if req.auth is None: raise https_fn.HttpsError( code=https_fn.FunctionsErrorCode.UNAUTHENTICATED, message="User not found.", ) content = req.data.get("content") if not content: raise https_fn. HttpsError( code=https_fn.FunctionsErrorCode.INVALID_ARGUMENT, message="Missing content", ) return generate_response(content)
  34. @https_fn.on_call(enforce_app_check=True) def generate(req: https_fn.CallableRequest) -> https_fn.Response: """Given a text, generate

    a response""" if req.auth is None: raise https_fn.HttpsError( code=https_fn.FunctionsErrorCode.UNAUTHENTICATED, message="User not found.", ) content = req.data.get("content") if not content: raise https_fn. HttpsError( code=https_fn.FunctionsErrorCode.INVALID_ARGUMENT, message="Missing content", ) return generate_response(content)
  35. private suspend fun generateResponse(content: String): String { val result =

    Firebase.functions .getHttpsCallable("generate") .call(mapOf("content" to content)) .await() return result.data as String } @https_fn.on_call(enforce_app_check=T def generate(req: https_fn.CallableRe """Given a text, generate a respo if req.auth is None: raise https_fn.HttpsError( code=https_fn.FunctionsEr message="User not found." ) content = req.data.get("content") if not content: raise https_fn. HttpsError( code=https_fn.FunctionsEr message="Missing content" ) return generate_response(content)
  36. private suspend fun generateResponse(content: String): String { val result =

    Firebase.functions .getHttpsCallable("generate") .call(mapOf("content" to content)) .await() return result.data as String } @https_fn.on_call(enforce_app_check=T def generate(req: https_fn.CallableRe """Given a text, generate a respo if req.auth is None: raise https_fn.HttpsError( code=https_fn.FunctionsEr message="User not found." ) content = req.data.get("content") if not content: raise https_fn. HttpsError( code=https_fn.FunctionsEr message="Missing content" ) return generate_response(content)
  37. THE GOOD THE UGLY THE BAD • Key never in

    the client • Avoids unauthorised usage • Change prompts easily • Server side logic
  38. THE GOOD THE UGLY THE BAD • Key never in

    the client • Avoids unauthorised usage • Change prompts easily • Server side logic • Increased time • More costly • Hard to implement streaming AI response
  39. THE GOOD THE UGLY THE BAD • Key never in

    the client • Avoids unauthorised usage • Change prompts easily • Server side logic • Increased time • More costly • Hard to implement streaming AI response • Super easy to implement • Direct communication • Easy to hack • Hard to update API Key
  40. THE GOOD THE UGLY THE BAD • Key never in

    the client • Avoids unauthorised usage • Change prompts easily • Server side logic • Increased time • More costly • Hard to implement streaming AI response • Super easy to implement • Direct communication • Easy to hack • Hard to update API Key • Direct communication • Fairly secure (for starting) • Easy to update API Key • Increase of cost • Slightly complex to implement • Harder to hack but possible
  41. Some recommendations… • Always use App Check or similar mechanism

    • Do not store the key in Version control
  42. Some recommendations… • Always use App Check or similar mechanism

    • Do not store the key in Version control • Use key management service
  43. Some recommendations… • Always use App Check or similar mechanism

    • Do not store the key in Version control • Use key management service • Prefer Proxy-server to local keys
  44. Some recommendations… • Always use App Check or similar mechanism

    • Do not store the key in Version control • Use key management service • Prefer Proxy-server to local keys • Do not store unencrypted key in device storage
  45. Some recommendations… • Always use App Check or similar mechanism

    • Do not store the key in Version control • Use key management service • Prefer Proxy-server to local keys • Do not store unencrypted key in device storage • Import encrypted keys into secure hardware (e.g KeyStore)
  46. Some recommendations… • Always use App Check or similar mechanism

    • Do not store the key in Version control • Use key management service • Prefer Proxy-server to local keys • Do not store unencrypted key in device storage • Import encrypted keys into secure hardware (e.g KeyStore) • Set hard-limits to your API when possible.
  47. Security last words THE GOOD THE UGLY THE BAD HARD

    NOT so EASY EASY "There's no silver bullet"