Compare commits

..

10 Commits

Author SHA1 Message Date
5a826234d0 fix 2020-12-03 14:55:02 +08:00
9f5ffb084a 修复APP更新逻辑,以VersionName为准
修复App更新无法下载安装包问题
2020-11-23 18:38:01 +08:00
b5d4cbe718 更新启动弹窗隐私协议和用户协议内容 2020-11-20 14:20:49 +08:00
b39a47c0bd 光环马甲包4.3.8修改 2020-11-20 14:18:39 +08:00
5ea6647bf7 Merge branch 'dev' of gitlab.ghzhushou.com:halo/assistant-android into video_only
# Conflicts:
#	app/src/main/java/com/gh/gamecenter/LoginActivity.java
#	app/src/main/java/com/gh/gamecenter/MainActivity.java
#	app/src/main/java/com/gh/gamecenter/SplashScreenActivity.java
#	app/src/main/java/com/gh/gamecenter/collection/CollectionWrapperFragment.java
#	app/src/main/java/com/gh/gamecenter/fragment/MainWrapperFragment.java
#	app/src/main/java/com/gh/gamecenter/history/HistoryWrapperFragment.kt
#	app/src/main/java/com/gh/gamecenter/personal/PersonalViewModel.kt
#	app/src/main/java/com/gh/gamecenter/personalhome/UserHomeFragment.kt
#	app/src/main/res/layout/fragment_home.xml
#	app/src/main/res/layout/fragment_login.xml
#	app/src/main/res/layout/fragment_personal.xml
2020-11-13 16:04:36 +08:00
798c4a2089 update 2020-04-29 10:59:39 +08:00
0686f70df6 更新到3.7.6 2020-04-29 10:51:28 +08:00
3c30564ab6 update 2020-04-10 19:10:11 +08:00
f04070e846 update 2020-04-02 18:39:01 +08:00
fc279839fa 光环助手马甲包——视频 https://gitlab.ghzs.com/pm/yunying/issues/1051 2020-04-02 15:07:11 +08:00
3487 changed files with 47428 additions and 167742 deletions

1
.gitignore vendored
View File

@ -7,6 +7,5 @@ local.properties
captures/
build/
release-app/
test-app/
scripts/apk-channel/
app/src/test/java/com/gh/gamecenter

2
.gitmodules vendored
View File

@ -1,4 +1,4 @@
[submodule "libraries/LGLibrary"]
path = libraries/LGLibrary
url = git@git.ghzs.com:android/common-library.git
url = git@gitlab.ghzs.com:android/common-library.git
branch = master

View File

