initial commit, 4.5 stable
Some checks failed
🔗 GHA / 📊 Static checks (push) Has been cancelled
🔗 GHA / 🤖 Android (push) Has been cancelled
🔗 GHA / 🍏 iOS (push) Has been cancelled
🔗 GHA / 🐧 Linux (push) Has been cancelled
🔗 GHA / 🍎 macOS (push) Has been cancelled
🔗 GHA / 🏁 Windows (push) Has been cancelled
🔗 GHA / 🌐 Web (push) Has been cancelled
Some checks failed
🔗 GHA / 📊 Static checks (push) Has been cancelled
🔗 GHA / 🤖 Android (push) Has been cancelled
🔗 GHA / 🍏 iOS (push) Has been cancelled
🔗 GHA / 🐧 Linux (push) Has been cancelled
🔗 GHA / 🍎 macOS (push) Has been cancelled
🔗 GHA / 🏁 Windows (push) Has been cancelled
🔗 GHA / 🌐 Web (push) Has been cancelled
This commit is contained in:
41
platform/android/java/THIRDPARTY.md
Normal file
41
platform/android/java/THIRDPARTY.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# Third-party libraries
|
||||
|
||||
This file list third-party libraries used in the Android source folder,
|
||||
with their provenance and, when relevant, modifications made to those files.
|
||||
|
||||
## com.google.android.vending.expansion.downloader
|
||||
|
||||
- Upstream: https://github.com/google/play-apk-expansion/tree/master/apkx_library
|
||||
- Version: git (9ecf54e, 2017)
|
||||
- License: Apache 2.0
|
||||
|
||||
Overwrite all files under:
|
||||
|
||||
- `lib/src/com/google/android/vending/expansion/downloader`
|
||||
|
||||
Some files have been modified for yet unclear reasons.
|
||||
See the `lib/patches/com.google.android.vending.expansion.downloader.patch` file.
|
||||
|
||||
## com.google.android.vending.licensing
|
||||
|
||||
- Upstream: https://github.com/google/play-licensing/tree/master/lvl_library/
|
||||
- Version: git (eb57657, 2018) with modifications
|
||||
- License: Apache 2.0
|
||||
|
||||
Overwrite all files under:
|
||||
|
||||
- `lib/aidl/com/android/vending/licensing`
|
||||
- `lib/src/com/google/android/vending/licensing`
|
||||
|
||||
Some files have been modified to silence linter errors or fix downstream issues.
|
||||
See the `lib/patches/com.google.android.vending.licensing.patch` file.
|
||||
|
||||
## com.android.apksig
|
||||
|
||||
- Upstream: https://android.googlesource.com/platform/tools/apksig/+/ac5cbb07d87cc342fcf07715857a812305d69888
|
||||
- Version: git (ac5cbb07d87cc342fcf07715857a812305d69888, 2024)
|
||||
- License: Apache 2.0
|
||||
|
||||
Overwrite all files under:
|
||||
|
||||
- `editor/src/main/java/com/android/apksig`
|
||||
54
platform/android/java/app/AndroidManifest.xml
Normal file
54
platform/android/java/app/AndroidManifest.xml
Normal file
@@ -0,0 +1,54 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:versionCode="1"
|
||||
android:versionName="1.0"
|
||||
android:installLocation="auto" >
|
||||
|
||||
<supports-screens
|
||||
android:smallScreens="true"
|
||||
android:normalScreens="true"
|
||||
android:largeScreens="true"
|
||||
android:xlargeScreens="true" />
|
||||
|
||||
<uses-feature
|
||||
android:glEsVersion="0x00030000"
|
||||
android:required="true" />
|
||||
|
||||
<application
|
||||
android:label="@string/godot_project_name_string"
|
||||
android:allowBackup="false"
|
||||
android:icon="@mipmap/icon"
|
||||
android:appCategory="game"
|
||||
android:isGame="true"
|
||||
android:hasFragileUserData="false"
|
||||
android:requestLegacyExternalStorage="false"
|
||||
tools:ignore="GoogleAppIndexingWarning" >
|
||||
<profileable
|
||||
android:shell="true"
|
||||
android:enabled="true"
|
||||
tools:targetApi="29" />
|
||||
|
||||
<activity
|
||||
android:name=".GodotApp"
|
||||
android:label="@string/godot_project_name_string"
|
||||
android:theme="@style/GodotAppSplashTheme"
|
||||
android:launchMode="singleInstancePerTask"
|
||||
android:excludeFromRecents="false"
|
||||
android:exported="true"
|
||||
android:screenOrientation="landscape"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:configChanges="layoutDirection|locale|orientation|keyboardHidden|screenSize|smallestScreenSize|density|keyboard|navigation|screenLayout|uiMode"
|
||||
android:resizeableActivity="false"
|
||||
tools:ignore="UnusedAttribute" >
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
10
platform/android/java/app/assetPackInstallTime/build.gradle
Normal file
10
platform/android/java/app/assetPackInstallTime/build.gradle
Normal file
@@ -0,0 +1,10 @@
|
||||
plugins {
|
||||
id 'com.android.asset-pack'
|
||||
}
|
||||
|
||||
assetPack {
|
||||
packName = "assetPackInstallTime" // Directory name for the asset pack
|
||||
dynamicDelivery {
|
||||
deliveryType = "install-time" // Delivery mode
|
||||
}
|
||||
}
|
||||
2
platform/android/java/app/assets/.gitignore
vendored
Normal file
2
platform/android/java/app/assets/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
293
platform/android/java/app/build.gradle
Normal file
293
platform/android/java/app/build.gradle
Normal file
@@ -0,0 +1,293 @@
|
||||
// Gradle build config for Godot Engine's Android port.
|
||||
plugins {
|
||||
id 'com.android.application'
|
||||
id 'org.jetbrains.kotlin.android'
|
||||
}
|
||||
|
||||
apply from: 'config.gradle'
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
maven { url "https://plugins.gradle.org/m2/" }
|
||||
maven { url "https://central.sonatype.com/repository/maven-snapshots/"}
|
||||
|
||||
// Godot user plugins custom maven repos
|
||||
String[] mavenRepos = getGodotPluginsMavenRepos()
|
||||
if (mavenRepos != null && mavenRepos.size() > 0) {
|
||||
for (String repoUrl : mavenRepos) {
|
||||
maven {
|
||||
url repoUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
configurations {
|
||||
// Initializes a placeholder for the devImplementation dependency configuration.
|
||||
devImplementation {}
|
||||
// Initializes a placeholder for the monoImplementation dependency configuration.
|
||||
monoImplementation {}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "androidx.fragment:fragment:$versions.fragmentVersion"
|
||||
implementation "androidx.core:core-splashscreen:$versions.splashscreenVersion"
|
||||
|
||||
if (rootProject.findProject(":lib")) {
|
||||
implementation project(":lib")
|
||||
} else if (rootProject.findProject(":godot:lib")) {
|
||||
implementation project(":godot:lib")
|
||||
} else {
|
||||
// Godot gradle build mode. In this scenario this project is the only one around and the Godot
|
||||
// library is available through the pre-generated godot-lib.*.aar android archive files.
|
||||
debugImplementation fileTree(dir: 'libs/debug', include: ['**/*.jar', '*.aar'])
|
||||
devImplementation fileTree(dir: 'libs/dev', include: ['**/*.jar', '*.aar'])
|
||||
releaseImplementation fileTree(dir: 'libs/release', include: ['**/*.jar', '*.aar'])
|
||||
}
|
||||
|
||||
// Godot user plugins remote dependencies
|
||||
String[] remoteDeps = getGodotPluginsRemoteBinaries()
|
||||
if (remoteDeps != null && remoteDeps.size() > 0) {
|
||||
for (String dep : remoteDeps) {
|
||||
implementation dep
|
||||
}
|
||||
}
|
||||
|
||||
// Godot user plugins local dependencies
|
||||
String[] pluginsBinaries = getGodotPluginsLocalBinaries()
|
||||
if (pluginsBinaries != null && pluginsBinaries.size() > 0) {
|
||||
implementation files(pluginsBinaries)
|
||||
}
|
||||
|
||||
// Automatically pick up local dependencies in res://addons
|
||||
String addonsDirectory = getAddonsDirectory()
|
||||
if (addonsDirectory != null && !addonsDirectory.isBlank()) {
|
||||
implementation fileTree(dir: "$addonsDirectory", include: ['*.jar', '*.aar'])
|
||||
}
|
||||
|
||||
// .NET dependencies
|
||||
String jar = '../../../../modules/mono/thirdparty/libSystem.Security.Cryptography.Native.Android.jar'
|
||||
if (file(jar).exists()) {
|
||||
monoImplementation files(jar)
|
||||
}
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion versions.compileSdk
|
||||
buildToolsVersion versions.buildTools
|
||||
ndkVersion versions.ndkVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility versions.javaVersion
|
||||
targetCompatibility versions.javaVersion
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = versions.javaVersion
|
||||
}
|
||||
|
||||
assetPacks = [":assetPackInstallTime"]
|
||||
|
||||
namespace = 'com.godot.game'
|
||||
|
||||
defaultConfig {
|
||||
// The default ignore pattern for the 'assets' directory includes hidden files and directories which are used by Godot projects.
|
||||
aaptOptions {
|
||||
ignoreAssetsPattern "!.svn:!.git:!.gitignore:!.ds_store:!*.scc:<dir>_*:!CVS:!thumbs.db:!picasa.ini:!*~"
|
||||
}
|
||||
|
||||
ndk {
|
||||
debugSymbolLevel 'NONE'
|
||||
String[] export_abi_list = getExportEnabledABIs()
|
||||
abiFilters export_abi_list
|
||||
}
|
||||
|
||||
// Feel free to modify the application id to your own.
|
||||
applicationId getExportPackageName()
|
||||
versionCode getExportVersionCode()
|
||||
versionName getExportVersionName()
|
||||
minSdkVersion getExportMinSdkVersion()
|
||||
targetSdkVersion getExportTargetSdkVersion()
|
||||
|
||||
missingDimensionStrategy 'products', 'template'
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
abortOnError false
|
||||
disable 'MissingTranslation', 'UnusedResources'
|
||||
}
|
||||
|
||||
ndkVersion versions.ndkVersion
|
||||
|
||||
packagingOptions {
|
||||
exclude 'META-INF/LICENSE'
|
||||
exclude 'META-INF/NOTICE'
|
||||
|
||||
// Debug symbols are kept for development within Android Studio.
|
||||
if (shouldNotStrip()) {
|
||||
jniLibs {
|
||||
keepDebugSymbols += '**/*.so'
|
||||
}
|
||||
}
|
||||
|
||||
jniLibs {
|
||||
// Setting this to true causes AGP to package compressed native libraries when building the app
|
||||
// For more background, see:
|
||||
// - https://developer.android.com/build/releases/past-releases/agp-3-6-0-release-notes#extractNativeLibs
|
||||
// - https://stackoverflow.com/a/44704840
|
||||
useLegacyPackaging shouldUseLegacyPackaging()
|
||||
}
|
||||
|
||||
// Always select Godot's version of libc++_shared.so in case deps have their own
|
||||
pickFirst 'lib/x86/libc++_shared.so'
|
||||
pickFirst 'lib/x86_64/libc++_shared.so'
|
||||
pickFirst 'lib/armeabi-v7a/libc++_shared.so'
|
||||
pickFirst 'lib/arm64-v8a/libc++_shared.so'
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
debug {
|
||||
if (hasCustomDebugKeystore()) {
|
||||
storeFile new File(getDebugKeystoreFile())
|
||||
storePassword getDebugKeystorePassword()
|
||||
keyAlias getDebugKeyAlias()
|
||||
keyPassword getDebugKeystorePassword()
|
||||
}
|
||||
}
|
||||
|
||||
release {
|
||||
File keystoreFile = new File(getReleaseKeystoreFile())
|
||||
if (keystoreFile.isFile()) {
|
||||
storeFile keystoreFile
|
||||
storePassword getReleaseKeystorePassword()
|
||||
keyAlias getReleaseKeyAlias()
|
||||
keyPassword getReleaseKeystorePassword()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
buildConfig = true
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
|
||||
debug {
|
||||
// Signing and zip-aligning are skipped for prebuilt builds, but
|
||||
// performed for Godot gradle builds.
|
||||
zipAlignEnabled shouldZipAlign()
|
||||
if (shouldSign()) {
|
||||
signingConfig signingConfigs.debug
|
||||
} else {
|
||||
signingConfig null
|
||||
}
|
||||
}
|
||||
|
||||
dev {
|
||||
initWith debug
|
||||
// Signing and zip-aligning are skipped for prebuilt builds, but
|
||||
// performed for Godot gradle builds.
|
||||
zipAlignEnabled shouldZipAlign()
|
||||
if (shouldSign()) {
|
||||
signingConfig signingConfigs.debug
|
||||
} else {
|
||||
signingConfig null
|
||||
}
|
||||
}
|
||||
|
||||
release {
|
||||
// Signing and zip-aligning are skipped for prebuilt builds, but
|
||||
// performed for Godot gradle builds.
|
||||
zipAlignEnabled shouldZipAlign()
|
||||
if (shouldSign()) {
|
||||
signingConfig signingConfigs.release
|
||||
} else {
|
||||
signingConfig null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
flavorDimensions 'edition'
|
||||
|
||||
productFlavors {
|
||||
standard {
|
||||
getIsDefault().set(true)
|
||||
}
|
||||
mono {}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main {
|
||||
manifest.srcFile 'AndroidManifest.xml'
|
||||
java.srcDirs = ['src']
|
||||
res.srcDirs = ['res']
|
||||
aidl.srcDirs = ['aidl']
|
||||
assets.srcDirs = ['assets']
|
||||
}
|
||||
debug.jniLibs.srcDirs = ['libs/debug', 'libs/debug/vulkan_validation_layers']
|
||||
dev.jniLibs.srcDirs = ['libs/dev']
|
||||
release.jniLibs.srcDirs = ['libs/release']
|
||||
}
|
||||
|
||||
applicationVariants.all { variant ->
|
||||
variant.outputs.all { output ->
|
||||
String filenameSuffix = variant.flavorName == "mono" ? variant.name : variant.buildType.name
|
||||
output.outputFileName = "android_${filenameSuffix}.apk"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
task copyAndRenameBinary(type: Copy) {
|
||||
// The 'doNotTrackState' is added to disable gradle's up-to-date checks for output files
|
||||
// and directories. Otherwise this check may cause permissions access failures on Windows
|
||||
// machines.
|
||||
doNotTrackState("No need for up-to-date checks for the copy-and-rename operation")
|
||||
|
||||
String exportPath = getExportPath()
|
||||
String exportFilename = getExportFilename()
|
||||
String exportEdition = getExportEdition()
|
||||
String exportBuildType = getExportBuildType()
|
||||
String exportBuildTypeCapitalized = exportBuildType.capitalize()
|
||||
String exportFormat = getExportFormat()
|
||||
|
||||
boolean isAab = exportFormat == "aab"
|
||||
boolean isMono = exportEdition == "mono"
|
||||
String filenameSuffix = isAab ? "${exportEdition}-${exportBuildType}" : exportBuildType
|
||||
if (isMono) {
|
||||
filenameSuffix = isAab ? "${exportEdition}-${exportBuildType}" : "${exportEdition}${exportBuildTypeCapitalized}"
|
||||
}
|
||||
|
||||
String sourceFilename = isAab ? "build-${filenameSuffix}.aab" : "android_${filenameSuffix}.apk"
|
||||
String sourceFilepath = isAab ? "$buildDir/outputs/bundle/${exportEdition}${exportBuildTypeCapitalized}/$sourceFilename" : "$buildDir/outputs/apk/$exportEdition/$exportBuildType/$sourceFilename"
|
||||
|
||||
from sourceFilepath
|
||||
into exportPath
|
||||
rename sourceFilename, exportFilename
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to validate the version of the Java SDK used for the Godot gradle builds.
|
||||
*/
|
||||
task validateJavaVersion {
|
||||
if (!JavaVersion.current().isCompatibleWith(versions.javaVersion)) {
|
||||
throw new GradleException("Invalid Java version ${JavaVersion.current()}. Version ${versions.javaVersion} is the minimum supported Java version for Godot gradle builds.")
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
When they're scheduled to run, the copy*AARToAppModule tasks generate dependencies for the 'app'
|
||||
module, so we're ensuring the ':app:preBuild' task is set to run after those tasks.
|
||||
*/
|
||||
if (rootProject.tasks.findByPath("copyDebugAARToAppModule") != null) {
|
||||
preBuild.mustRunAfter(rootProject.tasks.named("copyDebugAARToAppModule"))
|
||||
}
|
||||
if (rootProject.tasks.findByPath("copyDevAARToAppModule") != null) {
|
||||
preBuild.mustRunAfter(rootProject.tasks.named("copyDevAARToAppModule"))
|
||||
}
|
||||
if (rootProject.tasks.findByPath("copyReleaseAARToAppModule") != null) {
|
||||
preBuild.mustRunAfter(rootProject.tasks.named("copyReleaseAARToAppModule"))
|
||||
}
|
||||
404
platform/android/java/app/config.gradle
Normal file
404
platform/android/java/app/config.gradle
Normal file
@@ -0,0 +1,404 @@
|
||||
ext.versions = [
|
||||
androidGradlePlugin: '8.6.1',
|
||||
compileSdk : 35,
|
||||
// Also update:
|
||||
// - 'platform/android/export/export_plugin.cpp#DEFAULT_MIN_SDK_VERSION'
|
||||
// - 'platform/android/detect.py#get_min_target_api()'
|
||||
minSdk : 24,
|
||||
// Also update 'platform/android/export/export_plugin.cpp#DEFAULT_TARGET_SDK_VERSION'
|
||||
targetSdk : 35,
|
||||
buildTools : '35.0.0',
|
||||
kotlinVersion : '2.1.20',
|
||||
fragmentVersion : '1.8.6',
|
||||
nexusPublishVersion: '1.3.0',
|
||||
javaVersion : JavaVersion.VERSION_17,
|
||||
// Also update 'platform/android/detect.py#get_ndk_version()' when this is updated.
|
||||
ndkVersion : '28.1.13356709',
|
||||
splashscreenVersion: '1.0.1',
|
||||
openxrVendorsVersion: '4.1.1-stable'
|
||||
|
||||
]
|
||||
|
||||
ext.getExportPackageName = { ->
|
||||
// Retrieve the app id from the project property set by the Godot build command.
|
||||
String appId = project.hasProperty("export_package_name") ? project.property("export_package_name") : ""
|
||||
// Check if the app id is valid, otherwise use the default.
|
||||
if (appId == null || appId.isEmpty()) {
|
||||
appId = "com.godot.game"
|
||||
}
|
||||
return appId
|
||||
}
|
||||
|
||||
ext.getExportVersionCode = { ->
|
||||
String versionCode = project.hasProperty("export_version_code") ? project.property("export_version_code") : ""
|
||||
if (versionCode == null || versionCode.isEmpty()) {
|
||||
versionCode = "1"
|
||||
}
|
||||
try {
|
||||
return Integer.parseInt(versionCode)
|
||||
} catch (NumberFormatException ignored) {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
ext.getExportVersionName = { ->
|
||||
String versionName = project.hasProperty("export_version_name") ? project.property("export_version_name") : ""
|
||||
if (versionName == null || versionName.isEmpty()) {
|
||||
versionName = "1.0"
|
||||
}
|
||||
return versionName
|
||||
}
|
||||
|
||||
ext.getExportMinSdkVersion = { ->
|
||||
String minSdkVersion = project.hasProperty("export_version_min_sdk") ? project.property("export_version_min_sdk") : ""
|
||||
if (minSdkVersion == null || minSdkVersion.isEmpty()) {
|
||||
minSdkVersion = "$versions.minSdk"
|
||||
}
|
||||
try {
|
||||
return Integer.parseInt(minSdkVersion)
|
||||
} catch (NumberFormatException ignored) {
|
||||
return versions.minSdk
|
||||
}
|
||||
}
|
||||
|
||||
ext.getExportTargetSdkVersion = { ->
|
||||
String targetSdkVersion = project.hasProperty("export_version_target_sdk") ? project.property("export_version_target_sdk") : ""
|
||||
if (targetSdkVersion == null || targetSdkVersion.isEmpty()) {
|
||||
targetSdkVersion = "$versions.targetSdk"
|
||||
}
|
||||
try {
|
||||
return Integer.parseInt(targetSdkVersion)
|
||||
} catch (NumberFormatException ignored) {
|
||||
return versions.targetSdk
|
||||
}
|
||||
}
|
||||
|
||||
ext.getGodotLibraryVersionCode = { ->
|
||||
String versionName = ""
|
||||
int versionCode = 1
|
||||
(versionName, versionCode) = getGodotLibraryVersion()
|
||||
return versionCode
|
||||
}
|
||||
|
||||
ext.getGodotLibraryVersionName = { ->
|
||||
String versionName = ""
|
||||
int versionCode = 1
|
||||
(versionName, versionCode) = getGodotLibraryVersion()
|
||||
return versionName
|
||||
}
|
||||
|
||||
ext.generateGodotLibraryVersion = { List<String> requiredKeys ->
|
||||
// Attempt to read the version from the `version.py` file.
|
||||
String libraryVersionName = ""
|
||||
int libraryVersionCode = 0
|
||||
|
||||
File versionFile = new File("../../../version.py")
|
||||
if (versionFile.isFile()) {
|
||||
def map = [:]
|
||||
|
||||
List<String> lines = versionFile.readLines()
|
||||
for (String line in lines) {
|
||||
String[] keyValue = line.split("=")
|
||||
String key = keyValue[0].trim()
|
||||
String value = keyValue[1].trim().replaceAll("\"", "")
|
||||
|
||||
if (requiredKeys.contains(key)) {
|
||||
if (!value.isEmpty()) {
|
||||
map[key] = value
|
||||
}
|
||||
requiredKeys.remove(key)
|
||||
}
|
||||
}
|
||||
|
||||
if (requiredKeys.empty) {
|
||||
libraryVersionName = map.values().join(".")
|
||||
try {
|
||||
if (map.containsKey("status")) {
|
||||
int statusCode = 0
|
||||
String statusValue = map["status"]
|
||||
if (statusValue == null) {
|
||||
statusCode = 0
|
||||
} else if (statusValue.startsWith("dev")) {
|
||||
statusCode = 1
|
||||
} else if (statusValue.startsWith("alpha")) {
|
||||
statusCode = 2
|
||||
} else if (statusValue.startsWith("beta")) {
|
||||
statusCode = 3
|
||||
} else if (statusValue.startsWith("rc")) {
|
||||
statusCode = 4
|
||||
} else if (statusValue.startsWith("stable")) {
|
||||
statusCode = 5
|
||||
} else {
|
||||
statusCode = 0
|
||||
}
|
||||
|
||||
libraryVersionCode = statusCode
|
||||
}
|
||||
|
||||
if (map.containsKey("patch")) {
|
||||
libraryVersionCode += Integer.parseInt(map["patch"]) * 10
|
||||
}
|
||||
|
||||
if (map.containsKey("minor")) {
|
||||
libraryVersionCode += (Integer.parseInt(map["minor"]) * 1000)
|
||||
}
|
||||
|
||||
if (map.containsKey("major")) {
|
||||
libraryVersionCode += (Integer.parseInt(map["major"]) * 100000)
|
||||
}
|
||||
} catch (NumberFormatException ignore) {
|
||||
libraryVersionCode = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (libraryVersionName.isEmpty()) {
|
||||
// Fallback value in case we're unable to read the file.
|
||||
libraryVersionName = "custom_build"
|
||||
}
|
||||
|
||||
if (libraryVersionCode == 0) {
|
||||
libraryVersionCode = 1
|
||||
}
|
||||
|
||||
return [libraryVersionName, libraryVersionCode]
|
||||
}
|
||||
|
||||
ext.getGodotLibraryVersion = { ->
|
||||
List<String> requiredKeys = ["major", "minor", "patch", "status", "module_config"]
|
||||
return generateGodotLibraryVersion(requiredKeys)
|
||||
}
|
||||
|
||||
ext.getGodotPublishVersion = { ->
|
||||
List<String> requiredKeys = ["major", "minor", "patch", "status"]
|
||||
String versionName = ""
|
||||
int versionCode = 1
|
||||
(versionName, versionCode) = generateGodotLibraryVersion(requiredKeys)
|
||||
if (!versionName.endsWith("stable")) {
|
||||
versionName += "-SNAPSHOT"
|
||||
}
|
||||
return versionName
|
||||
}
|
||||
|
||||
final String VALUE_SEPARATOR_REGEX = "\\|"
|
||||
|
||||
// get the list of ABIs the project should be exported to
|
||||
ext.getExportEnabledABIs = { ->
|
||||
String enabledABIs = project.hasProperty("export_enabled_abis") ? project.property("export_enabled_abis") : ""
|
||||
if (enabledABIs == null || enabledABIs.isEmpty()) {
|
||||
enabledABIs = "armeabi-v7a|arm64-v8a|x86|x86_64|"
|
||||
}
|
||||
Set<String> exportAbiFilter = []
|
||||
for (String abi_name : enabledABIs.split(VALUE_SEPARATOR_REGEX)) {
|
||||
if (!abi_name.trim().isEmpty()){
|
||||
exportAbiFilter.add(abi_name)
|
||||
}
|
||||
}
|
||||
return exportAbiFilter
|
||||
}
|
||||
|
||||
ext.getExportPath = {
|
||||
String exportPath = project.hasProperty("export_path") ? project.property("export_path") : ""
|
||||
if (exportPath == null || exportPath.isEmpty()) {
|
||||
exportPath = "."
|
||||
}
|
||||
return exportPath
|
||||
}
|
||||
|
||||
ext.getExportFilename = {
|
||||
String exportFilename = project.hasProperty("export_filename") ? project.property("export_filename") : ""
|
||||
if (exportFilename == null || exportFilename.isEmpty()) {
|
||||
exportFilename = "godot_android"
|
||||
}
|
||||
return exportFilename
|
||||
}
|
||||
|
||||
ext.getExportEdition = {
|
||||
String exportEdition = project.hasProperty("export_edition") ? project.property("export_edition") : ""
|
||||
if (exportEdition == null || exportEdition.isEmpty()) {
|
||||
exportEdition = "standard"
|
||||
}
|
||||
return exportEdition
|
||||
}
|
||||
|
||||
ext.getExportBuildType = {
|
||||
String exportBuildType = project.hasProperty("export_build_type") ? project.property("export_build_type") : ""
|
||||
if (exportBuildType == null || exportBuildType.isEmpty()) {
|
||||
exportBuildType = "debug"
|
||||
}
|
||||
return exportBuildType
|
||||
}
|
||||
|
||||
ext.getExportFormat = {
|
||||
String exportFormat = project.hasProperty("export_format") ? project.property("export_format") : ""
|
||||
if (exportFormat == null || exportFormat.isEmpty()) {
|
||||
exportFormat = "apk"
|
||||
}
|
||||
return exportFormat
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the project properties for the 'plugins_maven_repos' property and return the list
|
||||
* of maven repos.
|
||||
*/
|
||||
ext.getGodotPluginsMavenRepos = { ->
|
||||
Set<String> mavenRepos = []
|
||||
|
||||
// Retrieve the list of maven repos.
|
||||
if (project.hasProperty("plugins_maven_repos")) {
|
||||
String mavenReposProperty = project.property("plugins_maven_repos")
|
||||
if (mavenReposProperty != null && !mavenReposProperty.trim().isEmpty()) {
|
||||
for (String mavenRepoUrl : mavenReposProperty.split(VALUE_SEPARATOR_REGEX)) {
|
||||
mavenRepos += mavenRepoUrl.trim()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return mavenRepos
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the project properties for the 'plugins_remote_binaries' property and return
|
||||
* it for inclusion in the build dependencies.
|
||||
*/
|
||||
ext.getGodotPluginsRemoteBinaries = { ->
|
||||
Set<String> remoteDeps = []
|
||||
|
||||
// Retrieve the list of remote plugins binaries.
|
||||
if (project.hasProperty("plugins_remote_binaries")) {
|
||||
String remoteDepsList = project.property("plugins_remote_binaries")
|
||||
if (remoteDepsList != null && !remoteDepsList.trim().isEmpty()) {
|
||||
for (String dep: remoteDepsList.split(VALUE_SEPARATOR_REGEX)) {
|
||||
remoteDeps += dep.trim()
|
||||
}
|
||||
}
|
||||
}
|
||||
return remoteDeps
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the project properties for the 'plugins_local_binaries' property and return
|
||||
* their binaries for inclusion in the build dependencies.
|
||||
*/
|
||||
ext.getGodotPluginsLocalBinaries = { ->
|
||||
Set<String> binDeps = []
|
||||
|
||||
// Retrieve the list of local plugins binaries.
|
||||
if (project.hasProperty("plugins_local_binaries")) {
|
||||
String pluginsList = project.property("plugins_local_binaries")
|
||||
if (pluginsList != null && !pluginsList.trim().isEmpty()) {
|
||||
for (String plugin : pluginsList.split(VALUE_SEPARATOR_REGEX)) {
|
||||
binDeps += plugin.trim()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return binDeps
|
||||
}
|
||||
|
||||
ext.getDebugKeystoreFile = { ->
|
||||
String keystoreFile = project.hasProperty("debug_keystore_file") ? project.property("debug_keystore_file") : ""
|
||||
if (keystoreFile == null || keystoreFile.isEmpty()) {
|
||||
keystoreFile = "."
|
||||
}
|
||||
return keystoreFile
|
||||
}
|
||||
|
||||
ext.hasCustomDebugKeystore = { ->
|
||||
File keystoreFile = new File(getDebugKeystoreFile())
|
||||
return keystoreFile.isFile()
|
||||
}
|
||||
|
||||
ext.getDebugKeystorePassword = { ->
|
||||
String keystorePassword = project.hasProperty("debug_keystore_password") ? project.property("debug_keystore_password") : ""
|
||||
if (keystorePassword == null || keystorePassword.isEmpty()) {
|
||||
keystorePassword = "android"
|
||||
}
|
||||
return keystorePassword
|
||||
}
|
||||
|
||||
ext.getDebugKeyAlias = { ->
|
||||
String keyAlias = project.hasProperty("debug_keystore_alias") ? project.property("debug_keystore_alias") : ""
|
||||
if (keyAlias == null || keyAlias.isEmpty()) {
|
||||
keyAlias = "androiddebugkey"
|
||||
}
|
||||
return keyAlias
|
||||
}
|
||||
|
||||
ext.getReleaseKeystoreFile = { ->
|
||||
String keystoreFile = project.hasProperty("release_keystore_file") ? project.property("release_keystore_file") : ""
|
||||
if (keystoreFile == null || keystoreFile.isEmpty()) {
|
||||
keystoreFile = "."
|
||||
}
|
||||
return keystoreFile
|
||||
}
|
||||
|
||||
ext.getReleaseKeystorePassword = { ->
|
||||
String keystorePassword = project.hasProperty("release_keystore_password") ? project.property("release_keystore_password") : ""
|
||||
return keystorePassword
|
||||
}
|
||||
|
||||
ext.getReleaseKeyAlias = { ->
|
||||
String keyAlias = project.hasProperty("release_keystore_alias") ? project.property("release_keystore_alias") : ""
|
||||
return keyAlias
|
||||
}
|
||||
|
||||
ext.isAndroidStudio = { ->
|
||||
return project.hasProperty('android.injected.invoked.from.ide')
|
||||
}
|
||||
|
||||
ext.shouldZipAlign = { ->
|
||||
String zipAlignFlag = project.hasProperty("perform_zipalign") ? project.property("perform_zipalign") : ""
|
||||
if (zipAlignFlag == null || zipAlignFlag.isEmpty()) {
|
||||
if (isAndroidStudio()) {
|
||||
zipAlignFlag = "true"
|
||||
} else {
|
||||
zipAlignFlag = "false"
|
||||
}
|
||||
}
|
||||
return Boolean.parseBoolean(zipAlignFlag)
|
||||
}
|
||||
|
||||
ext.shouldSign = { ->
|
||||
String signFlag = project.hasProperty("perform_signing") ? project.property("perform_signing") : ""
|
||||
if (signFlag == null || signFlag.isEmpty()) {
|
||||
if (isAndroidStudio()) {
|
||||
signFlag = "true"
|
||||
} else {
|
||||
signFlag = "false"
|
||||
}
|
||||
}
|
||||
return Boolean.parseBoolean(signFlag)
|
||||
}
|
||||
|
||||
ext.shouldNotStrip = { ->
|
||||
return isAndroidStudio() || project.hasProperty("doNotStrip")
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether to use the legacy convention of compressing all .so files in the APK.
|
||||
*
|
||||
* For more background, see:
|
||||
* - https://developer.android.com/build/releases/past-releases/agp-3-6-0-release-notes#extractNativeLibs
|
||||
* - https://stackoverflow.com/a/44704840
|
||||
*/
|
||||
ext.shouldUseLegacyPackaging = { ->
|
||||
String legacyPackagingFlag = project.hasProperty("compress_native_libraries") ? project.property("compress_native_libraries") : ""
|
||||
if (legacyPackagingFlag != null && !legacyPackagingFlag.isEmpty()) {
|
||||
return Boolean.parseBoolean(legacyPackagingFlag)
|
||||
}
|
||||
|
||||
if (getExportMinSdkVersion() <= 29) {
|
||||
// Use legacy packaging for compatibility with device running api <= 29.
|
||||
// See https://github.com/godotengine/godot/issues/108842 for reference.
|
||||
return true
|
||||
}
|
||||
|
||||
// Default behavior for minSdk > 29.
|
||||
return false
|
||||
}
|
||||
|
||||
ext.getAddonsDirectory = { ->
|
||||
String addonsDirectory = project.hasProperty("addons_directory") ? project.property("addons_directory") : ""
|
||||
return addonsDirectory
|
||||
}
|
||||
28
platform/android/java/app/gradle.properties
Normal file
28
platform/android/java/app/gradle.properties
Normal file
@@ -0,0 +1,28 @@
|
||||
# Godot gradle build settings.
|
||||
# These properties apply when running a gradle build from the Godot editor.
|
||||
# NOTE: This should be kept in sync with 'godot/platform/android/java/gradle.properties' except
|
||||
# where otherwise specified.
|
||||
|
||||
# For more details on how to configure your build environment visit
|
||||
# https://www.gradle.org/docs/current/userguide/build_environment.html
|
||||
|
||||
android.enableJetifier=true
|
||||
android.useAndroidX=true
|
||||
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
org.gradle.jvmargs=-Xmx4536m
|
||||
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. More details, visit
|
||||
# https://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||
# org.gradle.parallel=true
|
||||
|
||||
org.gradle.warning.mode=all
|
||||
|
||||
# Enable resource optimizations for release build.
|
||||
# NOTE: This is turned off for template release build in order to support the build legacy process.
|
||||
android.enableResourceOptimizations=true
|
||||
|
||||
# Fix gradle build errors when the build path contains non-ASCII characters
|
||||
android.overridePathCheck=true
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- WARNING: THIS FILE WILL BE OVERWRITTEN AT BUILD TIME-->
|
||||
<resources>
|
||||
<string name="godot_project_name_string">godot-project-name-ar</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- WARNING: THIS FILE WILL BE OVERWRITTEN AT BUILD TIME-->
|
||||
<resources>
|
||||
<string name="godot_project_name_string">godot-project-name-bg</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- WARNING: THIS FILE WILL BE OVERWRITTEN AT BUILD TIME-->
|
||||
<resources>
|
||||
<string name="godot_project_name_string">godot-project-name-ca</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- WARNING: THIS FILE WILL BE OVERWRITTEN AT BUILD TIME-->
|
||||
<resources>
|
||||
<string name="godot_project_name_string">godot-project-name-cs</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- WARNING: THIS FILE WILL BE OVERWRITTEN AT BUILD TIME-->
|
||||
<resources>
|
||||
<string name="godot_project_name_string">godot-project-name-da</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- WARNING: THIS FILE WILL BE OVERWRITTEN AT BUILD TIME-->
|
||||
<resources>
|
||||
<string name="godot_project_name_string">godot-project-name-de</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- WARNING: THIS FILE WILL BE OVERWRITTEN AT BUILD TIME-->
|
||||
<resources>
|
||||
<string name="godot_project_name_string">godot-project-name-el</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- WARNING: THIS FILE WILL BE OVERWRITTEN AT BUILD TIME-->
|
||||
<resources>
|
||||
<string name="godot_project_name_string">godot-project-name-en</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- WARNING: THIS FILE WILL BE OVERWRITTEN AT BUILD TIME-->
|
||||
<resources>
|
||||
<string name="godot_project_name_string">godot-project-name-es_ES</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- WARNING: THIS FILE WILL BE OVERWRITTEN AT BUILD TIME-->
|
||||
<resources>
|
||||
<string name="godot_project_name_string">godot-project-name-es</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- WARNING: THIS FILE WILL BE OVERWRITTEN AT BUILD TIME-->
|
||||
<resources>
|
||||
<string name="godot_project_name_string">godot-project-name-fa</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- WARNING: THIS FILE WILL BE OVERWRITTEN AT BUILD TIME-->
|
||||
<resources>
|
||||
<string name="godot_project_name_string">godot-project-name-fi</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- WARNING: THIS FILE WILL BE OVERWRITTEN AT BUILD TIME-->
|
||||
<resources>
|
||||
<string name="godot_project_name_string">godot-project-name-fr</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- WARNING: THIS FILE WILL BE OVERWRITTEN AT BUILD TIME-->
|
||||
<resources>
|
||||
<string name="godot_project_name_string">godot-project-name-hi</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- WARNING: THIS FILE WILL BE OVERWRITTEN AT BUILD TIME-->
|
||||
<resources>
|
||||
<string name="godot_project_name_string">godot-project-name-hr</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- WARNING: THIS FILE WILL BE OVERWRITTEN AT BUILD TIME-->
|
||||
<resources>
|
||||
<string name="godot_project_name_string">godot-project-name-hu</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- WARNING: THIS FILE WILL BE OVERWRITTEN AT BUILD TIME-->
|
||||
<resources>
|
||||
<string name="godot_project_name_string">godot-project-name-in</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- WARNING: THIS FILE WILL BE OVERWRITTEN AT BUILD TIME-->
|
||||
<resources>
|
||||
<string name="godot_project_name_string">godot-project-name-it</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- WARNING: THIS FILE WILL BE OVERWRITTEN AT BUILD TIME-->
|
||||
<resources>
|
||||
<string name="godot_project_name_string">godot-project-name-iw</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- WARNING: THIS FILE WILL BE OVERWRITTEN AT BUILD TIME-->
|
||||
<resources>
|
||||
<string name="godot_project_name_string">godot-project-name-ja</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- WARNING: THIS FILE WILL BE OVERWRITTEN AT BUILD TIME-->
|
||||
<resources>
|
||||
<string name="godot_project_name_string">godot-project-name-ko</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- WARNING: THIS FILE WILL BE OVERWRITTEN AT BUILD TIME-->
|
||||
<resources>
|
||||
<string name="godot_project_name_string">godot-project-name-lt</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- WARNING: THIS FILE WILL BE OVERWRITTEN AT BUILD TIME-->
|
||||
<resources>
|
||||
<string name="godot_project_name_string">godot-project-name-lv</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- WARNING: THIS FILE WILL BE OVERWRITTEN AT BUILD TIME-->
|
||||
<resources>
|
||||
<string name="godot_project_name_string">godot-project-name-nb</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- WARNING: THIS FILE WILL BE OVERWRITTEN AT BUILD TIME-->
|
||||
<resources>
|
||||
<string name="godot_project_name_string">godot-project-name-nl</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- WARNING: THIS FILE WILL BE OVERWRITTEN AT BUILD TIME-->
|
||||
<resources>
|
||||
<string name="godot_project_name_string">godot-project-name-pl</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- WARNING: THIS FILE WILL BE OVERWRITTEN AT BUILD TIME-->
|
||||
<resources>
|
||||
<string name="godot_project_name_string">godot-project-name-pt</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- WARNING: THIS FILE WILL BE OVERWRITTEN AT BUILD TIME-->
|
||||
<resources>
|
||||
<string name="godot_project_name_string">godot-project-name-ro</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- WARNING: THIS FILE WILL BE OVERWRITTEN AT BUILD TIME-->
|
||||
<resources>
|
||||
<string name="godot_project_name_string">godot-project-name-ru</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- WARNING: THIS FILE WILL BE OVERWRITTEN AT BUILD TIME-->
|
||||
<resources>
|
||||
<string name="godot_project_name_string">godot-project-name-sk</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- WARNING: THIS FILE WILL BE OVERWRITTEN AT BUILD TIME-->
|
||||
<resources>
|
||||
<string name="godot_project_name_string">godot-project-name-sl</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- WARNING: THIS FILE WILL BE OVERWRITTEN AT BUILD TIME-->
|
||||
<resources>
|
||||
<string name="godot_project_name_string">godot-project-name-sr</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- WARNING: THIS FILE WILL BE OVERWRITTEN AT BUILD TIME-->
|
||||
<resources>
|
||||
<string name="godot_project_name_string">godot-project-name-sv</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- WARNING: THIS FILE WILL BE OVERWRITTEN AT BUILD TIME-->
|
||||
<resources>
|
||||
<string name="godot_project_name_string">godot-project-name-th</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- WARNING: THIS FILE WILL BE OVERWRITTEN AT BUILD TIME-->
|
||||
<resources>
|
||||
<string name="godot_project_name_string">godot-project-name-tl</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- WARNING: THIS FILE WILL BE OVERWRITTEN AT BUILD TIME-->
|
||||
<resources>
|
||||
<string name="godot_project_name_string">godot-project-name-tr</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- WARNING: THIS FILE WILL BE OVERWRITTEN AT BUILD TIME-->
|
||||
<resources>
|
||||
<string name="godot_project_name_string">godot-project-name-uk</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- WARNING: THIS FILE WILL BE OVERWRITTEN AT BUILD TIME-->
|
||||
<resources>
|
||||
<string name="godot_project_name_string">godot-project-name-vi</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- WARNING: THIS FILE WILL BE OVERWRITTEN AT BUILD TIME-->
|
||||
<resources>
|
||||
<string name="godot_project_name_string">godot-project-name-zh_HK</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- WARNING: THIS FILE WILL BE OVERWRITTEN AT BUILD TIME-->
|
||||
<resources>
|
||||
<string name="godot_project_name_string">godot-project-name-zh_TW</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- WARNING: THIS FILE WILL BE OVERWRITTEN AT BUILD TIME-->
|
||||
<resources>
|
||||
<string name="godot_project_name_string">godot-project-name-zh</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- WARNING: THIS FILE WILL BE OVERWRITTEN AT BUILD TIME-->
|
||||
<resources>
|
||||
<string name="godot_project_name_string">godot-project-name</string>
|
||||
</resources>
|
||||
18
platform/android/java/app/res/values/themes.xml
Normal file
18
platform/android/java/app/res/values/themes.xml
Normal file
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- GodotAppMainTheme is auto-generated during export. Manual changes will be overwritten.
|
||||
To add custom attributes, use the "gradle_build/custom_theme_attributes" Android export option. -->
|
||||
<style name="GodotAppMainTheme" parent="@android:style/Theme.DeviceDefault.NoActionBar">
|
||||
<item name="android:windowSwipeToDismiss">false</item>
|
||||
<item name="android:windowIsTranslucent">false</item>
|
||||
</style>
|
||||
|
||||
<!-- GodotAppSplashTheme is auto-generated during export. Manual changes will be overwritten.
|
||||
To add custom attributes, use the "gradle_build/custom_theme_attributes" Android export option. -->
|
||||
<style name="GodotAppSplashTheme" parent="Theme.SplashScreen">
|
||||
<item name="android:windowSplashScreenBackground">@mipmap/icon_background</item>
|
||||
<item name="windowSplashScreenAnimatedIcon">@mipmap/icon_foreground</item>
|
||||
<item name="postSplashScreenTheme">@style/GodotAppMainTheme</item>
|
||||
<item name="android:windowIsTranslucent">false</item>
|
||||
</style>
|
||||
</resources>
|
||||
18
platform/android/java/app/settings.gradle
Normal file
18
platform/android/java/app/settings.gradle
Normal file
@@ -0,0 +1,18 @@
|
||||
// This is the root directory of the Godot Android gradle build.
|
||||
pluginManagement {
|
||||
apply from: 'config.gradle'
|
||||
|
||||
plugins {
|
||||
id 'com.android.application' version versions.androidGradlePlugin
|
||||
id 'org.jetbrains.kotlin.android' version versions.kotlinVersion
|
||||
}
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
maven { url "https://plugins.gradle.org/m2/" }
|
||||
maven { url "https://central.sonatype.com/repository/maven-snapshots/"}
|
||||
}
|
||||
}
|
||||
|
||||
include ':assetPackInstallTime'
|
||||
86
platform/android/java/app/src/com/godot/game/GodotApp.java
Normal file
86
platform/android/java/app/src/com/godot/game/GodotApp.java
Normal file
@@ -0,0 +1,86 @@
|
||||
/**************************************************************************/
|
||||
/* GodotApp.java */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
package com.godot.game;
|
||||
|
||||
import org.godotengine.godot.Godot;
|
||||
import org.godotengine.godot.GodotActivity;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.activity.EdgeToEdge;
|
||||
import androidx.core.splashscreen.SplashScreen;
|
||||
|
||||
/**
|
||||
* Template activity for Godot Android builds.
|
||||
* Feel free to extend and modify this class for your custom logic.
|
||||
*/
|
||||
public class GodotApp extends GodotActivity {
|
||||
static {
|
||||
// .NET libraries.
|
||||
if (BuildConfig.FLAVOR.equals("mono")) {
|
||||
try {
|
||||
Log.v("GODOT", "Loading System.Security.Cryptography.Native.Android library");
|
||||
System.loadLibrary("System.Security.Cryptography.Native.Android");
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
Log.e("GODOT", "Unable to load System.Security.Cryptography.Native.Android library");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final Runnable updateWindowAppearance = () -> {
|
||||
Godot godot = getGodot();
|
||||
if (godot != null) {
|
||||
godot.enableImmersiveMode(godot.isInImmersiveMode(), true);
|
||||
godot.enableEdgeToEdge(godot.isInEdgeToEdgeMode(), true);
|
||||
godot.setSystemBarsAppearance();
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
SplashScreen.installSplashScreen(this);
|
||||
EdgeToEdge.enable(this);
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
updateWindowAppearance.run();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGodotMainLoopStarted() {
|
||||
super.onGodotMainLoopStarted();
|
||||
runOnUiThread(updateWindowAppearance);
|
||||
}
|
||||
}
|
||||
340
platform/android/java/build.gradle
Normal file
340
platform/android/java/build.gradle
Normal file
@@ -0,0 +1,340 @@
|
||||
plugins {
|
||||
id 'io.github.gradle-nexus.publish-plugin'
|
||||
}
|
||||
|
||||
apply from: 'app/config.gradle'
|
||||
apply from: 'scripts/publish-root.gradle'
|
||||
|
||||
ext {
|
||||
PUBLISH_VERSION = getGodotPublishVersion()
|
||||
}
|
||||
|
||||
group = ossrhGroupId
|
||||
version = PUBLISH_VERSION
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
maven { url "https://plugins.gradle.org/m2/" }
|
||||
maven { url "https://central.sonatype.com/repository/maven-snapshots/"}
|
||||
}
|
||||
}
|
||||
|
||||
ext {
|
||||
supportedAbis = ["arm32", "arm64", "x86_32", "x86_64"]
|
||||
supportedFlavors = ["editor", "template"]
|
||||
supportedAndroidDistributions = ["android", "horizonos", "picoos"]
|
||||
supportedFlavorsBuildTypes = [
|
||||
"editor": ["dev", "debug", "release"],
|
||||
"template": ["dev", "debug", "release"]
|
||||
]
|
||||
supportedEditions = ["standard", "mono"]
|
||||
|
||||
// Used by gradle to specify which architecture to build for by default when running
|
||||
// `./gradlew build` (this command is usually used by Android Studio).
|
||||
// If building manually on the command line, it's recommended to use the
|
||||
// `./gradlew generateGodotTemplates` build command instead after running the `scons` command(s).
|
||||
// The {selectedAbis} values must be from the {supportedAbis} values.
|
||||
selectedAbis = ["arm64"]
|
||||
|
||||
rootDir = "../../.."
|
||||
binDir = "$rootDir/bin/"
|
||||
androidEditorBuildsDir = "$binDir/android_editor_builds/"
|
||||
}
|
||||
|
||||
def getSconsTaskName(String flavor, String buildType, String abi) {
|
||||
return "compileGodotNativeLibs" + flavor.capitalize() + buildType.capitalize() + abi.capitalize()
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate Godot gradle build template by zipping the source files from the app directory, as well
|
||||
* as the AAR files generated by 'copyDebugAAR', 'copyDevAAR' and 'copyReleaseAAR'.
|
||||
* The zip file also includes some gradle tools to enable gradle builds from the Godot Editor.
|
||||
*/
|
||||
task zipGradleBuild(type: Zip) {
|
||||
onlyIf { generateGodotTemplates.state.executed || generateGodotMonoTemplates.state.executed || generateDevTemplate.state.executed }
|
||||
doFirst {
|
||||
logger.lifecycle("Generating Godot gradle build template")
|
||||
}
|
||||
from(fileTree(dir: 'app', excludes: ['**/build/**', '**/.gradle/**', '**/*.iml']), fileTree(dir: '.', includes: ['gradlew', 'gradlew.bat', 'gradle/**']))
|
||||
include '**/*'
|
||||
archiveFileName = 'android_source.zip'
|
||||
destinationDirectory = file(binDir)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the scons build tasks responsible for generating the Godot native shared
|
||||
* libraries should be excluded.
|
||||
*/
|
||||
def excludeSconsBuildTasks() {
|
||||
return !isAndroidStudio() && !project.hasProperty("generateNativeLibs")
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the list of build tasks that should be excluded from the build process.\
|
||||
*/
|
||||
def templateExcludedBuildTask() {
|
||||
// We exclude these gradle tasks so we can run the scons command manually.
|
||||
def excludedTasks = []
|
||||
if (excludeSconsBuildTasks()) {
|
||||
logger.info("Excluding Android studio build tasks")
|
||||
for (String flavor : supportedFlavors) {
|
||||
String[] supportedBuildTypes = supportedFlavorsBuildTypes[flavor]
|
||||
for (String buildType : supportedBuildTypes) {
|
||||
for (String abi : selectedAbis) {
|
||||
excludedTasks += ":lib:" + getSconsTaskName(flavor, buildType, abi)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return excludedTasks
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the build tasks for the given flavor
|
||||
* @param flavor Must be one of the supported flavors ('template' / 'editor')
|
||||
* @param edition Must be one of the supported editions ('standard' / 'mono')
|
||||
* @param androidDistro Must be one of the supported Android distributions ('android' / 'horizonos' / 'picoos')
|
||||
*/
|
||||
def generateBuildTasks(String flavor = "template", String edition = "standard", String androidDistro = "android") {
|
||||
if (!supportedFlavors.contains(flavor)) {
|
||||
throw new GradleException("Invalid build flavor: $flavor")
|
||||
}
|
||||
if (!supportedAndroidDistributions.contains(androidDistro)) {
|
||||
throw new GradleException("Invalid Android distribution: $androidDistro")
|
||||
}
|
||||
if (!supportedEditions.contains(edition)) {
|
||||
throw new GradleException("Invalid build edition: $edition")
|
||||
}
|
||||
if (edition == "mono" && flavor != "template") {
|
||||
throw new GradleException("'mono' edition only supports the 'template' flavor.")
|
||||
}
|
||||
|
||||
String capitalizedAndroidDistro = androidDistro.capitalize()
|
||||
def buildTasks = []
|
||||
|
||||
// Only build the binary files for which we have native shared libraries unless we intend
|
||||
// to run the scons build tasks.
|
||||
boolean excludeSconsBuildTasks = excludeSconsBuildTasks()
|
||||
boolean isTemplate = flavor == "template"
|
||||
String libsDir = isTemplate ? "lib/libs/" : "lib/libs/tools/"
|
||||
for (String target : supportedFlavorsBuildTypes[flavor]) {
|
||||
File targetLibs = new File(libsDir + target)
|
||||
|
||||
String targetSuffix = target
|
||||
if (target == "dev") {
|
||||
targetSuffix = "debug.dev"
|
||||
}
|
||||
|
||||
if (!excludeSconsBuildTasks || (targetLibs != null
|
||||
&& targetLibs.isDirectory()
|
||||
&& targetLibs.listFiles() != null
|
||||
&& targetLibs.listFiles().length > 0)) {
|
||||
|
||||
String capitalizedTarget = target.capitalize()
|
||||
String capitalizedEdition = edition.capitalize()
|
||||
if (isTemplate) {
|
||||
// Copy the Godot android library archive file into the app module libs directory.
|
||||
// Depends on the library build task to ensure the AAR file is generated prior to copying.
|
||||
String copyAARTaskName = "copy${capitalizedTarget}AARToAppModule"
|
||||
if (tasks.findByName(copyAARTaskName) != null) {
|
||||
buildTasks += tasks.getByName(copyAARTaskName)
|
||||
} else {
|
||||
buildTasks += tasks.create(name: copyAARTaskName, type: Copy) {
|
||||
dependsOn ":lib:assembleTemplate${capitalizedTarget}"
|
||||
from('lib/build/outputs/aar')
|
||||
include("godot-lib.template_${targetSuffix}.aar")
|
||||
into("app/libs/${target}")
|
||||
}
|
||||
}
|
||||
|
||||
// Copy the Godot android library archive file into the root bin directory.
|
||||
// Depends on the library build task to ensure the AAR file is generated prior to copying.
|
||||
String copyAARToBinTaskName = "copy${capitalizedTarget}AARToBin"
|
||||
if (tasks.findByName(copyAARToBinTaskName) != null) {
|
||||
buildTasks += tasks.getByName(copyAARToBinTaskName)
|
||||
} else {
|
||||
buildTasks += tasks.create(name: copyAARToBinTaskName, type: Copy) {
|
||||
dependsOn ":lib:assembleTemplate${capitalizedTarget}"
|
||||
from('lib/build/outputs/aar')
|
||||
include("godot-lib.template_${targetSuffix}.aar")
|
||||
into(binDir)
|
||||
}
|
||||
}
|
||||
|
||||
// Copy the generated binary template into the Godot bin directory.
|
||||
// Depends on the app build task to ensure the binary is generated prior to copying.
|
||||
String copyBinaryTaskName = "copy${capitalizedEdition}${capitalizedTarget}BinaryToBin"
|
||||
if (tasks.findByName(copyBinaryTaskName) != null) {
|
||||
buildTasks += tasks.getByName(copyBinaryTaskName)
|
||||
} else {
|
||||
buildTasks += tasks.create(name: copyBinaryTaskName, type: Copy) {
|
||||
String filenameSuffix = edition == "mono" ? "${edition}${capitalizedTarget}" : target
|
||||
dependsOn ":app:assemble${capitalizedEdition}${capitalizedTarget}"
|
||||
from("app/build/outputs/apk/${edition}/${target}") {
|
||||
include("android_${filenameSuffix}.apk")
|
||||
}
|
||||
into(binDir)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Copy the generated editor apk to the bin directory.
|
||||
String copyEditorApkTaskName = "copyEditor${capitalizedAndroidDistro}${capitalizedTarget}ApkToBin"
|
||||
if (tasks.findByName(copyEditorApkTaskName) != null) {
|
||||
buildTasks += tasks.getByName(copyEditorApkTaskName)
|
||||
} else {
|
||||
buildTasks += tasks.create(name: copyEditorApkTaskName, type: Copy) {
|
||||
dependsOn ":editor:assemble${capitalizedAndroidDistro}${capitalizedTarget}"
|
||||
from("editor/build/outputs/apk/${androidDistro}/${target}") {
|
||||
include("android_editor-${androidDistro}-${target}*.apk")
|
||||
}
|
||||
into(androidEditorBuildsDir)
|
||||
}
|
||||
}
|
||||
|
||||
// Copy the generated editor aab to the bin directory.
|
||||
String copyEditorAabTaskName = "copyEditor${capitalizedAndroidDistro}${capitalizedTarget}AabToBin"
|
||||
if (tasks.findByName(copyEditorAabTaskName) != null) {
|
||||
buildTasks += tasks.getByName(copyEditorAabTaskName)
|
||||
} else {
|
||||
buildTasks += tasks.create(name: copyEditorAabTaskName, type: Copy) {
|
||||
dependsOn ":editor:bundle${capitalizedAndroidDistro}${capitalizedTarget}"
|
||||
from("editor/build/outputs/bundle/${androidDistro}${capitalizedTarget}")
|
||||
into(androidEditorBuildsDir)
|
||||
include("android_editor-${androidDistro}-${target}*.aab")
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logger.info("No native shared libs for target $target. Skipping build.")
|
||||
}
|
||||
}
|
||||
|
||||
return buildTasks
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the Godot Editor binaries for Android devices.
|
||||
*
|
||||
* Note: Unless the 'generateNativeLibs` argument is specified, the Godot 'tools' shared libraries
|
||||
* must have been generated (via scons) prior to running this gradle task.
|
||||
* The task will only build the binaries for which the shared libraries is available.
|
||||
*/
|
||||
task generateGodotEditor {
|
||||
gradle.startParameter.excludedTaskNames += templateExcludedBuildTask()
|
||||
dependsOn = generateBuildTasks("editor", "standard", "android")
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the Godot Editor binaries for HorizonOS devices.
|
||||
*
|
||||
* Note: Unless the 'generateNativeLibs` argument is specified, the Godot 'tools' shared libraries
|
||||
* must have been generated (via scons) prior to running this gradle task.
|
||||
* The task will only build the binaries for which the shared libraries is available.
|
||||
*/
|
||||
task generateGodotHorizonOSEditor {
|
||||
gradle.startParameter.excludedTaskNames += templateExcludedBuildTask()
|
||||
dependsOn = generateBuildTasks("editor", "standard", "horizonos")
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the Godot Editor binaries for PicoOS devices.
|
||||
*
|
||||
* Note: Unless the 'generateNativeLibs` argument is specified, the Godot 'tools' shared libraries
|
||||
* must have been generated (via scons) prior to running this gradle task.
|
||||
* The task will only build the binaries for which the shared libraries is available.
|
||||
*/
|
||||
task generateGodotPicoOSEditor {
|
||||
gradle.startParameter.excludedTaskNames += templateExcludedBuildTask()
|
||||
dependsOn = generateBuildTasks("editor", "standard", "picoos")
|
||||
}
|
||||
|
||||
/**
|
||||
* Master task used to coordinate the tasks defined above to generate the set of Godot templates.
|
||||
*/
|
||||
task generateGodotTemplates {
|
||||
gradle.startParameter.excludedTaskNames += templateExcludedBuildTask()
|
||||
dependsOn = generateBuildTasks("template")
|
||||
|
||||
finalizedBy 'zipGradleBuild'
|
||||
}
|
||||
|
||||
/**
|
||||
* Master task used to coordinate the tasks defined above to generate the set of Godot templates
|
||||
* for the 'mono' edition of the engine.
|
||||
*/
|
||||
task generateGodotMonoTemplates {
|
||||
gradle.startParameter.excludedTaskNames += templateExcludedBuildTask()
|
||||
dependsOn = generateBuildTasks("template", "mono")
|
||||
|
||||
finalizedBy 'zipGradleBuild'
|
||||
}
|
||||
|
||||
task clean(type: Delete) {
|
||||
dependsOn 'cleanGodotEditor'
|
||||
dependsOn 'cleanGodotTemplates'
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean the generated editor artifacts.
|
||||
*/
|
||||
task cleanGodotEditor(type: Delete) {
|
||||
// Delete the generated native tools libs
|
||||
delete("lib/libs/tools")
|
||||
|
||||
// Delete the library generated AAR files
|
||||
delete("lib/build/outputs/aar")
|
||||
|
||||
// Delete the generated binary apks
|
||||
delete("editor/build/outputs/apk")
|
||||
|
||||
// Delete the generated aab binaries
|
||||
delete("editor/build/outputs/bundle")
|
||||
|
||||
// Delete the Godot editor apks & aabs in the Godot bin directory
|
||||
delete(androidEditorBuildsDir)
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean the generated template artifacts.
|
||||
*/
|
||||
task cleanGodotTemplates(type: Delete) {
|
||||
// Delete the generated native libs
|
||||
delete("lib/libs")
|
||||
|
||||
// Delete the library generated AAR files
|
||||
delete("lib/build/outputs/aar")
|
||||
|
||||
// Delete the app libs directory contents
|
||||
delete("app/libs")
|
||||
|
||||
// Delete the generated binary apks
|
||||
delete("app/build/outputs/apk")
|
||||
|
||||
// Delete the Godot templates in the Godot bin directory
|
||||
delete("$binDir/android_debug.apk")
|
||||
delete("$binDir/android_dev.apk")
|
||||
delete("$binDir/android_release.apk")
|
||||
delete("$binDir/android_monoDebug.apk")
|
||||
delete("$binDir/android_monoDev.apk")
|
||||
delete("$binDir/android_monoRelease.apk")
|
||||
delete("$binDir/android_source.zip")
|
||||
delete("$binDir/godot-lib.template_debug.aar")
|
||||
delete("$binDir/godot-lib.template_debug.dev.aar")
|
||||
delete("$binDir/godot-lib.template_release.aar")
|
||||
|
||||
// Cover deletion for the libs using the previous naming scheme
|
||||
delete("$binDir/godot-lib.debug.aar")
|
||||
delete("$binDir/godot-lib.dev.aar")
|
||||
delete("$binDir/godot-lib.release.aar")
|
||||
|
||||
// Delete the native debug symbols files.
|
||||
delete("$binDir/android-editor-debug-native-symbols.zip")
|
||||
delete("$binDir/android-editor-dev-native-symbols.zip")
|
||||
delete("$binDir/android-editor-release-native-symbols.zip")
|
||||
delete("$binDir/android-template-debug-native-symbols.zip")
|
||||
delete("$binDir/android-template-dev-native-symbols.zip")
|
||||
delete("$binDir/android-template-release-native-symbols.zip")
|
||||
}
|
||||
199
platform/android/java/editor/build.gradle
Normal file
199
platform/android/java/editor/build.gradle
Normal file
@@ -0,0 +1,199 @@
|
||||
// Gradle build config for Godot Engine's Android port.
|
||||
plugins {
|
||||
id 'com.android.application'
|
||||
id 'org.jetbrains.kotlin.android'
|
||||
id 'base'
|
||||
}
|
||||
|
||||
ext {
|
||||
// Retrieve the build number from the environment variable; default to 0 if none is specified.
|
||||
// The build number is added as a suffix to the version code for upload to the Google Play store.
|
||||
getEditorBuildNumber = { ->
|
||||
int buildNumber = 0
|
||||
String versionStatus = System.getenv("GODOT_VERSION_STATUS")
|
||||
if (versionStatus != null && !versionStatus.isEmpty()) {
|
||||
try {
|
||||
buildNumber = Integer.parseInt(versionStatus.replaceAll("[^0-9]", ""))
|
||||
} catch (NumberFormatException ignored) {
|
||||
buildNumber = 0
|
||||
}
|
||||
}
|
||||
|
||||
return buildNumber
|
||||
}
|
||||
// Value by which the Godot version code should be offset by to make room for the build number
|
||||
editorBuildNumberOffset = 100
|
||||
|
||||
// Return the keystore file used for signing the release build.
|
||||
getGodotKeystoreFile = { ->
|
||||
def keyStore = System.getenv("GODOT_ANDROID_SIGN_KEYSTORE")
|
||||
if (keyStore == null || keyStore.isEmpty()) {
|
||||
return null
|
||||
}
|
||||
return file(keyStore)
|
||||
}
|
||||
|
||||
// Return the key alias used for signing the release build.
|
||||
getGodotKeyAlias = { ->
|
||||
def kAlias = System.getenv("GODOT_ANDROID_KEYSTORE_ALIAS")
|
||||
return kAlias
|
||||
}
|
||||
|
||||
// Return the password for the key used for signing the release build.
|
||||
getGodotSigningPassword = { ->
|
||||
def signingPassword = System.getenv("GODOT_ANDROID_SIGN_PASSWORD")
|
||||
return signingPassword
|
||||
}
|
||||
|
||||
// Returns true if the environment variables contains the configuration for signing the release
|
||||
// build.
|
||||
hasReleaseSigningConfigs = { ->
|
||||
def keystoreFile = getGodotKeystoreFile()
|
||||
def keyAlias = getGodotKeyAlias()
|
||||
def signingPassword = getGodotSigningPassword()
|
||||
|
||||
return keystoreFile != null && keystoreFile.isFile()
|
||||
&& keyAlias != null && !keyAlias.isEmpty()
|
||||
&& signingPassword != null && !signingPassword.isEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
def generateVersionCode() {
|
||||
int libraryVersionCode = getGodotLibraryVersionCode()
|
||||
return (libraryVersionCode * editorBuildNumberOffset) + getEditorBuildNumber()
|
||||
}
|
||||
|
||||
def generateVersionName() {
|
||||
String libraryVersionName = getGodotLibraryVersionName()
|
||||
int buildNumber = getEditorBuildNumber()
|
||||
return buildNumber == 0 ? libraryVersionName : libraryVersionName + ".$buildNumber"
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion versions.compileSdk
|
||||
buildToolsVersion versions.buildTools
|
||||
ndkVersion versions.ndkVersion
|
||||
|
||||
namespace = "org.godotengine.editor"
|
||||
|
||||
defaultConfig {
|
||||
// The 'applicationId' suffix allows to install Godot 3.x(v3) and 4.x(v4) on the same device
|
||||
applicationId "org.godotengine.editor.v4"
|
||||
versionCode generateVersionCode()
|
||||
versionName generateVersionName()
|
||||
minSdkVersion versions.minSdk
|
||||
targetSdkVersion versions.targetSdk
|
||||
|
||||
missingDimensionStrategy 'products', 'editor'
|
||||
manifestPlaceholders += [
|
||||
editorAppName: "Godot Engine 4",
|
||||
editorBuildSuffix: ""
|
||||
]
|
||||
|
||||
ndk { debugSymbolLevel 'NONE' }
|
||||
}
|
||||
|
||||
base {
|
||||
archivesName = "android_editor"
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility versions.javaVersion
|
||||
targetCompatibility versions.javaVersion
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = versions.javaVersion
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
release {
|
||||
storeFile getGodotKeystoreFile()
|
||||
storePassword getGodotSigningPassword()
|
||||
keyAlias getGodotKeyAlias()
|
||||
keyPassword getGodotSigningPassword()
|
||||
}
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
buildConfig = true
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
dev {
|
||||
initWith debug
|
||||
applicationIdSuffix ".dev"
|
||||
manifestPlaceholders += [editorBuildSuffix: " (dev)"]
|
||||
}
|
||||
|
||||
debug {
|
||||
initWith release
|
||||
applicationIdSuffix ".debug"
|
||||
manifestPlaceholders += [editorBuildSuffix: " (debug)"]
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
|
||||
release {
|
||||
if (hasReleaseSigningConfigs()) {
|
||||
signingConfig signingConfigs.release
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
// Debug symbols are kept for development within Android Studio.
|
||||
if (shouldNotStrip()) {
|
||||
jniLibs {
|
||||
keepDebugSymbols += '**/*.so'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
flavorDimensions = ["android_distribution"]
|
||||
productFlavors {
|
||||
android {
|
||||
dimension "android_distribution"
|
||||
missingDimensionStrategy 'products', 'editor'
|
||||
}
|
||||
horizonos {
|
||||
dimension "android_distribution"
|
||||
missingDimensionStrategy 'products', 'editor'
|
||||
ndk {
|
||||
//noinspection ChromeOsAbiSupport
|
||||
abiFilters "arm64-v8a"
|
||||
}
|
||||
applicationIdSuffix ".meta"
|
||||
versionNameSuffix "-meta"
|
||||
targetSdkVersion 32
|
||||
}
|
||||
picoos {
|
||||
dimension "android_distribution"
|
||||
missingDimensionStrategy 'products', 'editor'
|
||||
ndk {
|
||||
//noinspection ChromeOsAbiSupport
|
||||
abiFilters "arm64-v8a"
|
||||
}
|
||||
applicationIdSuffix ".pico"
|
||||
versionNameSuffix "-pico"
|
||||
minSdkVersion 29
|
||||
targetSdkVersion 32
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(dir: "libs", include: ["*.jar", "*.aar"])
|
||||
|
||||
implementation "androidx.fragment:fragment:$versions.fragmentVersion"
|
||||
implementation project(":lib")
|
||||
|
||||
implementation "androidx.window:window:1.3.0"
|
||||
implementation "androidx.core:core-splashscreen:$versions.splashscreenVersion"
|
||||
implementation "androidx.constraintlayout:constraintlayout:2.2.1"
|
||||
implementation "org.bouncycastle:bcprov-jdk15to18:1.78"
|
||||
|
||||
// Meta dependencies
|
||||
horizonosImplementation "org.godotengine:godot-openxr-vendors-meta:$versions.openxrVendorsVersion"
|
||||
// Pico dependencies
|
||||
picoosImplementation "org.godotengine:godot-openxr-vendors-pico:$versions.openxrVendorsVersion"
|
||||
}
|
||||
1
platform/android/java/editor/src/.gitignore
vendored
Normal file
1
platform/android/java/editor/src/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
!/debug
|
||||
@@ -0,0 +1,38 @@
|
||||
/**************************************************************************/
|
||||
/* GodotEditor.kt */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
package org.godotengine.editor
|
||||
|
||||
/**
|
||||
* Primary window of the Godot Editor.
|
||||
*
|
||||
* This is the implementation of the editor used when running on regular Android devices.
|
||||
*/
|
||||
open class GodotEditor : BaseGodotEditor()
|
||||
@@ -0,0 +1,91 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:horizonos="http://schemas.horizonos/sdk">
|
||||
|
||||
<horizonos:uses-horizonos-sdk
|
||||
horizonos:minSdkVersion="69"
|
||||
horizonos:targetSdkVersion="69" />
|
||||
|
||||
<uses-feature
|
||||
android:name="android.hardware.vr.headtracking"
|
||||
android:required="true"
|
||||
android:version="1"/>
|
||||
|
||||
<!-- Oculus Quest hand tracking -->
|
||||
<uses-permission android:name="com.oculus.permission.HAND_TRACKING" />
|
||||
<uses-feature
|
||||
android:name="oculus.software.handtracking"
|
||||
android:required="false" />
|
||||
|
||||
<!-- Passthrough feature flag -->
|
||||
<uses-feature android:name="com.oculus.feature.PASSTHROUGH"
|
||||
android:required="false" />
|
||||
|
||||
<!-- Overlay keyboard support -->
|
||||
<uses-feature android:name="oculus.software.overlay_keyboard" android:required="false"/>
|
||||
|
||||
<!-- Render model -->
|
||||
<uses-permission android:name="com.oculus.permission.RENDER_MODEL" />
|
||||
<uses-feature android:name="com.oculus.feature.RENDER_MODEL" android:required="false" />
|
||||
|
||||
<!-- Anchor api -->
|
||||
<uses-permission android:name="com.oculus.permission.USE_ANCHOR_API" />
|
||||
|
||||
<!-- Scene api -->
|
||||
<uses-permission android:name="com.oculus.permission.USE_SCENE" />
|
||||
|
||||
<!-- Temp removal of the 'REQUEST_INSTALL_PACKAGES' permission as it's currently forbidden by the Horizon OS store -->
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" tools:node="remove" />
|
||||
|
||||
<!-- Passthrough feature -->
|
||||
<uses-feature
|
||||
android:name="android.hardware.camera2.any"
|
||||
android:required="false"/>
|
||||
<uses-permission android:name="horizonos.permission.HEADSET_CAMERA"/>
|
||||
|
||||
<application>
|
||||
|
||||
<activity
|
||||
android:name=".GodotEditor"
|
||||
android:exported="true"
|
||||
android:screenOrientation="landscape"
|
||||
tools:node="merge"
|
||||
tools:replace="android:screenOrientation">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<category android:name="com.oculus.intent.category.2D" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data android:name="com.oculus.vrshell.free_resizing_lock_aspect_ratio" android:value="true"/>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".GodotXRGame"
|
||||
android:exported="false"
|
||||
tools:node="merge">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="com.oculus.intent.category.VR" />
|
||||
<category android:name="org.khronos.openxr.intent.category.IMMERSIVE_HMD" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- Supported Meta devices -->
|
||||
<meta-data
|
||||
android:name="com.oculus.supportedDevices"
|
||||
android:value="quest2|quest3|questpro"
|
||||
tools:replace="android:value" />
|
||||
|
||||
<!-- Enable system splash screen -->
|
||||
<meta-data android:name="com.oculus.ossplash" android:value="true"/>
|
||||
<!-- Enable passthrough background during the splash screen -->
|
||||
<meta-data android:name="com.oculus.ossplash.background" android:value="passthrough-contextual"/>
|
||||
|
||||
<meta-data android:name="com.oculus.handtracking.version" android:value="V2.0" />
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
BIN
platform/android/java/editor/src/horizonos/assets/vr_splash.png
Normal file
BIN
platform/android/java/editor/src/horizonos/assets/vr_splash.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
@@ -0,0 +1,56 @@
|
||||
/**************************************************************************/
|
||||
/* GodotEditor.kt */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
package org.godotengine.editor
|
||||
|
||||
/**
|
||||
* Primary window of the Godot Editor.
|
||||
*
|
||||
* This is the implementation of the editor used when running on HorizonOS devices.
|
||||
*/
|
||||
open class GodotEditor : BaseGodotEditor() {
|
||||
|
||||
override fun getExcludedPermissions(): MutableSet<String> {
|
||||
val excludedPermissions = super.getExcludedPermissions().apply {
|
||||
// The AVATAR_CAMERA and HEADSET_CAMERA permissions are requested when `CameraFeed.feed_is_active`
|
||||
// is enabled.
|
||||
add("horizonos.permission.AVATAR_CAMERA")
|
||||
add("horizonos.permission.HEADSET_CAMERA")
|
||||
}
|
||||
return excludedPermissions
|
||||
}
|
||||
|
||||
override fun getXRRuntimePermissions(): MutableSet<String> {
|
||||
val xrRuntimePermissions = super.getXRRuntimePermissions()
|
||||
xrRuntimePermissions.add("com.oculus.permission.USE_SCENE")
|
||||
xrRuntimePermissions.add("horizonos.permission.USE_SCENE")
|
||||
return xrRuntimePermissions
|
||||
}
|
||||
}
|
||||
134
platform/android/java/editor/src/main/AndroidManifest.xml
Normal file
134
platform/android/java/editor/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,134 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:installLocation="auto">
|
||||
|
||||
<supports-screens
|
||||
android:largeScreens="true"
|
||||
android:normalScreens="true"
|
||||
android:smallScreens="false"
|
||||
android:xlargeScreens="true" />
|
||||
|
||||
<uses-feature
|
||||
android:glEsVersion="0x00030000"
|
||||
android:required="true" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.camera"
|
||||
android:required="false" />
|
||||
|
||||
<uses-permission
|
||||
android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
|
||||
tools:ignore="ScopedStorage" />
|
||||
<uses-permission
|
||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="29" />
|
||||
<uses-permission
|
||||
android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="29" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
|
||||
<application
|
||||
android:allowBackup="false"
|
||||
android:icon="@mipmap/themed_icon"
|
||||
android:label="${editorAppName}${editorBuildSuffix}"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:theme="@style/GodotEditorSplashScreenTheme"
|
||||
tools:ignore="GoogleAppIndexingWarning">
|
||||
<profileable
|
||||
android:shell="true"
|
||||
android:enabled="true"
|
||||
tools:targetApi="29" />
|
||||
|
||||
<activity
|
||||
android:name=".GodotEditor"
|
||||
android:configChanges="layoutDirection|locale|orientation|keyboardHidden|screenSize|smallestScreenSize|density|keyboard|navigation|screenLayout|uiMode"
|
||||
android:exported="true"
|
||||
android:icon="@mipmap/themed_icon"
|
||||
android:launchMode="singleTask"
|
||||
android:screenOrientation="userLandscape">
|
||||
<layout
|
||||
android:defaultWidth="@dimen/editor_default_window_width"
|
||||
android:defaultHeight="@dimen/editor_default_window_height" />
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- Intent filter used to intercept hybrid PANEL launch for the current editor project, and route it
|
||||
properly through the editor 'run' logic (e.g: debugger setup) -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="org.godotengine.xr.hybrid.PANEL" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- Intent filter used to intercept hybrid IMMERSIVE launch for the current editor project, and route it
|
||||
properly through the editor 'run' logic (e.g: debugger setup) -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="org.godotengine.xr.hybrid.IMMERSIVE" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".GodotGame"
|
||||
android:configChanges="layoutDirection|locale|orientation|keyboardHidden|screenSize|smallestScreenSize|density|keyboard|navigation|screenLayout|uiMode"
|
||||
android:exported="false"
|
||||
android:icon="@mipmap/ic_play_window"
|
||||
android:label="@string/godot_game_activity_name"
|
||||
android:launchMode="singleTask"
|
||||
android:process=":GodotGame"
|
||||
android:autoRemoveFromRecents="true"
|
||||
android:theme="@style/GodotGameTheme"
|
||||
android:supportsPictureInPicture="true"
|
||||
android:screenOrientation="userLandscape">
|
||||
<layout
|
||||
android:defaultWidth="@dimen/editor_default_window_width"
|
||||
android:defaultHeight="@dimen/editor_default_window_height" />
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".embed.EmbeddedGodotGame"
|
||||
android:configChanges="layoutDirection|locale|orientation|keyboardHidden|screenSize|smallestScreenSize|density|keyboard|navigation|screenLayout|uiMode"
|
||||
android:exported="false"
|
||||
android:icon="@mipmap/ic_play_window"
|
||||
android:label="@string/godot_game_activity_name"
|
||||
android:theme="@style/GodotEmbeddedGameTheme"
|
||||
android:taskAffinity=":embed"
|
||||
android:excludeFromRecents="true"
|
||||
android:launchMode="singleTask"
|
||||
android:process=":EmbeddedGodotGame"
|
||||
android:supportsPictureInPicture="true" />
|
||||
<activity
|
||||
android:name=".GodotXRGame"
|
||||
android:configChanges="layoutDirection|locale|orientation|keyboardHidden|screenSize|smallestScreenSize|density|keyboard|navigation|screenLayout|uiMode"
|
||||
android:process=":GodotXRGame"
|
||||
android:launchMode="singleTask"
|
||||
android:icon="@mipmap/ic_play_window"
|
||||
android:label="@string/godot_game_activity_name"
|
||||
android:exported="false"
|
||||
android:autoRemoveFromRecents="true"
|
||||
android:screenOrientation="landscape"
|
||||
android:resizeableActivity="false"
|
||||
android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen" />
|
||||
|
||||
<!--
|
||||
We remove this meta-data originating from the vendors plugin as we only need the loader for
|
||||
now since the project being edited provides its own version of the vendors plugin.
|
||||
|
||||
This needs to be removed once we start implementing the immersive version of the project
|
||||
manager and editor windows.
|
||||
-->
|
||||
<meta-data
|
||||
android:name="org.godotengine.plugin.v2.GodotOpenXR"
|
||||
android:value="org.godotengine.openxr.vendors.GodotOpenXR"
|
||||
tools:node="remove" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,550 @@
|
||||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig;
|
||||
|
||||
import com.android.apksig.apk.ApkFormatException;
|
||||
import com.android.apksig.util.DataSink;
|
||||
import com.android.apksig.util.DataSource;
|
||||
import com.android.apksig.util.RunnablesExecutor;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SignatureException;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* APK signing logic which is independent of how input and output APKs are stored, parsed, and
|
||||
* generated.
|
||||
*
|
||||
* <p><h3>Operating Model</h3>
|
||||
*
|
||||
* The abstract operating model is that there is an input APK which is being signed, thus producing
|
||||
* an output APK. In reality, there may be just an output APK being built from scratch, or the input
|
||||
* APK and the output APK may be the same file. Because this engine does not deal with reading and
|
||||
* writing files, it can handle all of these scenarios.
|
||||
*
|
||||
* <p>The engine is stateful and thus cannot be used for signing multiple APKs. However, once
|
||||
* the engine signed an APK, the engine can be used to re-sign the APK after it has been modified.
|
||||
* This may be more efficient than signing the APK using a new instance of the engine. See
|
||||
* <a href="#incremental">Incremental Operation</a>.
|
||||
*
|
||||
* <p>In the engine's operating model, a signed APK is produced as follows.
|
||||
* <ol>
|
||||
* <li>JAR entries to be signed are output,</li>
|
||||
* <li>JAR archive is signed using JAR signing, thus adding the so-called v1 signature to the
|
||||
* output,</li>
|
||||
* <li>JAR archive is signed using APK Signature Scheme v2, thus adding the so-called v2 signature
|
||||
* to the output.</li>
|
||||
* </ol>
|
||||
*
|
||||
* <p>The input APK may contain JAR entries which, depending on the engine's configuration, may or
|
||||
* may not be output (e.g., existing signatures may need to be preserved or stripped) or which the
|
||||
* engine will overwrite as part of signing. The engine thus offers {@link #inputJarEntry(String)}
|
||||
* which tells the client whether the input JAR entry needs to be output. This avoids the need for
|
||||
* the client to hard-code the aspects of APK signing which determine which parts of input must be
|
||||
* ignored. Similarly, the engine offers {@link #inputApkSigningBlock(DataSource)} to help the
|
||||
* client avoid dealing with preserving or stripping APK Signature Scheme v2 signature of the input
|
||||
* APK.
|
||||
*
|
||||
* <p>To use the engine to sign an input APK (or a collection of JAR entries), follow these
|
||||
* steps:
|
||||
* <ol>
|
||||
* <li>Obtain a new instance of the engine -- engine instances are stateful and thus cannot be used
|
||||
* for signing multiple APKs.</li>
|
||||
* <li>Locate the input APK's APK Signing Block and provide it to
|
||||
* {@link #inputApkSigningBlock(DataSource)}.</li>
|
||||
* <li>For each JAR entry in the input APK, invoke {@link #inputJarEntry(String)} to determine
|
||||
* whether this entry should be output. The engine may request to inspect the entry.</li>
|
||||
* <li>For each output JAR entry, invoke {@link #outputJarEntry(String)} which may request to
|
||||
* inspect the entry.</li>
|
||||
* <li>Once all JAR entries have been output, invoke {@link #outputJarEntries()} which may request
|
||||
* that additional JAR entries are output. These entries comprise the output APK's JAR
|
||||
* signature.</li>
|
||||
* <li>Locate the ZIP Central Directory and ZIP End of Central Directory sections in the output and
|
||||
* invoke {@link #outputZipSections2(DataSource, DataSource, DataSource)} which may request that
|
||||
* an APK Signature Block is inserted before the ZIP Central Directory. The block contains the
|
||||
* output APK's APK Signature Scheme v2 signature.</li>
|
||||
* <li>Invoke {@link #outputDone()} to signal that the APK was output in full. The engine will
|
||||
* confirm that the output APK is signed.</li>
|
||||
* <li>Invoke {@link #close()} to signal that the engine will no longer be used. This lets the
|
||||
* engine free any resources it no longer needs.
|
||||
* </ol>
|
||||
*
|
||||
* <p>Some invocations of the engine may provide the client with a task to perform. The client is
|
||||
* expected to perform all requested tasks before proceeding to the next stage of signing. See
|
||||
* documentation of each method about the deadlines for performing the tasks requested by the
|
||||
* method.
|
||||
*
|
||||
* <p><h3 id="incremental">Incremental Operation</h3></a>
|
||||
*
|
||||
* The engine supports incremental operation where a signed APK is produced, then modified and
|
||||
* re-signed. This may be useful for IDEs, where an app is frequently re-signed after small changes
|
||||
* by the developer. Re-signing may be more efficient than signing from scratch.
|
||||
*
|
||||
* <p>To use the engine in incremental mode, keep notifying the engine of changes to the APK through
|
||||
* {@link #inputApkSigningBlock(DataSource)}, {@link #inputJarEntry(String)},
|
||||
* {@link #inputJarEntryRemoved(String)}, {@link #outputJarEntry(String)},
|
||||
* and {@link #outputJarEntryRemoved(String)}, perform the tasks requested by the engine through
|
||||
* these methods, and, when a new signed APK is desired, run through steps 5 onwards to re-sign the
|
||||
* APK.
|
||||
*
|
||||
* <p><h3>Output-only Operation</h3>
|
||||
*
|
||||
* The engine's abstract operating model consists of an input APK and an output APK. However, it is
|
||||
* possible to use the engine in output-only mode where the engine's {@code input...} methods are
|
||||
* not invoked. In this mode, the engine has less control over output because it cannot request that
|
||||
* some JAR entries are not output. Nevertheless, the engine will attempt to make the output APK
|
||||
* signed and will report an error if cannot do so.
|
||||
*
|
||||
* @see <a href="https://source.android.com/security/apksigning/index.html">Application Signing</a>
|
||||
*/
|
||||
public interface ApkSignerEngine extends Closeable {
|
||||
|
||||
default void setExecutor(RunnablesExecutor executor) {
|
||||
throw new UnsupportedOperationException("setExecutor method is not implemented");
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the signer engine with the data already present in the apk (if any). There
|
||||
* might already be data that can be reused if the entries has not been changed.
|
||||
*
|
||||
* @param manifestBytes
|
||||
* @param entryNames
|
||||
* @return set of entry names which were processed by the engine during the initialization, a
|
||||
* subset of entryNames
|
||||
*/
|
||||
default Set<String> initWith(byte[] manifestBytes, Set<String> entryNames) {
|
||||
throw new UnsupportedOperationException("initWith method is not implemented");
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates to this engine that the input APK contains the provided APK Signing Block. The
|
||||
* block may contain signatures of the input APK, such as APK Signature Scheme v2 signatures.
|
||||
*
|
||||
* @param apkSigningBlock APK signing block of the input APK. The provided data source is
|
||||
* guaranteed to not be used by the engine after this method terminates.
|
||||
*
|
||||
* @throws IOException if an I/O error occurs while reading the APK Signing Block
|
||||
* @throws ApkFormatException if the APK Signing Block is malformed
|
||||
* @throws IllegalStateException if this engine is closed
|
||||
*/
|
||||
void inputApkSigningBlock(DataSource apkSigningBlock)
|
||||
throws IOException, ApkFormatException, IllegalStateException;
|
||||
|
||||
/**
|
||||
* Indicates to this engine that the specified JAR entry was encountered in the input APK.
|
||||
*
|
||||
* <p>When an input entry is updated/changed, it's OK to not invoke
|
||||
* {@link #inputJarEntryRemoved(String)} before invoking this method.
|
||||
*
|
||||
* @return instructions about how to proceed with this entry
|
||||
*
|
||||
* @throws IllegalStateException if this engine is closed
|
||||
*/
|
||||
InputJarEntryInstructions inputJarEntry(String entryName) throws IllegalStateException;
|
||||
|
||||
/**
|
||||
* Indicates to this engine that the specified JAR entry was output.
|
||||
*
|
||||
* <p>It is unnecessary to invoke this method for entries added to output by this engine (e.g.,
|
||||
* requested by {@link #outputJarEntries()}) provided the entries were output with exactly the
|
||||
* data requested by the engine.
|
||||
*
|
||||
* <p>When an already output entry is updated/changed, it's OK to not invoke
|
||||
* {@link #outputJarEntryRemoved(String)} before invoking this method.
|
||||
*
|
||||
* @return request to inspect the entry or {@code null} if the engine does not need to inspect
|
||||
* the entry. The request must be fulfilled before {@link #outputJarEntries()} is
|
||||
* invoked.
|
||||
*
|
||||
* @throws IllegalStateException if this engine is closed
|
||||
*/
|
||||
InspectJarEntryRequest outputJarEntry(String entryName) throws IllegalStateException;
|
||||
|
||||
/**
|
||||
* Indicates to this engine that the specified JAR entry was removed from the input. It's safe
|
||||
* to invoke this for entries for which {@link #inputJarEntry(String)} hasn't been invoked.
|
||||
*
|
||||
* @return output policy of this JAR entry. The policy indicates how this input entry affects
|
||||
* the output APK. The client of this engine should use this information to determine
|
||||
* how the removal of this input APK's JAR entry affects the output APK.
|
||||
*
|
||||
* @throws IllegalStateException if this engine is closed
|
||||
*/
|
||||
InputJarEntryInstructions.OutputPolicy inputJarEntryRemoved(String entryName)
|
||||
throws IllegalStateException;
|
||||
|
||||
/**
|
||||
* Indicates to this engine that the specified JAR entry was removed from the output. It's safe
|
||||
* to invoke this for entries for which {@link #outputJarEntry(String)} hasn't been invoked.
|
||||
*
|
||||
* @throws IllegalStateException if this engine is closed
|
||||
*/
|
||||
void outputJarEntryRemoved(String entryName) throws IllegalStateException;
|
||||
|
||||
/**
|
||||
* Indicates to this engine that all JAR entries have been output.
|
||||
*
|
||||
* @return request to add JAR signature to the output or {@code null} if there is no need to add
|
||||
* a JAR signature. The request will contain additional JAR entries to be output. The
|
||||
* request must be fulfilled before
|
||||
* {@link #outputZipSections2(DataSource, DataSource, DataSource)} is invoked.
|
||||
*
|
||||
* @throws ApkFormatException if the APK is malformed in a way which is preventing this engine
|
||||
* from producing a valid signature. For example, if the engine uses the provided
|
||||
* {@code META-INF/MANIFEST.MF} as a template and the file is malformed.
|
||||
* @throws NoSuchAlgorithmException if a signature could not be generated because a required
|
||||
* cryptographic algorithm implementation is missing
|
||||
* @throws InvalidKeyException if a signature could not be generated because a signing key is
|
||||
* not suitable for generating the signature
|
||||
* @throws SignatureException if an error occurred while generating a signature
|
||||
* @throws IllegalStateException if there are unfulfilled requests, such as to inspect some JAR
|
||||
* entries, or if the engine is closed
|
||||
*/
|
||||
OutputJarSignatureRequest outputJarEntries()
|
||||
throws ApkFormatException, NoSuchAlgorithmException, InvalidKeyException,
|
||||
SignatureException, IllegalStateException;
|
||||
|
||||
/**
|
||||
* Indicates to this engine that the ZIP sections comprising the output APK have been output.
|
||||
*
|
||||
* <p>The provided data sources are guaranteed to not be used by the engine after this method
|
||||
* terminates.
|
||||
*
|
||||
* @deprecated This is now superseded by {@link #outputZipSections2(DataSource, DataSource,
|
||||
* DataSource)}.
|
||||
*
|
||||
* @param zipEntries the section of ZIP archive containing Local File Header records and data of
|
||||
* the ZIP entries. In a well-formed archive, this section starts at the start of the
|
||||
* archive and extends all the way to the ZIP Central Directory.
|
||||
* @param zipCentralDirectory ZIP Central Directory section
|
||||
* @param zipEocd ZIP End of Central Directory (EoCD) record
|
||||
*
|
||||
* @return request to add an APK Signing Block to the output or {@code null} if the output must
|
||||
* not contain an APK Signing Block. The request must be fulfilled before
|
||||
* {@link #outputDone()} is invoked.
|
||||
*
|
||||
* @throws IOException if an I/O error occurs while reading the provided ZIP sections
|
||||
* @throws ApkFormatException if the provided APK is malformed in a way which prevents this
|
||||
* engine from producing a valid signature. For example, if the APK Signing Block
|
||||
* provided to the engine is malformed.
|
||||
* @throws NoSuchAlgorithmException if a signature could not be generated because a required
|
||||
* cryptographic algorithm implementation is missing
|
||||
* @throws InvalidKeyException if a signature could not be generated because a signing key is
|
||||
* not suitable for generating the signature
|
||||
* @throws SignatureException if an error occurred while generating a signature
|
||||
* @throws IllegalStateException if there are unfulfilled requests, such as to inspect some JAR
|
||||
* entries or to output JAR signature, or if the engine is closed
|
||||
*/
|
||||
@Deprecated
|
||||
OutputApkSigningBlockRequest outputZipSections(
|
||||
DataSource zipEntries,
|
||||
DataSource zipCentralDirectory,
|
||||
DataSource zipEocd)
|
||||
throws IOException, ApkFormatException, NoSuchAlgorithmException,
|
||||
InvalidKeyException, SignatureException, IllegalStateException;
|
||||
|
||||
/**
|
||||
* Indicates to this engine that the ZIP sections comprising the output APK have been output.
|
||||
*
|
||||
* <p>The provided data sources are guaranteed to not be used by the engine after this method
|
||||
* terminates.
|
||||
*
|
||||
* @param zipEntries the section of ZIP archive containing Local File Header records and data of
|
||||
* the ZIP entries. In a well-formed archive, this section starts at the start of the
|
||||
* archive and extends all the way to the ZIP Central Directory.
|
||||
* @param zipCentralDirectory ZIP Central Directory section
|
||||
* @param zipEocd ZIP End of Central Directory (EoCD) record
|
||||
*
|
||||
* @return request to add an APK Signing Block to the output or {@code null} if the output must
|
||||
* not contain an APK Signing Block. The request must be fulfilled before
|
||||
* {@link #outputDone()} is invoked.
|
||||
*
|
||||
* @throws IOException if an I/O error occurs while reading the provided ZIP sections
|
||||
* @throws ApkFormatException if the provided APK is malformed in a way which prevents this
|
||||
* engine from producing a valid signature. For example, if the APK Signing Block
|
||||
* provided to the engine is malformed.
|
||||
* @throws NoSuchAlgorithmException if a signature could not be generated because a required
|
||||
* cryptographic algorithm implementation is missing
|
||||
* @throws InvalidKeyException if a signature could not be generated because a signing key is
|
||||
* not suitable for generating the signature
|
||||
* @throws SignatureException if an error occurred while generating a signature
|
||||
* @throws IllegalStateException if there are unfulfilled requests, such as to inspect some JAR
|
||||
* entries or to output JAR signature, or if the engine is closed
|
||||
*/
|
||||
OutputApkSigningBlockRequest2 outputZipSections2(
|
||||
DataSource zipEntries,
|
||||
DataSource zipCentralDirectory,
|
||||
DataSource zipEocd)
|
||||
throws IOException, ApkFormatException, NoSuchAlgorithmException,
|
||||
InvalidKeyException, SignatureException, IllegalStateException;
|
||||
|
||||
/**
|
||||
* Indicates to this engine that the signed APK was output.
|
||||
*
|
||||
* <p>This does not change the output APK. The method helps the client confirm that the current
|
||||
* output is signed.
|
||||
*
|
||||
* @throws IllegalStateException if there are unfulfilled requests, such as to inspect some JAR
|
||||
* entries or to output signatures, or if the engine is closed
|
||||
*/
|
||||
void outputDone() throws IllegalStateException;
|
||||
|
||||
/**
|
||||
* Generates a V4 signature proto and write to output file.
|
||||
*
|
||||
* @param data Input data to calculate a verity hash tree and hash root
|
||||
* @param outputFile To store the serialized V4 Signature.
|
||||
* @param ignoreFailures Whether any failures will be silently ignored.
|
||||
* @throws InvalidKeyException if a signature could not be generated because a signing key is
|
||||
* not suitable for generating the signature
|
||||
* @throws NoSuchAlgorithmException if a signature could not be generated because a required
|
||||
* cryptographic algorithm implementation is missing
|
||||
* @throws SignatureException if an error occurred while generating a signature
|
||||
* @throws IOException if protobuf fails to be serialized and written to file
|
||||
*/
|
||||
void signV4(DataSource data, File outputFile, boolean ignoreFailures)
|
||||
throws InvalidKeyException, NoSuchAlgorithmException, SignatureException, IOException;
|
||||
|
||||
/**
|
||||
* Checks if the signing configuration provided to the engine is capable of creating a
|
||||
* SourceStamp.
|
||||
*/
|
||||
default boolean isEligibleForSourceStamp() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Generates the digest of the certificate used to sign the source stamp. */
|
||||
default byte[] generateSourceStampCertificateDigest() throws SignatureException {
|
||||
return new byte[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates to this engine that it will no longer be used. Invoking this on an already closed
|
||||
* engine is OK.
|
||||
*
|
||||
* <p>This does not change the output APK. For example, if the output APK is not yet fully
|
||||
* signed, it will remain so after this method terminates.
|
||||
*/
|
||||
@Override
|
||||
void close();
|
||||
|
||||
/**
|
||||
* Instructions about how to handle an input APK's JAR entry.
|
||||
*
|
||||
* <p>The instructions indicate whether to output the entry (see {@link #getOutputPolicy()}) and
|
||||
* may contain a request to inspect the entry (see {@link #getInspectJarEntryRequest()}), in
|
||||
* which case the request must be fulfilled before {@link ApkSignerEngine#outputJarEntries()} is
|
||||
* invoked.
|
||||
*/
|
||||
public static class InputJarEntryInstructions {
|
||||
private final OutputPolicy mOutputPolicy;
|
||||
private final InspectJarEntryRequest mInspectJarEntryRequest;
|
||||
|
||||
/**
|
||||
* Constructs a new {@code InputJarEntryInstructions} instance with the provided entry
|
||||
* output policy and without a request to inspect the entry.
|
||||
*/
|
||||
public InputJarEntryInstructions(OutputPolicy outputPolicy) {
|
||||
this(outputPolicy, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new {@code InputJarEntryInstructions} instance with the provided entry
|
||||
* output mode and with the provided request to inspect the entry.
|
||||
*
|
||||
* @param inspectJarEntryRequest request to inspect the entry or {@code null} if there's no
|
||||
* need to inspect the entry.
|
||||
*/
|
||||
public InputJarEntryInstructions(
|
||||
OutputPolicy outputPolicy,
|
||||
InspectJarEntryRequest inspectJarEntryRequest) {
|
||||
mOutputPolicy = outputPolicy;
|
||||
mInspectJarEntryRequest = inspectJarEntryRequest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the output policy for this entry.
|
||||
*/
|
||||
public OutputPolicy getOutputPolicy() {
|
||||
return mOutputPolicy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request to inspect the JAR entry or {@code null} if there is no need to
|
||||
* inspect the entry.
|
||||
*/
|
||||
public InspectJarEntryRequest getInspectJarEntryRequest() {
|
||||
return mInspectJarEntryRequest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Output policy for an input APK's JAR entry.
|
||||
*/
|
||||
public static enum OutputPolicy {
|
||||
/** Entry must not be output. */
|
||||
SKIP,
|
||||
|
||||
/** Entry should be output. */
|
||||
OUTPUT,
|
||||
|
||||
/** Entry will be output by the engine. The client can thus ignore this input entry. */
|
||||
OUTPUT_BY_ENGINE,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Request to inspect the specified JAR entry.
|
||||
*
|
||||
* <p>The entry's uncompressed data must be provided to the data sink returned by
|
||||
* {@link #getDataSink()}. Once the entry's data has been provided to the sink, {@link #done()}
|
||||
* must be invoked.
|
||||
*/
|
||||
interface InspectJarEntryRequest {
|
||||
|
||||
/**
|
||||
* Returns the data sink into which the entry's uncompressed data should be sent.
|
||||
*/
|
||||
DataSink getDataSink();
|
||||
|
||||
/**
|
||||
* Indicates that entry's data has been provided in full.
|
||||
*/
|
||||
void done();
|
||||
|
||||
/**
|
||||
* Returns the name of the JAR entry.
|
||||
*/
|
||||
String getEntryName();
|
||||
}
|
||||
|
||||
/**
|
||||
* Request to add JAR signature (aka v1 signature) to the output APK.
|
||||
*
|
||||
* <p>Entries listed in {@link #getAdditionalJarEntries()} must be added to the output APK after
|
||||
* which {@link #done()} must be invoked.
|
||||
*/
|
||||
interface OutputJarSignatureRequest {
|
||||
|
||||
/**
|
||||
* Returns JAR entries that must be added to the output APK.
|
||||
*/
|
||||
List<JarEntry> getAdditionalJarEntries();
|
||||
|
||||
/**
|
||||
* Indicates that the JAR entries contained in this request were added to the output APK.
|
||||
*/
|
||||
void done();
|
||||
|
||||
/**
|
||||
* JAR entry.
|
||||
*/
|
||||
public static class JarEntry {
|
||||
private final String mName;
|
||||
private final byte[] mData;
|
||||
|
||||
/**
|
||||
* Constructs a new {@code JarEntry} with the provided name and data.
|
||||
*
|
||||
* @param data uncompressed data of the entry. Changes to this array will not be
|
||||
* reflected in {@link #getData()}.
|
||||
*/
|
||||
public JarEntry(String name, byte[] data) {
|
||||
mName = name;
|
||||
mData = data.clone();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of this ZIP entry.
|
||||
*/
|
||||
public String getName() {
|
||||
return mName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the uncompressed data of this JAR entry.
|
||||
*/
|
||||
public byte[] getData() {
|
||||
return mData.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Request to add the specified APK Signing Block to the output APK. APK Signature Scheme v2
|
||||
* signature(s) of the APK are contained in this block.
|
||||
*
|
||||
* <p>The APK Signing Block returned by {@link #getApkSigningBlock()} must be placed into the
|
||||
* output APK such that the block is immediately before the ZIP Central Directory, the offset of
|
||||
* ZIP Central Directory in the ZIP End of Central Directory record must be adjusted
|
||||
* accordingly, and then {@link #done()} must be invoked.
|
||||
*
|
||||
* <p>If the output contains an APK Signing Block, that block must be replaced by the block
|
||||
* contained in this request.
|
||||
*
|
||||
* @deprecated This is now superseded by {@link OutputApkSigningBlockRequest2}.
|
||||
*/
|
||||
@Deprecated
|
||||
interface OutputApkSigningBlockRequest {
|
||||
|
||||
/**
|
||||
* Returns the APK Signing Block.
|
||||
*/
|
||||
byte[] getApkSigningBlock();
|
||||
|
||||
/**
|
||||
* Indicates that the APK Signing Block was output as requested.
|
||||
*/
|
||||
void done();
|
||||
}
|
||||
|
||||
/**
|
||||
* Request to add the specified APK Signing Block to the output APK. APK Signature Scheme v2
|
||||
* signature(s) of the APK are contained in this block.
|
||||
*
|
||||
* <p>The APK Signing Block returned by {@link #getApkSigningBlock()} must be placed into the
|
||||
* output APK such that the block is immediately before the ZIP Central Directory. Immediately
|
||||
* before the APK Signing Block must be padding consists of the number of 0x00 bytes returned by
|
||||
* {@link #getPaddingSizeBeforeApkSigningBlock()}. The offset of ZIP Central Directory in the
|
||||
* ZIP End of Central Directory record must be adjusted accordingly, and then {@link #done()}
|
||||
* must be invoked.
|
||||
*
|
||||
* <p>If the output contains an APK Signing Block, that block must be replaced by the block
|
||||
* contained in this request.
|
||||
*/
|
||||
interface OutputApkSigningBlockRequest2 {
|
||||
/**
|
||||
* Returns the APK Signing Block.
|
||||
*/
|
||||
byte[] getApkSigningBlock();
|
||||
|
||||
/**
|
||||
* Indicates that the APK Signing Block was output as requested.
|
||||
*/
|
||||
void done();
|
||||
|
||||
/**
|
||||
* Returns the number of 0x00 bytes the caller must place immediately before APK Signing
|
||||
* Block.
|
||||
*/
|
||||
int getPaddingSizeBeforeApkSigningBlock();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
/*
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig;
|
||||
|
||||
/**
|
||||
* This class is intended as a lightweight representation of an APK signature verification issue
|
||||
* where the client does not require the additional textual details provided by a subclass.
|
||||
*/
|
||||
public class ApkVerificationIssue {
|
||||
/* The V2 signer(s) could not be read from the V2 signature block */
|
||||
public static final int V2_SIG_MALFORMED_SIGNERS = 1;
|
||||
/* A V2 signature block exists without any V2 signers */
|
||||
public static final int V2_SIG_NO_SIGNERS = 2;
|
||||
/* Failed to parse a signer's block in the V2 signature block */
|
||||
public static final int V2_SIG_MALFORMED_SIGNER = 3;
|
||||
/* Failed to parse the signer's signature record in the V2 signature block */
|
||||
public static final int V2_SIG_MALFORMED_SIGNATURE = 4;
|
||||
/* The V2 signer contained no signatures */
|
||||
public static final int V2_SIG_NO_SIGNATURES = 5;
|
||||
/* The V2 signer's certificate could not be parsed */
|
||||
public static final int V2_SIG_MALFORMED_CERTIFICATE = 6;
|
||||
/* No signing certificates exist for the V2 signer */
|
||||
public static final int V2_SIG_NO_CERTIFICATES = 7;
|
||||
/* Failed to parse the V2 signer's digest record */
|
||||
public static final int V2_SIG_MALFORMED_DIGEST = 8;
|
||||
/* The V3 signer(s) could not be read from the V3 signature block */
|
||||
public static final int V3_SIG_MALFORMED_SIGNERS = 9;
|
||||
/* A V3 signature block exists without any V3 signers */
|
||||
public static final int V3_SIG_NO_SIGNERS = 10;
|
||||
/* Failed to parse a signer's block in the V3 signature block */
|
||||
public static final int V3_SIG_MALFORMED_SIGNER = 11;
|
||||
/* Failed to parse the signer's signature record in the V3 signature block */
|
||||
public static final int V3_SIG_MALFORMED_SIGNATURE = 12;
|
||||
/* The V3 signer contained no signatures */
|
||||
public static final int V3_SIG_NO_SIGNATURES = 13;
|
||||
/* The V3 signer's certificate could not be parsed */
|
||||
public static final int V3_SIG_MALFORMED_CERTIFICATE = 14;
|
||||
/* No signing certificates exist for the V3 signer */
|
||||
public static final int V3_SIG_NO_CERTIFICATES = 15;
|
||||
/* Failed to parse the V3 signer's digest record */
|
||||
public static final int V3_SIG_MALFORMED_DIGEST = 16;
|
||||
/* The source stamp signer contained no signatures */
|
||||
public static final int SOURCE_STAMP_NO_SIGNATURE = 17;
|
||||
/* The source stamp signer's certificate could not be parsed */
|
||||
public static final int SOURCE_STAMP_MALFORMED_CERTIFICATE = 18;
|
||||
/* The source stamp contains a signature produced using an unknown algorithm */
|
||||
public static final int SOURCE_STAMP_UNKNOWN_SIG_ALGORITHM = 19;
|
||||
/* Failed to parse the signer's signature in the source stamp signature block */
|
||||
public static final int SOURCE_STAMP_MALFORMED_SIGNATURE = 20;
|
||||
/* The source stamp's signature block failed verification */
|
||||
public static final int SOURCE_STAMP_DID_NOT_VERIFY = 21;
|
||||
/* An exception was encountered when verifying the source stamp */
|
||||
public static final int SOURCE_STAMP_VERIFY_EXCEPTION = 22;
|
||||
/* The certificate digest in the APK does not match the expected digest */
|
||||
public static final int SOURCE_STAMP_EXPECTED_DIGEST_MISMATCH = 23;
|
||||
/*
|
||||
* The APK contains a source stamp signature block without a corresponding stamp certificate
|
||||
* digest in the APK contents.
|
||||
*/
|
||||
public static final int SOURCE_STAMP_SIGNATURE_BLOCK_WITHOUT_CERT_DIGEST = 24;
|
||||
/*
|
||||
* The APK does not contain the source stamp certificate digest file nor the source stamp
|
||||
* signature block.
|
||||
*/
|
||||
public static final int SOURCE_STAMP_CERT_DIGEST_AND_SIG_BLOCK_MISSING = 25;
|
||||
/*
|
||||
* None of the signatures provided by the source stamp were produced with a known signature
|
||||
* algorithm.
|
||||
*/
|
||||
public static final int SOURCE_STAMP_NO_SUPPORTED_SIGNATURE = 26;
|
||||
/*
|
||||
* The source stamp signer's certificate in the signing block does not match the certificate in
|
||||
* the APK.
|
||||
*/
|
||||
public static final int SOURCE_STAMP_CERTIFICATE_MISMATCH_BETWEEN_SIGNATURE_BLOCK_AND_APK = 27;
|
||||
/* The APK could not be properly parsed due to a ZIP or APK format exception */
|
||||
public static final int MALFORMED_APK = 28;
|
||||
/* An unexpected exception was caught when attempting to verify the APK's signatures */
|
||||
public static final int UNEXPECTED_EXCEPTION = 29;
|
||||
/* The APK contains the certificate digest file but does not contain a stamp signature block */
|
||||
public static final int SOURCE_STAMP_SIG_MISSING = 30;
|
||||
/* Source stamp block contains a malformed attribute. */
|
||||
public static final int SOURCE_STAMP_MALFORMED_ATTRIBUTE = 31;
|
||||
/* Source stamp block contains an unknown attribute. */
|
||||
public static final int SOURCE_STAMP_UNKNOWN_ATTRIBUTE = 32;
|
||||
/**
|
||||
* Failed to parse the SigningCertificateLineage structure in the source stamp
|
||||
* attributes section.
|
||||
*/
|
||||
public static final int SOURCE_STAMP_MALFORMED_LINEAGE = 33;
|
||||
/**
|
||||
* The source stamp certificate does not match the terminal node in the provided
|
||||
* proof-of-rotation structure describing the stamp certificate history.
|
||||
*/
|
||||
public static final int SOURCE_STAMP_POR_CERT_MISMATCH = 34;
|
||||
/**
|
||||
* The source stamp SigningCertificateLineage attribute contains a proof-of-rotation record
|
||||
* with signature(s) that did not verify.
|
||||
*/
|
||||
public static final int SOURCE_STAMP_POR_DID_NOT_VERIFY = 35;
|
||||
/** No V1 / jar signing signature blocks were found in the APK. */
|
||||
public static final int JAR_SIG_NO_SIGNATURES = 36;
|
||||
/** An exception was encountered when parsing the V1 / jar signer in the signature block. */
|
||||
public static final int JAR_SIG_PARSE_EXCEPTION = 37;
|
||||
/** The source stamp timestamp attribute has an invalid value. */
|
||||
public static final int SOURCE_STAMP_INVALID_TIMESTAMP = 38;
|
||||
|
||||
private final int mIssueId;
|
||||
private final String mFormat;
|
||||
private final Object[] mParams;
|
||||
|
||||
/**
|
||||
* Constructs a new {@code ApkVerificationIssue} using the provided {@code format} string and
|
||||
* {@code params}.
|
||||
*/
|
||||
public ApkVerificationIssue(String format, Object... params) {
|
||||
mIssueId = -1;
|
||||
mFormat = format;
|
||||
mParams = params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new {@code ApkVerificationIssue} using the provided {@code issueId} and {@code
|
||||
* params}.
|
||||
*/
|
||||
public ApkVerificationIssue(int issueId, Object... params) {
|
||||
mIssueId = issueId;
|
||||
mFormat = null;
|
||||
mParams = params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the numeric ID for this issue.
|
||||
*/
|
||||
public int getIssueId() {
|
||||
return mIssueId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the optional parameters for this issue.
|
||||
*/
|
||||
public Object[] getParams() {
|
||||
return mParams;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
// If this instance was created by a subclass with a format string then return the same
|
||||
// formatted String as the subclass.
|
||||
if (mFormat != null) {
|
||||
return String.format(mFormat, mParams);
|
||||
}
|
||||
StringBuilder result = new StringBuilder("mIssueId: ").append(mIssueId);
|
||||
for (Object param : mParams) {
|
||||
result.append(", ").append(param.toString());
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig;
|
||||
|
||||
import com.android.apksig.internal.apk.stamp.SourceStampConstants;
|
||||
import com.android.apksig.internal.apk.v1.V1SchemeConstants;
|
||||
import com.android.apksig.internal.apk.v2.V2SchemeConstants;
|
||||
import com.android.apksig.internal.apk.v3.V3SchemeConstants;
|
||||
|
||||
/**
|
||||
* Exports internally defined constants to allow clients to reference these values without relying
|
||||
* on internal code.
|
||||
*/
|
||||
public class Constants {
|
||||
private Constants() {}
|
||||
|
||||
public static final int VERSION_SOURCE_STAMP = 0;
|
||||
public static final int VERSION_JAR_SIGNATURE_SCHEME = 1;
|
||||
public static final int VERSION_APK_SIGNATURE_SCHEME_V2 = 2;
|
||||
public static final int VERSION_APK_SIGNATURE_SCHEME_V3 = 3;
|
||||
public static final int VERSION_APK_SIGNATURE_SCHEME_V31 = 31;
|
||||
public static final int VERSION_APK_SIGNATURE_SCHEME_V4 = 4;
|
||||
|
||||
/**
|
||||
* The maximum number of signers supported by the v1 and v2 APK Signature Schemes.
|
||||
*/
|
||||
public static final int MAX_APK_SIGNERS = 10;
|
||||
|
||||
/**
|
||||
* The default page alignment for native library files in bytes.
|
||||
*/
|
||||
public static final short LIBRARY_PAGE_ALIGNMENT_BYTES = 16384;
|
||||
|
||||
public static final String MANIFEST_ENTRY_NAME = V1SchemeConstants.MANIFEST_ENTRY_NAME;
|
||||
|
||||
public static final int APK_SIGNATURE_SCHEME_V2_BLOCK_ID =
|
||||
V2SchemeConstants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID;
|
||||
|
||||
public static final int APK_SIGNATURE_SCHEME_V3_BLOCK_ID =
|
||||
V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID;
|
||||
public static final int APK_SIGNATURE_SCHEME_V31_BLOCK_ID =
|
||||
V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID;
|
||||
public static final int PROOF_OF_ROTATION_ATTR_ID = V3SchemeConstants.PROOF_OF_ROTATION_ATTR_ID;
|
||||
|
||||
public static final int V1_SOURCE_STAMP_BLOCK_ID =
|
||||
SourceStampConstants.V1_SOURCE_STAMP_BLOCK_ID;
|
||||
public static final int V2_SOURCE_STAMP_BLOCK_ID =
|
||||
SourceStampConstants.V2_SOURCE_STAMP_BLOCK_ID;
|
||||
|
||||
public static final String OID_RSA_ENCRYPTION = "1.2.840.113549.1.1.1";
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
* Copyright (C) 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.apksig;
|
||||
import java.io.IOException;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public final class Hints {
|
||||
/**
|
||||
* Name of hint pattern asset file in APK.
|
||||
*/
|
||||
public static final String PIN_HINT_ASSET_ZIP_ENTRY_NAME = "assets/com.android.hints.pins.txt";
|
||||
|
||||
/**
|
||||
* Name of hint byte range data file in APK. Keep in sync with PinnerService.java.
|
||||
*/
|
||||
public static final String PIN_BYTE_RANGE_ZIP_ENTRY_NAME = "pinlist.meta";
|
||||
|
||||
private static int clampToInt(long value) {
|
||||
return (int) Math.max(0, Math.min(value, Integer.MAX_VALUE));
|
||||
}
|
||||
|
||||
public static final class ByteRange {
|
||||
final long start;
|
||||
final long end;
|
||||
|
||||
public ByteRange(long start, long end) {
|
||||
this.start = start;
|
||||
this.end = end;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class PatternWithRange {
|
||||
final Pattern pattern;
|
||||
final long offset;
|
||||
final long size;
|
||||
|
||||
public PatternWithRange(String pattern) {
|
||||
this.pattern = Pattern.compile(pattern);
|
||||
this.offset= 0;
|
||||
this.size = Long.MAX_VALUE;
|
||||
}
|
||||
|
||||
public PatternWithRange(String pattern, long offset, long size) {
|
||||
this.pattern = Pattern.compile(pattern);
|
||||
this.offset = offset;
|
||||
this.size = size;
|
||||
}
|
||||
|
||||
public Matcher matcher(CharSequence input) {
|
||||
return this.pattern.matcher(input);
|
||||
}
|
||||
|
||||
public ByteRange ClampToAbsoluteByteRange(ByteRange rangeIn) {
|
||||
if (rangeIn.end - rangeIn.start < this.offset) {
|
||||
return null;
|
||||
}
|
||||
long rangeOutStart = rangeIn.start + this.offset;
|
||||
long rangeOutSize = Math.min(rangeIn.end - rangeOutStart,
|
||||
this.size);
|
||||
return new ByteRange(rangeOutStart,
|
||||
rangeOutStart + rangeOutSize);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a blob of bytes that PinnerService understands as a
|
||||
* sequence of byte ranges to pin.
|
||||
*/
|
||||
public static byte[] encodeByteRangeList(List<ByteRange> pinByteRanges) {
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream(pinByteRanges.size() * 8);
|
||||
DataOutputStream out = new DataOutputStream(bos);
|
||||
try {
|
||||
for (ByteRange pinByteRange : pinByteRanges) {
|
||||
out.writeInt(clampToInt(pinByteRange.start));
|
||||
out.writeInt(clampToInt(pinByteRange.end - pinByteRange.start));
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
throw new AssertionError("impossible", ex);
|
||||
}
|
||||
return bos.toByteArray();
|
||||
}
|
||||
|
||||
public static ArrayList<PatternWithRange> parsePinPatterns(byte[] patternBlob) {
|
||||
ArrayList<PatternWithRange> pinPatterns = new ArrayList<>();
|
||||
try {
|
||||
for (String rawLine : new String(patternBlob, "UTF-8").split("\n")) {
|
||||
String line = rawLine.replaceFirst("#.*", ""); // # starts a comment
|
||||
String[] fields = line.split(" ");
|
||||
if (fields.length == 1) {
|
||||
pinPatterns.add(new PatternWithRange(fields[0]));
|
||||
} else if (fields.length == 3) {
|
||||
long start = Long.parseLong(fields[1]);
|
||||
long end = Long.parseLong(fields[2]);
|
||||
pinPatterns.add(new PatternWithRange(fields[0], start, end - start));
|
||||
} else {
|
||||
throw new AssertionError("bad pin pattern line " + line);
|
||||
}
|
||||
}
|
||||
} catch (UnsupportedEncodingException ex) {
|
||||
throw new RuntimeException("UTF-8 must be supported", ex);
|
||||
}
|
||||
return pinPatterns;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
# apksig ([commit ac5cbb07d87cc342fcf07715857a812305d69888](https://android.googlesource.com/platform/tools/apksig/+/ac5cbb07d87cc342fcf07715857a812305d69888))
|
||||
|
||||
apksig is a project which aims to simplify APK signing and checking whether APK signatures are
|
||||
expected to verify on Android. apksig supports
|
||||
[JAR signing](https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File)
|
||||
(used by Android since day one) and
|
||||
[APK Signature Scheme v2](https://source.android.com/security/apksigning/v2.html) (supported since
|
||||
Android Nougat, API Level 24). apksig is meant to be used outside of Android devices.
|
||||
|
||||
The key feature of apksig is that it knows about differences in APK signature verification logic
|
||||
between different versions of the Android platform. apksig thus thoroughly checks whether an APK's
|
||||
signature is expected to verify on all Android platform versions supported by the APK. When signing
|
||||
an APK, apksig chooses the most appropriate cryptographic algorithms based on the Android platform
|
||||
versions supported by the APK being signed.
|
||||
|
||||
## apksig library
|
||||
|
||||
apksig library offers three primitives:
|
||||
|
||||
* `ApkSigner` which signs the provided APK so that it verifies on all Android platform versions
|
||||
supported by the APK. The range of platform versions can be customized.
|
||||
* `ApkVerifier` which checks whether the provided APK is expected to verify on all Android
|
||||
platform versions supported by the APK. The range of platform versions can be customized.
|
||||
* `(Default)ApkSignerEngine` which abstracts away signing APKs from parsing and building APKs.
|
||||
This is useful in optimized APK building pipelines, such as in Android Plugin for Gradle,
|
||||
which need to perform signing while building an APK, instead of after. For simpler use cases
|
||||
where the APK to be signed is available upfront, the `ApkSigner` above is easier to use.
|
||||
|
||||
_NOTE: Some public classes of the library are in packages having the word "internal" in their name.
|
||||
These are not public API of the library. Do not use \*.internal.\* classes directly because these
|
||||
classes may change any time without regard to existing clients outside of `apksig` and `apksigner`._
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,911 @@
|
||||
/*
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig;
|
||||
|
||||
import static com.android.apksig.Constants.VERSION_APK_SIGNATURE_SCHEME_V2;
|
||||
import static com.android.apksig.Constants.VERSION_APK_SIGNATURE_SCHEME_V3;
|
||||
import static com.android.apksig.Constants.VERSION_JAR_SIGNATURE_SCHEME;
|
||||
import static com.android.apksig.apk.ApkUtilsLite.computeSha256DigestBytes;
|
||||
import static com.android.apksig.internal.apk.stamp.SourceStampConstants.SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME;
|
||||
import static com.android.apksig.internal.apk.v1.V1SchemeConstants.MANIFEST_ENTRY_NAME;
|
||||
|
||||
import com.android.apksig.apk.ApkFormatException;
|
||||
import com.android.apksig.apk.ApkUtilsLite;
|
||||
import com.android.apksig.internal.apk.ApkSigResult;
|
||||
import com.android.apksig.internal.apk.ApkSignerInfo;
|
||||
import com.android.apksig.internal.apk.ApkSigningBlockUtilsLite;
|
||||
import com.android.apksig.internal.apk.ContentDigestAlgorithm;
|
||||
import com.android.apksig.internal.apk.SignatureAlgorithm;
|
||||
import com.android.apksig.internal.apk.SignatureInfo;
|
||||
import com.android.apksig.internal.apk.SignatureNotFoundException;
|
||||
import com.android.apksig.internal.apk.stamp.SourceStampConstants;
|
||||
import com.android.apksig.internal.apk.stamp.V2SourceStampVerifier;
|
||||
import com.android.apksig.internal.apk.v2.V2SchemeConstants;
|
||||
import com.android.apksig.internal.apk.v3.V3SchemeConstants;
|
||||
import com.android.apksig.internal.util.AndroidSdkVersion;
|
||||
import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
|
||||
import com.android.apksig.internal.zip.CentralDirectoryRecord;
|
||||
import com.android.apksig.internal.zip.LocalFileRecord;
|
||||
import com.android.apksig.internal.zip.ZipUtils;
|
||||
import com.android.apksig.util.DataSource;
|
||||
import com.android.apksig.util.DataSources;
|
||||
import com.android.apksig.zip.ZipFormatException;
|
||||
import com.android.apksig.zip.ZipSections;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.Closeable;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.nio.BufferUnderflowException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.cert.Certificate;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.EnumMap;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* APK source stamp verifier intended only to verify the validity of the stamp signature.
|
||||
*
|
||||
* <p>Note, this verifier does not validate the signatures of the jar signing / APK signature blocks
|
||||
* when obtaining the digests for verification. This verifier should only be used in cases where
|
||||
* another mechanism has already been used to verify the APK signatures.
|
||||
*/
|
||||
public class SourceStampVerifier {
|
||||
private final File mApkFile;
|
||||
private final DataSource mApkDataSource;
|
||||
|
||||
private final int mMinSdkVersion;
|
||||
private final int mMaxSdkVersion;
|
||||
|
||||
private SourceStampVerifier(
|
||||
File apkFile,
|
||||
DataSource apkDataSource,
|
||||
int minSdkVersion,
|
||||
int maxSdkVersion) {
|
||||
mApkFile = apkFile;
|
||||
mApkDataSource = apkDataSource;
|
||||
mMinSdkVersion = minSdkVersion;
|
||||
mMaxSdkVersion = maxSdkVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies the APK's source stamp signature and returns the result of the verification.
|
||||
*
|
||||
* <p>The APK's source stamp can be considered verified if the result's {@link
|
||||
* Result#isVerified()} returns {@code true}. If source stamp verification fails all of the
|
||||
* resulting errors can be obtained from {@link Result#getAllErrors()}, or individual errors
|
||||
* can be obtained as follows:
|
||||
* <ul>
|
||||
* <li>Obtain the generic errors via {@link Result#getErrors()}
|
||||
* <li>Obtain the V2 signers via {@link Result#getV2SchemeSigners()}, then for each signer
|
||||
* query for any errors with {@link Result.SignerInfo#getErrors()}
|
||||
* <li>Obtain the V3 signers via {@link Result#getV3SchemeSigners()}, then for each signer
|
||||
* query for any errors with {@link Result.SignerInfo#getErrors()}
|
||||
* <li>Obtain the source stamp signer via {@link Result#getSourceStampInfo()}, then query
|
||||
* for any stamp errors with {@link Result.SourceStampInfo#getErrors()}
|
||||
* </ul>
|
||||
*/
|
||||
public SourceStampVerifier.Result verifySourceStamp() {
|
||||
return verifySourceStamp(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies the APK's source stamp signature, including verification that the SHA-256 digest of
|
||||
* the stamp signing certificate matches the {@code expectedCertDigest}, and returns the result
|
||||
* of the verification.
|
||||
*
|
||||
* <p>A value of {@code null} for the {@code expectedCertDigest} will verify the source stamp,
|
||||
* if present, without verifying the actual source stamp certificate used to sign the source
|
||||
* stamp. This can be used to verify an APK contains a properly signed source stamp without
|
||||
* verifying a particular signer.
|
||||
*
|
||||
* @see #verifySourceStamp()
|
||||
*/
|
||||
public SourceStampVerifier.Result verifySourceStamp(String expectedCertDigest) {
|
||||
Closeable in = null;
|
||||
try {
|
||||
DataSource apk;
|
||||
if (mApkDataSource != null) {
|
||||
apk = mApkDataSource;
|
||||
} else if (mApkFile != null) {
|
||||
RandomAccessFile f = new RandomAccessFile(mApkFile, "r");
|
||||
in = f;
|
||||
apk = DataSources.asDataSource(f, 0, f.length());
|
||||
} else {
|
||||
throw new IllegalStateException("APK not provided");
|
||||
}
|
||||
return verifySourceStamp(apk, expectedCertDigest);
|
||||
} catch (IOException e) {
|
||||
Result result = new Result();
|
||||
result.addVerificationError(ApkVerificationIssue.UNEXPECTED_EXCEPTION, e);
|
||||
return result;
|
||||
} finally {
|
||||
if (in != null) {
|
||||
try {
|
||||
in.close();
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies the provided {@code apk}'s source stamp signature, including verification of the
|
||||
* SHA-256 digest of the stamp signing certificate matches the {@code expectedCertDigest}, and
|
||||
* returns the result of the verification.
|
||||
*
|
||||
* @see #verifySourceStamp(String)
|
||||
*/
|
||||
private SourceStampVerifier.Result verifySourceStamp(DataSource apk,
|
||||
String expectedCertDigest) {
|
||||
Result result = new Result();
|
||||
try {
|
||||
ZipSections zipSections = ApkUtilsLite.findZipSections(apk);
|
||||
// Attempt to obtain the source stamp's certificate digest from the APK.
|
||||
List<CentralDirectoryRecord> cdRecords =
|
||||
ZipUtils.parseZipCentralDirectory(apk, zipSections);
|
||||
CentralDirectoryRecord sourceStampCdRecord = null;
|
||||
for (CentralDirectoryRecord cdRecord : cdRecords) {
|
||||
if (SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME.equals(cdRecord.getName())) {
|
||||
sourceStampCdRecord = cdRecord;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If the source stamp's certificate digest is not available within the APK then the
|
||||
// source stamp cannot be verified; check if a source stamp signing block is in the
|
||||
// APK's signature block to determine the appropriate status to return.
|
||||
if (sourceStampCdRecord == null) {
|
||||
boolean stampSigningBlockFound;
|
||||
try {
|
||||
ApkSigningBlockUtilsLite.findSignature(apk, zipSections,
|
||||
SourceStampConstants.V2_SOURCE_STAMP_BLOCK_ID);
|
||||
stampSigningBlockFound = true;
|
||||
} catch (SignatureNotFoundException e) {
|
||||
stampSigningBlockFound = false;
|
||||
}
|
||||
result.addVerificationError(stampSigningBlockFound
|
||||
? ApkVerificationIssue.SOURCE_STAMP_SIGNATURE_BLOCK_WITHOUT_CERT_DIGEST
|
||||
: ApkVerificationIssue.SOURCE_STAMP_CERT_DIGEST_AND_SIG_BLOCK_MISSING);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Verify that the contents of the source stamp certificate digest match the expected
|
||||
// value, if provided.
|
||||
byte[] sourceStampCertificateDigest =
|
||||
LocalFileRecord.getUncompressedData(
|
||||
apk,
|
||||
sourceStampCdRecord,
|
||||
zipSections.getZipCentralDirectoryOffset());
|
||||
if (expectedCertDigest != null) {
|
||||
String actualCertDigest = ApkSigningBlockUtilsLite.toHex(
|
||||
sourceStampCertificateDigest);
|
||||
if (!expectedCertDigest.equalsIgnoreCase(actualCertDigest)) {
|
||||
result.addVerificationError(
|
||||
ApkVerificationIssue.SOURCE_STAMP_EXPECTED_DIGEST_MISMATCH,
|
||||
actualCertDigest, expectedCertDigest);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeApkContentDigests =
|
||||
new HashMap<>();
|
||||
if (mMaxSdkVersion >= AndroidSdkVersion.P) {
|
||||
SignatureInfo signatureInfo;
|
||||
try {
|
||||
signatureInfo = ApkSigningBlockUtilsLite.findSignature(apk, zipSections,
|
||||
V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID);
|
||||
} catch (SignatureNotFoundException e) {
|
||||
signatureInfo = null;
|
||||
}
|
||||
if (signatureInfo != null) {
|
||||
Map<ContentDigestAlgorithm, byte[]> apkContentDigests = new EnumMap<>(
|
||||
ContentDigestAlgorithm.class);
|
||||
parseSigners(signatureInfo.signatureBlock, VERSION_APK_SIGNATURE_SCHEME_V3,
|
||||
apkContentDigests, result);
|
||||
signatureSchemeApkContentDigests.put(
|
||||
VERSION_APK_SIGNATURE_SCHEME_V3, apkContentDigests);
|
||||
}
|
||||
}
|
||||
|
||||
if (mMaxSdkVersion >= AndroidSdkVersion.N && (mMinSdkVersion < AndroidSdkVersion.P ||
|
||||
signatureSchemeApkContentDigests.isEmpty())) {
|
||||
SignatureInfo signatureInfo;
|
||||
try {
|
||||
signatureInfo = ApkSigningBlockUtilsLite.findSignature(apk, zipSections,
|
||||
V2SchemeConstants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID);
|
||||
} catch (SignatureNotFoundException e) {
|
||||
signatureInfo = null;
|
||||
}
|
||||
if (signatureInfo != null) {
|
||||
Map<ContentDigestAlgorithm, byte[]> apkContentDigests = new EnumMap<>(
|
||||
ContentDigestAlgorithm.class);
|
||||
parseSigners(signatureInfo.signatureBlock, VERSION_APK_SIGNATURE_SCHEME_V2,
|
||||
apkContentDigests, result);
|
||||
signatureSchemeApkContentDigests.put(
|
||||
VERSION_APK_SIGNATURE_SCHEME_V2, apkContentDigests);
|
||||
}
|
||||
}
|
||||
|
||||
if (mMinSdkVersion < AndroidSdkVersion.N
|
||||
|| signatureSchemeApkContentDigests.isEmpty()) {
|
||||
Map<ContentDigestAlgorithm, byte[]> apkContentDigests =
|
||||
getApkContentDigestFromV1SigningScheme(cdRecords, apk, zipSections, result);
|
||||
signatureSchemeApkContentDigests.put(VERSION_JAR_SIGNATURE_SCHEME,
|
||||
apkContentDigests);
|
||||
}
|
||||
|
||||
ApkSigResult sourceStampResult =
|
||||
V2SourceStampVerifier.verify(
|
||||
apk,
|
||||
zipSections,
|
||||
sourceStampCertificateDigest,
|
||||
signatureSchemeApkContentDigests,
|
||||
mMinSdkVersion,
|
||||
mMaxSdkVersion);
|
||||
result.mergeFrom(sourceStampResult);
|
||||
return result;
|
||||
} catch (ApkFormatException | IOException | ZipFormatException e) {
|
||||
result.addVerificationError(ApkVerificationIssue.MALFORMED_APK, e);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
result.addVerificationError(ApkVerificationIssue.UNEXPECTED_EXCEPTION, e);
|
||||
} catch (SignatureNotFoundException e) {
|
||||
result.addVerificationError(ApkVerificationIssue.SOURCE_STAMP_SIG_MISSING);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses each signer in the provided APK V2 / V3 signature block and populates corresponding
|
||||
* {@code SignerInfo} of the provided {@code result} and their {@code apkContentDigests}.
|
||||
*
|
||||
* <p>This method adds one or more errors to the {@code result} if a verification error is
|
||||
* expected to be encountered on an Android platform version in the
|
||||
* {@code [minSdkVersion, maxSdkVersion]} range.
|
||||
*/
|
||||
public static void parseSigners(
|
||||
ByteBuffer apkSignatureSchemeBlock,
|
||||
int apkSigSchemeVersion,
|
||||
Map<ContentDigestAlgorithm, byte[]> apkContentDigests,
|
||||
Result result) {
|
||||
boolean isV2Block = apkSigSchemeVersion == VERSION_APK_SIGNATURE_SCHEME_V2;
|
||||
// Both the V2 and V3 signature blocks contain the following:
|
||||
// * length-prefixed sequence of length-prefixed signers
|
||||
ByteBuffer signers;
|
||||
try {
|
||||
signers = ApkSigningBlockUtilsLite.getLengthPrefixedSlice(apkSignatureSchemeBlock);
|
||||
} catch (ApkFormatException e) {
|
||||
result.addVerificationWarning(isV2Block ? ApkVerificationIssue.V2_SIG_MALFORMED_SIGNERS
|
||||
: ApkVerificationIssue.V3_SIG_MALFORMED_SIGNERS);
|
||||
return;
|
||||
}
|
||||
if (!signers.hasRemaining()) {
|
||||
result.addVerificationWarning(isV2Block ? ApkVerificationIssue.V2_SIG_NO_SIGNERS
|
||||
: ApkVerificationIssue.V3_SIG_NO_SIGNERS);
|
||||
return;
|
||||
}
|
||||
|
||||
CertificateFactory certFactory;
|
||||
try {
|
||||
certFactory = CertificateFactory.getInstance("X.509");
|
||||
} catch (CertificateException e) {
|
||||
throw new RuntimeException("Failed to obtain X.509 CertificateFactory", e);
|
||||
}
|
||||
while (signers.hasRemaining()) {
|
||||
Result.SignerInfo signerInfo = new Result.SignerInfo();
|
||||
if (isV2Block) {
|
||||
result.addV2Signer(signerInfo);
|
||||
} else {
|
||||
result.addV3Signer(signerInfo);
|
||||
}
|
||||
try {
|
||||
ByteBuffer signer = ApkSigningBlockUtilsLite.getLengthPrefixedSlice(signers);
|
||||
parseSigner(
|
||||
signer,
|
||||
apkSigSchemeVersion,
|
||||
certFactory,
|
||||
apkContentDigests,
|
||||
signerInfo);
|
||||
} catch (ApkFormatException | BufferUnderflowException e) {
|
||||
signerInfo.addVerificationWarning(
|
||||
isV2Block ? ApkVerificationIssue.V2_SIG_MALFORMED_SIGNER
|
||||
: ApkVerificationIssue.V3_SIG_MALFORMED_SIGNER);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the provided signer block and populates the {@code result}.
|
||||
*
|
||||
* <p>This verifies signatures over {@code signed-data} contained in this block but does not
|
||||
* verify the integrity of the rest of the APK. To facilitate APK integrity verification, this
|
||||
* method adds the {@code contentDigestsToVerify}. These digests can then be used to verify the
|
||||
* integrity of the APK.
|
||||
*
|
||||
* <p>This method adds one or more errors to the {@code result} if a verification error is
|
||||
* expected to be encountered on an Android platform version in the
|
||||
* {@code [minSdkVersion, maxSdkVersion]} range.
|
||||
*/
|
||||
private static void parseSigner(
|
||||
ByteBuffer signerBlock,
|
||||
int apkSigSchemeVersion,
|
||||
CertificateFactory certFactory,
|
||||
Map<ContentDigestAlgorithm, byte[]> apkContentDigests,
|
||||
Result.SignerInfo signerInfo)
|
||||
throws ApkFormatException {
|
||||
boolean isV2Signer = apkSigSchemeVersion == VERSION_APK_SIGNATURE_SCHEME_V2;
|
||||
// Both the V2 and V3 signer blocks contain the following:
|
||||
// * length-prefixed signed data
|
||||
// * length-prefixed sequence of length-prefixed digests:
|
||||
// * uint32: signature algorithm ID
|
||||
// * length-prefixed bytes: digest of contents
|
||||
// * length-prefixed sequence of certificates:
|
||||
// * length-prefixed bytes: X.509 certificate (ASN.1 DER encoded).
|
||||
ByteBuffer signedData = ApkSigningBlockUtilsLite.getLengthPrefixedSlice(signerBlock);
|
||||
ByteBuffer digests = ApkSigningBlockUtilsLite.getLengthPrefixedSlice(signedData);
|
||||
ByteBuffer certificates = ApkSigningBlockUtilsLite.getLengthPrefixedSlice(signedData);
|
||||
|
||||
// Parse the digests block
|
||||
while (digests.hasRemaining()) {
|
||||
try {
|
||||
ByteBuffer digest = ApkSigningBlockUtilsLite.getLengthPrefixedSlice(digests);
|
||||
int sigAlgorithmId = digest.getInt();
|
||||
byte[] digestBytes = ApkSigningBlockUtilsLite.readLengthPrefixedByteArray(digest);
|
||||
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.findById(sigAlgorithmId);
|
||||
if (signatureAlgorithm == null) {
|
||||
continue;
|
||||
}
|
||||
apkContentDigests.put(signatureAlgorithm.getContentDigestAlgorithm(), digestBytes);
|
||||
} catch (ApkFormatException | BufferUnderflowException e) {
|
||||
signerInfo.addVerificationWarning(
|
||||
isV2Signer ? ApkVerificationIssue.V2_SIG_MALFORMED_DIGEST
|
||||
: ApkVerificationIssue.V3_SIG_MALFORMED_DIGEST);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Parse the certificates block
|
||||
if (certificates.hasRemaining()) {
|
||||
byte[] encodedCert = ApkSigningBlockUtilsLite.readLengthPrefixedByteArray(certificates);
|
||||
X509Certificate certificate;
|
||||
try {
|
||||
certificate = (X509Certificate) certFactory.generateCertificate(
|
||||
new ByteArrayInputStream(encodedCert));
|
||||
} catch (CertificateException e) {
|
||||
signerInfo.addVerificationWarning(
|
||||
isV2Signer ? ApkVerificationIssue.V2_SIG_MALFORMED_CERTIFICATE
|
||||
: ApkVerificationIssue.V3_SIG_MALFORMED_CERTIFICATE);
|
||||
return;
|
||||
}
|
||||
// Wrap the cert so that the result's getEncoded returns exactly the original encoded
|
||||
// form. Without this, getEncoded may return a different form from what was stored in
|
||||
// the signature. This is because some X509Certificate(Factory) implementations
|
||||
// re-encode certificates.
|
||||
certificate = new GuaranteedEncodedFormX509Certificate(certificate, encodedCert);
|
||||
signerInfo.setSigningCertificate(certificate);
|
||||
}
|
||||
|
||||
if (signerInfo.getSigningCertificate() == null) {
|
||||
signerInfo.addVerificationWarning(
|
||||
isV2Signer ? ApkVerificationIssue.V2_SIG_NO_CERTIFICATES
|
||||
: ApkVerificationIssue.V3_SIG_NO_CERTIFICATES);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a mapping of the {@link ContentDigestAlgorithm} to the {@code byte[]} digest of the
|
||||
* V1 / jar signing META-INF/MANIFEST.MF; if this file is not found then an empty {@code Map} is
|
||||
* returned.
|
||||
*
|
||||
* <p>If any errors are encountered while parsing the V1 signers the provided {@code result}
|
||||
* will be updated to include a warning, but the source stamp verification can still proceed.
|
||||
*/
|
||||
private static Map<ContentDigestAlgorithm, byte[]> getApkContentDigestFromV1SigningScheme(
|
||||
List<CentralDirectoryRecord> cdRecords,
|
||||
DataSource apk,
|
||||
ZipSections zipSections,
|
||||
Result result)
|
||||
throws IOException, ApkFormatException {
|
||||
CentralDirectoryRecord manifestCdRecord = null;
|
||||
List<CentralDirectoryRecord> signatureBlockRecords = new ArrayList<>(1);
|
||||
Map<ContentDigestAlgorithm, byte[]> v1ContentDigest = new EnumMap<>(
|
||||
ContentDigestAlgorithm.class);
|
||||
for (CentralDirectoryRecord cdRecord : cdRecords) {
|
||||
String cdRecordName = cdRecord.getName();
|
||||
if (cdRecordName == null) {
|
||||
continue;
|
||||
}
|
||||
if (manifestCdRecord == null && MANIFEST_ENTRY_NAME.equals(cdRecordName)) {
|
||||
manifestCdRecord = cdRecord;
|
||||
continue;
|
||||
}
|
||||
if (cdRecordName.startsWith("META-INF/")
|
||||
&& (cdRecordName.endsWith(".RSA")
|
||||
|| cdRecordName.endsWith(".DSA")
|
||||
|| cdRecordName.endsWith(".EC"))) {
|
||||
signatureBlockRecords.add(cdRecord);
|
||||
}
|
||||
}
|
||||
if (manifestCdRecord == null) {
|
||||
// No JAR signing manifest file found. For SourceStamp verification, returning an empty
|
||||
// digest is enough since this would affect the final digest signed by the stamp, and
|
||||
// thus an empty digest will invalidate that signature.
|
||||
return v1ContentDigest;
|
||||
}
|
||||
if (signatureBlockRecords.isEmpty()) {
|
||||
result.addVerificationWarning(ApkVerificationIssue.JAR_SIG_NO_SIGNATURES);
|
||||
} else {
|
||||
for (CentralDirectoryRecord signatureBlockRecord : signatureBlockRecords) {
|
||||
try {
|
||||
CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
|
||||
byte[] signatureBlockBytes = LocalFileRecord.getUncompressedData(apk,
|
||||
signatureBlockRecord, zipSections.getZipCentralDirectoryOffset());
|
||||
for (Certificate certificate : certFactory.generateCertificates(
|
||||
new ByteArrayInputStream(signatureBlockBytes))) {
|
||||
// If multiple certificates are found within the signature block only the
|
||||
// first is used as the signer of this block.
|
||||
if (certificate instanceof X509Certificate) {
|
||||
Result.SignerInfo signerInfo = new Result.SignerInfo();
|
||||
signerInfo.setSigningCertificate((X509Certificate) certificate);
|
||||
result.addV1Signer(signerInfo);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (CertificateException e) {
|
||||
// Log a warning for the parsing exception but still proceed with the stamp
|
||||
// verification.
|
||||
result.addVerificationWarning(ApkVerificationIssue.JAR_SIG_PARSE_EXCEPTION,
|
||||
signatureBlockRecord.getName(), e);
|
||||
break;
|
||||
} catch (ZipFormatException e) {
|
||||
throw new ApkFormatException("Failed to read APK", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
try {
|
||||
byte[] manifestBytes =
|
||||
LocalFileRecord.getUncompressedData(
|
||||
apk, manifestCdRecord, zipSections.getZipCentralDirectoryOffset());
|
||||
v1ContentDigest.put(
|
||||
ContentDigestAlgorithm.SHA256, computeSha256DigestBytes(manifestBytes));
|
||||
return v1ContentDigest;
|
||||
} catch (ZipFormatException e) {
|
||||
throw new ApkFormatException("Failed to read APK", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of verifying the APK's source stamp signature; this signature can only be considered
|
||||
* verified if {@link #isVerified()} returns true.
|
||||
*/
|
||||
public static class Result {
|
||||
private final List<SignerInfo> mV1SchemeSigners = new ArrayList<>();
|
||||
private final List<SignerInfo> mV2SchemeSigners = new ArrayList<>();
|
||||
private final List<SignerInfo> mV3SchemeSigners = new ArrayList<>();
|
||||
private final List<List<SignerInfo>> mAllSchemeSigners = Arrays.asList(mV1SchemeSigners,
|
||||
mV2SchemeSigners, mV3SchemeSigners);
|
||||
private SourceStampInfo mSourceStampInfo;
|
||||
|
||||
private final List<ApkVerificationIssue> mErrors = new ArrayList<>();
|
||||
private final List<ApkVerificationIssue> mWarnings = new ArrayList<>();
|
||||
|
||||
private boolean mVerified;
|
||||
|
||||
void addVerificationError(int errorId, Object... params) {
|
||||
mErrors.add(new ApkVerificationIssue(errorId, params));
|
||||
}
|
||||
|
||||
void addVerificationWarning(int warningId, Object... params) {
|
||||
mWarnings.add(new ApkVerificationIssue(warningId, params));
|
||||
}
|
||||
|
||||
private void addV1Signer(SignerInfo signerInfo) {
|
||||
mV1SchemeSigners.add(signerInfo);
|
||||
}
|
||||
|
||||
private void addV2Signer(SignerInfo signerInfo) {
|
||||
mV2SchemeSigners.add(signerInfo);
|
||||
}
|
||||
|
||||
private void addV3Signer(SignerInfo signerInfo) {
|
||||
mV3SchemeSigners.add(signerInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if the APK's source stamp signature
|
||||
*/
|
||||
public boolean isVerified() {
|
||||
return mVerified;
|
||||
}
|
||||
|
||||
private void mergeFrom(ApkSigResult source) {
|
||||
switch (source.signatureSchemeVersion) {
|
||||
case Constants.VERSION_SOURCE_STAMP:
|
||||
mVerified = source.verified;
|
||||
if (!source.mSigners.isEmpty()) {
|
||||
mSourceStampInfo = new SourceStampInfo(source.mSigners.get(0));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException(
|
||||
"Unknown ApkSigResult Signing Block Scheme Id "
|
||||
+ source.signatureSchemeVersion);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@code List} of {@link SignerInfo} objects representing the V1 signers of the
|
||||
* provided APK.
|
||||
*/
|
||||
public List<SignerInfo> getV1SchemeSigners() {
|
||||
return mV1SchemeSigners;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@code List} of {@link SignerInfo} objects representing the V2 signers of the
|
||||
* provided APK.
|
||||
*/
|
||||
public List<SignerInfo> getV2SchemeSigners() {
|
||||
return mV2SchemeSigners;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@code List} of {@link SignerInfo} objects representing the V3 signers of the
|
||||
* provided APK.
|
||||
*/
|
||||
public List<SignerInfo> getV3SchemeSigners() {
|
||||
return mV3SchemeSigners;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link SourceStampInfo} instance representing the source stamp signer for the
|
||||
* APK, or null if the source stamp signature verification failed before the stamp signature
|
||||
* block could be fully parsed.
|
||||
*/
|
||||
public SourceStampInfo getSourceStampInfo() {
|
||||
return mSourceStampInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if an error was encountered while verifying the APK.
|
||||
*
|
||||
* <p>Any error prevents the APK from being considered verified.
|
||||
*/
|
||||
public boolean containsErrors() {
|
||||
if (!mErrors.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
for (List<SignerInfo> signers : mAllSchemeSigners) {
|
||||
for (SignerInfo signer : signers) {
|
||||
if (signer.containsErrors()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (mSourceStampInfo != null) {
|
||||
if (mSourceStampInfo.containsErrors()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the errors encountered while verifying the APK's source stamp.
|
||||
*/
|
||||
public List<ApkVerificationIssue> getErrors() {
|
||||
return mErrors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the warnings encountered while verifying the APK's source stamp.
|
||||
*/
|
||||
public List<ApkVerificationIssue> getWarnings() {
|
||||
return mWarnings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all errors for this result, including any errors from signature scheme signers
|
||||
* and the source stamp.
|
||||
*/
|
||||
public List<ApkVerificationIssue> getAllErrors() {
|
||||
List<ApkVerificationIssue> errors = new ArrayList<>();
|
||||
errors.addAll(mErrors);
|
||||
|
||||
for (List<SignerInfo> signers : mAllSchemeSigners) {
|
||||
for (SignerInfo signer : signers) {
|
||||
errors.addAll(signer.getErrors());
|
||||
}
|
||||
}
|
||||
if (mSourceStampInfo != null) {
|
||||
errors.addAll(mSourceStampInfo.getErrors());
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all warnings for this result, including any warnings from signature scheme
|
||||
* signers and the source stamp.
|
||||
*/
|
||||
public List<ApkVerificationIssue> getAllWarnings() {
|
||||
List<ApkVerificationIssue> warnings = new ArrayList<>();
|
||||
warnings.addAll(mWarnings);
|
||||
|
||||
for (List<SignerInfo> signers : mAllSchemeSigners) {
|
||||
for (SignerInfo signer : signers) {
|
||||
warnings.addAll(signer.getWarnings());
|
||||
}
|
||||
}
|
||||
if (mSourceStampInfo != null) {
|
||||
warnings.addAll(mSourceStampInfo.getWarnings());
|
||||
}
|
||||
return warnings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Contains information about an APK's signer and any errors encountered while parsing the
|
||||
* corresponding signature block.
|
||||
*/
|
||||
public static class SignerInfo {
|
||||
private X509Certificate mSigningCertificate;
|
||||
private final List<ApkVerificationIssue> mErrors = new ArrayList<>();
|
||||
private final List<ApkVerificationIssue> mWarnings = new ArrayList<>();
|
||||
|
||||
void setSigningCertificate(X509Certificate signingCertificate) {
|
||||
mSigningCertificate = signingCertificate;
|
||||
}
|
||||
|
||||
void addVerificationError(int errorId, Object... params) {
|
||||
mErrors.add(new ApkVerificationIssue(errorId, params));
|
||||
}
|
||||
|
||||
void addVerificationWarning(int warningId, Object... params) {
|
||||
mWarnings.add(new ApkVerificationIssue(warningId, params));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current signing certificate used by this signer.
|
||||
*/
|
||||
public X509Certificate getSigningCertificate() {
|
||||
return mSigningCertificate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link List} of {@link ApkVerificationIssue} objects representing errors
|
||||
* encountered during processing of this signer's signature block.
|
||||
*/
|
||||
public List<ApkVerificationIssue> getErrors() {
|
||||
return mErrors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link List} of {@link ApkVerificationIssue} objects representing warnings
|
||||
* encountered during processing of this signer's signature block.
|
||||
*/
|
||||
public List<ApkVerificationIssue> getWarnings() {
|
||||
return mWarnings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if any errors were encountered while parsing this signer's
|
||||
* signature block.
|
||||
*/
|
||||
public boolean containsErrors() {
|
||||
return !mErrors.isEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Contains information about an APK's source stamp and any errors encountered while
|
||||
* parsing the stamp signature block.
|
||||
*/
|
||||
public static class SourceStampInfo {
|
||||
private final List<X509Certificate> mCertificates;
|
||||
private final List<X509Certificate> mCertificateLineage;
|
||||
|
||||
private final List<ApkVerificationIssue> mErrors = new ArrayList<>();
|
||||
private final List<ApkVerificationIssue> mWarnings = new ArrayList<>();
|
||||
private final List<ApkVerificationIssue> mInfoMessages = new ArrayList<>();
|
||||
|
||||
private final long mTimestamp;
|
||||
|
||||
/*
|
||||
* Since this utility is intended just to verify the source stamp, and the source stamp
|
||||
* currently only logs warnings to prevent failing the APK signature verification, treat
|
||||
* all warnings as errors. If the stamp verification is updated to log errors this
|
||||
* should be set to false to ensure only errors trigger a failure verifying the source
|
||||
* stamp.
|
||||
*/
|
||||
private static final boolean mWarningsAsErrors = true;
|
||||
|
||||
private SourceStampInfo(ApkSignerInfo result) {
|
||||
mCertificates = result.certs;
|
||||
mCertificateLineage = result.certificateLineage;
|
||||
mErrors.addAll(result.getErrors());
|
||||
mWarnings.addAll(result.getWarnings());
|
||||
mInfoMessages.addAll(result.getInfoMessages());
|
||||
mTimestamp = result.timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the SourceStamp's signing certificate or {@code null} if not available. The
|
||||
* certificate is guaranteed to be available if no errors were encountered during
|
||||
* verification (see {@link #containsErrors()}.
|
||||
*
|
||||
* <p>This certificate contains the SourceStamp's public key.
|
||||
*/
|
||||
public X509Certificate getCertificate() {
|
||||
return mCertificates.isEmpty() ? null : mCertificates.get(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@code List} of {@link X509Certificate} instances representing the source
|
||||
* stamp signer's lineage with the oldest signer at element 0, or an empty {@code List}
|
||||
* if the stamp's signing certificate has not been rotated.
|
||||
*/
|
||||
public List<X509Certificate> getCertificatesInLineage() {
|
||||
return mCertificateLineage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether any errors were encountered during the source stamp verification.
|
||||
*/
|
||||
public boolean containsErrors() {
|
||||
return !mErrors.isEmpty() || (mWarningsAsErrors && !mWarnings.isEmpty());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if any info messages were encountered during verification of
|
||||
* this source stamp.
|
||||
*/
|
||||
public boolean containsInfoMessages() {
|
||||
return !mInfoMessages.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@code List} of {@link ApkVerificationIssue} representing errors that were
|
||||
* encountered during source stamp verification.
|
||||
*/
|
||||
public List<ApkVerificationIssue> getErrors() {
|
||||
if (!mWarningsAsErrors) {
|
||||
return mErrors;
|
||||
}
|
||||
List<ApkVerificationIssue> result = new ArrayList<>();
|
||||
result.addAll(mErrors);
|
||||
result.addAll(mWarnings);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@code List} of {@link ApkVerificationIssue} representing warnings that
|
||||
* were encountered during source stamp verification.
|
||||
*/
|
||||
public List<ApkVerificationIssue> getWarnings() {
|
||||
return mWarnings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@code List} of {@link ApkVerificationIssue} representing info messages
|
||||
* that were encountered during source stamp verification.
|
||||
*/
|
||||
public List<ApkVerificationIssue> getInfoMessages() {
|
||||
return mInfoMessages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the epoch timestamp in seconds representing the time this source stamp block
|
||||
* was signed, or 0 if the timestamp is not available.
|
||||
*/
|
||||
public long getTimestampEpochSeconds() {
|
||||
return mTimestamp;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder of {@link SourceStampVerifier} instances.
|
||||
*
|
||||
* <p> The resulting verifier, by default, checks whether the APK's source stamp signature will
|
||||
* verify on all platform versions. The APK's {@code android:minSdkVersion} attribute is not
|
||||
* queried to determine the APK's minimum supported level, so the caller should specify a lower
|
||||
* bound with {@link #setMinCheckedPlatformVersion(int)}.
|
||||
*/
|
||||
public static class Builder {
|
||||
private final File mApkFile;
|
||||
private final DataSource mApkDataSource;
|
||||
|
||||
private int mMinSdkVersion = 1;
|
||||
private int mMaxSdkVersion = Integer.MAX_VALUE;
|
||||
|
||||
/**
|
||||
* Constructs a new {@code Builder} for source stamp verification of the provided {@code
|
||||
* apk}.
|
||||
*/
|
||||
public Builder(File apk) {
|
||||
if (apk == null) {
|
||||
throw new NullPointerException("apk == null");
|
||||
}
|
||||
mApkFile = apk;
|
||||
mApkDataSource = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new {@code Builder} for source stamp verification of the provided {@code
|
||||
* apk}.
|
||||
*/
|
||||
public Builder(DataSource apk) {
|
||||
if (apk == null) {
|
||||
throw new NullPointerException("apk == null");
|
||||
}
|
||||
mApkDataSource = apk;
|
||||
mApkFile = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the oldest Android platform version for which the APK's source stamp is verified.
|
||||
*
|
||||
* <p>APK source stamp verification will confirm that the APK's stamp is expected to verify
|
||||
* on all Android platforms starting from the platform version with the provided {@code
|
||||
* minSdkVersion}. The upper end of the platform versions range can be modified via
|
||||
* {@link #setMaxCheckedPlatformVersion(int)}.
|
||||
*
|
||||
* @param minSdkVersion API Level of the oldest platform for which to verify the APK
|
||||
*/
|
||||
public SourceStampVerifier.Builder setMinCheckedPlatformVersion(int minSdkVersion) {
|
||||
mMinSdkVersion = minSdkVersion;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the newest Android platform version for which the APK's source stamp is verified.
|
||||
*
|
||||
* <p>APK source stamp verification will confirm that the APK's stamp is expected to verify
|
||||
* on all platform versions up to and including the proviced {@code maxSdkVersion}. The
|
||||
* lower end of the platform versions range can be modified via {@link
|
||||
* #setMinCheckedPlatformVersion(int)}.
|
||||
*
|
||||
* @param maxSdkVersion API Level of the newest platform for which to verify the APK
|
||||
* @see #setMinCheckedPlatformVersion(int)
|
||||
*/
|
||||
public SourceStampVerifier.Builder setMaxCheckedPlatformVersion(int maxSdkVersion) {
|
||||
mMaxSdkVersion = maxSdkVersion;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link SourceStampVerifier} initialized according to the configuration of this
|
||||
* builder.
|
||||
*/
|
||||
public SourceStampVerifier build() {
|
||||
return new SourceStampVerifier(
|
||||
mApkFile,
|
||||
mApkDataSource,
|
||||
mMinSdkVersion,
|
||||
mMaxSdkVersion);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.apk;
|
||||
|
||||
/**
|
||||
* Indicates that an APK is not well-formed. For example, this may indicate that the APK is not a
|
||||
* well-formed ZIP archive, in which case {@link #getCause()} will return a
|
||||
* {@link com.android.apksig.zip.ZipFormatException ZipFormatException}, or that the APK contains
|
||||
* multiple ZIP entries with the same name.
|
||||
*/
|
||||
public class ApkFormatException extends Exception {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public ApkFormatException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public ApkFormatException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.apk;
|
||||
|
||||
/**
|
||||
* Indicates that no APK Signing Block was found in an APK.
|
||||
*/
|
||||
public class ApkSigningBlockNotFoundException extends Exception {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public ApkSigningBlockNotFoundException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public ApkSigningBlockNotFoundException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,670 @@
|
||||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.apk;
|
||||
|
||||
import com.android.apksig.internal.apk.AndroidBinXmlParser;
|
||||
import com.android.apksig.internal.apk.stamp.SourceStampConstants;
|
||||
import com.android.apksig.internal.apk.v1.V1SchemeVerifier;
|
||||
import com.android.apksig.internal.util.Pair;
|
||||
import com.android.apksig.internal.zip.CentralDirectoryRecord;
|
||||
import com.android.apksig.internal.zip.LocalFileRecord;
|
||||
import com.android.apksig.internal.zip.ZipUtils;
|
||||
import com.android.apksig.util.DataSource;
|
||||
import com.android.apksig.zip.ZipFormatException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* APK utilities.
|
||||
*/
|
||||
public abstract class ApkUtils {
|
||||
|
||||
/**
|
||||
* Name of the Android manifest ZIP entry in APKs.
|
||||
*/
|
||||
public static final String ANDROID_MANIFEST_ZIP_ENTRY_NAME = "AndroidManifest.xml";
|
||||
|
||||
/** Name of the SourceStamp certificate hash ZIP entry in APKs. */
|
||||
public static final String SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME =
|
||||
SourceStampConstants.SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME;
|
||||
|
||||
private ApkUtils() {}
|
||||
|
||||
/**
|
||||
* Finds the main ZIP sections of the provided APK.
|
||||
*
|
||||
* @throws IOException if an I/O error occurred while reading the APK
|
||||
* @throws ZipFormatException if the APK is malformed
|
||||
*/
|
||||
public static ZipSections findZipSections(DataSource apk)
|
||||
throws IOException, ZipFormatException {
|
||||
com.android.apksig.zip.ZipSections zipSections = ApkUtilsLite.findZipSections(apk);
|
||||
return new ZipSections(
|
||||
zipSections.getZipCentralDirectoryOffset(),
|
||||
zipSections.getZipCentralDirectorySizeBytes(),
|
||||
zipSections.getZipCentralDirectoryRecordCount(),
|
||||
zipSections.getZipEndOfCentralDirectoryOffset(),
|
||||
zipSections.getZipEndOfCentralDirectory());
|
||||
}
|
||||
|
||||
/**
|
||||
* Information about the ZIP sections of an APK.
|
||||
*/
|
||||
public static class ZipSections extends com.android.apksig.zip.ZipSections {
|
||||
public ZipSections(
|
||||
long centralDirectoryOffset,
|
||||
long centralDirectorySizeBytes,
|
||||
int centralDirectoryRecordCount,
|
||||
long eocdOffset,
|
||||
ByteBuffer eocd) {
|
||||
super(centralDirectoryOffset, centralDirectorySizeBytes, centralDirectoryRecordCount,
|
||||
eocdOffset, eocd);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the offset of the start of the ZIP Central Directory in the APK's ZIP End of Central
|
||||
* Directory record.
|
||||
*
|
||||
* @param zipEndOfCentralDirectory APK's ZIP End of Central Directory record
|
||||
* @param offset offset of the ZIP Central Directory relative to the start of the archive. Must
|
||||
* be between {@code 0} and {@code 2^32 - 1} inclusive.
|
||||
*/
|
||||
public static void setZipEocdCentralDirectoryOffset(
|
||||
ByteBuffer zipEndOfCentralDirectory, long offset) {
|
||||
ByteBuffer eocd = zipEndOfCentralDirectory.slice();
|
||||
eocd.order(ByteOrder.LITTLE_ENDIAN);
|
||||
ZipUtils.setZipEocdCentralDirectoryOffset(eocd, offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the length of EOCD comment.
|
||||
*
|
||||
* @param zipEndOfCentralDirectory APK's ZIP End of Central Directory record
|
||||
*/
|
||||
public static void updateZipEocdCommentLen(ByteBuffer zipEndOfCentralDirectory) {
|
||||
ByteBuffer eocd = zipEndOfCentralDirectory.slice();
|
||||
eocd.order(ByteOrder.LITTLE_ENDIAN);
|
||||
ZipUtils.updateZipEocdCommentLen(eocd);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the APK Signing Block of the provided {@code apk}.
|
||||
*
|
||||
* @throws ApkFormatException if the APK is not a valid ZIP archive
|
||||
* @throws IOException if an I/O error occurs
|
||||
* @throws ApkSigningBlockNotFoundException if there is no APK Signing Block in the APK
|
||||
*
|
||||
* @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature Scheme v2
|
||||
* </a>
|
||||
*/
|
||||
public static ApkSigningBlock findApkSigningBlock(DataSource apk)
|
||||
throws ApkFormatException, IOException, ApkSigningBlockNotFoundException {
|
||||
ApkUtils.ZipSections inputZipSections;
|
||||
try {
|
||||
inputZipSections = ApkUtils.findZipSections(apk);
|
||||
} catch (ZipFormatException e) {
|
||||
throw new ApkFormatException("Malformed APK: not a ZIP archive", e);
|
||||
}
|
||||
return findApkSigningBlock(apk, inputZipSections);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the APK Signing Block of the provided APK.
|
||||
*
|
||||
* @throws IOException if an I/O error occurs
|
||||
* @throws ApkSigningBlockNotFoundException if there is no APK Signing Block in the APK
|
||||
*
|
||||
* @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature Scheme v2
|
||||
* </a>
|
||||
*/
|
||||
public static ApkSigningBlock findApkSigningBlock(DataSource apk, ZipSections zipSections)
|
||||
throws IOException, ApkSigningBlockNotFoundException {
|
||||
ApkUtilsLite.ApkSigningBlock apkSigningBlock = ApkUtilsLite.findApkSigningBlock(apk,
|
||||
zipSections);
|
||||
return new ApkSigningBlock(apkSigningBlock.getStartOffset(), apkSigningBlock.getContents());
|
||||
}
|
||||
|
||||
/**
|
||||
* Information about the location of the APK Signing Block inside an APK.
|
||||
*/
|
||||
public static class ApkSigningBlock extends ApkUtilsLite.ApkSigningBlock {
|
||||
/**
|
||||
* Constructs a new {@code ApkSigningBlock}.
|
||||
*
|
||||
* @param startOffsetInApk start offset (in bytes, relative to start of file) of the APK
|
||||
* Signing Block inside the APK file
|
||||
* @param contents contents of the APK Signing Block
|
||||
*/
|
||||
public ApkSigningBlock(long startOffsetInApk, DataSource contents) {
|
||||
super(startOffsetInApk, contents);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the contents of the APK's {@code AndroidManifest.xml}.
|
||||
*
|
||||
* @throws IOException if an I/O error occurs while reading the APK
|
||||
* @throws ApkFormatException if the APK is malformed
|
||||
*/
|
||||
public static ByteBuffer getAndroidManifest(DataSource apk)
|
||||
throws IOException, ApkFormatException {
|
||||
ZipSections zipSections;
|
||||
try {
|
||||
zipSections = findZipSections(apk);
|
||||
} catch (ZipFormatException e) {
|
||||
throw new ApkFormatException("Not a valid ZIP archive", e);
|
||||
}
|
||||
List<CentralDirectoryRecord> cdRecords =
|
||||
V1SchemeVerifier.parseZipCentralDirectory(apk, zipSections);
|
||||
CentralDirectoryRecord androidManifestCdRecord = null;
|
||||
for (CentralDirectoryRecord cdRecord : cdRecords) {
|
||||
if (ANDROID_MANIFEST_ZIP_ENTRY_NAME.equals(cdRecord.getName())) {
|
||||
androidManifestCdRecord = cdRecord;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (androidManifestCdRecord == null) {
|
||||
throw new ApkFormatException("Missing " + ANDROID_MANIFEST_ZIP_ENTRY_NAME);
|
||||
}
|
||||
DataSource lfhSection = apk.slice(0, zipSections.getZipCentralDirectoryOffset());
|
||||
|
||||
try {
|
||||
return ByteBuffer.wrap(
|
||||
LocalFileRecord.getUncompressedData(
|
||||
lfhSection, androidManifestCdRecord, lfhSection.size()));
|
||||
} catch (ZipFormatException e) {
|
||||
throw new ApkFormatException("Failed to read " + ANDROID_MANIFEST_ZIP_ENTRY_NAME, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Android resource ID of the {@code android:minSdkVersion} attribute in AndroidManifest.xml.
|
||||
*/
|
||||
private static final int MIN_SDK_VERSION_ATTR_ID = 0x0101020c;
|
||||
|
||||
/**
|
||||
* Android resource ID of the {@code android:debuggable} attribute in AndroidManifest.xml.
|
||||
*/
|
||||
private static final int DEBUGGABLE_ATTR_ID = 0x0101000f;
|
||||
|
||||
/**
|
||||
* Android resource ID of the {@code android:targetSandboxVersion} attribute in
|
||||
* AndroidManifest.xml.
|
||||
*/
|
||||
private static final int TARGET_SANDBOX_VERSION_ATTR_ID = 0x0101054c;
|
||||
|
||||
/**
|
||||
* Android resource ID of the {@code android:targetSdkVersion} attribute in
|
||||
* AndroidManifest.xml.
|
||||
*/
|
||||
private static final int TARGET_SDK_VERSION_ATTR_ID = 0x01010270;
|
||||
private static final String USES_SDK_ELEMENT_TAG = "uses-sdk";
|
||||
|
||||
/**
|
||||
* Android resource ID of the {@code android:versionCode} attribute in AndroidManifest.xml.
|
||||
*/
|
||||
private static final int VERSION_CODE_ATTR_ID = 0x0101021b;
|
||||
private static final String MANIFEST_ELEMENT_TAG = "manifest";
|
||||
|
||||
/**
|
||||
* Android resource ID of the {@code android:versionCodeMajor} attribute in AndroidManifest.xml.
|
||||
*/
|
||||
private static final int VERSION_CODE_MAJOR_ATTR_ID = 0x01010576;
|
||||
|
||||
/**
|
||||
* Returns the lowest Android platform version (API Level) supported by an APK with the
|
||||
* provided {@code AndroidManifest.xml}.
|
||||
*
|
||||
* @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android
|
||||
* resource format
|
||||
*
|
||||
* @throws MinSdkVersionException if an error occurred while determining the API Level
|
||||
*/
|
||||
public static int getMinSdkVersionFromBinaryAndroidManifest(
|
||||
ByteBuffer androidManifestContents) throws MinSdkVersionException {
|
||||
// IMPLEMENTATION NOTE: Minimum supported Android platform version number is declared using
|
||||
// uses-sdk elements which are children of the top-level manifest element. uses-sdk element
|
||||
// declares the minimum supported platform version using the android:minSdkVersion attribute
|
||||
// whose default value is 1.
|
||||
// For each encountered uses-sdk element, the Android runtime checks that its minSdkVersion
|
||||
// is not higher than the runtime's API Level and rejects APKs if it is higher. Thus, the
|
||||
// effective minSdkVersion value is the maximum over the encountered minSdkVersion values.
|
||||
|
||||
try {
|
||||
// If no uses-sdk elements are encountered, Android accepts the APK. We treat this
|
||||
// scenario as though the minimum supported API Level is 1.
|
||||
int result = 1;
|
||||
|
||||
AndroidBinXmlParser parser = new AndroidBinXmlParser(androidManifestContents);
|
||||
int eventType = parser.getEventType();
|
||||
while (eventType != AndroidBinXmlParser.EVENT_END_DOCUMENT) {
|
||||
if ((eventType == AndroidBinXmlParser.EVENT_START_ELEMENT)
|
||||
&& (parser.getDepth() == 2)
|
||||
&& ("uses-sdk".equals(parser.getName()))
|
||||
&& (parser.getNamespace().isEmpty())) {
|
||||
// In each uses-sdk element, minSdkVersion defaults to 1
|
||||
int minSdkVersion = 1;
|
||||
for (int i = 0; i < parser.getAttributeCount(); i++) {
|
||||
if (parser.getAttributeNameResourceId(i) == MIN_SDK_VERSION_ATTR_ID) {
|
||||
int valueType = parser.getAttributeValueType(i);
|
||||
switch (valueType) {
|
||||
case AndroidBinXmlParser.VALUE_TYPE_INT:
|
||||
minSdkVersion = parser.getAttributeIntValue(i);
|
||||
break;
|
||||
case AndroidBinXmlParser.VALUE_TYPE_STRING:
|
||||
minSdkVersion =
|
||||
getMinSdkVersionForCodename(
|
||||
parser.getAttributeStringValue(i));
|
||||
break;
|
||||
default:
|
||||
throw new MinSdkVersionException(
|
||||
"Unable to determine APK's minimum supported Android"
|
||||
+ ": unsupported value type in "
|
||||
+ ANDROID_MANIFEST_ZIP_ENTRY_NAME + "'s"
|
||||
+ " minSdkVersion"
|
||||
+ ". Only integer values supported.");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
result = Math.max(result, minSdkVersion);
|
||||
}
|
||||
eventType = parser.next();
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (AndroidBinXmlParser.XmlParserException e) {
|
||||
throw new MinSdkVersionException(
|
||||
"Unable to determine APK's minimum supported Android platform version"
|
||||
+ ": malformed binary resource: " + ANDROID_MANIFEST_ZIP_ENTRY_NAME,
|
||||
e);
|
||||
}
|
||||
}
|
||||
|
||||
private static class CodenamesLazyInitializer {
|
||||
|
||||
/**
|
||||
* List of platform codename (first letter of) to API Level mappings. The list must be
|
||||
* sorted by the first letter. For codenames not in the list, the assumption is that the API
|
||||
* Level is incremented by one for every increase in the codename's first letter.
|
||||
*/
|
||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||
private static final Pair<Character, Integer>[] SORTED_CODENAMES_FIRST_CHAR_TO_API_LEVEL =
|
||||
new Pair[] {
|
||||
Pair.of('C', 2),
|
||||
Pair.of('D', 3),
|
||||
Pair.of('E', 4),
|
||||
Pair.of('F', 7),
|
||||
Pair.of('G', 8),
|
||||
Pair.of('H', 10),
|
||||
Pair.of('I', 13),
|
||||
Pair.of('J', 15),
|
||||
Pair.of('K', 18),
|
||||
Pair.of('L', 20),
|
||||
Pair.of('M', 22),
|
||||
Pair.of('N', 23),
|
||||
Pair.of('O', 25),
|
||||
};
|
||||
|
||||
private static final Comparator<Pair<Character, Integer>> CODENAME_FIRST_CHAR_COMPARATOR =
|
||||
new ByFirstComparator();
|
||||
|
||||
private static class ByFirstComparator implements Comparator<Pair<Character, Integer>> {
|
||||
@Override
|
||||
public int compare(Pair<Character, Integer> o1, Pair<Character, Integer> o2) {
|
||||
char c1 = o1.getFirst();
|
||||
char c2 = o2.getFirst();
|
||||
return c1 - c2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the API Level corresponding to the provided platform codename.
|
||||
*
|
||||
* <p>This method is pessimistic. It returns a value one lower than the API Level with which the
|
||||
* platform is actually released (e.g., 23 for N which was released as API Level 24). This is
|
||||
* because new features which first appear in an API Level are not available in the early days
|
||||
* of that platform version's existence, when the platform only has a codename. Moreover, this
|
||||
* method currently doesn't differentiate between initial and MR releases, meaning API Level
|
||||
* returned for MR releases may be more than one lower than the API Level with which the
|
||||
* platform version is actually released.
|
||||
*
|
||||
* @throws CodenameMinSdkVersionException if the {@code codename} is not supported
|
||||
*/
|
||||
static int getMinSdkVersionForCodename(String codename) throws CodenameMinSdkVersionException {
|
||||
char firstChar = codename.isEmpty() ? ' ' : codename.charAt(0);
|
||||
// Codenames are case-sensitive. Only codenames starting with A-Z are supported for now.
|
||||
// We only look at the first letter of the codename as this is the most important letter.
|
||||
if ((firstChar >= 'A') && (firstChar <= 'Z')) {
|
||||
Pair<Character, Integer>[] sortedCodenamesFirstCharToApiLevel =
|
||||
CodenamesLazyInitializer.SORTED_CODENAMES_FIRST_CHAR_TO_API_LEVEL;
|
||||
int searchResult =
|
||||
Arrays.binarySearch(
|
||||
sortedCodenamesFirstCharToApiLevel,
|
||||
Pair.of(firstChar, null), // second element of the pair is ignored here
|
||||
CodenamesLazyInitializer.CODENAME_FIRST_CHAR_COMPARATOR);
|
||||
if (searchResult >= 0) {
|
||||
// Exact match -- searchResult is the index of the matching element
|
||||
return sortedCodenamesFirstCharToApiLevel[searchResult].getSecond();
|
||||
}
|
||||
// Not an exact match -- searchResult is negative and is -(insertion index) - 1.
|
||||
// The element at insertionIndex - 1 (if present) is smaller than firstChar and the
|
||||
// element at insertionIndex (if present) is greater than firstChar.
|
||||
int insertionIndex = -1 - searchResult; // insertionIndex is in [0; array length]
|
||||
if (insertionIndex == 0) {
|
||||
// 'A' or 'B' -- never released to public
|
||||
return 1;
|
||||
} else {
|
||||
// The element at insertionIndex - 1 is the newest older codename.
|
||||
// API Level bumped by at least 1 for every change in the first letter of codename
|
||||
Pair<Character, Integer> newestOlderCodenameMapping =
|
||||
sortedCodenamesFirstCharToApiLevel[insertionIndex - 1];
|
||||
char newestOlderCodenameFirstChar = newestOlderCodenameMapping.getFirst();
|
||||
int newestOlderCodenameApiLevel = newestOlderCodenameMapping.getSecond();
|
||||
return newestOlderCodenameApiLevel + (firstChar - newestOlderCodenameFirstChar);
|
||||
}
|
||||
}
|
||||
|
||||
throw new CodenameMinSdkVersionException(
|
||||
"Unable to determine APK's minimum supported Android platform version"
|
||||
+ " : Unsupported codename in " + ANDROID_MANIFEST_ZIP_ENTRY_NAME
|
||||
+ "'s minSdkVersion: \"" + codename + "\"",
|
||||
codename);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if the APK is debuggable according to its {@code AndroidManifest.xml}.
|
||||
* See the {@code android:debuggable} attribute of the {@code application} element.
|
||||
*
|
||||
* @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android
|
||||
* resource format
|
||||
*
|
||||
* @throws ApkFormatException if the manifest is malformed
|
||||
*/
|
||||
public static boolean getDebuggableFromBinaryAndroidManifest(
|
||||
ByteBuffer androidManifestContents) throws ApkFormatException {
|
||||
// IMPLEMENTATION NOTE: Whether the package is debuggable is declared using the first
|
||||
// "application" element which is a child of the top-level manifest element. The debuggable
|
||||
// attribute of this application element is coerced to a boolean value. If there is no
|
||||
// application element or if it doesn't declare the debuggable attribute, the package is
|
||||
// considered not debuggable.
|
||||
|
||||
try {
|
||||
AndroidBinXmlParser parser = new AndroidBinXmlParser(androidManifestContents);
|
||||
int eventType = parser.getEventType();
|
||||
while (eventType != AndroidBinXmlParser.EVENT_END_DOCUMENT) {
|
||||
if ((eventType == AndroidBinXmlParser.EVENT_START_ELEMENT)
|
||||
&& (parser.getDepth() == 2)
|
||||
&& ("application".equals(parser.getName()))
|
||||
&& (parser.getNamespace().isEmpty())) {
|
||||
for (int i = 0; i < parser.getAttributeCount(); i++) {
|
||||
if (parser.getAttributeNameResourceId(i) == DEBUGGABLE_ATTR_ID) {
|
||||
int valueType = parser.getAttributeValueType(i);
|
||||
switch (valueType) {
|
||||
case AndroidBinXmlParser.VALUE_TYPE_BOOLEAN:
|
||||
case AndroidBinXmlParser.VALUE_TYPE_STRING:
|
||||
case AndroidBinXmlParser.VALUE_TYPE_INT:
|
||||
String value = parser.getAttributeStringValue(i);
|
||||
return ("true".equals(value))
|
||||
|| ("TRUE".equals(value))
|
||||
|| ("1".equals(value));
|
||||
case AndroidBinXmlParser.VALUE_TYPE_REFERENCE:
|
||||
// References to resources are not supported on purpose. The
|
||||
// reason is that the resolved value depends on the resource
|
||||
// configuration (e.g, MNC/MCC, locale, screen density) used
|
||||
// at resolution time. As a result, the same APK may appear as
|
||||
// debuggable in one situation and as non-debuggable in another
|
||||
// situation. Such APKs may put users at risk.
|
||||
throw new ApkFormatException(
|
||||
"Unable to determine whether APK is debuggable"
|
||||
+ ": " + ANDROID_MANIFEST_ZIP_ENTRY_NAME + "'s"
|
||||
+ " android:debuggable attribute references a"
|
||||
+ " resource. References are not supported for"
|
||||
+ " security reasons. Only constant boolean,"
|
||||
+ " string and int values are supported.");
|
||||
default:
|
||||
throw new ApkFormatException(
|
||||
"Unable to determine whether APK is debuggable"
|
||||
+ ": " + ANDROID_MANIFEST_ZIP_ENTRY_NAME + "'s"
|
||||
+ " android:debuggable attribute uses"
|
||||
+ " unsupported value type. Only boolean,"
|
||||
+ " string and int values are supported.");
|
||||
}
|
||||
}
|
||||
}
|
||||
// This application element does not declare the debuggable attribute
|
||||
return false;
|
||||
}
|
||||
eventType = parser.next();
|
||||
}
|
||||
|
||||
// No application element found
|
||||
return false;
|
||||
} catch (AndroidBinXmlParser.XmlParserException e) {
|
||||
throw new ApkFormatException(
|
||||
"Unable to determine whether APK is debuggable: malformed binary resource: "
|
||||
+ ANDROID_MANIFEST_ZIP_ENTRY_NAME,
|
||||
e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the package name of the APK according to its {@code AndroidManifest.xml} or
|
||||
* {@code null} if package name is not declared. See the {@code package} attribute of the
|
||||
* {@code manifest} element.
|
||||
*
|
||||
* @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android
|
||||
* resource format
|
||||
*
|
||||
* @throws ApkFormatException if the manifest is malformed
|
||||
*/
|
||||
public static String getPackageNameFromBinaryAndroidManifest(
|
||||
ByteBuffer androidManifestContents) throws ApkFormatException {
|
||||
// IMPLEMENTATION NOTE: Package name is declared as the "package" attribute of the top-level
|
||||
// manifest element. Interestingly, as opposed to most other attributes, Android Package
|
||||
// Manager looks up this attribute by its name rather than by its resource ID.
|
||||
|
||||
try {
|
||||
AndroidBinXmlParser parser = new AndroidBinXmlParser(androidManifestContents);
|
||||
int eventType = parser.getEventType();
|
||||
while (eventType != AndroidBinXmlParser.EVENT_END_DOCUMENT) {
|
||||
if ((eventType == AndroidBinXmlParser.EVENT_START_ELEMENT)
|
||||
&& (parser.getDepth() == 1)
|
||||
&& ("manifest".equals(parser.getName()))
|
||||
&& (parser.getNamespace().isEmpty())) {
|
||||
for (int i = 0; i < parser.getAttributeCount(); i++) {
|
||||
if ("package".equals(parser.getAttributeName(i))
|
||||
&& (parser.getNamespace().isEmpty())) {
|
||||
return parser.getAttributeStringValue(i);
|
||||
}
|
||||
}
|
||||
// No "package" attribute found
|
||||
return null;
|
||||
}
|
||||
eventType = parser.next();
|
||||
}
|
||||
|
||||
// No manifest element found
|
||||
return null;
|
||||
} catch (AndroidBinXmlParser.XmlParserException e) {
|
||||
throw new ApkFormatException(
|
||||
"Unable to determine APK package name: malformed binary resource: "
|
||||
+ ANDROID_MANIFEST_ZIP_ENTRY_NAME,
|
||||
e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the security sandbox version targeted by an APK with the provided
|
||||
* {@code AndroidManifest.xml}.
|
||||
*
|
||||
* <p>If the security sandbox version is not specified in the manifest a default value of 1 is
|
||||
* returned.
|
||||
*
|
||||
* @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android
|
||||
* resource format
|
||||
*/
|
||||
public static int getTargetSandboxVersionFromBinaryAndroidManifest(
|
||||
ByteBuffer androidManifestContents) {
|
||||
try {
|
||||
return getAttributeValueFromBinaryAndroidManifest(androidManifestContents,
|
||||
MANIFEST_ELEMENT_TAG, TARGET_SANDBOX_VERSION_ATTR_ID);
|
||||
} catch (ApkFormatException e) {
|
||||
// An ApkFormatException indicates the target sandbox is not specified in the manifest;
|
||||
// return a default value of 1.
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the SDK version targeted by an APK with the provided {@code AndroidManifest.xml}.
|
||||
*
|
||||
* <p>If the targetSdkVersion is not specified the minimumSdkVersion is returned. If neither
|
||||
* value is specified then a value of 1 is returned.
|
||||
*
|
||||
* @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android
|
||||
* resource format
|
||||
*/
|
||||
public static int getTargetSdkVersionFromBinaryAndroidManifest(
|
||||
ByteBuffer androidManifestContents) {
|
||||
// If the targetSdkVersion is not specified then the platform will use the value of the
|
||||
// minSdkVersion; if neither is specified then the platform will use a value of 1.
|
||||
int minSdkVersion = 1;
|
||||
try {
|
||||
return getAttributeValueFromBinaryAndroidManifest(androidManifestContents,
|
||||
USES_SDK_ELEMENT_TAG, TARGET_SDK_VERSION_ATTR_ID);
|
||||
} catch (ApkFormatException e) {
|
||||
// Expected if the APK does not contain a targetSdkVersion attribute or the uses-sdk
|
||||
// element is not specified at all.
|
||||
}
|
||||
androidManifestContents.rewind();
|
||||
try {
|
||||
minSdkVersion = getMinSdkVersionFromBinaryAndroidManifest(androidManifestContents);
|
||||
} catch (ApkFormatException e) {
|
||||
// Similar to above, expected if the APK does not contain a minSdkVersion attribute, or
|
||||
// the uses-sdk element is not specified at all.
|
||||
}
|
||||
return minSdkVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the versionCode of the APK according to its {@code AndroidManifest.xml}.
|
||||
*
|
||||
* <p>If the versionCode is not specified in the {@code AndroidManifest.xml} or is not a valid
|
||||
* integer an ApkFormatException is thrown.
|
||||
*
|
||||
* @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android
|
||||
* resource format
|
||||
* @throws ApkFormatException if an error occurred while determining the versionCode, or if the
|
||||
* versionCode attribute value is not available.
|
||||
*/
|
||||
public static int getVersionCodeFromBinaryAndroidManifest(ByteBuffer androidManifestContents)
|
||||
throws ApkFormatException {
|
||||
return getAttributeValueFromBinaryAndroidManifest(androidManifestContents,
|
||||
MANIFEST_ELEMENT_TAG, VERSION_CODE_ATTR_ID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the versionCode and versionCodeMajor of the APK according to its {@code
|
||||
* AndroidManifest.xml} combined together as a single long value.
|
||||
*
|
||||
* <p>The versionCodeMajor is placed in the upper 32 bits, and the versionCode is in the lower
|
||||
* 32 bits. If the versionCodeMajor is not specified then the versionCode is returned.
|
||||
*
|
||||
* @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android
|
||||
* resource format
|
||||
* @throws ApkFormatException if an error occurred while determining the version, or if the
|
||||
* versionCode attribute value is not available.
|
||||
*/
|
||||
public static long getLongVersionCodeFromBinaryAndroidManifest(
|
||||
ByteBuffer androidManifestContents) throws ApkFormatException {
|
||||
// If the versionCode is not found then allow the ApkFormatException to be thrown to notify
|
||||
// the caller that the versionCode is not available.
|
||||
int versionCode = getVersionCodeFromBinaryAndroidManifest(androidManifestContents);
|
||||
long versionCodeMajor = 0;
|
||||
try {
|
||||
androidManifestContents.rewind();
|
||||
versionCodeMajor = getAttributeValueFromBinaryAndroidManifest(androidManifestContents,
|
||||
MANIFEST_ELEMENT_TAG, VERSION_CODE_MAJOR_ATTR_ID);
|
||||
} catch (ApkFormatException e) {
|
||||
// This is expected if the versionCodeMajor has not been defined for the APK; in this
|
||||
// case the return value is just the versionCode.
|
||||
}
|
||||
return (versionCodeMajor << 32) | versionCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the integer value of the requested {@code attributeId} in the specified {@code
|
||||
* elementName} from the provided {@code androidManifestContents} in binary Android resource
|
||||
* format.
|
||||
*
|
||||
* @throws ApkFormatException if an error occurred while attempting to obtain the attribute, or
|
||||
* if the requested attribute is not found.
|
||||
*/
|
||||
private static int getAttributeValueFromBinaryAndroidManifest(
|
||||
ByteBuffer androidManifestContents, String elementName, int attributeId)
|
||||
throws ApkFormatException {
|
||||
if (elementName == null) {
|
||||
throw new NullPointerException("elementName cannot be null");
|
||||
}
|
||||
try {
|
||||
AndroidBinXmlParser parser = new AndroidBinXmlParser(androidManifestContents);
|
||||
int eventType = parser.getEventType();
|
||||
while (eventType != AndroidBinXmlParser.EVENT_END_DOCUMENT) {
|
||||
if ((eventType == AndroidBinXmlParser.EVENT_START_ELEMENT)
|
||||
&& (elementName.equals(parser.getName()))) {
|
||||
for (int i = 0; i < parser.getAttributeCount(); i++) {
|
||||
if (parser.getAttributeNameResourceId(i) == attributeId) {
|
||||
int valueType = parser.getAttributeValueType(i);
|
||||
switch (valueType) {
|
||||
case AndroidBinXmlParser.VALUE_TYPE_INT:
|
||||
case AndroidBinXmlParser.VALUE_TYPE_STRING:
|
||||
return parser.getAttributeIntValue(i);
|
||||
default:
|
||||
throw new ApkFormatException(
|
||||
"Unsupported value type, " + valueType
|
||||
+ ", for attribute " + String.format("0x%08X",
|
||||
attributeId) + " under element " + elementName);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
eventType = parser.next();
|
||||
}
|
||||
throw new ApkFormatException(
|
||||
"Failed to determine APK's " + elementName + " attribute "
|
||||
+ String.format("0x%08X", attributeId) + " value");
|
||||
} catch (AndroidBinXmlParser.XmlParserException e) {
|
||||
throw new ApkFormatException(
|
||||
"Unable to determine value for attribute " + String.format("0x%08X",
|
||||
attributeId) + " under element " + elementName
|
||||
+ "; malformed binary resource: " + ANDROID_MANIFEST_ZIP_ENTRY_NAME, e);
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] computeSha256DigestBytes(byte[] data) {
|
||||
return ApkUtilsLite.computeSha256DigestBytes(data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
/*
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.apk;
|
||||
|
||||
import com.android.apksig.internal.util.Pair;
|
||||
import com.android.apksig.internal.zip.ZipUtils;
|
||||
import com.android.apksig.util.DataSource;
|
||||
import com.android.apksig.zip.ZipFormatException;
|
||||
import com.android.apksig.zip.ZipSections;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
/**
|
||||
* Lightweight version of the ApkUtils for clients that only require a subset of the utility
|
||||
* functionality.
|
||||
*/
|
||||
public class ApkUtilsLite {
|
||||
private ApkUtilsLite() {}
|
||||
|
||||
/**
|
||||
* Finds the main ZIP sections of the provided APK.
|
||||
*
|
||||
* @throws IOException if an I/O error occurred while reading the APK
|
||||
* @throws ZipFormatException if the APK is malformed
|
||||
*/
|
||||
public static ZipSections findZipSections(DataSource apk)
|
||||
throws IOException, ZipFormatException {
|
||||
Pair<ByteBuffer, Long> eocdAndOffsetInFile =
|
||||
ZipUtils.findZipEndOfCentralDirectoryRecord(apk);
|
||||
if (eocdAndOffsetInFile == null) {
|
||||
throw new ZipFormatException("ZIP End of Central Directory record not found");
|
||||
}
|
||||
|
||||
ByteBuffer eocdBuf = eocdAndOffsetInFile.getFirst();
|
||||
long eocdOffset = eocdAndOffsetInFile.getSecond();
|
||||
eocdBuf.order(ByteOrder.LITTLE_ENDIAN);
|
||||
long cdStartOffset = ZipUtils.getZipEocdCentralDirectoryOffset(eocdBuf);
|
||||
if (cdStartOffset > eocdOffset) {
|
||||
throw new ZipFormatException(
|
||||
"ZIP Central Directory start offset out of range: " + cdStartOffset
|
||||
+ ". ZIP End of Central Directory offset: " + eocdOffset);
|
||||
}
|
||||
|
||||
long cdSizeBytes = ZipUtils.getZipEocdCentralDirectorySizeBytes(eocdBuf);
|
||||
long cdEndOffset = cdStartOffset + cdSizeBytes;
|
||||
if (cdEndOffset > eocdOffset) {
|
||||
throw new ZipFormatException(
|
||||
"ZIP Central Directory overlaps with End of Central Directory"
|
||||
+ ". CD end: " + cdEndOffset
|
||||
+ ", EoCD start: " + eocdOffset);
|
||||
}
|
||||
|
||||
int cdRecordCount = ZipUtils.getZipEocdCentralDirectoryTotalRecordCount(eocdBuf);
|
||||
|
||||
return new ZipSections(
|
||||
cdStartOffset,
|
||||
cdSizeBytes,
|
||||
cdRecordCount,
|
||||
eocdOffset,
|
||||
eocdBuf);
|
||||
}
|
||||
|
||||
// See https://source.android.com/security/apksigning/v2.html
|
||||
private static final long APK_SIG_BLOCK_MAGIC_HI = 0x3234206b636f6c42L;
|
||||
private static final long APK_SIG_BLOCK_MAGIC_LO = 0x20676953204b5041L;
|
||||
private static final int APK_SIG_BLOCK_MIN_SIZE = 32;
|
||||
|
||||
/**
|
||||
* Returns the APK Signing Block of the provided APK.
|
||||
*
|
||||
* @throws IOException if an I/O error occurs
|
||||
* @throws ApkSigningBlockNotFoundException if there is no APK Signing Block in the APK
|
||||
*
|
||||
* @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature Scheme v2
|
||||
* </a>
|
||||
*/
|
||||
public static ApkSigningBlock findApkSigningBlock(DataSource apk, ZipSections zipSections)
|
||||
throws IOException, ApkSigningBlockNotFoundException {
|
||||
// FORMAT (see https://source.android.com/security/apksigning/v2.html):
|
||||
// OFFSET DATA TYPE DESCRIPTION
|
||||
// * @+0 bytes uint64: size in bytes (excluding this field)
|
||||
// * @+8 bytes payload
|
||||
// * @-24 bytes uint64: size in bytes (same as the one above)
|
||||
// * @-16 bytes uint128: magic
|
||||
|
||||
long centralDirStartOffset = zipSections.getZipCentralDirectoryOffset();
|
||||
long centralDirEndOffset =
|
||||
centralDirStartOffset + zipSections.getZipCentralDirectorySizeBytes();
|
||||
long eocdStartOffset = zipSections.getZipEndOfCentralDirectoryOffset();
|
||||
if (centralDirEndOffset != eocdStartOffset) {
|
||||
throw new ApkSigningBlockNotFoundException(
|
||||
"ZIP Central Directory is not immediately followed by End of Central Directory"
|
||||
+ ". CD end: " + centralDirEndOffset
|
||||
+ ", EoCD start: " + eocdStartOffset);
|
||||
}
|
||||
|
||||
if (centralDirStartOffset < APK_SIG_BLOCK_MIN_SIZE) {
|
||||
throw new ApkSigningBlockNotFoundException(
|
||||
"APK too small for APK Signing Block. ZIP Central Directory offset: "
|
||||
+ centralDirStartOffset);
|
||||
}
|
||||
// Read the magic and offset in file from the footer section of the block:
|
||||
// * uint64: size of block
|
||||
// * 16 bytes: magic
|
||||
ByteBuffer footer = apk.getByteBuffer(centralDirStartOffset - 24, 24);
|
||||
footer.order(ByteOrder.LITTLE_ENDIAN);
|
||||
if ((footer.getLong(8) != APK_SIG_BLOCK_MAGIC_LO)
|
||||
|| (footer.getLong(16) != APK_SIG_BLOCK_MAGIC_HI)) {
|
||||
throw new ApkSigningBlockNotFoundException(
|
||||
"No APK Signing Block before ZIP Central Directory");
|
||||
}
|
||||
// Read and compare size fields
|
||||
long apkSigBlockSizeInFooter = footer.getLong(0);
|
||||
if ((apkSigBlockSizeInFooter < footer.capacity())
|
||||
|| (apkSigBlockSizeInFooter > Integer.MAX_VALUE - 8)) {
|
||||
throw new ApkSigningBlockNotFoundException(
|
||||
"APK Signing Block size out of range: " + apkSigBlockSizeInFooter);
|
||||
}
|
||||
int totalSize = (int) (apkSigBlockSizeInFooter + 8);
|
||||
long apkSigBlockOffset = centralDirStartOffset - totalSize;
|
||||
if (apkSigBlockOffset < 0) {
|
||||
throw new ApkSigningBlockNotFoundException(
|
||||
"APK Signing Block offset out of range: " + apkSigBlockOffset);
|
||||
}
|
||||
ByteBuffer apkSigBlock = apk.getByteBuffer(apkSigBlockOffset, 8);
|
||||
apkSigBlock.order(ByteOrder.LITTLE_ENDIAN);
|
||||
long apkSigBlockSizeInHeader = apkSigBlock.getLong(0);
|
||||
if (apkSigBlockSizeInHeader != apkSigBlockSizeInFooter) {
|
||||
throw new ApkSigningBlockNotFoundException(
|
||||
"APK Signing Block sizes in header and footer do not match: "
|
||||
+ apkSigBlockSizeInHeader + " vs " + apkSigBlockSizeInFooter);
|
||||
}
|
||||
return new ApkSigningBlock(apkSigBlockOffset, apk.slice(apkSigBlockOffset, totalSize));
|
||||
}
|
||||
|
||||
/**
|
||||
* Information about the location of the APK Signing Block inside an APK.
|
||||
*/
|
||||
public static class ApkSigningBlock {
|
||||
private final long mStartOffsetInApk;
|
||||
private final DataSource mContents;
|
||||
|
||||
/**
|
||||
* Constructs a new {@code ApkSigningBlock}.
|
||||
*
|
||||
* @param startOffsetInApk start offset (in bytes, relative to start of file) of the APK
|
||||
* Signing Block inside the APK file
|
||||
* @param contents contents of the APK Signing Block
|
||||
*/
|
||||
public ApkSigningBlock(long startOffsetInApk, DataSource contents) {
|
||||
mStartOffsetInApk = startOffsetInApk;
|
||||
mContents = contents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the start offset (in bytes, relative to start of file) of the APK Signing Block.
|
||||
*/
|
||||
public long getStartOffset() {
|
||||
return mStartOffsetInApk;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the data source which provides the full contents of the APK Signing Block,
|
||||
* including its footer.
|
||||
*/
|
||||
public DataSource getContents() {
|
||||
return mContents;
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] computeSha256DigestBytes(byte[] data) {
|
||||
MessageDigest messageDigest;
|
||||
try {
|
||||
messageDigest = MessageDigest.getInstance("SHA-256");
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new IllegalStateException("SHA-256 is not found", e);
|
||||
}
|
||||
messageDigest.update(data);
|
||||
return messageDigest.digest();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.apk;
|
||||
|
||||
/**
|
||||
* Indicates that there was an issue determining the minimum Android platform version supported by
|
||||
* an APK because the version is specified as a codename, rather than as API Level number, and the
|
||||
* codename is in an unexpected format.
|
||||
*/
|
||||
public class CodenameMinSdkVersionException extends MinSdkVersionException {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** Encountered codename. */
|
||||
private final String mCodename;
|
||||
|
||||
/**
|
||||
* Constructs a new {@code MinSdkVersionCodenameException} with the provided message and
|
||||
* codename.
|
||||
*/
|
||||
public CodenameMinSdkVersionException(String message, String codename) {
|
||||
super(message);
|
||||
mCodename = codename;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the codename.
|
||||
*/
|
||||
public String getCodename() {
|
||||
return mCodename;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.apk;
|
||||
|
||||
/**
|
||||
* Indicates that there was an issue determining the minimum Android platform version supported by
|
||||
* an APK.
|
||||
*/
|
||||
public class MinSdkVersionException extends ApkFormatException {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* Constructs a new {@code MinSdkVersionException} with the provided message.
|
||||
*/
|
||||
public MinSdkVersionException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new {@code MinSdkVersionException} with the provided message and cause.
|
||||
*/
|
||||
public MinSdkVersionException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,869 @@
|
||||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.apk;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* XML pull style parser of Android binary XML resources, such as {@code AndroidManifest.xml}.
|
||||
*
|
||||
* <p>For an input document, the parser outputs an event stream (see {@code EVENT_... constants} via
|
||||
* {@link #getEventType()} and {@link #next()} methods. Additional information about the current
|
||||
* event can be obtained via an assortment of getters, for example, {@link #getName()} or
|
||||
* {@link #getAttributeNameResourceId(int)}.
|
||||
*/
|
||||
public class AndroidBinXmlParser {
|
||||
|
||||
/** Event: start of document. */
|
||||
public static final int EVENT_START_DOCUMENT = 1;
|
||||
|
||||
/** Event: end of document. */
|
||||
public static final int EVENT_END_DOCUMENT = 2;
|
||||
|
||||
/** Event: start of an element. */
|
||||
public static final int EVENT_START_ELEMENT = 3;
|
||||
|
||||
/** Event: end of an document. */
|
||||
public static final int EVENT_END_ELEMENT = 4;
|
||||
|
||||
/** Attribute value type is not supported by this parser. */
|
||||
public static final int VALUE_TYPE_UNSUPPORTED = 0;
|
||||
|
||||
/** Attribute value is a string. Use {@link #getAttributeStringValue(int)} to obtain it. */
|
||||
public static final int VALUE_TYPE_STRING = 1;
|
||||
|
||||
/** Attribute value is an integer. Use {@link #getAttributeIntValue(int)} to obtain it. */
|
||||
public static final int VALUE_TYPE_INT = 2;
|
||||
|
||||
/**
|
||||
* Attribute value is a resource reference. Use {@link #getAttributeIntValue(int)} to obtain it.
|
||||
*/
|
||||
public static final int VALUE_TYPE_REFERENCE = 3;
|
||||
|
||||
/** Attribute value is a boolean. Use {@link #getAttributeBooleanValue(int)} to obtain it. */
|
||||
public static final int VALUE_TYPE_BOOLEAN = 4;
|
||||
|
||||
private static final long NO_NAMESPACE = 0xffffffffL;
|
||||
|
||||
private final ByteBuffer mXml;
|
||||
|
||||
private StringPool mStringPool;
|
||||
private ResourceMap mResourceMap;
|
||||
private int mDepth;
|
||||
private int mCurrentEvent = EVENT_START_DOCUMENT;
|
||||
|
||||
private String mCurrentElementName;
|
||||
private String mCurrentElementNamespace;
|
||||
private int mCurrentElementAttributeCount;
|
||||
private List<Attribute> mCurrentElementAttributes;
|
||||
private ByteBuffer mCurrentElementAttributesContents;
|
||||
private int mCurrentElementAttrSizeBytes;
|
||||
|
||||
/**
|
||||
* Constructs a new parser for the provided document.
|
||||
*/
|
||||
public AndroidBinXmlParser(ByteBuffer xml) throws XmlParserException {
|
||||
xml.order(ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
Chunk resXmlChunk = null;
|
||||
while (xml.hasRemaining()) {
|
||||
Chunk chunk = Chunk.get(xml);
|
||||
if (chunk == null) {
|
||||
break;
|
||||
}
|
||||
if (chunk.getType() == Chunk.TYPE_RES_XML) {
|
||||
resXmlChunk = chunk;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (resXmlChunk == null) {
|
||||
throw new XmlParserException("No XML chunk in file");
|
||||
}
|
||||
mXml = resXmlChunk.getContents();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the depth of the current element. Outside of the root of the document the depth is
|
||||
* {@code 0}. The depth is incremented by {@code 1} before each {@code start element} event and
|
||||
* is decremented by {@code 1} after each {@code end element} event.
|
||||
*/
|
||||
public int getDepth() {
|
||||
return mDepth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the type of the current event. See {@code EVENT_...} constants.
|
||||
*/
|
||||
public int getEventType() {
|
||||
return mCurrentEvent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the local name of the current element or {@code null} if the current event does not
|
||||
* pertain to an element.
|
||||
*/
|
||||
public String getName() {
|
||||
if ((mCurrentEvent != EVENT_START_ELEMENT) && (mCurrentEvent != EVENT_END_ELEMENT)) {
|
||||
return null;
|
||||
}
|
||||
return mCurrentElementName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the namespace of the current element or {@code null} if the current event does not
|
||||
* pertain to an element. Returns an empty string if the element is not associated with a
|
||||
* namespace.
|
||||
*/
|
||||
public String getNamespace() {
|
||||
if ((mCurrentEvent != EVENT_START_ELEMENT) && (mCurrentEvent != EVENT_END_ELEMENT)) {
|
||||
return null;
|
||||
}
|
||||
return mCurrentElementNamespace;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of attributes of the element associated with the current event or
|
||||
* {@code -1} if no element is associated with the current event.
|
||||
*/
|
||||
public int getAttributeCount() {
|
||||
if (mCurrentEvent != EVENT_START_ELEMENT) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return mCurrentElementAttributeCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the resource ID corresponding to the name of the specified attribute of the current
|
||||
* element or {@code 0} if the name is not associated with a resource ID.
|
||||
*
|
||||
* @throws IndexOutOfBoundsException if the index is out of range or the current event is not a
|
||||
* {@code start element} event
|
||||
* @throws XmlParserException if a parsing error is occurred
|
||||
*/
|
||||
public int getAttributeNameResourceId(int index) throws XmlParserException {
|
||||
return getAttribute(index).getNameResourceId();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the specified attribute of the current element.
|
||||
*
|
||||
* @throws IndexOutOfBoundsException if the index is out of range or the current event is not a
|
||||
* {@code start element} event
|
||||
* @throws XmlParserException if a parsing error is occurred
|
||||
*/
|
||||
public String getAttributeName(int index) throws XmlParserException {
|
||||
return getAttribute(index).getName();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the specified attribute of the current element or an empty string if
|
||||
* the attribute is not associated with a namespace.
|
||||
*
|
||||
* @throws IndexOutOfBoundsException if the index is out of range or the current event is not a
|
||||
* {@code start element} event
|
||||
* @throws XmlParserException if a parsing error is occurred
|
||||
*/
|
||||
public String getAttributeNamespace(int index) throws XmlParserException {
|
||||
return getAttribute(index).getNamespace();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value type of the specified attribute of the current element. See
|
||||
* {@code VALUE_TYPE_...} constants.
|
||||
*
|
||||
* @throws IndexOutOfBoundsException if the index is out of range or the current event is not a
|
||||
* {@code start element} event
|
||||
* @throws XmlParserException if a parsing error is occurred
|
||||
*/
|
||||
public int getAttributeValueType(int index) throws XmlParserException {
|
||||
int type = getAttribute(index).getValueType();
|
||||
switch (type) {
|
||||
case Attribute.TYPE_STRING:
|
||||
return VALUE_TYPE_STRING;
|
||||
case Attribute.TYPE_INT_DEC:
|
||||
case Attribute.TYPE_INT_HEX:
|
||||
return VALUE_TYPE_INT;
|
||||
case Attribute.TYPE_REFERENCE:
|
||||
return VALUE_TYPE_REFERENCE;
|
||||
case Attribute.TYPE_INT_BOOLEAN:
|
||||
return VALUE_TYPE_BOOLEAN;
|
||||
default:
|
||||
return VALUE_TYPE_UNSUPPORTED;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the integer value of the specified attribute of the current element. See
|
||||
* {@code VALUE_TYPE_...} constants.
|
||||
*
|
||||
* @throws IndexOutOfBoundsException if the index is out of range or the current event is not a
|
||||
* {@code start element} event.
|
||||
* @throws XmlParserException if a parsing error is occurred
|
||||
*/
|
||||
public int getAttributeIntValue(int index) throws XmlParserException {
|
||||
return getAttribute(index).getIntValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the boolean value of the specified attribute of the current element. See
|
||||
* {@code VALUE_TYPE_...} constants.
|
||||
*
|
||||
* @throws IndexOutOfBoundsException if the index is out of range or the current event is not a
|
||||
* {@code start element} event.
|
||||
* @throws XmlParserException if a parsing error is occurred
|
||||
*/
|
||||
public boolean getAttributeBooleanValue(int index) throws XmlParserException {
|
||||
return getAttribute(index).getBooleanValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the string value of the specified attribute of the current element. See
|
||||
* {@code VALUE_TYPE_...} constants.
|
||||
*
|
||||
* @throws IndexOutOfBoundsException if the index is out of range or the current event is not a
|
||||
* {@code start element} event.
|
||||
* @throws XmlParserException if a parsing error is occurred
|
||||
*/
|
||||
public String getAttributeStringValue(int index) throws XmlParserException {
|
||||
return getAttribute(index).getStringValue();
|
||||
}
|
||||
|
||||
private Attribute getAttribute(int index) {
|
||||
if (mCurrentEvent != EVENT_START_ELEMENT) {
|
||||
throw new IndexOutOfBoundsException("Current event not a START_ELEMENT");
|
||||
}
|
||||
if (index < 0) {
|
||||
throw new IndexOutOfBoundsException("index must be >= 0");
|
||||
}
|
||||
if (index >= mCurrentElementAttributeCount) {
|
||||
throw new IndexOutOfBoundsException(
|
||||
"index must be <= attr count (" + mCurrentElementAttributeCount + ")");
|
||||
}
|
||||
parseCurrentElementAttributesIfNotParsed();
|
||||
return mCurrentElementAttributes.get(index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Advances to the next parsing event and returns its type. See {@code EVENT_...} constants.
|
||||
*/
|
||||
public int next() throws XmlParserException {
|
||||
// Decrement depth if the previous event was "end element".
|
||||
if (mCurrentEvent == EVENT_END_ELEMENT) {
|
||||
mDepth--;
|
||||
}
|
||||
|
||||
// Read events from document, ignoring events that we don't report to caller. Stop at the
|
||||
// earliest event which we report to caller.
|
||||
while (mXml.hasRemaining()) {
|
||||
Chunk chunk = Chunk.get(mXml);
|
||||
if (chunk == null) {
|
||||
break;
|
||||
}
|
||||
switch (chunk.getType()) {
|
||||
case Chunk.TYPE_STRING_POOL:
|
||||
if (mStringPool != null) {
|
||||
throw new XmlParserException("Multiple string pools not supported");
|
||||
}
|
||||
mStringPool = new StringPool(chunk);
|
||||
break;
|
||||
|
||||
case Chunk.RES_XML_TYPE_START_ELEMENT:
|
||||
{
|
||||
if (mStringPool == null) {
|
||||
throw new XmlParserException(
|
||||
"Named element encountered before string pool");
|
||||
}
|
||||
ByteBuffer contents = chunk.getContents();
|
||||
if (contents.remaining() < 20) {
|
||||
throw new XmlParserException(
|
||||
"Start element chunk too short. Need at least 20 bytes. Available: "
|
||||
+ contents.remaining() + " bytes");
|
||||
}
|
||||
long nsId = getUnsignedInt32(contents);
|
||||
long nameId = getUnsignedInt32(contents);
|
||||
int attrStartOffset = getUnsignedInt16(contents);
|
||||
int attrSizeBytes = getUnsignedInt16(contents);
|
||||
int attrCount = getUnsignedInt16(contents);
|
||||
long attrEndOffset = attrStartOffset + ((long) attrCount) * attrSizeBytes;
|
||||
contents.position(0);
|
||||
if (attrStartOffset > contents.remaining()) {
|
||||
throw new XmlParserException(
|
||||
"Attributes start offset out of bounds: " + attrStartOffset
|
||||
+ ", max: " + contents.remaining());
|
||||
}
|
||||
if (attrEndOffset > contents.remaining()) {
|
||||
throw new XmlParserException(
|
||||
"Attributes end offset out of bounds: " + attrEndOffset
|
||||
+ ", max: " + contents.remaining());
|
||||
}
|
||||
|
||||
mCurrentElementName = mStringPool.getString(nameId);
|
||||
mCurrentElementNamespace =
|
||||
(nsId == NO_NAMESPACE) ? "" : mStringPool.getString(nsId);
|
||||
mCurrentElementAttributeCount = attrCount;
|
||||
mCurrentElementAttributes = null;
|
||||
mCurrentElementAttrSizeBytes = attrSizeBytes;
|
||||
mCurrentElementAttributesContents =
|
||||
sliceFromTo(contents, attrStartOffset, attrEndOffset);
|
||||
|
||||
mDepth++;
|
||||
mCurrentEvent = EVENT_START_ELEMENT;
|
||||
return mCurrentEvent;
|
||||
}
|
||||
|
||||
case Chunk.RES_XML_TYPE_END_ELEMENT:
|
||||
{
|
||||
if (mStringPool == null) {
|
||||
throw new XmlParserException(
|
||||
"Named element encountered before string pool");
|
||||
}
|
||||
ByteBuffer contents = chunk.getContents();
|
||||
if (contents.remaining() < 8) {
|
||||
throw new XmlParserException(
|
||||
"End element chunk too short. Need at least 8 bytes. Available: "
|
||||
+ contents.remaining() + " bytes");
|
||||
}
|
||||
long nsId = getUnsignedInt32(contents);
|
||||
long nameId = getUnsignedInt32(contents);
|
||||
mCurrentElementName = mStringPool.getString(nameId);
|
||||
mCurrentElementNamespace =
|
||||
(nsId == NO_NAMESPACE) ? "" : mStringPool.getString(nsId);
|
||||
mCurrentEvent = EVENT_END_ELEMENT;
|
||||
mCurrentElementAttributes = null;
|
||||
mCurrentElementAttributesContents = null;
|
||||
return mCurrentEvent;
|
||||
}
|
||||
case Chunk.RES_XML_TYPE_RESOURCE_MAP:
|
||||
if (mResourceMap != null) {
|
||||
throw new XmlParserException("Multiple resource maps not supported");
|
||||
}
|
||||
mResourceMap = new ResourceMap(chunk);
|
||||
break;
|
||||
default:
|
||||
// Unknown chunk type -- ignore
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
mCurrentEvent = EVENT_END_DOCUMENT;
|
||||
return mCurrentEvent;
|
||||
}
|
||||
|
||||
private void parseCurrentElementAttributesIfNotParsed() {
|
||||
if (mCurrentElementAttributes != null) {
|
||||
return;
|
||||
}
|
||||
mCurrentElementAttributes = new ArrayList<>(mCurrentElementAttributeCount);
|
||||
for (int i = 0; i < mCurrentElementAttributeCount; i++) {
|
||||
int startPosition = i * mCurrentElementAttrSizeBytes;
|
||||
ByteBuffer attr =
|
||||
sliceFromTo(
|
||||
mCurrentElementAttributesContents,
|
||||
startPosition,
|
||||
startPosition + mCurrentElementAttrSizeBytes);
|
||||
long nsId = getUnsignedInt32(attr);
|
||||
long nameId = getUnsignedInt32(attr);
|
||||
attr.position(attr.position() + 7); // skip ignored fields
|
||||
int valueType = getUnsignedInt8(attr);
|
||||
long valueData = getUnsignedInt32(attr);
|
||||
mCurrentElementAttributes.add(
|
||||
new Attribute(
|
||||
nsId,
|
||||
nameId,
|
||||
valueType,
|
||||
(int) valueData,
|
||||
mStringPool,
|
||||
mResourceMap));
|
||||
}
|
||||
}
|
||||
|
||||
private static class Attribute {
|
||||
private static final int TYPE_REFERENCE = 1;
|
||||
private static final int TYPE_STRING = 3;
|
||||
private static final int TYPE_INT_DEC = 0x10;
|
||||
private static final int TYPE_INT_HEX = 0x11;
|
||||
private static final int TYPE_INT_BOOLEAN = 0x12;
|
||||
|
||||
private final long mNsId;
|
||||
private final long mNameId;
|
||||
private final int mValueType;
|
||||
private final int mValueData;
|
||||
private final StringPool mStringPool;
|
||||
private final ResourceMap mResourceMap;
|
||||
|
||||
private Attribute(
|
||||
long nsId,
|
||||
long nameId,
|
||||
int valueType,
|
||||
int valueData,
|
||||
StringPool stringPool,
|
||||
ResourceMap resourceMap) {
|
||||
mNsId = nsId;
|
||||
mNameId = nameId;
|
||||
mValueType = valueType;
|
||||
mValueData = valueData;
|
||||
mStringPool = stringPool;
|
||||
mResourceMap = resourceMap;
|
||||
}
|
||||
|
||||
public int getNameResourceId() {
|
||||
return (mResourceMap != null) ? mResourceMap.getResourceId(mNameId) : 0;
|
||||
}
|
||||
|
||||
public String getName() throws XmlParserException {
|
||||
return mStringPool.getString(mNameId);
|
||||
}
|
||||
|
||||
public String getNamespace() throws XmlParserException {
|
||||
return (mNsId != NO_NAMESPACE) ? mStringPool.getString(mNsId) : "";
|
||||
}
|
||||
|
||||
public int getValueType() {
|
||||
return mValueType;
|
||||
}
|
||||
|
||||
public int getIntValue() throws XmlParserException {
|
||||
switch (mValueType) {
|
||||
case TYPE_REFERENCE:
|
||||
case TYPE_INT_DEC:
|
||||
case TYPE_INT_HEX:
|
||||
case TYPE_INT_BOOLEAN:
|
||||
return mValueData;
|
||||
default:
|
||||
throw new XmlParserException("Cannot coerce to int: value type " + mValueType);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean getBooleanValue() throws XmlParserException {
|
||||
switch (mValueType) {
|
||||
case TYPE_INT_BOOLEAN:
|
||||
return mValueData != 0;
|
||||
default:
|
||||
throw new XmlParserException(
|
||||
"Cannot coerce to boolean: value type " + mValueType);
|
||||
}
|
||||
}
|
||||
|
||||
public String getStringValue() throws XmlParserException {
|
||||
switch (mValueType) {
|
||||
case TYPE_STRING:
|
||||
return mStringPool.getString(mValueData & 0xffffffffL);
|
||||
case TYPE_INT_DEC:
|
||||
return Integer.toString(mValueData);
|
||||
case TYPE_INT_HEX:
|
||||
return "0x" + Integer.toHexString(mValueData);
|
||||
case TYPE_INT_BOOLEAN:
|
||||
return Boolean.toString(mValueData != 0);
|
||||
case TYPE_REFERENCE:
|
||||
return "@" + Integer.toHexString(mValueData);
|
||||
default:
|
||||
throw new XmlParserException(
|
||||
"Cannot coerce to string: value type " + mValueType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Chunk of a document. Each chunk is tagged with a type and consists of a header followed by
|
||||
* contents.
|
||||
*/
|
||||
private static class Chunk {
|
||||
public static final int TYPE_STRING_POOL = 1;
|
||||
public static final int TYPE_RES_XML = 3;
|
||||
public static final int RES_XML_TYPE_START_ELEMENT = 0x0102;
|
||||
public static final int RES_XML_TYPE_END_ELEMENT = 0x0103;
|
||||
public static final int RES_XML_TYPE_RESOURCE_MAP = 0x0180;
|
||||
|
||||
static final int HEADER_MIN_SIZE_BYTES = 8;
|
||||
|
||||
private final int mType;
|
||||
private final ByteBuffer mHeader;
|
||||
private final ByteBuffer mContents;
|
||||
|
||||
public Chunk(int type, ByteBuffer header, ByteBuffer contents) {
|
||||
mType = type;
|
||||
mHeader = header;
|
||||
mContents = contents;
|
||||
}
|
||||
|
||||
public ByteBuffer getContents() {
|
||||
ByteBuffer result = mContents.slice();
|
||||
result.order(mContents.order());
|
||||
return result;
|
||||
}
|
||||
|
||||
public ByteBuffer getHeader() {
|
||||
ByteBuffer result = mHeader.slice();
|
||||
result.order(mHeader.order());
|
||||
return result;
|
||||
}
|
||||
|
||||
public int getType() {
|
||||
return mType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Consumes the chunk located at the current position of the input and returns the chunk
|
||||
* or {@code null} if there is no chunk left in the input.
|
||||
*
|
||||
* @throws XmlParserException if the chunk is malformed
|
||||
*/
|
||||
public static Chunk get(ByteBuffer input) throws XmlParserException {
|
||||
if (input.remaining() < HEADER_MIN_SIZE_BYTES) {
|
||||
// Android ignores the last chunk if its header is too big to fit into the file
|
||||
input.position(input.limit());
|
||||
return null;
|
||||
}
|
||||
|
||||
int originalPosition = input.position();
|
||||
int type = getUnsignedInt16(input);
|
||||
int headerSize = getUnsignedInt16(input);
|
||||
long chunkSize = getUnsignedInt32(input);
|
||||
long chunkRemaining = chunkSize - 8;
|
||||
if (chunkRemaining > input.remaining()) {
|
||||
// Android ignores the last chunk if it's too big to fit into the file
|
||||
input.position(input.limit());
|
||||
return null;
|
||||
}
|
||||
if (headerSize < HEADER_MIN_SIZE_BYTES) {
|
||||
throw new XmlParserException(
|
||||
"Malformed chunk: header too short: " + headerSize + " bytes");
|
||||
} else if (headerSize > chunkSize) {
|
||||
throw new XmlParserException(
|
||||
"Malformed chunk: header too long: " + headerSize + " bytes. Chunk size: "
|
||||
+ chunkSize + " bytes");
|
||||
}
|
||||
int contentStartPosition = originalPosition + headerSize;
|
||||
long chunkEndPosition = originalPosition + chunkSize;
|
||||
Chunk chunk =
|
||||
new Chunk(
|
||||
type,
|
||||
sliceFromTo(input, originalPosition, contentStartPosition),
|
||||
sliceFromTo(input, contentStartPosition, chunkEndPosition));
|
||||
input.position((int) chunkEndPosition);
|
||||
return chunk;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* String pool of a document. Strings are referenced by their {@code 0}-based index in the pool.
|
||||
*/
|
||||
private static class StringPool {
|
||||
private static final int FLAG_UTF8 = 1 << 8;
|
||||
|
||||
private final ByteBuffer mChunkContents;
|
||||
private final ByteBuffer mStringsSection;
|
||||
private final int mStringCount;
|
||||
private final boolean mUtf8Encoded;
|
||||
private final Map<Integer, String> mCachedStrings = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Constructs a new string pool from the provided chunk.
|
||||
*
|
||||
* @throws XmlParserException if a parsing error occurred
|
||||
*/
|
||||
public StringPool(Chunk chunk) throws XmlParserException {
|
||||
ByteBuffer header = chunk.getHeader();
|
||||
int headerSizeBytes = header.remaining();
|
||||
header.position(Chunk.HEADER_MIN_SIZE_BYTES);
|
||||
if (header.remaining() < 20) {
|
||||
throw new XmlParserException(
|
||||
"XML chunk's header too short. Required at least 20 bytes. Available: "
|
||||
+ header.remaining() + " bytes");
|
||||
}
|
||||
long stringCount = getUnsignedInt32(header);
|
||||
if (stringCount > Integer.MAX_VALUE) {
|
||||
throw new XmlParserException("Too many strings: " + stringCount);
|
||||
}
|
||||
mStringCount = (int) stringCount;
|
||||
long styleCount = getUnsignedInt32(header);
|
||||
if (styleCount > Integer.MAX_VALUE) {
|
||||
throw new XmlParserException("Too many styles: " + styleCount);
|
||||
}
|
||||
long flags = getUnsignedInt32(header);
|
||||
long stringsStartOffset = getUnsignedInt32(header);
|
||||
long stylesStartOffset = getUnsignedInt32(header);
|
||||
|
||||
ByteBuffer contents = chunk.getContents();
|
||||
if (mStringCount > 0) {
|
||||
int stringsSectionStartOffsetInContents =
|
||||
(int) (stringsStartOffset - headerSizeBytes);
|
||||
int stringsSectionEndOffsetInContents;
|
||||
if (styleCount > 0) {
|
||||
// Styles section follows the strings section
|
||||
if (stylesStartOffset < stringsStartOffset) {
|
||||
throw new XmlParserException(
|
||||
"Styles offset (" + stylesStartOffset + ") < strings offset ("
|
||||
+ stringsStartOffset + ")");
|
||||
}
|
||||
stringsSectionEndOffsetInContents = (int) (stylesStartOffset - headerSizeBytes);
|
||||
} else {
|
||||
stringsSectionEndOffsetInContents = contents.remaining();
|
||||
}
|
||||
mStringsSection =
|
||||
sliceFromTo(
|
||||
contents,
|
||||
stringsSectionStartOffsetInContents,
|
||||
stringsSectionEndOffsetInContents);
|
||||
} else {
|
||||
mStringsSection = ByteBuffer.allocate(0);
|
||||
}
|
||||
|
||||
mUtf8Encoded = (flags & FLAG_UTF8) != 0;
|
||||
mChunkContents = contents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the string located at the specified {@code 0}-based index in this pool.
|
||||
*
|
||||
* @throws XmlParserException if the string does not exist or cannot be decoded
|
||||
*/
|
||||
public String getString(long index) throws XmlParserException {
|
||||
if (index < 0) {
|
||||
throw new XmlParserException("Unsuported string index: " + index);
|
||||
} else if (index >= mStringCount) {
|
||||
throw new XmlParserException(
|
||||
"Unsuported string index: " + index + ", max: " + (mStringCount - 1));
|
||||
}
|
||||
|
||||
int idx = (int) index;
|
||||
String result = mCachedStrings.get(idx);
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
|
||||
long offsetInStringsSection = getUnsignedInt32(mChunkContents, idx * 4);
|
||||
if (offsetInStringsSection >= mStringsSection.capacity()) {
|
||||
throw new XmlParserException(
|
||||
"Offset of string idx " + idx + " out of bounds: " + offsetInStringsSection
|
||||
+ ", max: " + (mStringsSection.capacity() - 1));
|
||||
}
|
||||
mStringsSection.position((int) offsetInStringsSection);
|
||||
result =
|
||||
(mUtf8Encoded)
|
||||
? getLengthPrefixedUtf8EncodedString(mStringsSection)
|
||||
: getLengthPrefixedUtf16EncodedString(mStringsSection);
|
||||
mCachedStrings.put(idx, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static String getLengthPrefixedUtf16EncodedString(ByteBuffer encoded)
|
||||
throws XmlParserException {
|
||||
// If the length (in uint16s) is 0x7fff or lower, it is stored as a single uint16.
|
||||
// Otherwise, it is stored as a big-endian uint32 with highest bit set. Thus, the range
|
||||
// of supported values is 0 to 0x7fffffff inclusive.
|
||||
int lengthChars = getUnsignedInt16(encoded);
|
||||
if ((lengthChars & 0x8000) != 0) {
|
||||
lengthChars = ((lengthChars & 0x7fff) << 16) | getUnsignedInt16(encoded);
|
||||
}
|
||||
if (lengthChars > Integer.MAX_VALUE / 2) {
|
||||
throw new XmlParserException("String too long: " + lengthChars + " uint16s");
|
||||
}
|
||||
int lengthBytes = lengthChars * 2;
|
||||
|
||||
byte[] arr;
|
||||
int arrOffset;
|
||||
if (encoded.hasArray()) {
|
||||
arr = encoded.array();
|
||||
arrOffset = encoded.arrayOffset() + encoded.position();
|
||||
encoded.position(encoded.position() + lengthBytes);
|
||||
} else {
|
||||
arr = new byte[lengthBytes];
|
||||
arrOffset = 0;
|
||||
encoded.get(arr);
|
||||
}
|
||||
// Reproduce the behavior of Android runtime which requires that the UTF-16 encoded
|
||||
// array of bytes is NULL terminated.
|
||||
if ((arr[arrOffset + lengthBytes] != 0)
|
||||
|| (arr[arrOffset + lengthBytes + 1] != 0)) {
|
||||
throw new XmlParserException("UTF-16 encoded form of string not NULL terminated");
|
||||
}
|
||||
try {
|
||||
return new String(arr, arrOffset, lengthBytes, "UTF-16LE");
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
throw new RuntimeException("UTF-16LE character encoding not supported", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static String getLengthPrefixedUtf8EncodedString(ByteBuffer encoded)
|
||||
throws XmlParserException {
|
||||
// If the length (in bytes) is 0x7f or lower, it is stored as a single uint8. Otherwise,
|
||||
// it is stored as a big-endian uint16 with highest bit set. Thus, the range of
|
||||
// supported values is 0 to 0x7fff inclusive.
|
||||
|
||||
// Skip UTF-16 encoded length (in uint16s)
|
||||
int lengthBytes = getUnsignedInt8(encoded);
|
||||
if ((lengthBytes & 0x80) != 0) {
|
||||
lengthBytes = ((lengthBytes & 0x7f) << 8) | getUnsignedInt8(encoded);
|
||||
}
|
||||
|
||||
// Read UTF-8 encoded length (in bytes)
|
||||
lengthBytes = getUnsignedInt8(encoded);
|
||||
if ((lengthBytes & 0x80) != 0) {
|
||||
lengthBytes = ((lengthBytes & 0x7f) << 8) | getUnsignedInt8(encoded);
|
||||
}
|
||||
|
||||
byte[] arr;
|
||||
int arrOffset;
|
||||
if (encoded.hasArray()) {
|
||||
arr = encoded.array();
|
||||
arrOffset = encoded.arrayOffset() + encoded.position();
|
||||
encoded.position(encoded.position() + lengthBytes);
|
||||
} else {
|
||||
arr = new byte[lengthBytes];
|
||||
arrOffset = 0;
|
||||
encoded.get(arr);
|
||||
}
|
||||
// Reproduce the behavior of Android runtime which requires that the UTF-8 encoded array
|
||||
// of bytes is NULL terminated.
|
||||
if (arr[arrOffset + lengthBytes] != 0) {
|
||||
throw new XmlParserException("UTF-8 encoded form of string not NULL terminated");
|
||||
}
|
||||
try {
|
||||
return new String(arr, arrOffset, lengthBytes, "UTF-8");
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
throw new RuntimeException("UTF-8 character encoding not supported", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resource map of a document. Resource IDs are referenced by their {@code 0}-based index in the
|
||||
* map.
|
||||
*/
|
||||
private static class ResourceMap {
|
||||
private final ByteBuffer mChunkContents;
|
||||
private final int mEntryCount;
|
||||
|
||||
/**
|
||||
* Constructs a new resource map from the provided chunk.
|
||||
*
|
||||
* @throws XmlParserException if a parsing error occurred
|
||||
*/
|
||||
public ResourceMap(Chunk chunk) throws XmlParserException {
|
||||
mChunkContents = chunk.getContents().slice();
|
||||
mChunkContents.order(chunk.getContents().order());
|
||||
// Each entry of the map is four bytes long, containing the int32 resource ID.
|
||||
mEntryCount = mChunkContents.remaining() / 4;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the resource ID located at the specified {@code 0}-based index in this pool or
|
||||
* {@code 0} if the index is out of range.
|
||||
*/
|
||||
public int getResourceId(long index) {
|
||||
if ((index < 0) || (index >= mEntryCount)) {
|
||||
return 0;
|
||||
}
|
||||
int idx = (int) index;
|
||||
// Each entry of the map is four bytes long, containing the int32 resource ID.
|
||||
return mChunkContents.getInt(idx * 4);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns new byte buffer whose content is a shared subsequence of this buffer's content
|
||||
* between the specified start (inclusive) and end (exclusive) positions. As opposed to
|
||||
* {@link ByteBuffer#slice()}, the returned buffer's byte order is the same as the source
|
||||
* buffer's byte order.
|
||||
*/
|
||||
private static ByteBuffer sliceFromTo(ByteBuffer source, long start, long end) {
|
||||
if (start < 0) {
|
||||
throw new IllegalArgumentException("start: " + start);
|
||||
}
|
||||
if (end < start) {
|
||||
throw new IllegalArgumentException("end < start: " + end + " < " + start);
|
||||
}
|
||||
int capacity = source.capacity();
|
||||
if (end > source.capacity()) {
|
||||
throw new IllegalArgumentException("end > capacity: " + end + " > " + capacity);
|
||||
}
|
||||
return sliceFromTo(source, (int) start, (int) end);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns new byte buffer whose content is a shared subsequence of this buffer's content
|
||||
* between the specified start (inclusive) and end (exclusive) positions. As opposed to
|
||||
* {@link ByteBuffer#slice()}, the returned buffer's byte order is the same as the source
|
||||
* buffer's byte order.
|
||||
*/
|
||||
private static ByteBuffer sliceFromTo(ByteBuffer source, int start, int end) {
|
||||
if (start < 0) {
|
||||
throw new IllegalArgumentException("start: " + start);
|
||||
}
|
||||
if (end < start) {
|
||||
throw new IllegalArgumentException("end < start: " + end + " < " + start);
|
||||
}
|
||||
int capacity = source.capacity();
|
||||
if (end > source.capacity()) {
|
||||
throw new IllegalArgumentException("end > capacity: " + end + " > " + capacity);
|
||||
}
|
||||
int originalLimit = source.limit();
|
||||
int originalPosition = source.position();
|
||||
try {
|
||||
source.position(0);
|
||||
source.limit(end);
|
||||
source.position(start);
|
||||
ByteBuffer result = source.slice();
|
||||
result.order(source.order());
|
||||
return result;
|
||||
} finally {
|
||||
source.position(0);
|
||||
source.limit(originalLimit);
|
||||
source.position(originalPosition);
|
||||
}
|
||||
}
|
||||
|
||||
private static int getUnsignedInt8(ByteBuffer buffer) {
|
||||
return buffer.get() & 0xff;
|
||||
}
|
||||
|
||||
private static int getUnsignedInt16(ByteBuffer buffer) {
|
||||
return buffer.getShort() & 0xffff;
|
||||
}
|
||||
|
||||
private static long getUnsignedInt32(ByteBuffer buffer) {
|
||||
return buffer.getInt() & 0xffffffffL;
|
||||
}
|
||||
|
||||
private static long getUnsignedInt32(ByteBuffer buffer, int position) {
|
||||
return buffer.getInt(position) & 0xffffffffL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates that an error occurred while parsing a document.
|
||||
*/
|
||||
public static class XmlParserException extends Exception {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public XmlParserException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public XmlParserException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.apk;
|
||||
|
||||
import com.android.apksig.ApkVerificationIssue;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Base implementation of an APK signature verification result.
|
||||
*/
|
||||
public class ApkSigResult {
|
||||
public final int signatureSchemeVersion;
|
||||
|
||||
/** Whether the APK's Signature Scheme signature verifies. */
|
||||
public boolean verified;
|
||||
|
||||
public final List<ApkSignerInfo> mSigners = new ArrayList<>();
|
||||
private final List<ApkVerificationIssue> mWarnings = new ArrayList<>();
|
||||
private final List<ApkVerificationIssue> mErrors = new ArrayList<>();
|
||||
|
||||
public ApkSigResult(int signatureSchemeVersion) {
|
||||
this.signatureSchemeVersion = signatureSchemeVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if this result encountered errors during verification.
|
||||
*/
|
||||
public boolean containsErrors() {
|
||||
if (!mErrors.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
if (!mSigners.isEmpty()) {
|
||||
for (ApkSignerInfo signer : mSigners) {
|
||||
if (signer.containsErrors()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if this result encountered warnings during verification.
|
||||
*/
|
||||
public boolean containsWarnings() {
|
||||
if (!mWarnings.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
if (!mSigners.isEmpty()) {
|
||||
for (ApkSignerInfo signer : mSigners) {
|
||||
if (signer.containsWarnings()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new {@link ApkVerificationIssue} as an error to this result using the provided {@code
|
||||
* issueId} and {@code params}.
|
||||
*/
|
||||
public void addError(int issueId, Object... parameters) {
|
||||
mErrors.add(new ApkVerificationIssue(issueId, parameters));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new {@link ApkVerificationIssue} as a warning to this result using the provided {@code
|
||||
* issueId} and {@code params}.
|
||||
*/
|
||||
public void addWarning(int issueId, Object... parameters) {
|
||||
mWarnings.add(new ApkVerificationIssue(issueId, parameters));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the errors encountered during verification.
|
||||
*/
|
||||
public List<? extends ApkVerificationIssue> getErrors() {
|
||||
return mErrors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the warnings encountered during verification.
|
||||
*/
|
||||
public List<? extends ApkVerificationIssue> getWarnings() {
|
||||
return mWarnings;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.apk;
|
||||
|
||||
import com.android.apksig.ApkVerificationIssue;
|
||||
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Base implementation of an APK signer.
|
||||
*/
|
||||
public class ApkSignerInfo {
|
||||
public int index;
|
||||
public long timestamp;
|
||||
public List<X509Certificate> certs = new ArrayList<>();
|
||||
public List<X509Certificate> certificateLineage = new ArrayList<>();
|
||||
|
||||
private final List<ApkVerificationIssue> mInfoMessages = new ArrayList<>();
|
||||
private final List<ApkVerificationIssue> mWarnings = new ArrayList<>();
|
||||
private final List<ApkVerificationIssue> mErrors = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Adds a new {@link ApkVerificationIssue} as an error to this signer using the provided {@code
|
||||
* issueId} and {@code params}.
|
||||
*/
|
||||
public void addError(int issueId, Object... params) {
|
||||
mErrors.add(new ApkVerificationIssue(issueId, params));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new {@link ApkVerificationIssue} as a warning to this signer using the provided {@code
|
||||
* issueId} and {@code params}.
|
||||
*/
|
||||
public void addWarning(int issueId, Object... params) {
|
||||
mWarnings.add(new ApkVerificationIssue(issueId, params));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new {@link ApkVerificationIssue} as an info message to this signer config using the
|
||||
* provided {@code issueId} and {@code params}.
|
||||
*/
|
||||
public void addInfoMessage(int issueId, Object... params) {
|
||||
mInfoMessages.add(new ApkVerificationIssue(issueId, params));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if any errors were encountered during verification for this signer.
|
||||
*/
|
||||
public boolean containsErrors() {
|
||||
return !mErrors.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if any warnings were encountered during verification for this signer.
|
||||
*/
|
||||
public boolean containsWarnings() {
|
||||
return !mWarnings.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if any info messages were encountered during verification of this
|
||||
* signer.
|
||||
*/
|
||||
public boolean containsInfoMessages() {
|
||||
return !mInfoMessages.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the errors encountered during verification for this signer.
|
||||
*/
|
||||
public List<? extends ApkVerificationIssue> getErrors() {
|
||||
return mErrors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the warnings encountered during verification for this signer.
|
||||
*/
|
||||
public List<? extends ApkVerificationIssue> getWarnings() {
|
||||
return mWarnings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the info messages encountered during verification of this signer.
|
||||
*/
|
||||
public List<? extends ApkVerificationIssue> getInfoMessages() {
|
||||
return mInfoMessages;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,393 @@
|
||||
/*
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.apk;
|
||||
|
||||
import com.android.apksig.apk.ApkFormatException;
|
||||
import com.android.apksig.apk.ApkSigningBlockNotFoundException;
|
||||
import com.android.apksig.apk.ApkUtilsLite;
|
||||
import com.android.apksig.internal.util.Pair;
|
||||
import com.android.apksig.util.DataSource;
|
||||
import com.android.apksig.zip.ZipSections;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.BufferUnderflowException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Lightweight version of the ApkSigningBlockUtils for clients that only require a subset of the
|
||||
* utility functionality.
|
||||
*/
|
||||
public class ApkSigningBlockUtilsLite {
|
||||
private ApkSigningBlockUtilsLite() {}
|
||||
|
||||
private static final char[] HEX_DIGITS = "0123456789abcdef".toCharArray();
|
||||
/**
|
||||
* Returns the APK Signature Scheme block contained in the provided APK file for the given ID
|
||||
* and the additional information relevant for verifying the block against the file.
|
||||
*
|
||||
* @param blockId the ID value in the APK Signing Block's sequence of ID-value pairs
|
||||
* identifying the appropriate block to find, e.g. the APK Signature Scheme v2
|
||||
* block ID.
|
||||
*
|
||||
* @throws SignatureNotFoundException if the APK is not signed using given APK Signature Scheme
|
||||
* @throws IOException if an I/O error occurs while reading the APK
|
||||
*/
|
||||
public static SignatureInfo findSignature(
|
||||
DataSource apk, ZipSections zipSections, int blockId)
|
||||
throws IOException, SignatureNotFoundException {
|
||||
// Find the APK Signing Block.
|
||||
DataSource apkSigningBlock;
|
||||
long apkSigningBlockOffset;
|
||||
try {
|
||||
ApkUtilsLite.ApkSigningBlock apkSigningBlockInfo =
|
||||
ApkUtilsLite.findApkSigningBlock(apk, zipSections);
|
||||
apkSigningBlockOffset = apkSigningBlockInfo.getStartOffset();
|
||||
apkSigningBlock = apkSigningBlockInfo.getContents();
|
||||
} catch (ApkSigningBlockNotFoundException e) {
|
||||
throw new SignatureNotFoundException(e.getMessage(), e);
|
||||
}
|
||||
ByteBuffer apkSigningBlockBuf =
|
||||
apkSigningBlock.getByteBuffer(0, (int) apkSigningBlock.size());
|
||||
apkSigningBlockBuf.order(ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
// Find the APK Signature Scheme Block inside the APK Signing Block.
|
||||
ByteBuffer apkSignatureSchemeBlock =
|
||||
findApkSignatureSchemeBlock(apkSigningBlockBuf, blockId);
|
||||
return new SignatureInfo(
|
||||
apkSignatureSchemeBlock,
|
||||
apkSigningBlockOffset,
|
||||
zipSections.getZipCentralDirectoryOffset(),
|
||||
zipSections.getZipEndOfCentralDirectoryOffset(),
|
||||
zipSections.getZipEndOfCentralDirectory());
|
||||
}
|
||||
|
||||
public static ByteBuffer findApkSignatureSchemeBlock(
|
||||
ByteBuffer apkSigningBlock,
|
||||
int blockId) throws SignatureNotFoundException {
|
||||
checkByteOrderLittleEndian(apkSigningBlock);
|
||||
// FORMAT:
|
||||
// OFFSET DATA TYPE DESCRIPTION
|
||||
// * @+0 bytes uint64: size in bytes (excluding this field)
|
||||
// * @+8 bytes pairs
|
||||
// * @-24 bytes uint64: size in bytes (same as the one above)
|
||||
// * @-16 bytes uint128: magic
|
||||
ByteBuffer pairs = sliceFromTo(apkSigningBlock, 8, apkSigningBlock.capacity() - 24);
|
||||
|
||||
int entryCount = 0;
|
||||
while (pairs.hasRemaining()) {
|
||||
entryCount++;
|
||||
if (pairs.remaining() < 8) {
|
||||
throw new SignatureNotFoundException(
|
||||
"Insufficient data to read size of APK Signing Block entry #" + entryCount);
|
||||
}
|
||||
long lenLong = pairs.getLong();
|
||||
if ((lenLong < 4) || (lenLong > Integer.MAX_VALUE)) {
|
||||
throw new SignatureNotFoundException(
|
||||
"APK Signing Block entry #" + entryCount
|
||||
+ " size out of range: " + lenLong);
|
||||
}
|
||||
int len = (int) lenLong;
|
||||
int nextEntryPos = pairs.position() + len;
|
||||
if (len > pairs.remaining()) {
|
||||
throw new SignatureNotFoundException(
|
||||
"APK Signing Block entry #" + entryCount + " size out of range: " + len
|
||||
+ ", available: " + pairs.remaining());
|
||||
}
|
||||
int id = pairs.getInt();
|
||||
if (id == blockId) {
|
||||
return getByteBuffer(pairs, len - 4);
|
||||
}
|
||||
pairs.position(nextEntryPos);
|
||||
}
|
||||
|
||||
throw new SignatureNotFoundException(
|
||||
"No APK Signature Scheme block in APK Signing Block with ID: " + blockId);
|
||||
}
|
||||
|
||||
public static void checkByteOrderLittleEndian(ByteBuffer buffer) {
|
||||
if (buffer.order() != ByteOrder.LITTLE_ENDIAN) {
|
||||
throw new IllegalArgumentException("ByteBuffer byte order must be little endian");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the subset of signatures which are expected to be verified by at least one Android
|
||||
* platform version in the {@code [minSdkVersion, maxSdkVersion]} range. The returned result is
|
||||
* guaranteed to contain at least one signature.
|
||||
*
|
||||
* <p>Each Android platform version typically verifies exactly one signature from the provided
|
||||
* {@code signatures} set. This method returns the set of these signatures collected over all
|
||||
* requested platform versions. As a result, the result may contain more than one signature.
|
||||
*
|
||||
* @throws NoApkSupportedSignaturesException if no supported signatures were
|
||||
* found for an Android platform version in the range.
|
||||
*/
|
||||
public static <T extends ApkSupportedSignature> List<T> getSignaturesToVerify(
|
||||
List<T> signatures, int minSdkVersion, int maxSdkVersion)
|
||||
throws NoApkSupportedSignaturesException {
|
||||
return getSignaturesToVerify(signatures, minSdkVersion, maxSdkVersion, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the subset of signatures which are expected to be verified by at least one Android
|
||||
* platform version in the {@code [minSdkVersion, maxSdkVersion]} range. The returned result is
|
||||
* guaranteed to contain at least one signature.
|
||||
*
|
||||
* <p>{@code onlyRequireJcaSupport} can be set to true for cases that only require verifying a
|
||||
* signature within the signing block using the standard JCA.
|
||||
*
|
||||
* <p>Each Android platform version typically verifies exactly one signature from the provided
|
||||
* {@code signatures} set. This method returns the set of these signatures collected over all
|
||||
* requested platform versions. As a result, the result may contain more than one signature.
|
||||
*
|
||||
* @throws NoApkSupportedSignaturesException if no supported signatures were
|
||||
* found for an Android platform version in the range.
|
||||
*/
|
||||
public static <T extends ApkSupportedSignature> List<T> getSignaturesToVerify(
|
||||
List<T> signatures, int minSdkVersion, int maxSdkVersion,
|
||||
boolean onlyRequireJcaSupport) throws
|
||||
NoApkSupportedSignaturesException {
|
||||
// Pick the signature with the strongest algorithm at all required SDK versions, to mimic
|
||||
// Android's behavior on those versions.
|
||||
//
|
||||
// Here we assume that, once introduced, a signature algorithm continues to be supported in
|
||||
// all future Android versions. We also assume that the better-than relationship between
|
||||
// algorithms is exactly the same on all Android platform versions (except that older
|
||||
// platforms might support fewer algorithms). If these assumption are no longer true, the
|
||||
// logic here will need to change accordingly.
|
||||
Map<Integer, T>
|
||||
bestSigAlgorithmOnSdkVersion = new HashMap<>();
|
||||
int minProvidedSignaturesVersion = Integer.MAX_VALUE;
|
||||
for (T sig : signatures) {
|
||||
SignatureAlgorithm sigAlgorithm = sig.algorithm;
|
||||
int sigMinSdkVersion = onlyRequireJcaSupport ? sigAlgorithm.getJcaSigAlgMinSdkVersion()
|
||||
: sigAlgorithm.getMinSdkVersion();
|
||||
if (sigMinSdkVersion > maxSdkVersion) {
|
||||
continue;
|
||||
}
|
||||
if (sigMinSdkVersion < minProvidedSignaturesVersion) {
|
||||
minProvidedSignaturesVersion = sigMinSdkVersion;
|
||||
}
|
||||
|
||||
T candidate = bestSigAlgorithmOnSdkVersion.get(sigMinSdkVersion);
|
||||
if ((candidate == null)
|
||||
|| (compareSignatureAlgorithm(
|
||||
sigAlgorithm, candidate.algorithm) > 0)) {
|
||||
bestSigAlgorithmOnSdkVersion.put(sigMinSdkVersion, sig);
|
||||
}
|
||||
}
|
||||
|
||||
// Must have some supported signature algorithms for minSdkVersion.
|
||||
if (minSdkVersion < minProvidedSignaturesVersion) {
|
||||
throw new NoApkSupportedSignaturesException(
|
||||
"Minimum provided signature version " + minProvidedSignaturesVersion +
|
||||
" > minSdkVersion " + minSdkVersion);
|
||||
}
|
||||
if (bestSigAlgorithmOnSdkVersion.isEmpty()) {
|
||||
throw new NoApkSupportedSignaturesException("No supported signature");
|
||||
}
|
||||
List<T> signaturesToVerify =
|
||||
new ArrayList<>(bestSigAlgorithmOnSdkVersion.values());
|
||||
Collections.sort(
|
||||
signaturesToVerify,
|
||||
(sig1, sig2) -> Integer.compare(sig1.algorithm.getId(), sig2.algorithm.getId()));
|
||||
return signaturesToVerify;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns positive number if {@code alg1} is preferred over {@code alg2}, {@code -1} if
|
||||
* {@code alg2} is preferred over {@code alg1}, and {@code 0} if there is no preference.
|
||||
*/
|
||||
public static int compareSignatureAlgorithm(SignatureAlgorithm alg1, SignatureAlgorithm alg2) {
|
||||
ContentDigestAlgorithm digestAlg1 = alg1.getContentDigestAlgorithm();
|
||||
ContentDigestAlgorithm digestAlg2 = alg2.getContentDigestAlgorithm();
|
||||
return compareContentDigestAlgorithm(digestAlg1, digestAlg2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a positive number if {@code alg1} is preferred over {@code alg2}, a negative number
|
||||
* if {@code alg2} is preferred over {@code alg1}, or {@code 0} if there is no preference.
|
||||
*/
|
||||
private static int compareContentDigestAlgorithm(
|
||||
ContentDigestAlgorithm alg1,
|
||||
ContentDigestAlgorithm alg2) {
|
||||
switch (alg1) {
|
||||
case CHUNKED_SHA256:
|
||||
switch (alg2) {
|
||||
case CHUNKED_SHA256:
|
||||
return 0;
|
||||
case CHUNKED_SHA512:
|
||||
case VERITY_CHUNKED_SHA256:
|
||||
return -1;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown alg2: " + alg2);
|
||||
}
|
||||
case CHUNKED_SHA512:
|
||||
switch (alg2) {
|
||||
case CHUNKED_SHA256:
|
||||
case VERITY_CHUNKED_SHA256:
|
||||
return 1;
|
||||
case CHUNKED_SHA512:
|
||||
return 0;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown alg2: " + alg2);
|
||||
}
|
||||
case VERITY_CHUNKED_SHA256:
|
||||
switch (alg2) {
|
||||
case CHUNKED_SHA256:
|
||||
return 1;
|
||||
case VERITY_CHUNKED_SHA256:
|
||||
return 0;
|
||||
case CHUNKED_SHA512:
|
||||
return -1;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown alg2: " + alg2);
|
||||
}
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown alg1: " + alg1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns new byte buffer whose content is a shared subsequence of this buffer's content
|
||||
* between the specified start (inclusive) and end (exclusive) positions. As opposed to
|
||||
* {@link ByteBuffer#slice()}, the returned buffer's byte order is the same as the source
|
||||
* buffer's byte order.
|
||||
*/
|
||||
private static ByteBuffer sliceFromTo(ByteBuffer source, int start, int end) {
|
||||
if (start < 0) {
|
||||
throw new IllegalArgumentException("start: " + start);
|
||||
}
|
||||
if (end < start) {
|
||||
throw new IllegalArgumentException("end < start: " + end + " < " + start);
|
||||
}
|
||||
int capacity = source.capacity();
|
||||
if (end > source.capacity()) {
|
||||
throw new IllegalArgumentException("end > capacity: " + end + " > " + capacity);
|
||||
}
|
||||
int originalLimit = source.limit();
|
||||
int originalPosition = source.position();
|
||||
try {
|
||||
source.position(0);
|
||||
source.limit(end);
|
||||
source.position(start);
|
||||
ByteBuffer result = source.slice();
|
||||
result.order(source.order());
|
||||
return result;
|
||||
} finally {
|
||||
source.position(0);
|
||||
source.limit(originalLimit);
|
||||
source.position(originalPosition);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Relative <em>get</em> method for reading {@code size} number of bytes from the current
|
||||
* position of this buffer.
|
||||
*
|
||||
* <p>This method reads the next {@code size} bytes at this buffer's current position,
|
||||
* returning them as a {@code ByteBuffer} with start set to 0, limit and capacity set to
|
||||
* {@code size}, byte order set to this buffer's byte order; and then increments the position by
|
||||
* {@code size}.
|
||||
*/
|
||||
private static ByteBuffer getByteBuffer(ByteBuffer source, int size) {
|
||||
if (size < 0) {
|
||||
throw new IllegalArgumentException("size: " + size);
|
||||
}
|
||||
int originalLimit = source.limit();
|
||||
int position = source.position();
|
||||
int limit = position + size;
|
||||
if ((limit < position) || (limit > originalLimit)) {
|
||||
throw new BufferUnderflowException();
|
||||
}
|
||||
source.limit(limit);
|
||||
try {
|
||||
ByteBuffer result = source.slice();
|
||||
result.order(source.order());
|
||||
source.position(limit);
|
||||
return result;
|
||||
} finally {
|
||||
source.limit(originalLimit);
|
||||
}
|
||||
}
|
||||
|
||||
public static String toHex(byte[] value) {
|
||||
StringBuilder sb = new StringBuilder(value.length * 2);
|
||||
int len = value.length;
|
||||
for (int i = 0; i < len; i++) {
|
||||
int hi = (value[i] & 0xff) >>> 4;
|
||||
int lo = value[i] & 0x0f;
|
||||
sb.append(HEX_DIGITS[hi]).append(HEX_DIGITS[lo]);
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public static ByteBuffer getLengthPrefixedSlice(ByteBuffer source) throws ApkFormatException {
|
||||
if (source.remaining() < 4) {
|
||||
throw new ApkFormatException(
|
||||
"Remaining buffer too short to contain length of length-prefixed field"
|
||||
+ ". Remaining: " + source.remaining());
|
||||
}
|
||||
int len = source.getInt();
|
||||
if (len < 0) {
|
||||
throw new IllegalArgumentException("Negative length");
|
||||
} else if (len > source.remaining()) {
|
||||
throw new ApkFormatException(
|
||||
"Length-prefixed field longer than remaining buffer"
|
||||
+ ". Field length: " + len + ", remaining: " + source.remaining());
|
||||
}
|
||||
return getByteBuffer(source, len);
|
||||
}
|
||||
|
||||
public static byte[] readLengthPrefixedByteArray(ByteBuffer buf) throws ApkFormatException {
|
||||
int len = buf.getInt();
|
||||
if (len < 0) {
|
||||
throw new ApkFormatException("Negative length");
|
||||
} else if (len > buf.remaining()) {
|
||||
throw new ApkFormatException(
|
||||
"Underflow while reading length-prefixed value. Length: " + len
|
||||
+ ", available: " + buf.remaining());
|
||||
}
|
||||
byte[] result = new byte[len];
|
||||
buf.get(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static byte[] encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
|
||||
List<Pair<Integer, byte[]>> sequence) {
|
||||
int resultSize = 0;
|
||||
for (Pair<Integer, byte[]> element : sequence) {
|
||||
resultSize += 12 + element.getSecond().length;
|
||||
}
|
||||
ByteBuffer result = ByteBuffer.allocate(resultSize);
|
||||
result.order(ByteOrder.LITTLE_ENDIAN);
|
||||
for (Pair<Integer, byte[]> element : sequence) {
|
||||
byte[] second = element.getSecond();
|
||||
result.putInt(8 + second.length);
|
||||
result.putInt(element.getFirst());
|
||||
result.putInt(second.length);
|
||||
result.put(second);
|
||||
}
|
||||
return result.array();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.apk;
|
||||
|
||||
/**
|
||||
* Base implementation of a supported signature for an APK.
|
||||
*/
|
||||
public class ApkSupportedSignature {
|
||||
public final SignatureAlgorithm algorithm;
|
||||
public final byte[] signature;
|
||||
|
||||
/**
|
||||
* Constructs a new supported signature using the provided {@code algorithm} and {@code
|
||||
* signature} bytes.
|
||||
*/
|
||||
public ApkSupportedSignature(SignatureAlgorithm algorithm, byte[] signature) {
|
||||
this.algorithm = algorithm;
|
||||
this.signature = signature;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.apk;
|
||||
|
||||
/** APK Signature Scheme v2 content digest algorithm. */
|
||||
public enum ContentDigestAlgorithm {
|
||||
/** SHA2-256 over 1 MB chunks. */
|
||||
CHUNKED_SHA256(1, "SHA-256", 256 / 8),
|
||||
|
||||
/** SHA2-512 over 1 MB chunks. */
|
||||
CHUNKED_SHA512(2, "SHA-512", 512 / 8),
|
||||
|
||||
/** SHA2-256 over 4 KB chunks for APK verity. */
|
||||
VERITY_CHUNKED_SHA256(3, "SHA-256", 256 / 8),
|
||||
|
||||
/** Non-chunk SHA2-256. */
|
||||
SHA256(4, "SHA-256", 256 / 8);
|
||||
|
||||
private final int mId;
|
||||
private final String mJcaMessageDigestAlgorithm;
|
||||
private final int mChunkDigestOutputSizeBytes;
|
||||
|
||||
private ContentDigestAlgorithm(
|
||||
int id, String jcaMessageDigestAlgorithm, int chunkDigestOutputSizeBytes) {
|
||||
mId = id;
|
||||
mJcaMessageDigestAlgorithm = jcaMessageDigestAlgorithm;
|
||||
mChunkDigestOutputSizeBytes = chunkDigestOutputSizeBytes;
|
||||
}
|
||||
|
||||
/** Returns the ID of the digest algorithm used on the APK. */
|
||||
public int getId() {
|
||||
return mId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link java.security.MessageDigest} algorithm used for computing digests of
|
||||
* chunks by this content digest algorithm.
|
||||
*/
|
||||
String getJcaMessageDigestAlgorithm() {
|
||||
return mJcaMessageDigestAlgorithm;
|
||||
}
|
||||
|
||||
/** Returns the size (in bytes) of the digest of a chunk of content. */
|
||||
int getChunkDigestOutputSizeBytes() {
|
||||
return mChunkDigestOutputSizeBytes;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.apk;
|
||||
|
||||
/**
|
||||
* Base exception that is thrown when there are no signatures that support the full range of
|
||||
* requested platform versions.
|
||||
*/
|
||||
public class NoApkSupportedSignaturesException extends Exception {
|
||||
public NoApkSupportedSignaturesException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.apk;
|
||||
|
||||
import com.android.apksig.internal.util.AndroidSdkVersion;
|
||||
import com.android.apksig.internal.util.Pair;
|
||||
import java.security.spec.AlgorithmParameterSpec;
|
||||
import java.security.spec.MGF1ParameterSpec;
|
||||
import java.security.spec.PSSParameterSpec;
|
||||
|
||||
/**
|
||||
* APK Signing Block signature algorithm.
|
||||
*/
|
||||
public enum SignatureAlgorithm {
|
||||
// TODO reserve the 0x0000 ID to mean null
|
||||
/**
|
||||
* RSASSA-PSS with SHA2-256 digest, SHA2-256 MGF1, 32 bytes of salt, trailer: 0xbc, content
|
||||
* digested using SHA2-256 in 1 MB chunks.
|
||||
*/
|
||||
RSA_PSS_WITH_SHA256(
|
||||
0x0101,
|
||||
ContentDigestAlgorithm.CHUNKED_SHA256,
|
||||
"RSA",
|
||||
Pair.of("SHA256withRSA/PSS",
|
||||
new PSSParameterSpec(
|
||||
"SHA-256", "MGF1", MGF1ParameterSpec.SHA256, 256 / 8, 1)),
|
||||
AndroidSdkVersion.N,
|
||||
AndroidSdkVersion.M),
|
||||
|
||||
/**
|
||||
* RSASSA-PSS with SHA2-512 digest, SHA2-512 MGF1, 64 bytes of salt, trailer: 0xbc, content
|
||||
* digested using SHA2-512 in 1 MB chunks.
|
||||
*/
|
||||
RSA_PSS_WITH_SHA512(
|
||||
0x0102,
|
||||
ContentDigestAlgorithm.CHUNKED_SHA512,
|
||||
"RSA",
|
||||
Pair.of(
|
||||
"SHA512withRSA/PSS",
|
||||
new PSSParameterSpec(
|
||||
"SHA-512", "MGF1", MGF1ParameterSpec.SHA512, 512 / 8, 1)),
|
||||
AndroidSdkVersion.N,
|
||||
AndroidSdkVersion.M),
|
||||
|
||||
/** RSASSA-PKCS1-v1_5 with SHA2-256 digest, content digested using SHA2-256 in 1 MB chunks. */
|
||||
RSA_PKCS1_V1_5_WITH_SHA256(
|
||||
0x0103,
|
||||
ContentDigestAlgorithm.CHUNKED_SHA256,
|
||||
"RSA",
|
||||
Pair.of("SHA256withRSA", null),
|
||||
AndroidSdkVersion.N,
|
||||
AndroidSdkVersion.INITIAL_RELEASE),
|
||||
|
||||
/** RSASSA-PKCS1-v1_5 with SHA2-512 digest, content digested using SHA2-512 in 1 MB chunks. */
|
||||
RSA_PKCS1_V1_5_WITH_SHA512(
|
||||
0x0104,
|
||||
ContentDigestAlgorithm.CHUNKED_SHA512,
|
||||
"RSA",
|
||||
Pair.of("SHA512withRSA", null),
|
||||
AndroidSdkVersion.N,
|
||||
AndroidSdkVersion.INITIAL_RELEASE),
|
||||
|
||||
/** ECDSA with SHA2-256 digest, content digested using SHA2-256 in 1 MB chunks. */
|
||||
ECDSA_WITH_SHA256(
|
||||
0x0201,
|
||||
ContentDigestAlgorithm.CHUNKED_SHA256,
|
||||
"EC",
|
||||
Pair.of("SHA256withECDSA", null),
|
||||
AndroidSdkVersion.N,
|
||||
AndroidSdkVersion.HONEYCOMB),
|
||||
|
||||
/** ECDSA with SHA2-512 digest, content digested using SHA2-512 in 1 MB chunks. */
|
||||
ECDSA_WITH_SHA512(
|
||||
0x0202,
|
||||
ContentDigestAlgorithm.CHUNKED_SHA512,
|
||||
"EC",
|
||||
Pair.of("SHA512withECDSA", null),
|
||||
AndroidSdkVersion.N,
|
||||
AndroidSdkVersion.HONEYCOMB),
|
||||
|
||||
/** DSA with SHA2-256 digest, content digested using SHA2-256 in 1 MB chunks. */
|
||||
DSA_WITH_SHA256(
|
||||
0x0301,
|
||||
ContentDigestAlgorithm.CHUNKED_SHA256,
|
||||
"DSA",
|
||||
Pair.of("SHA256withDSA", null),
|
||||
AndroidSdkVersion.N,
|
||||
AndroidSdkVersion.INITIAL_RELEASE),
|
||||
|
||||
/**
|
||||
* DSA with SHA2-256 digest, content digested using SHA2-256 in 1 MB chunks. Signing is done
|
||||
* deterministically according to RFC 6979.
|
||||
*/
|
||||
DETDSA_WITH_SHA256(
|
||||
0x0301,
|
||||
ContentDigestAlgorithm.CHUNKED_SHA256,
|
||||
"DSA",
|
||||
Pair.of("SHA256withDetDSA", null),
|
||||
AndroidSdkVersion.N,
|
||||
AndroidSdkVersion.INITIAL_RELEASE),
|
||||
|
||||
/**
|
||||
* RSASSA-PKCS1-v1_5 with SHA2-256 digest, content digested using SHA2-256 in 4 KB chunks, in
|
||||
* the same way fsverity operates. This digest and the content length (before digestion, 8 bytes
|
||||
* in little endian) construct the final digest.
|
||||
*/
|
||||
VERITY_RSA_PKCS1_V1_5_WITH_SHA256(
|
||||
0x0421,
|
||||
ContentDigestAlgorithm.VERITY_CHUNKED_SHA256,
|
||||
"RSA",
|
||||
Pair.of("SHA256withRSA", null),
|
||||
AndroidSdkVersion.P,
|
||||
AndroidSdkVersion.INITIAL_RELEASE),
|
||||
|
||||
/**
|
||||
* ECDSA with SHA2-256 digest, content digested using SHA2-256 in 4 KB chunks, in the same way
|
||||
* fsverity operates. This digest and the content length (before digestion, 8 bytes in little
|
||||
* endian) construct the final digest.
|
||||
*/
|
||||
VERITY_ECDSA_WITH_SHA256(
|
||||
0x0423,
|
||||
ContentDigestAlgorithm.VERITY_CHUNKED_SHA256,
|
||||
"EC",
|
||||
Pair.of("SHA256withECDSA", null),
|
||||
AndroidSdkVersion.P,
|
||||
AndroidSdkVersion.HONEYCOMB),
|
||||
|
||||
/**
|
||||
* DSA with SHA2-256 digest, content digested using SHA2-256 in 4 KB chunks, in the same way
|
||||
* fsverity operates. This digest and the content length (before digestion, 8 bytes in little
|
||||
* endian) construct the final digest.
|
||||
*/
|
||||
VERITY_DSA_WITH_SHA256(
|
||||
0x0425,
|
||||
ContentDigestAlgorithm.VERITY_CHUNKED_SHA256,
|
||||
"DSA",
|
||||
Pair.of("SHA256withDSA", null),
|
||||
AndroidSdkVersion.P,
|
||||
AndroidSdkVersion.INITIAL_RELEASE);
|
||||
|
||||
private final int mId;
|
||||
private final String mJcaKeyAlgorithm;
|
||||
private final ContentDigestAlgorithm mContentDigestAlgorithm;
|
||||
private final Pair<String, ? extends AlgorithmParameterSpec> mJcaSignatureAlgAndParams;
|
||||
private final int mMinSdkVersion;
|
||||
private final int mJcaSigAlgMinSdkVersion;
|
||||
|
||||
SignatureAlgorithm(int id,
|
||||
ContentDigestAlgorithm contentDigestAlgorithm,
|
||||
String jcaKeyAlgorithm,
|
||||
Pair<String, ? extends AlgorithmParameterSpec> jcaSignatureAlgAndParams,
|
||||
int minSdkVersion,
|
||||
int jcaSigAlgMinSdkVersion) {
|
||||
mId = id;
|
||||
mContentDigestAlgorithm = contentDigestAlgorithm;
|
||||
mJcaKeyAlgorithm = jcaKeyAlgorithm;
|
||||
mJcaSignatureAlgAndParams = jcaSignatureAlgAndParams;
|
||||
mMinSdkVersion = minSdkVersion;
|
||||
mJcaSigAlgMinSdkVersion = jcaSigAlgMinSdkVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ID of this signature algorithm as used in APK Signature Scheme v2 wire format.
|
||||
*/
|
||||
public int getId() {
|
||||
return mId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the content digest algorithm associated with this signature algorithm.
|
||||
*/
|
||||
public ContentDigestAlgorithm getContentDigestAlgorithm() {
|
||||
return mContentDigestAlgorithm;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the JCA {@link java.security.Key} algorithm used by this signature scheme.
|
||||
*/
|
||||
public String getJcaKeyAlgorithm() {
|
||||
return mJcaKeyAlgorithm;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link java.security.Signature} algorithm and the {@link AlgorithmParameterSpec}
|
||||
* (or null if not needed) to parameterize the {@code Signature}.
|
||||
*/
|
||||
public Pair<String, ? extends AlgorithmParameterSpec> getJcaSignatureAlgorithmAndParams() {
|
||||
return mJcaSignatureAlgAndParams;
|
||||
}
|
||||
|
||||
public int getMinSdkVersion() {
|
||||
return mMinSdkVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the minimum SDK version that supports the JCA signature algorithm.
|
||||
*/
|
||||
public int getJcaSigAlgMinSdkVersion() {
|
||||
return mJcaSigAlgMinSdkVersion;
|
||||
}
|
||||
|
||||
public static SignatureAlgorithm findById(int id) {
|
||||
for (SignatureAlgorithm alg : SignatureAlgorithm.values()) {
|
||||
if (alg.getId() == id) {
|
||||
return alg;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright (C) 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.apk;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/**
|
||||
* APK Signature Scheme block and additional information relevant to verifying the signatures
|
||||
* contained in the block against the file.
|
||||
*/
|
||||
public class SignatureInfo {
|
||||
/** Contents of APK Signature Scheme block. */
|
||||
public final ByteBuffer signatureBlock;
|
||||
|
||||
/** Position of the APK Signing Block in the file. */
|
||||
public final long apkSigningBlockOffset;
|
||||
|
||||
/** Position of the ZIP Central Directory in the file. */
|
||||
public final long centralDirOffset;
|
||||
|
||||
/** Position of the ZIP End of Central Directory (EoCD) in the file. */
|
||||
public final long eocdOffset;
|
||||
|
||||
/** Contents of ZIP End of Central Directory (EoCD) of the file. */
|
||||
public final ByteBuffer eocd;
|
||||
|
||||
public SignatureInfo(
|
||||
ByteBuffer signatureBlock,
|
||||
long apkSigningBlockOffset,
|
||||
long centralDirOffset,
|
||||
long eocdOffset,
|
||||
ByteBuffer eocd) {
|
||||
this.signatureBlock = signatureBlock;
|
||||
this.apkSigningBlockOffset = apkSigningBlockOffset;
|
||||
this.centralDirOffset = centralDirOffset;
|
||||
this.eocdOffset = eocdOffset;
|
||||
this.eocd = eocd;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.apk;
|
||||
|
||||
/**
|
||||
* Base exception that is thrown when the APK is not signed with the requested signature scheme.
|
||||
*/
|
||||
public class SignatureNotFoundException extends Exception {
|
||||
public SignatureNotFoundException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public SignatureNotFoundException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
/*
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.apk.stamp;
|
||||
|
||||
import static com.android.apksig.internal.apk.ApkSigningBlockUtilsLite.getLengthPrefixedSlice;
|
||||
import static com.android.apksig.internal.apk.ApkSigningBlockUtilsLite.readLengthPrefixedByteArray;
|
||||
|
||||
import com.android.apksig.apk.ApkFormatException;
|
||||
import com.android.apksig.internal.apk.ApkSigningBlockUtilsLite;
|
||||
import com.android.apksig.internal.apk.SignatureAlgorithm;
|
||||
import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.BufferUnderflowException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PublicKey;
|
||||
import java.security.Signature;
|
||||
import java.security.SignatureException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.security.spec.AlgorithmParameterSpec;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
|
||||
/** Lightweight version of the V3SigningCertificateLineage to be used for source stamps. */
|
||||
public class SourceStampCertificateLineage {
|
||||
|
||||
private final static int FIRST_VERSION = 1;
|
||||
private final static int CURRENT_VERSION = FIRST_VERSION;
|
||||
|
||||
/**
|
||||
* Deserializes the binary representation of a SourceStampCertificateLineage. Also
|
||||
* verifies that the structure is well-formed, e.g. that the signature for each node is from its
|
||||
* parent.
|
||||
*/
|
||||
public static List<SigningCertificateNode> readSigningCertificateLineage(ByteBuffer inputBytes)
|
||||
throws IOException {
|
||||
List<SigningCertificateNode> result = new ArrayList<>();
|
||||
int nodeCount = 0;
|
||||
if (inputBytes == null || !inputBytes.hasRemaining()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
ApkSigningBlockUtilsLite.checkByteOrderLittleEndian(inputBytes);
|
||||
|
||||
CertificateFactory certFactory;
|
||||
try {
|
||||
certFactory = CertificateFactory.getInstance("X.509");
|
||||
} catch (CertificateException e) {
|
||||
throw new IllegalStateException("Failed to obtain X.509 CertificateFactory", e);
|
||||
}
|
||||
|
||||
// FORMAT (little endian):
|
||||
// * uint32: version code
|
||||
// * sequence of length-prefixed (uint32): nodes
|
||||
// * length-prefixed bytes: signed data
|
||||
// * length-prefixed bytes: certificate
|
||||
// * uint32: signature algorithm id
|
||||
// * uint32: flags
|
||||
// * uint32: signature algorithm id (used by to sign next cert in lineage)
|
||||
// * length-prefixed bytes: signature over above signed data
|
||||
|
||||
X509Certificate lastCert = null;
|
||||
int lastSigAlgorithmId = 0;
|
||||
|
||||
try {
|
||||
int version = inputBytes.getInt();
|
||||
if (version != CURRENT_VERSION) {
|
||||
// we only have one version to worry about right now, so just check it
|
||||
throw new IllegalArgumentException("Encoded SigningCertificateLineage has a version"
|
||||
+ " different than any of which we are aware");
|
||||
}
|
||||
HashSet<X509Certificate> certHistorySet = new HashSet<>();
|
||||
while (inputBytes.hasRemaining()) {
|
||||
nodeCount++;
|
||||
ByteBuffer nodeBytes = getLengthPrefixedSlice(inputBytes);
|
||||
ByteBuffer signedData = getLengthPrefixedSlice(nodeBytes);
|
||||
int flags = nodeBytes.getInt();
|
||||
int sigAlgorithmId = nodeBytes.getInt();
|
||||
SignatureAlgorithm sigAlgorithm = SignatureAlgorithm.findById(lastSigAlgorithmId);
|
||||
byte[] signature = readLengthPrefixedByteArray(nodeBytes);
|
||||
|
||||
if (lastCert != null) {
|
||||
// Use previous level cert to verify current level
|
||||
String jcaSignatureAlgorithm =
|
||||
sigAlgorithm.getJcaSignatureAlgorithmAndParams().getFirst();
|
||||
AlgorithmParameterSpec jcaSignatureAlgorithmParams =
|
||||
sigAlgorithm.getJcaSignatureAlgorithmAndParams().getSecond();
|
||||
PublicKey publicKey = lastCert.getPublicKey();
|
||||
Signature sig = Signature.getInstance(jcaSignatureAlgorithm);
|
||||
sig.initVerify(publicKey);
|
||||
if (jcaSignatureAlgorithmParams != null) {
|
||||
sig.setParameter(jcaSignatureAlgorithmParams);
|
||||
}
|
||||
sig.update(signedData);
|
||||
if (!sig.verify(signature)) {
|
||||
throw new SecurityException("Unable to verify signature of certificate #"
|
||||
+ nodeCount + " using " + jcaSignatureAlgorithm + " when verifying"
|
||||
+ " SourceStampCertificateLineage object");
|
||||
}
|
||||
}
|
||||
|
||||
signedData.rewind();
|
||||
byte[] encodedCert = readLengthPrefixedByteArray(signedData);
|
||||
int signedSigAlgorithm = signedData.getInt();
|
||||
if (lastCert != null && lastSigAlgorithmId != signedSigAlgorithm) {
|
||||
throw new SecurityException("Signing algorithm ID mismatch for certificate #"
|
||||
+ nodeBytes + " when verifying SourceStampCertificateLineage object");
|
||||
}
|
||||
lastCert = (X509Certificate) certFactory.generateCertificate(
|
||||
new ByteArrayInputStream(encodedCert));
|
||||
lastCert = new GuaranteedEncodedFormX509Certificate(lastCert, encodedCert);
|
||||
if (certHistorySet.contains(lastCert)) {
|
||||
throw new SecurityException("Encountered duplicate entries in "
|
||||
+ "SigningCertificateLineage at certificate #" + nodeCount + ". All "
|
||||
+ "signing certificates should be unique");
|
||||
}
|
||||
certHistorySet.add(lastCert);
|
||||
lastSigAlgorithmId = sigAlgorithmId;
|
||||
result.add(new SigningCertificateNode(
|
||||
lastCert, SignatureAlgorithm.findById(signedSigAlgorithm),
|
||||
SignatureAlgorithm.findById(sigAlgorithmId), signature, flags));
|
||||
}
|
||||
} catch(ApkFormatException | BufferUnderflowException e){
|
||||
throw new IOException("Failed to parse SourceStampCertificateLineage object", e);
|
||||
} catch(NoSuchAlgorithmException | InvalidKeyException
|
||||
| InvalidAlgorithmParameterException | SignatureException e){
|
||||
throw new SecurityException(
|
||||
"Failed to verify signature over signed data for certificate #" + nodeCount
|
||||
+ " when parsing SourceStampCertificateLineage object", e);
|
||||
} catch(CertificateException e){
|
||||
throw new SecurityException("Failed to decode certificate #" + nodeCount
|
||||
+ " when parsing SourceStampCertificateLineage object", e);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents one signing certificate in the SourceStampCertificateLineage, which
|
||||
* generally means it is/was used at some point to sign source stamps.
|
||||
*/
|
||||
public static class SigningCertificateNode {
|
||||
|
||||
public SigningCertificateNode(
|
||||
X509Certificate signingCert,
|
||||
SignatureAlgorithm parentSigAlgorithm,
|
||||
SignatureAlgorithm sigAlgorithm,
|
||||
byte[] signature,
|
||||
int flags) {
|
||||
this.signingCert = signingCert;
|
||||
this.parentSigAlgorithm = parentSigAlgorithm;
|
||||
this.sigAlgorithm = sigAlgorithm;
|
||||
this.signature = signature;
|
||||
this.flags = flags;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (!(o instanceof SigningCertificateNode)) return false;
|
||||
|
||||
SigningCertificateNode that = (SigningCertificateNode) o;
|
||||
if (!signingCert.equals(that.signingCert)) return false;
|
||||
if (parentSigAlgorithm != that.parentSigAlgorithm) return false;
|
||||
if (sigAlgorithm != that.sigAlgorithm) return false;
|
||||
if (!Arrays.equals(signature, that.signature)) return false;
|
||||
if (flags != that.flags) return false;
|
||||
|
||||
// we made it
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
int result = 1;
|
||||
result = prime * result + ((signingCert == null) ? 0 : signingCert.hashCode());
|
||||
result = prime * result +
|
||||
((parentSigAlgorithm == null) ? 0 : parentSigAlgorithm.hashCode());
|
||||
result = prime * result + ((sigAlgorithm == null) ? 0 : sigAlgorithm.hashCode());
|
||||
result = prime * result + Arrays.hashCode(signature);
|
||||
result = prime * result + flags;
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* the signing cert for this node. This is part of the data signed by the parent node.
|
||||
*/
|
||||
public final X509Certificate signingCert;
|
||||
|
||||
/**
|
||||
* the algorithm used by this node's parent to bless this data. Its ID value is part of
|
||||
* the data signed by the parent node. {@code null} for first node.
|
||||
*/
|
||||
public final SignatureAlgorithm parentSigAlgorithm;
|
||||
|
||||
/**
|
||||
* the algorithm used by this node to bless the next node's data. Its ID value is part
|
||||
* of the signed data of the next node. {@code null} for the last node.
|
||||
*/
|
||||
public SignatureAlgorithm sigAlgorithm;
|
||||
|
||||
/**
|
||||
* signature over the signed data (above). The signature is from this node's parent
|
||||
* signing certificate, which should correspond to the signing certificate used to sign an
|
||||
* APK before rotating to this one, and is formed using {@code signatureAlgorithm}.
|
||||
*/
|
||||
public final byte[] signature;
|
||||
|
||||
/**
|
||||
* the flags detailing how the platform should treat this signing cert
|
||||
*/
|
||||
public int flags;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.apk.stamp;
|
||||
|
||||
/** Constants used for source stamp signing and verification. */
|
||||
public class SourceStampConstants {
|
||||
private SourceStampConstants() {}
|
||||
|
||||
public static final int V1_SOURCE_STAMP_BLOCK_ID = 0x2b09189e;
|
||||
public static final int V2_SOURCE_STAMP_BLOCK_ID = 0x6dff800d;
|
||||
public static final String SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME = "stamp-cert-sha256";
|
||||
public static final int PROOF_OF_ROTATION_ATTR_ID = 0x9d6303f7;
|
||||
/**
|
||||
* The source stamp timestamp attribute value is an 8-byte little-endian encoded long
|
||||
* representing the epoch time in seconds when the stamp block was signed. The first 8 bytes
|
||||
* of the attribute value buffer will be used to read the timestamp, and any additional buffer
|
||||
* space will be ignored.
|
||||
*/
|
||||
public static final int STAMP_TIME_ATTR_ID = 0xe43c5946;
|
||||
}
|
||||
@@ -0,0 +1,364 @@
|
||||
/*
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.apksig.internal.apk.stamp;
|
||||
|
||||
import static com.android.apksig.internal.apk.ApkSigningBlockUtilsLite.getLengthPrefixedSlice;
|
||||
import static com.android.apksig.internal.apk.ApkSigningBlockUtilsLite.getSignaturesToVerify;
|
||||
import static com.android.apksig.internal.apk.ApkSigningBlockUtilsLite.readLengthPrefixedByteArray;
|
||||
import static com.android.apksig.internal.apk.ApkSigningBlockUtilsLite.toHex;
|
||||
|
||||
import com.android.apksig.ApkVerificationIssue;
|
||||
import com.android.apksig.Constants;
|
||||
import com.android.apksig.apk.ApkFormatException;
|
||||
import com.android.apksig.internal.apk.ApkSignerInfo;
|
||||
import com.android.apksig.internal.apk.ApkSupportedSignature;
|
||||
import com.android.apksig.internal.apk.NoApkSupportedSignaturesException;
|
||||
import com.android.apksig.internal.apk.SignatureAlgorithm;
|
||||
import com.android.apksig.internal.util.ByteBufferUtils;
|
||||
import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.nio.BufferUnderflowException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PublicKey;
|
||||
import java.security.Signature;
|
||||
import java.security.SignatureException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.security.spec.AlgorithmParameterSpec;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Source Stamp verifier.
|
||||
*
|
||||
* <p>SourceStamp improves traceability of apps with respect to unauthorized distribution.
|
||||
*
|
||||
* <p>The stamp is part of the APK that is protected by the signing block.
|
||||
*
|
||||
* <p>The APK contents hash is signed using the stamp key, and is saved as part of the signing
|
||||
* block.
|
||||
*/
|
||||
class SourceStampVerifier {
|
||||
/** Hidden constructor to prevent instantiation. */
|
||||
private SourceStampVerifier() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the SourceStamp block and populates the {@code result}.
|
||||
*
|
||||
* <p>This verifies signatures over digest provided.
|
||||
*
|
||||
* <p>This method adds one or more errors to the {@code result} if a verification error is
|
||||
* expected to be encountered on an Android platform version in the {@code [minSdkVersion,
|
||||
* maxSdkVersion]} range.
|
||||
*/
|
||||
public static void verifyV1SourceStamp(
|
||||
ByteBuffer sourceStampBlockData,
|
||||
CertificateFactory certFactory,
|
||||
ApkSignerInfo result,
|
||||
byte[] apkDigest,
|
||||
byte[] sourceStampCertificateDigest,
|
||||
int minSdkVersion,
|
||||
int maxSdkVersion)
|
||||
throws ApkFormatException, NoSuchAlgorithmException {
|
||||
X509Certificate sourceStampCertificate =
|
||||
verifySourceStampCertificate(
|
||||
sourceStampBlockData, certFactory, sourceStampCertificateDigest, result);
|
||||
if (result.containsWarnings() || result.containsErrors()) {
|
||||
return;
|
||||
}
|
||||
|
||||
ByteBuffer apkDigestSignatures = getLengthPrefixedSlice(sourceStampBlockData);
|
||||
verifySourceStampSignature(
|
||||
apkDigest,
|
||||
minSdkVersion,
|
||||
maxSdkVersion,
|
||||
sourceStampCertificate,
|
||||
apkDigestSignatures,
|
||||
result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the SourceStamp block and populates the {@code result}.
|
||||
*
|
||||
* <p>This verifies signatures over digest of multiple signature schemes provided.
|
||||
*
|
||||
* <p>This method adds one or more errors to the {@code result} if a verification error is
|
||||
* expected to be encountered on an Android platform version in the {@code [minSdkVersion,
|
||||
* maxSdkVersion]} range.
|
||||
*/
|
||||
public static void verifyV2SourceStamp(
|
||||
ByteBuffer sourceStampBlockData,
|
||||
CertificateFactory certFactory,
|
||||
ApkSignerInfo result,
|
||||
Map<Integer, byte[]> signatureSchemeApkDigests,
|
||||
byte[] sourceStampCertificateDigest,
|
||||
int minSdkVersion,
|
||||
int maxSdkVersion)
|
||||
throws ApkFormatException, NoSuchAlgorithmException {
|
||||
X509Certificate sourceStampCertificate =
|
||||
verifySourceStampCertificate(
|
||||
sourceStampBlockData, certFactory, sourceStampCertificateDigest, result);
|
||||
if (result.containsWarnings() || result.containsErrors()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse signed signature schemes block.
|
||||
ByteBuffer signedSignatureSchemes = getLengthPrefixedSlice(sourceStampBlockData);
|
||||
Map<Integer, ByteBuffer> signedSignatureSchemeData = new HashMap<>();
|
||||
while (signedSignatureSchemes.hasRemaining()) {
|
||||
ByteBuffer signedSignatureScheme = getLengthPrefixedSlice(signedSignatureSchemes);
|
||||
int signatureSchemeId = signedSignatureScheme.getInt();
|
||||
ByteBuffer apkDigestSignatures = getLengthPrefixedSlice(signedSignatureScheme);
|
||||
signedSignatureSchemeData.put(signatureSchemeId, apkDigestSignatures);
|
||||
}
|
||||
|
||||
for (Map.Entry<Integer, byte[]> signatureSchemeApkDigest :
|
||||
signatureSchemeApkDigests.entrySet()) {
|
||||
// TODO(b/192301300): Should the new v3.1 be included in the source stamp, or since a
|
||||
// v3.0 block must always be present with a v3.1 block is it sufficient to just use the
|
||||
// v3.0 block?
|
||||
if (signatureSchemeApkDigest.getKey()
|
||||
== Constants.VERSION_APK_SIGNATURE_SCHEME_V31) {
|
||||
continue;
|
||||
}
|
||||
if (!signedSignatureSchemeData.containsKey(signatureSchemeApkDigest.getKey())) {
|
||||
result.addWarning(ApkVerificationIssue.SOURCE_STAMP_NO_SIGNATURE);
|
||||
return;
|
||||
}
|
||||
verifySourceStampSignature(
|
||||
signatureSchemeApkDigest.getValue(),
|
||||
minSdkVersion,
|
||||
maxSdkVersion,
|
||||
sourceStampCertificate,
|
||||
signedSignatureSchemeData.get(signatureSchemeApkDigest.getKey()),
|
||||
result);
|
||||
if (result.containsWarnings() || result.containsErrors()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (sourceStampBlockData.hasRemaining()) {
|
||||
// The stamp block contains some additional attributes.
|
||||
ByteBuffer stampAttributeData = getLengthPrefixedSlice(sourceStampBlockData);
|
||||
ByteBuffer stampAttributeDataSignatures = getLengthPrefixedSlice(sourceStampBlockData);
|
||||
|
||||
byte[] stampAttributeBytes = new byte[stampAttributeData.remaining()];
|
||||
stampAttributeData.get(stampAttributeBytes);
|
||||
stampAttributeData.flip();
|
||||
|
||||
verifySourceStampSignature(stampAttributeBytes, minSdkVersion, maxSdkVersion,
|
||||
sourceStampCertificate, stampAttributeDataSignatures, result);
|
||||
if (result.containsErrors() || result.containsWarnings()) {
|
||||
return;
|
||||
}
|
||||
parseStampAttributes(stampAttributeData, sourceStampCertificate, result);
|
||||
}
|
||||
}
|
||||
|
||||
private static X509Certificate verifySourceStampCertificate(
|
||||
ByteBuffer sourceStampBlockData,
|
||||
CertificateFactory certFactory,
|
||||
byte[] sourceStampCertificateDigest,
|
||||
ApkSignerInfo result)
|
||||
throws NoSuchAlgorithmException, ApkFormatException {
|
||||
// Parse the SourceStamp certificate.
|
||||
byte[] sourceStampEncodedCertificate = readLengthPrefixedByteArray(sourceStampBlockData);
|
||||
X509Certificate sourceStampCertificate;
|
||||
try {
|
||||
sourceStampCertificate = (X509Certificate) certFactory.generateCertificate(
|
||||
new ByteArrayInputStream(sourceStampEncodedCertificate));
|
||||
} catch (CertificateException e) {
|
||||
result.addWarning(ApkVerificationIssue.SOURCE_STAMP_MALFORMED_CERTIFICATE, e);
|
||||
return null;
|
||||
}
|
||||
// Wrap the cert so that the result's getEncoded returns exactly the original encoded
|
||||
// form. Without this, getEncoded may return a different form from what was stored in
|
||||
// the signature. This is because some X509Certificate(Factory) implementations
|
||||
// re-encode certificates.
|
||||
sourceStampCertificate =
|
||||
new GuaranteedEncodedFormX509Certificate(
|
||||
sourceStampCertificate, sourceStampEncodedCertificate);
|
||||
result.certs.add(sourceStampCertificate);
|
||||
// Verify the SourceStamp certificate found in the signing block is the same as the
|
||||
// SourceStamp certificate found in the APK.
|
||||
MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
|
||||
messageDigest.update(sourceStampEncodedCertificate);
|
||||
byte[] sourceStampBlockCertificateDigest = messageDigest.digest();
|
||||
if (!Arrays.equals(sourceStampCertificateDigest, sourceStampBlockCertificateDigest)) {
|
||||
result.addWarning(
|
||||
ApkVerificationIssue
|
||||
.SOURCE_STAMP_CERTIFICATE_MISMATCH_BETWEEN_SIGNATURE_BLOCK_AND_APK,
|
||||
toHex(sourceStampBlockCertificateDigest),
|
||||
toHex(sourceStampCertificateDigest));
|
||||
return null;
|
||||
}
|
||||
return sourceStampCertificate;
|
||||
}
|
||||
|
||||
private static void verifySourceStampSignature(
|
||||
byte[] data,
|
||||
int minSdkVersion,
|
||||
int maxSdkVersion,
|
||||
X509Certificate sourceStampCertificate,
|
||||
ByteBuffer signatures,
|
||||
ApkSignerInfo result) {
|
||||
// Parse the signatures block and identify supported signatures
|
||||
int signatureCount = 0;
|
||||
List<ApkSupportedSignature> supportedSignatures = new ArrayList<>(1);
|
||||
while (signatures.hasRemaining()) {
|
||||
signatureCount++;
|
||||
try {
|
||||
ByteBuffer signature = getLengthPrefixedSlice(signatures);
|
||||
int sigAlgorithmId = signature.getInt();
|
||||
byte[] sigBytes = readLengthPrefixedByteArray(signature);
|
||||
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.findById(sigAlgorithmId);
|
||||
if (signatureAlgorithm == null) {
|
||||
result.addInfoMessage(
|
||||
ApkVerificationIssue.SOURCE_STAMP_UNKNOWN_SIG_ALGORITHM,
|
||||
sigAlgorithmId);
|
||||
continue;
|
||||
}
|
||||
supportedSignatures.add(
|
||||
new ApkSupportedSignature(signatureAlgorithm, sigBytes));
|
||||
} catch (ApkFormatException | BufferUnderflowException e) {
|
||||
result.addWarning(
|
||||
ApkVerificationIssue.SOURCE_STAMP_MALFORMED_SIGNATURE, signatureCount);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (supportedSignatures.isEmpty()) {
|
||||
result.addWarning(ApkVerificationIssue.SOURCE_STAMP_NO_SIGNATURE);
|
||||
return;
|
||||
}
|
||||
// Verify signatures over digests using the SourceStamp's certificate.
|
||||
List<ApkSupportedSignature> signaturesToVerify;
|
||||
try {
|
||||
signaturesToVerify =
|
||||
getSignaturesToVerify(
|
||||
supportedSignatures, minSdkVersion, maxSdkVersion, true);
|
||||
} catch (NoApkSupportedSignaturesException e) {
|
||||
// To facilitate debugging capture the signature algorithms and resulting exception in
|
||||
// the warning.
|
||||
StringBuilder signatureAlgorithms = new StringBuilder();
|
||||
for (ApkSupportedSignature supportedSignature : supportedSignatures) {
|
||||
if (signatureAlgorithms.length() > 0) {
|
||||
signatureAlgorithms.append(", ");
|
||||
}
|
||||
signatureAlgorithms.append(supportedSignature.algorithm);
|
||||
}
|
||||
result.addWarning(ApkVerificationIssue.SOURCE_STAMP_NO_SUPPORTED_SIGNATURE,
|
||||
signatureAlgorithms.toString(), e);
|
||||
return;
|
||||
}
|
||||
for (ApkSupportedSignature signature : signaturesToVerify) {
|
||||
SignatureAlgorithm signatureAlgorithm = signature.algorithm;
|
||||
String jcaSignatureAlgorithm =
|
||||
signatureAlgorithm.getJcaSignatureAlgorithmAndParams().getFirst();
|
||||
AlgorithmParameterSpec jcaSignatureAlgorithmParams =
|
||||
signatureAlgorithm.getJcaSignatureAlgorithmAndParams().getSecond();
|
||||
PublicKey publicKey = sourceStampCertificate.getPublicKey();
|
||||
try {
|
||||
Signature sig = Signature.getInstance(jcaSignatureAlgorithm);
|
||||
sig.initVerify(publicKey);
|
||||
if (jcaSignatureAlgorithmParams != null) {
|
||||
sig.setParameter(jcaSignatureAlgorithmParams);
|
||||
}
|
||||
sig.update(data);
|
||||
byte[] sigBytes = signature.signature;
|
||||
if (!sig.verify(sigBytes)) {
|
||||
result.addWarning(
|
||||
ApkVerificationIssue.SOURCE_STAMP_DID_NOT_VERIFY, signatureAlgorithm);
|
||||
return;
|
||||
}
|
||||
} catch (InvalidKeyException
|
||||
| InvalidAlgorithmParameterException
|
||||
| SignatureException
|
||||
| NoSuchAlgorithmException e) {
|
||||
result.addWarning(
|
||||
ApkVerificationIssue.SOURCE_STAMP_VERIFY_EXCEPTION, signatureAlgorithm, e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void parseStampAttributes(ByteBuffer stampAttributeData,
|
||||
X509Certificate sourceStampCertificate, ApkSignerInfo result)
|
||||
throws ApkFormatException {
|
||||
ByteBuffer stampAttributes = getLengthPrefixedSlice(stampAttributeData);
|
||||
int stampAttributeCount = 0;
|
||||
while (stampAttributes.hasRemaining()) {
|
||||
stampAttributeCount++;
|
||||
try {
|
||||
ByteBuffer attribute = getLengthPrefixedSlice(stampAttributes);
|
||||
int id = attribute.getInt();
|
||||
byte[] value = ByteBufferUtils.toByteArray(attribute);
|
||||
if (id == SourceStampConstants.PROOF_OF_ROTATION_ATTR_ID) {
|
||||
readStampCertificateLineage(value, sourceStampCertificate, result);
|
||||
} else if (id == SourceStampConstants.STAMP_TIME_ATTR_ID) {
|
||||
long timestamp = ByteBuffer.wrap(value).order(
|
||||
ByteOrder.LITTLE_ENDIAN).getLong();
|
||||
if (timestamp > 0) {
|
||||
result.timestamp = timestamp;
|
||||
} else {
|
||||
result.addWarning(ApkVerificationIssue.SOURCE_STAMP_INVALID_TIMESTAMP,
|
||||
timestamp);
|
||||
}
|
||||
} else {
|
||||
result.addInfoMessage(ApkVerificationIssue.SOURCE_STAMP_UNKNOWN_ATTRIBUTE, id);
|
||||
}
|
||||
} catch (ApkFormatException | BufferUnderflowException e) {
|
||||
result.addWarning(ApkVerificationIssue.SOURCE_STAMP_MALFORMED_ATTRIBUTE,
|
||||
stampAttributeCount);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void readStampCertificateLineage(byte[] lineageBytes,
|
||||
X509Certificate sourceStampCertificate, ApkSignerInfo result) {
|
||||
try {
|
||||
// SourceStampCertificateLineage is verified when built
|
||||
List<SourceStampCertificateLineage.SigningCertificateNode> nodes =
|
||||
SourceStampCertificateLineage.readSigningCertificateLineage(
|
||||
ByteBuffer.wrap(lineageBytes).order(ByteOrder.LITTLE_ENDIAN));
|
||||
for (int i = 0; i < nodes.size(); i++) {
|
||||
result.certificateLineage.add(nodes.get(i).signingCert);
|
||||
}
|
||||
// Make sure that the last cert in the chain matches this signer cert
|
||||
if (!sourceStampCertificate.equals(
|
||||
result.certificateLineage.get(result.certificateLineage.size() - 1))) {
|
||||
result.addWarning(ApkVerificationIssue.SOURCE_STAMP_POR_CERT_MISMATCH);
|
||||
}
|
||||
} catch (SecurityException e) {
|
||||
result.addWarning(ApkVerificationIssue.SOURCE_STAMP_POR_DID_NOT_VERIFY);
|
||||
} catch (IllegalArgumentException e) {
|
||||
result.addWarning(ApkVerificationIssue.SOURCE_STAMP_POR_CERT_MISMATCH);
|
||||
} catch (Exception e) {
|
||||
result.addWarning(ApkVerificationIssue.SOURCE_STAMP_MALFORMED_LINEAGE);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.apk.stamp;
|
||||
|
||||
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsLengthPrefixedElement;
|
||||
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedElements;
|
||||
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes;
|
||||
|
||||
import com.android.apksig.internal.apk.ApkSigningBlockUtils;
|
||||
import com.android.apksig.internal.apk.ApkSigningBlockUtils.SignerConfig;
|
||||
import com.android.apksig.internal.apk.ContentDigestAlgorithm;
|
||||
import com.android.apksig.internal.util.Pair;
|
||||
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SignatureException;
|
||||
import java.security.cert.CertificateEncodingException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* SourceStamp signer.
|
||||
*
|
||||
* <p>SourceStamp improves traceability of apps with respect to unauthorized distribution.
|
||||
*
|
||||
* <p>The stamp is part of the APK that is protected by the signing block.
|
||||
*
|
||||
* <p>The APK contents hash is signed using the stamp key, and is saved as part of the signing
|
||||
* block.
|
||||
*
|
||||
* <p>V1 of the source stamp allows signing the digest of at most one signature scheme only.
|
||||
*/
|
||||
public abstract class V1SourceStampSigner {
|
||||
public static final int V1_SOURCE_STAMP_BLOCK_ID =
|
||||
SourceStampConstants.V1_SOURCE_STAMP_BLOCK_ID;
|
||||
|
||||
/** Hidden constructor to prevent instantiation. */
|
||||
private V1SourceStampSigner() {}
|
||||
|
||||
public static Pair<byte[], Integer> generateSourceStampBlock(
|
||||
SignerConfig sourceStampSignerConfig, Map<ContentDigestAlgorithm, byte[]> digestInfo)
|
||||
throws SignatureException, NoSuchAlgorithmException, InvalidKeyException {
|
||||
if (sourceStampSignerConfig.certificates.isEmpty()) {
|
||||
throw new SignatureException("No certificates configured for signer");
|
||||
}
|
||||
|
||||
List<Pair<Integer, byte[]>> digests = new ArrayList<>();
|
||||
for (Map.Entry<ContentDigestAlgorithm, byte[]> digest : digestInfo.entrySet()) {
|
||||
digests.add(Pair.of(digest.getKey().getId(), digest.getValue()));
|
||||
}
|
||||
Collections.sort(digests, Comparator.comparing(Pair::getFirst));
|
||||
|
||||
SourceStampBlock sourceStampBlock = new SourceStampBlock();
|
||||
|
||||
try {
|
||||
sourceStampBlock.stampCertificate =
|
||||
sourceStampSignerConfig.certificates.get(0).getEncoded();
|
||||
} catch (CertificateEncodingException e) {
|
||||
throw new SignatureException(
|
||||
"Retrieving the encoded form of the stamp certificate failed", e);
|
||||
}
|
||||
|
||||
byte[] digestBytes =
|
||||
encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(digests);
|
||||
sourceStampBlock.signedDigests =
|
||||
ApkSigningBlockUtils.generateSignaturesOverData(
|
||||
sourceStampSignerConfig, digestBytes);
|
||||
|
||||
// FORMAT:
|
||||
// * length-prefixed bytes: X.509 certificate (ASN.1 DER encoded)
|
||||
// * length-prefixed sequence of length-prefixed signatures:
|
||||
// * uint32: signature algorithm ID
|
||||
// * length-prefixed bytes: signature of signed data
|
||||
byte[] sourceStampSignerBlock =
|
||||
encodeAsSequenceOfLengthPrefixedElements(
|
||||
new byte[][] {
|
||||
sourceStampBlock.stampCertificate,
|
||||
encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
|
||||
sourceStampBlock.signedDigests),
|
||||
});
|
||||
|
||||
// FORMAT:
|
||||
// * length-prefixed stamp block.
|
||||
return Pair.of(encodeAsLengthPrefixedElement(sourceStampSignerBlock),
|
||||
SourceStampConstants.V1_SOURCE_STAMP_BLOCK_ID);
|
||||
}
|
||||
|
||||
private static final class SourceStampBlock {
|
||||
public byte[] stampCertificate;
|
||||
public List<Pair<Integer, byte[]>> signedDigests;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
/*
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.apksig.internal.apk.stamp;
|
||||
|
||||
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes;
|
||||
import static com.android.apksig.internal.apk.stamp.SourceStampConstants.V1_SOURCE_STAMP_BLOCK_ID;
|
||||
|
||||
import com.android.apksig.ApkVerifier;
|
||||
import com.android.apksig.apk.ApkFormatException;
|
||||
import com.android.apksig.apk.ApkUtils;
|
||||
import com.android.apksig.internal.apk.ApkSigningBlockUtils;
|
||||
import com.android.apksig.internal.apk.ContentDigestAlgorithm;
|
||||
import com.android.apksig.internal.apk.SignatureInfo;
|
||||
import com.android.apksig.internal.util.Pair;
|
||||
import com.android.apksig.util.DataSource;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.BufferUnderflowException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Source Stamp verifier.
|
||||
*
|
||||
* <p>V1 of the source stamp verifies the stamp signature of at most one signature scheme.
|
||||
*/
|
||||
public abstract class V1SourceStampVerifier {
|
||||
|
||||
/** Hidden constructor to prevent instantiation. */
|
||||
private V1SourceStampVerifier() {}
|
||||
|
||||
/**
|
||||
* Verifies the provided APK's SourceStamp signatures and returns the result of verification.
|
||||
* The APK must be considered verified only if {@link ApkSigningBlockUtils.Result#verified} is
|
||||
* {@code true}. If verification fails, the result will contain errors -- see {@link
|
||||
* ApkSigningBlockUtils.Result#getErrors()}.
|
||||
*
|
||||
* @throws NoSuchAlgorithmException if the APK's signatures cannot be verified because a
|
||||
* required cryptographic algorithm implementation is missing
|
||||
* @throws ApkSigningBlockUtils.SignatureNotFoundException if no SourceStamp signatures are
|
||||
* found
|
||||
* @throws IOException if an I/O error occurs when reading the APK
|
||||
*/
|
||||
public static ApkSigningBlockUtils.Result verify(
|
||||
DataSource apk,
|
||||
ApkUtils.ZipSections zipSections,
|
||||
byte[] sourceStampCertificateDigest,
|
||||
Map<ContentDigestAlgorithm, byte[]> apkContentDigests,
|
||||
int minSdkVersion,
|
||||
int maxSdkVersion)
|
||||
throws IOException, NoSuchAlgorithmException,
|
||||
ApkSigningBlockUtils.SignatureNotFoundException {
|
||||
ApkSigningBlockUtils.Result result =
|
||||
new ApkSigningBlockUtils.Result(ApkSigningBlockUtils.VERSION_SOURCE_STAMP);
|
||||
SignatureInfo signatureInfo =
|
||||
ApkSigningBlockUtils.findSignature(
|
||||
apk, zipSections, V1_SOURCE_STAMP_BLOCK_ID, result);
|
||||
|
||||
verify(
|
||||
signatureInfo.signatureBlock,
|
||||
sourceStampCertificateDigest,
|
||||
apkContentDigests,
|
||||
minSdkVersion,
|
||||
maxSdkVersion,
|
||||
result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies the provided APK's SourceStamp signatures and outputs the results into the provided
|
||||
* {@code result}. APK is considered verified only if there are no errors reported in the {@code
|
||||
* result}. See {@link #verify(DataSource, ApkUtils.ZipSections, byte[], Map, int, int)} for
|
||||
* more information about the contract of this method.
|
||||
*/
|
||||
private static void verify(
|
||||
ByteBuffer sourceStampBlock,
|
||||
byte[] sourceStampCertificateDigest,
|
||||
Map<ContentDigestAlgorithm, byte[]> apkContentDigests,
|
||||
int minSdkVersion,
|
||||
int maxSdkVersion,
|
||||
ApkSigningBlockUtils.Result result)
|
||||
throws NoSuchAlgorithmException {
|
||||
ApkSigningBlockUtils.Result.SignerInfo signerInfo =
|
||||
new ApkSigningBlockUtils.Result.SignerInfo();
|
||||
result.signers.add(signerInfo);
|
||||
try {
|
||||
CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
|
||||
ByteBuffer sourceStampBlockData =
|
||||
ApkSigningBlockUtils.getLengthPrefixedSlice(sourceStampBlock);
|
||||
byte[] digestBytes =
|
||||
encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
|
||||
getApkDigests(apkContentDigests));
|
||||
SourceStampVerifier.verifyV1SourceStamp(
|
||||
sourceStampBlockData,
|
||||
certFactory,
|
||||
signerInfo,
|
||||
digestBytes,
|
||||
sourceStampCertificateDigest,
|
||||
minSdkVersion,
|
||||
maxSdkVersion);
|
||||
result.verified = !result.containsErrors() && !result.containsWarnings();
|
||||
} catch (CertificateException e) {
|
||||
throw new IllegalStateException("Failed to obtain X.509 CertificateFactory", e);
|
||||
} catch (ApkFormatException | BufferUnderflowException e) {
|
||||
signerInfo.addWarning(ApkVerifier.Issue.SOURCE_STAMP_MALFORMED_SIGNATURE);
|
||||
}
|
||||
}
|
||||
|
||||
private static List<Pair<Integer, byte[]>> getApkDigests(
|
||||
Map<ContentDigestAlgorithm, byte[]> apkContentDigests) {
|
||||
List<Pair<Integer, byte[]>> digests = new ArrayList<>();
|
||||
for (Map.Entry<ContentDigestAlgorithm, byte[]> apkContentDigest :
|
||||
apkContentDigests.entrySet()) {
|
||||
digests.add(Pair.of(apkContentDigest.getKey().getId(), apkContentDigest.getValue()));
|
||||
}
|
||||
Collections.sort(digests, Comparator.comparing(Pair::getFirst));
|
||||
return digests;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,286 @@
|
||||
/*
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.apk.stamp;
|
||||
|
||||
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2;
|
||||
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3;
|
||||
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_JAR_SIGNATURE_SCHEME;
|
||||
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsLengthPrefixedElement;
|
||||
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedElements;
|
||||
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes;
|
||||
|
||||
import com.android.apksig.SigningCertificateLineage;
|
||||
import com.android.apksig.internal.apk.ApkSigningBlockUtils;
|
||||
import com.android.apksig.internal.apk.ApkSigningBlockUtils.SignerConfig;
|
||||
import com.android.apksig.internal.apk.ContentDigestAlgorithm;
|
||||
import com.android.apksig.internal.util.Pair;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SignatureException;
|
||||
import java.security.cert.CertificateEncodingException;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* SourceStamp signer.
|
||||
*
|
||||
* <p>SourceStamp improves traceability of apps with respect to unauthorized distribution.
|
||||
*
|
||||
* <p>The stamp is part of the APK that is protected by the signing block.
|
||||
*
|
||||
* <p>The APK contents hash is signed using the stamp key, and is saved as part of the signing
|
||||
* block.
|
||||
*
|
||||
* <p>V2 of the source stamp allows signing the digests of more than one signature schemes.
|
||||
*/
|
||||
public class V2SourceStampSigner {
|
||||
public static final int V2_SOURCE_STAMP_BLOCK_ID =
|
||||
SourceStampConstants.V2_SOURCE_STAMP_BLOCK_ID;
|
||||
|
||||
private final SignerConfig mSourceStampSignerConfig;
|
||||
private final Map<Integer, Map<ContentDigestAlgorithm, byte[]>> mSignatureSchemeDigestInfos;
|
||||
private final boolean mSourceStampTimestampEnabled;
|
||||
|
||||
/** Hidden constructor to prevent instantiation. */
|
||||
private V2SourceStampSigner(Builder builder) {
|
||||
mSourceStampSignerConfig = builder.mSourceStampSignerConfig;
|
||||
mSignatureSchemeDigestInfos = builder.mSignatureSchemeDigestInfos;
|
||||
mSourceStampTimestampEnabled = builder.mSourceStampTimestampEnabled;
|
||||
}
|
||||
|
||||
public static Pair<byte[], Integer> generateSourceStampBlock(
|
||||
SignerConfig sourceStampSignerConfig,
|
||||
Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeDigestInfos)
|
||||
throws SignatureException, NoSuchAlgorithmException, InvalidKeyException {
|
||||
return new Builder(sourceStampSignerConfig,
|
||||
signatureSchemeDigestInfos).build().generateSourceStampBlock();
|
||||
}
|
||||
|
||||
public Pair<byte[], Integer> generateSourceStampBlock()
|
||||
throws SignatureException, NoSuchAlgorithmException, InvalidKeyException {
|
||||
if (mSourceStampSignerConfig.certificates.isEmpty()) {
|
||||
throw new SignatureException("No certificates configured for signer");
|
||||
}
|
||||
|
||||
// Extract the digests for signature schemes.
|
||||
List<Pair<Integer, byte[]>> signatureSchemeDigests = new ArrayList<>();
|
||||
getSignedDigestsFor(
|
||||
VERSION_APK_SIGNATURE_SCHEME_V3,
|
||||
mSignatureSchemeDigestInfos,
|
||||
mSourceStampSignerConfig,
|
||||
signatureSchemeDigests);
|
||||
getSignedDigestsFor(
|
||||
VERSION_APK_SIGNATURE_SCHEME_V2,
|
||||
mSignatureSchemeDigestInfos,
|
||||
mSourceStampSignerConfig,
|
||||
signatureSchemeDigests);
|
||||
getSignedDigestsFor(
|
||||
VERSION_JAR_SIGNATURE_SCHEME,
|
||||
mSignatureSchemeDigestInfos,
|
||||
mSourceStampSignerConfig,
|
||||
signatureSchemeDigests);
|
||||
Collections.sort(signatureSchemeDigests, Comparator.comparing(Pair::getFirst));
|
||||
|
||||
SourceStampBlock sourceStampBlock = new SourceStampBlock();
|
||||
|
||||
try {
|
||||
sourceStampBlock.stampCertificate =
|
||||
mSourceStampSignerConfig.certificates.get(0).getEncoded();
|
||||
} catch (CertificateEncodingException e) {
|
||||
throw new SignatureException(
|
||||
"Retrieving the encoded form of the stamp certificate failed", e);
|
||||
}
|
||||
|
||||
sourceStampBlock.signedDigests = signatureSchemeDigests;
|
||||
|
||||
sourceStampBlock.stampAttributes = encodeStampAttributes(
|
||||
generateStampAttributes(mSourceStampSignerConfig.signingCertificateLineage));
|
||||
sourceStampBlock.signedStampAttributes =
|
||||
ApkSigningBlockUtils.generateSignaturesOverData(mSourceStampSignerConfig,
|
||||
sourceStampBlock.stampAttributes);
|
||||
|
||||
// FORMAT:
|
||||
// * length-prefixed bytes: X.509 certificate (ASN.1 DER encoded)
|
||||
// * length-prefixed sequence of length-prefixed signed signature scheme digests:
|
||||
// * uint32: signature scheme id
|
||||
// * length-prefixed bytes: signed digests for the respective signature scheme
|
||||
// * length-prefixed bytes: encoded stamp attributes
|
||||
// * length-prefixed sequence of length-prefixed signed stamp attributes:
|
||||
// * uint32: signature algorithm id
|
||||
// * length-prefixed bytes: signed stamp attributes for the respective signature algorithm
|
||||
byte[] sourceStampSignerBlock =
|
||||
encodeAsSequenceOfLengthPrefixedElements(
|
||||
new byte[][]{
|
||||
sourceStampBlock.stampCertificate,
|
||||
encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
|
||||
sourceStampBlock.signedDigests),
|
||||
sourceStampBlock.stampAttributes,
|
||||
encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
|
||||
sourceStampBlock.signedStampAttributes),
|
||||
});
|
||||
|
||||
// FORMAT:
|
||||
// * length-prefixed stamp block.
|
||||
return Pair.of(encodeAsLengthPrefixedElement(sourceStampSignerBlock),
|
||||
SourceStampConstants.V2_SOURCE_STAMP_BLOCK_ID);
|
||||
}
|
||||
|
||||
private static void getSignedDigestsFor(
|
||||
int signatureSchemeVersion,
|
||||
Map<Integer, Map<ContentDigestAlgorithm, byte[]>> mSignatureSchemeDigestInfos,
|
||||
SignerConfig mSourceStampSignerConfig,
|
||||
List<Pair<Integer, byte[]>> signatureSchemeDigests)
|
||||
throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
|
||||
if (!mSignatureSchemeDigestInfos.containsKey(signatureSchemeVersion)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Map<ContentDigestAlgorithm, byte[]> digestInfo =
|
||||
mSignatureSchemeDigestInfos.get(signatureSchemeVersion);
|
||||
List<Pair<Integer, byte[]>> digests = new ArrayList<>();
|
||||
for (Map.Entry<ContentDigestAlgorithm, byte[]> digest : digestInfo.entrySet()) {
|
||||
digests.add(Pair.of(digest.getKey().getId(), digest.getValue()));
|
||||
}
|
||||
Collections.sort(digests, Comparator.comparing(Pair::getFirst));
|
||||
|
||||
// FORMAT:
|
||||
// * length-prefixed sequence of length-prefixed digests:
|
||||
// * uint32: digest algorithm id
|
||||
// * length-prefixed bytes: digest of the respective digest algorithm
|
||||
byte[] digestBytes =
|
||||
encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(digests);
|
||||
|
||||
// FORMAT:
|
||||
// * length-prefixed sequence of length-prefixed signed digests:
|
||||
// * uint32: signature algorithm id
|
||||
// * length-prefixed bytes: signed digest for the respective signature algorithm
|
||||
List<Pair<Integer, byte[]>> signedDigest =
|
||||
ApkSigningBlockUtils.generateSignaturesOverData(
|
||||
mSourceStampSignerConfig, digestBytes);
|
||||
|
||||
// FORMAT:
|
||||
// * length-prefixed sequence of length-prefixed signed signature scheme digests:
|
||||
// * uint32: signature scheme id
|
||||
// * length-prefixed bytes: signed digests for the respective signature scheme
|
||||
signatureSchemeDigests.add(
|
||||
Pair.of(
|
||||
signatureSchemeVersion,
|
||||
encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
|
||||
signedDigest)));
|
||||
}
|
||||
|
||||
private static byte[] encodeStampAttributes(Map<Integer, byte[]> stampAttributes) {
|
||||
int payloadSize = 0;
|
||||
for (byte[] attributeValue : stampAttributes.values()) {
|
||||
// Pair size + Attribute ID + Attribute value
|
||||
payloadSize += 4 + 4 + attributeValue.length;
|
||||
}
|
||||
|
||||
// FORMAT (little endian):
|
||||
// * length-prefixed bytes: pair
|
||||
// * uint32: ID
|
||||
// * bytes: value
|
||||
ByteBuffer result = ByteBuffer.allocate(4 + payloadSize);
|
||||
result.order(ByteOrder.LITTLE_ENDIAN);
|
||||
result.putInt(payloadSize);
|
||||
for (Map.Entry<Integer, byte[]> stampAttribute : stampAttributes.entrySet()) {
|
||||
// Pair size
|
||||
result.putInt(4 + stampAttribute.getValue().length);
|
||||
result.putInt(stampAttribute.getKey());
|
||||
result.put(stampAttribute.getValue());
|
||||
}
|
||||
return result.array();
|
||||
}
|
||||
|
||||
private Map<Integer, byte[]> generateStampAttributes(SigningCertificateLineage lineage) {
|
||||
HashMap<Integer, byte[]> stampAttributes = new HashMap<>();
|
||||
|
||||
if (mSourceStampTimestampEnabled) {
|
||||
// Write the current epoch time as the timestamp for the source stamp.
|
||||
long timestamp = Instant.now().getEpochSecond();
|
||||
if (timestamp > 0) {
|
||||
ByteBuffer attributeBuffer = ByteBuffer.allocate(8);
|
||||
attributeBuffer.order(ByteOrder.LITTLE_ENDIAN);
|
||||
attributeBuffer.putLong(timestamp);
|
||||
stampAttributes.put(SourceStampConstants.STAMP_TIME_ATTR_ID,
|
||||
attributeBuffer.array());
|
||||
} else {
|
||||
// The epoch time should never be <= 0, and since security decisions can potentially
|
||||
// be made based on the value in the timestamp, throw an Exception to ensure the
|
||||
// issues with the environment are resolved before allowing the signing.
|
||||
throw new IllegalStateException(
|
||||
"Received an invalid value from Instant#getTimestamp: " + timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
if (lineage != null) {
|
||||
stampAttributes.put(SourceStampConstants.PROOF_OF_ROTATION_ATTR_ID,
|
||||
lineage.encodeSigningCertificateLineage());
|
||||
}
|
||||
return stampAttributes;
|
||||
}
|
||||
|
||||
private static final class SourceStampBlock {
|
||||
public byte[] stampCertificate;
|
||||
public List<Pair<Integer, byte[]>> signedDigests;
|
||||
// Optional stamp attributes that are not required for verification.
|
||||
public byte[] stampAttributes;
|
||||
public List<Pair<Integer, byte[]>> signedStampAttributes;
|
||||
}
|
||||
|
||||
/** Builder of {@link V2SourceStampSigner} instances. */
|
||||
public static class Builder {
|
||||
private final SignerConfig mSourceStampSignerConfig;
|
||||
private final Map<Integer, Map<ContentDigestAlgorithm, byte[]>> mSignatureSchemeDigestInfos;
|
||||
private boolean mSourceStampTimestampEnabled = true;
|
||||
|
||||
/**
|
||||
* Instantiates a new {@code Builder} with the provided {@code sourceStampSignerConfig}
|
||||
* and the {@code signatureSchemeDigestInfos}.
|
||||
*/
|
||||
public Builder(SignerConfig sourceStampSignerConfig,
|
||||
Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeDigestInfos) {
|
||||
mSourceStampSignerConfig = sourceStampSignerConfig;
|
||||
mSignatureSchemeDigestInfos = signatureSchemeDigestInfos;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether the source stamp should contain the timestamp attribute with the time
|
||||
* at which the source stamp was signed.
|
||||
*/
|
||||
public Builder setSourceStampTimestampEnabled(boolean value) {
|
||||
mSourceStampTimestampEnabled = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a new V2SourceStampSigner that can be used to generate a new source stamp
|
||||
* block signed with the specified signing config.
|
||||
*/
|
||||
public V2SourceStampSigner build() {
|
||||
return new V2SourceStampSigner(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
/*
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.apk.stamp;
|
||||
|
||||
import static com.android.apksig.internal.apk.ApkSigningBlockUtilsLite.encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes;
|
||||
import static com.android.apksig.internal.apk.stamp.SourceStampConstants.V2_SOURCE_STAMP_BLOCK_ID;
|
||||
|
||||
import com.android.apksig.ApkVerificationIssue;
|
||||
import com.android.apksig.Constants;
|
||||
import com.android.apksig.apk.ApkFormatException;
|
||||
import com.android.apksig.internal.apk.ApkSigResult;
|
||||
import com.android.apksig.internal.apk.ApkSignerInfo;
|
||||
import com.android.apksig.internal.apk.ApkSigningBlockUtilsLite;
|
||||
import com.android.apksig.internal.apk.ContentDigestAlgorithm;
|
||||
import com.android.apksig.internal.apk.SignatureInfo;
|
||||
import com.android.apksig.internal.apk.SignatureNotFoundException;
|
||||
import com.android.apksig.internal.util.Pair;
|
||||
import com.android.apksig.util.DataSource;
|
||||
import com.android.apksig.zip.ZipSections;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.BufferUnderflowException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Source Stamp verifier.
|
||||
*
|
||||
* <p>V2 of the source stamp verifies the stamp signature of more than one signature schemes.
|
||||
*/
|
||||
public abstract class V2SourceStampVerifier {
|
||||
|
||||
/** Hidden constructor to prevent instantiation. */
|
||||
private V2SourceStampVerifier() {}
|
||||
|
||||
/**
|
||||
* Verifies the provided APK's SourceStamp signatures and returns the result of verification.
|
||||
* The APK must be considered verified only if {@link ApkSigResult#verified} is
|
||||
* {@code true}. If verification fails, the result will contain errors -- see {@link
|
||||
* ApkSigResult#getErrors()}.
|
||||
*
|
||||
* @throws NoSuchAlgorithmException if the APK's signatures cannot be verified because a
|
||||
* required cryptographic algorithm implementation is missing
|
||||
* @throws SignatureNotFoundException if no SourceStamp signatures are
|
||||
* found
|
||||
* @throws IOException if an I/O error occurs when reading the APK
|
||||
*/
|
||||
public static ApkSigResult verify(
|
||||
DataSource apk,
|
||||
ZipSections zipSections,
|
||||
byte[] sourceStampCertificateDigest,
|
||||
Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeApkContentDigests,
|
||||
int minSdkVersion,
|
||||
int maxSdkVersion)
|
||||
throws IOException, NoSuchAlgorithmException, SignatureNotFoundException {
|
||||
ApkSigResult result =
|
||||
new ApkSigResult(Constants.VERSION_SOURCE_STAMP);
|
||||
SignatureInfo signatureInfo =
|
||||
ApkSigningBlockUtilsLite.findSignature(
|
||||
apk, zipSections, V2_SOURCE_STAMP_BLOCK_ID);
|
||||
|
||||
verify(
|
||||
signatureInfo.signatureBlock,
|
||||
sourceStampCertificateDigest,
|
||||
signatureSchemeApkContentDigests,
|
||||
minSdkVersion,
|
||||
maxSdkVersion,
|
||||
result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies the provided APK's SourceStamp signatures and outputs the results into the provided
|
||||
* {@code result}. APK is considered verified only if there are no errors reported in the {@code
|
||||
* result}. See {@link #verify(DataSource, ZipSections, byte[], Map, int, int)} for
|
||||
* more information about the contract of this method.
|
||||
*/
|
||||
private static void verify(
|
||||
ByteBuffer sourceStampBlock,
|
||||
byte[] sourceStampCertificateDigest,
|
||||
Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeApkContentDigests,
|
||||
int minSdkVersion,
|
||||
int maxSdkVersion,
|
||||
ApkSigResult result)
|
||||
throws NoSuchAlgorithmException {
|
||||
ApkSignerInfo signerInfo = new ApkSignerInfo();
|
||||
result.mSigners.add(signerInfo);
|
||||
try {
|
||||
CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
|
||||
ByteBuffer sourceStampBlockData =
|
||||
ApkSigningBlockUtilsLite.getLengthPrefixedSlice(sourceStampBlock);
|
||||
SourceStampVerifier.verifyV2SourceStamp(
|
||||
sourceStampBlockData,
|
||||
certFactory,
|
||||
signerInfo,
|
||||
getSignatureSchemeDigests(signatureSchemeApkContentDigests),
|
||||
sourceStampCertificateDigest,
|
||||
minSdkVersion,
|
||||
maxSdkVersion);
|
||||
result.verified = !result.containsErrors() && !result.containsWarnings();
|
||||
} catch (CertificateException e) {
|
||||
throw new IllegalStateException("Failed to obtain X.509 CertificateFactory", e);
|
||||
} catch (ApkFormatException | BufferUnderflowException e) {
|
||||
signerInfo.addWarning(ApkVerificationIssue.SOURCE_STAMP_MALFORMED_SIGNATURE);
|
||||
}
|
||||
}
|
||||
|
||||
private static Map<Integer, byte[]> getSignatureSchemeDigests(
|
||||
Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeApkContentDigests) {
|
||||
Map<Integer, byte[]> digests = new HashMap<>();
|
||||
for (Map.Entry<Integer, Map<ContentDigestAlgorithm, byte[]>>
|
||||
signatureSchemeApkContentDigest : signatureSchemeApkContentDigests.entrySet()) {
|
||||
List<Pair<Integer, byte[]>> apkDigests =
|
||||
getApkDigests(signatureSchemeApkContentDigest.getValue());
|
||||
digests.put(
|
||||
signatureSchemeApkContentDigest.getKey(),
|
||||
encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(apkDigests));
|
||||
}
|
||||
return digests;
|
||||
}
|
||||
|
||||
private static List<Pair<Integer, byte[]>> getApkDigests(
|
||||
Map<ContentDigestAlgorithm, byte[]> apkContentDigests) {
|
||||
List<Pair<Integer, byte[]>> digests = new ArrayList<>();
|
||||
for (Map.Entry<ContentDigestAlgorithm, byte[]> apkContentDigest :
|
||||
apkContentDigests.entrySet()) {
|
||||
digests.add(Pair.of(apkContentDigest.getKey().getId(), apkContentDigest.getValue()));
|
||||
}
|
||||
Collections.sort(digests, new Comparator<Pair<Integer, byte[]>>() {
|
||||
@Override
|
||||
public int compare(Pair<Integer, byte[]> pair1, Pair<Integer, byte[]> pair2) {
|
||||
return pair1.getFirst() - pair2.getFirst();
|
||||
}
|
||||
});
|
||||
return digests;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.apk.v1;
|
||||
|
||||
import java.util.Comparator;
|
||||
|
||||
/**
|
||||
* Digest algorithm used with JAR signing (aka v1 signing scheme).
|
||||
*/
|
||||
public enum DigestAlgorithm {
|
||||
/** SHA-1 */
|
||||
SHA1("SHA-1"),
|
||||
|
||||
/** SHA2-256 */
|
||||
SHA256("SHA-256");
|
||||
|
||||
private final String mJcaMessageDigestAlgorithm;
|
||||
|
||||
private DigestAlgorithm(String jcaMessageDigestAlgoritm) {
|
||||
mJcaMessageDigestAlgorithm = jcaMessageDigestAlgoritm;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link java.security.MessageDigest} algorithm represented by this digest
|
||||
* algorithm.
|
||||
*/
|
||||
String getJcaMessageDigestAlgorithm() {
|
||||
return mJcaMessageDigestAlgorithm;
|
||||
}
|
||||
|
||||
public static Comparator<DigestAlgorithm> BY_STRENGTH_COMPARATOR = new StrengthComparator();
|
||||
|
||||
private static class StrengthComparator implements Comparator<DigestAlgorithm> {
|
||||
@Override
|
||||
public int compare(DigestAlgorithm a1, DigestAlgorithm a2) {
|
||||
switch (a1) {
|
||||
case SHA1:
|
||||
switch (a2) {
|
||||
case SHA1:
|
||||
return 0;
|
||||
case SHA256:
|
||||
return -1;
|
||||
}
|
||||
throw new RuntimeException("Unsupported algorithm: " + a2);
|
||||
|
||||
case SHA256:
|
||||
switch (a2) {
|
||||
case SHA1:
|
||||
return 1;
|
||||
case SHA256:
|
||||
return 0;
|
||||
}
|
||||
throw new RuntimeException("Unsupported algorithm: " + a2);
|
||||
|
||||
default:
|
||||
throw new RuntimeException("Unsupported algorithm: " + a1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.apk.v1;
|
||||
|
||||
/** Constants used by the Jar Signing / V1 Signature Scheme signing and verification. */
|
||||
public class V1SchemeConstants {
|
||||
private V1SchemeConstants() {}
|
||||
|
||||
public static final String MANIFEST_ENTRY_NAME = "META-INF/MANIFEST.MF";
|
||||
public static final String SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME_STR =
|
||||
"X-Android-APK-Signed";
|
||||
}
|
||||
@@ -0,0 +1,586 @@
|
||||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.apk.v1;
|
||||
|
||||
import static com.android.apksig.Constants.MAX_APK_SIGNERS;
|
||||
import static com.android.apksig.Constants.OID_RSA_ENCRYPTION;
|
||||
import static com.android.apksig.internal.pkcs7.AlgorithmIdentifier.getSignerInfoDigestAlgorithmOid;
|
||||
import static com.android.apksig.internal.pkcs7.AlgorithmIdentifier.getSignerInfoSignatureAlgorithm;
|
||||
|
||||
import com.android.apksig.apk.ApkFormatException;
|
||||
import com.android.apksig.internal.apk.ApkSigningBlockUtils;
|
||||
import com.android.apksig.internal.asn1.Asn1EncodingException;
|
||||
import com.android.apksig.internal.jar.ManifestWriter;
|
||||
import com.android.apksig.internal.jar.SignatureFileWriter;
|
||||
import com.android.apksig.internal.pkcs7.AlgorithmIdentifier;
|
||||
import com.android.apksig.internal.util.Pair;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.security.Signature;
|
||||
import java.security.SignatureException;
|
||||
import java.security.cert.CertificateEncodingException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.SortedMap;
|
||||
import java.util.TreeMap;
|
||||
import java.util.jar.Attributes;
|
||||
import java.util.jar.Manifest;
|
||||
|
||||
/**
|
||||
* APK signer which uses JAR signing (aka v1 signing scheme).
|
||||
*
|
||||
* @see <a href="https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File">Signed JAR File</a>
|
||||
*/
|
||||
public abstract class V1SchemeSigner {
|
||||
public static final String MANIFEST_ENTRY_NAME = V1SchemeConstants.MANIFEST_ENTRY_NAME;
|
||||
|
||||
private static final Attributes.Name ATTRIBUTE_NAME_CREATED_BY =
|
||||
new Attributes.Name("Created-By");
|
||||
private static final String ATTRIBUTE_VALUE_MANIFEST_VERSION = "1.0";
|
||||
private static final String ATTRIBUTE_VALUE_SIGNATURE_VERSION = "1.0";
|
||||
|
||||
private static final Attributes.Name SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME =
|
||||
new Attributes.Name(V1SchemeConstants.SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME_STR);
|
||||
|
||||
/**
|
||||
* Signer configuration.
|
||||
*/
|
||||
public static class SignerConfig {
|
||||
/** Name. */
|
||||
public String name;
|
||||
|
||||
/** Private key. */
|
||||
public PrivateKey privateKey;
|
||||
|
||||
/**
|
||||
* Certificates, with the first certificate containing the public key corresponding to
|
||||
* {@link #privateKey}.
|
||||
*/
|
||||
public List<X509Certificate> certificates;
|
||||
|
||||
/**
|
||||
* Digest algorithm used for the signature.
|
||||
*/
|
||||
public DigestAlgorithm signatureDigestAlgorithm;
|
||||
|
||||
/**
|
||||
* If DSA is the signing algorithm, whether or not deterministic DSA signing should be used.
|
||||
*/
|
||||
public boolean deterministicDsaSigning;
|
||||
}
|
||||
|
||||
/** Hidden constructor to prevent instantiation. */
|
||||
private V1SchemeSigner() {}
|
||||
|
||||
/**
|
||||
* Gets the JAR signing digest algorithm to be used for signing an APK using the provided key.
|
||||
*
|
||||
* @param minSdkVersion minimum API Level of the platform on which the APK may be installed (see
|
||||
* AndroidManifest.xml minSdkVersion attribute)
|
||||
*
|
||||
* @throws InvalidKeyException if the provided key is not suitable for signing APKs using
|
||||
* JAR signing (aka v1 signature scheme)
|
||||
*/
|
||||
public static DigestAlgorithm getSuggestedSignatureDigestAlgorithm(
|
||||
PublicKey signingKey, int minSdkVersion) throws InvalidKeyException {
|
||||
String keyAlgorithm = signingKey.getAlgorithm();
|
||||
if ("RSA".equalsIgnoreCase(keyAlgorithm) || OID_RSA_ENCRYPTION.equals((keyAlgorithm))) {
|
||||
// Prior to API Level 18, only SHA-1 can be used with RSA.
|
||||
if (minSdkVersion < 18) {
|
||||
return DigestAlgorithm.SHA1;
|
||||
}
|
||||
return DigestAlgorithm.SHA256;
|
||||
} else if ("DSA".equalsIgnoreCase(keyAlgorithm)) {
|
||||
// Prior to API Level 21, only SHA-1 can be used with DSA
|
||||
if (minSdkVersion < 21) {
|
||||
return DigestAlgorithm.SHA1;
|
||||
} else {
|
||||
return DigestAlgorithm.SHA256;
|
||||
}
|
||||
} else if ("EC".equalsIgnoreCase(keyAlgorithm)) {
|
||||
if (minSdkVersion < 18) {
|
||||
throw new InvalidKeyException(
|
||||
"ECDSA signatures only supported for minSdkVersion 18 and higher");
|
||||
}
|
||||
return DigestAlgorithm.SHA256;
|
||||
} else {
|
||||
throw new InvalidKeyException("Unsupported key algorithm: " + keyAlgorithm);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a safe version of the provided signer name.
|
||||
*/
|
||||
public static String getSafeSignerName(String name) {
|
||||
if (name.isEmpty()) {
|
||||
throw new IllegalArgumentException("Empty name");
|
||||
}
|
||||
|
||||
// According to https://docs.oracle.com/javase/tutorial/deployment/jar/signing.html, the
|
||||
// name must not be longer than 8 characters and may contain only A-Z, 0-9, _, and -.
|
||||
StringBuilder result = new StringBuilder();
|
||||
char[] nameCharsUpperCase = name.toUpperCase(Locale.US).toCharArray();
|
||||
for (int i = 0; i < Math.min(nameCharsUpperCase.length, 8); i++) {
|
||||
char c = nameCharsUpperCase[i];
|
||||
if (((c >= 'A') && (c <= 'Z'))
|
||||
|| ((c >= '0') && (c <= '9'))
|
||||
|| (c == '-')
|
||||
|| (c == '_')) {
|
||||
result.append(c);
|
||||
} else {
|
||||
result.append('_');
|
||||
}
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new {@link MessageDigest} instance corresponding to the provided digest algorithm.
|
||||
*/
|
||||
private static MessageDigest getMessageDigestInstance(DigestAlgorithm digestAlgorithm)
|
||||
throws NoSuchAlgorithmException {
|
||||
String jcaAlgorithm = digestAlgorithm.getJcaMessageDigestAlgorithm();
|
||||
return MessageDigest.getInstance(jcaAlgorithm);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the JCA {@link MessageDigest} algorithm corresponding to the provided digest
|
||||
* algorithm.
|
||||
*/
|
||||
public static String getJcaMessageDigestAlgorithm(DigestAlgorithm digestAlgorithm) {
|
||||
return digestAlgorithm.getJcaMessageDigestAlgorithm();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if the provided JAR entry must be mentioned in signed JAR archive's
|
||||
* manifest.
|
||||
*/
|
||||
public static boolean isJarEntryDigestNeededInManifest(String entryName) {
|
||||
// See https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File
|
||||
|
||||
// Entries which represent directories sould not be listed in the manifest.
|
||||
if (entryName.endsWith("/")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Entries outside of META-INF must be listed in the manifest.
|
||||
if (!entryName.startsWith("META-INF/")) {
|
||||
return true;
|
||||
}
|
||||
// Entries in subdirectories of META-INF must be listed in the manifest.
|
||||
if (entryName.indexOf('/', "META-INF/".length()) != -1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Ignored file names (case-insensitive) in META-INF directory:
|
||||
// MANIFEST.MF
|
||||
// *.SF
|
||||
// *.RSA
|
||||
// *.DSA
|
||||
// *.EC
|
||||
// SIG-*
|
||||
String fileNameLowerCase =
|
||||
entryName.substring("META-INF/".length()).toLowerCase(Locale.US);
|
||||
if (("manifest.mf".equals(fileNameLowerCase))
|
||||
|| (fileNameLowerCase.endsWith(".sf"))
|
||||
|| (fileNameLowerCase.endsWith(".rsa"))
|
||||
|| (fileNameLowerCase.endsWith(".dsa"))
|
||||
|| (fileNameLowerCase.endsWith(".ec"))
|
||||
|| (fileNameLowerCase.startsWith("sig-"))) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Signs the provided APK using JAR signing (aka v1 signature scheme) and returns the list of
|
||||
* JAR entries which need to be added to the APK as part of the signature.
|
||||
*
|
||||
* @param signerConfigs signer configurations, one for each signer. At least one signer config
|
||||
* must be provided.
|
||||
*
|
||||
* @throws ApkFormatException if the source manifest is malformed
|
||||
* @throws NoSuchAlgorithmException if a required cryptographic algorithm implementation is
|
||||
* missing
|
||||
* @throws InvalidKeyException if a signing key is not suitable for this signature scheme or
|
||||
* cannot be used in general
|
||||
* @throws SignatureException if an error occurs when computing digests of generating
|
||||
* signatures
|
||||
*/
|
||||
public static List<Pair<String, byte[]>> sign(
|
||||
List<SignerConfig> signerConfigs,
|
||||
DigestAlgorithm jarEntryDigestAlgorithm,
|
||||
Map<String, byte[]> jarEntryDigests,
|
||||
List<Integer> apkSigningSchemeIds,
|
||||
byte[] sourceManifestBytes,
|
||||
String createdBy)
|
||||
throws NoSuchAlgorithmException, ApkFormatException, InvalidKeyException,
|
||||
CertificateException, SignatureException {
|
||||
if (signerConfigs.isEmpty()) {
|
||||
throw new IllegalArgumentException("At least one signer config must be provided");
|
||||
}
|
||||
if (signerConfigs.size() > MAX_APK_SIGNERS) {
|
||||
throw new IllegalArgumentException(
|
||||
"APK Signature Scheme v1 only supports a maximum of " + MAX_APK_SIGNERS + ", "
|
||||
+ signerConfigs.size() + " provided");
|
||||
}
|
||||
OutputManifestFile manifest =
|
||||
generateManifestFile(
|
||||
jarEntryDigestAlgorithm, jarEntryDigests, sourceManifestBytes);
|
||||
|
||||
return signManifest(
|
||||
signerConfigs, jarEntryDigestAlgorithm, apkSigningSchemeIds, createdBy, manifest);
|
||||
}
|
||||
|
||||
/**
|
||||
* Signs the provided APK using JAR signing (aka v1 signature scheme) and returns the list of
|
||||
* JAR entries which need to be added to the APK as part of the signature.
|
||||
*
|
||||
* @param signerConfigs signer configurations, one for each signer. At least one signer config
|
||||
* must be provided.
|
||||
*
|
||||
* @throws InvalidKeyException if a signing key is not suitable for this signature scheme or
|
||||
* cannot be used in general
|
||||
* @throws SignatureException if an error occurs when computing digests of generating
|
||||
* signatures
|
||||
*/
|
||||
public static List<Pair<String, byte[]>> signManifest(
|
||||
List<SignerConfig> signerConfigs,
|
||||
DigestAlgorithm digestAlgorithm,
|
||||
List<Integer> apkSigningSchemeIds,
|
||||
String createdBy,
|
||||
OutputManifestFile manifest)
|
||||
throws NoSuchAlgorithmException, InvalidKeyException, CertificateException,
|
||||
SignatureException {
|
||||
if (signerConfigs.isEmpty()) {
|
||||
throw new IllegalArgumentException("At least one signer config must be provided");
|
||||
}
|
||||
|
||||
// For each signer output .SF and .(RSA|DSA|EC) file, then output MANIFEST.MF.
|
||||
List<Pair<String, byte[]>> signatureJarEntries =
|
||||
new ArrayList<>(2 * signerConfigs.size() + 1);
|
||||
byte[] sfBytes =
|
||||
generateSignatureFile(apkSigningSchemeIds, digestAlgorithm, createdBy, manifest);
|
||||
for (SignerConfig signerConfig : signerConfigs) {
|
||||
String signerName = signerConfig.name;
|
||||
byte[] signatureBlock;
|
||||
try {
|
||||
signatureBlock = generateSignatureBlock(signerConfig, sfBytes);
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new InvalidKeyException(
|
||||
"Failed to sign using signer \"" + signerName + "\"", e);
|
||||
} catch (CertificateException e) {
|
||||
throw new CertificateException(
|
||||
"Failed to sign using signer \"" + signerName + "\"", e);
|
||||
} catch (SignatureException e) {
|
||||
throw new SignatureException(
|
||||
"Failed to sign using signer \"" + signerName + "\"", e);
|
||||
}
|
||||
signatureJarEntries.add(Pair.of("META-INF/" + signerName + ".SF", sfBytes));
|
||||
PublicKey publicKey = signerConfig.certificates.get(0).getPublicKey();
|
||||
String signatureBlockFileName =
|
||||
"META-INF/" + signerName + "."
|
||||
+ publicKey.getAlgorithm().toUpperCase(Locale.US);
|
||||
signatureJarEntries.add(
|
||||
Pair.of(signatureBlockFileName, signatureBlock));
|
||||
}
|
||||
signatureJarEntries.add(Pair.of(V1SchemeConstants.MANIFEST_ENTRY_NAME, manifest.contents));
|
||||
return signatureJarEntries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the names of JAR entries which this signer will produce as part of v1 signature.
|
||||
*/
|
||||
public static Set<String> getOutputEntryNames(List<SignerConfig> signerConfigs) {
|
||||
Set<String> result = new HashSet<>(2 * signerConfigs.size() + 1);
|
||||
for (SignerConfig signerConfig : signerConfigs) {
|
||||
String signerName = signerConfig.name;
|
||||
result.add("META-INF/" + signerName + ".SF");
|
||||
PublicKey publicKey = signerConfig.certificates.get(0).getPublicKey();
|
||||
String signatureBlockFileName =
|
||||
"META-INF/" + signerName + "."
|
||||
+ publicKey.getAlgorithm().toUpperCase(Locale.US);
|
||||
result.add(signatureBlockFileName);
|
||||
}
|
||||
result.add(V1SchemeConstants.MANIFEST_ENTRY_NAME);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generated and returns the {@code META-INF/MANIFEST.MF} file based on the provided (optional)
|
||||
* input {@code MANIFEST.MF} and digests of JAR entries covered by the manifest.
|
||||
*/
|
||||
public static OutputManifestFile generateManifestFile(
|
||||
DigestAlgorithm jarEntryDigestAlgorithm,
|
||||
Map<String, byte[]> jarEntryDigests,
|
||||
byte[] sourceManifestBytes) throws ApkFormatException {
|
||||
Manifest sourceManifest = null;
|
||||
if (sourceManifestBytes != null) {
|
||||
try {
|
||||
sourceManifest = new Manifest(new ByteArrayInputStream(sourceManifestBytes));
|
||||
} catch (IOException e) {
|
||||
throw new ApkFormatException("Malformed source META-INF/MANIFEST.MF", e);
|
||||
}
|
||||
}
|
||||
ByteArrayOutputStream manifestOut = new ByteArrayOutputStream();
|
||||
Attributes mainAttrs = new Attributes();
|
||||
// Copy the main section from the source manifest (if provided). Otherwise use defaults.
|
||||
// NOTE: We don't output our own Created-By header because this signer did not create the
|
||||
// JAR/APK being signed -- the signer only adds signatures to the already existing
|
||||
// JAR/APK.
|
||||
if (sourceManifest != null) {
|
||||
mainAttrs.putAll(sourceManifest.getMainAttributes());
|
||||
} else {
|
||||
mainAttrs.put(Attributes.Name.MANIFEST_VERSION, ATTRIBUTE_VALUE_MANIFEST_VERSION);
|
||||
}
|
||||
|
||||
try {
|
||||
ManifestWriter.writeMainSection(manifestOut, mainAttrs);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Failed to write in-memory MANIFEST.MF", e);
|
||||
}
|
||||
|
||||
List<String> sortedEntryNames = new ArrayList<>(jarEntryDigests.keySet());
|
||||
Collections.sort(sortedEntryNames);
|
||||
SortedMap<String, byte[]> invidualSectionsContents = new TreeMap<>();
|
||||
String entryDigestAttributeName = getEntryDigestAttributeName(jarEntryDigestAlgorithm);
|
||||
for (String entryName : sortedEntryNames) {
|
||||
checkEntryNameValid(entryName);
|
||||
byte[] entryDigest = jarEntryDigests.get(entryName);
|
||||
Attributes entryAttrs = new Attributes();
|
||||
entryAttrs.putValue(
|
||||
entryDigestAttributeName,
|
||||
Base64.getEncoder().encodeToString(entryDigest));
|
||||
ByteArrayOutputStream sectionOut = new ByteArrayOutputStream();
|
||||
byte[] sectionBytes;
|
||||
try {
|
||||
ManifestWriter.writeIndividualSection(sectionOut, entryName, entryAttrs);
|
||||
sectionBytes = sectionOut.toByteArray();
|
||||
manifestOut.write(sectionBytes);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Failed to write in-memory MANIFEST.MF", e);
|
||||
}
|
||||
invidualSectionsContents.put(entryName, sectionBytes);
|
||||
}
|
||||
|
||||
OutputManifestFile result = new OutputManifestFile();
|
||||
result.contents = manifestOut.toByteArray();
|
||||
result.mainSectionAttributes = mainAttrs;
|
||||
result.individualSectionsContents = invidualSectionsContents;
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void checkEntryNameValid(String name) throws ApkFormatException {
|
||||
// JAR signing spec says CR, LF, and NUL are not permitted in entry names
|
||||
// CR or LF in entry names will result in malformed MANIFEST.MF and .SF files because there
|
||||
// is no way to escape characters in MANIFEST.MF and .SF files. NUL can, presumably, cause
|
||||
// issues when parsing using C and C++ like languages.
|
||||
for (char c : name.toCharArray()) {
|
||||
if ((c == '\r') || (c == '\n') || (c == 0)) {
|
||||
throw new ApkFormatException(
|
||||
String.format(
|
||||
"Unsupported character 0x%1$02x in ZIP entry name \"%2$s\"",
|
||||
(int) c,
|
||||
name));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class OutputManifestFile {
|
||||
public byte[] contents;
|
||||
public SortedMap<String, byte[]> individualSectionsContents;
|
||||
public Attributes mainSectionAttributes;
|
||||
}
|
||||
|
||||
private static byte[] generateSignatureFile(
|
||||
List<Integer> apkSignatureSchemeIds,
|
||||
DigestAlgorithm manifestDigestAlgorithm,
|
||||
String createdBy,
|
||||
OutputManifestFile manifest) throws NoSuchAlgorithmException {
|
||||
Manifest sf = new Manifest();
|
||||
Attributes mainAttrs = sf.getMainAttributes();
|
||||
mainAttrs.put(Attributes.Name.SIGNATURE_VERSION, ATTRIBUTE_VALUE_SIGNATURE_VERSION);
|
||||
mainAttrs.put(ATTRIBUTE_NAME_CREATED_BY, createdBy);
|
||||
if (!apkSignatureSchemeIds.isEmpty()) {
|
||||
// Add APK Signature Scheme v2 (and newer) signature stripping protection.
|
||||
// This attribute indicates that this APK is supposed to have been signed using one or
|
||||
// more APK-specific signature schemes in addition to the standard JAR signature scheme
|
||||
// used by this code. APK signature verifier should reject the APK if it does not
|
||||
// contain a signature for the signature scheme the verifier prefers out of this set.
|
||||
StringBuilder attrValue = new StringBuilder();
|
||||
for (int id : apkSignatureSchemeIds) {
|
||||
if (attrValue.length() > 0) {
|
||||
attrValue.append(", ");
|
||||
}
|
||||
attrValue.append(String.valueOf(id));
|
||||
}
|
||||
mainAttrs.put(
|
||||
SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME,
|
||||
attrValue.toString());
|
||||
}
|
||||
|
||||
// Add main attribute containing the digest of MANIFEST.MF.
|
||||
MessageDigest md = getMessageDigestInstance(manifestDigestAlgorithm);
|
||||
mainAttrs.putValue(
|
||||
getManifestDigestAttributeName(manifestDigestAlgorithm),
|
||||
Base64.getEncoder().encodeToString(md.digest(manifest.contents)));
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
try {
|
||||
SignatureFileWriter.writeMainSection(out, mainAttrs);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Failed to write in-memory .SF file", e);
|
||||
}
|
||||
String entryDigestAttributeName = getEntryDigestAttributeName(manifestDigestAlgorithm);
|
||||
for (Map.Entry<String, byte[]> manifestSection
|
||||
: manifest.individualSectionsContents.entrySet()) {
|
||||
String sectionName = manifestSection.getKey();
|
||||
byte[] sectionContents = manifestSection.getValue();
|
||||
byte[] sectionDigest = md.digest(sectionContents);
|
||||
Attributes attrs = new Attributes();
|
||||
attrs.putValue(
|
||||
entryDigestAttributeName,
|
||||
Base64.getEncoder().encodeToString(sectionDigest));
|
||||
|
||||
try {
|
||||
SignatureFileWriter.writeIndividualSection(out, sectionName, attrs);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Failed to write in-memory .SF file", e);
|
||||
}
|
||||
}
|
||||
|
||||
// A bug in the java.util.jar implementation of Android platforms up to version 1.6 will
|
||||
// cause a spurious IOException to be thrown if the length of the signature file is a
|
||||
// multiple of 1024 bytes. As a workaround, add an extra CRLF in this case.
|
||||
if ((out.size() > 0) && ((out.size() % 1024) == 0)) {
|
||||
try {
|
||||
SignatureFileWriter.writeSectionDelimiter(out);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Failed to write to ByteArrayOutputStream", e);
|
||||
}
|
||||
}
|
||||
|
||||
return out.toByteArray();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Generates the CMS PKCS #7 signature block corresponding to the provided signature file and
|
||||
* signing configuration.
|
||||
*/
|
||||
private static byte[] generateSignatureBlock(
|
||||
SignerConfig signerConfig, byte[] signatureFileBytes)
|
||||
throws NoSuchAlgorithmException, InvalidKeyException, CertificateException,
|
||||
SignatureException {
|
||||
// Obtain relevant bits of signing configuration
|
||||
List<X509Certificate> signerCerts = signerConfig.certificates;
|
||||
X509Certificate signingCert = signerCerts.get(0);
|
||||
PublicKey publicKey = signingCert.getPublicKey();
|
||||
DigestAlgorithm digestAlgorithm = signerConfig.signatureDigestAlgorithm;
|
||||
Pair<String, AlgorithmIdentifier> signatureAlgs =
|
||||
getSignerInfoSignatureAlgorithm(publicKey, digestAlgorithm,
|
||||
signerConfig.deterministicDsaSigning);
|
||||
String jcaSignatureAlgorithm = signatureAlgs.getFirst();
|
||||
|
||||
// Generate the cryptographic signature of the signature file
|
||||
byte[] signatureBytes;
|
||||
try {
|
||||
Signature signature = Signature.getInstance(jcaSignatureAlgorithm);
|
||||
signature.initSign(signerConfig.privateKey);
|
||||
signature.update(signatureFileBytes);
|
||||
signatureBytes = signature.sign();
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new InvalidKeyException("Failed to sign using " + jcaSignatureAlgorithm, e);
|
||||
} catch (SignatureException e) {
|
||||
throw new SignatureException("Failed to sign using " + jcaSignatureAlgorithm, e);
|
||||
}
|
||||
|
||||
// Verify the signature against the public key in the signing certificate
|
||||
try {
|
||||
Signature signature = Signature.getInstance(jcaSignatureAlgorithm);
|
||||
signature.initVerify(publicKey);
|
||||
signature.update(signatureFileBytes);
|
||||
if (!signature.verify(signatureBytes)) {
|
||||
throw new SignatureException("Signature did not verify");
|
||||
}
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new InvalidKeyException(
|
||||
"Failed to verify generated " + jcaSignatureAlgorithm + " signature using"
|
||||
+ " public key from certificate",
|
||||
e);
|
||||
} catch (SignatureException e) {
|
||||
throw new SignatureException(
|
||||
"Failed to verify generated " + jcaSignatureAlgorithm + " signature using"
|
||||
+ " public key from certificate",
|
||||
e);
|
||||
}
|
||||
|
||||
AlgorithmIdentifier digestAlgorithmId =
|
||||
getSignerInfoDigestAlgorithmOid(digestAlgorithm);
|
||||
AlgorithmIdentifier signatureAlgorithmId = signatureAlgs.getSecond();
|
||||
try {
|
||||
return ApkSigningBlockUtils.generatePkcs7DerEncodedMessage(
|
||||
signatureBytes,
|
||||
null,
|
||||
signerCerts, digestAlgorithmId,
|
||||
signatureAlgorithmId);
|
||||
} catch (Asn1EncodingException | CertificateEncodingException ex) {
|
||||
throw new SignatureException("Failed to encode signature block");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private static String getEntryDigestAttributeName(DigestAlgorithm digestAlgorithm) {
|
||||
switch (digestAlgorithm) {
|
||||
case SHA1:
|
||||
return "SHA1-Digest";
|
||||
case SHA256:
|
||||
return "SHA-256-Digest";
|
||||
default:
|
||||
throw new IllegalArgumentException(
|
||||
"Unexpected content digest algorithm: " + digestAlgorithm);
|
||||
}
|
||||
}
|
||||
|
||||
private static String getManifestDigestAttributeName(DigestAlgorithm digestAlgorithm) {
|
||||
switch (digestAlgorithm) {
|
||||
case SHA1:
|
||||
return "SHA1-Digest-Manifest";
|
||||
case SHA256:
|
||||
return "SHA-256-Digest-Manifest";
|
||||
default:
|
||||
throw new IllegalArgumentException(
|
||||
"Unexpected content digest algorithm: " + digestAlgorithm);
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.apksig.internal.apk.v2;
|
||||
|
||||
/** Constants used by the V2 Signature Scheme signing and verification. */
|
||||
public class V2SchemeConstants {
|
||||
private V2SchemeConstants() {}
|
||||
|
||||
public static final int APK_SIGNATURE_SCHEME_V2_BLOCK_ID = 0x7109871a;
|
||||
public static final int STRIPPING_PROTECTION_ATTR_ID = 0xbeeff00d;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user