Skip to content

Commit 1351e94

Browse files
authored
feat(firebaseai): add proper headers for X-Android-Package, X-Android-Cert and x-ios-bundle-identifier (#18076)
* feat(ai): add proper headers for X-Android-Package, X-Android-Cert and x-ios-bundle-identifier * analyze fixes * clean * cleaning * fix casing and feedback
1 parent 4fa10c3 commit 1351e94

18 files changed

Lines changed: 601 additions & 0 deletions

File tree

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
group 'io.flutter.plugins.firebase.ai'
2+
version '1.0-SNAPSHOT'
3+
4+
apply plugin: 'com.android.library'
5+
apply from: file("local-config.gradle")
6+
7+
// AGP 9+ has built-in Kotlin support; older versions need the plugin explicitly.
8+
def agpMajor = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION.tokenize('.')[0] as int
9+
if (agpMajor < 9) {
10+
apply plugin: 'kotlin-android'
11+
}
12+
13+
buildscript {
14+
repositories {
15+
google()
16+
mavenCentral()
17+
}
18+
}
19+
20+
android {
21+
if (project.android.hasProperty("namespace")) {
22+
namespace 'io.flutter.plugins.firebase.ai'
23+
}
24+
25+
compileSdkVersion project.ext.compileSdk
26+
27+
defaultConfig {
28+
minSdkVersion project.ext.minSdk
29+
}
30+
31+
compileOptions {
32+
sourceCompatibility project.ext.javaVersion
33+
targetCompatibility project.ext.javaVersion
34+
}
35+
36+
if (agpMajor < 9) {
37+
kotlinOptions {
38+
jvmTarget = project.ext.javaVersion
39+
}
40+
}
41+
42+
sourceSets {
43+
main.java.srcDirs += "src/main/kotlin"
44+
}
45+
46+
lintOptions {
47+
disable 'InvalidPackage'
48+
}
49+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
ext {
2+
compileSdk=34
3+
minSdk=23
4+
targetSdk=34
5+
javaVersion = JavaVersion.toVersion(17)
6+
androidGradlePluginVersion = '8.3.0'
7+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
2+
package="io.flutter.plugins.firebase.ai">
3+
</manifest>
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
// Copyright 2026 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package io.flutter.plugins.firebase.ai
16+
17+
import android.content.Context
18+
import android.content.pm.PackageManager
19+
import android.os.Build
20+
import android.util.Log
21+
import io.flutter.embedding.engine.plugins.FlutterPlugin
22+
import io.flutter.plugin.common.MethodCall
23+
import io.flutter.plugin.common.MethodChannel
24+
import java.security.MessageDigest
25+
import java.security.NoSuchAlgorithmException
26+
27+
class FirebaseAIPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
28+
private lateinit var channel: MethodChannel
29+
private lateinit var context: Context
30+
31+
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
32+
context = binding.applicationContext
33+
channel = MethodChannel(binding.binaryMessenger, "plugins.flutter.io/firebase_ai")
34+
channel.setMethodCallHandler(this)
35+
}
36+
37+
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
38+
channel.setMethodCallHandler(null)
39+
}
40+
41+
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
42+
when (call.method) {
43+
"getPlatformHeaders" -> {
44+
val headers = mapOf(
45+
"X-Android-Package" to context.packageName,
46+
"X-Android-Cert" to (getSigningCertFingerprint() ?: "")
47+
)
48+
result.success(headers)
49+
}
50+
else -> result.notImplemented()
51+
}
52+
}
53+
54+
@OptIn(ExperimentalStdlibApi::class)
55+
private fun getSigningCertFingerprint(): String? {
56+
val packageName = context.packageName
57+
val signature = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
58+
val packageInfo = try {
59+
context.packageManager.getPackageInfo(
60+
packageName,
61+
PackageManager.GET_SIGNING_CERTIFICATES
62+
)
63+
} catch (e: PackageManager.NameNotFoundException) {
64+
Log.e(TAG, "PackageManager couldn't find the package \"$packageName\"", e)
65+
return null
66+
}
67+
val signingInfo = packageInfo?.signingInfo ?: return null
68+
if (signingInfo.hasMultipleSigners()) {
69+
signingInfo.apkContentsSigners.firstOrNull()
70+
} else {
71+
signingInfo.signingCertificateHistory.lastOrNull()
72+
}
73+
} else {
74+
@Suppress("DEPRECATION")
75+
val packageInfo = try {
76+
context.packageManager.getPackageInfo(
77+
packageName,
78+
PackageManager.GET_SIGNATURES
79+
)
80+
} catch (e: PackageManager.NameNotFoundException) {
81+
Log.e(TAG, "PackageManager couldn't find the package \"$packageName\"", e)
82+
return null
83+
}
84+
@Suppress("DEPRECATION")
85+
packageInfo?.signatures?.firstOrNull()
86+
} ?: return null
87+
88+
return try {
89+
val messageDigest = MessageDigest.getInstance("SHA-1")
90+
val digest = messageDigest.digest(signature.toByteArray())
91+
digest.toHexString(HexFormat.UpperCase)
92+
} catch (e: NoSuchAlgorithmException) {
93+
Log.w(TAG, "No support for SHA-1 algorithm found.", e)
94+
null
95+
}
96+
}
97+
98+
companion object {
99+
private const val TAG = "FirebaseAIPlugin"
100+
}
101+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
require 'yaml'
2+
3+
pubspec = YAML.load_file(File.join('..', 'pubspec.yaml'))
4+
library_version = pubspec['version'].gsub('+', '-')
5+
6+
Pod::Spec.new do |s|
7+
s.name = pubspec['name']
8+
s.version = library_version
9+
s.summary = pubspec['description']
10+
s.description = pubspec['description']
11+
s.homepage = pubspec['homepage']
12+
s.license = { :file => '../LICENSE' }
13+
s.authors = 'The Chromium Authors'
14+
s.source = { :path => '.' }
15+
16+
s.source_files = 'firebase_ai/Sources/firebase_ai/**/*.swift'
17+
18+
s.ios.deployment_target = '15.0'
19+
s.dependency 'Flutter'
20+
21+
s.swift_version = '5.0'
22+
23+
s.static_framework = true
24+
s.pod_target_xcconfig = {
25+
'GCC_PREPROCESSOR_DEFINITIONS' => "LIBRARY_VERSION=\\\"#{library_version}\\\" LIBRARY_NAME=\\\"flutter-fire-ai\\\"",
26+
'DEFINES_MODULE' => 'YES'
27+
}
28+
end
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// Copyright 2026 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
#if canImport(FlutterMacOS)
16+
import FlutterMacOS
17+
#else
18+
import Flutter
19+
#endif
20+
21+
public class FirebaseAIPlugin: NSObject, FlutterPlugin {
22+
public static func register(with registrar: FlutterPluginRegistrar) {
23+
#if canImport(FlutterMacOS)
24+
let messenger = registrar.messenger
25+
#else
26+
let messenger = registrar.messenger()
27+
#endif
28+
29+
let channel = FlutterMethodChannel(
30+
name: "plugins.flutter.io/firebase_ai",
31+
binaryMessenger: messenger
32+
)
33+
let instance = FirebaseAIPlugin()
34+
registrar.addMethodCallDelegate(instance, channel: channel)
35+
}
36+
37+
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
38+
switch call.method {
39+
case "getPlatformHeaders":
40+
var headers: [String: String] = [:]
41+
if let bundleId = Bundle.main.bundleIdentifier {
42+
headers["x-ios-bundle-identifier"] = bundleId
43+
}
44+
result(headers)
45+
default:
46+
result(FlutterMethodNotImplemented)
47+
}
48+
}
49+
}