@ -1,68 +1,69 @@
# 光环助手Android客户端
### 概述
### APK打包配置
光环助手Android客户端目前使用 Kotlin 作为主要开发语言,以 MVVM 作为参考架构模式进行开发
* 使用[ApkChannelPackage](https://github.com/ltlovezh/ApkChannelPackage)的方案
* 打包命令,视情况使用:
### 约束
> 打包Tinker基准包`./scripts/tinker_release_base.sh`
为编写易读易维护且较健壮的代码,可参考以下约束
> 以Tinker基准包打渠道包`./scripts/tinker_release_channel.sh`
1. 尽量将逻辑代码放置于 ViewModel 中View 中只执行 UI 操作
2. 尽量使 View 在被销毁之后仍能恢复状态,处理方式可参考 [保存界面状态](https://developer.android.com/topic/libraries/architecture/saving-states)
3. 尽量参考原有文件结构及命名规范,即以 大模块 - 小模块 的形式生成包关系
4. 遵循最小改动原则,在提交代码前务必先检查变动的代码,尽量以可控的变动规模来构成一个 commit ,以便日后追踪问题
5. 代码规范可参考 [AOSP Java 风格](https://source.android.com/setup/contribute/code-style)
6. 尽量使用 Kotlin 来写新文件
7. 尽量不要使用 DataBinding因为回影响编译性能
8. Commit 前请确保不带入非项目必须文件,可手动修改 [.gitignore](https://stackoverflow.com/questions/8527597/how-do-i-ignore-files-in-a-directory-in-git) 文件忽略
9. 新页面请勿使用 ButterKnife 来进行 View 获取和绑定,请使用 ViewBinding
10. No AsyncTask!
> 以Tinker基准包打补丁包`./scripts/tinker_release_patch.sh`
### 公用部分
### 混淆配置
本项目使用 LiveData 实现了一个简单通用的基础列表分页功能,具体可见 `ListFragment`, `ListViewModel` 等类,理想情况下只需少量代码即可新建一个简单分页列表
* 配置文件Android默认配置+proguard-rules.txt等
* 参考libraries下每个项目独立的配置文件`proguard-project.txt`
### 首次拉取项目代码
### apk大小优化
`git clone -b dev git@git.ghzs.com:halo/android/assistant-android.git --recursive`
### git 版本管理
本项目使用简化版的 git flow 来管理分支,细节请看 [光环安卓简单 git 规范](https://git.ghzs.com/halo/android/assistant-android/-/wikis/%E5%85%89%E7%8E%AF%E5%AE%89%E5%8D%93%E7%AE%80%E5%8D%95-git-%E8%A7%84%E8%8C%83)
### API 环境配置
本项目使用 Build Variants 来切换 API 环境
* internal 为测试环境
* publish 为正式环境
### 图片资源配置
* 新增图片资源时,默认只添加最高规格的 xxxhdpi 文件
* 新增图片资源时,需要将其转换为 .webp 格式 (包括含透明图层的图片默认质量为90%) (转换后体积变大的文件除外)
* 限制resConfig资源集
* 开启ShrinkResources
* 开启混淆使用minifyEnabled(仅在release开启
* pngquant对png压缩、png/jpg->webp(未尝试)
### 第三方appkey等配置
* 修改`gradle.properties`文件将各种key填入其中实现统一管理
* 通过gradle文件内的resValue/buildConfigField/manifestPlaceHolder方式实现编译期间修改具体情况请参考``./build.gradle``和``./app/build.gradle``配置
### 混淆配置
### 拉取代码步骤
* 本项目使用了微信的 [AndResGuard](https://github.com/shwenzhang/AndResGuard) 作为资源混淆压缩方案,新增需要使用 `getIdentifier` 获取的资源文件时需要添加至白名单
* 本项目默认使用 R8 作为混淆工具,往 proguard-rules.txt 添加 proguard 新配置项时请检查可用性(如语法等)
1. 拉取主项目代码: `git clone -b dev git@gitlab.ghzhushou.com:halo/assistant-android.git`
2. 初始化公用类库: `bash ./scripts/submodules_init.sh`
### APK打包配置
### submodule管理方式(只拉取master)
* 本项目使用了 [VasDolly](https://github.com/Tencent/VasDolly) 作为渠道包实现方案
* 打包命令,具体参数请见相应文件:
> 打内部测试包:`./scripts/test_build.sh`
> 打正式发布包:`./scripts/build_with_simple_backup.sh`
* 提交代码需要cd到submodule文件夹去做修改
* 更新远端代码,`bash ./scripts/submodules_update.sh`
### TODO
* 把原有 EventBus 的消息 Type 统一一个文件内
* 将实现细节从 View(Fragment、Activity) 剥离并以 MVVM 结构改造
* 重构 MainActivity
* GSON 序列化用统一一个, GsonUtil fromJson
* CleanApkAdapter 转化字符串size工具函数 比如SpeedUtils
* getString 解决 字符串hardcode问题
* ~~Adapter 里面clicklistener 用接口传参将点击操作委托给controller~~
* ~~Adapter ViewHolder的功能部分重写到ViewHolder类本身~~
* ~~activity 统一入口未完成(外部入口相关)去除多余activity使用统一toolbar~~
* ~~release / debug compile不同的类库不需要再做什么开关~~
* ~~Toolbar分离有图形按钮/没有图形按钮~~
### TODO Since 3.1
- 解决 Utils 工具类引发的内存泄漏问题
- 把原有 EventBus 的消息 Type 统一到一个文件内
- 将实现细节从 View(Fragment、Activity) 剥离并以 MVVM 结构改造
- ~~将 ListViewModel 所对应的 ListRepository 合并到 ListViewModel 中~~
- 依照光环助手界面功能以大模块 - 小模块的方式去修改包结构,包内文件建议以包名摘要作为前缀
- ~~使用 RxJava 的 Debounce 和 Map 操作优化搜索触发机制 参考资料:[1](https://proandroiddev.com/building-an-autocompleting-edittext-using-rxjava-f69c5c3f5a40),[2](https://medium.com/@kurtisnusbaum/rxandroid-basics-part-2-6e877af352)~~
- ~~把 ListViewModel 的数据结构类型转换方式换为抽象方法,让继承的类实现,避免出现无响应的问题~~
- ~~rxjava2 如果接口返回为空 会发生异常:java.lang.NullPointerException: Null is not a valid element (答案编辑) 解决方法->com.gh.gamecenter.retrofit.Response~~
- constraintLayout 1.1.2 导致布局出现异常(问题编辑标签选择弹窗)
- 搞清楚 GameManager 的用途,看能不能去掉
- 重构一下 MainActivity

View File

@ -2,10 +2,11 @@ apply plugin: 'com.android.application'
apply plugin: 'kotlin-android' // kotlin
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
//apply plugin: 'io.sentry.android.gradle'
apply plugin: 'AndResGuard'
import groovy.xml.XmlUtil
// apkChannelPackage
apply plugin: 'channel'
//apply from: 'tinker-support.gradle'
android {
@ -13,9 +14,8 @@ android {
experimental = true
}
buildFeatures {
viewBinding = true
dataBinding = true
dataBinding {
enabled = true
}
compileOptions {
@ -32,10 +32,6 @@ android {
javaMaxHeapSize "4g"
}
aaptOptions {
additionalParameters "--no-version-vectors"
}
defaultConfig {
multiDexEnabled true
@ -46,8 +42,7 @@ android {
}
ndk {
// 如果不添加 `arm64` 调用系统的 PackageManager 的方法读取安装包信息的时候会出现 native 层闪退,草
abiFilters "armeabi-v7a", "arm64-v8a", "x86"
abiFilters "armeabi-v7a"
}
renderscriptTargetApi 18
@ -77,8 +72,6 @@ android {
buildConfigField "String", "TTAD_APPID", "\"${TTAD_APPID}\""
buildConfigField "String", "DOUYIN_CLIENTKEY", "\"${DOUYIN_CLIENTKEY}\""
buildConfigField "String", "DOUYIN_CLIENTSECRET", "\"${DOUYIN_CLIENTSECRET}\""
buildConfigField "String", "QUICK_LOGIN_APPID", "\"${QUICK_LOGIN_APPID}\""
buildConfigField "String", "QUICK_LOGIN_APPKEY", "\"${QUICK_LOGIN_APPKEY}\""
buildConfigField "String", "MIPUSH_APPID", "\"${MIPUSH_APPID}\""
buildConfigField "String", "MIPUSH_APPKEY", "\"${MIPUSH_APPKEY}\""
@ -132,27 +125,15 @@ android {
}
}
flavorDimensions("env")
sourceSets {
publish {
java.srcDirs = ['src/main/java']
}
internal {
java.srcDirs = ['src/main/java']
}
tea {
java.srcDirs = ['src/main/java', 'src/tea/java']
}
gdt {
java.srcDirs = ['src/main/java', 'src/gdt/java']
}
}
flavorDimensions "nonsense"
/**
* 多渠道打包,渠道请参考"channel.txt"文件所有渠道值均通过java code设置
*/
productFlavors {
// publish release host
publish {
dimension "env"
dimension "nonsense"
buildConfigField "String", "API_HOST", "\"${API_HOST}\""
buildConfigField "String", "SENSITIVE_API_HOST", "\"${SENSITIVE_API_HOST}\""
@ -163,7 +144,7 @@ android {
}
// internal test dev host
internal {
dimension "env"
dimension "nonsense"
versionNameSuffix "-debug"
buildConfigField "String", "API_HOST", "\"${DEV_API_HOST}\""
@ -174,35 +155,26 @@ android {
buildConfigField "String", "UMENG_MESSAGE_SECRET", "\"${DEV_UMENG_MESSAGE_SECRET}\""
buildConfigField "String", "BUGLY_APPID", "\"${DEV_BUGLY_APPID}\""
}
tea {
dimension "env"
buildConfigField "String", "API_HOST", "\"${API_HOST}\""
buildConfigField "String", "SENSITIVE_API_HOST", "\"${SENSITIVE_API_HOST}\""
buildConfigField "String", "UMENG_APPKEY", "\"${UMENG_APPKEY}\""
buildConfigField "String", "UMENG_MESSAGE_SECRET", "\"${UMENG_MESSAGE_SECRET}\""
buildConfigField "String", "BUGLY_APPID", "\"${BUGLY_APPID}\""
manifestPlaceholders.put("APPLOG_SCHEME", "rangersapplog.byAx6uYt".toLowerCase())
}
gdt {
dimension "env"
buildConfigField "String", "API_HOST", "\"${API_HOST}\""
buildConfigField "String", "SENSITIVE_API_HOST", "\"${SENSITIVE_API_HOST}\""
buildConfigField "String", "UMENG_APPKEY", "\"${UMENG_APPKEY}\""
buildConfigField "String", "UMENG_MESSAGE_SECRET", "\"${UMENG_MESSAGE_SECRET}\""
buildConfigField "String", "BUGLY_APPID", "\"${BUGLY_APPID}\""
}
}
}
// apkChannelPackage
channel {
//多渠道包的输出目录默认为new File(project.buildDir,"channel")
baseOutputDir = new File(project.buildDir, "channel")
//多渠道包的命名规则,默认为:${appName}-${versionName}-${versionCode}-${flavorName}-${buildType}
apkNameFormat = '${appName}-${versionName}-${versionCode}-${flavorName}-${buildType}'
}
rebuildChannel {
// baseDebugApk = 已有Debug APK
// baseReleaseApk = 已有Release APK
// //默认为new File(project.buildDir, "rebuildChannel/debug")
// debugOutputDir = Debug渠道包输出目录
// //默认为new File(project.buildDir, "rebuildChannel/release")
// releaseOutputDir = Release渠道包输出目录
}
repositories {
flatDir {
dirs 'libs', 'libs/aars'
@ -212,17 +184,18 @@ repositories {
dependencies {
implementation fileTree(include: ['*.jar', '*.aar'], dir: 'libs')
gdtImplementation fileTree(include: ['*.jar', '*.aar'], dir: 'src/gdt/libs')
testImplementation 'junit:junit:4.12'
debugImplementation "com.squareup.leakcanary:leakcanary-android:${leakcanary}"
debugImplementation "com.facebook.stetho:stetho:${stetho}"
debugImplementation "com.facebook.stetho:stetho-okhttp3:${stetho}"
debugImplementation "com.squareup.okhttp3:logging-interceptor:${okHttp}"
// debugImplementation "com.gu.android:toolargetool:${toolargetool}" // 需要使用调试时才启用
debugImplementation "com.github.nichbar:WhatTheStack:${whatTheStack}"
debugImplementation "com.gu.android:toolargetool:${toolargetool}"
debugImplementation "com.github.nichbar:WhatTheStack:$whatTheStack"
implementation "androidx.core:core-ktx:${core}"
implementation "androidx.fragment:fragment-ktx:${fragment}"
implementation "androidx.core:core:${core}"
implementation "androidx.fragment:fragment:${fragment}"
implementation "androidx.multidex:multidex:${multiDex}"
implementation "androidx.appcompat:appcompat:${appCompat}"
implementation "androidx.cardview:cardview:${cardView}"
@ -243,10 +216,8 @@ dependencies {
implementation "com.kyleduo.switchbutton:library:${switchButton}"
implementation "com.facebook.fresco:fresco:${fresco}"
implementation "com.facebook.fresco:animated-gif-lite:${fresco}"
implementation "com.facebook.fresco:animated-gif:${fresco}"
implementation "com.facebook.fresco:animated-drawable:${fresco}"
implementation "com.facebook.fresco:animated-webp:${fresco}"
implementation "com.facebook.fresco:webpsupport:${fresco}"
implementation "com.squareup.okhttp3:okhttp:${okHttp}"
@ -275,6 +246,8 @@ dependencies {
implementation "com.daimajia.swipelayout:library:${swipeLayout}"
implementation "com.sina.weibo.sdk:core:${weiboSDK}"
// bugly with tinker support
// implementation "com.tencent.bugly:crashreport_upgrade:${buglyTinkerSupport}"
@ -297,11 +270,11 @@ dependencies {
implementation "com.squareup.picasso:picasso:${picasso}"
// for video streaming
implementation("com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-java:$gsyVideo", {
implementation("com.shuyu:gsyVideoPlayer-java:$gsyVideo", {
exclude module: "gsyvideoplayer-androidvideocache"
exclude group: "tv.danmaku.ijk.media"
})
implementation "com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-exo_player2:$gsyVideo"
implementation "com.shuyu:GSYVideoPlayer-exo2:$gsyVideo"
implementation "android.arch.work:work-runtime:${workManager}"
@ -312,39 +285,27 @@ dependencies {
implementation "com.ethanhua:skeleton:${skeleton}"
implementation "io.supercharge:shimmerlayout:${shimmerlayout}"
implementation "com.tencent.mm.opensdk:wechat-sdk-android-without-mta:${mta}"
implementation "com.github.nichbar:AndroidRomChecker:${romChecker}"
implementation "com.walkud.rom.checker:RomChecker:${romChecker}"
debugImplementation "com.github.nichbar.chucker:library:${chucker}"
releaseImplementation "com.github.nichbar.chucker:library-no-op:${chucker}"
teaImplementation "com.bytedance.applog:RangersAppLog-Lite-cn:${bytedanceApplog}"
debugImplementation "com.github.nichbar.chucker:library:$chucker"
releaseImplementation "com.github.nichbar.chucker:library-no-op:$chucker"
// implementation "com.bytedance.applog:RangersAppLog-Lite-cn:$bytedanceApplog"
// implementation "com.bytedance.ies.ugc.aweme:opensdk-china-external:$bytedanceAweme"
// implementation "com.bytedance.ies.ugc.aweme:opensdk-common:$bytedanceAweme"
implementation "com.aliyun.dpa:oss-android-sdk:${oss}"
implementation "com.airbnb.android:lottie:${lottie}"
implementation "com.airbnb.android:lottie:$lottie"
implementation "net.lingala.zip4j:zip4j:${zip4j}"
// plugin 需要字符串,故不能用值
implementation "io.sentry:sentry-android:3.2.0"
implementation("com.github.piasy:BigImageViewer:${bigImageViewer}", {
implementation("com.github.piasy:BigImageViewer:$bigImageViewer", {
exclude group: 'com.squareup.okhttp3'
exclude group: 'androidx.swiperefreshlayout'
exclude group: 'com.github.bumptech.glide'
exclude group: 'com.facebook.fresco'
})
implementation "com.github.PhilJay:MPAndroidChart:${chart}"
implementation "com.github.hsiafan:apk-parser:${apkParser}"
implementation "org.nanohttpd:nanohttpd:${nanohttpd}"
implementation "com.aliyun.openservices:aliyun-log-android-sdk:${aliyunLog}"
implementation "com.github.princekin-f:EasyFloat:${easyFloat}"
implementation "io.github.florent37:shapeofview:$shapeOfView"
implementation 'io.github.sinaweibosdk:core:11.6.0@aar'
implementation project(':libraries:LGLibrary')
// implementation project(':libraries:MTA')
implementation project(':libraries:QQShare')
@ -410,159 +371,3 @@ if (propFile.exists()) {
// task.name.startsWith('merge') && task.name.endsWith('Resources')
// }.each { t -> t.dependsOn generateMetaJson }
//}
andResGuard {
mappingFile = null
use7zip = true
useSign = true
// 打开这个开关会keep住所有资源的原始路径只混淆资源的名字
keepRoot = false
// 设置这个值会把arsc name列混淆成相同的名字减少string常量池的大小
fixedResName = "arg"
// 打开这个开关会合并所有哈希值相同的资源,但请不要过度依赖这个功能去除去冗余资源
mergeDuplicatedRes = true
whiteList = [
"R.drawable.icon",
"R.drawable.bg_notification_answer_style_1",
"R.drawable.bg_notification_answer_style_2",
"R.drawable.bg_notification_article_style_1",
"R.drawable.bg_notification_article_style_2",
"R.drawable.bg_notification_feedback_style_1",
"R.drawable.bg_notification_feedback_style_2",
"R.drawable.bg_notification_gift_style_1",
"R.drawable.bg_notification_gift_style_2",
"R.drawable.bg_notification_login_style_1",
"R.drawable.bg_notification_login_style_2",
"R.drawable.bg_notification_question_style_1",
"R.drawable.bg_notification_question_style_2",
"R.drawable.bg_notification_rating_style_1",
"R.drawable.bg_notification_rating_style_2",
"R.drawable.bg_notification_reserve_game_style_1",
"R.drawable.bg_notification_reserve_game_style_2",
"R.drawable.bg_notification_video_style_1",
"R.drawable.bg_notification_video_style_2",
"R.drawable.ic_search_no_1",
"R.drawable.ic_search_no_2",
"R.drawable.ic_search_no_3",
"R.drawable.ic_search_no_4",
"R.drawable.ic_search_no_5",
"R.drawable.ic_search_no_6",
"R.drawable.ic_search_no_7",
"R.drawable.ic_search_no_8",
"R.drawable.ic_search_no_9",
"R.drawable.ic_search_no_10",
"R.drawable.ic_search_no_11",
"R.drawable.ic_search_no_12",
"R.drawable.ic_search_no_13",
"R.drawable.ic_search_no_14",
"R.drawable.ic_search_no_15",
"R.drawable.ic_search_no_16",
"R.drawable.ic_search_no_17",
"R.drawable.ic_search_no_18",
"R.drawable.ic_search_no_19",
"R.drawable.ic_search_no_20",
"R.drawable.ic_recommend_activity",
"R.drawable.ic_recommend_discount",
"R.drawable.ic_recommend_function",
"R.drawable.ic_recommend_gift",
"R.drawable.ic_recommend_role",
"R.drawable.login_btn_bg",
"R.drawable.ic_quick_login_check",
"R.drawable.ic_quick_login_uncheck",
"R.anim.anim_auth_in",
"R.anim.anim_auth_out",
]
compressFilePattern = [
"*.png",
"*.jpg",
"*.jpeg",
"*.gif",
]
sevenzip {
artifact = 'com.tencent.mm:SevenZip:1.2.20'
}
}
project.afterEvaluate {
def variants = null
try {
variants = android.applicationVariants
} catch (Throwable t) {
t.printStackTrace()
try {
variants = android.libraryVariants
} catch (Throwable tt) {
tt.printStackTrace()
}
}
if (variants != null) {
variants.all { variant ->
variant.outputs.each { output ->
def task = output.processManifestProvider.get()
if (task == null) {
return
}
/**
* 为 Manifest 的 Activity 的 configChanges 添加自己手动处理 configurationChanges 配置 [https://developer.android.com/guide/topics/resources/runtime-changes]
* AGP 4.1.0 从 ProcessManifest task 里拿 manifest 的 API 变更调整可以参考这里 [https://github.com/Tencent/tinker/pull/1476/commits/d71645729b13d545ca4ba6826f93fbf558751434]
* (搞半天还是不会抽离方法,有空再把 gradle 改成用 kotlin 实现吧)
*/
task.doLast {
def manifestFile = new File(multiApkManifestOutputDirectory.get().asFile, "AndroidManifest.xml")
if (manifestFile == null || !manifestFile.exists()) {
return
}
String[] configChanges = [
"density",
"fontScale",
"keyboard",
"keyboardHidden",
"layoutDirection",
"locale",
"mcc",
"mnc",
"navigation",
"orientation",
"screenLayout",
"screenSize",
"smallestScreenSize",
"touchscreen",
"uiMode"]
def parser = new XmlSlurper(false, true)
def manifest = parser.parse(manifestFile)
def app = manifest.'application'[0]
app.'activity'.each { act ->
String value = act.attributes()['android:configChanges']
if (value == null || value.isEmpty()) {
if (value == null) value = ""
configChanges.eachWithIndex { config, index ->
if (index != configChanges.length - 1) {
value += config + "|"
} else {
value += config
}
}
act.attributes()['androidconfigChanges'] = value
} else {
String[] valueSplit = value.split("\\|")
println configChanges
configChanges.eachWithIndex { config, index ->
if (!valueSplit.contains(config)) {
value += ("|" + config)
}
}
act.attributes()['android:configChanges'] = value
}
}
def tmpManifest = XmlUtil.serialize(manifest).replaceAll("androidconfigChanges", "android:configChanges")
manifest = parser.parseText(tmpManifest)
manifestFile.setText(XmlUtil.serialize(manifest), "utf-8")
}
}
}
}
}

Binary file not shown.

View File

@ -1,271 +0,0 @@
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in C:\Android\sdk/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
#--------- remove logs start ----------------
-assumenosideeffects class com.lightgame.config.CommonDebug {
private static String getLogTag(...);
private static String getMethodName();
public static void logMethodName(...);
public static void logParams(...);
public static void logFields(...);
public static void logMethodWithParams(...);
}
#-assumenosideeffects class com.lightgame.config.CommonDebug {*;}
#-dontoptimize
#--------- remove logs end ----------------
-keepattributes *Annotation*,Signature,InnerClasses,EnclosingMethod
-dontwarn InnerClasses
# OrmLite uses reflection
-keep class com.j256.**
-keepclassmembers class com.j256.** { *; }
-keep enum com.j256.**
-keepclassmembers enum com.j256.** { *; }
-keep interface com.j256.**
-keepclassmembers interface com.j256.** { *; }
-dontwarn com.j256.**
#okhttp3
-dontwarn com.squareup.okhttp3.**
-dontwarn okio.**
-keep class com.squareup.okhttp3.** { *;}
# stetho
-keep class com.facebook.stetho.** { *; }
-dontwarn com.facebook.stetho.**
# Retrofit 2.2
# Platform calls Class.forName on types which do not exist on Android to determine platform.
-dontnote retrofit2.Platform
# Platform used when running on Java 8 VMs. Will not be used at runtime.
-dontwarn retrofit2.Platform$Java8
# Retain generic type information for use by reflection by converters and adapters.
-keepattributes Signature
# Retain declared checked exceptions for use by a Proxy instance.
-keepattributes Exceptions
# Retrofit 2.X
## https://square.github.io/retrofit/ ##
-dontwarn retrofit2.**
-keep class retrofit2.** { *; }
-keepattributes Signature
-keepattributes Exceptions
-keepclasseswithmembers class * {
@retrofit2.http.* <methods>;
}
# rxjava
-keep class rx.schedulers.Schedulers {
public static <methods>;
}
-keep class rx.schedulers.ImmediateScheduler {
public <methods>;
}
-keep class rx.schedulers.TestScheduler {
public <methods>;
}
-keep class rx.schedulers.Schedulers {
public static ** test();
}
-keepclassmembers class rx.internal.util.unsafe.*ArrayQueue*Field* {
long producerIndex;
long consumerIndex;
}
-keepclassmembers class rx.internal.util.unsafe.BaseLinkedQueueProducerNodeRef {
long producerNode;
long consumerNode;
}
-dontwarn rx.internal.util.**
## AutoScrollViewPager
-keep class cn.trinea.android.** { *; }
-keepclassmembers class cn.trinea.android.** { *; }
-dontwarn cn.trinea.android.**
## butterknife
# Retain generated class which implement Unbinder.
#-keep public class * implements butterknife.Unbinder { public <init>(**, android.view.View); }
#
## Prevent obfuscation of types which use ButterKnife annotations since the simple name
## is used to reflectively look up the generated ViewBinding.
#-keep class butterknife.*
#-keepclasseswithmembernames class * { @butterknife.* <methods>; }
#-keepclasseswithmembernames class * { @butterknife.* <fields>; }
-dontwarn butterknife.internal.**
-keep class **$$ViewInjector { *; }
-keepnames class * { @butterknife.InjectView *;}
-dontwarn butterknife.Views$InjectViewProcessor
-dontwarn com.gc.materialdesign.views.**
# eventbus
-keepattributes *Annotation*
-keepclassmembers class ** {
@org.greenrobot.eventbus.Subscribe <methods>;
}
-keep enum org.greenrobot.eventbus.ThreadMode { *; }
# Only required if you use AsyncExecutor
-keepclassmembers class * extends org.greenrobot.eventbus.util.ThrowableFailureEvent {
<init>(java.lang.Throwable);
}
# weiboSdk
-keep class com.sina.weibo.sdk.** { *; }
-dontwarn android.webkit.WebView
-dontwarn android.webkit.WebViewClient
# app models
-keep class com.gh.common.view.** {*;}
-keep class com.gh.gamecenter.db.info.** {*;}
-keep class com.gh.gamecenter.entity.** {*;}
-keep class com.gh.gamecenter.qa.entity.** {*;}
-keep class com.gh.gamecenter.retrofit.** {*;}
-keep class com.gh.gamecenter.eventbus.** {*;}
-keep class com.gh.gamecenter.video.detail.** {*;}
-keep class * extends rx.Subscriber
#---------------------------------webview------------------------------------
-keepclassmembers class * extends android.webkit.WebViewClient {
public void *(android.webkit.WebView, java.lang.String, android.graphics.Bitmap);
public boolean *(android.webkit.WebView, java.lang.String);
}
-keepclassmembers class * extends android.webkit.WebViewClient {
public void *(android.webkit.WebView, java.lang.String);
}
#----------------------------------------------------------------------------
##---------------Begin: proguard configuration for Gson ----------
# Gson uses generic type information stored in a class file when working with fields. Proguard
# removes such information by default, so configure it to keep all of it.
-keepattributes Signature
# For using GSON @Expose annotation
-keepattributes *Annotation*
# Gson specific classes
-keep class sun.misc.Unsafe { *; }
#-keep class com.google.gson.stream.** { *; }
# Prevent proguard from stripping interface information from TypeAdapterFactory,
# JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter)
-keep class * implements com.google.gson.TypeAdapterFactory
-keep class * implements com.google.gson.JsonSerializer
-keep class * implements com.google.gson.JsonDeserializer
-keepclassmembers enum * { *; }
##---------------End: proguard configuration for Gson ----------
# ------ bugly ---------
-dontwarn com.tencent.bugly.**
-keep public class com.tencent.bugly.**{*;}
# easypermission
-keepclassmembers class * {
@pub.devrel.easypermissions.AfterPermissionGranted <methods>;
}
# 重命名文件为SourceFile再配合mapping符号表可以拿到真实的类名
-renamesourcefileattribute SourceFile
# 保留源文件行号
-keepattributes SourceFile,LineNumberTable
-ignorewarnings
-keep @androidx.annotation.Keep class *
-keepclassmembers class ** {
@androidx.annotation.Keep *;
}
-keep class com.gh.loghub.** { *; }
### greenDAO 3
-keepclassmembers class * extends org.greenrobot.greendao.AbstractDao {
public static java.lang.String TABLENAME;
}
-keep class **$Properties
-keep class org.greenrobot.greendao.** { *; }
# If you do not use SQLCipher:
-dontwarn org.greenrobot.greendao.database.**
# If you do not use RxJava:
-dontwarn rx.**
-dontwarn org.greenrobot.greendao.rx.**
-dontwarn org.greenrobot.greendao.**
### fastJson
-dontwarn com.alibaba.fastjson.**
-keep class com.alibaba.fastjson.** { *; }
-keepattributes Signature
-keepattributes Annotation
### 广点通
-dontwarn com.qq.gdt.action.**
-keep class com.qq.gdt.action.** {*;}
### AndroidX
-keep class androidx.core.app.CoreComponentFactory { *; }
#阿里云上传
-keep class com.alibaba.sdk.android.oss.** { *; }
-dontwarn okio.**
-dontwarn org.apache.commons.codec.binary.**
#视频相关
-keep class com.shuyu.gsyvideoplayer.video.** { *; }
-dontwarn com.shuyu.gsyvideoplayer.video.**
-keep class com.shuyu.gsyvideoplayer.video.base.** { *; }
-dontwarn com.shuyu.gsyvideoplayer.video.base.**
-keep class com.shuyu.gsyvideoplayer.utils.** { *; }
-dontwarn com.shuyu.gsyvideoplayer.utils.**
-keep class tv.danmaku.ijk.** { *; }
-dontwarn tv.danmaku.ijk.**
-keep public class * extends android.view.View{
*** get*();
void set*(***);
public <init>(android.content.Context);
public <init>(android.content.Context, android.util.AttributeSet);
public <init>(android.content.Context, android.util.AttributeSet, int);
}
#穿山甲
-keep class com.bytedance.sdk.openadsdk.** { *; }
-keep public interface com.bytedance.sdk.openadsdk.downloadnew.** {*;}
-keep class com.pgl.sys.ces.* {*;}
-keep class com.gyf.immersionbar.* {*;}
-dontwarn com.gyf.immersionbar.**
-keep class com.taobao.securityjni.**{*;}
-keep class com.taobao.wireless.security.**{*;}
-keep class com.ut.secbody.**{*;}
-keep class com.taobao.dp.**{*;}
-keep class com.alibaba.wireless.security.**{*;}
-keep class com.alibaba.sdk.android.**{*;}
-keep class com.ut.**{*;}
-keep class com.ta.**{*;}
-keep class com.gh.gamecenter.GdtHelper { *; }
-keep class com.gh.gamecenter.TeaHelper { *; }

View File

@ -1,3 +1,20 @@
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in C:\Android\sdk/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
#--------- remove logs start ----------------
-assumenosideeffects class com.lightgame.config.CommonDebug {
@ -9,66 +26,126 @@
public static void logMethodWithParams(...);
}
-assumenosideeffects class com.lightgame.utils.Utils {
public static void log(...);
}
#-assumenosideeffects class com.lightgame.config.CommonDebug {*;}
#-dontoptimize
#--------- remove logs end ----------------
# TODO Dicard sourceFile in final release build but remain in internal build.
-renamesourcefileattribute SourceFile
# Keep Attribute
-keepattributes *Annotation*,Signature,InnerClasses,EnclosingMethod,SourceFile,LineNumberTable
-keepattributes *Annotation*,Signature,InnerClasses,EnclosingMethod
-dontwarn InnerClasses
# OrmLite
-keep class com.j256.*
-keepclassmembers class com.j256.* { *; }
-keep enum com.j256.*
-keepclassmembers enum com.j256.* { *; }
-keep interface com.j256.*
-keepclassmembers interface com.j256.* { *; }
# OrmLite uses reflection
-keep class com.j256.**
-keepclassmembers class com.j256.** { *; }
-keep enum com.j256.**
-keepclassmembers enum com.j256.** { *; }
-keep interface com.j256.**
-keepclassmembers interface com.j256.** { *; }
-dontwarn com.j256.**
### AutoScrollViewPager
-keep class cn.trinea.android.* { *; }
-keepclassmembers class cn.trinea.android.* { *; }
#okhttp3
-dontwarn com.squareup.okhttp3.**
-dontwarn okio.**
-keep class com.squareup.okhttp3.** { *;}
# stetho
-keep class com.facebook.stetho.** { *; }
-dontwarn com.facebook.stetho.**
# Retrofit 2.2
# Platform calls Class.forName on types which do not exist on Android to determine platform.
-dontnote retrofit2.Platform
# Platform used when running on Java 8 VMs. Will not be used at runtime.
-dontwarn retrofit2.Platform$Java8
# Retain generic type information for use by reflection by converters and adapters.
-keepattributes Signature
# Retain declared checked exceptions for use by a Proxy instance.
-keepattributes Exceptions
# Retrofit 2.X
## https://square.github.io/retrofit/ ##
-dontwarn retrofit2.**
-keep class retrofit2.** { *; }
-keepattributes Signature
-keepattributes Exceptions
-keepclasseswithmembers class * {
@retrofit2.http.* <methods>;
}
# rxjava
-keep class rx.schedulers.Schedulers {
public static <methods>;
}
-keep class rx.schedulers.ImmediateScheduler {
public <methods>;
}
-keep class rx.schedulers.TestScheduler {
public <methods>;
}
-keep class rx.schedulers.Schedulers {
public static ** test();
}
-keepclassmembers class rx.internal.util.unsafe.*ArrayQueue*Field* {
long producerIndex;
long consumerIndex;
}
-keepclassmembers class rx.internal.util.unsafe.BaseLinkedQueueProducerNodeRef {
long producerNode;
long consumerNode;
}
-dontwarn rx.internal.util.**
## AutoScrollViewPager
-keep class cn.trinea.android.** { *; }
-keepclassmembers class cn.trinea.android.** { *; }
-dontwarn cn.trinea.android.**
### Butterknife
-keep public class * implements butterknife.Unbinder { public <init>(**, android.view.View); }
-keep class butterknife.*
-keepclasseswithmembernames class * { @butterknife.* <methods>; }
-keepclasseswithmembernames class * { @butterknife.* <fields>; }
## butterknife
# Retain generated class which implement Unbinder.
#-keep public class * implements butterknife.Unbinder { public <init>(**, android.view.View); }
#
## Prevent obfuscation of types which use ButterKnife annotations since the simple name
## is used to reflectively look up the generated ViewBinding.
#-keep class butterknife.*
#-keepclasseswithmembernames class * { @butterknife.* <methods>; }
#-keepclasseswithmembernames class * { @butterknife.* <fields>; }
### eventbus
-keepclassmembers class * {
-dontwarn butterknife.internal.**
-keep class **$$ViewInjector { *; }
-keepnames class * { @butterknife.InjectView *;}
-dontwarn butterknife.Views$InjectViewProcessor
-dontwarn com.gc.materialdesign.views.**
# eventbus
-keepattributes *Annotation*
-keepclassmembers class ** {
@org.greenrobot.eventbus.Subscribe <methods>;
}
-keep enum org.greenrobot.eventbus.ThreadMode { *; }
### Only required if you use AsyncExecutor
# Only required if you use AsyncExecutor
-keepclassmembers class * extends org.greenrobot.eventbus.util.ThrowableFailureEvent {
<init>(java.lang.Throwable);
}
### weiboSdk
# weiboSdk
-keep class com.sina.weibo.sdk.** { *; }
-dontwarn android.webkit.WebView
-dontwarn android.webkit.WebViewClient
### wechatSdk
### TODO 这里用 com.tencent.*{*;} 不起效?但其它地方可以?
-keep class com.tencent.**{*;}
# app models
-keep class com.gh.common.view.** {*;}
-keep class com.gh.gamecenter.db.info.** {*;}
-keep class com.gh.gamecenter.entity.** {*;}
-keep class com.gh.gamecenter.qa.entity.** {*;}
-keep class com.gh.gamecenter.retrofit.** {*;}
-keep class com.gh.gamecenter.eventbus.** {*;}
-keep class * extends rx.Subscriber
### app models
-keep class com.gh.common.view.* {*;}
-keep class com.gh.gamecenter.db.info.* {*;}
-keep class com.gh.gamecenter.entity.* {*;}
-keep class com.gh.gamecenter.qa.entity.* {*;}
-keep class com.gh.gamecenter.retrofit.* {*;}
-keep class com.gh.gamecenter.eventbus.* {*;}
-keep class com.gh.gamecenter.video.detail.* {*;}
###
#---------------------------------webview------------------------------------
-keepclassmembers class * extends android.webkit.WebViewClient {
public void *(android.webkit.WebView, java.lang.String, android.graphics.Bitmap);
public boolean *(android.webkit.WebView, java.lang.String);
@ -76,67 +153,109 @@
-keepclassmembers class * extends android.webkit.WebViewClient {
public void *(android.webkit.WebView, java.lang.String);
}
#----------------------------------------------------------------------------
### easypermission
##---------------Begin: proguard configuration for Gson ----------
# Gson uses generic type information stored in a class file when working with fields. Proguard
# removes such information by default, so configure it to keep all of it.
-keepattributes Signature
# For using GSON @Expose annotation
-keepattributes *Annotation*
# Gson specific classes
-keep class sun.misc.Unsafe { *; }
#-keep class com.google.gson.stream.** { *; }
# Prevent proguard from stripping interface information from TypeAdapterFactory,
# JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter)
-keep class * implements com.google.gson.TypeAdapterFactory
-keep class * implements com.google.gson.JsonSerializer
-keep class * implements com.google.gson.JsonDeserializer
-keepclassmembers enum * { *; }
##---------------End: proguard configuration for Gson ----------
# ------ bugly ---------
-dontwarn com.tencent.bugly.**
-keep public class com.tencent.bugly.**{*;}
# easypermission
-keepclassmembers class * {
@pub.devrel.easypermissions.AfterPermissionGranted <methods>;
}
# TODO What's this ?
# 重命名文件为SourceFile再配合mapping符号表可以拿到真实的类名
-renamesourcefileattribute SourceFile
# 保留源文件行号
-keepattributes SourceFile,LineNumberTable
-ignorewarnings
### Keep Annotation
-keep @androidx.annotation.Keep class *
-keepclassmembers class * {
-keepclassmembers class ** {
@androidx.annotation.Keep *;
}
-keep class com.gh.loghub.** { *; }
### greenDAO 3
-keepclassmembers class * extends org.greenrobot.greendao.AbstractDao {
public static java.lang.String TABLENAME;
}
-keep class **$Properties
-keep class org.greenrobot.greendao.** { *; }
# If you do not use SQLCipher:
-dontwarn org.greenrobot.greendao.database.**
# If you do not use RxJava:
-dontwarn rx.**
-dontwarn org.greenrobot.greendao.rx.**
-dontwarn org.greenrobot.greendao.**
### fastJson
-dontwarn com.alibaba.fastjson.**
-keep class com.alibaba.fastjson.** { *; }
-keepattributes Signature
-keepattributes Annotation
### 广点通
-dontwarn com.qq.gdt.action.**
-keep class com.qq.gdt.action.* {*;}
-keep class com.qq.gdt.action.** {*;}
### 阿里云上传
-keep class com.alibaba.sdk.android.oss.* { *; }
### AndroidX
-keep class androidx.core.app.CoreComponentFactory { *; }
#阿里云上传
-keep class com.alibaba.sdk.android.oss.** { *; }
-dontwarn okio.**
-dontwarn org.apache.commons.codec.binary.**
### 视频相关
-keep class com.shuyu.gsyvideoplayer.video.* { *; }
#视频相关
-keep class com.shuyu.gsyvideoplayer.video.** { *; }
-dontwarn com.shuyu.gsyvideoplayer.video.**
-keep class com.shuyu.gsyvideoplayer.video.base.* { *; }
-keep class com.shuyu.gsyvideoplayer.video.base.** { *; }
-dontwarn com.shuyu.gsyvideoplayer.video.base.**
-keep class com.shuyu.gsyvideoplayer.utils.* { *; }
-keep class com.shuyu.gsyvideoplayer.utils.** { *; }
-dontwarn com.shuyu.gsyvideoplayer.utils.**
-keep class tv.danmaku.ijk.* { *; }
-keep class tv.danmaku.ijk.** { *; }
-dontwarn tv.danmaku.ijk.**
-keep public class * extends android.view.View{
*** get*();
void set*(***);
public <init>(android.content.Context);
public <init>(android.content.Context, android.util.AttributeSet);
public <init>(android.content.Context, android.util.AttributeSet, int);
}
-keep class com.alibaba.sdk.android.*{*;}
-keep class com.ut.*{*;}
-keep class com.ta.*{*;}
#穿山甲
-keep class com.bytedance.sdk.openadsdk.** { *; }
-keep public interface com.bytedance.sdk.openadsdk.downloadnew.** {*;}
-keep class com.pgl.sys.ces.* {*;}
### GDT & TEA
-keep class com.gh.gamecenter.GdtHelper { *; }
-keep class com.gh.gamecenter.TeaHelper { *; }
-keep class com.gyf.immersionbar.* {*;}
-dontwarn com.gyf.immersionbar.**
### 阿里云日志
-keep class com.aliyun.sls.android.producer.* { *; }
-keep interface com.aliyun.sls.android.producer.* { *; }
-keep class com.taobao.securityjni.**{*;}
-keep class com.taobao.wireless.security.**{*;}
-keep class com.ut.secbody.**{*;}
-keep class com.taobao.dp.**{*;}
-keep class com.alibaba.wireless.security.**{*;}
### 中国移动一键登录
-dontwarn com.cmic.sso.sdk.**
-keep class com.cmic.sso.sdk.* { *; }
### EasyFloat
-keep class com.lzf.easyfloat.* {*;}
### 避免 WebChromeClient 被混淆
-keepclassmembers class * extends android.webkit.WebChromeClient{
public void openFileChooser(...);
}
-keep class com.alibaba.sdk.android.**{*;}
-keep class com.ut.**{*;}
-keep class com.ta.**{*;}

View File

@ -2,6 +2,8 @@ package com.gh.gamecenter;
import android.app.Application;
import com.facebook.stetho.Stetho;
import com.facebook.stetho.okhttp3.StethoInterceptor;
import okhttp3.OkHttpClient;
import okhttp3.logging.HttpLoggingInterceptor;
@ -15,6 +17,9 @@ import okhttp3.logging.HttpLoggingInterceptor;
public class Injection {
public static boolean appInit(Application application) {
// init stetho
Stetho.initializeWithDefaults(application);
// 监控Bundle大小,预防溢出(需要调试的时候再开启吧!)
// TooLargeTool.startLogging(application);
return true;
@ -25,6 +30,7 @@ public class Injection {
HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor();
interceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
builder.addNetworkInterceptor(interceptor);
builder.addNetworkInterceptor(new StethoInterceptor());
return builder;
}

View File

@ -1,77 +0,0 @@
package com.gh.gamecenter
import android.app.Application
import android.text.TextUtils
import android.util.Log
import com.gh.common.util.ToastUtils
import com.lightgame.utils.Utils
import com.qq.gdt.action.GDTAction
import org.json.JSONObject
/**
* 广点通辅助类 [https://gitlab.ghzhushou.com/pm/halo-app-issues/issues/403]
*
* 更换帐号 [https://gitlab.ghzs.com/pm/yunying/issues/893]
*/
object GdtHelper {
const val NETWORK_TYPE = "NETWORK_TYPE"
const val PAGE_TYPE = "PAGE_TYPE"
const val CONTENT_TYPE = "CONTENT_TYPE"
const val CONTENT_ID = "CONTENT_ID"
const val KEYWORD = "KEYWORD"
const val GAME_ID = "GAME_ID"
const val SCORE = "SCORE"
const val PLATFORM = "PLATFORM"
@JvmStatic
fun init(application: Application, channel: String) {
if (shouldUseGdtHelper()) {
if (channel == "GH_728") {
GDTAction.init(application, "1111012969", "9d3d9da5b0948a317c03d08f14d445dc")
} else if (channel == "GH_729") {
GDTAction.init(application, "1111013063", "f53dabf458a356b101d99fc4069eb7f1")
} else if (channel == "GH_765") {
GDTAction.init(application, "1111327925", "588d503f0990f98f9b2394fbb795c570")
} else {
GDTAction.init(application, "1110680399", "f5ddaafbf520d7d7385499232a408d0a")
}
}
Utils.log("init GdtHelper")
}
// fun logAction(type: String) {
// if (shouldUseGdtHelper()) {
// GDTAction.logAction(type)
// Utils.log("GDT", type)
// }
// }
@JvmStatic
fun logAction(type: String, vararg kv: String?) {
try {
val actionParam = JSONObject()
for (i in kv.indices) {
if (i % 2 != 0) {
val key = kv[i - 1]
val value = kv[i]
if (!TextUtils.isEmpty(key) && !TextUtils.isEmpty(value)) {
actionParam.put(key, value)
}
}
}
Utils.log("GDT", "$type + [${kv.joinToString(" , ")}]")
GDTAction.logAction(type, actionParam)
} catch (e: Exception) {
e.printStackTrace()
}
}
// TODO 确认开启的渠道条件
private fun shouldUseGdtHelper(): Boolean {
return true
//
// val channel = HaloApp.getInstance().channel
// return !(TextUtils.isEmpty(channel) || channel.contains("GDT".toLowerCase(Locale.CHINA)))
}
}

View File

@ -9,6 +9,8 @@
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<!-- 允许应用程序读取扩展存储器 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<!-- 允许挂载和反挂载文件系统可移动存储 -->
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" />
<!-- 允许应用程序访问Wi-Fi网络状态信息 -->
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<!-- 允许应用程序获取网络信息状态 -->
@ -21,17 +23,25 @@
<uses-permission android:name="android.permission.VIBRATE" />
<!-- 允许应用程序改变Wi-Fi连接状态 -->
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<!-- 允许应用程序改变网络连接状态 -->
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<!-- 允许应用程序快捷方式 -->
<!-- 允许应用程序打开系统窗口,显示其他应用程序 -->
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<!-- 修改系统设置的权限 -->
<uses-permission android:name="android.permission.WRITE_SETTINGS" />
<!-- 创建快捷方式的权限 -->
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />
<uses-permission
android:name="android.permission.PACKAGE_USAGE_STATS"
tools:ignore="ProtectedPermissions" />
<!-- bugly with tinker -->
<uses-permission android:name="android.permission.READ_LOGS" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<!--可选,穿山甲提供“获取地理位置权限”和“不给予地理位置权限,开发者传入地理位置参数”两种方式上报用户位置,两种方式均可不选,添加位置权限或参数将帮助投放定位广告-->
<!--请注意:无论通过何种方式提供给穿山甲用户地理位置,均需向用户声明地理位置权限将应用于穿山甲广告投放,穿山甲不强制获取地理位置信息-->
<!--<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />-->
<!-- 如果有视频相关的广告且使用textureView播放请务必添加否则黑屏 -->
<uses-permission android:name="android.permission.WAKE_LOCK" />
@ -41,11 +51,9 @@
com.shuyu.gsyvideoplayer.armv7a,
com.shuyu.gsyvideoplayer.x86,
com.shuyu.gsy.base,
shuyu.com.androidvideocache,
com.google.android.exoplayer2,
tv.danmaku.ijk.media.exo2,
pl.droidsonroids.gif,
com.lzf.easyfloat"/>
pl.droidsonroids.gif" />
<!-- 去掉 SDK 一些流氓权限 -->
<uses-permission
@ -66,21 +74,12 @@
android:icon="@mipmap/logo"
android:label="@string/app_name"
android:largeHeap="true"
android:networkSecurityConfig="@xml/network_security_config"
android:resizeableActivity="true"
android:theme="@style/AppCompatTheme.APP"
tools:replace="android:allowBackup"
tools:targetApi="n">
<meta-data
android:name="io.sentry.auto-init"
android:value="false" />
<!-- 不让 sentry 读取系统事件 -->
<meta-data
android:name="io.sentry.breadcrumbs.system-events"
android:value="false" />
<!--android:launchMode = "singleTask"-->
<activity
android:name="com.gh.gamecenter.SplashScreenActivity"
android:configChanges="keyboardHidden|orientation|screenSize"
@ -107,6 +106,7 @@
android:launchMode="singleTask"
android:screenOrientation="portrait" />
<!--android:theme = "@android:style/Theme.Black.NoTitleBar.Fullscreen" 退出时屏幕抖动 -->
<activity
android:name="com.gh.gamecenter.ImageViewerActivity"
android:theme="@style/Theme.Transparent" />
@ -164,10 +164,6 @@
android:name="com.gh.gamecenter.WebActivity"
android:screenOrientation="portrait" />
<activity
android:name="com.gh.gamecenter.SingletonWebActivity"
android:screenOrientation="portrait" />
<activity
android:name="com.gh.gamecenter.FullScreenWebActivity"
android:screenOrientation="portrait" />
@ -466,14 +462,14 @@
<activity
android:name="com.gh.gamecenter.video.upload.view.UploadVideoActivity"
android:screenOrientation="portrait"
android:windowSoftInputMode="stateHidden" />
android:windowSoftInputMode="stateHidden"/>
<activity
android:name="com.gh.gamecenter.video.game.GameVideoActivity"
android:screenOrientation="portrait" />
<activity
android:name="com.gh.gamecenter.qa.editor.LocalMediaActivity"
android:name="com.gh.gamecenter.qa.editor.VideoActivity"
android:screenOrientation="portrait" />
<activity
@ -511,12 +507,6 @@
android:theme="@style/Theme.Transparent"
android:windowSoftInputMode="adjustNothing" />
<activity
android:name=".qa.dialog.ChooseForumActivity"
android:screenOrientation="portrait"
android:theme="@style/Theme.Transparent"
android:windowSoftInputMode="adjustNothing" />
<activity
android:name="com.gh.gamecenter.video.detail.VideoDetailActivity"
android:configChanges="orientation|screenSize|keyboardHidden"
@ -556,6 +546,10 @@
android:name=".forum.select.ForumSelectActivity"
android:screenOrientation="portrait" />
<activity
android:name=".forum.follow.ForumMyFollowActivity"
android:screenOrientation="portrait" />
<activity
android:name=".forum.detail.ForumDetailActivity"
android:screenOrientation="portrait" />
@ -564,10 +558,6 @@
android:name=".forum.moderator.ModeratorListActivity"
android:screenOrientation="portrait" />
<activity
android:name=".forum.moderator.ApplyModeratorActivity"
android:screenOrientation="portrait"/>
<activity
android:name=".video.label.VideoLabelActivity"
android:screenOrientation="portrait" />
@ -587,7 +577,11 @@
<activity
android:name=".personalhome.background.BackgroundClipActivity"
android:screenOrientation="portrait"
android:theme="@style/TransparentStatusBarAndNavigationBar" />
android:theme="@style/TransparentStatusBarAndNavigationBar"/>
<activity
android:name=".personalhome.excellentcomments.ExcellentCommentsActivity"
android:screenOrientation="portrait" />
<activity
android:name=".simulatorgame.SimulatorGameActivity"
@ -597,110 +591,12 @@
android:name=".simulatorgame.SimulatorManagementActivity"
android:screenOrientation="portrait" />
<!-- 使用小米/华为推送弹窗功能提高推送成功率-->
<activity
android:name=".catalog.CatalogActivity"
android:screenOrientation="portrait" />
<activity
android:name=".catalog.NewCatalogListActivity"
android:screenOrientation="portrait" />
<activity
android:name=".forum.search.ForumOrUserSearchActivity"
android:screenOrientation="portrait" />
<activity
android:name=".energy.EnergyCenterActivity"
android:name="com.gh.gamecenter.PushProxyActivity"
android:exported="true"
android:launchMode="singleTask"
android:screenOrientation="portrait" />
<activity
android:name=".energy.EnergyHouseActivity"
android:screenOrientation="portrait" />
<activity
android:name=".personal.NewPersonalActivity"
android:screenOrientation="portrait" />
<activity
android:name=".qa.questions.draft.QuestionDraftActivity"
android:screenOrientation="portrait" />
<activity
android:name=".servers.GameServerTestActivity"
android:screenOrientation="portrait" />
<activity
android:name=".category2.CategoryV2Activity"
android:screenOrientation="portrait" />
<activity
android:name=".personal.DeliveryInfoActivity"
android:screenOrientation="portrait" />
<activity
android:name=".qa.editor.PreviewVideoActivity"
android:screenOrientation="portrait" />
<activity
android:name=".qa.video.publish.VideoPublishActivity"
android:screenOrientation="portrait" />
<activity
android:name=".setting.GameDownloadSettingActivity"
android:screenOrientation="portrait" />
<activity
android:name=".setting.VideoSettingActivity"
android:screenOrientation="portrait" />
<activity
android:name=".qa.video.detail.ForumVideoDetailActivity"
android:screenOrientation="portrait" />
<activity
android:name=".video.videomanager.VideoDraftActivity"
android:screenOrientation="portrait" />
<activity
android:name=".qa.questions.newdetail.NewQuestionDetailActivity"
android:screenOrientation="portrait" />
<activity
android:name=".qa.editor.FullScreenVideoActivity"
android:screenOrientation="landscape"
android:theme="@style/AppFullScreenTheme" />
<activity
android:name=".forum.list.ForumListActivity"
android:screenOrientation="portrait" />
<activity
android:name=".qa.answer.detail.SimpleAnswerDetailActivity"
android:screenOrientation="portrait" />
<activity
android:name=".game.commoncollection.detail.CommonCollectionDetailActivity"
android:screenOrientation="portrait" />
<activity
android:name="com.cmic.sso.sdk.activity.LoginAuthActivity"
android:configChanges="orientation|keyboardHidden|screenSize"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:theme="@android:style/Theme.Dialog" />
<activity
android:name=".home.skip.PackageSkipActivity"
android:screenOrientation="portrait" />
<!-- &lt;!&ndash; 使用小米/华为推送弹窗功能提高推送成功率&ndash;&gt;-->
<!-- <activity-->
<!-- android:name="com.gh.gamecenter.PushProxyActivity"-->
<!-- android:exported="true"-->
<!-- android:launchMode="singleTask"-->
<!-- android:theme="@android:style/Theme.Translucent" />-->
android:theme="@android:style/Theme.Translucent" />
<activity
android:name="com.gh.gamecenter.SkipActivity"
@ -712,14 +608,6 @@
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
</intent-filter>
<intent-filter>
<data android:scheme="market" />
<category android:name="android.intent.category.DEFAULT" />
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
</intent-filter>
</activity>
<activity
@ -730,11 +618,11 @@
android:screenOrientation="portrait"
android:theme="@android:style/Theme.Translucent.NoTitleBar"></activity>
<!-- <activity-->
<!-- android:name="${applicationId}.douyinapi.DouYinEntryActivity"-->
<!-- android:launchMode="singleTask"-->
<!-- android:taskAffinity="${applicationId}"-->
<!-- android:exported="true" />-->
<!-- <activity-->
<!-- android:name="${applicationId}.douyinapi.DouYinEntryActivity"-->
<!-- android:launchMode="singleTask"-->
<!-- android:taskAffinity="${applicationId}"-->
<!-- android:exported="true" />-->
<provider
android:name="androidx.core.content.FileProvider"
@ -746,11 +634,6 @@
android:resource="@xml/provider_paths" />
</provider>
<provider
android:name="androidx.work.impl.WorkManagerInitializer"
android:authorities="${applicationId}.workmanager-init"
tools:node="remove" />
<receiver
android:name="com.gh.gamecenter.receiver.DownloadReceiver"
android:exported="false">
@ -774,45 +657,45 @@
</intent-filter>
</receiver>
<!-- <receiver android:name="com.gh.gamecenter.receiver.UmengMessageReceiver">-->
<!-- <intent-filter>-->
<!-- <action android:name="com.gh.gamecenter.UMENG" />-->
<!-- </intent-filter>-->
<!-- </receiver>-->
<!-- <receiver android:name="com.gh.gamecenter.receiver.UmengMessageReceiver">-->
<!-- <intent-filter>-->
<!-- <action android:name="com.gh.gamecenter.UMENG" />-->
<!-- </intent-filter>-->
<!-- </receiver>-->
<!-- &lt;!&ndash;魅族push应用定义消息receiver声明 &ndash;&gt;-->
<!-- <receiver android:name="com.gh.gamecenter.receiver.UmengMeizuPushReceiver">-->
<!-- <intent-filter>-->
<!-- &lt;!&ndash; 接收push消息 &ndash;&gt;-->
<!-- <action android:name="com.meizu.flyme.push.intent.MESSAGE" />-->
<!-- &lt;!&ndash; 接收register消息 &ndash;&gt;-->
<!-- <action android:name="com.meizu.flyme.push.intent.REGISTER.FEEDBACK" />-->
<!-- &lt;!&ndash; 接收unregister消息&ndash;&gt;-->
<!-- <action android:name="com.meizu.flyme.push.intent.UNREGISTER.FEEDBACK" />-->
<!-- &lt;!&ndash; 兼容低版本Flyme3推送服务配置 &ndash;&gt;-->
<!-- <action android:name="com.meizu.c2dm.intent.REGISTRATION" />-->
<!-- <action android:name="com.meizu.c2dm.intent.RECEIVE" />-->
<!-- &lt;!&ndash;魅族push应用定义消息receiver声明 &ndash;&gt;-->
<!-- <receiver android:name="com.gh.gamecenter.receiver.UmengMeizuPushReceiver">-->
<!-- <intent-filter>-->
<!-- &lt;!&ndash; 接收push消息 &ndash;&gt;-->
<!-- <action android:name="com.meizu.flyme.push.intent.MESSAGE" />-->
<!-- &lt;!&ndash; 接收register消息 &ndash;&gt;-->
<!-- <action android:name="com.meizu.flyme.push.intent.REGISTER.FEEDBACK" />-->
<!-- &lt;!&ndash; 接收unregister消息&ndash;&gt;-->
<!-- <action android:name="com.meizu.flyme.push.intent.UNREGISTER.FEEDBACK" />-->
<!-- &lt;!&ndash; 兼容低版本Flyme3推送服务配置 &ndash;&gt;-->
<!-- <action android:name="com.meizu.c2dm.intent.REGISTRATION" />-->
<!-- <action android:name="com.meizu.c2dm.intent.RECEIVE" />-->
<!-- <category android:name="${applicationId}" />-->
<!-- </intent-filter>-->
<!-- </receiver>-->
<!-- <category android:name="${applicationId}" />-->
<!-- </intent-filter>-->
<!-- </receiver>-->
<!-- <receiver-->
<!-- android:name="com.gh.common.im.ImReceiver"-->
<!-- android:enabled="true">-->
<!-- <intent-filter android:priority="2147483647">-->
<!-- <action android:name="com.gh.im" />-->
<!-- <action android:name="action_finish" />-->
<!-- </intent-filter>-->
<!-- </receiver>-->
<!-- <receiver-->
<!-- android:name="com.gh.common.im.ImReceiver"-->
<!-- android:enabled="true">-->
<!-- <intent-filter android:priority="2147483647">-->
<!-- <action android:name="com.gh.im" />-->
<!-- <action android:name="action_finish" />-->
<!-- </intent-filter>-->
<!-- </receiver>-->
<!-- <meta-data-->
<!-- android:name="com.huawei.hms.client.appid"-->
<!-- android:value="@string/huawei_push_appid" />-->
<!-- <meta-data-->
<!-- android:name="com.huawei.hms.client.appid"-->
<!-- android:value="@string/huawei_push_appid" />-->
<!-- <service-->
<!-- android:name="com.gh.base.GHUmengNotificationService"-->
<!-- android:permission="android.permission.BIND_JOB_SERVICE" />-->
<!-- <service-->
<!-- android:name="com.gh.base.GHUmengNotificationService"-->
<!-- android:permission="android.permission.BIND_JOB_SERVICE" />-->
<!--<service android:name = "com.gh.gamecenter.statistics.AppStaticService" />-->

View File

@ -0,0 +1,40 @@
emoji_kf_1.png,:smile:
emoji_kf_2.png,:smiley:
emoji_kf_3.png,:laughing:
emoji_kf_4.png,:blush:
emoji_kf_5.png,:heart_eyes:
emoji_kf_6.png,:smirk:
emoji_kf_7.png,:flushed:
emoji_kf_8.png,:kissing_heart:
emoji_kf_9.png,:grin:
emoji_kf_10.png,:wink:
emoji_kf_11.png,:stuck_out_tongue_winking_eye:
emoji_kf_12.png,:stuck_out_tongue_closed eyes:
emoji_kf_13.png,:worried:
emoji_kf_14.png,:sleeping:
emoji_kf_15.png,:expressionless:
emoji_kf_16.png,:sweat_smile:
emoji_kf_17.png,:joy:
emoji_kf_18.png,:cold_sweat:
emoji_kf_19.png,:sob:
emoji_kf_20.png,:angry:
emoji_kf_21.png,:mask:
emoji_kf_22.png,:scream:
emoji_kf_23.png,:sunglasses:
emoji_kf_24.png,:heart:
emoji_kf_25.png,:broken_heart:
emoji_kf_26.png,:star:
emoji_kf_27.png,:anger:
emoji_kf_28.png,:exclamation:
emoji_kf_29.png,:question:
emoji_kf_30.png,:zzz:
emoji_kf_31.png,:thumbsup:
emoji_kf_32.png,:thumbsdown:
emoji_kf_33.png,:ok_hand:
emoji_kf_34.png,:punch:
emoji_kf_35.png,:yeah:
emoji_kf_36.png,:clap:
emoji_kf_37.png,:muscle:
emoji_kf_38.png,:pray:
emoji_kf_39.png,:skull:
emoji_kf_40.png,:trollface:

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -0,0 +1,5 @@
# This is a simple Microlog configuration file
microlog.level=DEBUG
microlog.appender=LogCatAppender;FileAppender
microlog.formatter=PatternFormatter
microlog.formatter.PatternFormatter.pattern=%c [%P] %m %T

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,493 +1,161 @@
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>光环助手软件许可及服务协议</title>
</head>
<html lang="en"><head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>光环助手软件许可及服务协议</title>
<style>
* {
margin: 0;
padding: 0;
font-size: 14px;
-webkit-user-select: text;
-moz-user-select: text;
-ms-user-select: text;
user-select: text;
}
* {
-webkit-user-select: text;
-moz-user-select: text;
-ms-user-select: text;
user-select: text;
font-size: 16px;
}
</style>
</head>
<body>
<div class="content-area">
<p><b></b></p>
<p>
<b>光环</b>
<b>game</b>
<b>软件许可及服务协议</b>
</p>
<p><b>更新日期2020年11月20日</b></p>
<p><b>生效日期2020年11月30日</b></p>
<p><b>首部及导言</b></p>
<p>欢迎使用光环game软件许可及服务</p>
<p>
各位用户在使用光环game前请您务必审慎阅读、并充分理解本协议中的各项条款
<b>
特别是免除或者限制责任的条款,以及开通或使用某项服务的单独协议,并选择接受或不接受。
</b>
除非您已阅读并接受本协议所有条款,否则您无权下载、安装或使用本软件及相关服务。您的下载、安装、使用、登录等行为即视为您已阅读并同意上述协议的约束。
</p>
<p>如果您未满18周岁请在法定监护人的陪同下阅读本协议及其他上述协议。</p>
<p><b>一、权利声明</b></p>
<p>
“光环game”的一切知识产权以及与“光环game”相关的所有信息内容包括但不限于文字表述及其组合、图标、图饰、图像、图表、色彩、界面设计、版面框架、有关数据、附加程序、印刷材料或电子文档等均为光环game所有受著作权法和国际著作权条约以及其他知识产权法律法规的保护。
</p>
<p><b>二、软件使用规范</b></p>
<p>
2.1
本软件是基于Android安卓系统手机、平板电脑(PAD)等设备开发的一款软件,提供注册登录、观看视频、视频投稿等功能
</p>
<p>2.2 软件的下载、安装和使用</p>
<p>
本软件为免费软件用户可以非商业性、无限制数量地从光环game授权的渠道下载、安装及使用本软件。
</p>
<p>
<b>如果您从未经光环</b>
<b>game</b>
<b>授权的第三方获取本软件或与本软件名称相同的安装程序,光环</b>
<b>game</b>
<b>无法保证该软件能够正常使用,并对因此给您造成的损失不予负责。</b>
</p>
<p>2.3 软件的复制、分发和传播</p>
<p>
本产品以研究交流为目的。用户可以非商业性、无限制数量地复制、分发和传播本软件产品。但必须保证每一份复制、分发和传播都是完整和真实的,
包括所有有关本软件产品的软件、电子文档, 版权和商标,亦包括本协议。
</p>
<p>2.4 软件的更新</p>
<p>
为了改善用户体验、完善服务内容光环game将不断努力开发新的服务并为您不时提供软件更新这些更新可能会采取软件替换、修改、功能强化、版本升级等形式。为了保证本软件及服务的安全性和功能的一致性光环game有权不经向您特别通知而对软件进行更新或者对软件的部分功能效果进行改变或限制。本软件新版本发布后旧版本的软件可能无法使用。光环game不保证旧版本软件继续可用及相应的客户服务请您随时核对并下载最新版本。
</p>
<p><b>三、用户使用须知</b></p>
<p>3.1 您理解并同意:</p>
<p>
为了向您提供有效的服务,本软件会利用您移动通讯终端的处理器和带宽等资源。本软件使用过程中可能产生数据流量的费用,用户需自行向运营商了解相关资费信息,并自行承担相关费用.
</p>
<p>3.2您理解并同意:</p>
<p>
<b>
如果因您不正当使用本软件造成了不良影响,或因使用本软件造成的包括但不限于数据异常等问题,均由使用者自行承担,光环团队不对任意类型的使用结果承担责任;
</b>
</p>
<p>3.3您理解并同意:</p>
<p>
本软件不含任何破坏用户移动通讯设备数据和获取用户隐私信息的恶意代码,不会泄露用户的个人信息和隐私;
</p>
<p>3.4您理解并同意:</p>
<p>
<b>
对于包括但不限于互联网网络故障、计算机故障、手机故障或病毒、信息损坏或丢失、计算机系统问题,或其它任何基于不可抗力原因而产生的损失,光环团队不承担任何责任。
</b>
</p>
<p>3.5您理解并同意:</p>
<p>光环game发布、收录的视频均不代表光环立场。</p>
<p>3.6您理解并同意:</p>
<p>
用户应在遵守法律及本协议的前提下使用本软件。用户无权实施包括但不限于下列行为:
</p>
<p>3.6.1 不得删除或者改变本软件上的所有权利管理电子信息</p>
<p>
3.6.2
不得故意避开或者破坏著作权人为保护本软件著作权而采取的技术措施;&nbsp;
</p>
<p>3.6.3 用户不得利用本软件误导、欺骗他人;&nbsp;</p>
<p>
3.6.4
违反国家规定,对计算机信息系统功能进行删除、修改、增加、干扰,造成计算机信息系统不能正常运行;&nbsp;
</p>
<p>3.6.5 未经允许,进入计算机信息网络或者使用计算机信息网络资源;&nbsp;</p>
<p>3.6.6 未经允许,对计算机信息网络功能进行删除、修改或者增加;&nbsp;</p>
<p>
3.6.7
未经允许,对计算机信息网络中存储、处理或者传输的数据和应用程序进行删除、修改或者增加;&nbsp;
</p>
<p>
3.6.8
破坏本软件系统或网站的正常运行,故意传播计算机病毒等破坏性程序;&nbsp;
</p>
<p>3.6.9 其他任何危害计算机信息网络安全的行为。&nbsp;</p>
<p>3.7 您理解并同意:</p>
<p>
本软件经过详细的测试,但不能保证与所有的软硬件系统完全兼容,不能保证本软件完全没有错误。如果出现不兼容及软件错误的情况,用户可通过各反馈途径将情况告知光环团队,获得技术支持。如果无法解决兼容性问题,用户可以删除本软件。
</p>
<p><b>四、争议解决处理</b></p>
<p>
本《协议》的解释、效力及纠纷的解决适用于中华人民共和国法律。若用户和光环game之间发生任何纠纷或争议首先应友好协商解决协商不成的用户在此完全同意将纠纷或争议提交光环game所在地法院管辖
</p>
<p>
<b></b>
<b>、关于获取手机设备信息的说明</b>
</p>
<p>
1为方便区分每个用户的个人信息等本软件需获取用户的手机设备信息用于用户登录、视频评论互动交流等用户相关的行为
</p>
<p>
2为了保障软件与服务的安全运行我们会收集您的硬件型号、操作系统版本号、国际移动设备识别码、唯一设备标识符、网络设备硬件地址、IP
地址、WLAN接入点、蓝牙、基站、软件版本号、网络接入方式、类型、状态、网络质量数据、操作、使用、服务日志。
</p>
<p>
3为了预防恶意程序及安全运营所必需我们会收集安装的应用信息或正在运行的进程信息、应用程序的总体运行、使用情况与频率、应用崩溃情况、总体安装使用情况、性能数据、应用来源。
</p>
<p>
4我们可能使用您的账户信息、设备信息、服务日志信息以及我们关联公司、合作方在获得您授权或依法可以共享的信息用于判断账户安全、进行身份验证、检测及防范安全事件。
</p>
<p>5具体会发生获取手机设备信息场景如下说明</p>
<p>
&nbsp;&nbsp;1) 首次启动光环game&nbsp;2) 评论详情-发送评论功能&nbsp;3)
视频投稿-上传视频功能&nbsp;4) 视频详情-关注up主功能
</p>
<p><b>七、其他</b></p>
<p>
7.1
本协议所有条款的标题仅为阅读方便,本身并无实际涵义,不能作为本协议涵义解释的依据。
</p>
<p>
&nbsp;7.2
如果本协议中的任何条款无论因何种原因完全或部分无效或不具有执行力,或违反任何适用的法律,则该条款被视为删除,但本协议的其余条款仍应有效并且有约束力。
</p>
<p>
&nbsp;7.3
光环有权随时根据有关法律、法规的变化以及公司经营状况和经营策略的调整等修改本协议。修改后的协议会在软件设置内发布。
当发生有关争议时,以最新的协议文本为准。如果不同意改动的内容,用户可以自行删除本软件。如果用户继续使用本软件,则视为您接受本协议的变动。
</p>
<p>&nbsp;7.4 光环在法律允许的最大范围内对本协议拥有解释权与修改权。</p>
</div>
body {
margin: 10px;
}
.top {
margin-left: 0;
margin-right: 0;
padding: 10px 0 10px 0;
}
.title {
font-weight: 700;
}
p {
font-size: 14px;
word-break: break-all;
}
.bold {
font-weight: 700;
}
.margintop {
margin-top: 10px;
}
.left-indent {
margin-left: 20px;
}
.red-style {
color: red;
}
.bold-font {
font-weight: bold;
}
span.bold {
font-weight: bold;
}
.link-text {
color: #005ad0;
text-decoration: underline;
}
</style>
<body>
<h3 class="top">光环助手软件许可及服务协议</h3>
<h5 class="title">首部及导言</h5>
<p>欢迎使用光环助手软件许可及服务</p>
<p>
各位用户在使用光环助手前,请您务必审慎阅读、并充分理解本协议中的各项条款,
<span class="bold">
特别是免除或者限制责任的条款,以及开通或使用某项服务的单独协议,并选择接受或不接受。
</span>
除非您已阅读并接受本协议所有条款,否则您无权下载、安装或使用本软件及相关服务。您的下载、安装、使用、登录等行为即视为您已阅读并同意上述协议的约束。
</p>
<p>如果您未满18周岁请在法定监护人的陪同下阅读本协议及其他上述协议。</p>
<h5 class="title margintop">一、权利声明</h5>
<p>
“光环助手”的一切知识产权,以及与“光环助手”相关的所有信息内容,包括但不限于:文字表述及其组合、图标、图饰、图像、图表、色彩、界面设计、版面框架、有关数据、附加程序、印刷材料或电子文档等均为光环助手所有,受著作权法和国际著作权条约以及其他知识产权法律法规的保护。
</p>
<h5 class="title margintop">二、软件使用规范</h5>
<p>
2.1
本软件是基于Android安卓系统手机、平板电脑(PAD)等设备开发的一款软件,提供注册登录、手机游戏管理、游戏推荐、文章阅读等功能
</p>
<p>2.2 软件的下载、安装和使用</p>
<p>
本软件为免费软件,用户可以非商业性、无限制数量地从光环授权的渠道下载、安装及使用本软件。
</p>
<p>
<span class="bold">
如果您从未经光环授权的第三方获取本软件或与本软件名称相同的安装程序,光环无法保证该软件能够正常使用,并对因此给您造成的损失不予负责。
</span>
</p>
<p>2.3 软件的复制、分发和传播</p>
<p>
本产品以学习、研究交流为目的。用户可以非商业性、无限制数量地复制、分发和传播本软件产品。但必须保证每一份复制、分发和传播都是完整和真实的,
包括所有有关本软件产品的软件、电子文档, 版权和商标,亦包括本协议。
</p>
<p>2.4 软件的更新</p>
<p>
为了改善用户体验、完善服务内容,光环将不断努力开发新的服务,并为您不时提供软件更新(这些更新可能会采取软件替换、修改、功能强化、版本升级等形式)。为了保证本软件及服务的安全性和功能的一致性,光环有权不经向您特别通知而对软件进行更新,或者对软件的部分功能效果进行改变或限制。本软件新版本发布后,旧版本的软件可能无法使用。光环不保证旧版本软件继续可用及相应的客户服务,请您随时核对并下载最新版本。
</p>
<h5 class="title margintop">三、用户使用须知</h5>
<p>3.1 您理解并同意:</p>
<p>
为了向您提供有效的服务,本软件会利用您移动通讯终端的处理器和带宽等资源。本软件使用过程中可能产生数据流量的费用,用户需自行向运营商了解相关资费信息,并自行承担相关费用.
</p>
<p>3.2 您理解并同意:</p>
<p>
由本软件进行收录、推荐并提供下载、升级服务的第三方软件,由第三方享有一切合法权利,光环并不能识别用户利用本软件下载、安装的第三方软件是否有合法来源。
<span class="bold">
因第三方软件引发的任何纠纷,由该第三方负责解决,光环不承担任何责任。
</span>
同时光环不对第三方软件或技术提供客服支持,若用户需要获取支持,请与该软件或技术提供商联系,若您为有关软件的权利人,不愿本软件为您的软件提供用户下载、安装、使用的服务,也可按本协议约定的联系方式联系我们,我们将会积极配合进行处理。
</p>
<p>3.3 您理解并同意:</p>
<p>
<span class="bold">
如果因您不正当使用本软件造成了不良影响,或因使用本软件造成的包括但不限于数据异常等问题,均由使用者自行承担,光环团队不对任意类型的使用结果承担责任;
</span>
</p>
<p>3.4 您理解并同意:</p>
<p>
本软件不含任何破坏用户移动通讯设备数据和获取用户隐私信息的恶意代码,不会泄露用户的个人信息和隐私;
</p>
<p>3.5 您理解并同意:</p>
<p>
<span class="bold">
对于包括但不限于互联网网络故障、计算机故障、手机故障或病毒、信息损坏或丢失、计算机系统问题,或其它任何基于不可抗力原因而产生的损失,光环团队不承担任何责任。
</span>
</p>
<p>3.6 您理解并同意:</p>
<p>光环发布、收录的文章均不代表光环立场。</p>
<p>3.7 您理解并同意:</p>
<p>
为实现软件包括但不限于集中展示、下载、安装、卸载等游戏管理功能以及文章优先推荐功能,本软件会检测用户手机中已安装游戏的包名、版本号、版本名、游戏名称信息。除征得用户明确同意和法律明确规定外,光环不会向第三方泄露任何的用户信息
</p>
<p>3.8 您理解并同意:</p>
<p>
用户应在遵守法律及本协议的前提下使用本软件。用户无权实施包括但不限于下列行为:
</p>
3.8.1 不得删除或者改变本软件上的所有权利管理电子信息
<br />
3.8.2 不得故意避开或者破坏著作权人为保护本软件著作权而采取的技术措施;
<br />
3.8.3 用户不得利用本软件误导、欺骗他人;
<br />
3.8.4
违反国家规定,对计算机信息系统功能进行删除、修改、增加、干扰,造成计算机信息系统不能正常运行;
<br />
3.8.5 未经允许,进入计算机信息网络或者使用计算机信息网络资源;
<br />
3.8.6 未经允许,对计算机信息网络功能进行删除、修改或者增加;
<br />
3.8.7
未经允许,对计算机信息网络中存储、处理或者传输的数据和应用程序进行删除、修改或者增加;
<br />
3.8.8 破坏本软件系统或网站的正常运行,故意传播计算机病毒等破坏性程序;
<br />
3.8.9 其他任何危害计算机信息网络安全的行为。
<br />
<p>3.9 您理解并同意:</p>
<p>
本软件经过详细的测试,但不能保证与所有的软硬件系统完全兼容,不能保证本软件完全没有错误。如果出现不兼容及软件错误的情况,用户可通过各反馈途径将情况告知光环团队,获得技术支持。如果无法解决兼容性问题,用户可以删除本软件。
</p>
<h5 class="title margintop">四、争议解决处理</h5>
<p>
本《协议》的解释、效力及纠纷的解决,适用于中华人民共和国法律。若用户和光环助手之间发生任何纠纷或争议,首先应友好协商解决,协商不成的,用户在此完全同意将纠纷或争议提交光环助手所在地法院管辖
</p>
<p class="title margintop"><b>五、第三方SDK接入说明</b></p>
<p>
为保障光环助手App相关功能的实现与应用安全稳定的运行我们会接入由第三方提供的软件开发包SDK实现相关功能。
<br />
我们会对合作方获取有关信息的软件工具开发包SDK进行严格的安全检测并与授权合作伙伴约定严格的数据保护措施令其按照我们的委托目的、服务说明、本隐私权政策以及其他任何相关的保密和安全措施来处理个人信息。
<br />
<span class="red-style">
下方为整个光环助手
<span class="bold">所有版本</span>
内接入的所有信息收集类第三方SDK的权限说明因隐私政策会因光环助手版本迭代而新接入SDK或停止合作部分SDK方便照顾
<span class="bold">所有版本</span>
的用户查看自己SDK第三方权限说明。
<br />
我们对涉及用户信息使用的SDK相关情况进行了逐项列举具体如下
</span>
</p>
<p class="margintop red-style bold-font"><b>1数据统计类</b></p>
<p>1.头条推广</p>
<p>
SDK官网
<span class="link-text">
https://ad.oceanengine.com/openapi/index.html
</span>
</p>
<p>SDK包名com.bytedance</p>
<p>企业主体:北京有竹居网络技术有限公司</p>
<p>使用目的:用于广告流量统计相关服务</p>
<p>
收集信息类型设备品牌、型号、软件系统相关信息、安卓oaid、无线网SSID名称、WiFi路由器MAC地址、设备MAC地址、IMEI、地理位置
</p>
<p>
隐私政策链接:
<span class="link-text">
https://ad.oceanengine.com/openapi/register/protocol.html?rid=vo25p8sfqde
</span>
</p>
<p>2.talkingdata统计</p>
<p>
SDK官网
<span class="link-text">http://www.talkingdata.com/</span>
</p>
<p>SDK包名com.tendcloud</p>
<p>企业主体:北京腾云天下科技有限公司</p>
<p>使用目的:用于统计数据和效果分析,以便为用户提供更好的服务</p>
<p>收集信息类型:设备信息、网络信息、位置信息、应用信息</p>
<p>
隐私政策链接:
<span class="link-text">
http://www.talkingdata.com/privacy.jsp?languagetype=zh_cn
</span>
</p>
<p>3.腾讯MTA</p>
<p>
SDK官网
<span class="link-text">https://mta.qq.com/mta/</span>
</p>
<p>SDK包名com.tencent</p>
<p>企业主体:深圳市腾讯计算机系统有限公司</p>
<p>使用目的:用于统计数据和效果分析</p>
<p>
收集信息类型Mac地址、唯一设备识别码IMEI、android
ID、IDFA、OPENUDID、GUID/SIM卡IMSI信息、地理位置信息
</p>
<p>
隐私政策链接:
<span class="link-text">
https://mta.qq.com/mta/ctr_index/protocol_v2/
</span>
</p>
<p>4.腾讯广点通</p>
<p>
SDK官网
<span class="link-text">https://developers.e.qq.com/</span>
</p>
<p>SDK包名com.tencent</p>
<p>企业主体:深圳市腾讯计算机系统有限公司</p>
<p>使用目的:用于广告流量统计相关服务</p>
<p>
收集信息类型:
个人常用设备信息IMEI、AndroidID、位置信息IP地址、软件版本号
</p>
<p>
隐私政策链接:
<span class="link-text">https://e.qq.com/optout.html</span>
</p>
<p class="margintop red-style bold-font"><b>2社交登录类</b></p>
<p>5.微信登录分享</p>
<p>
SDK官网
<span class="link-text">https://open.weixin.qq.com/</span>
</p>
<p>SDK包名com.tencent.mm.opensdk</p>
<p>企业主体:深圳市腾讯计算机系统有限公司</p>
<p>使用目的:用于支持微信登录、分享</p>
<p>
收集信息类型个人常用设备信息MAC地址、IMEI、AndroidID、硬件型号、操作系统类型、软件信息软件版本号、浏览器类型、IP地址、服务日志信息、通讯日志信息
</p>
<p>
隐私政策链接:
<span class="link-text">https://privacy.tencent.com/</span>
</p>
<p>6.QQ登录分享</p>
<p>
SDK官网
<span class="link-text">https://connect.qq.com/</span>
</p>
<p>SDK包名com.tentcent</p>
<p>企业主体:深圳市腾讯计算机系统有限公司</p>
<p>使用目的用于支持QQ登录、分享</p>
<p>
收集信息类型个人常用设备信息MAC地址、IMEI、AndroidID、IMSI、ICCID、序列号、设备型号、操作系统版本、软件信息软件版本号、浏览器类型、网络信息、IP地址、服务日志信息、通讯日志信息 
</p>
<p>
隐私政策链接:
<span class="link-text">
https://wiki.connect.qq.com/qq互联sdk隐私保护声明
</span>
</p>
<p>7.微博登录分享</p>
<p>
SDK官网
<span class="link-text">http://open.weibo.com/authentication</span>
</p>
<p>SDK包名com.sina.weibo.sdk</p>
<p>企业主体:北京微梦创科网络技术有限公司</p>
<p>使用目的:用于支持微博登录、分享</p>
<p>
收集信息类型个人常用设备信息MAC地址、IMEI、AndroidID、IMSI、ICCID、序列号、网络信息、应用列表硬件型号、操作系统类型、软件信息软件版本号、浏览器类型、IP地址、服务日志信息、通讯日志信息
</p>
<p>
隐私政策链接:
<span class="link-text">https://open.weibo.com/wiki/开发者协议</span>
</p>
<p>8.头条抖音登录</p>
<p>
SDK官网
<span class="link-text">https://open.douyin.com/platform</span>
</p>
<p>SDK包名com.bytedance.sdk</p>
<p>企业主体:北京字节跳动科技有限公司</p>
<p>使用目的:用于支持抖音登录</p>
<p>
收集信息类型个人常用设备信息MAC地址、IMEI、AndroidID、硬件型号、操作系统类型、软件信息软件版本号、浏览器类型、IP地址、服务日志信息、通讯日志信息
</p>
<p>
隐私政策链接:
<span class="link-text">
https://www.douyin.com/agreements/?id=6773901168964798477
</span>
</p>
<p class="margintop red-style bold-font"><b>3推送通知类</b></p>
<p>9.友盟推送</p>
<p>
SDK官网
<span class="link-text">https://www.umeng.com/push</span>
</p>
<p>SDK包名com.umeng</p>
<p>企业主体:北京友盟网络科技有限公司</p>
<p>使用目的:用于游戏相关信息的提醒通知</p>
<p>
收集信息类型Mac地址、唯一设备识别码IMEI、android
ID、IDFA、OPENUDID、GUID/SIM卡IMSI信息、地理位置信息
</p>
<p>
隐私政策链接:
<span class="link-text">
https://www.umeng.com/page/policy?spm=a213m0.14063960.0.0.7f626e72hx3nnv
</span>
</p>
<p class="margintop red-style bold-font"><b>4其他功能类</b></p>
<p>10.阿里云反爬虫</p>
<p>
SDK官网
<span class="link-text">https://www.aliyun.com/product/antibot</span>
</p>
<p>SDK包名com.alibaba.wireless</p>
<p>企业主体:阿里巴巴网络技术有限公司</p>
<p>使用目的为APP提供网络应用安全防护</p>
<p>
收集信息类型设备相关信息例如设备型号、操作系统版本、设备设置、唯一设备标识符等软硬件特征信息、设备所在位置相关信息例如IP地址、GPS位置以及能够提供相关信息的Wi-Fi接入点、蓝牙和基站等传感器信息
</p>
<p>
隐私政策链接:
<span class="link-text">
http://terms.aliyun.com/legal-agreement/terms/suit_bu1_ali_cloud/suit_bu1_ali_cloud201902141711_54837.html?spm=a2c4g.11186623.J_9220772140.81.b7574832gmk0vr
</span>
</p>
<p>11.腾讯bugly</p>
<p>
SDK官网
<span class="link-text">https://bugly.qq.com/v2/</span>
</p>
<p>SDK包名com.tencent.bugly</p>
<p>企业主体:深圳市腾讯计算机系统有限公司</p>
<p>使用目的APP异常上报</p>
<p>
收集信息类型:设备及应用信息。如:设备名称、设备识别符、硬件型号、操作系统版本、应用程序版本
</p>
<p>
隐私政策链接:
<span class="link-text">https://bugly.qq.com/v2/contract</span>
</p>
<p>12.阿里云文件上传</p>
<p>
SDK官网
<span class="link-text">https://www.alibabacloud.com/zh</span>
</p>
<p>SDK包名com.alibaba.sdk.android</p>
<p>SDK包名com.alibaba.sdk.android</p>
<p>企业主体:阿里巴巴网络技术有限公司</p>
<p>使用目的:用于支持用户上传视频等相关内容</p>
<p>
收集信息类型设备相关信息例如设备型号、操作系统版本、设备设置、唯一设备标识符等软硬件特征信息、设备所在位置相关信息例如IP地址、GPS位置以及能够提供相关信息的Wi-Fi接入点、蓝牙和基站等传感器信息
</p>
<p>
隐私政策链接:
<span class="link-text">
http://terms.aliyun.com/legal-agreement/terms/suit_bu1_ali_cloud/suit_bu1_ali_cloud201902141711_54837.html?spm=a2c4g.11186623.J_9220772140.81.b7574832gmk0vr
</span>
</p>
<p>13.阿里云日志上传</p>
<p>
SDK官网
<span class="link-text">https://www.alibabacloud.com/zh</span>
</p>
<p>SDK包名com.aliyun.sls.android.sdk</p>
<p>企业主体:阿里巴巴网络技术有限公司</p>
<p>
使用目的:通过网络日志分析这些信息以便更及时响应您的帮助请求,以及用于改进服务
</p>
<p>
收集信息类型设备相关信息例如设备型号、操作系统版本、设备设置、唯一设备标识符等软硬件特征信息、设备所在位置相关信息例如IP地址、GPS位置以及能够提供相关信息的Wi-Fi接入点、蓝牙和基站等传感器信息
</p>
<p>
隐私政策链接:
<span class="link-text">
http://terms.aliyun.com/legal-agreement/terms/suit_bu1_ali_cloud/suit_bu1_ali_cloud201902141711_54837.html?spm=a2c4g.11186623.J_9220772140.81.b7574832gmk0vr
</span>
</p>
<p>14.容联七陌</p>
<p>
SDK官网
<span class="link-text">https://www.7moor.com/developer</span>
</p>
<p>SDK包名com.m7.imkfsdk</p>
<p>企业主体:北京七陌科技有限公司</p>
<p>使用目的:用于提供对应在线客服功能</p>
<p>
收集信息类型:设备相关信息(设备名称、设备型号、硬件序列号、操作系统和应用程序版本及类型、语言设置、分辨率、移动终端随机存储内存、摄像头/相册、通讯录权限等)
</p>
<p>
隐私政策链接:
<span class="link-text">http://m.7moor.com/72/57/p5077783560e807/</span>
</p>
<h5 class="title margintop">六、关于获取手机设备信息的说明</h5>
<div>
1为方便区分每个用户的个人信息等本软件需获取用户的手机设备信息用于游戏主动预约、论坛互动交流后进行推送等用户相关的行为
<br />
2为了保障软件与服务的安全运行我们会收集您的硬件型号、操作系统版本号、国际移动设备识别码、唯一设备标识符、网络设备硬件地址、IP
地址、WLAN接入点、蓝牙、基站、软件版本号、网络接入方式、类型、状态、网络质量数据、操作、使用、服务日志。
<br />
3为了预防恶意程序及安全运营所必需我们会收集安装的应用信息或正在运行的进程信息、应用程序的总体运行、使用情况与频率、应用崩溃情况、总体安装使用情况、性能数据、应用来源。
<br />
4我们可能使用您的账户信息、设备信息、服务日志信息以及我们关联公司、合作方在获得您授权或依法可以共享的信息用于判断账户安全、进行身份验证、检测及防范安全事件。
<br />
5具体会发生获取手机设备信息场景如下说明
<br />
<p class="left-indent">
1) 首次启动光环助手
<br />
2) 游戏列表/游戏详情/资讯文章详情/搜索结果页-预约功能
<br />
3) 礼包中心/礼包详情-领取功能
<br />
4) 评论详情-发送评论功能
<br />
5) 回答/问题详情-我来回答功能
<br />
6) 问答首页-提问功能
<br />
7) 个人主页-发文章功能
<br />
8) 帖子草稿/我的草稿-编辑功能
<br />
9) 游戏投稿功能
<br />
10) 视频投稿-上传视频功能
<br />
11) 游戏详情-关注游戏功能
</p>
</div>
<h5 class="title margintop">七、其他</h5>
<p>
7.1
本协议所有条款的标题仅为阅读方便,本身并无实际涵义,不能作为本协议涵义解释的依据。
<br />
7.2
如果本协议中的任何条款无论因何种原因完全或部分无效或不具有执行力,或违反任何适用的法律,则该条款被视为删除,但本协议的其余条款仍应有效并且有约束力。
<br />
7.3
光环有权随时根据有关法律、法规的变化以及公司经营状况和经营策略的调整等修改本协议。修改后的协议会在软件设置内发布。
当发生有关争议时,以最新的协议文本为准。如果不同意改动的内容,用户可以自行删除本软件。如果用户继续使用本软件,则视为您接受本协议的变动。
<br />
<span class="bold">
7.4 光环在法律允许的最大范围内对本协议拥有解释权与修改权。
</span>
</p>
</body>
</html>
</body></html>

View File

@ -1,58 +0,0 @@
package androidx.swiperefreshlayout.widget;
import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.ViewConfiguration;
public class ViewPagerSwipeRefreshLayout extends SwipeRefreshLayout {
private float startY;
private float startX;
// 记录viewPager是否拖拽的标记
private boolean mIsVpDragger;
private final int mTouchSlop;
public ViewPagerSwipeRefreshLayout(Context context, AttributeSet attrs) {
super(context, attrs);
mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
int action = ev.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
// 记录手指按下的位置
startY = ev.getY();
startX = ev.getX();
// 初始化标记
mIsVpDragger = false;
break;
case MotionEvent.ACTION_MOVE:
// 如果viewpager正在拖拽中那么不拦截它的事件直接return false
if(mIsVpDragger) {
return false;
}
// 获取当前手指位置
float endY = ev.getY();
float endX = ev.getX();
float distanceX = Math.abs(endX - startX);
float distanceY = Math.abs(endY - startY);
// 如果X轴位移大于Y轴位移那么将事件交给viewPager处理。
if(distanceX > mTouchSlop && distanceX > distanceY) {
mIsVpDragger = true;
return false;
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
// 初始化标记
mIsVpDragger = false;
break;
}
// 如果是Y轴位移大于X轴事件交给swipeRefreshLayout处理。
return super.onInterceptTouchEvent(ev);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,550 +0,0 @@
/*
* 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

@ -1,171 +0,0 @@
/*
* 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;
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

@ -1,52 +0,0 @@
/*
* 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_V4 = 4;
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 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

@ -1,123 +0,0 @@
/*
* 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

@ -1,882 +0,0 @@
/*
* 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<>();
/*
* 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());
}
/**
* 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 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;
}
}
}
/**
* 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

@ -1,35 +0,0 @@
/*
* 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

@ -1,32 +0,0 @@
/*
* 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

@ -1,670 +0,0 @@
/*
* 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

@ -1,199 +0,0 @@
/*
* 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

@ -1,46 +0,0 @@
/*
* 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

@ -1,40 +0,0 @@
/*
* 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

@ -1,869 +0,0 @@
/*
* 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

@ -1,104 +0,0 @@
/*
* 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

@ -1,79 +0,0 @@
/*
* 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 List<X509Certificate> certs = new ArrayList<>();
public List<X509Certificate> certificateLineage = 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));
}
/**
* 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 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;
}
}

View File

@ -1,393 +0,0 @@
/*
* 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

@ -1,35 +0,0 @@
/*
* 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

@ -1,61 +0,0 @@
/*
* 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

@ -1,27 +0,0 @@
/*
* 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

@ -1,225 +0,0 @@
/*
* 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

@ -1,53 +0,0 @@
/*
* 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

@ -1,30 +0,0 @@
/*
* 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

@ -1,235 +0,0 @@
/*
* 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

@ -1,27 +0,0 @@
/*
* 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;
}

View File

@ -1,348 +0,0 @@
/*
* 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.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.apk.v3.V3SigningCertificateLineage;
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()) {
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.addWarning(
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 {
result.addWarning(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

@ -1,109 +0,0 @@
/*
* 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

@ -1,139 +0,0 @@
/*
* 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

@ -1,219 +0,0 @@
/*
* 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.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 abstract class V2SourceStampSigner {
public static final int V2_SOURCE_STAMP_BLOCK_ID =
SourceStampConstants.V2_SOURCE_STAMP_BLOCK_ID;
/** Hidden constructor to prevent instantiation. */
private V2SourceStampSigner() {
}
public static Pair<byte[], Integer> generateSourceStampBlock(
SignerConfig sourceStampSignerConfig,
Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeDigestInfos)
throws SignatureException, NoSuchAlgorithmException, InvalidKeyException {
if (sourceStampSignerConfig.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,
signatureSchemeDigestInfos,
sourceStampSignerConfig,
signatureSchemeDigests);
getSignedDigestsFor(
VERSION_APK_SIGNATURE_SCHEME_V2,
signatureSchemeDigestInfos,
sourceStampSignerConfig,
signatureSchemeDigests);
getSignedDigestsFor(
VERSION_JAR_SIGNATURE_SCHEME,
signatureSchemeDigestInfos,
sourceStampSignerConfig,
signatureSchemeDigests);
Collections.sort(signatureSchemeDigests, 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);
}
sourceStampBlock.signedDigests = signatureSchemeDigests;
sourceStampBlock.stampAttributes = encodeStampAttributes(
generateStampAttributes(sourceStampSignerConfig.mSigningCertificateLineage));
sourceStampBlock.signedStampAttributes =
ApkSigningBlockUtils.generateSignaturesOverData(sourceStampSignerConfig,
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[]>> signatureSchemeDigestInfos,
SignerConfig sourceStampSignerConfig,
List<Pair<Integer, byte[]>> signatureSchemeDigests)
throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
if (!signatureSchemeDigestInfos.containsKey(signatureSchemeVersion)) {
return;
}
Map<ContentDigestAlgorithm, byte[]> digestInfo =
signatureSchemeDigestInfos.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(
sourceStampSignerConfig, 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 static Map<Integer, byte[]> generateStampAttributes(SigningCertificateLineage lineage) {
HashMap<Integer, byte[]> stampAttributes = new HashMap<>();
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;
}
}

View File

@ -1,154 +0,0 @@
/*
* 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, Comparator.comparing(Pair::getFirst));
return digests;
}
}

View File

@ -1,74 +0,0 @@
/*
* 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

@ -1,26 +0,0 @@
/*
* 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

@ -1,580 +0,0 @@
/*
* 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.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");
}
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

@ -1,25 +0,0 @@
/*
* 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;
}

View File

@ -1,321 +0,0 @@
/*
* 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.v2;
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedElements;
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes;
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeCertificates;
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodePublicKey;
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.apk.SignatureAlgorithm;
import com.android.apksig.internal.util.Pair;
import com.android.apksig.util.DataSource;
import com.android.apksig.util.RunnablesExecutor;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.SignatureException;
import java.security.cert.CertificateEncodingException;
import java.security.interfaces.ECKey;
import java.security.interfaces.RSAKey;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
/**
* APK Signature Scheme v2 signer.
*
* <p>APK Signature Scheme v2 is a whole-file signature scheme which aims to protect every single
* bit of the APK, as opposed to the JAR Signature Scheme which protects only the names and
* uncompressed contents of ZIP entries.
*
* @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature Scheme v2</a>
*/
public abstract class V2SchemeSigner {
/*
* The two main goals of APK Signature Scheme v2 are:
* 1. Detect any unauthorized modifications to the APK. This is achieved by making the signature
* cover every byte of the APK being signed.
* 2. Enable much faster signature and integrity verification. This is achieved by requiring
* only a minimal amount of APK parsing before the signature is verified, thus completely
* bypassing ZIP entry decompression and by making integrity verification parallelizable by
* employing a hash tree.
*
* The generated signature block is wrapped into an APK Signing Block and inserted into the
* original APK immediately before the start of ZIP Central Directory. This is to ensure that
* JAR and ZIP parsers continue to work on the signed APK. The APK Signing Block is designed for
* extensibility. For example, a future signature scheme could insert its signatures there as
* well. The contract of the APK Signing Block is that all contents outside of the block must be
* protected by signatures inside the block.
*/
public static final int APK_SIGNATURE_SCHEME_V2_BLOCK_ID =
V2SchemeConstants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID;
/** Hidden constructor to prevent instantiation. */
private V2SchemeSigner() {}
/**
* Gets the APK Signature Scheme v2 signature algorithms 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 APK
* Signature Scheme v2
*/
public static List<SignatureAlgorithm> getSuggestedSignatureAlgorithms(PublicKey signingKey,
int minSdkVersion, boolean verityEnabled, boolean deterministicDsaSigning)
throws InvalidKeyException {
String keyAlgorithm = signingKey.getAlgorithm();
if ("RSA".equalsIgnoreCase(keyAlgorithm)) {
// Use RSASSA-PKCS1-v1_5 signature scheme instead of RSASSA-PSS to guarantee
// deterministic signatures which make life easier for OTA updates (fewer files
// changed when deterministic signature schemes are used).
// Pick a digest which is no weaker than the key.
int modulusLengthBits = ((RSAKey) signingKey).getModulus().bitLength();
if (modulusLengthBits <= 3072) {
// 3072-bit RSA is roughly 128-bit strong, meaning SHA-256 is a good fit.
List<SignatureAlgorithm> algorithms = new ArrayList<>();
algorithms.add(SignatureAlgorithm.RSA_PKCS1_V1_5_WITH_SHA256);
if (verityEnabled) {
algorithms.add(SignatureAlgorithm.VERITY_RSA_PKCS1_V1_5_WITH_SHA256);
}
return algorithms;
} else {
// Keys longer than 3072 bit need to be paired with a stronger digest to avoid the
// digest being the weak link. SHA-512 is the next strongest supported digest.
return Collections.singletonList(SignatureAlgorithm.RSA_PKCS1_V1_5_WITH_SHA512);
}
} else if ("DSA".equalsIgnoreCase(keyAlgorithm)) {
// DSA is supported only with SHA-256.
List<SignatureAlgorithm> algorithms = new ArrayList<>();
algorithms.add(
deterministicDsaSigning ?
SignatureAlgorithm.DETDSA_WITH_SHA256 :
SignatureAlgorithm.DSA_WITH_SHA256);
if (verityEnabled) {
algorithms.add(SignatureAlgorithm.VERITY_DSA_WITH_SHA256);
}
return algorithms;
} else if ("EC".equalsIgnoreCase(keyAlgorithm)) {
// Pick a digest which is no weaker than the key.
int keySizeBits = ((ECKey) signingKey).getParams().getOrder().bitLength();
if (keySizeBits <= 256) {
// 256-bit Elliptic Curve is roughly 128-bit strong, meaning SHA-256 is a good fit.
List<SignatureAlgorithm> algorithms = new ArrayList<>();
algorithms.add(SignatureAlgorithm.ECDSA_WITH_SHA256);
if (verityEnabled) {
algorithms.add(SignatureAlgorithm.VERITY_ECDSA_WITH_SHA256);
}
return algorithms;
} else {
// Keys longer than 256 bit need to be paired with a stronger digest to avoid the
// digest being the weak link. SHA-512 is the next strongest supported digest.
return Collections.singletonList(SignatureAlgorithm.ECDSA_WITH_SHA512);
}
} else {
throw new InvalidKeyException("Unsupported key algorithm: " + keyAlgorithm);
}
}
public static ApkSigningBlockUtils.SigningSchemeBlockAndDigests
generateApkSignatureSchemeV2Block(RunnablesExecutor executor,
DataSource beforeCentralDir,
DataSource centralDir,
DataSource eocd,
List<SignerConfig> signerConfigs,
boolean v3SigningEnabled)
throws IOException, InvalidKeyException, NoSuchAlgorithmException,
SignatureException {
return generateApkSignatureSchemeV2Block(executor, beforeCentralDir, centralDir, eocd,
signerConfigs, v3SigningEnabled, null);
}
public static ApkSigningBlockUtils.SigningSchemeBlockAndDigests
generateApkSignatureSchemeV2Block(
RunnablesExecutor executor,
DataSource beforeCentralDir,
DataSource centralDir,
DataSource eocd,
List<SignerConfig> signerConfigs,
boolean v3SigningEnabled,
List<byte[]> preservedV2SignerBlocks)
throws IOException, InvalidKeyException, NoSuchAlgorithmException,
SignatureException {
Pair<List<SignerConfig>, Map<ContentDigestAlgorithm, byte[]>> digestInfo =
ApkSigningBlockUtils.computeContentDigests(
executor, beforeCentralDir, centralDir, eocd, signerConfigs);
return new ApkSigningBlockUtils.SigningSchemeBlockAndDigests(
generateApkSignatureSchemeV2Block(
digestInfo.getFirst(), digestInfo.getSecond(), v3SigningEnabled,
preservedV2SignerBlocks),
digestInfo.getSecond());
}
private static Pair<byte[], Integer> generateApkSignatureSchemeV2Block(
List<SignerConfig> signerConfigs,
Map<ContentDigestAlgorithm, byte[]> contentDigests,
boolean v3SigningEnabled,
List<byte[]> preservedV2SignerBlocks)
throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
// FORMAT:
// * length-prefixed sequence of length-prefixed signer blocks.
List<byte[]> signerBlocks = new ArrayList<>(signerConfigs.size());
if (preservedV2SignerBlocks != null && preservedV2SignerBlocks.size() > 0) {
signerBlocks.addAll(preservedV2SignerBlocks);
}
int signerNumber = 0;
for (SignerConfig signerConfig : signerConfigs) {
signerNumber++;
byte[] signerBlock;
try {
signerBlock = generateSignerBlock(signerConfig, contentDigests, v3SigningEnabled);
} catch (InvalidKeyException e) {
throw new InvalidKeyException("Signer #" + signerNumber + " failed", e);
} catch (SignatureException e) {
throw new SignatureException("Signer #" + signerNumber + " failed", e);
}
signerBlocks.add(signerBlock);
}
return Pair.of(
encodeAsSequenceOfLengthPrefixedElements(
new byte[][] {
encodeAsSequenceOfLengthPrefixedElements(signerBlocks),
}),
V2SchemeConstants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID);
}
private static byte[] generateSignerBlock(
SignerConfig signerConfig,
Map<ContentDigestAlgorithm, byte[]> contentDigests,
boolean v3SigningEnabled)
throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
if (signerConfig.certificates.isEmpty()) {
throw new SignatureException("No certificates configured for signer");
}
PublicKey publicKey = signerConfig.certificates.get(0).getPublicKey();
byte[] encodedPublicKey = encodePublicKey(publicKey);
V2SignatureSchemeBlock.SignedData signedData = new V2SignatureSchemeBlock.SignedData();
try {
signedData.certificates = encodeCertificates(signerConfig.certificates);
} catch (CertificateEncodingException e) {
throw new SignatureException("Failed to encode certificates", e);
}
List<Pair<Integer, byte[]>> digests =
new ArrayList<>(signerConfig.signatureAlgorithms.size());
for (SignatureAlgorithm signatureAlgorithm : signerConfig.signatureAlgorithms) {
ContentDigestAlgorithm contentDigestAlgorithm =
signatureAlgorithm.getContentDigestAlgorithm();
byte[] contentDigest = contentDigests.get(contentDigestAlgorithm);
if (contentDigest == null) {
throw new RuntimeException(
contentDigestAlgorithm
+ " content digest for "
+ signatureAlgorithm
+ " not computed");
}
digests.add(Pair.of(signatureAlgorithm.getId(), contentDigest));
}
signedData.digests = digests;
signedData.additionalAttributes = generateAdditionalAttributes(v3SigningEnabled);
V2SignatureSchemeBlock.Signer signer = new V2SignatureSchemeBlock.Signer();
// FORMAT:
// * 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).
// * length-prefixed sequence of length-prefixed additional attributes:
// * uint32: ID
// * (length - 4) bytes: value
signer.signedData =
encodeAsSequenceOfLengthPrefixedElements(
new byte[][] {
encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
signedData.digests),
encodeAsSequenceOfLengthPrefixedElements(signedData.certificates),
signedData.additionalAttributes,
new byte[0],
});
signer.publicKey = encodedPublicKey;
signer.signatures = new ArrayList<>();
signer.signatures =
ApkSigningBlockUtils.generateSignaturesOverData(signerConfig, signer.signedData);
// FORMAT:
// * length-prefixed signed data
// * length-prefixed sequence of length-prefixed signatures:
// * uint32: signature algorithm ID
// * length-prefixed bytes: signature of signed data
// * length-prefixed bytes: public key (X.509 SubjectPublicKeyInfo, ASN.1 DER encoded)
return encodeAsSequenceOfLengthPrefixedElements(
new byte[][] {
signer.signedData,
encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
signer.signatures),
signer.publicKey,
});
}
private static byte[] generateAdditionalAttributes(boolean v3SigningEnabled) {
if (v3SigningEnabled) {
// FORMAT (little endian):
// * length-prefixed bytes: attribute pair
// * uint32: ID - STRIPPING_PROTECTION_ATTR_ID in this case
// * uint32: value - 3 (v3 signature scheme id) in this case
int payloadSize = 4 + 4 + 4;
ByteBuffer result = ByteBuffer.allocate(payloadSize);
result.order(ByteOrder.LITTLE_ENDIAN);
result.putInt(payloadSize - 4);
result.putInt(V2SchemeConstants.STRIPPING_PROTECTION_ATTR_ID);
result.putInt(ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3);
return result.array();
} else {
return new byte[0];
}
}
private static final class V2SignatureSchemeBlock {
private static final class Signer {
public byte[] signedData;
public List<Pair<Integer, byte[]>> signatures;
public byte[] publicKey;
}
private static final class SignedData {
public List<Pair<Integer, byte[]>> digests;
public List<byte[]> certificates;
public byte[] additionalAttributes;
}
}
}

View File

@ -1,466 +0,0 @@
/*
* 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.v2;
import com.android.apksig.ApkVerifier.Issue;
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.SignatureAlgorithm;
import com.android.apksig.internal.apk.SignatureInfo;
import com.android.apksig.internal.util.ByteBufferUtils;
import com.android.apksig.internal.util.X509CertificateUtils;
import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
import com.android.apksig.util.DataSource;
import com.android.apksig.util.RunnablesExecutor;
import java.io.IOException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
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.security.spec.X509EncodedKeySpec;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* APK Signature Scheme v2 verifier.
*
* <p>APK Signature Scheme v2 is a whole-file signature scheme which aims to protect every single
* bit of the APK, as opposed to the JAR Signature Scheme which protects only the names and
* uncompressed contents of ZIP entries.
*
* @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature Scheme v2</a>
*/
public abstract class V2SchemeVerifier {
/** Hidden constructor to prevent instantiation. */
private V2SchemeVerifier() {}
/**
* Verifies the provided APK's APK Signature Scheme v2 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()}.
*
* <p>Verification succeeds iff the APK's APK Signature Scheme v2 signatures are expected to
* verify on all Android platform versions in the {@code [minSdkVersion, maxSdkVersion]} range.
* If the APK's signature is expected to not verify on any of the specified platform versions,
* this method returns a result with one or more errors and whose
* {@code Result.verified == false}, or this method throws an exception.
*
* @throws ApkFormatException if the APK is malformed
* @throws NoSuchAlgorithmException if the APK's signatures cannot be verified because a
* required cryptographic algorithm implementation is missing
* @throws ApkSigningBlockUtils.SignatureNotFoundException if no APK Signature Scheme v2
* signatures are found
* @throws IOException if an I/O error occurs when reading the APK
*/
public static ApkSigningBlockUtils.Result verify(
RunnablesExecutor executor,
DataSource apk,
ApkUtils.ZipSections zipSections,
Map<Integer, String> supportedApkSigSchemeNames,
Set<Integer> foundSigSchemeIds,
int minSdkVersion,
int maxSdkVersion)
throws IOException, ApkFormatException, NoSuchAlgorithmException,
ApkSigningBlockUtils.SignatureNotFoundException {
ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result(
ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2);
SignatureInfo signatureInfo =
ApkSigningBlockUtils.findSignature(apk, zipSections,
V2SchemeConstants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID , result);
DataSource beforeApkSigningBlock = apk.slice(0, signatureInfo.apkSigningBlockOffset);
DataSource centralDir =
apk.slice(
signatureInfo.centralDirOffset,
signatureInfo.eocdOffset - signatureInfo.centralDirOffset);
ByteBuffer eocd = signatureInfo.eocd;
verify(executor,
beforeApkSigningBlock,
signatureInfo.signatureBlock,
centralDir,
eocd,
supportedApkSigSchemeNames,
foundSigSchemeIds,
minSdkVersion,
maxSdkVersion,
result);
return result;
}
/**
* Verifies the provided APK's v2 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(RunnablesExecutor, DataSource, ApkUtils.ZipSections, Map,
* Set, int, int)} for more information about the contract of this method.
*
* @param result result populated by this method with interesting information about the APK,
* such as information about signers, and verification errors and warnings.
*/
private static void verify(
RunnablesExecutor executor,
DataSource beforeApkSigningBlock,
ByteBuffer apkSignatureSchemeV2Block,
DataSource centralDir,
ByteBuffer eocd,
Map<Integer, String> supportedApkSigSchemeNames,
Set<Integer> foundSigSchemeIds,
int minSdkVersion,
int maxSdkVersion,
ApkSigningBlockUtils.Result result)
throws IOException, NoSuchAlgorithmException {
Set<ContentDigestAlgorithm> contentDigestsToVerify = new HashSet<>(1);
parseSigners(
apkSignatureSchemeV2Block,
contentDigestsToVerify,
supportedApkSigSchemeNames,
foundSigSchemeIds,
minSdkVersion,
maxSdkVersion,
result);
if (result.containsErrors()) {
return;
}
ApkSigningBlockUtils.verifyIntegrity(
executor, beforeApkSigningBlock, centralDir, eocd, contentDigestsToVerify, result);
if (!result.containsErrors()) {
result.verified = true;
}
}
/**
* Parses each signer in the provided APK Signature Scheme v2 block and populates corresponding
* {@code signerInfos} of the provided {@code result}.
*
* <p>This verifies signatures over {@code signed-data} block contained in each signer block.
* However, this does not verify the integrity of the rest of the APK but rather simply reports
* the expected digests of the rest of the APK (see {@code contentDigestsToVerify}).
*
* <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 apkSignatureSchemeV2Block,
Set<ContentDigestAlgorithm> contentDigestsToVerify,
Map<Integer, String> supportedApkSigSchemeNames,
Set<Integer> foundApkSigSchemeIds,
int minSdkVersion,
int maxSdkVersion,
ApkSigningBlockUtils.Result result) throws NoSuchAlgorithmException {
ByteBuffer signers;
try {
signers = ApkSigningBlockUtils.getLengthPrefixedSlice(apkSignatureSchemeV2Block);
} catch (ApkFormatException e) {
result.addError(Issue.V2_SIG_MALFORMED_SIGNERS);
return;
}
if (!signers.hasRemaining()) {
result.addError(Issue.V2_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);
}
int signerCount = 0;
while (signers.hasRemaining()) {
int signerIndex = signerCount;
signerCount++;
ApkSigningBlockUtils.Result.SignerInfo signerInfo =
new ApkSigningBlockUtils.Result.SignerInfo();
signerInfo.index = signerIndex;
result.signers.add(signerInfo);
try {
ByteBuffer signer = ApkSigningBlockUtils.getLengthPrefixedSlice(signers);
parseSigner(
signer,
certFactory,
signerInfo,
contentDigestsToVerify,
supportedApkSigSchemeNames,
foundApkSigSchemeIds,
minSdkVersion,
maxSdkVersion);
} catch (ApkFormatException | BufferUnderflowException e) {
signerInfo.addError(Issue.V2_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,
CertificateFactory certFactory,
ApkSigningBlockUtils.Result.SignerInfo result,
Set<ContentDigestAlgorithm> contentDigestsToVerify,
Map<Integer, String> supportedApkSigSchemeNames,
Set<Integer> foundApkSigSchemeIds,
int minSdkVersion,
int maxSdkVersion) throws ApkFormatException, NoSuchAlgorithmException {
ByteBuffer signedData = ApkSigningBlockUtils.getLengthPrefixedSlice(signerBlock);
byte[] signedDataBytes = new byte[signedData.remaining()];
signedData.get(signedDataBytes);
signedData.flip();
result.signedData = signedDataBytes;
ByteBuffer signatures = ApkSigningBlockUtils.getLengthPrefixedSlice(signerBlock);
byte[] publicKeyBytes = ApkSigningBlockUtils.readLengthPrefixedByteArray(signerBlock);
// Parse the signatures block and identify supported signatures
int signatureCount = 0;
List<ApkSigningBlockUtils.SupportedSignature> supportedSignatures = new ArrayList<>(1);
while (signatures.hasRemaining()) {
signatureCount++;
try {
ByteBuffer signature = ApkSigningBlockUtils.getLengthPrefixedSlice(signatures);
int sigAlgorithmId = signature.getInt();
byte[] sigBytes = ApkSigningBlockUtils.readLengthPrefixedByteArray(signature);
result.signatures.add(
new ApkSigningBlockUtils.Result.SignerInfo.Signature(
sigAlgorithmId, sigBytes));
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.findById(sigAlgorithmId);
if (signatureAlgorithm == null) {
result.addWarning(Issue.V2_SIG_UNKNOWN_SIG_ALGORITHM, sigAlgorithmId);
continue;
}
supportedSignatures.add(
new ApkSigningBlockUtils.SupportedSignature(signatureAlgorithm, sigBytes));
} catch (ApkFormatException | BufferUnderflowException e) {
result.addError(Issue.V2_SIG_MALFORMED_SIGNATURE, signatureCount);
return;
}
}
if (result.signatures.isEmpty()) {
result.addError(Issue.V2_SIG_NO_SIGNATURES);
return;
}
// Verify signatures over signed-data block using the public key
List<ApkSigningBlockUtils.SupportedSignature> signaturesToVerify = null;
try {
signaturesToVerify =
ApkSigningBlockUtils.getSignaturesToVerify(
supportedSignatures, minSdkVersion, maxSdkVersion);
} catch (ApkSigningBlockUtils.NoSupportedSignaturesException e) {
result.addError(Issue.V2_SIG_NO_SUPPORTED_SIGNATURES, e);
return;
}
for (ApkSigningBlockUtils.SupportedSignature signature : signaturesToVerify) {
SignatureAlgorithm signatureAlgorithm = signature.algorithm;
String jcaSignatureAlgorithm =
signatureAlgorithm.getJcaSignatureAlgorithmAndParams().getFirst();
AlgorithmParameterSpec jcaSignatureAlgorithmParams =
signatureAlgorithm.getJcaSignatureAlgorithmAndParams().getSecond();
String keyAlgorithm = signatureAlgorithm.getJcaKeyAlgorithm();
PublicKey publicKey;
try {
publicKey =
KeyFactory.getInstance(keyAlgorithm).generatePublic(
new X509EncodedKeySpec(publicKeyBytes));
} catch (Exception e) {
result.addError(Issue.V2_SIG_MALFORMED_PUBLIC_KEY, e);
return;
}
try {
Signature sig = Signature.getInstance(jcaSignatureAlgorithm);
sig.initVerify(publicKey);
if (jcaSignatureAlgorithmParams != null) {
sig.setParameter(jcaSignatureAlgorithmParams);
}
signedData.position(0);
sig.update(signedData);
byte[] sigBytes = signature.signature;
if (!sig.verify(sigBytes)) {
result.addError(Issue.V2_SIG_DID_NOT_VERIFY, signatureAlgorithm);
return;
}
result.verifiedSignatures.put(signatureAlgorithm, sigBytes);
contentDigestsToVerify.add(signatureAlgorithm.getContentDigestAlgorithm());
} catch (InvalidKeyException | InvalidAlgorithmParameterException
| SignatureException e) {
result.addError(Issue.V2_SIG_VERIFY_EXCEPTION, signatureAlgorithm, e);
return;
}
}
// At least one signature over signedData has verified. We can now parse signed-data.
signedData.position(0);
ByteBuffer digests = ApkSigningBlockUtils.getLengthPrefixedSlice(signedData);
ByteBuffer certificates = ApkSigningBlockUtils.getLengthPrefixedSlice(signedData);
ByteBuffer additionalAttributes = ApkSigningBlockUtils.getLengthPrefixedSlice(signedData);
// Parse the certificates block
int certificateIndex = -1;
while (certificates.hasRemaining()) {
certificateIndex++;
byte[] encodedCert = ApkSigningBlockUtils.readLengthPrefixedByteArray(certificates);
X509Certificate certificate;
try {
certificate = X509CertificateUtils.generateCertificate(encodedCert, certFactory);
} catch (CertificateException e) {
result.addError(
Issue.V2_SIG_MALFORMED_CERTIFICATE,
certificateIndex,
certificateIndex + 1,
e);
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);
result.certs.add(certificate);
}
if (result.certs.isEmpty()) {
result.addError(Issue.V2_SIG_NO_CERTIFICATES);
return;
}
X509Certificate mainCertificate = result.certs.get(0);
byte[] certificatePublicKeyBytes;
try {
certificatePublicKeyBytes = ApkSigningBlockUtils.encodePublicKey(
mainCertificate.getPublicKey());
} catch (InvalidKeyException e) {
System.out.println("Caught an exception encoding the public key: " + e);
e.printStackTrace();
certificatePublicKeyBytes = mainCertificate.getPublicKey().getEncoded();
}
if (!Arrays.equals(publicKeyBytes, certificatePublicKeyBytes)) {
result.addError(
Issue.V2_SIG_PUBLIC_KEY_MISMATCH_BETWEEN_CERTIFICATE_AND_SIGNATURES_RECORD,
ApkSigningBlockUtils.toHex(certificatePublicKeyBytes),
ApkSigningBlockUtils.toHex(publicKeyBytes));
return;
}
// Parse the digests block
int digestCount = 0;
while (digests.hasRemaining()) {
digestCount++;
try {
ByteBuffer digest = ApkSigningBlockUtils.getLengthPrefixedSlice(digests);
int sigAlgorithmId = digest.getInt();
byte[] digestBytes = ApkSigningBlockUtils.readLengthPrefixedByteArray(digest);
result.contentDigests.add(
new ApkSigningBlockUtils.Result.SignerInfo.ContentDigest(
sigAlgorithmId, digestBytes));
} catch (ApkFormatException | BufferUnderflowException e) {
result.addError(Issue.V2_SIG_MALFORMED_DIGEST, digestCount);
return;
}
}
List<Integer> sigAlgsFromSignaturesRecord = new ArrayList<>(result.signatures.size());
for (ApkSigningBlockUtils.Result.SignerInfo.Signature signature : result.signatures) {
sigAlgsFromSignaturesRecord.add(signature.getAlgorithmId());
}
List<Integer> sigAlgsFromDigestsRecord = new ArrayList<>(result.contentDigests.size());
for (ApkSigningBlockUtils.Result.SignerInfo.ContentDigest digest : result.contentDigests) {
sigAlgsFromDigestsRecord.add(digest.getSignatureAlgorithmId());
}
if (!sigAlgsFromSignaturesRecord.equals(sigAlgsFromDigestsRecord)) {
result.addError(
Issue.V2_SIG_SIG_ALG_MISMATCH_BETWEEN_SIGNATURES_AND_DIGESTS_RECORDS,
sigAlgsFromSignaturesRecord,
sigAlgsFromDigestsRecord);
return;
}
// Parse the additional attributes block.
int additionalAttributeCount = 0;
Set<Integer> supportedApkSigSchemeIds = supportedApkSigSchemeNames.keySet();
Set<Integer> supportedExpectedApkSigSchemeIds = new HashSet<>(1);
while (additionalAttributes.hasRemaining()) {
additionalAttributeCount++;
try {
ByteBuffer attribute =
ApkSigningBlockUtils.getLengthPrefixedSlice(additionalAttributes);
int id = attribute.getInt();
byte[] value = ByteBufferUtils.toByteArray(attribute);
result.additionalAttributes.add(
new ApkSigningBlockUtils.Result.SignerInfo.AdditionalAttribute(id, value));
switch (id) {
case V2SchemeConstants.STRIPPING_PROTECTION_ATTR_ID:
// stripping protection added when signing with a newer scheme
int foundId = ByteBuffer.wrap(value).order(
ByteOrder.LITTLE_ENDIAN).getInt();
if (supportedApkSigSchemeIds.contains(foundId)) {
supportedExpectedApkSigSchemeIds.add(foundId);
} else {
result.addWarning(
Issue.V2_SIG_UNKNOWN_APK_SIG_SCHEME_ID, result.index, foundId);
}
break;
default:
result.addWarning(Issue.V2_SIG_UNKNOWN_ADDITIONAL_ATTRIBUTE, id);
}
} catch (ApkFormatException | BufferUnderflowException e) {
result.addError(
Issue.V2_SIG_MALFORMED_ADDITIONAL_ATTRIBUTE, additionalAttributeCount);
return;
}
}
// make sure that all known IDs indicated in stripping protection have already verified
for (int id : supportedExpectedApkSigSchemeIds) {
if (!foundApkSigSchemeIds.contains(id)) {
String apkSigSchemeName = supportedApkSigSchemeNames.get(id);
result.addError(
Issue.V2_SIG_MISSING_APK_SIG_REFERENCED,
result.index,
apkSigSchemeName);
}
}
}
}

View File

@ -1,25 +0,0 @@
/*
* 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.v3;
/** Constants used by the V3 Signature Scheme signing and verification. */
public class V3SchemeConstants {
private V3SchemeConstants() {}
public static final int APK_SIGNATURE_SCHEME_V3_BLOCK_ID = 0xf05368c0;
public static final int PROOF_OF_ROTATION_ATTR_ID = 0x3ba06f8c;
}

View File

@ -1,332 +0,0 @@
/*
* 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.v3;
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 static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeCertificates;
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodePublicKey;
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.apk.SignatureAlgorithm;
import com.android.apksig.internal.util.Pair;
import com.android.apksig.util.DataSource;
import com.android.apksig.util.RunnablesExecutor;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.SignatureException;
import java.security.cert.CertificateEncodingException;
import java.security.interfaces.ECKey;
import java.security.interfaces.RSAKey;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
/**
* APK Signature Scheme v3 signer.
*
* <p>APK Signature Scheme v3 builds upon APK Signature Scheme v3, and maintains all of the APK
* Signature Scheme v2 goals.
*
* @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature Scheme v2</a>
* <p>The main contribution of APK Signature Scheme v3 is the introduction of the {@link
* SigningCertificateLineage}, which enables an APK to change its signing certificate as long as
* it can prove the new siging certificate was signed by the old.
*/
public abstract class V3SchemeSigner {
public static final int APK_SIGNATURE_SCHEME_V3_BLOCK_ID =
V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID;
public static final int PROOF_OF_ROTATION_ATTR_ID = V3SchemeConstants.PROOF_OF_ROTATION_ATTR_ID;
/** Hidden constructor to prevent instantiation. */
private V3SchemeSigner() {}
/**
* Gets the APK Signature Scheme v3 signature algorithms 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 APK
* Signature Scheme v3
*/
public static List<SignatureAlgorithm> getSuggestedSignatureAlgorithms(PublicKey signingKey,
int minSdkVersion, boolean verityEnabled, boolean deterministicDsaSigning)
throws InvalidKeyException {
String keyAlgorithm = signingKey.getAlgorithm();
if ("RSA".equalsIgnoreCase(keyAlgorithm)) {
// Use RSASSA-PKCS1-v1_5 signature scheme instead of RSASSA-PSS to guarantee
// deterministic signatures which make life easier for OTA updates (fewer files
// changed when deterministic signature schemes are used).
// Pick a digest which is no weaker than the key.
int modulusLengthBits = ((RSAKey) signingKey).getModulus().bitLength();
if (modulusLengthBits <= 3072) {
// 3072-bit RSA is roughly 128-bit strong, meaning SHA-256 is a good fit.
List<SignatureAlgorithm> algorithms = new ArrayList<>();
algorithms.add(SignatureAlgorithm.RSA_PKCS1_V1_5_WITH_SHA256);
if (verityEnabled) {
algorithms.add(SignatureAlgorithm.VERITY_RSA_PKCS1_V1_5_WITH_SHA256);
}
return algorithms;
} else {
// Keys longer than 3072 bit need to be paired with a stronger digest to avoid the
// digest being the weak link. SHA-512 is the next strongest supported digest.
return Collections.singletonList(SignatureAlgorithm.RSA_PKCS1_V1_5_WITH_SHA512);
}
} else if ("DSA".equalsIgnoreCase(keyAlgorithm)) {
// DSA is supported only with SHA-256.
List<SignatureAlgorithm> algorithms = new ArrayList<>();
algorithms.add(
deterministicDsaSigning ?
SignatureAlgorithm.DETDSA_WITH_SHA256 :
SignatureAlgorithm.DSA_WITH_SHA256);
if (verityEnabled) {
algorithms.add(SignatureAlgorithm.VERITY_DSA_WITH_SHA256);
}
return algorithms;
} else if ("EC".equalsIgnoreCase(keyAlgorithm)) {
// Pick a digest which is no weaker than the key.
int keySizeBits = ((ECKey) signingKey).getParams().getOrder().bitLength();
if (keySizeBits <= 256) {
// 256-bit Elliptic Curve is roughly 128-bit strong, meaning SHA-256 is a good fit.
List<SignatureAlgorithm> algorithms = new ArrayList<>();
algorithms.add(SignatureAlgorithm.ECDSA_WITH_SHA256);
if (verityEnabled) {
algorithms.add(SignatureAlgorithm.VERITY_ECDSA_WITH_SHA256);
}
return algorithms;
} else {
// Keys longer than 256 bit need to be paired with a stronger digest to avoid the
// digest being the weak link. SHA-512 is the next strongest supported digest.
return Collections.singletonList(SignatureAlgorithm.ECDSA_WITH_SHA512);
}
} else {
throw new InvalidKeyException("Unsupported key algorithm: " + keyAlgorithm);
}
}
public static ApkSigningBlockUtils.SigningSchemeBlockAndDigests
generateApkSignatureSchemeV3Block(
RunnablesExecutor executor,
DataSource beforeCentralDir,
DataSource centralDir,
DataSource eocd,
List<SignerConfig> signerConfigs)
throws IOException, InvalidKeyException, NoSuchAlgorithmException,
SignatureException {
Pair<List<SignerConfig>, Map<ContentDigestAlgorithm, byte[]>> digestInfo =
ApkSigningBlockUtils.computeContentDigests(
executor, beforeCentralDir, centralDir, eocd, signerConfigs);
return new ApkSigningBlockUtils.SigningSchemeBlockAndDigests(
generateApkSignatureSchemeV3Block(digestInfo.getFirst(), digestInfo.getSecond()),
digestInfo.getSecond());
}
public static byte[] generateV3SignerAttribute(
SigningCertificateLineage signingCertificateLineage) {
// FORMAT (little endian):
// * length-prefixed bytes: attribute pair
// * uint32: ID
// * bytes: value - encoded V3 SigningCertificateLineage
byte[] encodedLineage = signingCertificateLineage.encodeSigningCertificateLineage();
int payloadSize = 4 + 4 + encodedLineage.length;
ByteBuffer result = ByteBuffer.allocate(payloadSize);
result.order(ByteOrder.LITTLE_ENDIAN);
result.putInt(4 + encodedLineage.length);
result.putInt(V3SchemeConstants.PROOF_OF_ROTATION_ATTR_ID);
result.put(encodedLineage);
return result.array();
}
private static Pair<byte[], Integer> generateApkSignatureSchemeV3Block(
List<SignerConfig> signerConfigs, Map<ContentDigestAlgorithm, byte[]> contentDigests)
throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
// FORMAT:
// * length-prefixed sequence of length-prefixed signer blocks.
List<byte[]> signerBlocks = new ArrayList<>(signerConfigs.size());
int signerNumber = 0;
for (SignerConfig signerConfig : signerConfigs) {
signerNumber++;
byte[] signerBlock;
try {
signerBlock = generateSignerBlock(signerConfig, contentDigests);
} catch (InvalidKeyException e) {
throw new InvalidKeyException("Signer #" + signerNumber + " failed", e);
} catch (SignatureException e) {
throw new SignatureException("Signer #" + signerNumber + " failed", e);
}
signerBlocks.add(signerBlock);
}
return Pair.of(
encodeAsSequenceOfLengthPrefixedElements(
new byte[][] {
encodeAsSequenceOfLengthPrefixedElements(signerBlocks),
}),
V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID);
}
private static byte[] generateSignerBlock(
SignerConfig signerConfig, Map<ContentDigestAlgorithm, byte[]> contentDigests)
throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
if (signerConfig.certificates.isEmpty()) {
throw new SignatureException("No certificates configured for signer");
}
PublicKey publicKey = signerConfig.certificates.get(0).getPublicKey();
byte[] encodedPublicKey = encodePublicKey(publicKey);
V3SignatureSchemeBlock.SignedData signedData = new V3SignatureSchemeBlock.SignedData();
try {
signedData.certificates = encodeCertificates(signerConfig.certificates);
} catch (CertificateEncodingException e) {
throw new SignatureException("Failed to encode certificates", e);
}
List<Pair<Integer, byte[]>> digests =
new ArrayList<>(signerConfig.signatureAlgorithms.size());
for (SignatureAlgorithm signatureAlgorithm : signerConfig.signatureAlgorithms) {
ContentDigestAlgorithm contentDigestAlgorithm =
signatureAlgorithm.getContentDigestAlgorithm();
byte[] contentDigest = contentDigests.get(contentDigestAlgorithm);
if (contentDigest == null) {
throw new RuntimeException(
contentDigestAlgorithm
+ " content digest for "
+ signatureAlgorithm
+ " not computed");
}
digests.add(Pair.of(signatureAlgorithm.getId(), contentDigest));
}
signedData.digests = digests;
signedData.minSdkVersion = signerConfig.minSdkVersion;
signedData.maxSdkVersion = signerConfig.maxSdkVersion;
signedData.additionalAttributes = generateAdditionalAttributes(signerConfig);
V3SignatureSchemeBlock.Signer signer = new V3SignatureSchemeBlock.Signer();
signer.signedData = encodeSignedData(signedData);
signer.minSdkVersion = signerConfig.minSdkVersion;
signer.maxSdkVersion = signerConfig.maxSdkVersion;
signer.publicKey = encodedPublicKey;
signer.signatures =
ApkSigningBlockUtils.generateSignaturesOverData(signerConfig, signer.signedData);
return encodeSigner(signer);
}
private static byte[] encodeSigner(V3SignatureSchemeBlock.Signer signer) {
byte[] signedData = encodeAsLengthPrefixedElement(signer.signedData);
byte[] signatures =
encodeAsLengthPrefixedElement(
encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
signer.signatures));
byte[] publicKey = encodeAsLengthPrefixedElement(signer.publicKey);
// FORMAT:
// * length-prefixed signed data
// * uint32: minSdkVersion
// * uint32: maxSdkVersion
// * length-prefixed sequence of length-prefixed signatures:
// * uint32: signature algorithm ID
// * length-prefixed bytes: signature of signed data
// * length-prefixed bytes: public key (X.509 SubjectPublicKeyInfo, ASN.1 DER encoded)
int payloadSize = signedData.length + 4 + 4 + signatures.length + publicKey.length;
ByteBuffer result = ByteBuffer.allocate(payloadSize);
result.order(ByteOrder.LITTLE_ENDIAN);
result.put(signedData);
result.putInt(signer.minSdkVersion);
result.putInt(signer.maxSdkVersion);
result.put(signatures);
result.put(publicKey);
return result.array();
}
private static byte[] encodeSignedData(V3SignatureSchemeBlock.SignedData signedData) {
byte[] digests =
encodeAsLengthPrefixedElement(
encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
signedData.digests));
byte[] certs =
encodeAsLengthPrefixedElement(
encodeAsSequenceOfLengthPrefixedElements(signedData.certificates));
byte[] attributes = encodeAsLengthPrefixedElement(signedData.additionalAttributes);
// FORMAT:
// * 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).
// * uint-32: minSdkVersion
// * uint-32: maxSdkVersion
// * length-prefixed sequence of length-prefixed additional attributes:
// * uint32: ID
// * (length - 4) bytes: value
// * uint32: Proof-of-rotation ID: 0x3ba06f8c
// * length-prefixed roof-of-rotation structure
int payloadSize = digests.length + certs.length + 4 + 4 + attributes.length;
ByteBuffer result = ByteBuffer.allocate(payloadSize);
result.order(ByteOrder.LITTLE_ENDIAN);
result.put(digests);
result.put(certs);
result.putInt(signedData.minSdkVersion);
result.putInt(signedData.maxSdkVersion);
result.put(attributes);
return result.array();
}
private static byte[] generateAdditionalAttributes(SignerConfig signerConfig) {
if (signerConfig.mSigningCertificateLineage == null) {
return new byte[0];
}
return generateV3SignerAttribute(signerConfig.mSigningCertificateLineage);
}
private static final class V3SignatureSchemeBlock {
private static final class Signer {
public byte[] signedData;
public int minSdkVersion;
public int maxSdkVersion;
public List<Pair<Integer, byte[]>> signatures;
public byte[] publicKey;
}
private static final class SignedData {
public List<Pair<Integer, byte[]>> digests;
public List<byte[]> certificates;
public int minSdkVersion;
public int maxSdkVersion;
public byte[] additionalAttributes;
}
}
}

View File

@ -1,522 +0,0 @@
/*
* 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.v3;
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.getLengthPrefixedSlice;
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.readLengthPrefixedByteArray;
import com.android.apksig.ApkVerifier.Issue;
import com.android.apksig.SigningCertificateLineage;
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.ApkSigningBlockUtils.SignatureNotFoundException;
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.util.AndroidSdkVersion;
import com.android.apksig.internal.util.ByteBufferUtils;
import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
import com.android.apksig.internal.util.X509CertificateUtils;
import com.android.apksig.util.DataSource;
import com.android.apksig.util.RunnablesExecutor;
import java.io.IOException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
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.security.spec.X509EncodedKeySpec;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
/**
* APK Signature Scheme v3 verifier.
*
* <p>APK Signature Scheme v3, like v2 is a whole-file signature scheme which aims to protect every
* single bit of the APK, as opposed to the JAR Signature Scheme which protects only the names and
* uncompressed contents of ZIP entries.
*
* @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature Scheme v2</a>
*/
public abstract class V3SchemeVerifier {
/** Hidden constructor to prevent instantiation. */
private V3SchemeVerifier() {}
/**
* Verifies the provided APK's APK Signature Scheme v3 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()}.
*
* <p>Verification succeeds iff the APK's APK Signature Scheme v3 signatures are expected to
* verify on all Android platform versions in the {@code [minSdkVersion, maxSdkVersion]} range.
* If the APK's signature is expected to not verify on any of the specified platform versions,
* this method returns a result with one or more errors and whose
* {@code Result.verified == false}, or this method throws an exception.
*
* @throws ApkFormatException if the APK is malformed
* @throws NoSuchAlgorithmException if the APK's signatures cannot be verified because a
* required cryptographic algorithm implementation is missing
* @throws SignatureNotFoundException if no APK Signature Scheme v3
* signatures are found
* @throws IOException if an I/O error occurs when reading the APK
*/
public static ApkSigningBlockUtils.Result verify(
RunnablesExecutor executor,
DataSource apk,
ApkUtils.ZipSections zipSections,
int minSdkVersion,
int maxSdkVersion)
throws IOException, NoSuchAlgorithmException, SignatureNotFoundException {
ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result(
ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3);
SignatureInfo signatureInfo =
ApkSigningBlockUtils.findSignature(apk, zipSections,
V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID, result);
DataSource beforeApkSigningBlock = apk.slice(0, signatureInfo.apkSigningBlockOffset);
DataSource centralDir =
apk.slice(
signatureInfo.centralDirOffset,
signatureInfo.eocdOffset - signatureInfo.centralDirOffset);
ByteBuffer eocd = signatureInfo.eocd;
// v3 didn't exist prior to P, so make sure that we're only judging v3 on its supported
// platforms
if (minSdkVersion < AndroidSdkVersion.P) {
minSdkVersion = AndroidSdkVersion.P;
}
verify(executor,
beforeApkSigningBlock,
signatureInfo.signatureBlock,
centralDir,
eocd,
minSdkVersion,
maxSdkVersion,
result);
return result;
}
/**
* Verifies the provided APK's v3 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(RunnablesExecutor, DataSource, ApkUtils.ZipSections, int,
* int)} for more information about the contract of this method.
*
* @param result result populated by this method with interesting information about the APK,
* such as information about signers, and verification errors and warnings.
*/
private static void verify(
RunnablesExecutor executor,
DataSource beforeApkSigningBlock,
ByteBuffer apkSignatureSchemeV3Block,
DataSource centralDir,
ByteBuffer eocd,
int minSdkVersion,
int maxSdkVersion,
ApkSigningBlockUtils.Result result)
throws IOException, NoSuchAlgorithmException {
Set<ContentDigestAlgorithm> contentDigestsToVerify = new HashSet<>(1);
parseSigners(apkSignatureSchemeV3Block, contentDigestsToVerify, result);
if (result.containsErrors()) {
return;
}
ApkSigningBlockUtils.verifyIntegrity(
executor, beforeApkSigningBlock, centralDir, eocd, contentDigestsToVerify, result);
// make sure that the v3 signers cover the entire targeted sdk version ranges and that the
// longest SigningCertificateHistory, if present, corresponds to the newest platform
// versions
SortedMap<Integer, ApkSigningBlockUtils.Result.SignerInfo> sortedSigners = new TreeMap<>();
for (ApkSigningBlockUtils.Result.SignerInfo signer : result.signers) {
sortedSigners.put(signer.minSdkVersion, signer);
}
// first make sure there is neither overlap nor holes
int firstMin = 0;
int lastMax = 0;
int lastLineageSize = 0;
// while we're iterating through the signers, build up the list of lineages
List<SigningCertificateLineage> lineages = new ArrayList<>(result.signers.size());
for (ApkSigningBlockUtils.Result.SignerInfo signer : sortedSigners.values()) {
int currentMin = signer.minSdkVersion;
int currentMax = signer.maxSdkVersion;
if (firstMin == 0) {
// first round sets up our basis
firstMin = currentMin;
} else {
if (currentMin != lastMax + 1) {
result.addError(Issue.V3_INCONSISTENT_SDK_VERSIONS);
break;
}
}
lastMax = currentMax;
// also, while we're here, make sure that the lineage sizes only increase
if (signer.signingCertificateLineage != null) {
int currLineageSize = signer.signingCertificateLineage.size();
if (currLineageSize < lastLineageSize) {
result.addError(Issue.V3_INCONSISTENT_LINEAGES);
break;
}
lastLineageSize = currLineageSize;
lineages.add(signer.signingCertificateLineage);
}
}
// make sure we support our desired sdk ranges
if (firstMin > minSdkVersion || lastMax < maxSdkVersion) {
result.addError(Issue.V3_MISSING_SDK_VERSIONS, firstMin, lastMax);
}
try {
result.signingCertificateLineage =
SigningCertificateLineage.consolidateLineages(lineages);
} catch (IllegalArgumentException e) {
result.addError(Issue.V3_INCONSISTENT_LINEAGES);
}
if (!result.containsErrors()) {
result.verified = true;
}
}
/**
* Parses each signer in the provided APK Signature Scheme v3 block and populates corresponding
* {@code signerInfos} of the provided {@code result}.
*
* <p>This verifies signatures over {@code signed-data} block contained in each signer block.
* However, this does not verify the integrity of the rest of the APK but rather simply reports
* the expected digests of the rest of the APK (see {@code contentDigestsToVerify}).
*
* <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 apkSignatureSchemeV3Block,
Set<ContentDigestAlgorithm> contentDigestsToVerify,
ApkSigningBlockUtils.Result result) throws NoSuchAlgorithmException {
ByteBuffer signers;
try {
signers = getLengthPrefixedSlice(apkSignatureSchemeV3Block);
} catch (ApkFormatException e) {
result.addError(Issue.V3_SIG_MALFORMED_SIGNERS);
return;
}
if (!signers.hasRemaining()) {
result.addError(Issue.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);
}
int signerCount = 0;
while (signers.hasRemaining()) {
int signerIndex = signerCount;
signerCount++;
ApkSigningBlockUtils.Result.SignerInfo signerInfo =
new ApkSigningBlockUtils.Result.SignerInfo();
signerInfo.index = signerIndex;
result.signers.add(signerInfo);
try {
ByteBuffer signer = getLengthPrefixedSlice(signers);
parseSigner(signer, certFactory, signerInfo, contentDigestsToVerify);
} catch (ApkFormatException | BufferUnderflowException e) {
signerInfo.addError(Issue.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, as well as
* the data contained therein, 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,
CertificateFactory certFactory,
ApkSigningBlockUtils.Result.SignerInfo result,
Set<ContentDigestAlgorithm> contentDigestsToVerify)
throws ApkFormatException, NoSuchAlgorithmException {
ByteBuffer signedData = getLengthPrefixedSlice(signerBlock);
byte[] signedDataBytes = new byte[signedData.remaining()];
signedData.get(signedDataBytes);
signedData.flip();
result.signedData = signedDataBytes;
int parsedMinSdkVersion = signerBlock.getInt();
int parsedMaxSdkVersion = signerBlock.getInt();
result.minSdkVersion = parsedMinSdkVersion;
result.maxSdkVersion = parsedMaxSdkVersion;
if (parsedMinSdkVersion < 0 || parsedMinSdkVersion > parsedMaxSdkVersion) {
result.addError(
Issue.V3_SIG_INVALID_SDK_VERSIONS, parsedMinSdkVersion, parsedMaxSdkVersion);
}
ByteBuffer signatures = getLengthPrefixedSlice(signerBlock);
byte[] publicKeyBytes = readLengthPrefixedByteArray(signerBlock);
// Parse the signatures block and identify supported signatures
int signatureCount = 0;
List<ApkSigningBlockUtils.SupportedSignature> supportedSignatures = new ArrayList<>(1);
while (signatures.hasRemaining()) {
signatureCount++;
try {
ByteBuffer signature = getLengthPrefixedSlice(signatures);
int sigAlgorithmId = signature.getInt();
byte[] sigBytes = readLengthPrefixedByteArray(signature);
result.signatures.add(
new ApkSigningBlockUtils.Result.SignerInfo.Signature(
sigAlgorithmId, sigBytes));
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.findById(sigAlgorithmId);
if (signatureAlgorithm == null) {
result.addWarning(Issue.V3_SIG_UNKNOWN_SIG_ALGORITHM, sigAlgorithmId);
continue;
}
// TODO consider dropping deprecated signatures for v3 or modifying
// getSignaturesToVerify (called below)
supportedSignatures.add(
new ApkSigningBlockUtils.SupportedSignature(signatureAlgorithm, sigBytes));
} catch (ApkFormatException | BufferUnderflowException e) {
result.addError(Issue.V3_SIG_MALFORMED_SIGNATURE, signatureCount);
return;
}
}
if (result.signatures.isEmpty()) {
result.addError(Issue.V3_SIG_NO_SIGNATURES);
return;
}
// Verify signatures over signed-data block using the public key
List<ApkSigningBlockUtils.SupportedSignature> signaturesToVerify = null;
try {
signaturesToVerify =
ApkSigningBlockUtils.getSignaturesToVerify(
supportedSignatures, result.minSdkVersion, result.maxSdkVersion);
} catch (ApkSigningBlockUtils.NoSupportedSignaturesException e) {
result.addError(Issue.V3_SIG_NO_SUPPORTED_SIGNATURES);
return;
}
for (ApkSigningBlockUtils.SupportedSignature signature : signaturesToVerify) {
SignatureAlgorithm signatureAlgorithm = signature.algorithm;
String jcaSignatureAlgorithm =
signatureAlgorithm.getJcaSignatureAlgorithmAndParams().getFirst();
AlgorithmParameterSpec jcaSignatureAlgorithmParams =
signatureAlgorithm.getJcaSignatureAlgorithmAndParams().getSecond();
String keyAlgorithm = signatureAlgorithm.getJcaKeyAlgorithm();
PublicKey publicKey;
try {
publicKey =
KeyFactory.getInstance(keyAlgorithm).generatePublic(
new X509EncodedKeySpec(publicKeyBytes));
} catch (Exception e) {
result.addError(Issue.V3_SIG_MALFORMED_PUBLIC_KEY, e);
return;
}
try {
Signature sig = Signature.getInstance(jcaSignatureAlgorithm);
sig.initVerify(publicKey);
if (jcaSignatureAlgorithmParams != null) {
sig.setParameter(jcaSignatureAlgorithmParams);
}
signedData.position(0);
sig.update(signedData);
byte[] sigBytes = signature.signature;
if (!sig.verify(sigBytes)) {
result.addError(Issue.V3_SIG_DID_NOT_VERIFY, signatureAlgorithm);
return;
}
result.verifiedSignatures.put(signatureAlgorithm, sigBytes);
contentDigestsToVerify.add(signatureAlgorithm.getContentDigestAlgorithm());
} catch (InvalidKeyException | InvalidAlgorithmParameterException
| SignatureException e) {
result.addError(Issue.V3_SIG_VERIFY_EXCEPTION, signatureAlgorithm, e);
return;
}
}
// At least one signature over signedData has verified. We can now parse signed-data.
signedData.position(0);
ByteBuffer digests = getLengthPrefixedSlice(signedData);
ByteBuffer certificates = getLengthPrefixedSlice(signedData);
int signedMinSdkVersion = signedData.getInt();
if (signedMinSdkVersion != parsedMinSdkVersion) {
result.addError(
Issue.V3_MIN_SDK_VERSION_MISMATCH_BETWEEN_SIGNER_AND_SIGNED_DATA_RECORD,
parsedMinSdkVersion,
signedMinSdkVersion);
}
int signedMaxSdkVersion = signedData.getInt();
if (signedMaxSdkVersion != parsedMaxSdkVersion) {
result.addError(
Issue.V3_MAX_SDK_VERSION_MISMATCH_BETWEEN_SIGNER_AND_SIGNED_DATA_RECORD,
parsedMaxSdkVersion,
signedMaxSdkVersion);
}
ByteBuffer additionalAttributes = getLengthPrefixedSlice(signedData);
// Parse the certificates block
int certificateIndex = -1;
while (certificates.hasRemaining()) {
certificateIndex++;
byte[] encodedCert = readLengthPrefixedByteArray(certificates);
X509Certificate certificate;
try {
certificate = X509CertificateUtils.generateCertificate(encodedCert, certFactory);
} catch (CertificateException e) {
result.addError(
Issue.V3_SIG_MALFORMED_CERTIFICATE,
certificateIndex,
certificateIndex + 1,
e);
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);
result.certs.add(certificate);
}
if (result.certs.isEmpty()) {
result.addError(Issue.V3_SIG_NO_CERTIFICATES);
return;
}
X509Certificate mainCertificate = result.certs.get(0);
byte[] certificatePublicKeyBytes;
try {
certificatePublicKeyBytes = ApkSigningBlockUtils.encodePublicKey(mainCertificate.getPublicKey());
} catch (InvalidKeyException e) {
System.out.println("Caught an exception encoding the public key: " + e);
e.printStackTrace();
certificatePublicKeyBytes = mainCertificate.getPublicKey().getEncoded();
}
if (!Arrays.equals(publicKeyBytes, certificatePublicKeyBytes)) {
result.addError(
Issue.V3_SIG_PUBLIC_KEY_MISMATCH_BETWEEN_CERTIFICATE_AND_SIGNATURES_RECORD,
ApkSigningBlockUtils.toHex(certificatePublicKeyBytes),
ApkSigningBlockUtils.toHex(publicKeyBytes));
return;
}
// Parse the digests block
int digestCount = 0;
while (digests.hasRemaining()) {
digestCount++;
try {
ByteBuffer digest = getLengthPrefixedSlice(digests);
int sigAlgorithmId = digest.getInt();
byte[] digestBytes = readLengthPrefixedByteArray(digest);
result.contentDigests.add(
new ApkSigningBlockUtils.Result.SignerInfo.ContentDigest(
sigAlgorithmId, digestBytes));
} catch (ApkFormatException | BufferUnderflowException e) {
result.addError(Issue.V3_SIG_MALFORMED_DIGEST, digestCount);
return;
}
}
List<Integer> sigAlgsFromSignaturesRecord = new ArrayList<>(result.signatures.size());
for (ApkSigningBlockUtils.Result.SignerInfo.Signature signature : result.signatures) {
sigAlgsFromSignaturesRecord.add(signature.getAlgorithmId());
}
List<Integer> sigAlgsFromDigestsRecord = new ArrayList<>(result.contentDigests.size());
for (ApkSigningBlockUtils.Result.SignerInfo.ContentDigest digest : result.contentDigests) {
sigAlgsFromDigestsRecord.add(digest.getSignatureAlgorithmId());
}
if (!sigAlgsFromSignaturesRecord.equals(sigAlgsFromDigestsRecord)) {
result.addError(
Issue.V3_SIG_SIG_ALG_MISMATCH_BETWEEN_SIGNATURES_AND_DIGESTS_RECORDS,
sigAlgsFromSignaturesRecord,
sigAlgsFromDigestsRecord);
return;
}
// Parse the additional attributes block.
int additionalAttributeCount = 0;
while (additionalAttributes.hasRemaining()) {
additionalAttributeCount++;
try {
ByteBuffer attribute =
getLengthPrefixedSlice(additionalAttributes);
int id = attribute.getInt();
byte[] value = ByteBufferUtils.toByteArray(attribute);
result.additionalAttributes.add(
new ApkSigningBlockUtils.Result.SignerInfo.AdditionalAttribute(id, value));
if (id == V3SchemeConstants.PROOF_OF_ROTATION_ATTR_ID) {
try {
// SigningCertificateLineage is verified when built
result.signingCertificateLineage =
SigningCertificateLineage.readFromV3AttributeValue(value);
// make sure that the last cert in the chain matches this signer cert
SigningCertificateLineage subLineage =
result.signingCertificateLineage.getSubLineage(result.certs.get(0));
if (result.signingCertificateLineage.size() != subLineage.size()) {
result.addError(Issue.V3_SIG_POR_CERT_MISMATCH);
}
} catch (SecurityException e) {
result.addError(Issue.V3_SIG_POR_DID_NOT_VERIFY);
} catch (IllegalArgumentException e) {
result.addError(Issue.V3_SIG_POR_CERT_MISMATCH);
} catch (Exception e) {
result.addError(Issue.V3_SIG_MALFORMED_LINEAGE);
}
} else {
result.addWarning(Issue.V3_SIG_UNKNOWN_ADDITIONAL_ATTRIBUTE, id);
}
} catch (ApkFormatException | BufferUnderflowException e) {
result.addError(
Issue.V3_SIG_MALFORMED_ADDITIONAL_ATTRIBUTE, additionalAttributeCount);
return;
}
}
}
}

View File

@ -1,314 +0,0 @@
/*
* 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.v3;
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.getLengthPrefixedSlice;
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.readLengthPrefixedByteArray;
import com.android.apksig.apk.ApkFormatException;
import com.android.apksig.internal.apk.ApkSigningBlockUtils;
import com.android.apksig.internal.apk.SignatureAlgorithm;
import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
import com.android.apksig.internal.util.X509CertificateUtils;
import java.io.IOException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
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.CertificateEncodingException;
import java.security.cert.CertificateException;
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;
import java.util.Objects;
/**
* APK Signer Lineage.
*
* <p>The signer lineage contains a history of signing certificates with each ancestor attesting to
* the validity of its descendant. Each additional descendant represents a new identity that can be
* used to sign an APK, and each generation has accompanying attributes which represent how the
* APK would like to view the older signing certificates, specifically how they should be trusted in
* certain situations.
*
* <p> Its primary use is to enable APK Signing Certificate Rotation. The Android platform verifies
* the APK Signer Lineage, and if the current signing certificate for the APK is in the Signer
* Lineage, and the Lineage contains the certificate the platform associates with the APK, it will
* allow upgrades to the new certificate.
*
* @see <a href="https://source.android.com/security/apksigning/index.html">Application Signing</a>
*/
public class V3SigningCertificateLineage {
private final static int FIRST_VERSION = 1;
private final static int CURRENT_VERSION = FIRST_VERSION;
/**
* Deserializes the binary representation of an {@link V3SigningCertificateLineage}. 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;
}
ApkSigningBlockUtils.checkByteOrderLittleEndian(inputBytes);
// 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"
+ " V3SigningCertificateLineage 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 V3SigningCertificateLineage object");
}
lastCert = X509CertificateUtils.generateCertificate(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 V3SigningCertificateLineage object", e);
} catch(NoSuchAlgorithmException | InvalidKeyException
| InvalidAlgorithmParameterException | SignatureException e){
throw new SecurityException(
"Failed to verify signature over signed data for certificate #" + nodeCount
+ " when parsing V3SigningCertificateLineage object", e);
} catch(CertificateException e){
throw new SecurityException("Failed to decode certificate #" + nodeCount
+ " when parsing V3SigningCertificateLineage object", e);
}
return result;
}
/**
* encode the in-memory representation of this {@code V3SigningCertificateLineage}
*/
public static byte[] encodeSigningCertificateLineage(
List<SigningCertificateNode> signingCertificateLineage) {
// FORMAT (little endian):
// * 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)
List<byte[]> nodes = new ArrayList<>();
for (SigningCertificateNode node : signingCertificateLineage) {
nodes.add(encodeSigningCertificateNode(node));
}
byte [] encodedSigningCertificateLineage = encodeAsSequenceOfLengthPrefixedElements(nodes);
// add the version code (uint32) on top of the encoded nodes
int payloadSize = 4 + encodedSigningCertificateLineage.length;
ByteBuffer encodedWithVersion = ByteBuffer.allocate(payloadSize);
encodedWithVersion.order(ByteOrder.LITTLE_ENDIAN);
encodedWithVersion.putInt(CURRENT_VERSION);
encodedWithVersion.put(encodedSigningCertificateLineage);
return encodedWithVersion.array();
}
public static byte[] encodeSigningCertificateNode(SigningCertificateNode node) {
// FORMAT (little endian):
// * 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 signed data
int parentSigAlgorithmId = 0;
if (node.parentSigAlgorithm != null) {
parentSigAlgorithmId = node.parentSigAlgorithm.getId();
}
int sigAlgorithmId = 0;
if (node.sigAlgorithm != null) {
sigAlgorithmId = node.sigAlgorithm.getId();
}
byte[] prefixedSignedData = encodeSignedData(node.signingCert, parentSigAlgorithmId);
byte[] prefixedSignature = encodeAsLengthPrefixedElement(node.signature);
int payloadSize = prefixedSignedData.length + 4 + 4 + prefixedSignature.length;
ByteBuffer result = ByteBuffer.allocate(payloadSize);
result.order(ByteOrder.LITTLE_ENDIAN);
result.put(prefixedSignedData);
result.putInt(node.flags);
result.putInt(sigAlgorithmId);
result.put(prefixedSignature);
return result.array();
}
public static byte[] encodeSignedData(X509Certificate certificate, int flags) {
try {
byte[] prefixedCertificate = encodeAsLengthPrefixedElement(certificate.getEncoded());
int payloadSize = 4 + prefixedCertificate.length;
ByteBuffer result = ByteBuffer.allocate(payloadSize);
result.order(ByteOrder.LITTLE_ENDIAN);
result.put(prefixedCertificate);
result.putInt(flags);
return encodeAsLengthPrefixedElement(result.array());
} catch (CertificateEncodingException e) {
throw new RuntimeException(
"Failed to encode V3SigningCertificateLineage certificate", e);
}
}
/**
* Represents one signing certificate in the {@link V3SigningCertificateLineage}, which
* generally means it is/was used at some point to sign the same APK of the others in the
* lineage.
*/
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() {
int result = Objects.hash(signingCert, parentSigAlgorithm, sigAlgorithm, flags);
result = 31 * result + Arrays.hashCode(signature);
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 the 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 the this nodeto 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

@ -1,357 +0,0 @@
/*
* 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.v4;
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeCertificates;
import static com.android.apksig.internal.apk.v2.V2SchemeConstants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID;
import static com.android.apksig.internal.apk.v3.V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID;
import com.android.apksig.apk.ApkUtils;
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.apk.SignatureAlgorithm;
import com.android.apksig.internal.apk.SignatureInfo;
import com.android.apksig.internal.apk.v2.V2SchemeVerifier;
import com.android.apksig.internal.apk.v3.V3SchemeSigner;
import com.android.apksig.internal.apk.v3.V3SchemeVerifier;
import com.android.apksig.internal.util.Pair;
import com.android.apksig.util.DataSource;
import com.android.apksig.zip.ZipFormatException;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.SignatureException;
import java.security.cert.CertificateEncodingException;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
/**
* APK Signature Scheme V4 signer. V4 scheme file contains 2 mandatory fields - used during
* installation. And optional verity tree - has to be present during session commit.
* <p>
* The fields:
* <p>
* 1. hashingInfo - verity root hash and hashing info,
* 2. signingInfo - certificate, public key and signature,
* For more details see V4Signature.
* </p>
* (optional) verityTree: integer size prepended bytes of the verity hash tree.
* <p>
* TODO(schfan): Add v4 unit tests
*/
public abstract class V4SchemeSigner {
/**
* Hidden constructor to prevent instantiation.
*/
private V4SchemeSigner() {
}
/**
* Based on a public key, return a signing algorithm that supports verity.
*/
public static List<SignatureAlgorithm> getSuggestedSignatureAlgorithms(PublicKey signingKey,
int minSdkVersion, boolean apkSigningBlockPaddingSupported,
boolean deterministicDsaSigning)
throws InvalidKeyException {
List<SignatureAlgorithm> algorithms = V3SchemeSigner.getSuggestedSignatureAlgorithms(
signingKey, minSdkVersion,
apkSigningBlockPaddingSupported, deterministicDsaSigning);
// Keeping only supported algorithms.
for (Iterator<SignatureAlgorithm> iter = algorithms.listIterator(); iter.hasNext(); ) {
final SignatureAlgorithm algorithm = iter.next();
if (!isSupported(algorithm.getContentDigestAlgorithm(), false)) {
iter.remove();
}
}
return algorithms;
}
/**
* Compute hash tree and generate v4 signature for a given APK. Write the serialized data to
* output file.
*/
public static void generateV4Signature(
DataSource apkContent, SignerConfig signerConfig, File outputFile)
throws IOException, InvalidKeyException, NoSuchAlgorithmException {
Pair<V4Signature, byte[]> pair = generateV4Signature(apkContent, signerConfig);
try (final OutputStream output = new FileOutputStream(outputFile)) {
pair.getFirst().writeTo(output);
V4Signature.writeBytes(output, pair.getSecond());
} catch (IOException e) {
outputFile.delete();
throw e;
}
}
/** Generate v4 signature and hash tree for a given APK. */
public static Pair<V4Signature, byte[]> generateV4Signature(
DataSource apkContent,
SignerConfig signerConfig)
throws IOException, InvalidKeyException, NoSuchAlgorithmException {
// Salt has to stay empty for fs-verity compatibility.
final byte[] salt = null;
// Not used by apksigner.
final byte[] additionalData = null;
final long fileSize = apkContent.size();
// Obtaining first supported digest from v2/v3 blocks (SHA256 or SHA512).
final byte[] apkDigest = getApkDigest(apkContent);
// Obtaining the merkle tree and the root hash in verity format.
ApkSigningBlockUtils.VerityTreeAndDigest verityContentDigestInfo =
ApkSigningBlockUtils.computeChunkVerityTreeAndDigest(apkContent);
final ContentDigestAlgorithm verityContentDigestAlgorithm =
verityContentDigestInfo.contentDigestAlgorithm;
final byte[] rootHash = verityContentDigestInfo.rootHash;
final byte[] tree = verityContentDigestInfo.tree;
final Pair<Integer, Byte> hashingAlgorithmBlockSizePair = convertToV4HashingInfo(
verityContentDigestAlgorithm);
final V4Signature.HashingInfo hashingInfo = new V4Signature.HashingInfo(
hashingAlgorithmBlockSizePair.getFirst(), hashingAlgorithmBlockSizePair.getSecond(),
salt, rootHash);
// Generating SigningInfo and combining everything into V4Signature.
final V4Signature signature;
try {
signature = generateSignature(signerConfig, hashingInfo, apkDigest, additionalData,
fileSize);
} catch (InvalidKeyException | SignatureException | CertificateEncodingException e) {
throw new InvalidKeyException("Signer failed", e);
}
return Pair.of(signature, tree);
}
private static V4Signature generateSignature(
SignerConfig signerConfig,
V4Signature.HashingInfo hashingInfo,
byte[] apkDigest, byte[] additionaData, long fileSize)
throws NoSuchAlgorithmException, InvalidKeyException, SignatureException,
CertificateEncodingException {
if (signerConfig.certificates.isEmpty()) {
throw new SignatureException("No certificates configured for signer");
}
if (signerConfig.certificates.size() != 1) {
throw new CertificateEncodingException("Should only have one certificate");
}
// Collecting data for signing.
final PublicKey publicKey = signerConfig.certificates.get(0).getPublicKey();
final List<byte[]> encodedCertificates = encodeCertificates(signerConfig.certificates);
final byte[] encodedCertificate = encodedCertificates.get(0);
final V4Signature.SigningInfo signingInfoNoSignature = new V4Signature.SigningInfo(apkDigest,
encodedCertificate, additionaData, publicKey.getEncoded(), -1, null);
final byte[] data = V4Signature.getSigningData(fileSize, hashingInfo,
signingInfoNoSignature);
// Signing.
final List<Pair<Integer, byte[]>> signatures =
ApkSigningBlockUtils.generateSignaturesOverData(signerConfig, data);
if (signatures.size() != 1) {
throw new SignatureException("Should only be one signature generated");
}
final int signatureAlgorithmId = signatures.get(0).getFirst();
final byte[] signature = signatures.get(0).getSecond();
final V4Signature.SigningInfo signingInfo = new V4Signature.SigningInfo(apkDigest,
encodedCertificate, additionaData, publicKey.getEncoded(), signatureAlgorithmId,
signature);
return new V4Signature(V4Signature.CURRENT_VERSION, hashingInfo.toByteArray(),
signingInfo.toByteArray());
}
// Get digest by parsing the V2/V3-signed apk and choosing the first digest of supported type.
private static byte[] getApkDigest(DataSource apk) throws IOException {
ApkUtils.ZipSections zipSections;
try {
zipSections = ApkUtils.findZipSections(apk);
} catch (ZipFormatException e) {
throw new IOException("Malformed APK: not a ZIP archive", e);
}
final SignatureException v3Exception;
try {
return getBestV3Digest(apk, zipSections);
} catch (SignatureException e) {
v3Exception = e;
}
final SignatureException v2Exception;
try {
return getBestV2Digest(apk, zipSections);
} catch (SignatureException e) {
v2Exception = e;
}
throw new IOException(
"Failed to obtain v2/v3 digest, v3 exception: " + v3Exception + ", v2 exception: "
+ v2Exception);
}
private static byte[] getBestV3Digest(DataSource apk, ApkUtils.ZipSections zipSections)
throws SignatureException {
final Set<ContentDigestAlgorithm> contentDigestsToVerify = new HashSet<>(1);
final ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result(
ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3);
try {
final SignatureInfo signatureInfo =
ApkSigningBlockUtils.findSignature(apk, zipSections,
APK_SIGNATURE_SCHEME_V3_BLOCK_ID, result);
final ByteBuffer apkSignatureSchemeV3Block = signatureInfo.signatureBlock;
V3SchemeVerifier.parseSigners(apkSignatureSchemeV3Block, contentDigestsToVerify,
result);
} catch (Exception e) {
throw new SignatureException("Failed to extract and parse v3 block", e);
}
if (result.signers.size() != 1) {
throw new SignatureException("Should only have one signer, errors: " + result.getErrors());
}
ApkSigningBlockUtils.Result.SignerInfo signer = result.signers.get(0);
if (signer.containsErrors()) {
throw new SignatureException("Parsing failed: " + signer.getErrors());
}
final List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest> contentDigests =
result.signers.get(0).contentDigests;
return pickBestDigest(contentDigests);
}
private static byte[] getBestV2Digest(DataSource apk, ApkUtils.ZipSections zipSections)
throws SignatureException {
final Set<ContentDigestAlgorithm> contentDigestsToVerify = new HashSet<>(1);
final Set<Integer> foundApkSigSchemeIds = new HashSet<>(1);
final ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result(
ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2);
try {
final SignatureInfo signatureInfo =
ApkSigningBlockUtils.findSignature(apk, zipSections,
APK_SIGNATURE_SCHEME_V2_BLOCK_ID, result);
final ByteBuffer apkSignatureSchemeV2Block = signatureInfo.signatureBlock;
V2SchemeVerifier.parseSigners(
apkSignatureSchemeV2Block,
contentDigestsToVerify,
Collections.emptyMap(),
foundApkSigSchemeIds,
Integer.MAX_VALUE,
Integer.MAX_VALUE,
result);
} catch (Exception e) {
throw new SignatureException("Failed to extract and parse v2 block", e);
}
if (result.signers.size() != 1) {
throw new SignatureException("Should only have one signer, errors: " + result.getErrors());
}
ApkSigningBlockUtils.Result.SignerInfo signer = result.signers.get(0);
if (signer.containsErrors()) {
throw new SignatureException("Parsing failed: " + signer.getErrors());
}
final List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest> contentDigests =
signer.contentDigests;
return pickBestDigest(contentDigests);
}
private static byte[] pickBestDigest(List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest> contentDigests) throws SignatureException {
if (contentDigests == null || contentDigests.isEmpty()) {
throw new SignatureException("Should have at least one digest");
}
int bestAlgorithmOrder = -1;
byte[] bestDigest = null;
for (ApkSigningBlockUtils.Result.SignerInfo.ContentDigest contentDigest : contentDigests) {
final SignatureAlgorithm signatureAlgorithm =
SignatureAlgorithm.findById(contentDigest.getSignatureAlgorithmId());
final ContentDigestAlgorithm contentDigestAlgorithm =
signatureAlgorithm.getContentDigestAlgorithm();
if (!isSupported(contentDigestAlgorithm, true)) {
continue;
}
final int algorithmOrder = digestAlgorithmSortingOrder(contentDigestAlgorithm);
if (bestAlgorithmOrder < algorithmOrder) {
bestAlgorithmOrder = algorithmOrder;
bestDigest = contentDigest.getValue();
}
}
if (bestDigest == null) {
throw new SignatureException("Failed to find a supported digest in the source APK");
}
return bestDigest;
}
// Use the same order as in the ApkSignatureSchemeV3Verifier to make sure the digest
// verification in framework works.
public static int digestAlgorithmSortingOrder(ContentDigestAlgorithm contentDigestAlgorithm) {
switch (contentDigestAlgorithm) {
case CHUNKED_SHA256:
return 0;
case VERITY_CHUNKED_SHA256:
return 1;
case CHUNKED_SHA512:
return 2;
default:
return -1;
}
}
private static boolean isSupported(final ContentDigestAlgorithm contentDigestAlgorithm,
boolean forV3Digest) {
if (contentDigestAlgorithm == null) {
return false;
}
if (contentDigestAlgorithm == ContentDigestAlgorithm.CHUNKED_SHA256
|| contentDigestAlgorithm == ContentDigestAlgorithm.CHUNKED_SHA512
|| (forV3Digest
&& contentDigestAlgorithm == ContentDigestAlgorithm.VERITY_CHUNKED_SHA256)) {
return true;
}
return false;
}
private static Pair<Integer, Byte> convertToV4HashingInfo(ContentDigestAlgorithm algorithm)
throws NoSuchAlgorithmException {
switch (algorithm) {
case VERITY_CHUNKED_SHA256:
return Pair.of(V4Signature.HASHING_ALGORITHM_SHA256,
V4Signature.LOG2_BLOCK_SIZE_4096_BYTES);
default:
throw new NoSuchAlgorithmException(
"Invalid hash algorithm, only SHA2-256 over 4 KB chunks supported.");
}
}
}

View File

@ -1,250 +0,0 @@
/*
* 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.v4;
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.toHex;
import com.android.apksig.ApkVerifier;
import com.android.apksig.ApkVerifier.Issue;
import com.android.apksig.internal.apk.ApkSigningBlockUtils;
import com.android.apksig.internal.apk.ContentDigestAlgorithm;
import com.android.apksig.internal.apk.SignatureAlgorithm;
import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
import com.android.apksig.internal.util.X509CertificateUtils;
import com.android.apksig.util.DataSource;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
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.X509Certificate;
import java.security.spec.AlgorithmParameterSpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Arrays;
/**
* APK Signature Scheme V4 verifier.
* <p>
* Verifies the serialized V4Signature file against an APK.
*/
public abstract class V4SchemeVerifier {
/**
* Hidden constructor to prevent instantiation.
*/
private V4SchemeVerifier() {
}
/**
* <p>
* The main goals of the verifier are: 1) parse V4Signature file fields 2) verifies the PKCS7
* signature block against the raw root hash bytes in the proto field 3) verifies that the raw
* root hash matches with the actual hash tree root of the give APK 4) if the file contains a
* verity tree, verifies that it matches with the actual verity tree computed from the given
* APK.
* </p>
*/
public static ApkSigningBlockUtils.Result verify(DataSource apk, File v4SignatureFile)
throws IOException, NoSuchAlgorithmException {
final V4Signature signature;
final byte[] tree;
try (InputStream input = new FileInputStream(v4SignatureFile)) {
signature = V4Signature.readFrom(input);
tree = V4Signature.readBytes(input);
}
final ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result(
ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V4);
if (signature == null) {
result.addError(Issue.V4_SIG_NO_SIGNATURES,
"Signature file does not contain a v4 signature.");
return result;
}
if (signature.version != V4Signature.CURRENT_VERSION) {
result.addWarning(Issue.V4_SIG_VERSION_NOT_CURRENT, signature.version,
V4Signature.CURRENT_VERSION);
}
V4Signature.HashingInfo hashingInfo = V4Signature.HashingInfo.fromByteArray(
signature.hashingInfo);
V4Signature.SigningInfo signingInfo = V4Signature.SigningInfo.fromByteArray(
signature.signingInfo);
final byte[] signedData = V4Signature.getSigningData(apk.size(), hashingInfo, signingInfo);
// First, verify the signature over signedData.
ApkSigningBlockUtils.Result.SignerInfo signerInfo = parseAndVerifySignatureBlock(
signingInfo, signedData);
result.signers.add(signerInfo);
if (result.containsErrors()) {
return result;
}
// Second, check if the root hash and the tree are correct.
verifyRootHashAndTree(apk, signerInfo, hashingInfo.rawRootHash, tree);
if (!result.containsErrors()) {
result.verified = true;
}
return result;
}
/**
* Parses the provided signature block and populates the {@code result}.
* <p>
* This verifies {@signingInfo} over {@code signedData}, as well as parsing the certificate
* contained in the signature block. This method adds one or more errors to the {@code result}.
*/
private static ApkSigningBlockUtils.Result.SignerInfo parseAndVerifySignatureBlock(
V4Signature.SigningInfo signingInfo,
final byte[] signedData) throws NoSuchAlgorithmException {
final ApkSigningBlockUtils.Result.SignerInfo result =
new ApkSigningBlockUtils.Result.SignerInfo();
result.index = 0;
final int sigAlgorithmId = signingInfo.signatureAlgorithmId;
final byte[] sigBytes = signingInfo.signature;
result.signatures.add(
new ApkSigningBlockUtils.Result.SignerInfo.Signature(sigAlgorithmId, sigBytes));
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.findById(sigAlgorithmId);
if (signatureAlgorithm == null) {
result.addError(Issue.V4_SIG_UNKNOWN_SIG_ALGORITHM, sigAlgorithmId);
return result;
}
String jcaSignatureAlgorithm =
signatureAlgorithm.getJcaSignatureAlgorithmAndParams().getFirst();
AlgorithmParameterSpec jcaSignatureAlgorithmParams =
signatureAlgorithm.getJcaSignatureAlgorithmAndParams().getSecond();
String keyAlgorithm = signatureAlgorithm.getJcaKeyAlgorithm();
final byte[] publicKeyBytes = signingInfo.publicKey;
PublicKey publicKey;
try {
publicKey = KeyFactory.getInstance(keyAlgorithm).generatePublic(
new X509EncodedKeySpec(publicKeyBytes));
} catch (Exception e) {
result.addError(Issue.V4_SIG_MALFORMED_PUBLIC_KEY, e);
return result;
}
try {
Signature sig = Signature.getInstance(jcaSignatureAlgorithm);
sig.initVerify(publicKey);
if (jcaSignatureAlgorithmParams != null) {
sig.setParameter(jcaSignatureAlgorithmParams);
}
sig.update(signedData);
if (!sig.verify(sigBytes)) {
result.addError(Issue.V4_SIG_DID_NOT_VERIFY, signatureAlgorithm);
return result;
}
result.verifiedSignatures.put(signatureAlgorithm, sigBytes);
} catch (InvalidKeyException | InvalidAlgorithmParameterException
| SignatureException e) {
result.addError(Issue.V4_SIG_VERIFY_EXCEPTION, signatureAlgorithm, e);
return result;
}
if (signingInfo.certificate == null) {
result.addError(Issue.V4_SIG_NO_CERTIFICATE);
return result;
}
final X509Certificate certificate;
try {
// 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(
X509CertificateUtils.generateCertificate(signingInfo.certificate),
signingInfo.certificate);
} catch (CertificateException e) {
result.addError(Issue.V4_SIG_MALFORMED_CERTIFICATE, e);
return result;
}
result.certs.add(certificate);
byte[] certificatePublicKeyBytes;
try {
certificatePublicKeyBytes = ApkSigningBlockUtils.encodePublicKey(
certificate.getPublicKey());
} catch (InvalidKeyException e) {
System.out.println("Caught an exception encoding the public key: " + e);
e.printStackTrace();
certificatePublicKeyBytes = certificate.getPublicKey().getEncoded();
}
if (!Arrays.equals(publicKeyBytes, certificatePublicKeyBytes)) {
result.addError(
Issue.V4_SIG_PUBLIC_KEY_MISMATCH_BETWEEN_CERTIFICATE_AND_SIGNATURES_RECORD,
ApkSigningBlockUtils.toHex(certificatePublicKeyBytes),
ApkSigningBlockUtils.toHex(publicKeyBytes));
return result;
}
// Add apk digest from the file to the result.
ApkSigningBlockUtils.Result.SignerInfo.ContentDigest contentDigest =
new ApkSigningBlockUtils.Result.SignerInfo.ContentDigest(
0 /* signature algorithm id doesn't matter here */, signingInfo.apkDigest);
result.contentDigests.add(contentDigest);
return result;
}
private static void verifyRootHashAndTree(DataSource apkContent,
ApkSigningBlockUtils.Result.SignerInfo signerInfo, byte[] expectedDigest,
byte[] expectedTree) throws IOException, NoSuchAlgorithmException {
ApkSigningBlockUtils.VerityTreeAndDigest actualContentDigestInfo =
ApkSigningBlockUtils.computeChunkVerityTreeAndDigest(apkContent);
ContentDigestAlgorithm algorithm = actualContentDigestInfo.contentDigestAlgorithm;
final byte[] actualDigest = actualContentDigestInfo.rootHash;
final byte[] actualTree = actualContentDigestInfo.tree;
if (!Arrays.equals(expectedDigest, actualDigest)) {
signerInfo.addError(
ApkVerifier.Issue.V4_SIG_APK_ROOT_DID_NOT_VERIFY,
algorithm,
toHex(expectedDigest),
toHex(actualDigest));
return;
}
// Only check verity tree if it is not empty
if (expectedTree != null && !Arrays.equals(expectedTree, actualTree)) {
signerInfo.addError(
ApkVerifier.Issue.V4_SIG_APK_TREE_DID_NOT_VERIFY,
algorithm,
toHex(expectedDigest),
toHex(actualDigest));
return;
}
signerInfo.verifiedContentDigests.put(algorithm, actualDigest);
}
}

View File

@ -1,225 +0,0 @@
/*
* 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.v4;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
public class V4Signature {
public static final int CURRENT_VERSION = 2;
public static final int HASHING_ALGORITHM_SHA256 = 1;
public static final byte LOG2_BLOCK_SIZE_4096_BYTES = 12;
public static class HashingInfo {
public final int hashAlgorithm; // only 1 == SHA256 supported
public final byte log2BlockSize; // only 12 (block size 4096) supported now
public final byte[] salt; // used exactly as in fs-verity, 32 bytes max
public final byte[] rawRootHash; // salted digest of the first Merkle tree page
HashingInfo(int hashAlgorithm, byte log2BlockSize, byte[] salt, byte[] rawRootHash) {
this.hashAlgorithm = hashAlgorithm;
this.log2BlockSize = log2BlockSize;
this.salt = salt;
this.rawRootHash = rawRootHash;
}
static HashingInfo fromByteArray(byte[] bytes) throws IOException {
ByteBuffer buffer = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN);
final int hashAlgorithm = buffer.getInt();
final byte log2BlockSize = buffer.get();
byte[] salt = readBytes(buffer);
byte[] rawRootHash = readBytes(buffer);
return new HashingInfo(hashAlgorithm, log2BlockSize, salt, rawRootHash);
}
byte[] toByteArray() {
final int size = 4/*hashAlgorithm*/ + 1/*log2BlockSize*/ + bytesSize(this.salt)
+ bytesSize(this.rawRootHash);
ByteBuffer buffer = ByteBuffer.allocate(size).order(ByteOrder.LITTLE_ENDIAN);
buffer.putInt(this.hashAlgorithm);
buffer.put(this.log2BlockSize);
writeBytes(buffer, this.salt);
writeBytes(buffer, this.rawRootHash);
return buffer.array();
}
}
public static class SigningInfo {
public final byte[] apkDigest; // used to match with the corresponding APK
public final byte[] certificate; // ASN.1 DER form
public final byte[] additionalData; // a free-form binary data blob
public final byte[] publicKey; // ASN.1 DER, must match the certificate
public final int signatureAlgorithmId; // see the APK v2 doc for the list
public final byte[] signature;
SigningInfo(byte[] apkDigest, byte[] certificate, byte[] additionalData,
byte[] publicKey, int signatureAlgorithmId, byte[] signature) {
this.apkDigest = apkDigest;
this.certificate = certificate;
this.additionalData = additionalData;
this.publicKey = publicKey;
this.signatureAlgorithmId = signatureAlgorithmId;
this.signature = signature;
}
static SigningInfo fromByteArray(byte[] bytes) throws IOException {
ByteBuffer buffer = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN);
byte[] apkDigest = readBytes(buffer);
byte[] certificate = readBytes(buffer);
byte[] additionalData = readBytes(buffer);
byte[] publicKey = readBytes(buffer);
int signatureAlgorithmId = buffer.getInt();
byte[] signature = readBytes(buffer);
return new SigningInfo(apkDigest, certificate, additionalData, publicKey,
signatureAlgorithmId, signature);
}
byte[] toByteArray() {
final int size = bytesSize(this.apkDigest) + bytesSize(this.certificate) + bytesSize(
this.additionalData) + bytesSize(this.publicKey) + 4/*signatureAlgorithmId*/
+ bytesSize(this.signature);
ByteBuffer buffer = ByteBuffer.allocate(size).order(ByteOrder.LITTLE_ENDIAN);
writeBytes(buffer, this.apkDigest);
writeBytes(buffer, this.certificate);
writeBytes(buffer, this.additionalData);
writeBytes(buffer, this.publicKey);
buffer.putInt(this.signatureAlgorithmId);
writeBytes(buffer, this.signature);
return buffer.array();
}
}
public final int version; // Always 2 for now.
public final byte[] hashingInfo;
public final byte[] signingInfo; // Passed as-is to the kernel. Can be retrieved later.
V4Signature(int version, byte[] hashingInfo, byte[] signingInfo) {
this.version = version;
this.hashingInfo = hashingInfo;
this.signingInfo = signingInfo;
}
static V4Signature readFrom(InputStream stream) throws IOException {
final int version = readIntLE(stream);
if (version != CURRENT_VERSION) {
throw new IOException("Invalid signature version.");
}
final byte[] hashingInfo = readBytes(stream);
final byte[] signingInfo = readBytes(stream);
return new V4Signature(version, hashingInfo, signingInfo);
}
public void writeTo(OutputStream stream) throws IOException {
writeIntLE(stream, this.version);
writeBytes(stream, this.hashingInfo);
writeBytes(stream, this.signingInfo);
}
static byte[] getSigningData(long fileSize, HashingInfo hashingInfo, SigningInfo signingInfo) {
final int size =
4/*size*/ + 8/*fileSize*/ + 4/*hash_algorithm*/ + 1/*log2_blocksize*/ + bytesSize(
hashingInfo.salt) + bytesSize(hashingInfo.rawRootHash) + bytesSize(
signingInfo.apkDigest) + bytesSize(signingInfo.certificate) + bytesSize(
signingInfo.additionalData);
ByteBuffer buffer = ByteBuffer.allocate(size).order(ByteOrder.LITTLE_ENDIAN);
buffer.putInt(size);
buffer.putLong(fileSize);
buffer.putInt(hashingInfo.hashAlgorithm);
buffer.put(hashingInfo.log2BlockSize);
writeBytes(buffer, hashingInfo.salt);
writeBytes(buffer, hashingInfo.rawRootHash);
writeBytes(buffer, signingInfo.apkDigest);
writeBytes(buffer, signingInfo.certificate);
writeBytes(buffer, signingInfo.additionalData);
return buffer.array();
}
// Utility methods.
static int bytesSize(byte[] bytes) {
return 4/*length*/ + (bytes == null ? 0 : bytes.length);
}
static void readFully(InputStream stream, byte[] buffer) throws IOException {
int len = buffer.length;
int n = 0;
while (n < len) {
int count = stream.read(buffer, n, len - n);
if (count < 0) {
throw new EOFException();
}
n += count;
}
}
static int readIntLE(InputStream stream) throws IOException {
final byte[] buffer = new byte[4];
readFully(stream, buffer);
return ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN).getInt();
}
static void writeIntLE(OutputStream stream, int v) throws IOException {
final byte[] buffer = ByteBuffer.wrap(new byte[4]).order(ByteOrder.LITTLE_ENDIAN).putInt(v).array();
stream.write(buffer);
}
static byte[] readBytes(InputStream stream) throws IOException {
try {
final int size = readIntLE(stream);
final byte[] bytes = new byte[size];
readFully(stream, bytes);
return bytes;
} catch (EOFException ignored) {
return null;
}
}
static byte[] readBytes(ByteBuffer buffer) throws IOException {
if (buffer.remaining() < 4) {
throw new EOFException();
}
final int size = buffer.getInt();
if (buffer.remaining() < size) {
throw new EOFException();
}
final byte[] bytes = new byte[size];
buffer.get(bytes);
return bytes;
}
static void writeBytes(OutputStream stream, byte[] bytes) throws IOException {
if (bytes == null) {
writeIntLE(stream, 0);
return;
}
writeIntLE(stream, bytes.length);
stream.write(bytes);
}
static void writeBytes(ByteBuffer buffer, byte[] bytes) {
if (bytes == null) {
buffer.putInt(0);
return;
}
buffer.putInt(bytes.length);
buffer.put(bytes);
}
}

View File

@ -1,673 +0,0 @@
/*
* 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.internal.asn1;
import com.android.apksig.internal.asn1.ber.BerDataValue;
import com.android.apksig.internal.asn1.ber.BerDataValueFormatException;
import com.android.apksig.internal.asn1.ber.BerDataValueReader;
import com.android.apksig.internal.asn1.ber.BerEncoding;
import com.android.apksig.internal.asn1.ber.ByteBufferBerDataValueReader;
import com.android.apksig.internal.util.ByteBufferUtils;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* Parser of ASN.1 BER-encoded structures.
*
* <p>Structure is described to the parser by providing a class annotated with {@link Asn1Class},
* containing fields annotated with {@link Asn1Field}.
*/
public final class Asn1BerParser {
private Asn1BerParser() {}
/**
* Returns the ASN.1 structure contained in the BER encoded input.
*
* @param encoded encoded input. If the decoding operation succeeds, the position of this buffer
* is advanced to the first position following the end of the consumed structure.
* @param containerClass class describing the structure of the input. The class must meet the
* following requirements:
* <ul>
* <li>The class must be annotated with {@link Asn1Class}.</li>
* <li>The class must expose a public no-arg constructor.</li>
* <li>Member fields of the class which are populated with parsed input must be
* annotated with {@link Asn1Field} and be public and non-final.</li>
* </ul>
*
* @throws Asn1DecodingException if the input could not be decoded into the specified Java
* object
*/
public static <T> T parse(ByteBuffer encoded, Class<T> containerClass)
throws Asn1DecodingException {
BerDataValue containerDataValue;
try {
containerDataValue = new ByteBufferBerDataValueReader(encoded).readDataValue();
} catch (BerDataValueFormatException e) {
throw new Asn1DecodingException("Failed to decode top-level data value", e);
}
if (containerDataValue == null) {
throw new Asn1DecodingException("Empty input");
}
return parse(containerDataValue, containerClass);
}
/**
* Returns the implicit {@code SET OF} contained in the provided ASN.1 BER input. Implicit means
* that this method does not care whether the tag number of this data structure is
* {@code SET OF} and whether the tag class is {@code UNIVERSAL}.
*
* <p>Note: The returned type is {@link List} rather than {@link java.util.Set} because ASN.1
* SET may contain duplicate elements.
*
* @param encoded encoded input. If the decoding operation succeeds, the position of this buffer
* is advanced to the first position following the end of the consumed structure.
* @param elementClass class describing the structure of the values/elements contained in this
* container. The class must meet the following requirements:
* <ul>
* <li>The class must be annotated with {@link Asn1Class}.</li>
* <li>The class must expose a public no-arg constructor.</li>
* <li>Member fields of the class which are populated with parsed input must be
* annotated with {@link Asn1Field} and be public and non-final.</li>
* </ul>
*
* @throws Asn1DecodingException if the input could not be decoded into the specified Java
* object
*/
public static <T> List<T> parseImplicitSetOf(ByteBuffer encoded, Class<T> elementClass)
throws Asn1DecodingException {
BerDataValue containerDataValue;
try {
containerDataValue = new ByteBufferBerDataValueReader(encoded).readDataValue();
} catch (BerDataValueFormatException e) {
throw new Asn1DecodingException("Failed to decode top-level data value", e);
}
if (containerDataValue == null) {
throw new Asn1DecodingException("Empty input");
}
return parseSetOf(containerDataValue, elementClass);
}
private static <T> T parse(BerDataValue container, Class<T> containerClass)
throws Asn1DecodingException {
if (container == null) {
throw new NullPointerException("container == null");
}
if (containerClass == null) {
throw new NullPointerException("containerClass == null");
}
Asn1Type dataType = getContainerAsn1Type(containerClass);
switch (dataType) {
case CHOICE:
return parseChoice(container, containerClass);
case SEQUENCE:
{
int expectedTagClass = BerEncoding.TAG_CLASS_UNIVERSAL;
int expectedTagNumber = BerEncoding.getTagNumber(dataType);
if ((container.getTagClass() != expectedTagClass)
|| (container.getTagNumber() != expectedTagNumber)) {
throw new Asn1UnexpectedTagException(
"Unexpected data value read as " + containerClass.getName()
+ ". Expected " + BerEncoding.tagClassAndNumberToString(
expectedTagClass, expectedTagNumber)
+ ", but read: " + BerEncoding.tagClassAndNumberToString(
container.getTagClass(), container.getTagNumber()));
}
return parseSequence(container, containerClass);
}
case UNENCODED_CONTAINER:
return parseSequence(container, containerClass, true);
default:
throw new Asn1DecodingException("Parsing container " + dataType + " not supported");
}
}
private static <T> T parseChoice(BerDataValue dataValue, Class<T> containerClass)
throws Asn1DecodingException {
List<AnnotatedField> fields = getAnnotatedFields(containerClass);
if (fields.isEmpty()) {
throw new Asn1DecodingException(
"No fields annotated with " + Asn1Field.class.getName()
+ " in CHOICE class " + containerClass.getName());
}
// Check that class + tagNumber don't clash between the choices
for (int i = 0; i < fields.size() - 1; i++) {
AnnotatedField f1 = fields.get(i);
int tagNumber1 = f1.getBerTagNumber();
int tagClass1 = f1.getBerTagClass();
for (int j = i + 1; j < fields.size(); j++) {
AnnotatedField f2 = fields.get(j);
int tagNumber2 = f2.getBerTagNumber();
int tagClass2 = f2.getBerTagClass();
if ((tagNumber1 == tagNumber2) && (tagClass1 == tagClass2)) {
throw new Asn1DecodingException(
"CHOICE fields are indistinguishable because they have the same tag"
+ " class and number: " + containerClass.getName()
+ "." + f1.getField().getName()
+ " and ." + f2.getField().getName());
}
}
}
// Instantiate the container object / result
T obj;
try {
obj = containerClass.getConstructor().newInstance();
} catch (IllegalArgumentException | ReflectiveOperationException e) {
throw new Asn1DecodingException("Failed to instantiate " + containerClass.getName(), e);
}
// Set the matching field's value from the data value
for (AnnotatedField field : fields) {
try {
field.setValueFrom(dataValue, obj);
return obj;
} catch (Asn1UnexpectedTagException expected) {
// not a match
}
}
throw new Asn1DecodingException(
"No options of CHOICE " + containerClass.getName() + " matched");
}
private static <T> T parseSequence(BerDataValue container, Class<T> containerClass)
throws Asn1DecodingException {
return parseSequence(container, containerClass, false);
}
private static <T> T parseSequence(BerDataValue container, Class<T> containerClass,
boolean isUnencodedContainer) throws Asn1DecodingException {
List<AnnotatedField> fields = getAnnotatedFields(containerClass);
Collections.sort(
fields, (f1, f2) -> f1.getAnnotation().index() - f2.getAnnotation().index());
// Check that there are no fields with the same index
if (fields.size() > 1) {
AnnotatedField lastField = null;
for (AnnotatedField field : fields) {
if ((lastField != null)
&& (lastField.getAnnotation().index() == field.getAnnotation().index())) {
throw new Asn1DecodingException(
"Fields have the same index: " + containerClass.getName()
+ "." + lastField.getField().getName()
+ " and ." + field.getField().getName());
}
lastField = field;
}
}
// Instantiate the container object / result
T t;
try {
t = containerClass.getConstructor().newInstance();
} catch (IllegalArgumentException | ReflectiveOperationException e) {
throw new Asn1DecodingException("Failed to instantiate " + containerClass.getName(), e);
}
// Parse fields one by one. A complication is that there may be optional fields.
int nextUnreadFieldIndex = 0;
BerDataValueReader elementsReader = container.contentsReader();
while (nextUnreadFieldIndex < fields.size()) {
BerDataValue dataValue;
try {
// if this is the first field of an unencoded container then the entire contents of
// the container should be used when assigning to this field.
if (isUnencodedContainer && nextUnreadFieldIndex == 0) {
dataValue = container;
} else {
dataValue = elementsReader.readDataValue();
}
} catch (BerDataValueFormatException e) {
throw new Asn1DecodingException("Malformed data value", e);
}
if (dataValue == null) {
break;
}
for (int i = nextUnreadFieldIndex; i < fields.size(); i++) {
AnnotatedField field = fields.get(i);
try {
if (field.isOptional()) {
// Optional field -- might not be present and we may thus be trying to set
// it from the wrong tag.
try {
field.setValueFrom(dataValue, t);
nextUnreadFieldIndex = i + 1;
break;
} catch (Asn1UnexpectedTagException e) {
// This field is not present, attempt to use this data value for the
// next / iteration of the loop
continue;
}
} else {
// Mandatory field -- if we can't set its value from this data value, then
// it's an error
field.setValueFrom(dataValue, t);
nextUnreadFieldIndex = i + 1;
break;
}
} catch (Asn1DecodingException e) {
throw new Asn1DecodingException(
"Failed to parse " + containerClass.getName()
+ "." + field.getField().getName(),
e);
}
}
}
return t;
}
// NOTE: This method returns List rather than Set because ASN.1 SET_OF does require uniqueness
// of elements -- it's an unordered collection.
@SuppressWarnings("unchecked")
private static <T> List<T> parseSetOf(BerDataValue container, Class<T> elementClass)
throws Asn1DecodingException {
List<T> result = new ArrayList<>();
BerDataValueReader elementsReader = container.contentsReader();
while (true) {
BerDataValue dataValue;
try {
dataValue = elementsReader.readDataValue();
} catch (BerDataValueFormatException e) {
throw new Asn1DecodingException("Malformed data value", e);
}
if (dataValue == null) {
break;
}
T element;
if (ByteBuffer.class.equals(elementClass)) {
element = (T) dataValue.getEncodedContents();
} else if (Asn1OpaqueObject.class.equals(elementClass)) {
element = (T) new Asn1OpaqueObject(dataValue.getEncoded());
} else {
element = parse(dataValue, elementClass);
}
result.add(element);
}
return result;
}
private static Asn1Type getContainerAsn1Type(Class<?> containerClass)
throws Asn1DecodingException {
Asn1Class containerAnnotation = containerClass.getDeclaredAnnotation(Asn1Class.class);
if (containerAnnotation == null) {
throw new Asn1DecodingException(
containerClass.getName() + " is not annotated with "
+ Asn1Class.class.getName());
}
switch (containerAnnotation.type()) {
case CHOICE:
case SEQUENCE:
case UNENCODED_CONTAINER:
return containerAnnotation.type();
default:
throw new Asn1DecodingException(
"Unsupported ASN.1 container annotation type: "
+ containerAnnotation.type());
}
}
private static Class<?> getElementType(Field field)
throws Asn1DecodingException, ClassNotFoundException {
String type = field.getGenericType().getTypeName();
int delimiterIndex = type.indexOf('<');
if (delimiterIndex == -1) {
throw new Asn1DecodingException("Not a container type: " + field.getGenericType());
}
int startIndex = delimiterIndex + 1;
int endIndex = type.indexOf('>', startIndex);
// TODO: handle comma?
if (endIndex == -1) {
throw new Asn1DecodingException("Not a container type: " + field.getGenericType());
}
String elementClassName = type.substring(startIndex, endIndex);
return Class.forName(elementClassName);
}
private static final class AnnotatedField {
private final Field mField;
private final Asn1Field mAnnotation;
private final Asn1Type mDataType;
private final Asn1TagClass mTagClass;
private final int mBerTagClass;
private final int mBerTagNumber;
private final Asn1Tagging mTagging;
private final boolean mOptional;
public AnnotatedField(Field field, Asn1Field annotation) throws Asn1DecodingException {
mField = field;
mAnnotation = annotation;
mDataType = annotation.type();
Asn1TagClass tagClass = annotation.cls();
if (tagClass == Asn1TagClass.AUTOMATIC) {
if (annotation.tagNumber() != -1) {
tagClass = Asn1TagClass.CONTEXT_SPECIFIC;
} else {
tagClass = Asn1TagClass.UNIVERSAL;
}
}
mTagClass = tagClass;
mBerTagClass = BerEncoding.getTagClass(mTagClass);
int tagNumber;
if (annotation.tagNumber() != -1) {
tagNumber = annotation.tagNumber();
} else if ((mDataType == Asn1Type.CHOICE) || (mDataType == Asn1Type.ANY)) {
tagNumber = -1;
} else {
tagNumber = BerEncoding.getTagNumber(mDataType);
}
mBerTagNumber = tagNumber;
mTagging = annotation.tagging();
if (((mTagging == Asn1Tagging.EXPLICIT) || (mTagging == Asn1Tagging.IMPLICIT))
&& (annotation.tagNumber() == -1)) {
throw new Asn1DecodingException(
"Tag number must be specified when tagging mode is " + mTagging);
}
mOptional = annotation.optional();
}
public Field getField() {
return mField;
}
public Asn1Field getAnnotation() {
return mAnnotation;
}
public boolean isOptional() {
return mOptional;
}
public int getBerTagClass() {
return mBerTagClass;
}
public int getBerTagNumber() {
return mBerTagNumber;
}
public void setValueFrom(BerDataValue dataValue, Object obj) throws Asn1DecodingException {
int readTagClass = dataValue.getTagClass();
if (mBerTagNumber != -1) {
int readTagNumber = dataValue.getTagNumber();
if ((readTagClass != mBerTagClass) || (readTagNumber != mBerTagNumber)) {
throw new Asn1UnexpectedTagException(
"Tag mismatch. Expected: "
+ BerEncoding.tagClassAndNumberToString(mBerTagClass, mBerTagNumber)
+ ", but found "
+ BerEncoding.tagClassAndNumberToString(readTagClass, readTagNumber));
}
} else {
if (readTagClass != mBerTagClass) {
throw new Asn1UnexpectedTagException(
"Tag mismatch. Expected class: "
+ BerEncoding.tagClassToString(mBerTagClass)
+ ", but found "
+ BerEncoding.tagClassToString(readTagClass));
}
}
if (mTagging == Asn1Tagging.EXPLICIT) {
try {
dataValue = dataValue.contentsReader().readDataValue();
} catch (BerDataValueFormatException e) {
throw new Asn1DecodingException(
"Failed to read contents of EXPLICIT data value", e);
}
}
BerToJavaConverter.setFieldValue(obj, mField, mDataType, dataValue);
}
}
private static class Asn1UnexpectedTagException extends Asn1DecodingException {
private static final long serialVersionUID = 1L;
public Asn1UnexpectedTagException(String message) {
super(message);
}
}
private static String oidToString(ByteBuffer encodedOid) throws Asn1DecodingException {
if (!encodedOid.hasRemaining()) {
throw new Asn1DecodingException("Empty OBJECT IDENTIFIER");
}
// First component encodes the first two nodes, X.Y, as X * 40 + Y, with 0 <= X <= 2
long firstComponent = decodeBase128UnsignedLong(encodedOid);
int firstNode = (int) Math.min(firstComponent / 40, 2);
long secondNode = firstComponent - firstNode * 40;
StringBuilder result = new StringBuilder();
result.append(Long.toString(firstNode)).append('.')
.append(Long.toString(secondNode));
// Each consecutive node is encoded as a separate component
while (encodedOid.hasRemaining()) {
long node = decodeBase128UnsignedLong(encodedOid);
result.append('.').append(Long.toString(node));
}
return result.toString();
}
private static long decodeBase128UnsignedLong(ByteBuffer encoded) throws Asn1DecodingException {
if (!encoded.hasRemaining()) {
return 0;
}
long result = 0;
while (encoded.hasRemaining()) {
if (result > Long.MAX_VALUE >>> 7) {
throw new Asn1DecodingException("Base-128 number too large");
}
int b = encoded.get() & 0xff;
result <<= 7;
result |= b & 0x7f;
if ((b & 0x80) == 0) {
return result;
}
}
throw new Asn1DecodingException(
"Truncated base-128 encoded input: missing terminating byte, with highest bit not"
+ " set");
}
private static BigInteger integerToBigInteger(ByteBuffer encoded) {
if (!encoded.hasRemaining()) {
return BigInteger.ZERO;
}
return new BigInteger(ByteBufferUtils.toByteArray(encoded));
}
private static int integerToInt(ByteBuffer encoded) throws Asn1DecodingException {
BigInteger value = integerToBigInteger(encoded);
if (value.compareTo(BigInteger.valueOf(Integer.MIN_VALUE)) < 0
|| value.compareTo(BigInteger.valueOf(Integer.MAX_VALUE)) > 0) {
throw new Asn1DecodingException(
String.format("INTEGER cannot be represented as int: %1$d (0x%1$x)", value));
}
return value.intValue();
}
private static long integerToLong(ByteBuffer encoded) throws Asn1DecodingException {
BigInteger value = integerToBigInteger(encoded);
if (value.compareTo(BigInteger.valueOf(Long.MIN_VALUE)) < 0
|| value.compareTo(BigInteger.valueOf(Long.MAX_VALUE)) > 0) {
throw new Asn1DecodingException(
String.format("INTEGER cannot be represented as long: %1$d (0x%1$x)", value));
}
return value.longValue();
}
private static List<AnnotatedField> getAnnotatedFields(Class<?> containerClass)
throws Asn1DecodingException {
Field[] declaredFields = containerClass.getDeclaredFields();
List<AnnotatedField> result = new ArrayList<>(declaredFields.length);
for (Field field : declaredFields) {
Asn1Field annotation = field.getDeclaredAnnotation(Asn1Field.class);
if (annotation == null) {
continue;
}
if (Modifier.isStatic(field.getModifiers())) {
throw new Asn1DecodingException(
Asn1Field.class.getName() + " used on a static field: "
+ containerClass.getName() + "." + field.getName());
}
AnnotatedField annotatedField;
try {
annotatedField = new AnnotatedField(field, annotation);
} catch (Asn1DecodingException e) {
throw new Asn1DecodingException(
"Invalid ASN.1 annotation on "
+ containerClass.getName() + "." + field.getName(),
e);
}
result.add(annotatedField);
}
return result;
}
private static final class BerToJavaConverter {
private BerToJavaConverter() {}
public static void setFieldValue(
Object obj, Field field, Asn1Type type, BerDataValue dataValue)
throws Asn1DecodingException {
try {
switch (type) {
case SET_OF:
case SEQUENCE_OF:
if (Asn1OpaqueObject.class.equals(field.getType())) {
field.set(obj, convert(type, dataValue, field.getType()));
} else {
field.set(obj, parseSetOf(dataValue, getElementType(field)));
}
return;
default:
field.set(obj, convert(type, dataValue, field.getType()));
break;
}
} catch (ReflectiveOperationException e) {
throw new Asn1DecodingException(
"Failed to set value of " + obj.getClass().getName()
+ "." + field.getName(),
e);
}
}
private static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
@SuppressWarnings("unchecked")
public static <T> T convert(
Asn1Type sourceType,
BerDataValue dataValue,
Class<T> targetType) throws Asn1DecodingException {
if (ByteBuffer.class.equals(targetType)) {
return (T) dataValue.getEncodedContents();
} else if (byte[].class.equals(targetType)) {
ByteBuffer resultBuf = dataValue.getEncodedContents();
if (!resultBuf.hasRemaining()) {
return (T) EMPTY_BYTE_ARRAY;
}
byte[] result = new byte[resultBuf.remaining()];
resultBuf.get(result);
return (T) result;
} else if (Asn1OpaqueObject.class.equals(targetType)) {
return (T) new Asn1OpaqueObject(dataValue.getEncoded());
}
ByteBuffer encodedContents = dataValue.getEncodedContents();
switch (sourceType) {
case INTEGER:
if ((int.class.equals(targetType)) || (Integer.class.equals(targetType))) {
return (T) Integer.valueOf(integerToInt(encodedContents));
} else if ((long.class.equals(targetType)) || (Long.class.equals(targetType))) {
return (T) Long.valueOf(integerToLong(encodedContents));
} else if (BigInteger.class.equals(targetType)) {
return (T) integerToBigInteger(encodedContents);
}
break;
case OBJECT_IDENTIFIER:
if (String.class.equals(targetType)) {
return (T) oidToString(encodedContents);
}
break;
case UTC_TIME:
case GENERALIZED_TIME:
if (String.class.equals(targetType)) {
return (T) new String(ByteBufferUtils.toByteArray(encodedContents));
}
break;
case BOOLEAN:
// A boolean should be encoded in a single byte with a value of 0 for false and
// any non-zero value for true.
if (boolean.class.equals(targetType)) {
if (encodedContents.remaining() != 1) {
throw new Asn1DecodingException(
"Incorrect encoded size of boolean value: "
+ encodedContents.remaining());
}
boolean result;
if (encodedContents.get() == 0) {
result = false;
} else {
result = true;
}
return (T) new Boolean(result);
}
break;
case SEQUENCE:
{
Asn1Class containerAnnotation =
targetType.getDeclaredAnnotation(Asn1Class.class);
if ((containerAnnotation != null)
&& (containerAnnotation.type() == Asn1Type.SEQUENCE)) {
return parseSequence(dataValue, targetType);
}
break;
}
case CHOICE:
{
Asn1Class containerAnnotation =
targetType.getDeclaredAnnotation(Asn1Class.class);
if ((containerAnnotation != null)
&& (containerAnnotation.type() == Asn1Type.CHOICE)) {
return parseChoice(dataValue, targetType);
}
break;
}
default:
break;
}
throw new Asn1DecodingException(
"Unsupported conversion: ASN.1 " + sourceType + " to " + targetType.getName());
}
}
}

View File

@ -1,28 +0,0 @@
/*
* 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.internal.asn1;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Asn1Class {
public Asn1Type type();
}

View File

@ -1,32 +0,0 @@
/*
* 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.internal.asn1;
/**
* Indicates that input could not be decoded into intended ASN.1 structure.
*/
public class Asn1DecodingException extends Exception {
private static final long serialVersionUID = 1L;
public Asn1DecodingException(String message) {
super(message);
}
public Asn1DecodingException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -1,596 +0,0 @@
/*
* 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.internal.asn1;
import com.android.apksig.internal.asn1.ber.BerEncoding;
import java.io.ByteArrayOutputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
/**
* Encoder of ASN.1 structures into DER-encoded form.
*
* <p>Structure is described to the encoder by providing a class annotated with {@link Asn1Class},
* containing fields annotated with {@link Asn1Field}.
*/
public final class Asn1DerEncoder {
private Asn1DerEncoder() {}
/**
* Returns the DER-encoded form of the provided ASN.1 structure.
*
* @param container container to be encoded. The container's class must meet the following
* requirements:
* <ul>
* <li>The class must be annotated with {@link Asn1Class}.</li>
* <li>Member fields of the class which are to be encoded must be annotated with
* {@link Asn1Field} and be public.</li>
* </ul>
*
* @throws Asn1EncodingException if the input could not be encoded
*/
public static byte[] encode(Object container) throws Asn1EncodingException {
Class<?> containerClass = container.getClass();
Asn1Class containerAnnotation = containerClass.getDeclaredAnnotation(Asn1Class.class);
if (containerAnnotation == null) {
throw new Asn1EncodingException(
containerClass.getName() + " not annotated with " + Asn1Class.class.getName());
}
Asn1Type containerType = containerAnnotation.type();
switch (containerType) {
case CHOICE:
return toChoice(container);
case SEQUENCE:
return toSequence(container);
case UNENCODED_CONTAINER:
return toSequence(container, true);
default:
throw new Asn1EncodingException("Unsupported container type: " + containerType);
}
}
private static byte[] toChoice(Object container) throws Asn1EncodingException {
Class<?> containerClass = container.getClass();
List<AnnotatedField> fields = getAnnotatedFields(container);
if (fields.isEmpty()) {
throw new Asn1EncodingException(
"No fields annotated with " + Asn1Field.class.getName()
+ " in CHOICE class " + containerClass.getName());
}
AnnotatedField resultField = null;
for (AnnotatedField field : fields) {
Object fieldValue = getMemberFieldValue(container, field.getField());
if (fieldValue != null) {
if (resultField != null) {
throw new Asn1EncodingException(
"Multiple non-null fields in CHOICE class " + containerClass.getName()
+ ": " + resultField.getField().getName()
+ ", " + field.getField().getName());
}
resultField = field;
}
}
if (resultField == null) {
throw new Asn1EncodingException(
"No non-null fields in CHOICE class " + containerClass.getName());
}
return resultField.toDer();
}
private static byte[] toSequence(Object container) throws Asn1EncodingException {
return toSequence(container, false);
}
private static byte[] toSequence(Object container, boolean omitTag)
throws Asn1EncodingException {
Class<?> containerClass = container.getClass();
List<AnnotatedField> fields = getAnnotatedFields(container);
Collections.sort(
fields, (f1, f2) -> f1.getAnnotation().index() - f2.getAnnotation().index());
if (fields.size() > 1) {
AnnotatedField lastField = null;
for (AnnotatedField field : fields) {
if ((lastField != null)
&& (lastField.getAnnotation().index() == field.getAnnotation().index())) {
throw new Asn1EncodingException(
"Fields have the same index: " + containerClass.getName()
+ "." + lastField.getField().getName()
+ " and ." + field.getField().getName());
}
lastField = field;
}
}
List<byte[]> serializedFields = new ArrayList<>(fields.size());
int contentLen = 0;
for (AnnotatedField field : fields) {
byte[] serializedField;
try {
serializedField = field.toDer();
} catch (Asn1EncodingException e) {
throw new Asn1EncodingException(
"Failed to encode " + containerClass.getName()
+ "." + field.getField().getName(),
e);
}
if (serializedField != null) {
serializedFields.add(serializedField);
contentLen += serializedField.length;
}
}
if (omitTag) {
byte[] unencodedResult = new byte[contentLen];
int index = 0;
for (byte[] serializedField : serializedFields) {
System.arraycopy(serializedField, 0, unencodedResult, index, serializedField.length);
index += serializedField.length;
}
return unencodedResult;
} else {
return createTag(
BerEncoding.TAG_CLASS_UNIVERSAL, true, BerEncoding.TAG_NUMBER_SEQUENCE,
serializedFields.toArray(new byte[0][]));
}
}
private static byte[] toSetOf(Collection<?> values, Asn1Type elementType) throws Asn1EncodingException {
return toSequenceOrSetOf(values, elementType, true);
}
private static byte[] toSequenceOf(Collection<?> values, Asn1Type elementType) throws Asn1EncodingException {
return toSequenceOrSetOf(values, elementType, false);
}
private static byte[] toSequenceOrSetOf(Collection<?> values, Asn1Type elementType, boolean toSet)
throws Asn1EncodingException {
List<byte[]> serializedValues = new ArrayList<>(values.size());
for (Object value : values) {
serializedValues.add(JavaToDerConverter.toDer(value, elementType, null));
}
int tagNumber;
if (toSet) {
if (serializedValues.size() > 1) {
Collections.sort(serializedValues, ByteArrayLexicographicComparator.INSTANCE);
}
tagNumber = BerEncoding.TAG_NUMBER_SET;
} else {
tagNumber = BerEncoding.TAG_NUMBER_SEQUENCE;
}
return createTag(
BerEncoding.TAG_CLASS_UNIVERSAL, true, tagNumber,
serializedValues.toArray(new byte[0][]));
}
/**
* Compares two bytes arrays based on their lexicographic order. Corresponding elements of the
* two arrays are compared in ascending order. Elements at out of range indices are assumed to
* be smaller than the smallest possible value for an element.
*/
private static class ByteArrayLexicographicComparator implements Comparator<byte[]> {
private static final ByteArrayLexicographicComparator INSTANCE =
new ByteArrayLexicographicComparator();
@Override
public int compare(byte[] arr1, byte[] arr2) {
int commonLength = Math.min(arr1.length, arr2.length);
for (int i = 0; i < commonLength; i++) {
int diff = (arr1[i] & 0xff) - (arr2[i] & 0xff);
if (diff != 0) {
return diff;
}
}
return arr1.length - arr2.length;
}
}
private static List<AnnotatedField> getAnnotatedFields(Object container)
throws Asn1EncodingException {
Class<?> containerClass = container.getClass();
Field[] declaredFields = containerClass.getDeclaredFields();
List<AnnotatedField> result = new ArrayList<>(declaredFields.length);
for (Field field : declaredFields) {
Asn1Field annotation = field.getDeclaredAnnotation(Asn1Field.class);
if (annotation == null) {
continue;
}
if (Modifier.isStatic(field.getModifiers())) {
throw new Asn1EncodingException(
Asn1Field.class.getName() + " used on a static field: "
+ containerClass.getName() + "." + field.getName());
}
AnnotatedField annotatedField;
try {
annotatedField = new AnnotatedField(container, field, annotation);
} catch (Asn1EncodingException e) {
throw new Asn1EncodingException(
"Invalid ASN.1 annotation on "
+ containerClass.getName() + "." + field.getName(),
e);
}
result.add(annotatedField);
}
return result;
}
private static byte[] toInteger(int value) {
return toInteger((long) value);
}
private static byte[] toInteger(long value) {
return toInteger(BigInteger.valueOf(value));
}
private static byte[] toInteger(BigInteger value) {
return createTag(
BerEncoding.TAG_CLASS_UNIVERSAL, false, BerEncoding.TAG_NUMBER_INTEGER,
value.toByteArray());
}
private static byte[] toBoolean(boolean value) {
// A boolean should be encoded in a single byte with a value of 0 for false and any non-zero
// value for true.
byte[] result = new byte[1];
if (value == false) {
result[0] = 0;
} else {
result[0] = 1;
}
return createTag(BerEncoding.TAG_CLASS_UNIVERSAL, false, BerEncoding.TAG_NUMBER_BOOLEAN, result);
}
private static byte[] toOid(String oid) throws Asn1EncodingException {
ByteArrayOutputStream encodedValue = new ByteArrayOutputStream();
String[] nodes = oid.split("\\.");
if (nodes.length < 2) {
throw new Asn1EncodingException(
"OBJECT IDENTIFIER must contain at least two nodes: " + oid);
}
int firstNode;
try {
firstNode = Integer.parseInt(nodes[0]);
} catch (NumberFormatException e) {
throw new Asn1EncodingException("Node #1 not numeric: " + nodes[0]);
}
if ((firstNode > 6) || (firstNode < 0)) {
throw new Asn1EncodingException("Invalid value for node #1: " + firstNode);
}
int secondNode;
try {
secondNode = Integer.parseInt(nodes[1]);
} catch (NumberFormatException e) {
throw new Asn1EncodingException("Node #2 not numeric: " + nodes[1]);
}
if ((secondNode >= 40) || (secondNode < 0)) {
throw new Asn1EncodingException("Invalid value for node #2: " + secondNode);
}
int firstByte = firstNode * 40 + secondNode;
if (firstByte > 0xff) {
throw new Asn1EncodingException(
"First two nodes out of range: " + firstNode + "." + secondNode);
}
encodedValue.write(firstByte);
for (int i = 2; i < nodes.length; i++) {
String nodeString = nodes[i];
int node;
try {
node = Integer.parseInt(nodeString);
} catch (NumberFormatException e) {
throw new Asn1EncodingException("Node #" + (i + 1) + " not numeric: " + nodeString);
}
if (node < 0) {
throw new Asn1EncodingException("Invalid value for node #" + (i + 1) + ": " + node);
}
if (node <= 0x7f) {
encodedValue.write(node);
continue;
}
if (node < 1 << 14) {
encodedValue.write(0x80 | (node >> 7));
encodedValue.write(node & 0x7f);
continue;
}
if (node < 1 << 21) {
encodedValue.write(0x80 | (node >> 14));
encodedValue.write(0x80 | ((node >> 7) & 0x7f));
encodedValue.write(node & 0x7f);
continue;
}
throw new Asn1EncodingException("Node #" + (i + 1) + " too large: " + node);
}
return createTag(
BerEncoding.TAG_CLASS_UNIVERSAL, false, BerEncoding.TAG_NUMBER_OBJECT_IDENTIFIER,
encodedValue.toByteArray());
}
private static Object getMemberFieldValue(Object obj, Field field)
throws Asn1EncodingException {
try {
return field.get(obj);
} catch (ReflectiveOperationException e) {
throw new Asn1EncodingException(
"Failed to read " + obj.getClass().getName() + "." + field.getName(), e);
}
}
private static final class AnnotatedField {
private final Field mField;
private final Object mObject;
private final Asn1Field mAnnotation;
private final Asn1Type mDataType;
private final Asn1Type mElementDataType;
private final Asn1TagClass mTagClass;
private final int mDerTagClass;
private final int mDerTagNumber;
private final Asn1Tagging mTagging;
private final boolean mOptional;
public AnnotatedField(Object obj, Field field, Asn1Field annotation)
throws Asn1EncodingException {
mObject = obj;
mField = field;
mAnnotation = annotation;
mDataType = annotation.type();
mElementDataType = annotation.elementType();
Asn1TagClass tagClass = annotation.cls();
if (tagClass == Asn1TagClass.AUTOMATIC) {
if (annotation.tagNumber() != -1) {
tagClass = Asn1TagClass.CONTEXT_SPECIFIC;
} else {
tagClass = Asn1TagClass.UNIVERSAL;
}
}
mTagClass = tagClass;
mDerTagClass = BerEncoding.getTagClass(mTagClass);
int tagNumber;
if (annotation.tagNumber() != -1) {
tagNumber = annotation.tagNumber();
} else if ((mDataType == Asn1Type.CHOICE) || (mDataType == Asn1Type.ANY)) {
tagNumber = -1;
} else {
tagNumber = BerEncoding.getTagNumber(mDataType);
}
mDerTagNumber = tagNumber;
mTagging = annotation.tagging();
if (((mTagging == Asn1Tagging.EXPLICIT) || (mTagging == Asn1Tagging.IMPLICIT))
&& (annotation.tagNumber() == -1)) {
throw new Asn1EncodingException(
"Tag number must be specified when tagging mode is " + mTagging);
}
mOptional = annotation.optional();
}
public Field getField() {
return mField;
}
public Asn1Field getAnnotation() {
return mAnnotation;
}
public byte[] toDer() throws Asn1EncodingException {
Object fieldValue = getMemberFieldValue(mObject, mField);
if (fieldValue == null) {
if (mOptional) {
return null;
}
throw new Asn1EncodingException("Required field not set");
}
byte[] encoded = JavaToDerConverter.toDer(fieldValue, mDataType, mElementDataType);
switch (mTagging) {
case NORMAL:
return encoded;
case EXPLICIT:
return createTag(mDerTagClass, true, mDerTagNumber, encoded);
case IMPLICIT:
int originalTagNumber = BerEncoding.getTagNumber(encoded[0]);
if (originalTagNumber == 0x1f) {
throw new Asn1EncodingException("High-tag-number form not supported");
}
if (mDerTagNumber >= 0x1f) {
throw new Asn1EncodingException(
"Unsupported high tag number: " + mDerTagNumber);
}
encoded[0] = BerEncoding.setTagNumber(encoded[0], mDerTagNumber);
encoded[0] = BerEncoding.setTagClass(encoded[0], mDerTagClass);
return encoded;
default:
throw new RuntimeException("Unknown tagging mode: " + mTagging);
}
}
}
private static byte[] createTag(
int tagClass, boolean constructed, int tagNumber, byte[]... contents) {
if (tagNumber >= 0x1f) {
throw new IllegalArgumentException("High tag numbers not supported: " + tagNumber);
}
// tag class & number fit into the first byte
byte firstIdentifierByte =
(byte) ((tagClass << 6) | (constructed ? 1 << 5 : 0) | tagNumber);
int contentsLength = 0;
for (byte[] c : contents) {
contentsLength += c.length;
}
int contentsPosInResult;
byte[] result;
if (contentsLength < 0x80) {
// Length fits into one byte
contentsPosInResult = 2;
result = new byte[contentsPosInResult + contentsLength];
result[0] = firstIdentifierByte;
result[1] = (byte) contentsLength;
} else {
// Length is represented as multiple bytes
// The low 7 bits of the first byte represent the number of length bytes (following the
// first byte) in which the length is in big-endian base-256 form
if (contentsLength <= 0xff) {
contentsPosInResult = 3;
result = new byte[contentsPosInResult + contentsLength];
result[1] = (byte) 0x81; // 1 length byte
result[2] = (byte) contentsLength;
} else if (contentsLength <= 0xffff) {
contentsPosInResult = 4;
result = new byte[contentsPosInResult + contentsLength];
result[1] = (byte) 0x82; // 2 length bytes
result[2] = (byte) (contentsLength >> 8);
result[3] = (byte) (contentsLength & 0xff);
} else if (contentsLength <= 0xffffff) {
contentsPosInResult = 5;
result = new byte[contentsPosInResult + contentsLength];
result[1] = (byte) 0x83; // 3 length bytes
result[2] = (byte) (contentsLength >> 16);
result[3] = (byte) ((contentsLength >> 8) & 0xff);
result[4] = (byte) (contentsLength & 0xff);
} else {
contentsPosInResult = 6;
result = new byte[contentsPosInResult + contentsLength];
result[1] = (byte) 0x84; // 4 length bytes
result[2] = (byte) (contentsLength >> 24);
result[3] = (byte) ((contentsLength >> 16) & 0xff);
result[4] = (byte) ((contentsLength >> 8) & 0xff);
result[5] = (byte) (contentsLength & 0xff);
}
result[0] = firstIdentifierByte;
}
for (byte[] c : contents) {
System.arraycopy(c, 0, result, contentsPosInResult, c.length);
contentsPosInResult += c.length;
}
return result;
}
private static final class JavaToDerConverter {
private JavaToDerConverter() {}
public static byte[] toDer(Object source, Asn1Type targetType, Asn1Type targetElementType)
throws Asn1EncodingException {
Class<?> sourceType = source.getClass();
if (Asn1OpaqueObject.class.equals(sourceType)) {
ByteBuffer buf = ((Asn1OpaqueObject) source).getEncoded();
byte[] result = new byte[buf.remaining()];
buf.get(result);
return result;
}
if ((targetType == null) || (targetType == Asn1Type.ANY)) {
return encode(source);
}
switch (targetType) {
case OCTET_STRING:
case BIT_STRING:
byte[] value = null;
if (source instanceof ByteBuffer) {
ByteBuffer buf = (ByteBuffer) source;
value = new byte[buf.remaining()];
buf.slice().get(value);
} else if (source instanceof byte[]) {
value = (byte[]) source;
}
if (value != null) {
return createTag(
BerEncoding.TAG_CLASS_UNIVERSAL,
false,
BerEncoding.getTagNumber(targetType),
value);
}
break;
case INTEGER:
if (source instanceof Integer) {
return toInteger((Integer) source);
} else if (source instanceof Long) {
return toInteger((Long) source);
} else if (source instanceof BigInteger) {
return toInteger((BigInteger) source);
}
break;
case BOOLEAN:
if (source instanceof Boolean) {
return toBoolean((Boolean) (source));
}
break;
case UTC_TIME:
case GENERALIZED_TIME:
if (source instanceof String) {
return createTag(BerEncoding.TAG_CLASS_UNIVERSAL, false,
BerEncoding.getTagNumber(targetType), ((String) source).getBytes());
}
break;
case OBJECT_IDENTIFIER:
if (source instanceof String) {
return toOid((String) source);
}
break;
case SEQUENCE:
{
Asn1Class containerAnnotation =
sourceType.getDeclaredAnnotation(Asn1Class.class);
if ((containerAnnotation != null)
&& (containerAnnotation.type() == Asn1Type.SEQUENCE)) {
return toSequence(source);
}
break;
}
case CHOICE:
{
Asn1Class containerAnnotation =
sourceType.getDeclaredAnnotation(Asn1Class.class);
if ((containerAnnotation != null)
&& (containerAnnotation.type() == Asn1Type.CHOICE)) {
return toChoice(source);
}
break;
}
case SET_OF:
return toSetOf((Collection<?>) source, targetElementType);
case SEQUENCE_OF:
return toSequenceOf((Collection<?>) source, targetElementType);
default:
break;
}
throw new Asn1EncodingException(
"Unsupported conversion: " + sourceType.getName() + " to ASN.1 " + targetType);
}
}
/** ASN.1 DER-encoded {@code NULL}. */
public static final Asn1OpaqueObject ASN1_DER_NULL =
new Asn1OpaqueObject(new byte[] {BerEncoding.TAG_NUMBER_NULL, 0});
}

View File

@ -1,32 +0,0 @@
/*
* 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.internal.asn1;
/**
* Indicates that an ASN.1 structure could not be encoded.
*/
public class Asn1EncodingException extends Exception {
private static final long serialVersionUID = 1L;
public Asn1EncodingException(String message) {
super(message);
}
public Asn1EncodingException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -1,45 +0,0 @@
/*
* 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.internal.asn1;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Asn1Field {
/** Index used to order fields in a container. Required for fields of SEQUENCE containers. */
public int index() default 0;
public Asn1TagClass cls() default Asn1TagClass.AUTOMATIC;
public Asn1Type type();
/** Tagging mode. Default: NORMAL. */
public Asn1Tagging tagging() default Asn1Tagging.NORMAL;
/** Tag number. Required when IMPLICIT and EXPLICIT tagging mode is used.*/
public int tagNumber() default -1;
/** {@code true} if this field is optional. Ignored for fields of CHOICE containers. */
public boolean optional() default false;
/** Type of elements. Used only for SET_OF or SEQUENCE_OF. */
public Asn1Type elementType() default Asn1Type.ANY;
}

View File

@ -1,38 +0,0 @@
/*
* 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.internal.asn1;
import java.nio.ByteBuffer;
/**
* Opaque holder of encoded ASN.1 stuff.
*/
public class Asn1OpaqueObject {
private final ByteBuffer mEncoded;
public Asn1OpaqueObject(ByteBuffer encoded) {
mEncoded = encoded.slice();
}
public Asn1OpaqueObject(byte[] encoded) {
mEncoded = ByteBuffer.wrap(encoded);
}
public ByteBuffer getEncoded() {
return mEncoded.slice();
}
}

View File

@ -1,30 +0,0 @@
/*
* 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.internal.asn1;
public enum Asn1TagClass {
UNIVERSAL,
APPLICATION,
CONTEXT_SPECIFIC,
PRIVATE,
/**
* Not really an actual tag class: decoder/encoder will attempt to deduce the correct tag class
* automatically.
*/
AUTOMATIC,
}

View File

@ -1,23 +0,0 @@
/*
* 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.internal.asn1;
public enum Asn1Tagging {
NORMAL,
EXPLICIT,
IMPLICIT,
}

View File

@ -1,35 +0,0 @@
/*
* 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.internal.asn1;
public enum Asn1Type {
ANY,
CHOICE,
INTEGER,
OBJECT_IDENTIFIER,
OCTET_STRING,
SEQUENCE,
SEQUENCE_OF,
SET_OF,
BIT_STRING,
UTC_TIME,
GENERALIZED_TIME,
BOOLEAN,
// This type can be used to annotate classes that encapsulate ASN.1 structures that are not
// classified as a SEQUENCE or SET.
UNENCODED_CONTAINER
}

View File

@ -1,115 +0,0 @@
/*
* 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.internal.asn1.ber;
import java.nio.ByteBuffer;
/**
* ASN.1 Basic Encoding Rules (BER) data value -- see {@code X.690}.
*/
public class BerDataValue {
private final ByteBuffer mEncoded;
private final ByteBuffer mEncodedContents;
private final int mTagClass;
private final boolean mConstructed;
private final int mTagNumber;
BerDataValue(
ByteBuffer encoded,
ByteBuffer encodedContents,
int tagClass,
boolean constructed,
int tagNumber) {
mEncoded = encoded;
mEncodedContents = encodedContents;
mTagClass = tagClass;
mConstructed = constructed;
mTagNumber = tagNumber;
}
/**
* Returns the tag class of this data value. See {@link BerEncoding} {@code TAG_CLASS}
* constants.
*/
public int getTagClass() {
return mTagClass;
}
/**
* Returns {@code true} if the content octets of this data value are the complete BER encoding
* of one or more data values, {@code false} if the content octets of this data value directly
* represent the value.
*/
public boolean isConstructed() {
return mConstructed;
}
/**
* Returns the tag number of this data value. See {@link BerEncoding} {@code TAG_NUMBER}
* constants.
*/
public int getTagNumber() {
return mTagNumber;
}
/**
* Returns the encoded form of this data value.
*/
public ByteBuffer getEncoded() {
return mEncoded.slice();
}
/**
* Returns the encoded contents of this data value.
*/
public ByteBuffer getEncodedContents() {
return mEncodedContents.slice();
}
/**
* Returns a new reader of the contents of this data value.
*/
public BerDataValueReader contentsReader() {
return new ByteBufferBerDataValueReader(getEncodedContents());
}
/**
* Returns a new reader which returns just this data value. This may be useful for re-reading
* this value in different contexts.
*/
public BerDataValueReader dataValueReader() {
return new ParsedValueReader(this);
}
private static final class ParsedValueReader implements BerDataValueReader {
private final BerDataValue mValue;
private boolean mValueOutput;
public ParsedValueReader(BerDataValue value) {
mValue = value;
}
@Override
public BerDataValue readDataValue() throws BerDataValueFormatException {
if (mValueOutput) {
return null;
}
mValueOutput = true;
return mValue;
}
}
}

View File

@ -1,34 +0,0 @@
/*
* 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.internal.asn1.ber;
/**
* Indicates that an ASN.1 data value being read could not be decoded using
* Basic Encoding Rules (BER).
*/
public class BerDataValueFormatException extends Exception {
private static final long serialVersionUID = 1L;
public BerDataValueFormatException(String message) {
super(message);
}
public BerDataValueFormatException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -1,34 +0,0 @@
/*
* 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.internal.asn1.ber;
/**
* Reader of ASN.1 Basic Encoding Rules (BER) data values.
*
* <p>BER data value reader returns data values, one by one, from a source. The interpretation of
* data values (e.g., how to obtain a numeric value from an INTEGER data value, or how to extract
* the elements of a SEQUENCE value) is left to clients of the reader.
*/
public interface BerDataValueReader {
/**
* Returns the next data value or {@code null} if end of input has been reached.
*
* @throws BerDataValueFormatException if the value being read is malformed.
*/
BerDataValue readDataValue() throws BerDataValueFormatException;
}

View File

@ -1,225 +0,0 @@
/*
* 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.internal.asn1.ber;
import com.android.apksig.internal.asn1.Asn1Type;
import com.android.apksig.internal.asn1.Asn1TagClass;
/**
* ASN.1 Basic Encoding Rules (BER) constants and helper methods. See {@code X.690}.
*/
public abstract class BerEncoding {
private BerEncoding() {}
/**
* Constructed vs primitive flag in the first identifier byte.
*/
public static final int ID_FLAG_CONSTRUCTED_ENCODING = 1 << 5;
/**
* Tag class: UNIVERSAL
*/
public static final int TAG_CLASS_UNIVERSAL = 0;
/**
* Tag class: APPLICATION
*/
public static final int TAG_CLASS_APPLICATION = 1;
/**
* Tag class: CONTEXT SPECIFIC
*/
public static final int TAG_CLASS_CONTEXT_SPECIFIC = 2;
/**
* Tag class: PRIVATE
*/
public static final int TAG_CLASS_PRIVATE = 3;
/**
* Tag number: BOOLEAN
*/
public static final int TAG_NUMBER_BOOLEAN = 0x1;
/**
* Tag number: INTEGER
*/
public static final int TAG_NUMBER_INTEGER = 0x2;
/**
* Tag number: BIT STRING
*/
public static final int TAG_NUMBER_BIT_STRING = 0x3;
/**
* Tag number: OCTET STRING
*/
public static final int TAG_NUMBER_OCTET_STRING = 0x4;
/**
* Tag number: NULL
*/
public static final int TAG_NUMBER_NULL = 0x05;
/**
* Tag number: OBJECT IDENTIFIER
*/
public static final int TAG_NUMBER_OBJECT_IDENTIFIER = 0x6;
/**
* Tag number: SEQUENCE
*/
public static final int TAG_NUMBER_SEQUENCE = 0x10;
/**
* Tag number: SET
*/
public static final int TAG_NUMBER_SET = 0x11;
/**
* Tag number: UTC_TIME
*/
public final static int TAG_NUMBER_UTC_TIME = 0x17;
/**
* Tag number: GENERALIZED_TIME
*/
public final static int TAG_NUMBER_GENERALIZED_TIME = 0x18;
public static int getTagNumber(Asn1Type dataType) {
switch (dataType) {
case INTEGER:
return TAG_NUMBER_INTEGER;
case OBJECT_IDENTIFIER:
return TAG_NUMBER_OBJECT_IDENTIFIER;
case OCTET_STRING:
return TAG_NUMBER_OCTET_STRING;
case BIT_STRING:
return TAG_NUMBER_BIT_STRING;
case SET_OF:
return TAG_NUMBER_SET;
case SEQUENCE:
case SEQUENCE_OF:
return TAG_NUMBER_SEQUENCE;
case UTC_TIME:
return TAG_NUMBER_UTC_TIME;
case GENERALIZED_TIME:
return TAG_NUMBER_GENERALIZED_TIME;
case BOOLEAN:
return TAG_NUMBER_BOOLEAN;
default:
throw new IllegalArgumentException("Unsupported data type: " + dataType);
}
}
public static int getTagClass(Asn1TagClass tagClass) {
switch (tagClass) {
case APPLICATION:
return TAG_CLASS_APPLICATION;
case CONTEXT_SPECIFIC:
return TAG_CLASS_CONTEXT_SPECIFIC;
case PRIVATE:
return TAG_CLASS_PRIVATE;
case UNIVERSAL:
return TAG_CLASS_UNIVERSAL;
default:
throw new IllegalArgumentException("Unsupported tag class: " + tagClass);
}
}
public static String tagClassToString(int typeClass) {
switch (typeClass) {
case TAG_CLASS_APPLICATION:
return "APPLICATION";
case TAG_CLASS_CONTEXT_SPECIFIC:
return "";
case TAG_CLASS_PRIVATE:
return "PRIVATE";
case TAG_CLASS_UNIVERSAL:
return "UNIVERSAL";
default:
throw new IllegalArgumentException("Unsupported type class: " + typeClass);
}
}
public static String tagClassAndNumberToString(int tagClass, int tagNumber) {
String classString = tagClassToString(tagClass);
String numberString = tagNumberToString(tagNumber);
return classString.isEmpty() ? numberString : classString + " " + numberString;
}
public static String tagNumberToString(int tagNumber) {
switch (tagNumber) {
case TAG_NUMBER_INTEGER:
return "INTEGER";
case TAG_NUMBER_OCTET_STRING:
return "OCTET STRING";
case TAG_NUMBER_BIT_STRING:
return "BIT STRING";
case TAG_NUMBER_NULL:
return "NULL";
case TAG_NUMBER_OBJECT_IDENTIFIER:
return "OBJECT IDENTIFIER";
case TAG_NUMBER_SEQUENCE:
return "SEQUENCE";
case TAG_NUMBER_SET:
return "SET";
case TAG_NUMBER_BOOLEAN:
return "BOOLEAN";
case TAG_NUMBER_GENERALIZED_TIME:
return "GENERALIZED TIME";
case TAG_NUMBER_UTC_TIME:
return "UTC TIME";
default:
return "0x" + Integer.toHexString(tagNumber);
}
}
/**
* Returns {@code true} if the provided first identifier byte indicates that the data value uses
* constructed encoding for its contents, or {@code false} if the data value uses primitive
* encoding for its contents.
*/
public static boolean isConstructed(byte firstIdentifierByte) {
return (firstIdentifierByte & ID_FLAG_CONSTRUCTED_ENCODING) != 0;
}
/**
* Returns the tag class encoded in the provided first identifier byte. See {@code TAG_CLASS}
* constants.
*/
public static int getTagClass(byte firstIdentifierByte) {
return (firstIdentifierByte & 0xff) >> 6;
}
public static byte setTagClass(byte firstIdentifierByte, int tagClass) {
return (byte) ((firstIdentifierByte & 0x3f) | (tagClass << 6));
}
/**
* Returns the tag number encoded in the provided first identifier byte. See {@code TAG_NUMBER}
* constants.
*/
public static int getTagNumber(byte firstIdentifierByte) {
return firstIdentifierByte & 0x1f;
}
public static byte setTagNumber(byte firstIdentifierByte, int tagNumber) {
return (byte) ((firstIdentifierByte & ~0x1f) | tagNumber);
}
}

View File

@ -1,208 +0,0 @@
/*
* 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.internal.asn1.ber;
import java.nio.ByteBuffer;
/**
* {@link BerDataValueReader} which reads from a {@link ByteBuffer} containing BER-encoded data
* values. See {@code X.690} for the encoding.
*/
public class ByteBufferBerDataValueReader implements BerDataValueReader {
private final ByteBuffer mBuf;
public ByteBufferBerDataValueReader(ByteBuffer buf) {
if (buf == null) {
throw new NullPointerException("buf == null");
}
mBuf = buf;
}
@Override
public BerDataValue readDataValue() throws BerDataValueFormatException {
int startPosition = mBuf.position();
if (!mBuf.hasRemaining()) {
return null;
}
byte firstIdentifierByte = mBuf.get();
int tagNumber = readTagNumber(firstIdentifierByte);
boolean constructed = BerEncoding.isConstructed(firstIdentifierByte);
if (!mBuf.hasRemaining()) {
throw new BerDataValueFormatException("Missing length");
}
int firstLengthByte = mBuf.get() & 0xff;
int contentsLength;
int contentsOffsetInTag;
if ((firstLengthByte & 0x80) == 0) {
// short form length
contentsLength = readShortFormLength(firstLengthByte);
contentsOffsetInTag = mBuf.position() - startPosition;
skipDefiniteLengthContents(contentsLength);
} else if (firstLengthByte != 0x80) {
// long form length
contentsLength = readLongFormLength(firstLengthByte);
contentsOffsetInTag = mBuf.position() - startPosition;
skipDefiniteLengthContents(contentsLength);
} else {
// indefinite length -- value ends with 0x00 0x00
contentsOffsetInTag = mBuf.position() - startPosition;
contentsLength =
constructed
? skipConstructedIndefiniteLengthContents()
: skipPrimitiveIndefiniteLengthContents();
}
// Create the encoded data value ByteBuffer
int endPosition = mBuf.position();
mBuf.position(startPosition);
int bufOriginalLimit = mBuf.limit();
mBuf.limit(endPosition);
ByteBuffer encoded = mBuf.slice();
mBuf.position(mBuf.limit());
mBuf.limit(bufOriginalLimit);
// Create the encoded contents ByteBuffer
encoded.position(contentsOffsetInTag);
encoded.limit(contentsOffsetInTag + contentsLength);
ByteBuffer encodedContents = encoded.slice();
encoded.clear();
return new BerDataValue(
encoded,
encodedContents,
BerEncoding.getTagClass(firstIdentifierByte),
constructed,
tagNumber);
}
private int readTagNumber(byte firstIdentifierByte) throws BerDataValueFormatException {
int tagNumber = BerEncoding.getTagNumber(firstIdentifierByte);
if (tagNumber == 0x1f) {
// high-tag-number form, where the tag number follows this byte in base-128
// big-endian form, where each byte has the highest bit set, except for the last
// byte
return readHighTagNumber();
} else {
// low-tag-number form
return tagNumber;
}
}
private int readHighTagNumber() throws BerDataValueFormatException {
// Base-128 big-endian form, where each byte has the highest bit set, except for the last
// byte
int b;
int result = 0;
do {
if (!mBuf.hasRemaining()) {
throw new BerDataValueFormatException("Truncated tag number");
}
b = mBuf.get();
if (result > Integer.MAX_VALUE >>> 7) {
throw new BerDataValueFormatException("Tag number too large");
}
result <<= 7;
result |= b & 0x7f;
} while ((b & 0x80) != 0);
return result;
}
private int readShortFormLength(int firstLengthByte) {
return firstLengthByte & 0x7f;
}
private int readLongFormLength(int firstLengthByte) throws BerDataValueFormatException {
// The low 7 bits of the first byte represent the number of bytes (following the first
// byte) in which the length is in big-endian base-256 form
int byteCount = firstLengthByte & 0x7f;
if (byteCount > 4) {
throw new BerDataValueFormatException("Length too large: " + byteCount + " bytes");
}
int result = 0;
for (int i = 0; i < byteCount; i++) {
if (!mBuf.hasRemaining()) {
throw new BerDataValueFormatException("Truncated length");
}
int b = mBuf.get();
if (result > Integer.MAX_VALUE >>> 8) {
throw new BerDataValueFormatException("Length too large");
}
result <<= 8;
result |= b & 0xff;
}
return result;
}
private void skipDefiniteLengthContents(int contentsLength) throws BerDataValueFormatException {
if (mBuf.remaining() < contentsLength) {
throw new BerDataValueFormatException(
"Truncated contents. Need: " + contentsLength + " bytes, available: "
+ mBuf.remaining());
}
mBuf.position(mBuf.position() + contentsLength);
}
private int skipPrimitiveIndefiniteLengthContents() throws BerDataValueFormatException {
// Contents are terminated by 0x00 0x00
boolean prevZeroByte = false;
int bytesRead = 0;
while (true) {
if (!mBuf.hasRemaining()) {
throw new BerDataValueFormatException(
"Truncated indefinite-length contents: " + bytesRead + " bytes read");
}
int b = mBuf.get();
bytesRead++;
if (bytesRead < 0) {
throw new BerDataValueFormatException("Indefinite-length contents too long");
}
if (b == 0) {
if (prevZeroByte) {
// End of contents reached -- we've read the value and its terminator 0x00 0x00
return bytesRead - 2;
}
prevZeroByte = true;
} else {
prevZeroByte = false;
}
}
}
private int skipConstructedIndefiniteLengthContents() throws BerDataValueFormatException {
// Contents are terminated by 0x00 0x00. However, this data value is constructed, meaning it
// can contain data values which are themselves indefinite length encoded. As a result, we
// must parse the direct children of this data value to correctly skip over the contents of
// this data value.
int startPos = mBuf.position();
while (mBuf.hasRemaining()) {
// Check whether the 0x00 0x00 terminator is at current position
if ((mBuf.remaining() > 1) && (mBuf.getShort(mBuf.position()) == 0)) {
int contentsLength = mBuf.position() - startPos;
mBuf.position(mBuf.position() + 2);
return contentsLength;
}
// No luck. This must be a BER-encoded data value -- skip over it by parsing it
readDataValue();
}
throw new BerDataValueFormatException(
"Truncated indefinite-length contents: "
+ (mBuf.position() - startPos) + " bytes read");
}
}

View File

@ -1,313 +0,0 @@
/*
* 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.internal.asn1.ber;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
/**
* {@link BerDataValueReader} which reads from an {@link InputStream} returning BER-encoded data
* values. See {@code X.690} for the encoding.
*/
public class InputStreamBerDataValueReader implements BerDataValueReader {
private final InputStream mIn;
public InputStreamBerDataValueReader(InputStream in) {
if (in == null) {
throw new NullPointerException("in == null");
}
mIn = in;
}
@Override
public BerDataValue readDataValue() throws BerDataValueFormatException {
return readDataValue(mIn);
}
/**
* Returns the next data value or {@code null} if end of input has been reached.
*
* @throws BerDataValueFormatException if the value being read is malformed.
*/
@SuppressWarnings("resource")
private static BerDataValue readDataValue(InputStream input)
throws BerDataValueFormatException {
RecordingInputStream in = new RecordingInputStream(input);
try {
int firstIdentifierByte = in.read();
if (firstIdentifierByte == -1) {
// End of input
return null;
}
int tagNumber = readTagNumber(in, firstIdentifierByte);
int firstLengthByte = in.read();
if (firstLengthByte == -1) {
throw new BerDataValueFormatException("Missing length");
}
boolean constructed = BerEncoding.isConstructed((byte) firstIdentifierByte);
int contentsLength;
int contentsOffsetInDataValue;
if ((firstLengthByte & 0x80) == 0) {
// short form length
contentsLength = readShortFormLength(firstLengthByte);
contentsOffsetInDataValue = in.getReadByteCount();
skipDefiniteLengthContents(in, contentsLength);
} else if ((firstLengthByte & 0xff) != 0x80) {
// long form length
contentsLength = readLongFormLength(in, firstLengthByte);
contentsOffsetInDataValue = in.getReadByteCount();
skipDefiniteLengthContents(in, contentsLength);
} else {
// indefinite length
contentsOffsetInDataValue = in.getReadByteCount();
contentsLength =
constructed
? skipConstructedIndefiniteLengthContents(in)
: skipPrimitiveIndefiniteLengthContents(in);
}
byte[] encoded = in.getReadBytes();
ByteBuffer encodedContents =
ByteBuffer.wrap(encoded, contentsOffsetInDataValue, contentsLength);
return new BerDataValue(
ByteBuffer.wrap(encoded),
encodedContents,
BerEncoding.getTagClass((byte) firstIdentifierByte),
constructed,
tagNumber);
} catch (IOException e) {
throw new BerDataValueFormatException("Failed to read data value", e);
}
}
private static int readTagNumber(InputStream in, int firstIdentifierByte)
throws IOException, BerDataValueFormatException {
int tagNumber = BerEncoding.getTagNumber((byte) firstIdentifierByte);
if (tagNumber == 0x1f) {
// high-tag-number form
return readHighTagNumber(in);
} else {
// low-tag-number form
return tagNumber;
}
}
private static int readHighTagNumber(InputStream in)
throws IOException, BerDataValueFormatException {
// Base-128 big-endian form, where each byte has the highest bit set, except for the last
// byte where the highest bit is not set
int b;
int result = 0;
do {
b = in.read();
if (b == -1) {
throw new BerDataValueFormatException("Truncated tag number");
}
if (result > Integer.MAX_VALUE >>> 7) {
throw new BerDataValueFormatException("Tag number too large");
}
result <<= 7;
result |= b & 0x7f;
} while ((b & 0x80) != 0);
return result;
}
private static int readShortFormLength(int firstLengthByte) {
return firstLengthByte & 0x7f;
}
private static int readLongFormLength(InputStream in, int firstLengthByte)
throws IOException, BerDataValueFormatException {
// The low 7 bits of the first byte represent the number of bytes (following the first
// byte) in which the length is in big-endian base-256 form
int byteCount = firstLengthByte & 0x7f;
if (byteCount > 4) {
throw new BerDataValueFormatException("Length too large: " + byteCount + " bytes");
}
int result = 0;
for (int i = 0; i < byteCount; i++) {
int b = in.read();
if (b == -1) {
throw new BerDataValueFormatException("Truncated length");
}
if (result > Integer.MAX_VALUE >>> 8) {
throw new BerDataValueFormatException("Length too large");
}
result <<= 8;
result |= b & 0xff;
}
return result;
}
private static void skipDefiniteLengthContents(InputStream in, int len)
throws IOException, BerDataValueFormatException {
long bytesRead = 0;
while (len > 0) {
int skipped = (int) in.skip(len);
if (skipped <= 0) {
throw new BerDataValueFormatException(
"Truncated definite-length contents: " + bytesRead + " bytes read"
+ ", " + len + " missing");
}
len -= skipped;
bytesRead += skipped;
}
}
private static int skipPrimitiveIndefiniteLengthContents(InputStream in)
throws IOException, BerDataValueFormatException {
// Contents are terminated by 0x00 0x00
boolean prevZeroByte = false;
int bytesRead = 0;
while (true) {
int b = in.read();
if (b == -1) {
throw new BerDataValueFormatException(
"Truncated indefinite-length contents: " + bytesRead + " bytes read");
}
bytesRead++;
if (bytesRead < 0) {
throw new BerDataValueFormatException("Indefinite-length contents too long");
}
if (b == 0) {
if (prevZeroByte) {
// End of contents reached -- we've read the value and its terminator 0x00 0x00
return bytesRead - 2;
}
prevZeroByte = true;
continue;
} else {
prevZeroByte = false;
}
}
}
private static int skipConstructedIndefiniteLengthContents(RecordingInputStream in)
throws BerDataValueFormatException {
// Contents are terminated by 0x00 0x00. However, this data value is constructed, meaning it
// can contain data values which are indefinite length encoded as well. As a result, we
// must parse the direct children of this data value to correctly skip over the contents of
// this data value.
int readByteCountBefore = in.getReadByteCount();
while (true) {
// We can't easily peek for the 0x00 0x00 terminator using the provided InputStream.
// Thus, we use the fact that 0x00 0x00 parses as a data value whose encoded form we
// then check below to see whether it's 0x00 0x00.
BerDataValue dataValue = readDataValue(in);
if (dataValue == null) {
throw new BerDataValueFormatException(
"Truncated indefinite-length contents: "
+ (in.getReadByteCount() - readByteCountBefore) + " bytes read");
}
if (in.getReadByteCount() <= 0) {
throw new BerDataValueFormatException("Indefinite-length contents too long");
}
ByteBuffer encoded = dataValue.getEncoded();
if ((encoded.remaining() == 2) && (encoded.get(0) == 0) && (encoded.get(1) == 0)) {
// 0x00 0x00 encountered
return in.getReadByteCount() - readByteCountBefore - 2;
}
}
}
private static class RecordingInputStream extends InputStream {
private final InputStream mIn;
private final ByteArrayOutputStream mBuf;
private RecordingInputStream(InputStream in) {
mIn = in;
mBuf = new ByteArrayOutputStream();
}
public byte[] getReadBytes() {
return mBuf.toByteArray();
}
public int getReadByteCount() {
return mBuf.size();
}
@Override
public int read() throws IOException {
int b = mIn.read();
if (b != -1) {
mBuf.write(b);
}
return b;
}
@Override
public int read(byte[] b) throws IOException {
int len = mIn.read(b);
if (len > 0) {
mBuf.write(b, 0, len);
}
return len;
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
len = mIn.read(b, off, len);
if (len > 0) {
mBuf.write(b, off, len);
}
return len;
}
@Override
public long skip(long n) throws IOException {
if (n <= 0) {
return mIn.skip(n);
}
byte[] buf = new byte[4096];
int len = mIn.read(buf, 0, (int) Math.min(buf.length, n));
if (len > 0) {
mBuf.write(buf, 0, len);
}
return (len < 0) ? 0 : len;
}
@Override
public int available() throws IOException {
return super.available();
}
@Override
public void close() throws IOException {
super.close();
}
@Override
public synchronized void mark(int readlimit) {}
@Override
public synchronized void reset() throws IOException {
throw new IOException("mark/reset not supported");
}
@Override
public boolean markSupported() {
return false;
}
}
}

View File

@ -1,363 +0,0 @@
/*
* 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.jar;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.jar.Attributes;
/**
* JAR manifest and signature file parser.
*
* <p>These files consist of a main section followed by individual sections. Individual sections
* are named, their names referring to JAR entries.
*
* @see <a href="https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#JAR_Manifest">JAR Manifest format</a>
*/
public class ManifestParser {
private final byte[] mManifest;
private int mOffset;
private int mEndOffset;
private byte[] mBufferedLine;
/**
* Constructs a new {@code ManifestParser} with the provided input.
*/
public ManifestParser(byte[] data) {
this(data, 0, data.length);
}
/**
* Constructs a new {@code ManifestParser} with the provided input.
*/
public ManifestParser(byte[] data, int offset, int length) {
mManifest = data;
mOffset = offset;
mEndOffset = offset + length;
}
/**
* Returns the remaining sections of this file.
*/
public List<Section> readAllSections() {
List<Section> sections = new ArrayList<>();
Section section;
while ((section = readSection()) != null) {
sections.add(section);
}
return sections;
}
/**
* Returns the next section from this file or {@code null} if end of file has been reached.
*/
public Section readSection() {
// Locate the first non-empty line
int sectionStartOffset;
String attr;
do {
sectionStartOffset = mOffset;
attr = readAttribute();
if (attr == null) {
return null;
}
} while (attr.length() == 0);
List<Attribute> attrs = new ArrayList<>();
attrs.add(parseAttr(attr));
// Read attributes until end of section reached
while (true) {
attr = readAttribute();
if ((attr == null) || (attr.length() == 0)) {
// End of section
break;
}
attrs.add(parseAttr(attr));
}
int sectionEndOffset = mOffset;
int sectionSizeBytes = sectionEndOffset - sectionStartOffset;
return new Section(sectionStartOffset, sectionSizeBytes, attrs);
}
private static Attribute parseAttr(String attr) {
// Name is separated from value by a semicolon followed by a single SPACE character.
// This permits trailing spaces in names and leading and trailing spaces in values.
// Some APK obfuscators take advantage of this fact. We thus need to preserve these unusual
// spaces to be able to parse such obfuscated APKs.
int delimiterIndex = attr.indexOf(": ");
if (delimiterIndex == -1) {
return new Attribute(attr, "");
} else {
return new Attribute(
attr.substring(0, delimiterIndex),
attr.substring(delimiterIndex + ": ".length()));
}
}
/**
* Returns the next attribute or empty {@code String} if end of section has been reached or
* {@code null} if end of input has been reached.
*/
private String readAttribute() {
byte[] bytes = readAttributeBytes();
if (bytes == null) {
return null;
} else if (bytes.length == 0) {
return "";
} else {
return new String(bytes, StandardCharsets.UTF_8);
}
}
/**
* Returns the next attribute or empty array if end of section has been reached or {@code null}
* if end of input has been reached.
*/
private byte[] readAttributeBytes() {
// Check whether end of section was reached during previous invocation
if ((mBufferedLine != null) && (mBufferedLine.length == 0)) {
mBufferedLine = null;
return EMPTY_BYTE_ARRAY;
}
// Read the next line
byte[] line = readLine();
if (line == null) {
// End of input
if (mBufferedLine != null) {
byte[] result = mBufferedLine;
mBufferedLine = null;
return result;
}
return null;
}
// Consume the read line
if (line.length == 0) {
// End of section
if (mBufferedLine != null) {
byte[] result = mBufferedLine;
mBufferedLine = EMPTY_BYTE_ARRAY;
return result;
}
return EMPTY_BYTE_ARRAY;
}
byte[] attrLine;
if (mBufferedLine == null) {
attrLine = line;
} else {
if ((line.length == 0) || (line[0] != ' ')) {
// The most common case: buffered line is a full attribute
byte[] result = mBufferedLine;
mBufferedLine = line;
return result;
}
attrLine = mBufferedLine;
mBufferedLine = null;
attrLine = concat(attrLine, line, 1, line.length - 1);
}
// Everything's buffered in attrLine now. mBufferedLine is null
// Read more lines
while (true) {
line = readLine();
if (line == null) {
// End of input
return attrLine;
} else if (line.length == 0) {
// End of section
mBufferedLine = EMPTY_BYTE_ARRAY; // return "end of section" next time
return attrLine;
}
if (line[0] == ' ') {
// Continuation line
attrLine = concat(attrLine, line, 1, line.length - 1);
} else {
// Next attribute
mBufferedLine = line;
return attrLine;
}
}
}
private static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
private static byte[] concat(byte[] arr1, byte[] arr2, int offset2, int length2) {
byte[] result = new byte[arr1.length + length2];
System.arraycopy(arr1, 0, result, 0, arr1.length);
System.arraycopy(arr2, offset2, result, arr1.length, length2);
return result;
}
/**
* Returns the next line (without line delimiter characters) or {@code null} if end of input has
* been reached.
*/
private byte[] readLine() {
if (mOffset >= mEndOffset) {
return null;
}
int startOffset = mOffset;
int newlineStartOffset = -1;
int newlineEndOffset = -1;
for (int i = startOffset; i < mEndOffset; i++) {
byte b = mManifest[i];
if (b == '\r') {
newlineStartOffset = i;
int nextIndex = i + 1;
if ((nextIndex < mEndOffset) && (mManifest[nextIndex] == '\n')) {
newlineEndOffset = nextIndex + 1;
break;
}
newlineEndOffset = nextIndex;
break;
} else if (b == '\n') {
newlineStartOffset = i;
newlineEndOffset = i + 1;
break;
}
}
if (newlineStartOffset == -1) {
newlineStartOffset = mEndOffset;
newlineEndOffset = mEndOffset;
}
mOffset = newlineEndOffset;
if (newlineStartOffset == startOffset) {
return EMPTY_BYTE_ARRAY;
}
return Arrays.copyOfRange(mManifest, startOffset, newlineStartOffset);
}
/**
* Attribute.
*/
public static class Attribute {
private final String mName;
private final String mValue;
/**
* Constructs a new {@code Attribute} with the provided name and value.
*/
public Attribute(String name, String value) {
mName = name;
mValue = value;
}
/**
* Returns this attribute's name.
*/
public String getName() {
return mName;
}
/**
* Returns this attribute's value.
*/
public String getValue() {
return mValue;
}
}
/**
* Section.
*/
public static class Section {
private final int mStartOffset;
private final int mSizeBytes;
private final String mName;
private final List<Attribute> mAttributes;
/**
* Constructs a new {@code Section}.
*
* @param startOffset start offset (in bytes) of the section in the input file
* @param sizeBytes size (in bytes) of the section in the input file
* @param attrs attributes contained in the section
*/
public Section(int startOffset, int sizeBytes, List<Attribute> attrs) {
mStartOffset = startOffset;
mSizeBytes = sizeBytes;
String sectionName = null;
if (!attrs.isEmpty()) {
Attribute firstAttr = attrs.get(0);
if ("Name".equalsIgnoreCase(firstAttr.getName())) {
sectionName = firstAttr.getValue();
}
}
mName = sectionName;
mAttributes = Collections.unmodifiableList(new ArrayList<>(attrs));
}
public String getName() {
return mName;
}
/**
* Returns the offset (in bytes) at which this section starts in the input.
*/
public int getStartOffset() {
return mStartOffset;
}
/**
* Returns the size (in bytes) of this section in the input.
*/
public int getSizeBytes() {
return mSizeBytes;
}
/**
* Returns this section's attributes, in the order in which they appear in the input.
*/
public List<Attribute> getAttributes() {
return mAttributes;
}
/**
* Returns the value of the specified attribute in this section or {@code null} if this
* section does not contain a matching attribute.
*/
public String getAttributeValue(Attributes.Name name) {
return getAttributeValue(name.toString());
}
/**
* Returns the value of the specified attribute in this section or {@code null} if this
* section does not contain a matching attribute.
*
* @param name name of the attribute. Attribute names are case-insensitive.
*/
public String getAttributeValue(String name) {
for (Attribute attr : mAttributes) {
if (attr.getName().equalsIgnoreCase(name)) {
return attr.getValue();
}
}
return null;
}
}
}

View File

@ -1,127 +0,0 @@
/*
* 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.jar;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.jar.Attributes;
/**
* Producer of {@code META-INF/MANIFEST.MF} file.
*
* @see <a href="https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#JAR_Manifest">JAR Manifest format</a>
*/
public abstract class ManifestWriter {
private static final byte[] CRLF = new byte[] {'\r', '\n'};
private static final int MAX_LINE_LENGTH = 70;
private ManifestWriter() {}
public static void writeMainSection(OutputStream out, Attributes attributes)
throws IOException {
// Main section must start with the Manifest-Version attribute.
// See https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File.
String manifestVersion = attributes.getValue(Attributes.Name.MANIFEST_VERSION);
if (manifestVersion == null) {
throw new IllegalArgumentException(
"Mandatory " + Attributes.Name.MANIFEST_VERSION + " attribute missing");
}
writeAttribute(out, Attributes.Name.MANIFEST_VERSION, manifestVersion);
if (attributes.size() > 1) {
SortedMap<String, String> namedAttributes = getAttributesSortedByName(attributes);
namedAttributes.remove(Attributes.Name.MANIFEST_VERSION.toString());
writeAttributes(out, namedAttributes);
}
writeSectionDelimiter(out);
}
public static void writeIndividualSection(OutputStream out, String name, Attributes attributes)
throws IOException {
writeAttribute(out, "Name", name);
if (!attributes.isEmpty()) {
writeAttributes(out, getAttributesSortedByName(attributes));
}
writeSectionDelimiter(out);
}
static void writeSectionDelimiter(OutputStream out) throws IOException {
out.write(CRLF);
}
static void writeAttribute(OutputStream out, Attributes.Name name, String value)
throws IOException {
writeAttribute(out, name.toString(), value);
}
private static void writeAttribute(OutputStream out, String name, String value)
throws IOException {
writeLine(out, name + ": " + value);
}
private static void writeLine(OutputStream out, String line) throws IOException {
byte[] lineBytes = line.getBytes(StandardCharsets.UTF_8);
int offset = 0;
int remaining = lineBytes.length;
boolean firstLine = true;
while (remaining > 0) {
int chunkLength;
if (firstLine) {
// First line
chunkLength = Math.min(remaining, MAX_LINE_LENGTH);
} else {
// Continuation line
out.write(CRLF);
out.write(' ');
chunkLength = Math.min(remaining, MAX_LINE_LENGTH - 1);
}
out.write(lineBytes, offset, chunkLength);
offset += chunkLength;
remaining -= chunkLength;
firstLine = false;
}
out.write(CRLF);
}
static SortedMap<String, String> getAttributesSortedByName(Attributes attributes) {
Set<Map.Entry<Object, Object>> attributesEntries = attributes.entrySet();
SortedMap<String, String> namedAttributes = new TreeMap<String, String>();
for (Map.Entry<Object, Object> attribute : attributesEntries) {
String attrName = attribute.getKey().toString();
String attrValue = attribute.getValue().toString();
namedAttributes.put(attrName, attrValue);
}
return namedAttributes;
}
static void writeAttributes(
OutputStream out, SortedMap<String, String> attributesSortedByName) throws IOException {
for (Map.Entry<String, String> attribute : attributesSortedByName.entrySet()) {
String attrName = attribute.getKey();
String attrValue = attribute.getValue();
writeAttribute(out, attrName, attrValue);
}
}
}

View File

@ -1,61 +0,0 @@
/*
* 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.jar;
import java.io.IOException;
import java.io.OutputStream;
import java.util.SortedMap;
import java.util.jar.Attributes;
/**
* Producer of JAR signature file ({@code *.SF}).
*
* @see <a href="https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#JAR_Manifest">JAR Manifest format</a>
*/
public abstract class SignatureFileWriter {
private SignatureFileWriter() {}
public static void writeMainSection(OutputStream out, Attributes attributes)
throws IOException {
// Main section must start with the Signature-Version attribute.
// See https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File.
String signatureVersion = attributes.getValue(Attributes.Name.SIGNATURE_VERSION);
if (signatureVersion == null) {
throw new IllegalArgumentException(
"Mandatory " + Attributes.Name.SIGNATURE_VERSION + " attribute missing");
}
ManifestWriter.writeAttribute(out, Attributes.Name.SIGNATURE_VERSION, signatureVersion);
if (attributes.size() > 1) {
SortedMap<String, String> namedAttributes =
ManifestWriter.getAttributesSortedByName(attributes);
namedAttributes.remove(Attributes.Name.SIGNATURE_VERSION.toString());
ManifestWriter.writeAttributes(out, namedAttributes);
}
writeSectionDelimiter(out);
}
public static void writeIndividualSection(OutputStream out, String name, Attributes attributes)
throws IOException {
ManifestWriter.writeIndividualSection(out, name, attributes);
}
public static void writeSectionDelimiter(OutputStream out) throws IOException {
ManifestWriter.writeSectionDelimiter(out);
}
}

View File

@ -1,463 +0,0 @@
/*
* 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.oid;
import com.android.apksig.internal.util.InclusiveIntRange;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class OidConstants {
public static final String OID_DIGEST_MD5 = "1.2.840.113549.2.5";
public static final String OID_DIGEST_SHA1 = "1.3.14.3.2.26";
public static final String OID_DIGEST_SHA224 = "2.16.840.1.101.3.4.2.4";
public static final String OID_DIGEST_SHA256 = "2.16.840.1.101.3.4.2.1";
public static final String OID_DIGEST_SHA384 = "2.16.840.1.101.3.4.2.2";
public static final String OID_DIGEST_SHA512 = "2.16.840.1.101.3.4.2.3";
public static final String OID_SIG_RSA = "1.2.840.113549.1.1.1";
public static final String OID_SIG_MD5_WITH_RSA = "1.2.840.113549.1.1.4";
public static final String OID_SIG_SHA1_WITH_RSA = "1.2.840.113549.1.1.5";
public static final String OID_SIG_SHA224_WITH_RSA = "1.2.840.113549.1.1.14";
public static final String OID_SIG_SHA256_WITH_RSA = "1.2.840.113549.1.1.11";
public static final String OID_SIG_SHA384_WITH_RSA = "1.2.840.113549.1.1.12";
public static final String OID_SIG_SHA512_WITH_RSA = "1.2.840.113549.1.1.13";
public static final String OID_SIG_DSA = "1.2.840.10040.4.1";
public static final String OID_SIG_SHA1_WITH_DSA = "1.2.840.10040.4.3";
public static final String OID_SIG_SHA224_WITH_DSA = "2.16.840.1.101.3.4.3.1";
public static final String OID_SIG_SHA256_WITH_DSA = "2.16.840.1.101.3.4.3.2";
public static final String OID_SIG_SHA384_WITH_DSA = "2.16.840.1.101.3.4.3.3";
public static final String OID_SIG_SHA512_WITH_DSA = "2.16.840.1.101.3.4.3.4";
public static final String OID_SIG_EC_PUBLIC_KEY = "1.2.840.10045.2.1";
public static final String OID_SIG_SHA1_WITH_ECDSA = "1.2.840.10045.4.1";
public static final String OID_SIG_SHA224_WITH_ECDSA = "1.2.840.10045.4.3.1";
public static final String OID_SIG_SHA256_WITH_ECDSA = "1.2.840.10045.4.3.2";
public static final String OID_SIG_SHA384_WITH_ECDSA = "1.2.840.10045.4.3.3";
public static final String OID_SIG_SHA512_WITH_ECDSA = "1.2.840.10045.4.3.4";
public static final Map<String, List<InclusiveIntRange>> SUPPORTED_SIG_ALG_OIDS =
new HashMap<>();
static {
addSupportedSigAlg(
OID_DIGEST_MD5, OID_SIG_RSA,
InclusiveIntRange.from(0));
addSupportedSigAlg(
OID_DIGEST_MD5, OID_SIG_MD5_WITH_RSA,
InclusiveIntRange.fromTo(0, 8), InclusiveIntRange.from(21));
addSupportedSigAlg(
OID_DIGEST_MD5, OID_SIG_SHA1_WITH_RSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_MD5, OID_SIG_SHA224_WITH_RSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_MD5, OID_SIG_SHA256_WITH_RSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_MD5, OID_SIG_SHA384_WITH_RSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_MD5, OID_SIG_SHA512_WITH_RSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA1, OID_SIG_RSA,
InclusiveIntRange.from(0));
addSupportedSigAlg(
OID_DIGEST_SHA1, OID_SIG_MD5_WITH_RSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA1, OID_SIG_SHA1_WITH_RSA,
InclusiveIntRange.from(0));
addSupportedSigAlg(
OID_DIGEST_SHA1, OID_SIG_SHA224_WITH_RSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA1, OID_SIG_SHA256_WITH_RSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA1, OID_SIG_SHA384_WITH_RSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA1, OID_SIG_SHA512_WITH_RSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA224, OID_SIG_RSA,
InclusiveIntRange.fromTo(0, 8), InclusiveIntRange.from(21));
addSupportedSigAlg(
OID_DIGEST_SHA224, OID_SIG_MD5_WITH_RSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA224, OID_SIG_SHA1_WITH_RSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA224, OID_SIG_SHA224_WITH_RSA,
InclusiveIntRange.fromTo(0, 8), InclusiveIntRange.from(21));
addSupportedSigAlg(
OID_DIGEST_SHA224, OID_SIG_SHA256_WITH_RSA,
InclusiveIntRange.fromTo(21, 21));
addSupportedSigAlg(
OID_DIGEST_SHA224, OID_SIG_SHA384_WITH_RSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA224, OID_SIG_SHA512_WITH_RSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA256, OID_SIG_RSA,
InclusiveIntRange.fromTo(0, 8), InclusiveIntRange.from(18));
addSupportedSigAlg(
OID_DIGEST_SHA256, OID_SIG_MD5_WITH_RSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA256, OID_SIG_SHA1_WITH_RSA,
InclusiveIntRange.fromTo(21, 21));
addSupportedSigAlg(
OID_DIGEST_SHA256, OID_SIG_SHA224_WITH_RSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA256, OID_SIG_SHA256_WITH_RSA,
InclusiveIntRange.fromTo(0, 8), InclusiveIntRange.from(18));
addSupportedSigAlg(
OID_DIGEST_SHA256, OID_SIG_SHA384_WITH_RSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA256, OID_SIG_SHA512_WITH_RSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA384, OID_SIG_RSA,
InclusiveIntRange.from(18));
addSupportedSigAlg(
OID_DIGEST_SHA384, OID_SIG_MD5_WITH_RSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA384, OID_SIG_SHA1_WITH_RSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA384, OID_SIG_SHA224_WITH_RSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA384, OID_SIG_SHA256_WITH_RSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA384, OID_SIG_SHA384_WITH_RSA,
InclusiveIntRange.from(21));
addSupportedSigAlg(
OID_DIGEST_SHA384, OID_SIG_SHA512_WITH_RSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA512, OID_SIG_RSA,
InclusiveIntRange.from(18));
addSupportedSigAlg(
OID_DIGEST_SHA512, OID_SIG_MD5_WITH_RSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA512, OID_SIG_SHA1_WITH_RSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA512, OID_SIG_SHA224_WITH_RSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA512, OID_SIG_SHA256_WITH_RSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA512, OID_SIG_SHA384_WITH_RSA,
InclusiveIntRange.fromTo(21, 21));
addSupportedSigAlg(
OID_DIGEST_SHA512, OID_SIG_SHA512_WITH_RSA,
InclusiveIntRange.from(21));
addSupportedSigAlg(
OID_DIGEST_MD5, OID_SIG_SHA1_WITH_DSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_MD5, OID_SIG_SHA224_WITH_DSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_MD5, OID_SIG_SHA256_WITH_DSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA1, OID_SIG_DSA,
InclusiveIntRange.from(0));
addSupportedSigAlg(
OID_DIGEST_SHA1, OID_SIG_SHA1_WITH_DSA,
InclusiveIntRange.from(9));
addSupportedSigAlg(
OID_DIGEST_SHA1, OID_SIG_SHA224_WITH_DSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA1, OID_SIG_SHA256_WITH_DSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA224, OID_SIG_DSA,
InclusiveIntRange.from(22));
addSupportedSigAlg(
OID_DIGEST_SHA224, OID_SIG_SHA1_WITH_DSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA224, OID_SIG_SHA224_WITH_DSA,
InclusiveIntRange.from(21));
addSupportedSigAlg(
OID_DIGEST_SHA224, OID_SIG_SHA256_WITH_DSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA256, OID_SIG_DSA,
InclusiveIntRange.from(22));
addSupportedSigAlg(
OID_DIGEST_SHA256, OID_SIG_SHA1_WITH_DSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA256, OID_SIG_SHA224_WITH_DSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA256, OID_SIG_SHA256_WITH_DSA,
InclusiveIntRange.from(21));
addSupportedSigAlg(
OID_DIGEST_SHA384, OID_SIG_SHA1_WITH_DSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA384, OID_SIG_SHA224_WITH_DSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA384, OID_SIG_SHA256_WITH_DSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA512, OID_SIG_SHA1_WITH_DSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA512, OID_SIG_SHA224_WITH_DSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA512, OID_SIG_SHA256_WITH_DSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA1, OID_SIG_EC_PUBLIC_KEY,
InclusiveIntRange.from(18));
addSupportedSigAlg(
OID_DIGEST_SHA224, OID_SIG_EC_PUBLIC_KEY,
InclusiveIntRange.from(21));
addSupportedSigAlg(
OID_DIGEST_SHA256, OID_SIG_EC_PUBLIC_KEY,
InclusiveIntRange.from(18));
addSupportedSigAlg(
OID_DIGEST_SHA384, OID_SIG_EC_PUBLIC_KEY,
InclusiveIntRange.from(18));
addSupportedSigAlg(
OID_DIGEST_SHA512, OID_SIG_EC_PUBLIC_KEY,
InclusiveIntRange.from(18));
addSupportedSigAlg(
OID_DIGEST_MD5, OID_SIG_SHA1_WITH_ECDSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_MD5, OID_SIG_SHA224_WITH_ECDSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_MD5, OID_SIG_SHA256_WITH_ECDSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_MD5, OID_SIG_SHA384_WITH_ECDSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_MD5, OID_SIG_SHA512_WITH_ECDSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA1, OID_SIG_SHA1_WITH_ECDSA,
InclusiveIntRange.from(18));
addSupportedSigAlg(
OID_DIGEST_SHA1, OID_SIG_SHA224_WITH_ECDSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA1, OID_SIG_SHA256_WITH_ECDSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA1, OID_SIG_SHA384_WITH_ECDSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA1, OID_SIG_SHA512_WITH_ECDSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA224, OID_SIG_SHA1_WITH_ECDSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA224, OID_SIG_SHA224_WITH_ECDSA,
InclusiveIntRange.from(21));
addSupportedSigAlg(
OID_DIGEST_SHA224, OID_SIG_SHA256_WITH_ECDSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA224, OID_SIG_SHA384_WITH_ECDSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA224, OID_SIG_SHA512_WITH_ECDSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA256, OID_SIG_SHA1_WITH_ECDSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA256, OID_SIG_SHA224_WITH_ECDSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA256, OID_SIG_SHA256_WITH_ECDSA,
InclusiveIntRange.from(21));
addSupportedSigAlg(
OID_DIGEST_SHA256, OID_SIG_SHA384_WITH_ECDSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA256, OID_SIG_SHA512_WITH_ECDSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA384, OID_SIG_SHA1_WITH_ECDSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA384, OID_SIG_SHA224_WITH_ECDSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA384, OID_SIG_SHA256_WITH_ECDSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA384, OID_SIG_SHA384_WITH_ECDSA,
InclusiveIntRange.from(21));
addSupportedSigAlg(
OID_DIGEST_SHA384, OID_SIG_SHA512_WITH_ECDSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA512, OID_SIG_SHA1_WITH_ECDSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA512, OID_SIG_SHA224_WITH_ECDSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA512, OID_SIG_SHA256_WITH_ECDSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA512, OID_SIG_SHA384_WITH_ECDSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA512, OID_SIG_SHA512_WITH_ECDSA,
InclusiveIntRange.from(21));
}
public static void addSupportedSigAlg(
String digestAlgorithmOid,
String signatureAlgorithmOid,
InclusiveIntRange... supportedApiLevels) {
SUPPORTED_SIG_ALG_OIDS.put(
digestAlgorithmOid + "with" + signatureAlgorithmOid,
Arrays.asList(supportedApiLevels));
}
public static List<InclusiveIntRange> getSigAlgSupportedApiLevels(
String digestAlgorithmOid,
String signatureAlgorithmOid) {
List<InclusiveIntRange> result =
SUPPORTED_SIG_ALG_OIDS.get(digestAlgorithmOid + "with" + signatureAlgorithmOid);
return (result != null) ? result : Collections.emptyList();
}
public static class OidToUserFriendlyNameMapper {
private OidToUserFriendlyNameMapper() {}
private static final Map<String, String> OID_TO_USER_FRIENDLY_NAME = new HashMap<>();
static {
OID_TO_USER_FRIENDLY_NAME.put(OID_DIGEST_MD5, "MD5");
OID_TO_USER_FRIENDLY_NAME.put(OID_DIGEST_SHA1, "SHA-1");
OID_TO_USER_FRIENDLY_NAME.put(OID_DIGEST_SHA224, "SHA-224");
OID_TO_USER_FRIENDLY_NAME.put(OID_DIGEST_SHA256, "SHA-256");
OID_TO_USER_FRIENDLY_NAME.put(OID_DIGEST_SHA384, "SHA-384");
OID_TO_USER_FRIENDLY_NAME.put(OID_DIGEST_SHA512, "SHA-512");
OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_RSA, "RSA");
OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_MD5_WITH_RSA, "MD5 with RSA");
OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA1_WITH_RSA, "SHA-1 with RSA");
OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA224_WITH_RSA, "SHA-224 with RSA");
OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA256_WITH_RSA, "SHA-256 with RSA");
OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA384_WITH_RSA, "SHA-384 with RSA");
OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA512_WITH_RSA, "SHA-512 with RSA");
OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_DSA, "DSA");
OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA1_WITH_DSA, "SHA-1 with DSA");
OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA224_WITH_DSA, "SHA-224 with DSA");
OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA256_WITH_DSA, "SHA-256 with DSA");
OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA384_WITH_DSA, "SHA-384 with DSA");
OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA512_WITH_DSA, "SHA-512 with DSA");
OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_EC_PUBLIC_KEY, "ECDSA");
OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA1_WITH_ECDSA, "SHA-1 with ECDSA");
OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA224_WITH_ECDSA, "SHA-224 with ECDSA");
OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA256_WITH_ECDSA, "SHA-256 with ECDSA");
OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA384_WITH_ECDSA, "SHA-384 with ECDSA");
OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA512_WITH_ECDSA, "SHA-512 with ECDSA");
}
public static String getUserFriendlyNameForOid(String oid) {
return OID_TO_USER_FRIENDLY_NAME.get(oid);
}
}
public static final Map<String, String> OID_TO_JCA_DIGEST_ALG = new HashMap<>();
static {
OID_TO_JCA_DIGEST_ALG.put(OID_DIGEST_MD5, "MD5");
OID_TO_JCA_DIGEST_ALG.put(OID_DIGEST_SHA1, "SHA-1");
OID_TO_JCA_DIGEST_ALG.put(OID_DIGEST_SHA224, "SHA-224");
OID_TO_JCA_DIGEST_ALG.put(OID_DIGEST_SHA256, "SHA-256");
OID_TO_JCA_DIGEST_ALG.put(OID_DIGEST_SHA384, "SHA-384");
OID_TO_JCA_DIGEST_ALG.put(OID_DIGEST_SHA512, "SHA-512");
}
public static final Map<String, String> OID_TO_JCA_SIGNATURE_ALG = new HashMap<>();
static {
OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_MD5_WITH_RSA, "MD5withRSA");
OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA1_WITH_RSA, "SHA1withRSA");
OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA224_WITH_RSA, "SHA224withRSA");
OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA256_WITH_RSA, "SHA256withRSA");
OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA384_WITH_RSA, "SHA384withRSA");
OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA512_WITH_RSA, "SHA512withRSA");
OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA1_WITH_DSA, "SHA1withDSA");
OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA224_WITH_DSA, "SHA224withDSA");
OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA256_WITH_DSA, "SHA256withDSA");
OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA1_WITH_ECDSA, "SHA1withECDSA");
OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA224_WITH_ECDSA, "SHA224withECDSA");
OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA256_WITH_ECDSA, "SHA256withECDSA");
OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA384_WITH_ECDSA, "SHA384withECDSA");
OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA512_WITH_ECDSA, "SHA512withECDSA");
}
private OidConstants() {}
}

View File

@ -1,173 +0,0 @@
/*
* 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.internal.pkcs7;
import static com.android.apksig.Constants.OID_RSA_ENCRYPTION;
import static com.android.apksig.internal.asn1.Asn1DerEncoder.ASN1_DER_NULL;
import static com.android.apksig.internal.oid.OidConstants.OID_DIGEST_SHA1;
import static com.android.apksig.internal.oid.OidConstants.OID_DIGEST_SHA256;
import static com.android.apksig.internal.oid.OidConstants.OID_SIG_DSA;
import static com.android.apksig.internal.oid.OidConstants.OID_SIG_EC_PUBLIC_KEY;
import static com.android.apksig.internal.oid.OidConstants.OID_SIG_RSA;
import static com.android.apksig.internal.oid.OidConstants.OID_SIG_SHA256_WITH_DSA;
import static com.android.apksig.internal.oid.OidConstants.OID_TO_JCA_DIGEST_ALG;
import static com.android.apksig.internal.oid.OidConstants.OID_TO_JCA_SIGNATURE_ALG;
import com.android.apksig.internal.apk.v1.DigestAlgorithm;
import com.android.apksig.internal.asn1.Asn1Class;
import com.android.apksig.internal.asn1.Asn1Field;
import com.android.apksig.internal.asn1.Asn1OpaqueObject;
import com.android.apksig.internal.asn1.Asn1Type;
import com.android.apksig.internal.util.Pair;
import java.security.InvalidKeyException;
import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;
/**
* PKCS #7 {@code AlgorithmIdentifier} as specified in RFC 5652.
*/
@Asn1Class(type = Asn1Type.SEQUENCE)
public class AlgorithmIdentifier {
@Asn1Field(index = 0, type = Asn1Type.OBJECT_IDENTIFIER)
public String algorithm;
@Asn1Field(index = 1, type = Asn1Type.ANY, optional = true)
public Asn1OpaqueObject parameters;
public AlgorithmIdentifier() {}
public AlgorithmIdentifier(String algorithmOid, Asn1OpaqueObject parameters) {
this.algorithm = algorithmOid;
this.parameters = parameters;
}
/**
* Returns the PKCS #7 {@code DigestAlgorithm} to use when signing using the specified digest
* algorithm.
*/
public static AlgorithmIdentifier getSignerInfoDigestAlgorithmOid(
DigestAlgorithm digestAlgorithm) {
switch (digestAlgorithm) {
case SHA1:
return new AlgorithmIdentifier(OID_DIGEST_SHA1, ASN1_DER_NULL);
case SHA256:
return new AlgorithmIdentifier(OID_DIGEST_SHA256, ASN1_DER_NULL);
}
throw new IllegalArgumentException("Unsupported digest algorithm: " + digestAlgorithm);
}
/**
* Returns the JCA {@link Signature} algorithm and PKCS #7 {@code SignatureAlgorithm} to use
* when signing with the specified key and digest algorithm.
*/
public static Pair<String, AlgorithmIdentifier> getSignerInfoSignatureAlgorithm(
PublicKey publicKey, DigestAlgorithm digestAlgorithm, boolean deterministicDsaSigning)
throws InvalidKeyException {
String keyAlgorithm = publicKey.getAlgorithm();
String jcaDigestPrefixForSigAlg;
switch (digestAlgorithm) {
case SHA1:
jcaDigestPrefixForSigAlg = "SHA1";
break;
case SHA256:
jcaDigestPrefixForSigAlg = "SHA256";
break;
default:
throw new IllegalArgumentException(
"Unexpected digest algorithm: " + digestAlgorithm);
}
if ("RSA".equalsIgnoreCase(keyAlgorithm) || OID_RSA_ENCRYPTION.equals(keyAlgorithm)) {
return Pair.of(
jcaDigestPrefixForSigAlg + "withRSA",
new AlgorithmIdentifier(OID_SIG_RSA, ASN1_DER_NULL));
} else if ("DSA".equalsIgnoreCase(keyAlgorithm)) {
AlgorithmIdentifier sigAlgId;
switch (digestAlgorithm) {
case SHA1:
sigAlgId =
new AlgorithmIdentifier(OID_SIG_DSA, ASN1_DER_NULL);
break;
case SHA256:
// DSA signatures with SHA-256 in SignedData are accepted by Android API Level
// 21 and higher. However, there are two ways to specify their SignedData
// SignatureAlgorithm: dsaWithSha256 (2.16.840.1.101.3.4.3.2) and
// dsa (1.2.840.10040.4.1). The latter works only on API Level 22+. Thus, we use
// the former.
sigAlgId =
new AlgorithmIdentifier(OID_SIG_SHA256_WITH_DSA, ASN1_DER_NULL);
break;
default:
throw new IllegalArgumentException(
"Unexpected digest algorithm: " + digestAlgorithm);
}
String signingAlgorithmName =
jcaDigestPrefixForSigAlg + (deterministicDsaSigning ? "withDetDSA" : "withDSA");
return Pair.of(signingAlgorithmName, sigAlgId);
} else if ("EC".equalsIgnoreCase(keyAlgorithm)) {
return Pair.of(
jcaDigestPrefixForSigAlg + "withECDSA",
new AlgorithmIdentifier(OID_SIG_EC_PUBLIC_KEY, ASN1_DER_NULL));
} else {
throw new InvalidKeyException("Unsupported key algorithm: " + keyAlgorithm);
}
}
public static String getJcaSignatureAlgorithm(
String digestAlgorithmOid,
String signatureAlgorithmOid) throws SignatureException {
// First check whether the signature algorithm OID alone is sufficient
String result = OID_TO_JCA_SIGNATURE_ALG.get(signatureAlgorithmOid);
if (result != null) {
return result;
}
// Signature algorithm OID alone is insufficient. Need to combine digest algorithm OID
// with signature algorithm OID.
String suffix;
if (OID_SIG_RSA.equals(signatureAlgorithmOid)) {
suffix = "RSA";
} else if (OID_SIG_DSA.equals(signatureAlgorithmOid)) {
suffix = "DSA";
} else if (OID_SIG_EC_PUBLIC_KEY.equals(signatureAlgorithmOid)) {
suffix = "ECDSA";
} else {
throw new SignatureException(
"Unsupported JCA Signature algorithm"
+ " . Digest algorithm: " + digestAlgorithmOid
+ ", signature algorithm: " + signatureAlgorithmOid);
}
String jcaDigestAlg = getJcaDigestAlgorithm(digestAlgorithmOid);
// Canonical name for SHA-1 with ... is SHA1with, rather than SHA1. Same for all other
// SHA algorithms.
if (jcaDigestAlg.startsWith("SHA-")) {
jcaDigestAlg = "SHA" + jcaDigestAlg.substring("SHA-".length());
}
return jcaDigestAlg + "with" + suffix;
}
public static String getJcaDigestAlgorithm(String oid)
throws SignatureException {
String result = OID_TO_JCA_DIGEST_ALG.get(oid);
if (result == null) {
throw new SignatureException("Unsupported digest algorithm: " + oid);
}
return result;
}
}

View File

@ -1,36 +0,0 @@
/*
* 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.internal.pkcs7;
import com.android.apksig.internal.asn1.Asn1Class;
import com.android.apksig.internal.asn1.Asn1Field;
import com.android.apksig.internal.asn1.Asn1OpaqueObject;
import com.android.apksig.internal.asn1.Asn1Type;
import java.util.List;
/**
* PKCS #7 {@code Attribute} as specified in RFC 5652.
*/
@Asn1Class(type = Asn1Type.SEQUENCE)
public class Attribute {
@Asn1Field(index = 0, type = Asn1Type.OBJECT_IDENTIFIER)
public String attrType;
@Asn1Field(index = 1, type = Asn1Type.SET_OF)
public List<Asn1OpaqueObject> attrValues;
}

View File

@ -1,36 +0,0 @@
/*
* 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.internal.pkcs7;
import com.android.apksig.internal.asn1.Asn1Class;
import com.android.apksig.internal.asn1.Asn1Field;
import com.android.apksig.internal.asn1.Asn1OpaqueObject;
import com.android.apksig.internal.asn1.Asn1Type;
import com.android.apksig.internal.asn1.Asn1Tagging;
/**
* PKCS #7 {@code ContentInfo} as specified in RFC 5652.
*/
@Asn1Class(type = Asn1Type.SEQUENCE)
public class ContentInfo {
@Asn1Field(index = 1, type = Asn1Type.OBJECT_IDENTIFIER)
public String contentType;
@Asn1Field(index = 2, type = Asn1Type.ANY, tagging = Asn1Tagging.EXPLICIT, tagNumber = 0)
public Asn1OpaqueObject content;
}

View File

@ -1,46 +0,0 @@
/*
* 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.internal.pkcs7;
import com.android.apksig.internal.asn1.Asn1Class;
import com.android.apksig.internal.asn1.Asn1Field;
import com.android.apksig.internal.asn1.Asn1Type;
import com.android.apksig.internal.asn1.Asn1Tagging;
import java.nio.ByteBuffer;
/**
* PKCS #7 {@code EncapsulatedContentInfo} as specified in RFC 5652.
*/
@Asn1Class(type = Asn1Type.SEQUENCE)
public class EncapsulatedContentInfo {
@Asn1Field(index = 0, type = Asn1Type.OBJECT_IDENTIFIER)
public String contentType;
@Asn1Field(
index = 1,
type = Asn1Type.OCTET_STRING,
tagging = Asn1Tagging.EXPLICIT, tagNumber = 0,
optional = true)
public ByteBuffer content;
public EncapsulatedContentInfo() {}
public EncapsulatedContentInfo(String contentTypeOid) {
contentType = contentTypeOid;
}
}

View File

@ -1,43 +0,0 @@
/*
* 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.internal.pkcs7;
import com.android.apksig.internal.asn1.Asn1Class;
import com.android.apksig.internal.asn1.Asn1Field;
import com.android.apksig.internal.asn1.Asn1OpaqueObject;
import com.android.apksig.internal.asn1.Asn1Type;
import java.math.BigInteger;
/**
* PKCS #7 {@code IssuerAndSerialNumber} as specified in RFC 5652.
*/
@Asn1Class(type = Asn1Type.SEQUENCE)
public class IssuerAndSerialNumber {
@Asn1Field(index = 0, type = Asn1Type.ANY)
public Asn1OpaqueObject issuer;
@Asn1Field(index = 1, type = Asn1Type.INTEGER)
public BigInteger certificateSerialNumber;
public IssuerAndSerialNumber() {}
public IssuerAndSerialNumber(Asn1OpaqueObject issuer, BigInteger certificateSerialNumber) {
this.issuer = issuer;
this.certificateSerialNumber = certificateSerialNumber;
}
}

View File

@ -1,29 +0,0 @@
/*
* 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.internal.pkcs7;
/**
* Assorted PKCS #7 constants from RFC 5652.
*/
public abstract class Pkcs7Constants {
private Pkcs7Constants() {}
public static final String OID_DATA = "1.2.840.113549.1.7.1";
public static final String OID_SIGNED_DATA = "1.2.840.113549.1.7.2";
public static final String OID_CONTENT_TYPE = "1.2.840.113549.1.9.3";
public static final String OID_MESSAGE_DIGEST = "1.2.840.113549.1.9.4";
}

View File

@ -1,32 +0,0 @@
/*
* 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.internal.pkcs7;
/**
* Indicates that an error was encountered while decoding a PKCS #7 structure.
*/
public class Pkcs7DecodingException extends Exception {
private static final long serialVersionUID = 1L;
public Pkcs7DecodingException(String message) {
super(message);
}
public Pkcs7DecodingException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -1,58 +0,0 @@
/*
* 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.internal.pkcs7;
import com.android.apksig.internal.asn1.Asn1Class;
import com.android.apksig.internal.asn1.Asn1Field;
import com.android.apksig.internal.asn1.Asn1OpaqueObject;
import com.android.apksig.internal.asn1.Asn1Type;
import com.android.apksig.internal.asn1.Asn1Tagging;
import java.nio.ByteBuffer;
import java.util.List;
/**
* PKCS #7 {@code SignedData} as specified in RFC 5652.
*/
@Asn1Class(type = Asn1Type.SEQUENCE)
public class SignedData {
@Asn1Field(index = 0, type = Asn1Type.INTEGER)
public int version;
@Asn1Field(index = 1, type = Asn1Type.SET_OF)
public List<AlgorithmIdentifier> digestAlgorithms;
@Asn1Field(index = 2, type = Asn1Type.SEQUENCE)
public EncapsulatedContentInfo encapContentInfo;
@Asn1Field(
index = 3,
type = Asn1Type.SET_OF,
tagging = Asn1Tagging.IMPLICIT, tagNumber = 0,
optional = true)
public List<Asn1OpaqueObject> certificates;
@Asn1Field(
index = 4,
type = Asn1Type.SET_OF,
tagging = Asn1Tagging.IMPLICIT, tagNumber = 1,
optional = true)
public List<ByteBuffer> crls;
@Asn1Field(index = 5, type = Asn1Type.SET_OF)
public List<SignerInfo> signerInfos;
}

View File

@ -1,42 +0,0 @@
/*
* 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.internal.pkcs7;
import com.android.apksig.internal.asn1.Asn1Class;
import com.android.apksig.internal.asn1.Asn1Field;
import com.android.apksig.internal.asn1.Asn1Type;
import com.android.apksig.internal.asn1.Asn1Tagging;
import java.nio.ByteBuffer;
/**
* PKCS #7 {@code SignerIdentifier} as specified in RFC 5652.
*/
@Asn1Class(type = Asn1Type.CHOICE)
public class SignerIdentifier {
@Asn1Field(type = Asn1Type.SEQUENCE)
public IssuerAndSerialNumber issuerAndSerialNumber;
@Asn1Field(type = Asn1Type.OCTET_STRING, tagging = Asn1Tagging.IMPLICIT, tagNumber = 0)
public ByteBuffer subjectKeyIdentifier;
public SignerIdentifier() {}
public SignerIdentifier(IssuerAndSerialNumber issuerAndSerialNumber) {
this.issuerAndSerialNumber = issuerAndSerialNumber;
}
}

View File

@ -1,61 +0,0 @@
/*
* 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.internal.pkcs7;
import com.android.apksig.internal.asn1.Asn1Class;
import com.android.apksig.internal.asn1.Asn1Field;
import com.android.apksig.internal.asn1.Asn1OpaqueObject;
import com.android.apksig.internal.asn1.Asn1Type;
import com.android.apksig.internal.asn1.Asn1Tagging;
import java.nio.ByteBuffer;
import java.util.List;
/**
* PKCS #7 {@code SignerInfo} as specified in RFC 5652.
*/
@Asn1Class(type = Asn1Type.SEQUENCE)
public class SignerInfo {
@Asn1Field(index = 0, type = Asn1Type.INTEGER)
public int version;
@Asn1Field(index = 1, type = Asn1Type.CHOICE)
public SignerIdentifier sid;
@Asn1Field(index = 2, type = Asn1Type.SEQUENCE)
public AlgorithmIdentifier digestAlgorithm;
@Asn1Field(
index = 3,
type = Asn1Type.SET_OF,
tagging = Asn1Tagging.IMPLICIT, tagNumber = 0,
optional = true)
public Asn1OpaqueObject signedAttrs;
@Asn1Field(index = 4, type = Asn1Type.SEQUENCE)
public AlgorithmIdentifier signatureAlgorithm;
@Asn1Field(index = 5, type = Asn1Type.OCTET_STRING)
public ByteBuffer signature;
@Asn1Field(
index = 6,
type = Asn1Type.SET_OF,
tagging = Asn1Tagging.IMPLICIT, tagNumber = 1,
optional = true)
public List<Attribute> unsignedAttrs;
}

View File

@ -1,59 +0,0 @@
/*
* 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.util;
/**
* Android SDK version / API Level constants.
*/
public abstract class AndroidSdkVersion {
/** Hidden constructor to prevent instantiation. */
private AndroidSdkVersion() {}
/** Android 1.0 */
public static final int INITIAL_RELEASE = 1;
/** Android 2.3. */
public static final int GINGERBREAD = 9;
/** Android 3.0 */
public static final int HONEYCOMB = 11;
/** Android 4.3. The revenge of the beans. */
public static final int JELLY_BEAN_MR2 = 18;
/** Android 4.4. KitKat, another tasty treat. */
public static final int KITKAT = 19;
/** Android 5.0. A flat one with beautiful shadows. But still tasty. */
public static final int LOLLIPOP = 21;
/** Android 6.0. M is for Marshmallow! */
public static final int M = 23;
/** Android 7.0. N is for Nougat. */
public static final int N = 24;
/** Android O. */
public static final int O = 26;
/** Android P. */
public static final int P = 28;
/** Android R. */
public static final int R = 30;
}

View File

@ -1,240 +0,0 @@
/*
* 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.util;
import com.android.apksig.util.DataSink;
import com.android.apksig.util.DataSource;
import com.android.apksig.util.ReadableDataSink;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Arrays;
/**
* Growable byte array which can be appended to via {@link DataSink} interface and read from via
* {@link DataSource} interface.
*/
public class ByteArrayDataSink implements ReadableDataSink {
private static final int MAX_READ_CHUNK_SIZE = 65536;
private byte[] mArray;
private int mSize;
public ByteArrayDataSink() {
this(65536);
}
public ByteArrayDataSink(int initialCapacity) {
if (initialCapacity < 0) {
throw new IllegalArgumentException("initial capacity: " + initialCapacity);
}
mArray = new byte[initialCapacity];
}
@Override
public void consume(byte[] buf, int offset, int length) throws IOException {
if (offset < 0) {
// Must perform this check because System.arraycopy below doesn't perform it when
// length == 0
throw new IndexOutOfBoundsException("offset: " + offset);
}
if (offset > buf.length) {
// Must perform this check because System.arraycopy below doesn't perform it when
// length == 0
throw new IndexOutOfBoundsException(
"offset: " + offset + ", buf.length: " + buf.length);
}
if (length == 0) {
return;
}
ensureAvailable(length);
System.arraycopy(buf, offset, mArray, mSize, length);
mSize += length;
}
@Override
public void consume(ByteBuffer buf) throws IOException {
if (!buf.hasRemaining()) {
return;
}
if (buf.hasArray()) {
consume(buf.array(), buf.arrayOffset() + buf.position(), buf.remaining());
buf.position(buf.limit());
return;
}
ensureAvailable(buf.remaining());
byte[] tmp = new byte[Math.min(buf.remaining(), MAX_READ_CHUNK_SIZE)];
while (buf.hasRemaining()) {
int chunkSize = Math.min(buf.remaining(), tmp.length);
buf.get(tmp, 0, chunkSize);
System.arraycopy(tmp, 0, mArray, mSize, chunkSize);
mSize += chunkSize;
}
}
private void ensureAvailable(int minAvailable) throws IOException {
if (minAvailable <= 0) {
return;
}
long minCapacity = ((long) mSize) + minAvailable;
if (minCapacity <= mArray.length) {
return;
}
if (minCapacity > Integer.MAX_VALUE) {
throw new IOException(
"Required capacity too large: " + minCapacity + ", max: " + Integer.MAX_VALUE);
}
int doubleCurrentSize = (int) Math.min(mArray.length * 2L, Integer.MAX_VALUE);
int newSize = (int) Math.max(minCapacity, doubleCurrentSize);
mArray = Arrays.copyOf(mArray, newSize);
}
@Override
public long size() {
return mSize;
}
@Override
public ByteBuffer getByteBuffer(long offset, int size) {
checkChunkValid(offset, size);
// checkChunkValid ensures that it's OK to cast offset to int.
return ByteBuffer.wrap(mArray, (int) offset, size).slice();
}
@Override
public void feed(long offset, long size, DataSink sink) throws IOException {
checkChunkValid(offset, size);
// checkChunkValid ensures that it's OK to cast offset and size to int.
sink.consume(mArray, (int) offset, (int) size);
}
@Override
public void copyTo(long offset, int size, ByteBuffer dest) throws IOException {
checkChunkValid(offset, size);
// checkChunkValid ensures that it's OK to cast offset to int.
dest.put(mArray, (int) offset, size);
}
private void checkChunkValid(long offset, long size) {
if (offset < 0) {
throw new IndexOutOfBoundsException("offset: " + offset);
}
if (size < 0) {
throw new IndexOutOfBoundsException("size: " + size);
}
if (offset > mSize) {
throw new IndexOutOfBoundsException(
"offset (" + offset + ") > source size (" + mSize + ")");
}
long endOffset = offset + size;
if (endOffset < offset) {
throw new IndexOutOfBoundsException(
"offset (" + offset + ") + size (" + size + ") overflow");
}
if (endOffset > mSize) {
throw new IndexOutOfBoundsException(
"offset (" + offset + ") + size (" + size + ") > source size (" + mSize + ")");
}
}
@Override
public DataSource slice(long offset, long size) {
checkChunkValid(offset, size);
// checkChunkValid ensures that it's OK to cast offset and size to int.
return new SliceDataSource((int) offset, (int) size);
}
/**
* Slice of the growable byte array. The slice's offset and size in the array are fixed.
*/
private class SliceDataSource implements DataSource {
private final int mSliceOffset;
private final int mSliceSize;
private SliceDataSource(int offset, int size) {
mSliceOffset = offset;
mSliceSize = size;
}
@Override
public long size() {
return mSliceSize;
}
@Override
public void feed(long offset, long size, DataSink sink) throws IOException {
checkChunkValid(offset, size);
// checkChunkValid combined with the way instances of this class are constructed ensures
// that mSliceOffset + offset does not overflow and that it's fine to cast size to int.
sink.consume(mArray, (int) (mSliceOffset + offset), (int) size);
}
@Override
public ByteBuffer getByteBuffer(long offset, int size) throws IOException {
checkChunkValid(offset, size);
// checkChunkValid combined with the way instances of this class are constructed ensures
// that mSliceOffset + offset does not overflow.
return ByteBuffer.wrap(mArray, (int) (mSliceOffset + offset), size).slice();
}
@Override
public void copyTo(long offset, int size, ByteBuffer dest) throws IOException {
checkChunkValid(offset, size);
// checkChunkValid combined with the way instances of this class are constructed ensures
// that mSliceOffset + offset does not overflow.
dest.put(mArray, (int) (mSliceOffset + offset), size);
}
@Override
public DataSource slice(long offset, long size) {
checkChunkValid(offset, size);
// checkChunkValid combined with the way instances of this class are constructed ensures
// that mSliceOffset + offset does not overflow and that it's fine to cast size to int.
return new SliceDataSource((int) (mSliceOffset + offset), (int) size);
}
private void checkChunkValid(long offset, long size) {
if (offset < 0) {
throw new IndexOutOfBoundsException("offset: " + offset);
}
if (size < 0) {
throw new IndexOutOfBoundsException("size: " + size);
}
if (offset > mSliceSize) {
throw new IndexOutOfBoundsException(
"offset (" + offset + ") > source size (" + mSliceSize + ")");
}
long endOffset = offset + size;
if (endOffset < offset) {
throw new IndexOutOfBoundsException(
"offset (" + offset + ") + size (" + size + ") overflow");
}
if (endOffset > mSliceSize) {
throw new IndexOutOfBoundsException(
"offset (" + offset + ") + size (" + size + ") > source size (" + mSliceSize
+ ")");
}
}
}
}

View File

@ -1,125 +0,0 @@
/*
* 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.util;
import com.android.apksig.util.DataSink;
import com.android.apksig.util.DataSource;
import java.io.IOException;
import java.nio.ByteBuffer;
/**
* {@link DataSource} backed by a {@link ByteBuffer}.
*/
public class ByteBufferDataSource implements DataSource {
private final ByteBuffer mBuffer;
private final int mSize;
/**
* Constructs a new {@code ByteBufferDigestSource} based on the data contained in the provided
* buffer between the buffer's position and limit.
*/
public ByteBufferDataSource(ByteBuffer buffer) {
this(buffer, true);
}
/**
* Constructs a new {@code ByteBufferDigestSource} based on the data contained in the provided
* buffer between the buffer's position and limit.
*/
private ByteBufferDataSource(ByteBuffer buffer, boolean sliceRequired) {
mBuffer = (sliceRequired) ? buffer.slice() : buffer;
mSize = buffer.remaining();
}
@Override
public long size() {
return mSize;
}
@Override
public ByteBuffer getByteBuffer(long offset, int size) {
checkChunkValid(offset, size);
// checkChunkValid ensures that it's OK to cast offset to int.
int chunkPosition = (int) offset;
int chunkLimit = chunkPosition + size;
// Creating a slice of ByteBuffer modifies the state of the source ByteBuffer (position
// and limit fields, to be more specific). We thus use synchronization around these
// state-changing operations to make instances of this class thread-safe.
synchronized (mBuffer) {
// ByteBuffer.limit(int) and .position(int) check that that the position >= limit
// invariant is not broken. Thus, the only way to safely change position and limit
// without caring about their current values is to first set position to 0 or set the
// limit to capacity.
mBuffer.position(0);
mBuffer.limit(chunkLimit);
mBuffer.position(chunkPosition);
return mBuffer.slice();
}
}
@Override
public void copyTo(long offset, int size, ByteBuffer dest) {
dest.put(getByteBuffer(offset, size));
}
@Override
public void feed(long offset, long size, DataSink sink) throws IOException {
if ((size < 0) || (size > mSize)) {
throw new IndexOutOfBoundsException("size: " + size + ", source size: " + mSize);
}
sink.consume(getByteBuffer(offset, (int) size));
}
@Override
public ByteBufferDataSource slice(long offset, long size) {
if ((offset == 0) && (size == mSize)) {
return this;
}
if ((size < 0) || (size > mSize)) {
throw new IndexOutOfBoundsException("size: " + size + ", source size: " + mSize);
}
return new ByteBufferDataSource(
getByteBuffer(offset, (int) size),
false // no need to slice -- it's already a slice
);
}
private void checkChunkValid(long offset, long size) {
if (offset < 0) {
throw new IndexOutOfBoundsException("offset: " + offset);
}
if (size < 0) {
throw new IndexOutOfBoundsException("size: " + size);
}
if (offset > mSize) {
throw new IndexOutOfBoundsException(
"offset (" + offset + ") > source size (" + mSize + ")");
}
long endOffset = offset + size;
if (endOffset < offset) {
throw new IndexOutOfBoundsException(
"offset (" + offset + ") + size (" + size + ") overflow");
}
if (endOffset > mSize) {
throw new IndexOutOfBoundsException(
"offset (" + offset + ") + size (" + size + ") > source size (" + mSize +")");
}
}
}

Some files were not shown because too many files have changed in this diff Show More