Edit an AndroidManifest when compiling to remove API-key - android

I'm currently working on an Android project, and learning how to use git.
I'm blocked because of a problem with git : I have my Google Maps api key declared in my android-manifest file :
<meta-data
android:name="com.google.android.maps.v2.API_KEY"
android:value="HEREISMYKEY"/>
Now, I'd like to push my code in github, but I can't push my AndroidManifest, because it contains my api key (which is supposed to remain secret).
I'd like to know if there is a way to modify it before every push, or maybe modify it each time I compile my application?
Thank you for your help !

Create a new *.xml file in your res/values (call it api-keys.xml or something similar).
Change your manifest to point to this string:
<meta-data
android:name="com.google.android.maps.v2.API_KEY"
android:value="#string/GoogleMapsKey"/>
When you first push your changes to the public, put in a dummy key.
Then edit the file with your real API key.
Then add the file to your .gitignore file so it doesn't get pushed again.

I suggest to use Gradle to dynamically load the API key for different build types and load a dummy key if none is found. This has the advantage that also a CI can build your application without the need of a real API key, but every new developer in your team has to set up the API key.
create a properties file google-maps-api.properties in your root gradle folder. Ignore the file by adding this line gradle/google-maps-api.properties to your .gitignore and put in the following 2 properties:
googleMapsApiKeyDebug=[YOUR_DEBUG_GOOGLE_MAPS_API_KEY_HERE]
googleMapsApiKeyRelease=[YOUR_RELEASE_GOOGLE_MAPS_API_KEY_HERE]
Next create the following google-maps-api-config.gradle file in your root gradle folder. This Gradle file will load the property file you created in step 1.
ext {
googleMapsApiConfig = new GoogleMapsApiConfigLogic(project)
}
/*
Before you run your application, you need a Google Maps API key.
Using these values:
Package name:
at.ict4d.covid19map
SHA-1 certificate fingerprint:
DE:4A:F0:B1:F5:F4:00:88:B6:E0:03:3E:31:1F:11:88:3F:43:0D:8B
Follow the directions here:
https://developers.google.com/maps/documentation/android/start#get-key
Once you have your key (it starts with "AIza") create a file with the name "google-maps-api.properties" in the gradle folder:
gradle/google-maps-api.properties
Put in 2 properties:
googleMapsApiKeyDebug:[THE KEY YOU JUST CREATED]
googleMapsApiKeyRelease:[YOUR RELEASE GOOGLE MAPS KEY] (if you are just developing and not planning to publish then fill in "YOUR_RELEASE_KEY_HERE")
"gradle/google-maps-api.properties" example:
googleMapsApiKeyDebug=AIzaFJKLJKsdLFDDsjlkdfDFJKLdjkf
googleMapsApiKeyRelease=YOUR_RELEASE_KEY_HERE
Sync your project and build.
*/
class GoogleMapsApiConfigLogic {
public final keyGoogleMapsApiKeyDebug = "googleMapsApiKeyDebug"
public final keyGoogleMapsApiKeyRelease = "googleMapsApiKeyRelease"
private final configGoogleMapsApiPropsFileName = "gradle/google-maps-api.properties"
public final props
private final project
GoogleMapsApiConfigLogic(project) {
this.project = project
this.props = loadGoogleMapsApiProperties()
}
def loadGoogleMapsApiProperties() {
Properties propertiesObj
if ((propertiesObj = loadFromProperties("$project.rootDir/$configGoogleMapsApiPropsFileName")) != null) {
project.logger.quiet("google-maps-api-config: use local properties file for Google Maps API")
return propertiesObj
} else {
propertiesObj = new Properties()
propertiesObj.put(keyGoogleMapsApiKeyDebug, "YOUR_KEY_HERE")
propertiesObj.put(keyGoogleMapsApiKeyRelease, "YOUR_KEY_HERE")
}
project.logger.quiet("google-maps-api-config: no API key found for Google Maps")
return propertiesObj
}
def loadFromProperties(fileName) {
Properties props = new Properties()
File propFile = new File(fileName)
if (propFile.exists()) {
props.load(new FileInputStream(propFile))
if (props.get(keyGoogleMapsApiKeyDebug) != null &&
props.get(keyGoogleMapsApiKeyRelease) != null) {
project.logger.quiet("google-maps-api-config: use $fileName for Google Maps API")
return props
}
} else {
project.logger.quiet("google-maps-api-config: $propFile does not exist for Google Maps API")
}
return null
}
}
Add the the following to your app/build.gradle file:
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-android-extensions'
// ...
}
apply from: "$rootDir/gradle/google-maps-api-config.gradle" // handles Google Maps API key
android {
// ...
buildTypes {
release {
// other config
resValue "string", "google_maps_key", "\"${googleMapsApiConfig.props.getProperty(googleMapsApiConfig.keyGoogleMapsApiKeyRelease)}\""
}
debug {
// other config
resValue "string", "google_maps_key", "\"${googleMapsApiConfig.props.getProperty(googleMapsApiConfig.keyGoogleMapsApiKeyDebug)}\""
}
}
This will create a String Resource with the key google_maps_key and the value of your API key. You can delete any other API key references, e.g. Android Studio generate the following files: app/src/debug/res/values/google_maps_api.xml and app/src/release/res/values/google_maps_api.xml - you can delete them.
Add the Google Maps API Key to your manifest:
<application>
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="#string/google_maps_key" />

The way I have approached similar issues in the past is by using specific branches in my git repo for pubic pushes.
Say you have a local master branch with your keys in the manifest. When you are ready to push to github (or wherever else) you can make a new "release" branch with no history. You can see a bit more about that here: How to push new branch without history git branch --orphan release. Once you do that remove all private information, commit all files and push only that branch to github git push origin release.
The issue with this is you will not have a commit history, maybe someone else can come up with something better.

If you create Google Maps Activity from Android Studio as a template it will add needed plugin in your build.gradle file, and then you can put your API key in local.properties file and use it as variable in Manifest file as for example: "${API_KEY}"
For more information visit https://developers.google.com/maps/documentation/places/android-sdk/secrets-gradle-plugin

Related

In my Android app how can I define properties that won't get checked into the version control system?

I want to define an API key as property in my Adroid app written in Kotlin so that it won't get checked into github.
I have found several sources of inspiration, as in
https://medium.com/swlh/how-to-safely-store-credentials-in-android-projects-using-gradle-properties-8cf500561095
and faced errors like
Cannot get property 'xyz' on extra properties extension as it does not exist
Cannot get property 'compileSdkVersion' on extra properties extension as it does not exist Open File
but with no simple solution.
I'd like to share how I eventually implemented it. Please comment and or suggest improvements.
Create a new file secret.properties at the root of your project. This file will store properties that should remain secret. Add it to .gitignore so it won't be checked into the version control system (e.g. github).
In addition, we want to be able to set or overwrite those properties with using environment variables.
Add a buildscript at the top of your app's build.gradle to read the secret.properties file.
Call buildConfigField() in the defaultConfig section. You must provide a string wrapped inside escaped double quotes (see example here).
build.gradle
buildscript {
Properties properties = new Properties()
def propertiesFile = rootProject.file('secret.properties')
if (propertiesFile.exists()) {
properties.load(propertiesFile.newDataInputStream())
}
def localPropertyValue = properties.getProperty('custom.variable')
def systemEnvValue = System.getenv('CUSTOM_VARIABLE')
rootProject.ext.custom_variable = systemEnvValue != null ? systemEnvValue : localPropertyValue
}
plugins {
...
}
android {
...
defaultConfig {
...
buildConfigField("String","CUSTOM_VARIABLE", "\"${rootProject.ext.custom_variable}\"")
}
...
}
secret.properties
custom.variable=my_custom_variable_value_here
Remember to click on the "Sync now" button at the top of your build.gradle file after you've edited and saved it.
Finally, rebuild your project and the new property becomes available using BuildConfig.CUSTOM_VARIABLE throughout your code.

How to secure Fabric API Key from APK decompilation?

While decompiling my android app APK file, I have found the fabric ApiKey key in manifest file. How to secure Fabric API Key from APK decompilation?
I have done following code changes for hiding Fabric API key from manifest file. But still it is visible after APK decompilation.
I added my fabric API key
FabricAPIKey=0123456789ABCDEF012345123456789ABCDEF012
in gradle.properties.
In build.gradle(Module)
...........
def FABRIC_API_ID = FabricAPIKey
.....
buildTypes {
debug {
..........
manifestPlaceholders = [//this is used for defining the variable for manifest file
FABRIC_API_KEY:FABRIC_API_ID
]
}
release{ ..........
manifestPlaceholders = [//this is used for defining the variable for manifest file
FABRIC_API_KEY:FABRIC_API_ID
]
}
And in AndroidManifest.xml
<meta-data
android:name="io.fabric.ApiKey"
android:value="${FABRIC_API_KEY}" />
Mike from Fabric here. Seva's point should be well noticed - "a sufficiently motivated hacker can eventually get to it, given a debugger and enough time".
If you want, you can place the API key and Build secret in a fabric.properties file. Copy your api key out of your android manifest, and delete the line that reads: <meta-data android:name="com.crashlytics.ApiKey" android:value="YOUR_API_KEY_HERE"/>
Then make a file called fabric.properties and place this folder in the root of the module that applies crashlytics in its' build.gradle In the fabric.properies file, add:apiKey=YOUR_API_KEY_HERE
Once that's complete, refresh your dependencies to pull in the change: ./gradlew clean --refresh-dependencies
Try to save that in strings.xml and refer that here.
Then manifest will only show resource id in int format.
But if you open the strings.xml file it will be retrieved.

Setting FabricIO key from Gradle: "Crashalytics developer tools error"

So, I have this code in my build.gradle:
defaultConfig {
manifestPlaceholders = [fabricKey: getFabricKey()]
}
def getFabricKey() {
String path = System.getenv("HOME") + "/.apikeys/fabric_key.gradle"
if (file(path).exists()) {
return new File(path).text;
} else {
return "null";
}
}
While in AndroidManifest.xml I have this:
<meta-data
android:name="io.fabric.ApiKey"
android:value="${fabricKey}" />
~/.apikeys/fabric_key.gradle exist on my machine. However, when I'm trying to build the project, "Crashalytics developer tools error" pops up.
If I just place my key instead of ${fabricKey}, it works just fine.
What am I doing wrong?
Also, just to clarify things: What I want to achieve is, while having apikey removed from publically-accessible git repo, still be able to produce builds on my Jenkins server.
EDIT:
After running gradlew assembleDebug by-hand instead of IDE, I've found out this:
java.lang.IllegalArgumentException: Crashlytics found an invalid API key: myValidApiKey .
(where myValidApiKey is a key that works when replacing ${fabricKey} with itself).

Crashlytics (Fabric) separate organizations for application variants (build types, product flavors)

This is self-answered question to share my knowledge.
I have a project with multiple product flavors and I want to integrate Fabric using separate organizations for each product flavor.
I tried to integrate Fabric using Android Studio Fabric Plugin. It adds
<meta-data
android:name="io.fabric.ApiKey"
android:value="DEFAULT_ORGANIZATION_API_KEY" />
entry to AndroidManifest.xml of main source set.
I decided to rewrite this entry in application variant specific source sets:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application>
<meta-data
android:name="io.fabric.ApiKey"
android:value="SECOND_ORGANIZATION_API_KEY"
tools:replace="android:value" />
</application>
</manifest>
Then I discovered that Fabric Gradle plugin generates crashlytics.properties file with fabric api secret (AKA build secret) during build and I should include this file to source control. But this file is overwritten each time I build specific application variant because api secret is unique for each application.
How can I integrate Fabric using separate organizations for each application variant?
During the build fabricGenerateResources task is called and it looks for a file named fabric.properties with following content:
apiSecret=YOUR_BUILD_SECRET
apiKey=YOUR_API_KEY
So all we need is to generate fabric.properties file before this.
I found this solution and slightly modified it to fully support application variants not only build types.
Add this code to android section of build.gradle:
File crashlyticsProperties = new File("${project.projectDir.absolutePath}/fabric.properties")
applicationVariants.all { variant ->
variant.productFlavors.each { flavor ->
def variantSuffix = variant.name.capitalize()
def generatePropertiesTask = task("fabricGenerateProperties${variantSuffix}") << {
Properties properties = new Properties()
properties.put("apiKey", flavor.fabricApiKey)
properties.put("apiSecret", flavor.fabricApiSecret)
properties.store(new FileWriter(crashlyticsProperties), "")
}
def generateResourcesTask = project.tasks.getByName("fabricGenerateResources${variantSuffix}")
generateResourcesTask.dependsOn generatePropertiesTask
generateResourcesTask.doLast {
println "Removing fabric.properties"
crashlyticsProperties.delete()
}
}
}
It iterates over application variants and for each application variant creates task that generates fabric.properties file and task that deletes this file after Fabric Gradle plugin generates application resources.
All you need now is to define product flavor or build type specific fabricApiKey and fabricApiSecret:
productFlavors {
flavor1 {
ext.fabricApiKey = "FLAVOR1_API_KEY"
ext.fabricApiSecret = "FLAVOR1_API_SECRET"
}
}
ext is an ExtraPropertiesExtention object provided by every ExtensionAware object. It allows new properties to be added to existing object. In my case flavor1 is ExtensionAware object and it can be extended with new properties by using ext.someProperty = "value" syntax and later these properties can be used as flavor.someProperty, flavor.fabricApiKey.
Also it's better to include fabric.properties to .gitignore.
And do not forget to remove ext.enableCrashlytics = false from debug build type if you used it to disable Crashlytics during debug. Instead of this you can disable it in Application.onCreate:
Fabric.with(this, new Crashlytics.Builder().core(
new CrashlyticsCore.Builder().disabled(BuildConfig.DEBUG).build()).build());
If you're not opposed to using an application id suffix, you don't need separate organizations. The crashes and answers will be treated as separate apps.
For instance, let's say my application id is io.example
In your build.gradle:
buildTypes {
debug {
applicationIdSuffix ".debug"
}
release {
//options
}
}
After you deploy the debug version to a device or emulator, on the Fabric site you will see two apps:
io.example
io.example.debug
One thing that is nice about this approach is that you can also keep track of other build flavors seprately: io.exmaple.free, io.exmaple.paid, io.example.exterprise, and so on.
A simpler solution, which is also compatible with Gradle 5.x+ is to create separate fabric.properties files for each of the build variants that needs a unique Fabric API key and secret. Create the fabric.properties files as:
#Contains API Secret used to validate your application. Commit to internal source control; avoid making secret public.
apiSecret=YOUR_API_SECRET
apiKey=YOUR_API_KEY
replacing YOUR_API_SECRET with the build variant's API secret and YOUR_API_KEY with the build variant's API key.
Then place each variant's fabric.properties under the project src/variant folder, e.g. app/src/debug or app/src/release. See documentation on build variants for additional details.
At build time, the fabric.properties for the variant being built will be used.

Crashlytics found an invalid API key

When I am trying to build project with value of meta-data tag as a string reference, crashlytics fail with following error:
Crashlytics found an invalid API key: #string/crashlytics.
Check the Crashlytics plugin to make sure that the application has been added successfully!
Contact support#crashlytics.com for assistance.
Doesn't work
<meta-data
android:name="com.crashlytics.ApiKey"
android:value="#string/crashlytics"/>
Works
<meta-data
android:name="com.crashlytics.ApiKey"
android:value="1234567890..."/>
I am want to define different keys inside string.xml for different productFlavors of my android project.
Update
After writing to crashlytics support:
Currently we only are able to evaluate the AndroidManifest.xml at build time so we don't look at any strings resources so we only support a hard coded string. I'll definitely share this with the team that you're interested so we can look into supporting this in a future release.
Edit: The solution accepted is working only if you are using an old version of Crashlytics (I was using v1.1.11). If you are using Fabric SDK you will notice
the tasks of the plugin have changed considerably and the script below
will not work. Also the API secret is not needed anymore, therefore
you can just use the <meta> in the manifest to specify the API key along with a manifest placeholder defined in your flavor:
in build.gradle:
flavor1 {
...
manifestPlaceholders = [crashlyticsApiKey: CRASHLYTICS_API_SECRET_HERE]
...
}
in AndroidManifest.xml:
...
<meta-data
android:name="com.crashlytics.ApiKey"
android:value="${crashlyticsApiKey}" />
...
There is another undocumented way to specify the Crashlytics key as noted here, and it is to use the crashlytics.properties (in the root of your project) to specify that value along with the API secret:
apiKey=YOUR_API_KEY
apiSecret=YOUR_API_SECRET
Unfortuntately this will not allow you to simply specify a different crashlytics.properties for each flavor, because it needs to be in the root of your project in order to be picked correctly by the gradle plugin. That means you need to generate that file dynamically.
The idea is to add the key/secret values in your flavor as custom properties, and generate the crashlytics.properties at buildtime, using the values from the current flavor to fill the file.
The build.gradle inside your android module should look like this:
...
productFlavors {
flavor1 {
...
set("crashlyticsApiKey", CRASHLYTICS_API_KEY_HERE)
set("crashlyticsApiSecret", CRASHLYTICS_API_SECRET_HERE)
...
}
...
}
File crashlyticsProperties = new File("${project.projectDir.absolutePath}/crashlytics.properties")
applicationVariants.all { variant ->
variant.productFlavors.each { flavor ->
def variantSuffix = variant.name.capitalize()
def generateResourcesTask = project.tasks.getByName("crashlyticsGenerateResources${variantSuffix}")
def generatePropertiesTask = task("crashlyticsGenerateProperties${variantSuffix}") << {
Properties properties = new Properties()
println "...copying apiSecret for ${variant.name}"
properties.put("apiSecret", flavor.crashlyticsApiSecret)
println "...copying apiKey for ${variant.name}"
properties.put("apiKey", flavor.crashlyticsApiKey)
properties.store(new FileWriter(crashlyticsProperties), "")
}
generateResourcesTask.dependsOn generatePropertiesTask
def cleanResourcesTask = project.tasks.getByName("crashlyticsCleanupResourcesAfterUpload${variantSuffix}")
cleanResourcesTask.doLast {
println "...removing crashlytics.properties"
crashlyticsProperties.delete()
}
}
}
...
Basically the script hooks in the building process and generates/fills the properties file just before the Crashlytics gradle plugin does its magic.
With Fabric's Crashlytics 2.6.6. I'm able to simply select a Build Variant from the menu (usually located on the left in Android Studio) and run the app. It takes a minute or so to propagate through to the Fabric dashboard, but I didn't have any need for a workaround.

Categories

Resources