packages/firebase_ai/firebase_ai/ios/firebase_ai/Sources/firebase_ai/Resources/.gitkeep

Whitespace-only changes.

packages/firebase_ai/firebase_ai/lib/src/base_model.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import 'imagen/imagen_edit.dart';
3636
import 'imagen/imagen_reference.dart';
3737
import 'live_api.dart';
3838
import 'live_session.dart';
39+
import 'platform_header_helper.dart';
3940
import 'tool.dart';
4041

4142
part 'generative_model.dart';
@@ -300,6 +301,10 @@ abstract class BaseModel {
300301
if (app != null && app.isAutomaticDataCollectionEnabled) {
301302
headers['X-Firebase-AppId'] = app.options.appId;
302303
}
304+
// Add platform-specific headers for API key restrictions.
305+
// Android: X-Android-Package + X-Android-Cert
306+
// iOS/macOS: x-ios-bundle-identifier
307+
headers.addAll(await getPlatformSecurityHeaders());
303308
return headers;
304309
};
305310
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Copyright 2026 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import 'package:flutter/foundation.dart';
16+
import 'package:flutter/services.dart';
17+
18+
/// Method channel for the native platform helper plugin.
19+
@visibleForTesting
20+
const platformHeaderChannel = MethodChannel('plugins.flutter.io/firebase_ai');
21+
22+
Map<String, String>? _cachedHeaders;
23+
24+
/// Clears the cached platform headers. Only for use in tests.
25+
@visibleForTesting
26+
void clearPlatformSecurityHeadersCache() {
27+
_cachedHeaders = null;
28+
}
29+
30+
/// Returns platform-specific security headers for API key restrictions.
31+
///
32+
/// Each platform's native plugin returns the appropriate headers:
33+
/// - **Android**: `X-Android-Package` and `X-Android-Cert`
34+
/// - **iOS/macOS**: `x-ios-bundle-identifier`
35+
/// - **Web/other**: empty map (no plugin registered)
36+
///
37+
/// Results are cached since platform identity does not change at runtime.
38+
Future<Map<String, String>> getPlatformSecurityHeaders() async {
39+
if (kIsWeb) return const {};
40+
if (_cachedHeaders != null) return _cachedHeaders!;
41+
42+
try {
43+
final result = await platformHeaderChannel
44+
.invokeMapMethod<String, String>('getPlatformHeaders');
45+
_cachedHeaders = result ?? const {};
46+
} catch (_) {
47+
_cachedHeaders = const {};
48+
}
49+
return _cachedHeaders!;
50+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
require 'yaml'
2+
3+
pubspec = YAML.load_file(File.join('..', 'pubspec.yaml'))
4+
library_version = pubspec['version'].gsub('+', '-')
5+
6+
Pod::Spec.new do |s|
7+
s.name = pubspec['name']
8+
s.version = library_version
9+
s.summary = pubspec['description']
10+
s.description = pubspec['description']
11+
s.homepage = pubspec['homepage']
12+
s.license = { :file => '../LICENSE' }
13+
s.authors = 'The Chromium Authors'
14+
s.source = { :path => '.' }
15+
16+
s.source_files = 'firebase_ai/Sources/firebase_ai/**/*.swift'
17+
18+
s.platform = :osx, '10.15'
19+
s.swift_version = '5.0'
20+
21+
s.dependency 'FlutterMacOS'
22+
23+
s.static_framework = true
24+
s.pod_target_xcconfig = {
25+
'GCC_PREPROCESSOR_DEFINITIONS' => "LIBRARY_VERSION=\\\"#{library_version}\\\" LIBRARY_NAME=\\\"flutter-fire-ai\\\"",
26+
'DEFINES_MODULE' => 'YES'
27+
}
28+
end

0 commit comments

Comments
 (0)