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

This commit is contained in:
2025-09-16 20:46:46 -04:00
commit 9d30169a8d
13378 changed files with 7050105 additions and 0 deletions

View 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`

View 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>

View 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
}
}

View File

@@ -0,0 +1,2 @@
*
!.gitignore

View 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"))
}

View 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
}

View 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

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View 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>

View 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'

View 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);
}
}

View 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")
}

View 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"
}

View File

@@ -0,0 +1 @@
!/debug

View File

@@ -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()

View File

@@ -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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -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
}
}

View 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>

File diff suppressed because it is too large Load Diff

View File

@@ -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();
}
}

View File

@@ -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

View File

@@ -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";
}

View File

@@ -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;
}
}

View File

@@ -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`._

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}
}
}

View File

@@ -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";
}

View File

@@ -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);
}
}
}

View File

@@ -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