Compare commits

..

728 Commits

Author SHA1 Message Date
fa2fb66f44 取消实名认证身份证类型输入限制 2021-11-05 11:12:16 +08:00
9982ebea9e 版本更新到 5.3.3 2021-11-05 11:07:34 +08:00
2b30cc1f41 Merge branch 'hotfix-v5.3.2-412-crash' into 'release'
修复部分闪退

See merge request halo/android/assistant-android!142
2021-11-05 11:06:54 +08:00
96a19eb5ee 修复部分闪退
1. 部分5.0设备启动读取错误abi造成闪退的问题
2. 部分华为鸿蒙设备读取系统类型闪退的问题
2021-11-05 11:05:26 +08:00
85276d462f Merge branch 'hotfix-v5.3.2-412-fix_realname_input' into 'release'
修复实名身份证号无法输入 X 的问题

See merge request halo/android/assistant-android!141
2021-11-05 09:59:50 +08:00
44aebc39ea 修复实名身份证号无法输入 X 的问题 2021-11-05 09:58:42 +08:00
1478b59479 相关版本合并及处理要求 https://git.ghzs.com/pm/halo-app-issues/-/issues/1605 2021-11-01 16:14:04 +08:00
dd1f399425 Merge branch 'feature-issues1572' into 'release'
完成实名认证优化第三期 https://git.ghzs.com/pm/halo-app-issues/-/issues/1572

See merge request halo/android/assistant-android!140
2021-11-01 15:03:26 +08:00
3e8923f34a 完成实名认证优化第三期 https://git.ghzs.com/pm/halo-app-issues/-/issues/1572 2021-11-01 15:00:35 +08:00
41f7e3a05a Merge branch 'hotfix-v5.3.1-411-crash' into 'release'
修复一些闪退

See merge request halo/android/assistant-android!139
2021-11-01 11:26:26 +08:00
25ce3ccdc9 修复一些闪退
1. 修复游戏详情页回到顶部的闪退
2. 尝试修复获取游戏更新时偶发的闪退
2021-11-01 11:24:43 +08:00
5df0d85004 Merge branch 'hotfix-v5.3.1-411-comment' into 'release'
修改未登录评论时一键登录弹窗弹起键盘没有收起问题

See merge request halo/android/assistant-android!138
2021-10-29 16:00:47 +08:00
63b88383a4 修改未登录评论时一键登录弹窗弹起键盘没有收起问题 2021-10-29 14:59:55 +08:00
36bc35a7e3 版本更新至 5.3.1 2021-10-29 11:13:46 +08:00
1c55ab8f86 Merge branch 'hotfix-v5.3.0-410-generic_crash' into 'release'
修复一系列闪退

See merge request halo/android/assistant-android!137
2021-10-29 11:02:35 +08:00
9a1f3b0fe3 修复一系列闪退
1. 跳转用户详情参数有误造成的闪退
2. 标记更新已读红点时的闪退
3. 5.0 以下设备点击更新按钮的闪退
4. 横向列表专题点击时偶发的闪退
5. 首页滑动顶部 BANNER 偶发的闪退
2021-10-29 11:01:16 +08:00
913bcdeeb2 捕抓卸载应用时偶发的闪退异常 2021-10-28 17:52:43 +08:00
e40857d24b Merge branch 'hotfix-v5.3.0-410-image_viewer_crash' into 'release'
修复查看大图传入数组为空时的闪退异常

See merge request halo/android/assistant-android!136
2021-10-28 17:40:56 +08:00
d3fab1aa73 修复查看大图传入数组为空时的闪退异常 2021-10-28 17:40:18 +08:00
d32698ca91 Merge branch 'hotfix-v5.3.0-410-store_old_version_info' into 'release'
为后续版本提供当前版本的版本和渠道

See merge request halo/android/assistant-android!135
2021-10-28 17:26:27 +08:00
7832276560 为后续版本提供当前版本的版本和渠道 2021-10-28 17:25:37 +08:00
2fca73dfe8 Merge branch 'hotfix-v5.3.0-410-crash' into 'release'
修复游戏搜索页面列表数组越界问题

See merge request halo/android/assistant-android!134
2021-10-28 17:11:06 +08:00
lyr
0f194a80bb 修复游戏搜索页面列表数组越界问题 2021-10-28 17:09:08 +08:00
bb60f7f22a 修改模拟器游戏下载按钮显示错误问题
(cherry picked from commit d3e21251d9)
2021-10-27 17:52:03 +08:00
30ae815717 尝试修复游戏搜索详情可能发生的进度同步问题 2021-10-26 17:18:35 +08:00
1eb1b6a956 处理合并异常 2021-10-26 16:35:52 +08:00
f46b303a67 Merge branch 'feature-issues1526' into dev
# Conflicts:
#	app/src/main/java/com/gh/download/DownloadManager.java
#	app/src/main/java/com/halo/assistant/fragment/WebFragment.java
#	dependencies.gradle
2021-10-26 16:28:25 +08:00
a1ecb784e2 完成实名认证第二期优化 https://git.ghzs.com/pm/halo-app-issues/-/issues/1526 2021-10-26 16:16:08 +08:00
1f43b8b220 修复专题列表游戏大小不受后台配置控制的问题 2021-10-22 10:35:41 +08:00
88098a8255 适配接口返回 DSA 形式的游戏签名 2021-10-21 18:07:02 +08:00
lyr
37460ceac2 优化首页滑动到专题合集-排行榜样式刷新加载 2021-10-18 18:09:21 +08:00
lyr
6dfded50d6 Merge remote-tracking branch 'origin/dev' into dev 2021-10-18 11:37:12 +08:00
lyr
5e01ecf0a2 修改视频发布页-内容来源-转载网址弹窗无法触发回调问题 2021-10-18 11:37:06 +08:00
a1be42a135 【光环助手V5.4.0】后台优化汇总第三周(10(2)) https://git.ghzs.com/pm/halo-app-issues/-/issues/1564
(cherry picked from commit 5bdd655715)
2021-10-18 11:34:45 +08:00
lyr
f4a5dd47fc 【光环助手V5.2.0&V5.3.0】同步正式环境后测试问题汇总(13)https://git.ghzs.com/pm/halo-app-issues/-/issues/1565 2021-10-15 18:15:16 +08:00
lyr
b9554d11a8 【光环助手V5.2.0&V5.3.0】同步正式环境后测试问题汇总(12)https://git.ghzs.com/pm/halo-app-issues/-/issues/1565 2021-10-13 11:36:52 +08:00
b910075c4c Merge remote-tracking branch 'origin/dev-5.3.0' into dev 2021-10-13 09:40:23 +08:00
1966e844bd 【光环助手V5.2.0&V5.3.0】同步正式环境后测试问题汇总 (8,9,10) https://git.ghzs.com/pm/halo-app-issues/-/issues/1565 2021-10-12 17:46:12 +08:00
lyr
bfa236f802 【光环助手V5.4.0】新社区5期-活动主页(3)https://git.ghzs.com/pm/halo-app-issues/-/issues/1535 2021-10-12 16:24:28 +08:00
lyr
7f64b51582 适配评论详情Footer显示 2021-10-12 15:31:32 +08:00
4324126660 Merge branch 'dev-5.3.0' of git.ghzs.com:halo/android/assistant-android into dev-5.3.0 2021-10-12 15:22:37 +08:00
a8c54de47d 【光环助手V5.2.0&V5.3.0】同步正式环境后测试问题汇总(6) https://git.ghzs.com/pm/halo-app-issues/-/issues/1565 2021-10-12 15:22:28 +08:00
562b3e9d3b 【光环助手V5.2.0&V5.3.0】同步正式环境后测试问题汇总 (1) https://git.ghzs.com/pm/halo-app-issues/-/issues/1565 2021-10-12 10:59:29 +08:00
679c6da972 修复礼包中心、工具箱清理搜索文字功能实效的问题 2021-10-11 18:22:51 +08:00
lyr
793adb9a03 解决首页滑动到专题合集-排行榜样式卡顿问题 2021-10-11 18:10:21 +08:00
23c62f1092 缺省渠道改为 GH_TEST3 2021-10-11 17:25:07 +08:00
516d8d8044 正式环境切换到 5.3.0 的接口 2021-10-11 10:35:08 +08:00
ed2b41bc55 【光环助手V5.3.0】Q&A管理-事件埋点(1009测试1) https://git.ghzs.com/pm/halo-app-issues/-/issues/1393 2021-10-11 09:10:29 +08:00
5351ffd6b6 【光环助手V5.3.0】Q&A管理-事件埋点(替换埋点字段qa_title为help_id) https://git.ghzs.com/pm/halo-app-issues/-/issues/1393 2021-10-09 11:56:53 +08:00
c9bef1fb1a 修复 MUMU 模拟器无法显示部分图片的问题 2021-09-29 14:19:56 +08:00
454e8bb934 修复 MUMU 模拟器无法显示部分图片的问题 2021-09-29 12:00:41 +08:00
e587d0daaa 【光环助手V5.3.0】通用链接内容合集-客户端需求(0928测试:5) https://git.ghzs.com/pm/halo-app-issues/-/issues/1473 2021-09-28 20:24:33 +08:00
fcc23ddfc2 修改部分弹窗为居中样式 2021-09-28 18:51:10 +08:00
c541805bed 【光环助手V5.3.0】通用链接内容合集-客户端需求(0928补充:1) https://git.ghzs.com/pm/halo-app-issues/-/issues/1473 2021-09-28 17:45:35 +08:00
lyr
2c38c8fd91 优化专题合集-排行榜样式标签加载 2021-09-28 17:10:36 +08:00
15385cf78c 统一视频贴加精/取消加精接口错误处理 2021-09-28 16:33:52 +08:00
6df3a8ec51 【光环助手V5.3.0】新社区4期-详情页分享面板权限优化(0922 产品验收问题6) https://git.ghzs.com/pm/halo-app-issues/-/issues/1501 2021-09-28 16:13:10 +08:00
bca9c6d96f Merge branch 'feature-optimise_home_page_performance' into 'dev-5.3.0'
优化首页列表性能

See merge request halo/android/assistant-android!126
2021-09-28 16:04:19 +08:00
26feccc28b 优化首页列表性能 2021-09-28 16:04:18 +08:00
a1fc7d2e48 Merge remote-tracking branch 'origin/dev-5.3.0' into dev-5.3.0 2021-09-28 15:42:29 +08:00
0b42b10e41 【光环助手V5.3.0】游戏详情-开服相关优化(20210928UI测试)https://git.ghzs.com/pm/halo-app-issues/-/issues/1469 2021-09-28 15:42:11 +08:00
lyr
0a0912dbab 微调游戏搜索-专题样式UI 2021-09-28 15:32:55 +08:00
5e3ff2de31 Merge branch 'dev-5.3.0' of git.ghzs.com:halo/android/assistant-android into dev-5.3.0 2021-09-28 15:31:48 +08:00
55db52f8f0 【光环助手V5.3.0】新社区4期-提问帖详情页评论区权限优化(0923 产品验收问题2) https://git.ghzs.com/pm/halo-app-issues/-/issues/1502 2021-09-28 15:31:43 +08:00
2888ab56cd 【光环助手V5.3.0】客户端内部需求(整理简单弹窗样式测试)https://git.ghzs.com/pm/halo-app-issues/-/issues/1527 2021-09-28 15:30:45 +08:00
1edc20f665 更改通用链接合集数据结构 2021-09-28 14:34:25 +08:00
72c818b17b 【光环助手V5.3.0】游戏详情-开服相关优化(20210927UI测试补充)https://git.ghzs.com/pm/halo-app-issues/-/issues/1469 2021-09-28 11:49:25 +08:00
7393dc83f5 修改选择视频时删除本地视频闪退问题 2021-09-28 10:27:23 +08:00
6043553a39 【光环助手V5.3.0】开发者中心相关配套需求 https://git.ghzs.com/pm/halo-app-issues/-/issues/1518 2021-09-28 10:25:19 +08:00
lyr
87461b52ce 【光环助手V5.3.0】专题合集-排行榜功能优化(20210927UI测试)https://git.ghzs.com/pm/halo-app-issues/-/issues/1471#note_119397 2021-09-28 10:07:25 +08:00
lyr
df807d746f 修改搜索结果页面加载下一页时,页面会跳动即滑动不流畅问题 2021-09-27 19:07:57 +08:00
lyr
9e7a519806 【光环助手V5.3.0】游戏搜索-客户端功能优化(第8期)(0926测试 第1点、0927测试 第1点、20210927UI测试) https://git.ghzs.com/pm/halo-app-issues/-/issues/1402 2021-09-27 18:35:03 +08:00
6ee4af82ad 【光环助手V5.3.0】光环前端优化汇总第二周(完成0927测试:1) https://git.ghzs.com/pm/halo-app-issues/-/issues/1517 2021-09-27 18:07:28 +08:00
75c8a4597c Merge branch 'dev-5.3.0' of git.ghzs.com:halo/android/assistant-android into dev-5.3.0 2021-09-27 17:21:54 +08:00
de274f0954 修改通用链接详情刷新过程中滑动列表闪退问题 2021-09-27 17:21:35 +08:00
82195950b3 论坛详情精华 tab 支持视频贴 https://git.ghzs.com/pm/halo-app-issues/-/issues/1461#note_119153 2021-09-27 17:21:22 +08:00
806b3ac77e 【光环助手V5.3.0】游戏详情-开服相关优化(20210927UI测试)https://git.ghzs.com/pm/halo-app-issues/-/issues/1469 2021-09-27 17:17:44 +08:00
lyr
ae95bd1db8 【光环助手V5.3.0】游戏搜索-数据埋点(0927测试)https://git.ghzs.com/pm/halo-app-issues/-/issues/1403#note_119302 2021-09-27 16:48:33 +08:00
62ffefb92f 论坛发帖添加使用移动网络上传视频的弹窗 2021-09-27 16:10:58 +08:00
1c739a6269 【光环助手V5.3.0】客户端内部需求(整理简单弹窗样式测试)https://git.ghzs.com/pm/halo-app-issues/-/issues/1527 2021-09-27 15:21:54 +08:00
lyr
f5386cd3c1 优化专题合集-排行榜样式显示更新逻辑 2021-09-27 15:16:06 +08:00
lyr
7540659396 【光环助手V5.3.0】专题合集-排行榜功能优化(20210926UI测试)https://git.ghzs.com/pm/halo-app-issues/-/issues/1471#note_119218 2021-09-27 11:56:21 +08:00
056985531b 【光环助手V5.3.0】新社区4期-详情页分享面板权限优化(0922 产品验收问题5) https://git.ghzs.com/pm/halo-app-issues/-/issues/1501 2021-09-27 11:07:36 +08:00
730739077b 【光环助手V5.3.0】游戏详情-开服相关优化(0926测试)https://git.ghzs.com/pm/halo-app-issues/-/issues/1469 2021-09-27 09:41:15 +08:00
6c82184d1d 【光环助手V5.3.0】通用链接内容合集-客户端需求(20210926UI测试) https://git.ghzs.com/pm/halo-app-issues/-/issues/1473 2021-09-26 20:15:05 +08:00
d3b6f6a1e4 修改通用链接列表滑动闪退问题 2021-09-26 17:29:42 +08:00
lyr
d35550900b 【光环助手V5.3.0】专题合集-排行榜功能优化(0926测试 2)https://git.ghzs.com/pm/halo-app-issues/-/issues/1471#note_119210 2021-09-26 17:11:30 +08:00
dfc16e38eb 修改游戏详情标签被切割问题 2021-09-26 15:28:01 +08:00
08bd5f3081 【光环助手V5.3.0】新社区4期-详情页分享面板权限优化(0922 产品验收问题4) https://git.ghzs.com/pm/halo-app-issues/-/issues/1501 2021-09-26 11:08:02 +08:00
7d98fde53a Merge branch 'dev-5.3.0' of git.ghzs.com:halo/android/assistant-android into dev-5.3.0
# Conflicts:
#	app/src/main/java/com/gh/gamecenter/entity/LinkEntity.kt
2021-09-26 10:38:50 +08:00
486e9ae0c5 【光环助手V5.3.0】通用链接内容合集-数据埋点(测试补充:3(1)(3),4) https://git.ghzs.com/pm/halo-app-issues/-/issues/1474 2021-09-26 10:37:32 +08:00
d51587aa66 【光环助手V5.3.0】通用链接内容合集-数据埋点 (0924测试 2) https://git.ghzs.com/pm/halo-app-issues/-/issues/1474 2021-09-26 10:34:13 +08:00
1fabcc756d 视频帖详情页的埋点新增字段 2021-09-26 09:44:45 +08:00
ee3fc058e1 【光环助手V5.3.0】通用链接内容合集-客户端需求(0924测试3,4,5) https://git.ghzs.com/pm/halo-app-issues/-/issues/1473 2021-09-24 17:59:02 +08:00
lyr
e66e9e1ba8 【光环助手V5.3.0】游戏搜索-客户端功能优化(第8期)(20210917测试问题 2、9) https://git.ghzs.com/pm/halo-app-issues/-/issues/1402#note_118197 2021-09-24 17:28:44 +08:00
1cb4bea73c 修改QA详情关闭页面由web端控制 2021-09-24 16:37:59 +08:00
lyr
b3a491e723 【光环助手V5.3.0】专题合集-排行榜功能优化(2、3)https://git.ghzs.com/pm/halo-app-issues/-/issues/1471 2021-09-24 15:28:37 +08:00
9709c6cbf0 【光环助手V5.3.0】光环前端优化汇总第二周 (0922测试 1) https://git.ghzs.com/pm/halo-app-issues/-/issues/1517 2021-09-23 18:32:19 +08:00
ca81eb2ecb 【光环助手V5.3.0】游戏管理-镜像设置优化 https://git.ghzs.com/pm/halo-app-issues/-/issues/1400 2021-09-23 17:58:32 +08:00
910d886f65 【光环助手V5.3.0】通用链接内容合集-数据埋点 (二) https://git.ghzs.com/pm/halo-app-issues/-/issues/1474 2021-09-23 17:18:44 +08:00
376411c0cf 调整申请版主按钮点击逻辑 2021-09-23 17:12:44 +08:00
ad019eff69 修改申请版主弹窗REQUEST_CODE 2021-09-23 16:43:42 +08:00
3712e67594 修改通用链接详情列表滑动闪退问题 2021-09-23 16:40:52 +08:00
c98084ec39 Merge remote-tracking branch 'origin/dev-5.3.0' into dev-5.3.0 2021-09-23 16:25:46 +08:00
5a851ea99d 调整申请版主按钮点击逻辑 2021-09-23 16:25:39 +08:00
849cdb234c 调整通用链接合集图片圆角 2021-09-23 15:43:54 +08:00
9f9313b5e7 调整申请版主按钮点击逻辑 2021-09-23 15:40:12 +08:00
1888d8cfc7 修改通用链接合集列表复用问题 2021-09-23 12:09:06 +08:00
d8f9a7ed7f 【光环助手V5.3.0】[安装方式管理]新增[白名单]功能https://git.ghzs.com/pm/halo-app-issues/-/issues/1264 2021-09-23 11:09:52 +08:00
7be4541f89 调整帖子/提问详情间距 2021-09-23 10:29:24 +08:00
2d84309f04 【光环助手V5.3.0】新社区4期-详情页布局优化(0916UI测试问题6,10) https://git.ghzs.com/pm/halo-app-issues/-/issues/1500 2021-09-22 17:05:09 +08:00
f2ed7f8b72 【光环助手V5.3.0】通用链接内容合集-数据埋点(一) https://git.ghzs.com/pm/halo-app-issues/-/issues/1474 2021-09-22 16:47:31 +08:00
c839d535d8 【光环助手V5.3.0】通用链接内容合集-客户端需求(20210918测试问题5) https://git.ghzs.com/pm/halo-app-issues/-/issues/1473 2021-09-22 11:24:37 +08:00
623e155c75 【光环助手V5.3.0】光环前端优化汇总第三周 (4) https://git.ghzs.com/pm/halo-app-issues/-/issues/1529 2021-09-18 17:16:36 +08:00
b20a74de00 整理下载管理类方法描述 2021-09-18 15:52:11 +08:00
c9402dec4e 整理下载管理类方法描述 2021-09-18 15:39:51 +08:00
636a826583 【光环助手V5.3.0】后台优化汇总第三周(10) https://git.ghzs.com/pm/halo-app-issues/-/issues/1525 2021-09-18 10:41:25 +08:00
4aacc425a9 简单弹窗样式整理https://git.ghzs.com/halo/android/assistant-android/-/issues/39 2021-09-17 16:28:24 +08:00
lyr
c7db544684 【光环助手V5.3.0】光环前端优化汇总第三周(2)https://git.ghzs.com/pm/halo-app-issues/-/issues/1529 2021-09-17 16:09:21 +08:00
c7833bae11 处理游戏实体数组越界问题 2021-09-17 15:53:00 +08:00
lyr
3d777781b8 【光环助手V5.3.0】新社区4期-游戏详情页埋点 https://git.ghzs.com/pm/halo-app-issues/-/issues/1468 2021-09-17 11:57:21 +08:00
b72f9819d7 【光环助手V5.3.0】光环前端优化汇总第三周(3) https://git.ghzs.com/pm/halo-app-issues/-/issues/1529 2021-09-16 16:59:22 +08:00
5f134fad33 【光环助手V5.3.0】新社区4期-详情页布局优化(0916UI测试问题 2,6,7,8,9,10) https://git.ghzs.com/pm/halo-app-issues/-/issues/1500 2021-09-16 16:09:36 +08:00
21cec51fad 优化首页滑动流畅度 2021-09-16 15:19:54 +08:00
048d3aa361 【光环助手V5.3.0】光环前端优化汇总第三周(1) https://git.ghzs.com/pm/halo-app-issues/-/issues/1529 2021-09-16 14:24:44 +08:00
lyr
8871d12fa8 微信分享图片改用FileProvider方式分享 2021-09-16 11:43:54 +08:00
lyr
67f931f9c8 修复游戏动态-分享图片过程中保存路径与分享路径不一致的问题 2021-09-16 11:15:57 +08:00
lyr
2fe9d6c3e1 修复游戏动态-分享图片过程中保存路径与分享路径不一致的问题 2021-09-16 11:05:51 +08:00
a662d5b90c Merge branch 'feature-issue1473' into dev-5.3.0 2021-09-16 10:52:56 +08:00
c30ec4bd87 【光环助手V5.3.0】通用链接内容合集-客户端需求 https://git.ghzs.com/pm/halo-app-issues/-/issues/1473 2021-09-16 10:49:48 +08:00
094a8abfe2 Merge branch 'feature-v2signature_verifier' into 'dev-5.3.0'
提高 V2 签名公钥获取效率

See merge request halo/android/assistant-android!124
2021-09-15 11:36:57 +08:00
e1c39e90a9 提高 V2 签名公钥获取效率 2021-09-15 11:36:57 +08:00
aca3f2d2fc 【光环助手V5.3.0】曝光事件数据埋点优化 https://git.ghzs.com/pm/halo-app-issues/-/issues/1322 2021-09-15 10:02:02 +08:00
lyr
b5d8d40462 【光环助手V5.3.0】游戏搜索-数据埋点 https://git.ghzs.com/pm/halo-app-issues/-/issues/1403 2021-09-14 18:25:35 +08:00
c1b7c380da 【光环助手V5.3.0】曝光事件数据埋点优化 https://git.ghzs.com/pm/halo-app-issues/-/issues/1322 2021-09-14 16:21:54 +08:00
e4f0ba0495 Merge branch 'cherry-pick-92941bcc' into 'dev'
修复新分类-精选-专题合集图片无法正常显示问题

See merge request halo/android/assistant-android!123
2021-09-14 15:53:12 +08:00
lyr
ccd4a479a9 修复新分类-精选-专题合集图片无法正常显示问题
(cherry picked from commit 92941bcc38)
2021-09-14 15:52:20 +08:00
lyr
92941bcc38 修复新分类-精选-专题合集图片无法正常显示问题 2021-09-14 15:49:50 +08:00
6b55875547 调整开服快速填写弹窗逻辑 2021-09-14 11:06:00 +08:00
a0947a84d9 Merge remote-tracking branch 'origin/dev' into dev-5.3.0 2021-09-14 10:33:47 +08:00
fca0e03180 【光环助手V5.3.0】曝光事件数据埋点优化 (4) https://git.ghzs.com/pm/halo-app-issues/-/issues/1322 2021-09-14 10:31:28 +08:00
3dad7d8850 【光环助手V5.3.0】曝光事件数据埋点优化 20210913测试问题(2) 2021-09-13 18:46:31 +08:00
1ebe8eba4e 【光环助手V5.3.0】曝光事件数据埋点优化 (20210913测试问题 1~3) https://git.ghzs.com/pm/halo-app-issues/-/issues/1322 2021-09-13 18:27:09 +08:00
lyr
3ec3388b08 【光环助手V5.3.0】游戏搜索-客户端功能优化(第8期)https://git.ghzs.com/pm/halo-app-issues/-/issues/1402 2021-09-13 18:22:27 +08:00
71c2e8eb6b 移除WebFragment多余代码 2021-09-13 17:49:55 +08:00
009aa9f73e ROM 信息获取支持鸿蒙 2021-09-13 14:49:19 +08:00
14e38bc738 调整开服快速填写弹窗UI 2021-09-13 11:20:27 +08:00
74ffd80b10 处理评论列表卡片不存在机型信息时版本信息显示错位的问题 2021-09-12 18:08:22 +08:00
cb7a04ff09 修复长按环境标签不能保存部分页面快捷跳转的问题 2021-09-12 17:24:49 +08:00
96640ad91e 修复长按环境标签不能保存部分页面快捷跳转的问题 2021-09-12 16:44:34 +08:00
91017caac4 Merge remote-tracking branch 'origin/dev-5.3.0' into dev-5.3.0 2021-09-10 18:13:48 +08:00
5d8720687a 【光环助手V5.3.0】曝光事件数据埋点优化 https://git.ghzs.com/pm/halo-app-issues/-/issues/1322 2021-09-10 18:11:34 +08:00
75b1234376 Merge branch 'feature-download_btn' into dev-5.3.0 2021-09-10 16:44:28 +08:00
1e866a9eab 【光环助手V5.3.0】新社区4期-详情页布局优化(调整页面边距) https://git.ghzs.com/pm/halo-app-issues/-/issues/1500 2021-09-10 16:31:14 +08:00
80e58080c4 整理 WebFragment/WebActivity 的代码https://git.ghzs.com/halo/android/assistant-android/-/issues/36 2021-09-10 16:29:00 +08:00
394036a0d0 处理首页内存泄露问题 2021-09-10 15:53:11 +08:00
2f2355f36e 首页视频封面图改用fresco加载 2021-09-10 11:37:27 +08:00
e711e30c21 Update README.md 2021-09-09 11:45:12 +08:00
e3d32057ee 统一处理列表下载按钮文案和样式(优化) https://git.ghzs.com/halo/android/assistant-android/-/issues/10 2021-09-09 11:42:18 +08:00
e42c4e595e 【光环助手V5.3.0】埋点数据问题汇总[3(2)] https://git.ghzs.com/pm/halo-app-issues/-/issues/1369 2021-09-09 09:38:17 +08:00
73d71e5dac qa详情链接增加qa_title参数 2021-09-09 09:31:01 +08:00
d6cad14e7b Merge branch 'dev' into dev-5.3.0
# Conflicts:
#	app/src/main/java/com/gh/gamecenter/fragment/HomeSearchToolWrapperFragment.kt
2021-09-08 20:18:15 +08:00
lyr
9beaf93595 调整 DialogFragment 的实例化代码(优化)https://git.ghzs.com/halo/android/assistant-android/-/issues/38#note_117062 2021-09-08 18:46:17 +08:00
074610a9f0 统一处理列表下载按钮文案和样式 https://git.ghzs.com/halo/android/assistant-android/-/issues/10 2021-09-08 18:04:10 +08:00
7881edaf21 Merge remote-tracking branch 'origin/dev-5.3.0' into dev-5.3.0 2021-09-08 17:05:30 +08:00
f2643308f9 【光环助手V5.3.0】游戏详情-开服相关优化(2)https://git.ghzs.com/pm/halo-app-issues/-/issues/1469 2021-09-08 17:05:17 +08:00
1168be9a92 更新 leakCanary 和 lottie 的依赖版本 2021-09-08 16:12:03 +08:00
c34d6141bc 【光环助手V5.3.0】游戏详情-开服相关优化(1)https://git.ghzs.com/pm/halo-app-issues/-/issues/1469 2021-09-08 15:31:12 +08:00
e1f62e3a3a 去掉不必要的 armeabi 2021-09-08 14:55:20 +08:00
1d067dbaa2 【光环助手V5.3.0】Q&A管理-事件埋点(1(1)-(4)) https://git.ghzs.com/pm/halo-app-issues/-/issues/1393 2021-09-08 10:44:17 +08:00
dca3542cec 停用游戏替换数量统计 2021-09-07 18:12:49 +08:00
lyr
4a37af4ce4 调整 DialogFragment 的实例化代码 https://git.ghzs.com/halo/android/assistant-android/-/issues/38 2021-09-07 17:31:49 +08:00
a2e907faeb 调整新游戏更新页面的隐藏逻辑 2021-09-07 15:28:07 +08:00
7e33243fbb 【光环助手V5.3.0】游戏详情-开服相关优化(2)https://git.ghzs.com/pm/halo-app-issues/-/issues/1469 2021-09-07 15:04:00 +08:00
a4603213b9 修复新游戏更新页面其它游戏版本忽略更新无效的问题 2021-09-07 11:56:02 +08:00
da9ea61fbf 显示版本更新为 5.3.0 2021-09-07 11:08:33 +08:00
4ceef741ea 【光环助手V5.3.0】光环前端优化汇总第二周 (3, 4) https://git.ghzs.com/pm/halo-app-issues/-/issues/1517 2021-09-07 11:06:21 +08:00
d323fc07f9 【光环助手V5.3.0】光环前端优化汇总第二周(1) https://git.ghzs.com/pm/halo-app-issues/-/issues/1517 2021-09-06 18:19:38 +08:00
2356e175f9 【光环助手V5.3.0】光环前端优化汇总第二周(2) https://git.ghzs.com/pm/halo-app-issues/-/issues/1517 2021-09-06 18:00:52 +08:00
7a9c0ed8f2 修复光环插件更新没有在新游戏更新页面显示的问题 2021-09-06 17:42:51 +08:00
d406cf3fcb 【光环助手V5.3.0】新社区4期-详情页分享面板权限优化 https://git.ghzs.com/pm/halo-app-issues/-/issues/1501 2021-09-06 15:16:13 +08:00
456519e06c 优化首页滑动改变背景颜色性能问题 2021-09-03 17:01:53 +08:00
638e0c988f 【光环助手V5.3.0】新社区4期-提问帖详情页评论区权限优化 https://git.ghzs.com/pm/halo-app-issues/-/issues/1502 2021-09-03 16:02:18 +08:00
40aaabad7d 测试环境api版本改为v5d3d0 2021-09-03 15:49:11 +08:00
46cf8cfbdc 网页保存图片到图库添加存储权限检测 2021-09-03 15:32:58 +08:00
2226bcf515 调整游戏更新页面我的版本的筛选逻辑 2021-09-03 15:22:56 +08:00
38dcdaf8e3 修复社区 tab 选中异常问题 2021-09-03 11:15:38 +08:00
2a440e91fa 修改首页点击tab切换背景异常问题 2021-09-03 10:16:02 +08:00
2cecb9a14c 修复随机生成数组的越界问题 2021-09-02 19:05:25 +08:00
241f976866 修复返回活动页面显示异常的问题 2021-09-02 16:32:02 +08:00
560c270723 添加初始化微博 SDK 异常日志 2021-09-02 12:36:47 +08:00
5159957722 【光环助手V5.3.0】Q&A管理-前端优化需求(第三期)(1(3),2) https://git.ghzs.com/pm/halo-app-issues/-/issues/1394 2021-09-02 11:14:32 +08:00
e45218de17 微调游戏详情详情信息间距 2021-09-02 09:45:04 +08:00
0ff34bd3f7 【光环助手V5.3.0】Q&A管理-前端优化需求(第三期)(1(1)(2),3) https://git.ghzs.com/pm/halo-app-issues/-/issues/1394 2021-09-01 17:59:11 +08:00
lyr
4a04f6bb0f 修复开服表游戏分类筛选结果为空而导致的闪退问题 2021-09-01 12:35:46 +08:00
633b7df021 升级 QQ/微信 SDK https://git.ghzs.com/halo/android/assistant-android/-/issues/37 2021-09-01 11:47:57 +08:00
389c20b6bf 更改游戏安装引导显示逻辑 2021-09-01 10:10:58 +08:00
20149b6041 更改游戏安装引导显示逻辑 2021-08-31 20:57:16 +08:00
b764a43abe Merge remote-tracking branch 'origin/dev' into dev 2021-08-31 18:08:20 +08:00
c3fb8b9c89 修复帖子详情页底部的阴影显示问题 2021-08-31 18:08:09 +08:00
d574b09dfa 完善安装引导显示逻辑 2021-08-31 17:11:13 +08:00
7fd7197909 版本升级至 5.1.2 2021-08-31 15:12:20 +08:00
58178d2871 Merge branch 'hotfix-v5.1.1-371-crash' into 'release'
处理一系列闪退

See merge request halo/android/assistant-android!122
2021-08-31 15:09:32 +08:00
880838c263 处理一系列闪退
1. 修复文本自动填充偶发的闪退
2. 修复软键盘弹起时切换页面触发的闪退
3. 修复在后台执行下载任务时偶发的闪退
4. 修复用户发表旧视频内容在新版本个人主页浏览时的闪退
5. 修复游戏详情专区返回时偶发的闪退
2021-08-31 15:06:33 +08:00
c196d5cdf5 【光环助手V5.2.0】小米等设备第三方安装引导功能-客户端(测试1、2、4,数据埋点0830测试)https://git.ghzs.com/pm/halo-app-issues/-/issues/1410 2021-08-31 14:23:37 +08:00
c370edaa72 Merge remote-tracking branch 'origin/dev-5.2.0' into dev-5.2.0 2021-08-30 21:38:14 +08:00
0640aee554 更新游戏安装引导接口 2021-08-30 21:37:06 +08:00
8a5f11e6bd 移除测试用旧游戏更新 tab 2021-08-30 21:31:43 +08:00
92b6ebcb08 更新游戏安装引导接口 2021-08-30 21:28:49 +08:00
758c331af5 【光环助手V5.2.0】新社区3期-论坛展示-社区搜索页(20200830UI测试) https://git.ghzs.com/pm/halo-app-issues/-/issues/1405 2021-08-30 19:12:49 +08:00
17e5c0535d Merge branch 'hotfix-v5.1.1-indicator' into 'release'
修改首页 indicator 不显示问题

See merge request halo/android/assistant-android!121
2021-08-30 18:06:01 +08:00
66fee84b49 Merge branch 'hotfix-v5.1.1-setting' into 'release'
修复"游戏下载设置页面的自动安装游戏选项和自动关注游戏选项默认值错误"问题

See merge request halo/android/assistant-android!120
2021-08-30 18:00:20 +08:00
lyr
bbe0350f2c 修复"游戏下载设置页面的自动安装游戏选项和自动关注游戏选项默认值错误"问题 2021-08-30 17:57:19 +08:00
b31fe4b94a Merge branch 'dev-5.2.0' of git.ghzs.com:halo/android/assistant-android into dev-5.2.0 2021-08-30 17:46:45 +08:00
11b414fd29 修改首页indicator不显示问题 2021-08-30 17:45:48 +08:00
1a35b5ded0 修改首页indicator不显示问题 2021-08-30 17:35:50 +08:00
4245c1511e Merge remote-tracking branch 'origin/release' into dev-5.2.0
# Conflicts:
#	dependencies.gradle
2021-08-30 17:22:45 +08:00
989c3706dd 【光环助手V5.2.0】专题推荐图标功能优化 (20210826测试问题 3) https://git.ghzs.com/pm/halo-app-issues/-/issues/1390 2021-08-30 17:09:17 +08:00
49500768db 修复正式环境微博分享内容不显示的问题 2021-08-30 16:28:17 +08:00
65becdac85 【光环助手V5.2.0】下载管理相关优化 (0830UI测试) https://git.ghzs.com/pm/halo-app-issues/-/issues/1244 2021-08-30 15:49:42 +08:00
125c78fccd 【光环助手V5.2.0】光环前端优化汇总第四周 (0827测试) https://git.ghzs.com/pm/halo-app-issues/-/issues/1454 2021-08-30 15:44:14 +08:00
lyr
58c576b950 【光环助手V5.2.0】开服表相关优化(0827测试 2)https://git.ghzs.com/pm/halo-app-issues/-/issues/1220#note_115509 2021-08-30 14:25:53 +08:00
b08aa46992 【光环助手V5.2.0】下载管理相关优化 (0830测试) https://git.ghzs.com/pm/halo-app-issues/-/issues/1244 2021-08-30 14:20:25 +08:00
c77f0cf817 完成下载管理相关优化(20210827UI测试问题 & 0827测试) https://git.ghzs.com/pm/halo-app-issues/-/issues/1244 2021-08-30 11:44:42 +08:00
bb0378c216 【光环助手V5.2.0】游戏礼包优化(第二期)(0830测试2) https://git.ghzs.com/pm/halo-app-issues/-/issues/1325 2021-08-30 11:26:35 +08:00
f2e7a64e99 【光环助手V5.2.0】适龄等级提示(0830测试反馈2) https://git.ghzs.com/pm/halo-app-issues/-/issues/1397 2021-08-30 10:04:59 +08:00
759cacf933 更新游戏安装引导实体类 2021-08-27 17:32:34 +08:00
lyr
0b213253ce 【光环助手V5.2.0】开服表相关优化(0827测试 1)https://git.ghzs.com/pm/halo-app-issues/-/issues/1220#note_115509 2021-08-27 16:11:56 +08:00
f0ab333892 【光环助手V5.2.0】首页轮播图显示背景色功能(20210826UI测试问题) https://git.ghzs.com/pm/halo-app-issues/-/issues/1321 2021-08-27 16:06:26 +08:00
lyr
7516db3a60 【光环助手V5.2.0】开服表相关优化(0827测试 1)https://git.ghzs.com/pm/halo-app-issues/-/issues/1220#note_115509 2021-08-27 15:54:34 +08:00
lyr
29cd798ca8 【光环助手V5.2.0】开服表相关优化(0826测试 第3、4、5、6点)https://git.ghzs.com/pm/halo-app-issues/-/issues/1220#note_115199 2021-08-27 12:00:57 +08:00
40cb902baf Merge branch 'dev-5.2.0' of git.ghzs.com:halo/android/assistant-android into dev-5.2.0 2021-08-27 11:51:07 +08:00
15bbcf3eae 完成下载管理相关优化(0826测试 2~5) https://git.ghzs.com/pm/halo-app-issues/-/issues/1244 2021-08-27 11:49:06 +08:00
f508e103c1 【光环助手V5.2.0】适龄等级提示(20210827UI测试问题) https://git.ghzs.com/pm/halo-app-issues/-/issues/1397 2021-08-27 11:38:38 +08:00
94fa36125d Merge branch 'dev-5.2.0' of git.ghzs.com:halo/android/assistant-android into dev-5.2.0 2021-08-27 10:41:59 +08:00
1c754a6dd0 【光环助手V5.2.0】首页轮播图显示背景色功能(20210827测试问题1) https://git.ghzs.com/pm/halo-app-issues/-/issues/1321 2021-08-27 10:41:42 +08:00
609e4a49b8 完成光环前端优化汇总第三周(0827测试 1,2) https://git.ghzs.com/pm/halo-app-issues/-/issues/1449 2021-08-27 09:45:18 +08:00
98710c9542 Merge remote-tracking branch 'origin/dev-5.2.0' into dev-5.2.0 2021-08-26 18:34:33 +08:00
0e2f5fdf53 【光环助手V5.2.0】浏览器安装功能优化(0826测试、0826UI测试)https://git.ghzs.com/pm/halo-app-issues/-/issues/1323 2021-08-26 18:34:01 +08:00
e9ed5530d5 Merge branch 'dev-5.2.0' of git.ghzs.com:halo/android/assistant-android into dev-5.2.0 2021-08-26 18:28:36 +08:00
113f1d3ed8 【光环助手V5.2.0】新社区3期-论坛展示-头像信息(20210826UI测试) https://git.ghzs.com/pm/halo-app-issues/-/issues/1406 2021-08-26 18:28:26 +08:00
3fc60a57b6 完成光环前端优化汇总第四周(1) https://git.ghzs.com/pm/halo-app-issues/-/issues/1454 2021-08-26 18:18:52 +08:00
d14e09187e 完成光环前端优化汇总第三周(1) 2021-08-26 18:10:26 +08:00
bf90131841 完成开服表相关优化(0826测试第7点) https://git.ghzs.com/pm/halo-app-issues/-/issues/1220 2021-08-26 17:22:39 +08:00
40b7a16b21 Merge branch 'dev-5.2.0' of git.ghzs.com:halo/android/assistant-android into dev-5.2.0 2021-08-26 16:26:43 +08:00
882d7273a1 【光环助手V5.2.0】首页轮播图显示背景色功能(20210826测试问题1) https://git.ghzs.com/pm/halo-app-issues/-/issues/1321 2021-08-26 16:26:38 +08:00
6cfeddf3d4 处理首页 debug 状态闪退 2021-08-26 11:43:38 +08:00
lyr
a06e6c581d 优化开测表加载逻辑 2021-08-26 11:04:52 +08:00
bd8bda40ec 【光环助手V5.2.0】新社区3期-论坛展示-头像信息(0825 产品验收问题2) https://git.ghzs.com/pm/halo-app-issues/-/issues/1406 2021-08-26 10:42:08 +08:00
efd81a4e0c 版本升级到 5.1.1 2021-08-26 10:22:00 +08:00
493e08ce2c Merge branch 'hotfix-v5.1.0-370-crash' into 'release'
修复 5.1.0-370 的一些闪退

See merge request halo/android/assistant-android!119
2021-08-26 10:21:23 +08:00
1906451060 修复一系列闪退问题
1. 修复因为特殊处理光遇更新判断而触发的其它下载闪退问题
2. 修复帖子视频详情页点击点赞/关注等按钮触发一键登录时的闪退问题
3. 修复下载完成点击通知栏下载完成通知偶发的闪退问题
4. 修复从首页安利墙发表新安利后点击到达评论可能触发的闪退问题
5. 修复论坛详情在页面被内存回收重建时下拉刷新的闪退问题
6. 修复游戏详情页浏览专区在页面被内存回收重建时点返回按钮偶发的闪退问题
7. 修复开测表列表不存在推荐标签时的闪退问题
2021-08-26 10:17:08 +08:00
f865e95ae1 Revert "临时简单处理光遇游戏包点击更新时会卡住的问题"
This reverts commit 86edc8b9
2021-08-25 18:23:30 +08:00
480e18b79a 完成开服表相关优化(0825测试2) https://git.ghzs.com/pm/halo-app-issues/-/issues/1220#note_114996 2021-08-25 18:02:17 +08:00
6436bc6191 修复下载管理优化 0824测试问题 https://git.ghzs.com/pm/halo-app-issues/-/issues/1244 2021-08-25 18:00:24 +08:00
7ec7eb29e4 【光环助手V5.2.0】新社区3期-论坛展示-社区搜索页 https://git.ghzs.com/pm/halo-app-issues/-/issues/1405 2021-08-25 17:47:04 +08:00
5deb744ba3 Merge branch 'dev-5.2.0' of git.ghzs.com:halo/android/assistant-android into dev-5.2.0 2021-08-25 14:56:06 +08:00
e908939560 【光环助手V5.2.0】新社区3期-论坛展示-头像信息(0825 产品验收问题1,2,3) https://git.ghzs.com/pm/halo-app-issues/-/issues/1406 2021-08-25 14:55:55 +08:00
e02708bb0e 【光环助手V5.2.0】浏览器安装功能优化(0825测试)https://git.ghzs.com/pm/halo-app-issues/-/issues/1323 2021-08-25 14:52:41 +08:00
4826c35a70 【光环助手V5.2.0】新社区3期-论坛展示-内容举报弹窗(0825 产品验收问题1) https://git.ghzs.com/pm/halo-app-issues/-/issues/1421 2021-08-25 11:52:59 +08:00
c51f185438 Merge branch 'dev-5.2.0' of git.ghzs.com:halo/android/assistant-android into dev-5.2.0 2021-08-24 18:07:40 +08:00
65bd15b573 处理首页轮播图Tab切换颜色问题 2021-08-24 18:07:30 +08:00
5283254c1a Merge remote-tracking branch 'origin/dev-5.2.0' into dev-5.2.0 2021-08-24 17:41:53 +08:00
15458df982 添加UI通用组件 2021-08-24 17:41:22 +08:00
0e27e8ee3b 处理礼包详情使用说明不显示问题 2021-08-24 14:29:05 +08:00
17455efe82 应用版本更新至 5.2.0 2021-08-24 11:45:35 +08:00
e2c80cb416 Merge branch 'dev-5.2.0' of git.ghzs.com:halo/android/assistant-android into dev-5.2.0 2021-08-24 11:33:49 +08:00
a48b4db550 【光环助手V5.2.0】游戏礼包优化(第二期)(4) https://git.ghzs.com/pm/halo-app-issues/-/issues/1325 2021-08-24 11:33:40 +08:00
1d66637b11 补充数据埋点注释 2021-08-24 10:20:07 +08:00
lyr
3e7e98d555 添加在新页面打开全屏webview的JS调用方法 2021-08-23 17:21:44 +08:00
e737d16f0f 【光环助手V5.2.0】小米等设备第三方安装引导功能-数据埋点 https://git.ghzs.com/pm/halo-app-issues/-/issues/1412 2021-08-23 16:16:22 +08:00
a04219518b Merge remote-tracking branch 'origin/dev-5.2.0' into dev-5.2.0
# Conflicts:
#	app/src/main/res/values/colors.xml
2021-08-23 15:39:20 +08:00
ce2c6bed0a 【光环助手V5.2.0】小米等设备第三方安装引导功能-客户端 https://git.ghzs.com/pm/halo-app-issues/-/issues/1410 2021-08-23 15:37:35 +08:00
84a08493e3 优化首页轮播图切换时的背景显示效果 2021-08-23 14:42:32 +08:00
3d411fa49c 优化首页轮播图显示背景色性能问题 2021-08-23 11:55:07 +08:00
c0a0c90a49 Merge branch 'dev-5.2.0' of git.ghzs.com:halo/android/assistant-android into dev-5.2.0 2021-08-23 10:58:02 +08:00
a67d41296e 【光环助手V5.2.0】适龄等级提示 https://git.ghzs.com/pm/halo-app-issues/-/issues/1397 2021-08-23 10:57:50 +08:00
fac255a27c 完成游戏更新相关 https://git.ghzs.com/pm/halo-app-issues/-/issues/1392 2021-08-21 15:27:42 +08:00
0811c8dc15 完成专题推荐图标功能优化 https://git.ghzs.com/pm/halo-app-issues/-/issues/1390 2021-08-21 15:12:04 +08:00
7537963a8e 支持url跳转视频详情 2021-08-21 14:50:33 +08:00
36f8b1de0b Merge branch 'feature-issues1244' into 'dev-5.2.0'
下载管理相关优化

See merge request halo/android/assistant-android!118
2021-08-21 10:57:45 +08:00
1d9e1bc9d8 下载管理相关优化 2021-08-21 10:57:45 +08:00
bb9fd24068 【光环助手V5.1.0】新社区2期-埋点文档(0820 产品验收问题) https://git.ghzs.com/pm/halo-app-issues/-/issues/1303 2021-08-20 16:13:21 +08:00
9c044ae98b Merge branch 'dev-5.2.0' of git.ghzs.com:halo/android/assistant-android into dev-5.2.0 2021-08-20 15:25:37 +08:00
9807883d65 【光环助手V5.2.0】游戏礼包优化(第二期)(1-3) https://git.ghzs.com/pm/halo-app-issues/-/issues/1325 2021-08-20 15:25:26 +08:00
lyr
04e410615d 开服表向上加载末尾增加开服时间 2021-08-20 11:25:42 +08:00
eda1eea2de 完成社区探索之旅(测试汇总8) https://git.ghzs.com/pm/halo-app-issues/-/issues/1285 2021-08-20 10:54:52 +08:00
lyr
c1f6ef35f2 优化开服表页面处理数据逻辑 2021-08-20 10:09:41 +08:00
lyr
b61ce1e42a 【光环助手V5.2.0】开服表相关优化(前端部分)https://git.ghzs.com/pm/halo-app-issues/-/issues/1220 2021-08-19 18:20:10 +08:00
62e60f4309 修改删除社区帖子toast文案 2021-08-19 15:50:17 +08:00
c6f544136f 测试环境api版本改为v5d2d0 2021-08-19 11:13:42 +08:00
d588315759 【光环助手V5.2.0】新社区3期-论坛展示-头像信息 https://git.ghzs.com/pm/halo-app-issues/-/issues/1406 2021-08-19 11:04:19 +08:00
b1fef73c54 【光环助手V5.1.0】新社区运营验收问题(11) https://git.ghzs.com/pm/halo-app-issues/-/issues/1442 2021-08-18 18:54:56 +08:00
c4fc31c963 【光环助手V5.1.0】新社区运营验收问题(17) https://git.ghzs.com/pm/halo-app-issues/-/issues/1442 2021-08-18 18:49:15 +08:00
85b0d0eef4 【光环助手V5.2.0】新社区3期-论坛展示-内容举报弹窗 https://git.ghzs.com/pm/halo-app-issues/-/issues/1421 2021-08-18 17:36:20 +08:00
812eb842e2 【光环助手V5.1.0】新社区运营验收问题(12) https://git.ghzs.com/pm/halo-app-issues/-/issues/1442 2021-08-18 14:43:51 +08:00
783cb95f24 对接版主删除视频评论接口 2021-08-18 11:04:14 +08:00
63b7f294c4 【光环助手V5.2.0】浏览器安装功能优化(1-3)https://git.ghzs.com/pm/halo-app-issues/-/issues/1323 2021-08-18 10:10:48 +08:00
80c95b0f11 【光环助手V5.2.0】首页轮播图显示背景色功能 https://git.ghzs.com/pm/halo-app-issues/-/issues/1321 2021-08-17 17:23:46 +08:00
5969fa2ca5 网页跳转上传视频的论坛类型改为可选 2021-08-17 15:47:45 +08:00
0187608918 【光环助手V5.1.0】新社区运营验收问题(27) https://git.ghzs.com/pm/halo-app-issues/-/issues/1442 2021-08-17 10:57:55 +08:00
e2fa4990bd 重构我的光环页面https://git.ghzs.com/halo/android/assistant-android/-/issues/31 2021-08-16 18:34:38 +08:00
6753d04817 Merge branch 'dev' into dev-5.2.0
# Conflicts:
#	app/build.gradle
2021-08-16 18:26:15 +08:00
98531376d9 Merge remote-tracking branch 'origin/dev' into dev 2021-08-16 15:30:27 +08:00
86edc8b919 临时简单处理光遇游戏包点击更新时会卡住的问题 2021-08-16 15:30:15 +08:00
7903751d85 【光环助手V5.1.0】新社区运营验收问题(23) https://git.ghzs.com/pm/halo-app-issues/-/issues/1442 2021-08-16 14:28:35 +08:00
2620a29a2b Merge branch 'dev' of git.ghzs.com:halo/android/assistant-android into dev 2021-08-16 11:25:57 +08:00
5cc40c09dc 【光环助手V5.1.0】新社区运营验收问题(17) https://git.ghzs.com/pm/halo-app-issues/-/issues/1442 2021-08-16 11:25:46 +08:00
5c02d37852 启动事件添加应用安装来源信息 2021-08-16 11:19:04 +08:00
2e5d445d65 完成暑期好游安利大赏(0813测试汇总反馈 1) https://git.ghzs.com/pm/halo-app-issues/-/issues/1284 2021-08-16 10:19:28 +08:00
c8bae7d89b 【光环助手V5.1.0】新社区运营验收问题(2) https://git.ghzs.com/pm/halo-app-issues/-/issues/1442 2021-08-13 16:20:20 +08:00
a5aceb3d1f 更新微博 SDK https://git.ghzs.com/halo/android/assistant-android/-/issues/30 2021-08-13 16:13:01 +08:00
3ef7343309 Merge branch 'weibo' into dev-5.2.0 2021-08-12 16:41:24 +08:00
2c98c38721 修改视频贴详情原创标签被切割 2021-08-11 23:05:33 +08:00
lyr
4f28c54591 论坛活动url增加分类id参数 2021-08-11 22:47:07 +08:00
06b5b885e9 修改视频帖详情展开按钮是否显示 2021-08-11 21:05:42 +08:00
612cc2ca9b Merge branch 'dev' of git.ghzs.com:halo/android/assistant-android into dev 2021-08-11 18:26:56 +08:00
18f7b695e5 修改发布视频帖内容来源改为必选 2021-08-11 18:26:45 +08:00
f29e2fe1de 添加简单的扩大点击区域方法 2021-08-11 18:14:56 +08:00
799c22093f 处理编译警告 2021-08-11 16:38:06 +08:00
2496c1d96e 修复全屏网页网络异常加载失败时无法返回的问题 2021-08-11 16:28:31 +08:00
8ee76af30f 优化内存占用 2021-08-11 15:53:40 +08:00
lyr
2f35442558 【光环助手V5.1.0】 0809产品测试问题(第14点)https://git.ghzs.com/pm/halo-app-issues/-/issues/1439 2021-08-11 15:38:01 +08:00
lyr
81fa6a6233 【光环助手V5.1.0】 0809产品测试问题(第11点)https://git.ghzs.com/pm/halo-app-issues/-/issues/1439 2021-08-11 15:10:51 +08:00
lyr
e5778d5b5f 【光环助手V5.1.0】 0809产品测试问题(第6点)https://git.ghzs.com/pm/halo-app-issues/-/issues/1439 2021-08-10 17:39:35 +08:00
f69d607ecb Merge branch 'dev' of git.ghzs.com:halo/android/assistant-android into dev 2021-08-10 16:48:30 +08:00
79fc2e1f93 【光环助手V5.1.0】 0809产品测试问题(3) https://git.ghzs.com/pm/halo-app-issues/-/issues/1439 2021-08-10 16:48:20 +08:00
6dfdbb9ee8 【光环助手V5.1.0】 0809产品测试问题(5)https://git.ghzs.com/pm/halo-app-issues/-/issues/1439 2021-08-10 15:55:35 +08:00
5d8f7b3f8d 模拟器游戏启动时更新金手指文件 https://git.ghzs.com/pm/halo-app-issues/-/issues/1362 2021-08-10 12:02:17 +08:00
cd4601a9c9 添加下载更新简单文件的方法 2021-08-10 12:00:04 +08:00
462a011401 提交LGLibrary 2021-08-10 09:09:59 +08:00
31d55dae14 Merge branch 'dev' of git.ghzs.com:halo/android/assistant-android into dev 2021-08-09 20:03:56 +08:00
8dc9731299 【光环助手V5.1.0】新社区2期-论坛展示-提问帖详情页(0806UI测试1) https://git.ghzs.com/pm/halo-app-issues/-/issues/1347 2021-08-09 20:03:45 +08:00
66c19c644e 【光环助手V5.1.0】 0809运营测试问题(1)https://git.ghzs.com/pm/halo-app-issues/-/issues/1438 2021-08-09 17:24:31 +08:00
2b617e2697 完成单机模拟器-金手指功能后台(0809测试反馈) https://git.ghzs.com/pm/halo-app-issues/-/issues/1362 2021-08-09 16:35:24 +08:00
3a60a497e1 更换版主修改问题提示弹窗文案 2021-08-09 16:15:18 +08:00
a9337aee63 Merge branch 'dev' of git.ghzs.com:halo/android/assistant-android into dev 2021-08-09 15:52:49 +08:00
c80cd47ccc 【光环助手V5.1.0】 0803产品运营测试问题(2)(8) https://git.ghzs.com/pm/halo-app-issues/-/issues/1428 2021-08-09 15:52:38 +08:00
lyr
d9c2371488 移除冗余的资源文件 https://git.ghzs.com/halo/android/assistant-android/-/issues/33 2021-08-09 15:42:15 +08:00
096d19751a 【光环助手V5.1.0】0805产品测试问题(9)https://git.ghzs.com/pm/halo-app-issues/-/issues/1434 2021-08-09 15:15:41 +08:00
ef70119090 尝试修复 Loghub 日志上报 logstore 错乱的问题 https://github.com/aliyun/aliyun-log-android-sdk/issues/68 2021-08-09 11:56:16 +08:00
95dba71bb9 添加异步登录 JS Api 2021-08-09 10:17:16 +08:00
ec37c7a6f8 【光环助手V5.1.0】 0806产品运营测试问题(7) https://git.ghzs.com/pm/halo-app-issues/-/issues/1436 2021-08-09 09:28:17 +08:00
dcc9352301 调整 Kotlin 版本 2021-08-06 14:34:43 +08:00
c509c6bb38 添加使用快捷方式启动模拟器游戏失败的日志记录 2021-08-06 12:01:23 +08:00
4784d689f8 处理内嵌网页的一些空指针异常 2021-08-06 12:00:31 +08:00
8903a075be Merge branch 'dev' of git.ghzs.com:halo/android/assistant-android into dev 2021-08-06 11:46:37 +08:00
48d6e91b0e 【光环助手V5.1.0】0805运营测试问题(4,6,8) https://git.ghzs.com/pm/halo-app-issues/-/issues/1435 2021-08-06 11:46:27 +08:00
30865239d2 【光环助手V5.1.0】 0806产品运营测试问题(3)https://git.ghzs.com/pm/halo-app-issues/-/issues/1436 2021-08-06 11:31:09 +08:00
8b7cd92ae8 微调 chucker 编译脚本 2021-08-06 11:21:46 +08:00
69f336553e 调整 Kotlin 版本,修复 XAPK 解压在 4.4 以下设备的闪退问题 2021-08-06 11:07:10 +08:00
ed82f96ae3 Merge branch 'dev' of git.ghzs.com:halo/android/assistant-android into dev 2021-08-06 10:17:22 +08:00
8f02016a76 【光环助手V5.1.0】新社区2期-论坛展示-提问帖详情页(0806UI测试1) https://git.ghzs.com/pm/halo-app-issues/-/issues/1347 2021-08-06 10:17:12 +08:00
c06f397d12 【光环助手V5.1.0】0805产品测试问题(9,10,12)https://git.ghzs.com/pm/halo-app-issues/-/issues/1434 2021-08-06 10:08:34 +08:00
lyr
912b7280cd 【光环助手V5.1.0】0805产品测试问题(第5点)https://git.ghzs.com/pm/halo-app-issues/-/issues/1434 2021-08-05 17:15:48 +08:00
50f7dd2c63 【光环助手V5.1.0】0805产品测试问题(3,4) https://git.ghzs.com/pm/halo-app-issues/-/issues/1434 2021-08-05 15:49:12 +08:00
23469543c7 Merge branch 'dev' of git.ghzs.com:halo/android/assistant-android into dev 2021-08-05 14:48:14 +08:00
2d3f70dd12 【光环助手V5.1.0】 0804产品运营测试问题(1)(12) https://git.ghzs.com/pm/halo-app-issues/-/issues/1430 2021-08-05 14:48:09 +08:00
lyr
f57d19e797 【光环助手V5.1.0】 0804产品运营测试问题(1)(第10点) https://git.ghzs.com/pm/halo-app-issues/-/issues/1430#note_112113 2021-08-05 10:48:52 +08:00
lyr
8c529f0724 【光环助手V5.1.0】新社区2期-论坛展示-信息流-提问帖评论(0804UI测试)https://git.ghzs.com/pm/halo-app-issues/-/issues/1344#note_111919 2021-08-05 10:19:24 +08:00
lyr
1583f1957a 完成积分体系-光能中心、光能屋UI更改 https://git.ghzs.com/pm/halo-app-issues/-/issues/1300 2021-08-04 18:44:40 +08:00
lyr
676ccb133f 【光环助手V5.1.0】 0803产品运营测试问题(2)(第3点)https://git.ghzs.com/pm/halo-app-issues/-/issues/1428 2021-08-04 15:45:31 +08:00
489b8143a1 【光环助手V5.1.0】 0804产品运营测试问题(1) (1) https://git.ghzs.com/pm/halo-app-issues/-/issues/1430 2021-08-04 15:06:44 +08:00
c4cf3efa21 Merge branch 'dev' of git.ghzs.com:halo/android/assistant-android into dev 2021-08-04 14:26:57 +08:00
871cfd638d 【光环助手V5.1.0】新社区2期-论坛展示-帖子详情页(0804UI测试1) https://git.ghzs.com/pm/halo-app-issues/-/issues/1346 2021-08-04 14:26:47 +08:00
lyr
1f3ae1c687 【光环助手V5.1.0】 0803产品运营测试问题(1)(第23、24点)https://git.ghzs.com/pm/halo-app-issues/-/issues/1426 2021-08-04 14:21:55 +08:00
59eb101a48 Merge branch 'dev' of git.ghzs.com:halo/android/assistant-android into dev 2021-08-04 12:14:09 +08:00
b3b8b6ba29 【光环助手V5.1.0】 0803产品运营测试问题(2)https://git.ghzs.com/pm/halo-app-issues/-/issues/1428 2021-08-04 12:14:02 +08:00
lyr
42baaa8950 【光环助手V5.1.0】 0803产品运营测试问题(2)(第3点) https://git.ghzs.com/pm/halo-app-issues/-/issues/1428 2021-08-04 11:07:51 +08:00
lyr
61ad70d3b8 【光环助手V5.1.0】新社区2期-论坛展示-信息流-提问帖评论(0804UI测试)https://git.ghzs.com/pm/halo-app-issues/-/issues/1344#note_111919 2021-08-04 10:59:35 +08:00
lyr
04252e3b91 【光环助手V5.1.0】 0803产品运营测试问题(22)https://git.ghzs.com/pm/halo-app-issues/-/issues/1426 2021-08-03 18:20:41 +08:00
lyr
b40543d7b5 【光环助手V5.1.0】 0803产品运营测试问题(16)https://git.ghzs.com/pm/halo-app-issues/-/issues/1426 2021-08-03 17:30:26 +08:00
lyr
903fa49b71 【光环助手V5.1.0】 0803产品运营测试问题(13)https://git.ghzs.com/pm/halo-app-issues/-/issues/1426 2021-08-03 16:28:08 +08:00
lyr
03ae2699c6 【光环助手V5.1.0】 0803产品运营测试问题(11)https://git.ghzs.com/pm/halo-app-issues/-/issues/1426 2021-08-03 16:09:05 +08:00
8ecc4078bb 【光环助手V5.1.0】 0803产品运营测试问题(1,20,27,31) https://git.ghzs.com/pm/halo-app-issues/-/issues/1426 2021-08-03 15:59:02 +08:00
ebaf4f02f0 【光环助手V5.1.0】新社区2期-论坛展示-论坛详情-版主成员(0802UI测试)https://git.ghzs.com/pm/halo-app-issues/-/issues/1345 2021-08-03 11:21:03 +08:00
e85437379a 调整金手指功能数据字段 https://git.ghzs.com/pm/halo-app-issues/-/issues/1362 2021-08-02 17:20:44 +08:00
2e9642019b 微博SDK更新到11.6 2021-08-02 17:08:58 +08:00
5a77522a04 Merge branch 'dev' of git.ghzs.com:halo/android/assistant-android into dev 2021-08-02 16:18:48 +08:00
e5512604fa 修改上传图片的 type 参数 2021-08-02 16:18:39 +08:00
lyr
146a46f8be 【光环助手V5.1.0】0802产品测试问题(1)https://git.ghzs.com/pm/halo-app-issues/-/issues/1423 2021-08-02 16:04:00 +08:00
91f33f06d3 Merge branch 'dev' of git.ghzs.com:halo/android/assistant-android into dev 2021-08-02 15:59:00 +08:00
59da7b6ba6 【光环助手V5.1.0】新社区2期-视频帖发布页(0802UI测试1,2) https://git.ghzs.com/pm/halo-app-issues/-/issues/1355 2021-08-02 15:58:45 +08:00
lyr
0a1ad238f8 【光环助手V5.1.0】新社区2期-论坛详情页引导(0802UI测试)https://git.ghzs.com/pm/halo-app-issues/-/issues/1352#note_111430 2021-08-02 15:43:24 +08:00
302e6c145c 【光环助手V5.1.0】新社区2期-论坛展示-帖子详情页(0802UI测试1) https://git.ghzs.com/pm/halo-app-issues/-/issues/1346 2021-08-02 15:37:40 +08:00
a6bbf0dfc7 Merge branch 'dev' of git.ghzs.com:halo/android/assistant-android into dev 2021-08-02 15:17:06 +08:00
9c3d22964c 【光环助手V5.1.0】0730产品测试问题(10,22,23,24) https://git.ghzs.com/pm/halo-app-issues/-/issues/1422 2021-08-02 15:16:56 +08:00
ab60ed8473 【光环助手V5.1.0】新社区2期-论坛展示-论坛详情-版主成员(0802UI测试)https://git.ghzs.com/pm/halo-app-issues/-/issues/1345 2021-08-02 14:31:07 +08:00
lyr
9a00e70cc4 【光环助手V5.1.0】0730产品测试问题(5) https://git.ghzs.com/pm/halo-app-issues/-/issues/1422 2021-08-02 11:16:46 +08:00
lyr
35954f52c5 【光环助手V5.1.0】0730产品测试问题(1-3)https://git.ghzs.com/pm/halo-app-issues/-/issues/1422 2021-08-02 10:58:25 +08:00
e13d80d063 【光环助手V5.1.0】0730产品测试问题(11)https://git.ghzs.com/pm/halo-app-issues/-/issues/1422 2021-08-02 10:13:56 +08:00
73ed15689d 基本完成金手指功能 https://git.ghzs.com/pm/halo-app-issues/-/issues/1362 2021-07-30 18:07:23 +08:00
lyr
65d949bade 【光环助手V5.1.0】新社区2期-推荐页引导(0729UI测试)https://git.ghzs.com/pm/halo-app-issues/-/issues/1350#note_110977 2021-07-29 18:08:01 +08:00
33d0d01051 【光环助手V5.1.0】新社区2期-帖子发布页(0729UI测试2) https://git.ghzs.com/pm/halo-app-issues/-/issues/1354 2021-07-29 16:34:29 +08:00
9998ed0a14 【光环助手V5.1.0】新社区2期-发布页引导(帖子、提问)(0729UI测试1) https://git.ghzs.com/pm/halo-app-issues/-/issues/1351 2021-07-29 16:15:41 +08:00
85a8a17acb Merge branch 'dev' of git.ghzs.com:halo/android/assistant-android into dev 2021-07-29 15:44:34 +08:00
4032cb8abe 【光环助手V5.1.0】新社区2期-视频帖发布页(0729UI测试) https://git.ghzs.com/pm/halo-app-issues/-/issues/1355 2021-07-29 15:44:18 +08:00
fc2e87063e Merge remote-tracking branch 'origin/dev' into dev 2021-07-29 14:34:37 +08:00
a63b809642 清理测试内容 2021-07-29 14:34:22 +08:00
c30048c268 正式环境接口切换到 5.0 2021-07-29 11:01:41 +08:00
956d5a39be 修复优化汇总(1) https://git.ghzs.com/pm/halo-app-issues/-/issues/1342#note_110932 2021-07-29 09:59:51 +08:00
76fd1ab7e3 Merge branch 'dev' of git.ghzs.com:halo/android/assistant-android into dev 2021-07-28 20:47:40 +08:00
2c4c954c64 【光环助手V5.1.0】新社区2期-论坛展示-信息流-审核中/审核不通过(2) https://git.ghzs.com/pm/halo-app-issues/-/issues/1343 2021-07-28 20:47:23 +08:00
aef4da961d 正式包丢失渠道后的默认渠道改为 GH_LOST 2021-07-28 17:24:17 +08:00
lyr
31e00bb681 【光环助手V5.1.0】新社区2期-埋点文档(版规说明遗漏部分) https://git.ghzs.com/pm/halo-app-issues/-/issues/1303 2021-07-28 16:50:30 +08:00
369fdd24ef 调整内容及评论投诉弹窗交互 https://git.ghzs.com/pm/halo-app-issues/-/issues/1373 2021-07-28 15:28:25 +08:00
2a57994c7f Merge branch 'dev' of git.ghzs.com:halo/android/assistant-android into dev 2021-07-28 15:21:58 +08:00
6f84b14277 处理升级gsyplayer版本报错问题 2021-07-28 15:21:52 +08:00
10ed05c8ef Merge branch 'feature-upgrade_gsyplayer' into dev
# Conflicts:
#	app/build.gradle
2021-07-28 15:05:18 +08:00
lyr
799bd1042f 【光环助手V5.1.0】新社区2期-埋点文档 https://git.ghzs.com/pm/halo-app-issues/-/issues/1303 2021-07-28 14:07:27 +08:00
6f9acab2d2 修改版主申请审核中的字段 2021-07-28 11:29:45 +08:00
lyr
387d8eb5af 优化社区-论坛页代码 2021-07-27 18:31:10 +08:00
c813c5d5ef 暂时放弃 arm64 的 SO 2021-07-27 17:35:58 +08:00
1c6df6741f 完成光环前端优化汇总第五周(3) https://git.ghzs.com/pm/halo-app-issues/-/issues/1388 2021-07-27 16:45:11 +08:00
b54ab5f824 支持网页将图片保存至图库中 https://git.ghzs.com/pm/halo-app-issues/-/issues/1286 2021-07-27 16:31:05 +08:00
lyr
37fdf6c18e 【光环助手V5.1.0】光环前端优化汇总第五周(4)https://git.ghzs.com/pm/halo-app-issues/-/issues/1388 2021-07-27 16:00:00 +08:00
2685de9b3e Merge remote-tracking branch 'origin/dev' into dev 2021-07-27 15:17:59 +08:00
c049100f5f webView 支持选择本地图片 2021-07-27 15:16:13 +08:00
711d1f9d65 【光环助手V5.1.0】新社区2期-论坛展示-论坛详情-版主成员https://git.ghzs.com/pm/halo-app-issues/-/issues/1345 2021-07-27 15:14:11 +08:00
5d7453afec 完成浏览论坛页面时间任务功能 https://git.ghzs.com/pm/halo-app-issues/-/issues/1285 2021-07-27 11:00:21 +08:00
e4acf466ba 版主加精问题评论更换接口 2021-07-26 14:49:45 +08:00
2d66cbd29c 【光环助手V5.1.0】新社区2期-埋点文档(分享面板) https://git.ghzs.com/pm/halo-app-issues/-/issues/1303 2021-07-26 10:07:10 +08:00
8786f7d500 【光环助手V5.1.0】新社区2期-论坛展示-提问帖详情页 https://git.ghzs.com/pm/halo-app-issues/-/issues/1347 2021-07-23 18:07:13 +08:00
3d40d7a819 新增默认选中论坛首页tab的 url scheme https://git.ghzs.com/pm/halo-app-issues/-/issues/1285 2021-07-22 17:50:22 +08:00
6f7862a0f2 新增默认选中论坛首页tab的 url scheme https://git.ghzs.com/pm/halo-app-issues/-/issues/1285 2021-07-22 17:37:43 +08:00
58c6211d54 Merge branch 'dev' of git.ghzs.com:halo/android/assistant-android into dev 2021-07-22 17:34:02 +08:00
d10b187a1f 视频帖详情版主修改活动标签 2021-07-22 17:33:52 +08:00
lyr
1ebf34c83a 【光环助手V5.1.0】新社区2期-埋点文档(版规说明页以及处理漏传)https://git.ghzs.com/pm/halo-app-issues/-/issues/1303 2021-07-22 16:53:01 +08:00
01e2a3c708 1.版主修改活动标签 2.审核中、审核不通过点击图片toast提示 2021-07-22 15:08:20 +08:00
lyr
c9b97552d4 【光环助手V5.1.0】新社区2期-埋点文档(论坛详情页)https://git.ghzs.com/pm/halo-app-issues/-/issues/1303 2021-07-21 17:59:30 +08:00
428f39aa86 Merge branch 'dev' of git.ghzs.com:halo/android/assistant-android into dev 2021-07-21 11:13:48 +08:00
5fecb723bc 【光环助手V5.1.0】新社区2期-论坛展示-帖子详情页(1(1)(3)(4),2) https://git.ghzs.com/pm/halo-app-issues/-/issues/1346 2021-07-21 11:13:38 +08:00
0168b0d93a 【光环助手V5.1.0】新社区2期-帖子发布页(4) https://git.ghzs.com/pm/halo-app-issues/-/issues/1354 2021-07-21 11:09:39 +08:00
895fa510fe 【光环助手V5.1.0】新社区2期-论坛展示-视频帖详情页 https://git.ghzs.com/pm/halo-app-issues/-/issues/1348 2021-07-21 11:04:00 +08:00
0aae59a1ba 恢复网页跳转旧视频流详情页的能力 2021-07-21 10:39:11 +08:00
574a2b4dc7 完成内容举报弹窗更改 https://git.ghzs.com/pm/halo-app-issues/-/issues/1373 2021-07-21 09:25:30 +08:00
acae608966 添加跳转至个人主页定位到视频分类的 url scheme https://git.ghzs.com/pm/halo-app-issues/-/issues/1284#note_107842 2021-07-20 18:29:37 +08:00
lyr
6ca6c34f92 【光环助手V5.1.0】新社区2期-埋点文档(部分)https://git.ghzs.com/pm/halo-app-issues/-/issues/1303 2021-07-20 17:55:13 +08:00
16724a2d2e 完成部分内容举报弹窗更改(帖子、提问帖、视频帖) https://git.ghzs.com/pm/halo-app-issues/-/issues/1373 2021-07-20 16:15:54 +08:00
8b50cc561a 补充判断用户类型的逻辑 https://git.ghzs.com/pm/halo-app-issues/-/issues/1293 2021-07-20 09:23:01 +08:00
1ec1d482c4 【光环助手V5.1.0】新社区2期-埋点文档(跳出事件埋点) https://git.ghzs.com/pm/halo-app-issues/-/issues/1303 2021-07-19 19:49:47 +08:00
869b9d507b Merge branch 'dev' of git.ghzs.com:halo/android/assistant-android into dev 2021-07-19 16:21:14 +08:00
514fe66347 【光环助手V5.1.0】新社区2期-埋点文档 https://git.ghzs.com/pm/halo-app-issues/-/issues/1303 2021-07-19 16:21:04 +08:00
lyr
657c91a5ef 【光环助手V5.1.0】新社区2期-论坛展示-信息流-提问帖评论 https://git.ghzs.com/pm/halo-app-issues/-/issues/1344 2021-07-19 11:29:36 +08:00
82354a1156 添加从网页跳转至发视频页面的跳转 urlScheme https://git.ghzs.com/pm/halo-app-issues/-/issues/1284 2021-07-16 18:05:40 +08:00
b7619f0c93 Merge branch 'dev' of git.ghzs.com:halo/android/assistant-android into dev 2021-07-16 16:38:25 +08:00
598a3ad0b6 【光环助手V5.1.0】新社区2期-埋点文档(部分) https://git.ghzs.com/pm/halo-app-issues/-/issues/1303 2021-07-16 16:38:18 +08:00
bf0be93dc9 完成"返回活动"的小浮窗功能 https://git.ghzs.com/pm/halo-app-issues/-/issues/1284 2021-07-16 15:28:54 +08:00
lyr
90a676f778 【光环助手V5.1.0】新社区2期-论坛展示-信息流-审核中/审核不通过 https://git.ghzs.com/pm/halo-app-issues/-/issues/1343 2021-07-16 10:29:50 +08:00
lyr
a675e4bbe3 【光环助手V5.1.0】光环前端优化汇总第二周(6)https://git.ghzs.com/pm/halo-app-issues/-/issues/1342 2021-07-16 10:08:01 +08:00
5eee8c6785 更改积分"返回活动"浮窗的实现方式 2021-07-15 18:11:12 +08:00
3c54e4313c Merge branch 'dev' of git.ghzs.com:halo/android/assistant-android into dev 2021-07-15 17:08:09 +08:00
67c4b76de9 【光环助手V5.1.0】新社区2期-帖子发布页(UI部分) https://git.ghzs.com/pm/halo-app-issues/-/issues/1354 2021-07-15 17:07:59 +08:00
34fd08d06a 【光环助手V5.1.0】新社区2期-视频帖发布页(UI部分) https://git.ghzs.com/pm/halo-app-issues/-/issues/1355 2021-07-15 17:07:21 +08:00
lyr
d08b95021d 暂时隐藏社区-论坛-其他福利-万能加速器(因为跳转目的地不唯一) 2021-07-15 16:12:16 +08:00
8517a2aef1 应用启动日志上报添加包名/签名等其它信息 2021-07-14 14:57:43 +08:00
lyr
cbe90141f1 修改社区-推荐引导图错位问题 2021-07-14 14:50:10 +08:00
lyr
fd43bc9426 【光环助手V5.1.0】新社区2期-论坛详情页引导 https://git.ghzs.com/pm/halo-app-issues/-/issues/1352 2021-07-14 14:48:52 +08:00
lyr
34a143b632 【光环助手V5.1.0】新社区2期-推荐页引导 https://git.ghzs.com/pm/halo-app-issues/-/issues/1350 2021-07-14 09:39:56 +08:00
d96e02eca0 完成光环前端优化汇总第二周(1~3) https://git.ghzs.com/pm/halo-app-issues/-/issues/1342 2021-07-13 17:52:36 +08:00
lyr
398b1ae58b 【光环助手V5.1.0】新社区2期-引导页 https://git.ghzs.com/pm/halo-app-issues/-/issues/1349 2021-07-13 14:30:59 +08:00
0749205bef 更新离线已收录游戏列表 2021-07-13 14:13:56 +08:00
54ec115fee 修复判断新老用户接口问题 2021-07-13 12:37:08 +08:00
lyr
ebe7b84dc8 【光环助手V5.1.0】个性化推荐合规相关(2)https://git.ghzs.com/pm/halo-app-issues/-/issues/1308 2021-07-13 09:38:49 +08:00
lyr
332abe66f5 【光环助手V5.1.0】光环前端优化汇总第二周(5 优化)https://git.ghzs.com/pm/halo-app-issues/-/issues/1342 2021-07-12 14:33:31 +08:00
lyr
9dccbfbd51 【光环助手V5.1.0】光环前端优化汇总第二周(5)https://git.ghzs.com/pm/halo-app-issues/-/issues/1342 2021-07-12 14:13:22 +08:00
23a8c9e6aa 临时处理 JCenter 依赖问题 2021-07-12 11:32:41 +08:00
1e4408ac6e 【光环助手V5.1.0】新社区2期-消息中心相关优化 https://git.ghzs.com/pm/halo-app-issues/-/issues/1309 2021-07-09 16:06:33 +08:00
lyr
05ece9c999 光环助手v5.0.0-新社区后台问题汇总(20210708)(8 优化) https://git.ghzs.com/pm/halo-app-issues/-/issues/1364 2021-07-09 15:18:23 +08:00
lyr
216905e455 光环助手v5.0.0-新社区APP端问题汇总(20210708)(8) https://git.ghzs.com/pm/halo-app-issues/-/issues/1364 2021-07-09 14:27:37 +08:00
a1bd88dad8 Merge branch 'dev' of git.ghzs.com:halo/android/assistant-android into dev 2021-07-09 12:41:14 +08:00
16c66c707f 【光环助手V5.1.0】新社区2期-发布页引导(帖子、提问)https://git.ghzs.com/pm/halo-app-issues/-/issues/1351 2021-07-09 12:40:56 +08:00
lyr
50cdea2b03 光环助手v5.0.0-新社区APP端问题汇总(20210707)(11) https://git.ghzs.com/pm/halo-app-issues/-/issues/1360 2021-07-09 11:12:49 +08:00
95b0fda15a 版本号更新到 5.1.0 2021-07-09 10:29:32 +08:00
9c99273f81 光环助手v5.0.0-新社区APP端问题汇总(20210707)(1) https://git.ghzs.com/pm/halo-app-issues/-/issues/1360 2021-07-08 21:47:46 +08:00
8d9d36a5df 移除部分无用代码 2021-07-08 18:25:18 +08:00
lyr
b22d168e97 光环助手v5.0.0-新社区APP端问题汇总(20210706)(10 优化) https://git.ghzs.com/pm/halo-app-issues/-/issues/1358 2021-07-08 15:53:49 +08:00
lyr
1fee166e3b 光环助手v5.0.0-新社区APP端问题汇总(20210707)(13) https://git.ghzs.com/pm/halo-app-issues/-/issues/1360 2021-07-08 14:54:25 +08:00
lyr
7c098c29b2 光环助手v5.0.0-新社区APP端问题汇总(20210706)(10) https://git.ghzs.com/pm/halo-app-issues/-/issues/1358 2021-07-08 14:42:19 +08:00
ffc9dc46e2 Merge branch 'dev' of git.ghzs.com:halo/android/assistant-android into dev 2021-07-08 11:19:41 +08:00
c491a4084c 修改选择活动标签抖动问题 2021-07-08 11:19:24 +08:00
lyr
0f076a03c5 光环助手v5.0.0-新社区APP端问题汇总(20210707)(11) https://git.ghzs.com/pm/halo-app-issues/-/issues/1360 2021-07-08 10:44:33 +08:00
3345ab2044 Merge branch 'dev' of git.ghzs.com:halo/android/assistant-android into dev 2021-07-07 20:41:58 +08:00
5874bdd311 发布帖子/问题视频未上传完不能发布 2021-07-07 20:41:34 +08:00
lyr
4669211196 微调我的光环UI 2021-07-07 18:47:29 +08:00
4879e7f0aa Merge branch 'dev' of git.ghzs.com:halo/android/assistant-android into dev 2021-07-07 18:27:53 +08:00
f5283d386b 1.视频详情点赞按钮防重复点击实现限制为2s 2.修改插入视频时间显示错误问题 2021-07-07 18:27:44 +08:00
lyr
602333f923 光环助手v5.0.0-新社区APP端问题汇总(20210707)第6点 https://git.ghzs.com/pm/halo-app-issues/-/issues/1360 2021-07-07 16:39:23 +08:00
e9810c129e 光环助手v5.0.0-新社区后台问题汇总(20210707)(4) https://git.ghzs.com/pm/halo-app-issues/-/issues/1361 2021-07-07 15:22:56 +08:00
c503f15df9 光环助手v5.0.0-新社区APP端问题汇总(20210706)(13) https://git.ghzs.com/pm/halo-app-issues/-/issues/1358 2021-07-07 14:40:02 +08:00
cba1012bbd Merge branch 'dev' of git.ghzs.com:halo/android/assistant-android into dev 2021-07-07 11:47:39 +08:00
646493ae61 光环助手v5.0.0-新社区APP端问题汇总(20210706)(11) https://git.ghzs.com/pm/halo-app-issues/-/issues/1358 2021-07-07 11:47:30 +08:00
b649089ab0 更换发帖子/问题插入视频接口 2021-07-07 11:46:49 +08:00
lyr
e97fd9023e 光环助手v5.0.0-新社区APP端问题汇总(20210706)第2点 https://git.ghzs.com/pm/halo-app-issues/-/issues/1358 2021-07-07 10:42:39 +08:00
478dd658ab 光环助手v5.0.0-新社区APP端问题汇总(20210706)(5) https://git.ghzs.com/pm/halo-app-issues/-/issues/1358 2021-07-06 18:04:47 +08:00
b313feac01 Merge branch 'dev' of git.ghzs.com:halo/android/assistant-android into dev 2021-07-06 17:24:36 +08:00
c04b82549e 视频帖详情页全屏播放顶部增加标题 2021-07-06 17:24:23 +08:00
lyr
842e62f909 微调个人主页和新分类2.0的UI 2021-07-06 16:28:12 +08:00
lyr
51c6de0506 微调社区-论坛UI 2021-07-06 15:20:19 +08:00
lyr
d350f65c1f 修改活动详情测试环境地址 2021-07-05 18:36:49 +08:00
lyr
aaf8e28abc 光环助手v5.0.0-新社区APP端问题汇总(4)https://git.ghzs.com/pm/halo-app-issues/-/issues/1329 2021-07-05 18:28:07 +08:00
7cb921e969 选择活动标签接口增加location参数 2021-07-05 16:35:57 +08:00
lyr
3a4b41058d 光环助手V5.0.0-新社区展示功能(20200702UI测试问题、UI测试问题补充)https://git.ghzs.com/pm/halo-app-issues/-/issues/1253#note_106840 2021-07-05 15:03:28 +08:00
lyr
26308d1852 光环助手V5.0.0-新社区其他相关改动功能(0702 产品测试问题 1)https://git.ghzs.com/pm/halo-app-issues/-/issues/1254#note_106775 2021-07-05 14:24:34 +08:00
af54932309 Merge branch 'dev-5.0.0' of git.ghzs.com:halo/android/assistant-android into dev-5.0.0 2021-07-05 11:51:03 +08:00
83799048ed 光环助手v5.0.0-新社区APP端问题汇总(2) https://git.ghzs.com/pm/halo-app-issues/-/issues/1329 2021-07-05 11:50:54 +08:00
ce410ad2a0 更改网页 url 的地址添加来源 query 的实现 2021-07-05 09:30:55 +08:00
933c40458f 社区插入视频length字段的统一单位为秒 2021-07-05 09:24:10 +08:00
6ae7578281 光环助手V5.0.0-新社区展示功能(0702 产品测试问题1,3,4) https://git.ghzs.com/pm/halo-app-issues/-/issues/1253 2021-07-03 15:25:57 +08:00
lyr
72b804e26e 处理论坛信息流GIF图边框显示不对的问题 2021-07-02 18:03:48 +08:00
lyr
7e58cdc59e 修改社区-论坛Tab每次进入页面都会被切割的问题 2021-07-02 17:20:20 +08:00
lyr
d782ea90b6 光环助手V5.0.0-新社区展示功能(0701 产品测试问题 3)https://git.ghzs.com/pm/halo-app-issues/-/issues/1253#note_106529 2021-07-02 15:18:49 +08:00
lyr
efffd752f5 提交漏传资源文件 2021-07-02 14:25:20 +08:00
47f3f45fe6 修复推荐优先专题游戏没有推荐信息时简介不显示大小的问题 2021-07-02 10:30:00 +08:00
b4e12e2ec3 Merge branch 'dev-5.0.0' of git.ghzs.com:halo/android/assistant-android into dev-5.0.0 2021-07-01 21:08:15 +08:00
2d331f6294 光环助手V5.0.0-新社区展示功能(0701 产品测试问题4) https://git.ghzs.com/pm/halo-app-issues/-/issues/1253 2021-07-01 21:08:09 +08:00
lyr
feb4137f84 光环助手V5.0.0-新社区展示功能(V5.0.0-新社区展示功能-UI测试问题汇总 7 gif没有边框问题)https://git.ghzs.com/pm/halo-app-issues/-/issues/1253#note_105161 2021-07-01 18:57:45 +08:00
6a2c4a2967 Merge branch 'dev-5.0.0' of git.ghzs.com:halo/android/assistant-android into dev-5.0.0 2021-07-01 18:16:32 +08:00
68fc5532ff 1.用户回答了问题消息通知跳转新回答详情 2.删除无用导包 2021-07-01 18:16:26 +08:00
lyr
43ae479271 光环助手V5.0.0-新社区展示功能(0630 产品测试问题 5)https://git.ghzs.com/pm/halo-app-issues/-/issues/1253#note_106346 2021-07-01 18:12:52 +08:00
7a3ea4d939 处理版本升级时的数据库更新问题 2021-07-01 18:04:43 +08:00
lyr
1bcb343355 光环助手V5.0.0-新社区展示功能(0630 运营测试问题 4)https://git.ghzs.com/pm/halo-app-issues/-/issues/1253#note_106346 2021-07-01 17:54:36 +08:00
1b7a601cee 处理游戏详情点击启动状态的下载按钮的滚动问题 2021-07-01 16:26:53 +08:00
lyr
8bc9544dc2 光环助手V5.0.0-新社区展示功能(0630 运营测试问题 2)https://git.ghzs.com/pm/halo-app-issues/-/issues/1253#note_106346 2021-07-01 16:03:13 +08:00
e586e65e44 Merge branch 'dev-5.0.0' of git.ghzs.com:halo/android/assistant-android into dev-5.0.0 2021-07-01 15:47:38 +08:00
8096da5f50 光环助手V5.0.0-新社区展示功能(0701 产品测试问题1) https://git.ghzs.com/pm/halo-app-issues/-/issues/1253 2021-07-01 15:47:34 +08:00
91868fda15 微调专题游戏推荐语布局 2021-07-01 14:48:23 +08:00
lyr
a71e8824ce 光环助手V5.0.0-新社区展示功能(0629 产品测试问题 15-17)https://git.ghzs.com/pm/halo-app-issues/-/issues/1253#note_106052 2021-07-01 11:04:28 +08:00
6cbc354921 完成前端优化汇总(16) https://git.ghzs.com/pm/halo-app-issues/-/issues/1260 2021-06-30 18:14:50 +08:00
lyr
79e2077a3d 光环助手V5.0.0-新社区展示功能(0629 产品测试问题 14)https://git.ghzs.com/pm/halo-app-issues/-/issues/1253#note_106052 2021-06-30 18:04:55 +08:00
lyr
6ea3ed067d 光环助手V5.0.0-新社区展示功能(V5.0.0-新社区展示功能-UI测试问题汇总 7)https://git.ghzs.com/pm/halo-app-issues/-/issues/1253#note_105805 2021-06-30 17:06:22 +08:00
lyr
84fe33becc 光环助手V5.0.0-新社区其他相关改动功能 (0629UI问题 4) https://git.ghzs.com/pm/halo-app-issues/-/issues/1254#note_106173 2021-06-30 16:38:25 +08:00
276f53fead Merge branch 'dev-5.0.0' of git.ghzs.com:halo/android/assistant-android into dev-5.0.0 2021-06-30 16:15:06 +08:00
0789cc6fdf 光环助手V5.0.0-新社区其他相关改动功能(0625 运营测试问题2(1)) https://git.ghzs.com/pm/halo-app-issues/-/issues/1254 2021-06-30 16:15:01 +08:00
lyr
3a563b7bd0 光环助手V5.0.0-新社区其他相关改动功能 (0628 产品测试问题 2) https://git.ghzs.com/pm/halo-app-issues/-/issues/1254#note_105919 2021-06-30 15:55:30 +08:00
lyr
9b8ba0c3e6 光环助手V5.0.0-新社区其他相关改动功能(20210622UI测试问题、0623 产品测试问题、0625 运营测试问题)https://git.ghzs.com/pm/halo-app-issues/-/issues/1254 2021-06-30 15:38:38 +08:00
258f054161 光环助手V5.0.0-新社区展示功能(0629 运营测试问题22) https://git.ghzs.com/pm/halo-app-issues/-/issues/1253 2021-06-30 15:15:19 +08:00
258c8db0f9 光环助手V5.0.0-新社区展示功能(0629 运营测试问题23,24) https://git.ghzs.com/pm/halo-app-issues/-/issues/1253 2021-06-30 11:44:16 +08:00
741d0b11af Merge branch 'dev-5.0.0' of git.ghzs.com:halo/android/assistant-android into dev-5.0.0 2021-06-29 18:47:13 +08:00
9267896b50 光环助手V5.0.0-新社区展示功能(0629 运营测试问题13) https://git.ghzs.com/pm/halo-app-issues/-/issues/1253 2021-06-29 18:47:03 +08:00
85c0bd69c1 调整 VolumeObserver 入参,避免内存泄漏 2021-06-29 18:45:58 +08:00
2436507b7b Merge branch 'dev-5.0.0' of git.ghzs.com:halo/android/assistant-android into dev-5.0.0 2021-06-29 17:58:06 +08:00
1b68b07e53 光环助手V5.0.0-新社区展示功能(0629 产品测试问题8,10) https://git.ghzs.com/pm/halo-app-issues/-/issues/1253 2021-06-29 17:57:55 +08:00
lyr
c4ba28de1a 光环助手V5.0.0-新社区展示功能(20200629UI测试问题)https://git.ghzs.com/pm/halo-app-issues/-/issues/1253#note_106168 2021-06-29 17:26:37 +08:00
lyr
8daa55c16a 光环助手V5.0.0-新社区展示功能 (0629 产品测试问题 第4、5点) 2021-06-29 17:09:47 +08:00
lyr
5e6c6aca13 光环助手V5.0.0-新社区展示功能(V5.0.0-新社区展示功能-UI测试问题补充 第2点)https://git.ghzs.com/pm/halo-app-issues/-/issues/1253#note_105806 2021-06-29 16:31:06 +08:00
lyr
e08ab790ee 优化新分类2.0引导UI 2021-06-29 15:59:26 +08:00
lyr
a394901cb4 光环前端优化汇总(2021年5月)(20210629UI问题补充) https://git.ghzs.com/pm/halo-app-issues/-/issues/1260#note_106103 2021-06-29 15:34:12 +08:00
lyr
2aaa7f2795 优化社区-论坛Tab的UI 2021-06-29 15:07:10 +08:00
7b0d2a25c8 Merge branch 'dev-5.0.0' of git.ghzs.com:halo/android/assistant-android into dev-5.0.0 2021-06-29 09:40:35 +08:00
e3f553a96f 光环助手V5.0.0-新社区展示功能(0628 产品测试问题4) https://git.ghzs.com/pm/halo-app-issues/-/issues/1253 2021-06-29 09:40:25 +08:00
lyr
809a25e3e2 光环助手V5.0.0-新社区展示功能(0628测试问题-热门论坛应该不展示已关注的论坛)https://git.ghzs.com/pm/halo-app-issues/-/issues/1253#note_105293 2021-06-28 18:58:20 +08:00
lyr
88041d9b5f 优化社区-推荐Tab搜索框UI 2021-06-28 16:57:40 +08:00
lyr
a11d741f6f 修复社区推荐Tab和论坛详情在当前论坛列表为空时,发布新内容而未显示的问题 2021-06-28 16:44:06 +08:00
1283def6da 修复首页搜索栏嵌套滚动不灵敏的问题 2021-06-28 16:33:21 +08:00
9c621c31a2 光环助手V5.0.0-新社区展示功能(0621 产品测试问题8(11)) https://git.ghzs.com/pm/halo-app-issues/-/issues/1253 2021-06-28 15:54:50 +08:00
9e3f40723f 光环助手V5.0.0-新社区发布功能(0628 产品测试问题1) https://git.ghzs.com/pm/halo-app-issues/-/issues/1252 2021-06-28 15:13:46 +08:00
682bb9a3e7 Merge branch 'dev-5.0.0' of git.ghzs.com:halo/android/assistant-android into dev-5.0.0 2021-06-28 14:53:55 +08:00
9f2d164c70 光环助手V5.0.0-新社区展示功能(0628 产品测试问题1,2) https://git.ghzs.com/pm/halo-app-issues/-/issues/1253 2021-06-28 14:53:49 +08:00
lyr
bad5130047 修复社区-推荐Tab本人新发布的视频贴时间显示不对问题 2021-06-28 14:23:23 +08:00
e3f831e0ca 修复应用内网页因为地址带特殊字符而出现的加载问题 2021-06-28 11:45:04 +08:00
a1432452f5 Merge branch 'dev-5.0.0' of git.ghzs.com:halo/android/assistant-android into dev-5.0.0 2021-06-28 11:31:30 +08:00
b3cc68b7c6 修改跳转至新的问题详情和回答详情 2021-06-28 11:31:25 +08:00
lyr
cc1f5a269a 修改个人主页发布Tab-切换子tab视频还在播放的问题 2021-06-28 11:27:25 +08:00
a2892a713a Merge branch 'release' into dev-5.0.0
# Conflicts:
#	dependencies.gradle
2021-06-28 11:01:59 +08:00
lyr
c342e22ce4 Merge remote-tracking branch 'origin/dev-5.0.0' into dev-5.0.0 2021-06-28 10:02:51 +08:00
lyr
57dcbab096 光环助手V5.0.0-新社区展示功能(V5.0.0-新社区展示功能-UI测试问题补充)https://git.ghzs.com/pm/halo-app-issues/-/issues/1253#note_105806 2021-06-28 10:02:39 +08:00
5aa4c5bcbc 光环助手V5.0.0-新社区展示功能(0625 产品测试问题3,4(2)) https://git.ghzs.com/pm/halo-app-issues/-/issues/1253 2021-06-28 09:58:16 +08:00
lyr
039e1fc957 光环助手V5.0.0-新社区展示功能(0625 产品测试问题 第2点)https://git.ghzs.com/pm/halo-app-issues/-/issues/1253#note_105792 2021-06-25 18:55:30 +08:00
f7b2d10543 Merge branch 'dev-5.0.0' of git.ghzs.com:halo/android/assistant-android into dev-5.0.0 2021-06-25 17:24:13 +08:00
38972eca76 光环助手V5.0.0-新社区发布功能(UI测试问题汇总0621AM 9) https://git.ghzs.com/pm/halo-app-issues/-/issues/1252 2021-06-25 17:24:05 +08:00
lyr
c2d853d709 【光环助手V5.0.0】提问帖相关需求(V5.0.0-新社区提问帖相关需求-UI测试问题汇总 第1点)https://git.ghzs.com/pm/halo-app-issues/-/issues/1251#note_105291 2021-06-25 16:47:04 +08:00
lyr
239209dbf2 光环助手V5.0.0-新社区展示功能(0623 产品测试问题 第1、2、4点)https://git.ghzs.com/pm/halo-app-issues/-/issues/1253#note_105473 2021-06-25 16:05:43 +08:00
lyr
ba1f3e20d3 优化新分类2.0引导图逻辑 2021-06-25 15:02:57 +08:00
lyr
92ac62d949 光环助手V5.0.0-新社区展示功能(20210622UI测试问题)https://git.ghzs.com/pm/halo-app-issues/-/issues/1253#note_105369 2021-06-25 15:00:29 +08:00
6499e3b718 修改视频贴详情用户关注按钮样式 2021-06-25 11:12:23 +08:00
5f0d2604ec Merge branch 'dev-5.0.0' of git.ghzs.com:halo/android/assistant-android into dev-5.0.0 2021-06-25 10:09:49 +08:00
5f7bb6cb49 光环助手V5.0.0-新社区发布功能(0622 产品测试问题2(2)) https://git.ghzs.com/pm/halo-app-issues/-/issues/1252 2021-06-25 10:09:40 +08:00
lyr
7360eef244 光环助手V5.0.0-新社区展示功能(V5.0.0-新社区展示功能-UI测试问题汇总 第1-10点)https://git.ghzs.com/pm/halo-app-issues/-/issues/1253#note_105161 2021-06-25 09:27:03 +08:00
52e6feab59 修改视频帖详情游戏标签样式 2021-06-24 20:56:09 +08:00
5f44252410 视频贴详情调整视频区域高度 2021-06-24 18:35:13 +08:00
7ae3b1cafb Merge branch 'dev-5.0.0' of git.ghzs.com:halo/android/assistant-android into dev-5.0.0 2021-06-24 18:13:49 +08:00
0fb77e79fd 光环助手V5.0.0-新社区发布功能(UI测试问题补充0624PM 1,2,3) https://git.ghzs.com/pm/halo-app-issues/-/issues/1252 2021-06-24 18:13:39 +08:00
lyr
d4ae878947 光环前端优化汇总(2021年5月)(第9-11点) https://git.ghzs.com/pm/halo-app-issues/-/issues/1260 2021-06-24 17:58:04 +08:00
37a2e3cc0e 光环助手V5.0.0-新社区发布功能(UI测试问题汇总4,9) https://git.ghzs.com/pm/halo-app-issues/-/issues/1252 2021-06-24 17:04:21 +08:00
30595741cc Merge branch 'dev-5.0.0' of git.ghzs.com:halo/android/assistant-android into dev-5.0.0 2021-06-24 16:46:02 +08:00
11f177d572 完成视频贴详情仿bilibili交互效果 2021-06-24 16:45:51 +08:00
9e13675ca5 完成前端优化汇总(0623测试2) https://git.ghzs.com/pm/halo-app-issues/-/issues/1260#note_105557 2021-06-24 15:14:34 +08:00
144a0641b6 完成游戏专题功能优化(20210617测试问题6) 和 20210622UI测试 2021-06-24 15:13:35 +08:00
95266f6f68 【光环助手V5.0.0】提问帖相关需求(0623测试反馈2) https://git.ghzs.com/pm/halo-app-issues/-/issues/1251 2021-06-24 11:27:56 +08:00
9ded647c0d 光环助手V5.0.0-新社区发布功能(0623 产品测试问题1,2) https://git.ghzs.com/pm/halo-app-issues/-/issues/1252 2021-06-24 10:57:49 +08:00
f566e4d5c3 Merge branch 'dev-5.0.0' of git.ghzs.com:halo/android/assistant-android into dev-5.0.0 2021-06-24 10:40:31 +08:00
5df3efd087 光环助手V5.0.0-新社区展示功能(0623 产品测试问题5) https://git.ghzs.com/pm/halo-app-issues/-/issues/1253 2021-06-24 10:40:19 +08:00
98f4361bf0 Merge branch 'dev-5.0.0' of git.ghzs.com:halo/android/assistant-android into dev-5.0.0 2021-06-24 10:09:21 +08:00
000d1e020a 完成关于MOD游戏下载用户的相关限制(APP 接口修改) https://git.ghzs.com/pm/halo-app-issues/-/issues/1293 2021-06-24 10:07:58 +08:00
lyr
93431a7e37 光环助手V5.0.0-新社区展示功能(0622 产品测试问题 2、4、5)https://git.ghzs.com/pm/halo-app-issues/-/issues/1253#note_105293 2021-06-23 17:59:34 +08:00
lyr
d54523cdb6 光环助手V5.0.0-新社区展示功能(0621 产品测试问题 0、1、2、5)https://git.ghzs.com/pm/halo-app-issues/-/issues/1253#note_105232 2021-06-23 15:59:03 +08:00
ed468b7c73 【光环助手V5.0.0】提问帖相关需求(0623测试反馈1,3) https://git.ghzs.com/pm/halo-app-issues/-/issues/1251 2021-06-23 15:55:11 +08:00
bad6211bf5 光环助手V5.0.0-新社区发布功能 0622 产品测试问题 1,2(1)(3)(4)(5) https://git.ghzs.com/pm/halo-app-issues/-/issues/1252 2021-06-23 14:33:27 +08:00
2743d4d0de Merge branch 'dev-5.0.0' of git.ghzs.com:halo/android/assistant-android into dev-5.0.0 2021-06-22 15:43:58 +08:00
a0f5387917 光环助手V5.0.0-新社区展示功能(0621 产品测试问题4,6,7,8(1,3,4,5,7,8,9,11)) https://git.ghzs.com/pm/halo-app-issues/-/issues/1253 2021-06-22 15:43:49 +08:00
1abfcfdb51 abiFilters 添加 arm64 2021-06-22 14:28:42 +08:00
ecc0a75f52 光环助手V5.0.0-新社区展示功能(UI测试问题汇总10-14) https://git.ghzs.com/pm/halo-app-issues/-/issues/1253 2021-06-21 16:53:21 +08:00
c98bf26337 光环助手V5.0.0-新社区发布功能(UI测试问题汇总1,3-9) https://git.ghzs.com/pm/halo-app-issues/-/issues/1252 2021-06-21 15:44:25 +08:00
187d07d02d 完成光环前端优化汇总(5, 14, 16, 17) https://git.ghzs.com/pm/halo-app-issues/-/issues/1260 2021-06-18 17:51:59 +08:00
c47c23ad21 修复因懒加载优化而造成的下载进度更新异常问题 2021-06-18 09:46:30 +08:00
8d011ae7ef Merge branch 'hotfix-v4.9.6-337-generic_bugs' into 'release'
修复下载模拟器时会暂停其它下载任务的问题

See merge request halo/android/assistant-android!117
2021-06-17 18:38:04 +08:00
36b58383f7 版本更新至 4.9.6-338 2021-06-17 18:37:32 +08:00
e6ea1738e5 光环前端优化汇总(2021年5月)(4) https://git.ghzs.com/pm/halo-app-issues/-/issues/1260 2021-06-17 16:32:55 +08:00
70b7689ac5 修复下载模拟器时会暂停其它下载任务的问题 2021-06-17 15:18:41 +08:00
40983ecebc 光环前端优化汇总(2021年5月)(3) https://git.ghzs.com/pm/halo-app-issues/-/issues/1260 2021-06-17 14:41:35 +08:00
47b0a8d1ac 版本更新至 4.9.6-337 2021-06-17 10:43:33 +08:00
1ae63d3010 Merge branch 'hotfix-v4.9.6-336-recreate' into 'release'
修复首页和游戏详情页页面重建时的 tab 显示问题

See merge request halo/android/assistant-android!116
2021-06-17 10:39:51 +08:00
fbe73ce4da Merge branch 'dev-5.0.0' of git.ghzs.com:halo/android/assistant-android into dev-5.0.0 2021-06-16 18:43:48 +08:00
d46617237f 光环助手V5.0.0-新社区展示功能(社区搜索页的搜索结果增加论坛tab) https://git.ghzs.com/pm/halo-app-issues/-/issues/1253 2021-06-16 18:43:43 +08:00
lyr
0894957d73 修改帖子再次编辑修改成功后的闪退问题 2021-06-16 17:58:15 +08:00
9b9777ae19 修复首页和游戏详情页页面重建时的 tab 显示问题 2021-06-16 16:18:55 +08:00
lyr
7f2145d0ef 论坛信息流视频在切换tab时暂停播放 2021-06-16 15:19:21 +08:00
lyr
acefbba6bf Merge remote-tracking branch 'origin/dev-5.0.0' into dev-5.0.0 2021-06-16 14:12:31 +08:00
lyr
660a091b7b 【光环助手5.0.0】一键登录相关优化(第1点)https://git.ghzs.com/pm/halo-app-issues/-/issues/1294 2021-06-16 14:12:25 +08:00
103078d63e 修改图片预览页面点击查看帖子异常 2021-06-16 11:00:28 +08:00
lyr
178395f531 优化论坛信息流审核中视频的显示 2021-06-16 09:52:52 +08:00
lyr
8db213d040 【光环助手V5.0.0】提问帖相关需求(0615测试反馈 3)https://git.ghzs.com/pm/halo-app-issues/-/issues/1251#note_104578 2021-06-16 09:51:13 +08:00
lyr
5904d3c5c7 光环助手V5.0.0-新社区展示功能(前置说明 1) https://git.ghzs.com/pm/halo-app-issues/-/issues/1253 2021-06-15 17:53:01 +08:00
1a511ebbad 【光环助手V5.0.0】提问帖相关需求 (0615测试反馈 1) https://git.ghzs.com/pm/halo-app-issues/-/issues/1251 2021-06-15 17:09:04 +08:00
ef97e8cc6d 新社区插入视频后封面上传到oss 2021-06-15 16:07:21 +08:00
814cef6218 Merge branch 'hotfix-v4.9.6-336-crash' into 'release'
1.修复Xapk解压过程中获取解压文件大小时的闪退问题

See merge request halo/android/assistant-android!115
2021-06-15 11:55:02 +08:00
lyr
fd6c4836d4 1.修复Xapk解压过程中获取解压文件大小时的闪退问题
2.修复新分类2.0游戏列表时的空指针问题
3.修复新分类2.0已选分类列表的数组越界问题
2021-06-15 11:51:22 +08:00
9df138ed7c Merge branch 'hotfix-v4.9.6-336-install' into 'release'
特殊处理山寨机在后台开启使用浏览器安装时的安装 intent

See merge request halo/android/assistant-android!114
2021-06-15 11:02:37 +08:00
7b11b0e99a Merge branch 'dev-5.0.0' of git.ghzs.com:halo/android/assistant-android into dev-5.0.0 2021-06-11 17:47:24 +08:00
01034f093a 处理合并冲突 2021-06-11 17:45:52 +08:00
8e28766449 Merge branch 'feature-bbs' into dev-5.0.0
# Conflicts:
#	app/src/main/AndroidManifest.xml
#	app/src/main/java/com/gh/common/util/NewLogUtils.kt
#	app/src/main/java/com/gh/gamecenter/personalhome/UserHomeFragment.kt
#	app/src/main/java/com/gh/gamecenter/qa/dialog/MoreFunctionPanelDialog.kt
#	app/src/main/res/layout/fragment_home.xml
#	app/src/main/res/values/colors.xml
2021-06-11 17:34:03 +08:00
lyr
d06d0bd15c 一键登录设置每步超时时间为1.5秒 2021-06-11 17:04:31 +08:00
4fe4a33b80 光环前端优化汇总(2021年5月)(13) https://git.ghzs.com/pm/halo-app-issues/-/issues/1260 2021-06-11 14:55:10 +08:00
4a8e91bbff 对接视频帖加精 2021-06-11 10:09:13 +08:00
lyr
b1f6c7d55c 1.优化游戏详情顶部视频和论坛信息流视频的静音设置
2.修改个人主页发布记录的类型、点赞、评论、回答数据字段转化
2021-06-10 18:06:55 +08:00
lyr
727eeac52e 修改游戏详情顶部静音设置 2021-06-10 16:26:35 +08:00
fb26ec0c13 Merge branch 'feature-bbs' of git.ghzs.com:halo/android/assistant-android into feature-bbs 2021-06-10 15:45:55 +08:00
3e25619bf8 论坛修改点赞、评论、回答、收藏数据字段 2021-06-10 15:45:50 +08:00
lyr
e81fa4ff1a 论坛根据类型展示图标 2021-06-10 11:41:02 +08:00
d7d2bf2667 更换问题收藏/取消收藏接口 2021-06-10 11:16:39 +08:00
c763a0a9e6 完成选择论坛-搜索论坛功能 2021-06-10 10:28:06 +08:00
1f34f95e7c 特殊处理山寨机在后台开启使用浏览器安装时的安装 intent 2021-06-10 09:53:54 +08:00
2afb99b603 修改编辑视频帖发布按钮一直是置灰状态 2021-06-09 18:38:06 +08:00
lyr
2e09ac033b 优化视频贴类型转化 2021-06-09 18:25:27 +08:00
lyr
54747143d1 1.修改论坛详情-视频Tab无法下拉刷新问题
2.修改论坛详情-列表视频贴未显示内容问题
3.修改论坛详情-全部Tab的问答贴回答数量不对问题
2021-06-09 18:10:10 +08:00
076c98977d 1.修改问题详情评论不显示作者标签 2.修改问题详情点击顶部返回按钮无法关闭页面 2021-06-09 16:47:19 +08:00
lyr
b96104fcd9 对接社区推荐Tab的接口 2021-06-09 16:26:04 +08:00
lyr
daece9337c 修改浏览记录-帖子Item左下角对应论坛UI未显示问题 2021-06-09 14:35:13 +08:00
lyr
3547330e2a 论坛详情页-内容搜索结果增加视频帖的展示 2021-06-08 18:19:20 +08:00
4d76fb0159 Merge branch 'feature-bbs' of git.ghzs.com:halo/android/assistant-android into feature-bbs 2021-06-08 17:47:04 +08:00
f34a95b36f 1.更换提问、草稿、问题详情接口 2.对接视频帖评论接口 2021-06-08 17:46:56 +08:00
lyr
6e3cb7a196 1.论坛列表增加视频自动播放功能
2.论坛首页搜索适配视频贴展示
2021-06-08 17:39:06 +08:00
03f87c46ba 游戏更新相关接口添加版本和渠道 https://git.ghzs.com/pm/halo-app-issues/-/issues/1267 2021-06-08 17:08:02 +08:00
b052ba1635 移除无用的 activity 声明 2021-06-08 16:34:27 +08:00
d1183199cd 全局更改跳转视频详情的页面 https://git.ghzs.com/pm/halo-app-issues/-/issues/1254 2021-06-08 16:31:30 +08:00
31fed948f0 更换社区-推荐的接口 https://git.ghzs.com/pm/halo-app-issues/-/issues/1246 2021-06-08 15:54:44 +08:00
f1c5b80dc2 完成社区内容浏览日志上报 https://git.ghzs.com/pm/halo-app-issues/-/issues/1246 2021-06-08 15:51:10 +08:00
86f2aec9ca 修复社区内容分享日志上报丢失的问题 2021-06-08 15:28:26 +08:00
f826dc07c6 版本更新至 4.9.6 2021-06-08 10:23:08 +08:00
34670d08c0 Merge branch 'hotfix-v4.9.5-335-crash' into 'release'
Hotfix v4.9.5 335 crash

See merge request halo/android/assistant-android!113
2021-06-08 10:22:04 +08:00
5e5646468e 1. 捕抓分类2.0页面延迟操作造成的闪退异常
2. 修复首页滑动 tab 时偶发的闪退
3. 捕抓模拟器游戏延迟操作造成的闪退异常
4. 捕抓礼包详情页延迟操作造成的闪退异常
2021-06-08 10:22:04 +08:00
a218f13ccb Merge branch 'feature-bbs' of git.ghzs.com:halo/android/assistant-android into feature-bbs 2021-06-08 10:15:23 +08:00
1fc37571df 修改ImageUtils不能加载视频第一帧的图片 2021-06-08 10:15:12 +08:00
lyr
576f06f250 1.修改论坛详情页列表内容显示不对问题
2.修改论坛信息流左下角对应论坛图标显示不出来问题
2021-06-08 10:03:36 +08:00
3805cb52f6 优化论坛插入视频 2021-06-07 21:38:07 +08:00
lyr
f8d7377236 完成新社区-活动Tab 2021-06-07 18:45:43 +08:00
354fa13a35 优化论坛发布功能 2021-06-07 16:02:29 +08:00
3c533b896b 对接我的论坛帖子、我的收藏帖子 2021-06-05 19:18:57 +08:00
11c6618c0f Merge branch 'feature-bbs' of git.ghzs.com:halo/android/assistant-android into feature-bbs 2021-06-05 10:27:55 +08:00
898bf3e432 粗略完成新问题详情页面 2021-06-05 10:27:49 +08:00
lyr
0faf408762 修复新社区相关数据库迁移报错问题 2021-06-04 17:44:49 +08:00
lyr
1dbabe3cda 提交漏传文件 2021-06-04 16:19:07 +08:00
lyr
a107a19d95 完成社区-论坛Tab-官方论坛对接 2021-06-04 15:43:53 +08:00
lyr
58ea0bb51d 完善论坛信息流视频播放逻辑 2021-06-04 14:25:17 +08:00
557f22b23b rxPermission 更新至 androidX 版本 2021-06-03 16:59:40 +08:00
f40a192e82 调整带 chucker 打包脚本 2021-06-03 16:54:55 +08:00
84f3556275 Merge remote-tracking branch 'origin/release' into dev-5.0.0
# Conflicts:
#	dependencies.gradle
2021-06-03 16:47:45 +08:00
cddc2bff2b 添加简单的 Log 工具类 2021-06-03 16:38:58 +08:00
a0b8caa60a 完成5.0.0游戏专题功能优化 https://git.ghzs.com/pm/halo-app-issues/-/issues/1247 2021-06-03 16:38:12 +08:00
lyr
22cadc77f0 1.修改"社区-推荐Tab最近浏览列表首个Item未读红点已读但未消失"问题
2.修改首页底部导航栏动画不对应问题
3.修改论坛详情页关注按钮显示不全问题
2021-06-03 11:28:09 +08:00
28fb45f0b8 添加简单的 Log 工具类 2021-06-03 11:27:44 +08:00
3d7f8b3f41 Merge branch 'feature-bbs' of git.ghzs.com:halo/android/assistant-android into feature-bbs 2021-06-03 10:30:04 +08:00
bba326cfd9 1.收藏/取消收藏问题 2.修改我的论坛 2021-06-03 10:29:57 +08:00
lyr
f76bb7070a 优化社区-推荐Tab列表显示 2021-06-03 10:28:36 +08:00
lyr
a6a3d769bd 完成论坛详情页的视频Tab和个人主页的发布Tab的视频分类 2021-06-02 18:16:56 +08:00
ea5627b3da 区分正式包和测试包的默认渠道,避免正式包因渠道丢失而回落到测试渠道 2021-06-02 16:58:41 +08:00
d9807227ec 处理列表分割线显示异常问题 2021-06-01 09:26:26 +08:00
039203408a 版本更新至 4.9.5 2021-05-31 10:32:07 +08:00
60b325812e Merge branch 'hotfix-v4.9.4-334-crash' into 'release'
Hotfix v4.9.4 334 crash

See merge request halo/android/assistant-android!112
2021-05-31 10:29:00 +08:00
06a43f617b 处理闪退
1. 处理首页 tab 触摸时某些设备可能会闪退的问题
2. 修复游戏库刷新按钮被遮盖的问题
3. 尝试修复游戏库横向列表因恢复滚动状态而出现的闪退
4. 修复偶发的因为获取不到渠道号而触发的闪退
5. 尝试修复启动时偶发的初始化图片加载库触发的闪退
6. 修复 Android 11 首次安装 XAPK 获取权限回到页面时的闪退问题
2021-05-31 10:29:00 +08:00
5cc1cfa1a6 完成视频详情推荐列表 2021-05-29 17:17:09 +08:00
35cdd140cc Merge branch 'feature-bbs' of git.ghzs.com:halo/android/assistant-android into feature-bbs 2021-05-29 16:07:48 +08:00
87597dbd1f 完成帖子、问题发布功能 2021-05-29 16:07:33 +08:00
lyr
398f7b6642 1.完成首页和游戏详情的视频播放设置
2.完成个人主页的发布Tab(除视频分类)
2021-05-28 18:52:10 +08:00
85d3412fd8 版本更新到 4.9.4 2021-05-28 10:28:06 +08:00
71b8cbbef3 Merge branch 'hotfix-v4.9.3-333-crash' into 'release'
Hotfix v4.9.3 333 crash

See merge request halo/android/assistant-android!111
2021-05-28 10:26:50 +08:00
dfc0183a14 1. 处理包名检测弹窗上报日志的空指针闪退问题
2. 捕抓评论列表一键登录可能触发的闪退问题
3. 修复礼包详情页重建时的闪退问题
4. 修复视频流评论页页面重建时的闪退问题
2021-05-28 10:26:50 +08:00
lyr
f0ca0a2ab1 推荐页的浏览记录和论坛页的关注论坛增加未读状态 2021-05-27 15:44:01 +08:00
c9ffd4e5bd Merge branch 'feature-bbs' of git.ghzs.com:halo/android/assistant-android into feature-bbs 2021-05-26 20:45:06 +08:00
d354b07fe2 1.抽离公共评论详情页面 2.粗略完成问题详情页面UI(未对接接口) 2021-05-26 20:44:46 +08:00
lyr
71a61fdef0 粗略完成社区的推荐Tab和论坛Tab 2021-05-26 19:00:06 +08:00
2eded37321 首页社区 TAB 和首页视频 TAB 互换位置 2021-05-26 10:20:06 +08:00
f35baf0e5b 完成[安装方式管理]新增[白名单]功能 https://git.ghzs.com/pm/halo-app-issues/-/issues/1264 2021-05-25 17:54:02 +08:00
4d64281c78 修改视频贴保存草稿功能 2021-05-24 21:20:04 +08:00
95f1692434 Merge branch 'feature-bbs' of git.ghzs.com:halo/android/assistant-android into feature-bbs 2021-05-24 15:51:12 +08:00
f1302dd1bb 完成视频详情顶部区域 2021-05-24 15:51:00 +08:00
lyr
fec34f1efa 提交遗漏资源文件 2021-05-24 10:45:18 +08:00
lyr
def3b55e51 完成光环助手V5.0.0-新社区其他相关改动功能(第七点UI) 2021-05-24 09:57:08 +08:00
4549f78ede 粗略完成发视频贴功能 2021-05-23 21:04:00 +08:00
3054b56f4a Merge branch 'feature-bbs' of git.ghzs.com:halo/android/assistant-android into feature-bbs 2021-05-20 14:13:28 +08:00
4cf5e0f2f4 论坛富文本支持插入视频功能 2021-05-20 14:13:22 +08:00
lyr
1a18c82f06 个人主页-根据个人信息内容设置背景的高度 2021-05-19 18:51:49 +08:00
lyr
b8fbb429f1 个人主页-根据个人信息内容设置背景的高度 2021-05-19 18:40:25 +08:00
lyr
005229fe8a 提交个人主页游戏tab改动遗漏部分 2021-05-19 18:31:25 +08:00
lyr
3a0bbbf3d0 完成个人主页游戏tab改动 2021-05-19 18:09:34 +08:00
lyr
868e6d31b8 提交论坛详情页顶部UI遗漏部分 2021-05-18 15:31:29 +08:00
lyr
a687f8d877 完成论坛详情页顶部UI调整 2021-05-18 15:15:38 +08:00
3e86a40215 发提问改为富文本 2021-05-13 20:34:09 +08:00
6a68839d0f 1完成选择活动弹窗UI 2.发布帖子、提问删除添加标签入口 2021-05-13 15:31:18 +08:00
2140c9eb40 Update README.md 2021-05-13 15:13:41 +08:00
942af39b2d 大致完成选择论坛、选择本地视频、预览视频、发视频UI 2021-05-12 16:55:18 +08:00
lyr
a0331f0437 光环前端优化汇总(2021年5月)第6点 https://git.ghzs.com/pm/halo-app-issues/-/issues/1260 2021-05-12 14:46:31 +08:00
lyr
63798c94cf Merge branch 'dev' into dev-5.0.0
# Conflicts:
#	app/src/main/java/com/gh/common/util/QuickLoginHelper.kt
2021-05-10 17:06:12 +08:00
lyr
6c2ff6a94f 光环助手V4.9.0-优化登录功能(第3期)(0507 补充运营意见--增加一键登录说明弹窗) https://git.ghzs.com/pm/halo-app-issues/-/issues/1206#note_100061
(cherry picked from commit 3f5f3bf57c)
2021-05-08 16:15:54 +08:00
2211f562c7 抽离发布帖子、回答公共代码 2021-05-08 15:52:41 +08:00
7a0165375c 去掉视频预加载打印日志 2021-04-29 14:46:47 +08:00
e280ea008a 升级gsyVideoPlayer版本并修改预加载 2021-04-28 11:08:04 +08:00
1114 changed files with 124699 additions and 19530 deletions

View File

@ -1,69 +1,68 @@
# 光环助手Android客户端
### APK打包配置
### 概述
* 使用[ApkChannelPackage](https://github.com/ltlovezh/ApkChannelPackage)的方案
* 打包命令,视情况使用:
光环助手Android客户端目前使用 Kotlin 作为主要开发语言,以 MVVM 作为参考架构模式进行开发
> 打包Tinker基准包`./scripts/tinker_release_base.sh`
### 约束
> 以Tinker基准包打渠道包`./scripts/tinker_release_channel.sh`
为编写易读易维护且较健壮的代码,可参考以下约束
> 以Tinker基准包打补丁包`./scripts/tinker_release_patch.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!
### 混淆配置
### 公用部分
* 配置文件Android默认配置+proguard-rules.txt等
* 参考libraries下每个项目独立的配置文件`proguard-project.txt`
本项目使用 LiveData 实现了一个简单通用的基础列表分页功能,具体可见 `ListFragment`, `ListViewModel` 等类,理想情况下只需少量代码即可新建一个简单分页列表
### apk大小优化
### 首次拉取项目代码
* 限制resConfig资源集
* 开启ShrinkResources
* 开启混淆使用minifyEnabled(仅在release开启
* pngquant对png压缩、png/jpg->webp(未尝试)
`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%) (转换后体积变大的文件除外)
### 第三方appkey等配置
* 修改`gradle.properties`文件将各种key填入其中实现统一管理
* 通过gradle文件内的resValue/buildConfigField/manifestPlaceHolder方式实现编译期间修改具体情况请参考``./build.gradle``和``./app/build.gradle``配置
### 拉取代码步骤
### 混淆配置
1. 拉取主项目代码: `git clone -b dev git@gitlab.ghzhushou.com:halo/assistant-android.git`
2. 初始化公用类库: `bash ./scripts/submodules_init.sh`
* 本项目使用了微信的 [AndResGuard](https://github.com/shwenzhang/AndResGuard) 作为资源混淆压缩方案,新增需要使用 `getIdentifier` 获取的资源文件时需要添加至白名单
* 本项目默认使用 R8 作为混淆工具,往 proguard-rules.txt 添加 proguard 新配置项时请检查可用性(如语法等)
### submodule管理方式(只拉取master)
### APK打包配置
* 提交代码需要cd到submodule文件夹去做修改
* 更新远端代码,`bash ./scripts/submodules_update.sh`
* 本项目使用了 [VasDolly](https://github.com/Tencent/VasDolly) 作为渠道包实现方案
* 打包命令,具体参数请见相应文件:
> 打内部测试包:`./scripts/test_build.sh`
> 打正式发布包:`./scripts/build_with_simple_backup.sh`
### TODO
* 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
* 把原有 EventBus 的消息 Type 统一一个文件内
* 将实现细节从 View(Fragment、Activity) 剥离并以 MVVM 结构改造
* 重构 MainActivity

View File

@ -46,6 +46,11 @@ android {
}
ndk {
// 如果不添加 `arm64` 调用系统的 PackageManager 的方法读取安装包信息的时候会出现 native 层闪退,草
// 添加了 `arm64` 以后部分 5.0 的设备会报用错 so 的问题,
// couldn't find DSO to load: libimagepipeline.so caused by: dlopen failed: "/data/data/com.gh.gamecenter/lib-main/libimagepipeline.so" is 64-bit instead of 32-bit result: 0
// 以 OPPO R7PLUS 为例,明明设备是骁龙 615ARMv8-64 bit 的设备却不支持 arm64 的 abi
// 惊了
abiFilters "armeabi-v7a", "x86"
}
@ -216,11 +221,9 @@ dependencies {
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}"
@ -276,8 +279,6 @@ 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}"
@ -300,11 +301,11 @@ dependencies {
implementation "com.squareup.picasso:picasso:${picasso}"
// for video streaming
implementation("com.shuyu:gsyVideoPlayer-java:$gsyVideo", {
implementation("com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-java:$gsyVideo", {
exclude module: "gsyvideoplayer-androidvideocache"
exclude group: "tv.danmaku.ijk.media"
})
implementation "com.shuyu:GSYVideoPlayer-exo2:$gsyVideo"
implementation "com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-exo_player2:$gsyVideo"
implementation "android.arch.work:work-runtime:${workManager}"
@ -317,22 +318,20 @@ dependencies {
implementation "com.tencent.mm.opensdk:wechat-sdk-android-without-mta:${mta}"
implementation "com.github.nichbar:AndroidRomChecker:${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"
// implementation "com.bytedance.ies.ugc.aweme:opensdk-china-external:$bytedanceAweme"
// implementation "com.bytedance.ies.ugc.aweme:opensdk-common:$bytedanceAweme"
debugImplementation "com.github.nichbar.chucker:library:${chucker}"
releaseImplementation "com.github.nichbar.chucker:library-no-op:${chucker}"
teaImplementation "com.bytedance.applog:RangersAppLog-Lite-cn:${bytedanceApplog}"
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'
@ -340,10 +339,15 @@ dependencies {
})
implementation "com.github.PhilJay:MPAndroidChart:${chart}"
implementation "com.github.hsiafan:apk-parser:$apkParser"
implementation "org.nanohttpd:nanohttpd:$nanohttpd"
implementation "com.github.hsiafan:apk-parser:${apkParser}"
implementation "org.nanohttpd:nanohttpd:${nanohttpd}"
implementation "com.aliyun.openservices:aliyun-log-android-sdk:$aliyunLog"
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')
@ -461,6 +465,11 @@ andResGuard {
"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",

View File

@ -51,7 +51,7 @@
}
### weiboSdk
-keep class com.sina.weibo.sdk.* { *; }
-keep class com.sina.weibo.sdk.** { *; }
-dontwarn android.webkit.WebView
-dontwarn android.webkit.WebViewClient
@ -131,4 +131,12 @@
### 中国移动一键登录
-dontwarn com.cmic.sso.sdk.**
-keep class 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(...);
}

View File

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

View File

@ -30,14 +30,8 @@
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" />
@ -47,9 +41,11 @@
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" />
pl.droidsonroids.gif,
com.lzf.easyfloat"/>
<!-- 去掉 SDK 一些流氓权限 -->
<uses-permission
@ -70,9 +66,9 @@
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"
android:networkSecurityConfig="@xml/network_security_config"
tools:replace="android:allowBackup"
tools:targetApi="n">
@ -85,7 +81,6 @@
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"
@ -112,7 +107,6 @@
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" />
@ -170,6 +164,10 @@
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" />
@ -475,7 +473,7 @@
android:screenOrientation="portrait" />
<activity
android:name="com.gh.gamecenter.qa.editor.VideoActivity"
android:name="com.gh.gamecenter.qa.editor.LocalMediaActivity"
android:screenOrientation="portrait" />
<activity
@ -513,6 +511,12 @@
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"
@ -552,10 +556,6 @@
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,6 +564,10 @@
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" />
@ -585,10 +589,6 @@
android:screenOrientation="portrait"
android:theme="@style/TransparentStatusBarAndNavigationBar" />
<activity
android:name=".personalhome.excellentcomments.ExcellentCommentsActivity"
android:screenOrientation="portrait" />
<activity
android:name=".simulatorgame.SimulatorGameActivity"
android:screenOrientation="portrait" />
@ -611,8 +611,8 @@
<activity
android:name=".energy.EnergyCenterActivity"
android:screenOrientation="portrait"
android:launchMode="singleTask" />
android:launchMode="singleTask"
android:screenOrientation="portrait" />
<activity
android:name=".energy.EnergyHouseActivity"
@ -638,13 +638,57 @@
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:theme="@android:style/Theme.Dialog"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:launchMode="singleTop">
</activity>
android:theme="@android:style/Theme.Dialog" />
<activity

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,58 @@
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

@ -0,0 +1,550 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig;
import com.android.apksig.apk.ApkFormatException;
import com.android.apksig.util.DataSink;
import com.android.apksig.util.DataSource;
import com.android.apksig.util.RunnablesExecutor;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SignatureException;
import java.util.List;
import java.util.Set;
/**
* APK signing logic which is independent of how input and output APKs are stored, parsed, and
* generated.
*
* <p><h3>Operating Model</h3>
*
* The abstract operating model is that there is an input APK which is being signed, thus producing
* an output APK. In reality, there may be just an output APK being built from scratch, or the input
* APK and the output APK may be the same file. Because this engine does not deal with reading and
* writing files, it can handle all of these scenarios.
*
* <p>The engine is stateful and thus cannot be used for signing multiple APKs. However, once
* the engine signed an APK, the engine can be used to re-sign the APK after it has been modified.
* This may be more efficient than signing the APK using a new instance of the engine. See
* <a href="#incremental">Incremental Operation</a>.
*
* <p>In the engine's operating model, a signed APK is produced as follows.
* <ol>
* <li>JAR entries to be signed are output,</li>
* <li>JAR archive is signed using JAR signing, thus adding the so-called v1 signature to the
* output,</li>
* <li>JAR archive is signed using APK Signature Scheme v2, thus adding the so-called v2 signature
* to the output.</li>
* </ol>
*
* <p>The input APK may contain JAR entries which, depending on the engine's configuration, may or
* may not be output (e.g., existing signatures may need to be preserved or stripped) or which the
* engine will overwrite as part of signing. The engine thus offers {@link #inputJarEntry(String)}
* which tells the client whether the input JAR entry needs to be output. This avoids the need for
* the client to hard-code the aspects of APK signing which determine which parts of input must be
* ignored. Similarly, the engine offers {@link #inputApkSigningBlock(DataSource)} to help the
* client avoid dealing with preserving or stripping APK Signature Scheme v2 signature of the input
* APK.
*
* <p>To use the engine to sign an input APK (or a collection of JAR entries), follow these
* steps:
* <ol>
* <li>Obtain a new instance of the engine -- engine instances are stateful and thus cannot be used
* for signing multiple APKs.</li>
* <li>Locate the input APK's APK Signing Block and provide it to
* {@link #inputApkSigningBlock(DataSource)}.</li>
* <li>For each JAR entry in the input APK, invoke {@link #inputJarEntry(String)} to determine
* whether this entry should be output. The engine may request to inspect the entry.</li>
* <li>For each output JAR entry, invoke {@link #outputJarEntry(String)} which may request to
* inspect the entry.</li>
* <li>Once all JAR entries have been output, invoke {@link #outputJarEntries()} which may request
* that additional JAR entries are output. These entries comprise the output APK's JAR
* signature.</li>
* <li>Locate the ZIP Central Directory and ZIP End of Central Directory sections in the output and
* invoke {@link #outputZipSections2(DataSource, DataSource, DataSource)} which may request that
* an APK Signature Block is inserted before the ZIP Central Directory. The block contains the
* output APK's APK Signature Scheme v2 signature.</li>
* <li>Invoke {@link #outputDone()} to signal that the APK was output in full. The engine will
* confirm that the output APK is signed.</li>
* <li>Invoke {@link #close()} to signal that the engine will no longer be used. This lets the
* engine free any resources it no longer needs.
* </ol>
*
* <p>Some invocations of the engine may provide the client with a task to perform. The client is
* expected to perform all requested tasks before proceeding to the next stage of signing. See
* documentation of each method about the deadlines for performing the tasks requested by the
* method.
*
* <p><h3 id="incremental">Incremental Operation</h3></a>
*
* The engine supports incremental operation where a signed APK is produced, then modified and
* re-signed. This may be useful for IDEs, where an app is frequently re-signed after small changes
* by the developer. Re-signing may be more efficient than signing from scratch.
*
* <p>To use the engine in incremental mode, keep notifying the engine of changes to the APK through
* {@link #inputApkSigningBlock(DataSource)}, {@link #inputJarEntry(String)},
* {@link #inputJarEntryRemoved(String)}, {@link #outputJarEntry(String)},
* and {@link #outputJarEntryRemoved(String)}, perform the tasks requested by the engine through
* these methods, and, when a new signed APK is desired, run through steps 5 onwards to re-sign the
* APK.
*
* <p><h3>Output-only Operation</h3>
*
* The engine's abstract operating model consists of an input APK and an output APK. However, it is
* possible to use the engine in output-only mode where the engine's {@code input...} methods are
* not invoked. In this mode, the engine has less control over output because it cannot request that
* some JAR entries are not output. Nevertheless, the engine will attempt to make the output APK
* signed and will report an error if cannot do so.
*
* @see <a href="https://source.android.com/security/apksigning/index.html">Application Signing</a>
*/
public interface ApkSignerEngine extends Closeable {
default void setExecutor(RunnablesExecutor executor) {
throw new UnsupportedOperationException("setExecutor method is not implemented");
}
/**
* Initializes the signer engine with the data already present in the apk (if any). There
* might already be data that can be reused if the entries has not been changed.
*
* @param manifestBytes
* @param entryNames
* @return set of entry names which were processed by the engine during the initialization, a
* subset of entryNames
*/
default Set<String> initWith(byte[] manifestBytes, Set<String> entryNames) {
throw new UnsupportedOperationException("initWith method is not implemented");
}
/**
* Indicates to this engine that the input APK contains the provided APK Signing Block. The
* block may contain signatures of the input APK, such as APK Signature Scheme v2 signatures.
*
* @param apkSigningBlock APK signing block of the input APK. The provided data source is
* guaranteed to not be used by the engine after this method terminates.
*
* @throws IOException if an I/O error occurs while reading the APK Signing Block
* @throws ApkFormatException if the APK Signing Block is malformed
* @throws IllegalStateException if this engine is closed
*/
void inputApkSigningBlock(DataSource apkSigningBlock)
throws IOException, ApkFormatException, IllegalStateException;
/**
* Indicates to this engine that the specified JAR entry was encountered in the input APK.
*
* <p>When an input entry is updated/changed, it's OK to not invoke
* {@link #inputJarEntryRemoved(String)} before invoking this method.
*
* @return instructions about how to proceed with this entry
*
* @throws IllegalStateException if this engine is closed
*/
InputJarEntryInstructions inputJarEntry(String entryName) throws IllegalStateException;
/**
* Indicates to this engine that the specified JAR entry was output.
*
* <p>It is unnecessary to invoke this method for entries added to output by this engine (e.g.,
* requested by {@link #outputJarEntries()}) provided the entries were output with exactly the
* data requested by the engine.
*
* <p>When an already output entry is updated/changed, it's OK to not invoke
* {@link #outputJarEntryRemoved(String)} before invoking this method.
*
* @return request to inspect the entry or {@code null} if the engine does not need to inspect
* the entry. The request must be fulfilled before {@link #outputJarEntries()} is
* invoked.
*
* @throws IllegalStateException if this engine is closed
*/
InspectJarEntryRequest outputJarEntry(String entryName) throws IllegalStateException;
/**
* Indicates to this engine that the specified JAR entry was removed from the input. It's safe
* to invoke this for entries for which {@link #inputJarEntry(String)} hasn't been invoked.
*
* @return output policy of this JAR entry. The policy indicates how this input entry affects
* the output APK. The client of this engine should use this information to determine
* how the removal of this input APK's JAR entry affects the output APK.
*
* @throws IllegalStateException if this engine is closed
*/
InputJarEntryInstructions.OutputPolicy inputJarEntryRemoved(String entryName)
throws IllegalStateException;
/**
* Indicates to this engine that the specified JAR entry was removed from the output. It's safe
* to invoke this for entries for which {@link #outputJarEntry(String)} hasn't been invoked.
*
* @throws IllegalStateException if this engine is closed
*/
void outputJarEntryRemoved(String entryName) throws IllegalStateException;
/**
* Indicates to this engine that all JAR entries have been output.
*
* @return request to add JAR signature to the output or {@code null} if there is no need to add
* a JAR signature. The request will contain additional JAR entries to be output. The
* request must be fulfilled before
* {@link #outputZipSections2(DataSource, DataSource, DataSource)} is invoked.
*
* @throws ApkFormatException if the APK is malformed in a way which is preventing this engine
* from producing a valid signature. For example, if the engine uses the provided
* {@code META-INF/MANIFEST.MF} as a template and the file is malformed.
* @throws NoSuchAlgorithmException if a signature could not be generated because a required
* cryptographic algorithm implementation is missing
* @throws InvalidKeyException if a signature could not be generated because a signing key is
* not suitable for generating the signature
* @throws SignatureException if an error occurred while generating a signature
* @throws IllegalStateException if there are unfulfilled requests, such as to inspect some JAR
* entries, or if the engine is closed
*/
OutputJarSignatureRequest outputJarEntries()
throws ApkFormatException, NoSuchAlgorithmException, InvalidKeyException,
SignatureException, IllegalStateException;
/**
* Indicates to this engine that the ZIP sections comprising the output APK have been output.
*
* <p>The provided data sources are guaranteed to not be used by the engine after this method
* terminates.
*
* @deprecated This is now superseded by {@link #outputZipSections2(DataSource, DataSource,
* DataSource)}.
*
* @param zipEntries the section of ZIP archive containing Local File Header records and data of
* the ZIP entries. In a well-formed archive, this section starts at the start of the
* archive and extends all the way to the ZIP Central Directory.
* @param zipCentralDirectory ZIP Central Directory section
* @param zipEocd ZIP End of Central Directory (EoCD) record
*
* @return request to add an APK Signing Block to the output or {@code null} if the output must
* not contain an APK Signing Block. The request must be fulfilled before
* {@link #outputDone()} is invoked.
*
* @throws IOException if an I/O error occurs while reading the provided ZIP sections
* @throws ApkFormatException if the provided APK is malformed in a way which prevents this
* engine from producing a valid signature. For example, if the APK Signing Block
* provided to the engine is malformed.
* @throws NoSuchAlgorithmException if a signature could not be generated because a required
* cryptographic algorithm implementation is missing
* @throws InvalidKeyException if a signature could not be generated because a signing key is
* not suitable for generating the signature
* @throws SignatureException if an error occurred while generating a signature
* @throws IllegalStateException if there are unfulfilled requests, such as to inspect some JAR
* entries or to output JAR signature, or if the engine is closed
*/
@Deprecated
OutputApkSigningBlockRequest outputZipSections(
DataSource zipEntries,
DataSource zipCentralDirectory,
DataSource zipEocd)
throws IOException, ApkFormatException, NoSuchAlgorithmException,
InvalidKeyException, SignatureException, IllegalStateException;
/**
* Indicates to this engine that the ZIP sections comprising the output APK have been output.
*
* <p>The provided data sources are guaranteed to not be used by the engine after this method
* terminates.
*
* @param zipEntries the section of ZIP archive containing Local File Header records and data of
* the ZIP entries. In a well-formed archive, this section starts at the start of the
* archive and extends all the way to the ZIP Central Directory.
* @param zipCentralDirectory ZIP Central Directory section
* @param zipEocd ZIP End of Central Directory (EoCD) record
*
* @return request to add an APK Signing Block to the output or {@code null} if the output must
* not contain an APK Signing Block. The request must be fulfilled before
* {@link #outputDone()} is invoked.
*
* @throws IOException if an I/O error occurs while reading the provided ZIP sections
* @throws ApkFormatException if the provided APK is malformed in a way which prevents this
* engine from producing a valid signature. For example, if the APK Signing Block
* provided to the engine is malformed.
* @throws NoSuchAlgorithmException if a signature could not be generated because a required
* cryptographic algorithm implementation is missing
* @throws InvalidKeyException if a signature could not be generated because a signing key is
* not suitable for generating the signature
* @throws SignatureException if an error occurred while generating a signature
* @throws IllegalStateException if there are unfulfilled requests, such as to inspect some JAR
* entries or to output JAR signature, or if the engine is closed
*/
OutputApkSigningBlockRequest2 outputZipSections2(
DataSource zipEntries,
DataSource zipCentralDirectory,
DataSource zipEocd)
throws IOException, ApkFormatException, NoSuchAlgorithmException,
InvalidKeyException, SignatureException, IllegalStateException;
/**
* Indicates to this engine that the signed APK was output.
*
* <p>This does not change the output APK. The method helps the client confirm that the current
* output is signed.
*
* @throws IllegalStateException if there are unfulfilled requests, such as to inspect some JAR
* entries or to output signatures, or if the engine is closed
*/
void outputDone() throws IllegalStateException;
/**
* Generates a V4 signature proto and write to output file.
*
* @param data Input data to calculate a verity hash tree and hash root
* @param outputFile To store the serialized V4 Signature.
* @param ignoreFailures Whether any failures will be silently ignored.
* @throws InvalidKeyException if a signature could not be generated because a signing key is
* not suitable for generating the signature
* @throws NoSuchAlgorithmException if a signature could not be generated because a required
* cryptographic algorithm implementation is missing
* @throws SignatureException if an error occurred while generating a signature
* @throws IOException if protobuf fails to be serialized and written to file
*/
void signV4(DataSource data, File outputFile, boolean ignoreFailures)
throws InvalidKeyException, NoSuchAlgorithmException, SignatureException, IOException;
/**
* Checks if the signing configuration provided to the engine is capable of creating a
* SourceStamp.
*/
default boolean isEligibleForSourceStamp() {
return false;
}
/** Generates the digest of the certificate used to sign the source stamp. */
default byte[] generateSourceStampCertificateDigest() throws SignatureException {
return new byte[0];
}
/**
* Indicates to this engine that it will no longer be used. Invoking this on an already closed
* engine is OK.
*
* <p>This does not change the output APK. For example, if the output APK is not yet fully
* signed, it will remain so after this method terminates.
*/
@Override
void close();
/**
* Instructions about how to handle an input APK's JAR entry.
*
* <p>The instructions indicate whether to output the entry (see {@link #getOutputPolicy()}) and
* may contain a request to inspect the entry (see {@link #getInspectJarEntryRequest()}), in
* which case the request must be fulfilled before {@link ApkSignerEngine#outputJarEntries()} is
* invoked.
*/
public static class InputJarEntryInstructions {
private final OutputPolicy mOutputPolicy;
private final InspectJarEntryRequest mInspectJarEntryRequest;
/**
* Constructs a new {@code InputJarEntryInstructions} instance with the provided entry
* output policy and without a request to inspect the entry.
*/
public InputJarEntryInstructions(OutputPolicy outputPolicy) {
this(outputPolicy, null);
}
/**
* Constructs a new {@code InputJarEntryInstructions} instance with the provided entry
* output mode and with the provided request to inspect the entry.
*
* @param inspectJarEntryRequest request to inspect the entry or {@code null} if there's no
* need to inspect the entry.
*/
public InputJarEntryInstructions(
OutputPolicy outputPolicy,
InspectJarEntryRequest inspectJarEntryRequest) {
mOutputPolicy = outputPolicy;
mInspectJarEntryRequest = inspectJarEntryRequest;
}
/**
* Returns the output policy for this entry.
*/
public OutputPolicy getOutputPolicy() {
return mOutputPolicy;
}
/**
* Returns the request to inspect the JAR entry or {@code null} if there is no need to
* inspect the entry.
*/
public InspectJarEntryRequest getInspectJarEntryRequest() {
return mInspectJarEntryRequest;
}
/**
* Output policy for an input APK's JAR entry.
*/
public static enum OutputPolicy {
/** Entry must not be output. */
SKIP,
/** Entry should be output. */
OUTPUT,
/** Entry will be output by the engine. The client can thus ignore this input entry. */
OUTPUT_BY_ENGINE,
}
}
/**
* Request to inspect the specified JAR entry.
*
* <p>The entry's uncompressed data must be provided to the data sink returned by
* {@link #getDataSink()}. Once the entry's data has been provided to the sink, {@link #done()}
* must be invoked.
*/
interface InspectJarEntryRequest {
/**
* Returns the data sink into which the entry's uncompressed data should be sent.
*/
DataSink getDataSink();
/**
* Indicates that entry's data has been provided in full.
*/
void done();
/**
* Returns the name of the JAR entry.
*/
String getEntryName();
}
/**
* Request to add JAR signature (aka v1 signature) to the output APK.
*
* <p>Entries listed in {@link #getAdditionalJarEntries()} must be added to the output APK after
* which {@link #done()} must be invoked.
*/
interface OutputJarSignatureRequest {
/**
* Returns JAR entries that must be added to the output APK.
*/
List<JarEntry> getAdditionalJarEntries();
/**
* Indicates that the JAR entries contained in this request were added to the output APK.
*/
void done();
/**
* JAR entry.
*/
public static class JarEntry {
private final String mName;
private final byte[] mData;
/**
* Constructs a new {@code JarEntry} with the provided name and data.
*
* @param data uncompressed data of the entry. Changes to this array will not be
* reflected in {@link #getData()}.
*/
public JarEntry(String name, byte[] data) {
mName = name;
mData = data.clone();
}
/**
* Returns the name of this ZIP entry.
*/
public String getName() {
return mName;
}
/**
* Returns the uncompressed data of this JAR entry.
*/
public byte[] getData() {
return mData.clone();
}
}
}
/**
* Request to add the specified APK Signing Block to the output APK. APK Signature Scheme v2
* signature(s) of the APK are contained in this block.
*
* <p>The APK Signing Block returned by {@link #getApkSigningBlock()} must be placed into the
* output APK such that the block is immediately before the ZIP Central Directory, the offset of
* ZIP Central Directory in the ZIP End of Central Directory record must be adjusted
* accordingly, and then {@link #done()} must be invoked.
*
* <p>If the output contains an APK Signing Block, that block must be replaced by the block
* contained in this request.
*
* @deprecated This is now superseded by {@link OutputApkSigningBlockRequest2}.
*/
@Deprecated
interface OutputApkSigningBlockRequest {
/**
* Returns the APK Signing Block.
*/
byte[] getApkSigningBlock();
/**
* Indicates that the APK Signing Block was output as requested.
*/
void done();
}
/**
* Request to add the specified APK Signing Block to the output APK. APK Signature Scheme v2
* signature(s) of the APK are contained in this block.
*
* <p>The APK Signing Block returned by {@link #getApkSigningBlock()} must be placed into the
* output APK such that the block is immediately before the ZIP Central Directory. Immediately
* before the APK Signing Block must be padding consists of the number of 0x00 bytes returned by
* {@link #getPaddingSizeBeforeApkSigningBlock()}. The offset of ZIP Central Directory in the
* ZIP End of Central Directory record must be adjusted accordingly, and then {@link #done()}
* must be invoked.
*
* <p>If the output contains an APK Signing Block, that block must be replaced by the block
* contained in this request.
*/
interface OutputApkSigningBlockRequest2 {
/**
* Returns the APK Signing Block.
*/
byte[] getApkSigningBlock();
/**
* Indicates that the APK Signing Block was output as requested.
*/
void done();
/**
* Returns the number of 0x00 bytes the caller must place immediately before APK Signing
* Block.
*/
int getPaddingSizeBeforeApkSigningBlock();
}
}

View File

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

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

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,123 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig;
import java.io.IOException;
import java.io.DataOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public final class Hints {
/**
* Name of hint pattern asset file in APK.
*/
public static final String PIN_HINT_ASSET_ZIP_ENTRY_NAME = "assets/com.android.hints.pins.txt";
/**
* Name of hint byte range data file in APK. Keep in sync with PinnerService.java.
*/
public static final String PIN_BYTE_RANGE_ZIP_ENTRY_NAME = "pinlist.meta";
private static int clampToInt(long value) {
return (int) Math.max(0, Math.min(value, Integer.MAX_VALUE));
}
public static final class ByteRange {
final long start;
final long end;
public ByteRange(long start, long end) {
this.start = start;
this.end = end;
}
}
public static final class PatternWithRange {
final Pattern pattern;
final long offset;
final long size;
public PatternWithRange(String pattern) {
this.pattern = Pattern.compile(pattern);
this.offset= 0;
this.size = Long.MAX_VALUE;
}
public PatternWithRange(String pattern, long offset, long size) {
this.pattern = Pattern.compile(pattern);
this.offset = offset;
this.size = size;
}
public Matcher matcher(CharSequence input) {
return this.pattern.matcher(input);
}
public ByteRange ClampToAbsoluteByteRange(ByteRange rangeIn) {
if (rangeIn.end - rangeIn.start < this.offset) {
return null;
}
long rangeOutStart = rangeIn.start + this.offset;
long rangeOutSize = Math.min(rangeIn.end - rangeOutStart,
this.size);
return new ByteRange(rangeOutStart,
rangeOutStart + rangeOutSize);
}
}
/**
* Create a blob of bytes that PinnerService understands as a
* sequence of byte ranges to pin.
*/
public static byte[] encodeByteRangeList(List<ByteRange> pinByteRanges) {
ByteArrayOutputStream bos = new ByteArrayOutputStream(pinByteRanges.size() * 8);
DataOutputStream out = new DataOutputStream(bos);
try {
for (ByteRange pinByteRange : pinByteRanges) {
out.writeInt(clampToInt(pinByteRange.start));
out.writeInt(clampToInt(pinByteRange.end - pinByteRange.start));
}
} catch (IOException ex) {
throw new AssertionError("impossible", ex);
}
return bos.toByteArray();
}
public static ArrayList<PatternWithRange> parsePinPatterns(byte[] patternBlob) {
ArrayList<PatternWithRange> pinPatterns = new ArrayList<>();
try {
for (String rawLine : new String(patternBlob, "UTF-8").split("\n")) {
String line = rawLine.replaceFirst("#.*", ""); // # starts a comment
String[] fields = line.split(" ");
if (fields.length == 1) {
pinPatterns.add(new PatternWithRange(fields[0]));
} else if (fields.length == 3) {
long start = Long.parseLong(fields[1]);
long end = Long.parseLong(fields[2]);
pinPatterns.add(new PatternWithRange(fields[0], start, end - start));
} else {
throw new AssertionError("bad pin pattern line " + line);
}
}
} catch (UnsupportedEncodingException ex) {
throw new RuntimeException("UTF-8 must be supported", ex);
}
return pinPatterns;
}
}

File diff suppressed because it is too large Load Diff

View File

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

@ -0,0 +1,35 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.apk;
/**
* Indicates that an APK is not well-formed. For example, this may indicate that the APK is not a
* well-formed ZIP archive, in which case {@link #getCause()} will return a
* {@link com.android.apksig.zip.ZipFormatException ZipFormatException}, or that the APK contains
* multiple ZIP entries with the same name.
*/
public class ApkFormatException extends Exception {
private static final long serialVersionUID = 1L;
public ApkFormatException(String message) {
super(message);
}
public ApkFormatException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -0,0 +1,32 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.apk;
/**
* Indicates that no APK Signing Block was found in an APK.
*/
public class ApkSigningBlockNotFoundException extends Exception {
private static final long serialVersionUID = 1L;
public ApkSigningBlockNotFoundException(String message) {
super(message);
}
public ApkSigningBlockNotFoundException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -0,0 +1,670 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.apk;
import com.android.apksig.internal.apk.AndroidBinXmlParser;
import com.android.apksig.internal.apk.stamp.SourceStampConstants;
import com.android.apksig.internal.apk.v1.V1SchemeVerifier;
import com.android.apksig.internal.util.Pair;
import com.android.apksig.internal.zip.CentralDirectoryRecord;
import com.android.apksig.internal.zip.LocalFileRecord;
import com.android.apksig.internal.zip.ZipUtils;
import com.android.apksig.util.DataSource;
import com.android.apksig.zip.ZipFormatException;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
/**
* APK utilities.
*/
public abstract class ApkUtils {
/**
* Name of the Android manifest ZIP entry in APKs.
*/
public static final String ANDROID_MANIFEST_ZIP_ENTRY_NAME = "AndroidManifest.xml";
/** Name of the SourceStamp certificate hash ZIP entry in APKs. */
public static final String SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME =
SourceStampConstants.SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME;
private ApkUtils() {}
/**
* Finds the main ZIP sections of the provided APK.
*
* @throws IOException if an I/O error occurred while reading the APK
* @throws ZipFormatException if the APK is malformed
*/
public static ZipSections findZipSections(DataSource apk)
throws IOException, ZipFormatException {
com.android.apksig.zip.ZipSections zipSections = ApkUtilsLite.findZipSections(apk);
return new ZipSections(
zipSections.getZipCentralDirectoryOffset(),
zipSections.getZipCentralDirectorySizeBytes(),
zipSections.getZipCentralDirectoryRecordCount(),
zipSections.getZipEndOfCentralDirectoryOffset(),
zipSections.getZipEndOfCentralDirectory());
}
/**
* Information about the ZIP sections of an APK.
*/
public static class ZipSections extends com.android.apksig.zip.ZipSections {
public ZipSections(
long centralDirectoryOffset,
long centralDirectorySizeBytes,
int centralDirectoryRecordCount,
long eocdOffset,
ByteBuffer eocd) {
super(centralDirectoryOffset, centralDirectorySizeBytes, centralDirectoryRecordCount,
eocdOffset, eocd);
}
}
/**
* Sets the offset of the start of the ZIP Central Directory in the APK's ZIP End of Central
* Directory record.
*
* @param zipEndOfCentralDirectory APK's ZIP End of Central Directory record
* @param offset offset of the ZIP Central Directory relative to the start of the archive. Must
* be between {@code 0} and {@code 2^32 - 1} inclusive.
*/
public static void setZipEocdCentralDirectoryOffset(
ByteBuffer zipEndOfCentralDirectory, long offset) {
ByteBuffer eocd = zipEndOfCentralDirectory.slice();
eocd.order(ByteOrder.LITTLE_ENDIAN);
ZipUtils.setZipEocdCentralDirectoryOffset(eocd, offset);
}
/**
* Updates the length of EOCD comment.
*
* @param zipEndOfCentralDirectory APK's ZIP End of Central Directory record
*/
public static void updateZipEocdCommentLen(ByteBuffer zipEndOfCentralDirectory) {
ByteBuffer eocd = zipEndOfCentralDirectory.slice();
eocd.order(ByteOrder.LITTLE_ENDIAN);
ZipUtils.updateZipEocdCommentLen(eocd);
}
/**
* Returns the APK Signing Block of the provided {@code apk}.
*
* @throws ApkFormatException if the APK is not a valid ZIP archive
* @throws IOException if an I/O error occurs
* @throws ApkSigningBlockNotFoundException if there is no APK Signing Block in the APK
*
* @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature Scheme v2
* </a>
*/
public static ApkSigningBlock findApkSigningBlock(DataSource apk)
throws ApkFormatException, IOException, ApkSigningBlockNotFoundException {
ApkUtils.ZipSections inputZipSections;
try {
inputZipSections = ApkUtils.findZipSections(apk);
} catch (ZipFormatException e) {
throw new ApkFormatException("Malformed APK: not a ZIP archive", e);
}
return findApkSigningBlock(apk, inputZipSections);
}
/**
* Returns the APK Signing Block of the provided APK.
*
* @throws IOException if an I/O error occurs
* @throws ApkSigningBlockNotFoundException if there is no APK Signing Block in the APK
*
* @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature Scheme v2
* </a>
*/
public static ApkSigningBlock findApkSigningBlock(DataSource apk, ZipSections zipSections)
throws IOException, ApkSigningBlockNotFoundException {
ApkUtilsLite.ApkSigningBlock apkSigningBlock = ApkUtilsLite.findApkSigningBlock(apk,
zipSections);
return new ApkSigningBlock(apkSigningBlock.getStartOffset(), apkSigningBlock.getContents());
}
/**
* Information about the location of the APK Signing Block inside an APK.
*/
public static class ApkSigningBlock extends ApkUtilsLite.ApkSigningBlock {
/**
* Constructs a new {@code ApkSigningBlock}.
*
* @param startOffsetInApk start offset (in bytes, relative to start of file) of the APK
* Signing Block inside the APK file
* @param contents contents of the APK Signing Block
*/
public ApkSigningBlock(long startOffsetInApk, DataSource contents) {
super(startOffsetInApk, contents);
}
}
/**
* Returns the contents of the APK's {@code AndroidManifest.xml}.
*
* @throws IOException if an I/O error occurs while reading the APK
* @throws ApkFormatException if the APK is malformed
*/
public static ByteBuffer getAndroidManifest(DataSource apk)
throws IOException, ApkFormatException {
ZipSections zipSections;
try {
zipSections = findZipSections(apk);
} catch (ZipFormatException e) {
throw new ApkFormatException("Not a valid ZIP archive", e);
}
List<CentralDirectoryRecord> cdRecords =
V1SchemeVerifier.parseZipCentralDirectory(apk, zipSections);
CentralDirectoryRecord androidManifestCdRecord = null;
for (CentralDirectoryRecord cdRecord : cdRecords) {
if (ANDROID_MANIFEST_ZIP_ENTRY_NAME.equals(cdRecord.getName())) {
androidManifestCdRecord = cdRecord;
break;
}
}
if (androidManifestCdRecord == null) {
throw new ApkFormatException("Missing " + ANDROID_MANIFEST_ZIP_ENTRY_NAME);
}
DataSource lfhSection = apk.slice(0, zipSections.getZipCentralDirectoryOffset());
try {
return ByteBuffer.wrap(
LocalFileRecord.getUncompressedData(
lfhSection, androidManifestCdRecord, lfhSection.size()));
} catch (ZipFormatException e) {
throw new ApkFormatException("Failed to read " + ANDROID_MANIFEST_ZIP_ENTRY_NAME, e);
}
}
/**
* Android resource ID of the {@code android:minSdkVersion} attribute in AndroidManifest.xml.
*/
private static final int MIN_SDK_VERSION_ATTR_ID = 0x0101020c;
/**
* Android resource ID of the {@code android:debuggable} attribute in AndroidManifest.xml.
*/
private static final int DEBUGGABLE_ATTR_ID = 0x0101000f;
/**
* Android resource ID of the {@code android:targetSandboxVersion} attribute in
* AndroidManifest.xml.
*/
private static final int TARGET_SANDBOX_VERSION_ATTR_ID = 0x0101054c;
/**
* Android resource ID of the {@code android:targetSdkVersion} attribute in
* AndroidManifest.xml.
*/
private static final int TARGET_SDK_VERSION_ATTR_ID = 0x01010270;
private static final String USES_SDK_ELEMENT_TAG = "uses-sdk";
/**
* Android resource ID of the {@code android:versionCode} attribute in AndroidManifest.xml.
*/
private static final int VERSION_CODE_ATTR_ID = 0x0101021b;
private static final String MANIFEST_ELEMENT_TAG = "manifest";
/**
* Android resource ID of the {@code android:versionCodeMajor} attribute in AndroidManifest.xml.
*/
private static final int VERSION_CODE_MAJOR_ATTR_ID = 0x01010576;
/**
* Returns the lowest Android platform version (API Level) supported by an APK with the
* provided {@code AndroidManifest.xml}.
*
* @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android
* resource format
*
* @throws MinSdkVersionException if an error occurred while determining the API Level
*/
public static int getMinSdkVersionFromBinaryAndroidManifest(
ByteBuffer androidManifestContents) throws MinSdkVersionException {
// IMPLEMENTATION NOTE: Minimum supported Android platform version number is declared using
// uses-sdk elements which are children of the top-level manifest element. uses-sdk element
// declares the minimum supported platform version using the android:minSdkVersion attribute
// whose default value is 1.
// For each encountered uses-sdk element, the Android runtime checks that its minSdkVersion
// is not higher than the runtime's API Level and rejects APKs if it is higher. Thus, the
// effective minSdkVersion value is the maximum over the encountered minSdkVersion values.
try {
// If no uses-sdk elements are encountered, Android accepts the APK. We treat this
// scenario as though the minimum supported API Level is 1.
int result = 1;
AndroidBinXmlParser parser = new AndroidBinXmlParser(androidManifestContents);
int eventType = parser.getEventType();
while (eventType != AndroidBinXmlParser.EVENT_END_DOCUMENT) {
if ((eventType == AndroidBinXmlParser.EVENT_START_ELEMENT)
&& (parser.getDepth() == 2)
&& ("uses-sdk".equals(parser.getName()))
&& (parser.getNamespace().isEmpty())) {
// In each uses-sdk element, minSdkVersion defaults to 1
int minSdkVersion = 1;
for (int i = 0; i < parser.getAttributeCount(); i++) {
if (parser.getAttributeNameResourceId(i) == MIN_SDK_VERSION_ATTR_ID) {
int valueType = parser.getAttributeValueType(i);
switch (valueType) {
case AndroidBinXmlParser.VALUE_TYPE_INT:
minSdkVersion = parser.getAttributeIntValue(i);
break;
case AndroidBinXmlParser.VALUE_TYPE_STRING:
minSdkVersion =
getMinSdkVersionForCodename(
parser.getAttributeStringValue(i));
break;
default:
throw new MinSdkVersionException(
"Unable to determine APK's minimum supported Android"
+ ": unsupported value type in "
+ ANDROID_MANIFEST_ZIP_ENTRY_NAME + "'s"
+ " minSdkVersion"
+ ". Only integer values supported.");
}
break;
}
}
result = Math.max(result, minSdkVersion);
}
eventType = parser.next();
}
return result;
} catch (AndroidBinXmlParser.XmlParserException e) {
throw new MinSdkVersionException(
"Unable to determine APK's minimum supported Android platform version"
+ ": malformed binary resource: " + ANDROID_MANIFEST_ZIP_ENTRY_NAME,
e);
}
}
private static class CodenamesLazyInitializer {
/**
* List of platform codename (first letter of) to API Level mappings. The list must be
* sorted by the first letter. For codenames not in the list, the assumption is that the API
* Level is incremented by one for every increase in the codename's first letter.
*/
@SuppressWarnings({"rawtypes", "unchecked"})
private static final Pair<Character, Integer>[] SORTED_CODENAMES_FIRST_CHAR_TO_API_LEVEL =
new Pair[] {
Pair.of('C', 2),
Pair.of('D', 3),
Pair.of('E', 4),
Pair.of('F', 7),
Pair.of('G', 8),
Pair.of('H', 10),
Pair.of('I', 13),
Pair.of('J', 15),
Pair.of('K', 18),
Pair.of('L', 20),
Pair.of('M', 22),
Pair.of('N', 23),
Pair.of('O', 25),
};
private static final Comparator<Pair<Character, Integer>> CODENAME_FIRST_CHAR_COMPARATOR =
new ByFirstComparator();
private static class ByFirstComparator implements Comparator<Pair<Character, Integer>> {
@Override
public int compare(Pair<Character, Integer> o1, Pair<Character, Integer> o2) {
char c1 = o1.getFirst();
char c2 = o2.getFirst();
return c1 - c2;
}
}
}
/**
* Returns the API Level corresponding to the provided platform codename.
*
* <p>This method is pessimistic. It returns a value one lower than the API Level with which the
* platform is actually released (e.g., 23 for N which was released as API Level 24). This is
* because new features which first appear in an API Level are not available in the early days
* of that platform version's existence, when the platform only has a codename. Moreover, this
* method currently doesn't differentiate between initial and MR releases, meaning API Level
* returned for MR releases may be more than one lower than the API Level with which the
* platform version is actually released.
*
* @throws CodenameMinSdkVersionException if the {@code codename} is not supported
*/
static int getMinSdkVersionForCodename(String codename) throws CodenameMinSdkVersionException {
char firstChar = codename.isEmpty() ? ' ' : codename.charAt(0);
// Codenames are case-sensitive. Only codenames starting with A-Z are supported for now.
// We only look at the first letter of the codename as this is the most important letter.
if ((firstChar >= 'A') && (firstChar <= 'Z')) {
Pair<Character, Integer>[] sortedCodenamesFirstCharToApiLevel =
CodenamesLazyInitializer.SORTED_CODENAMES_FIRST_CHAR_TO_API_LEVEL;
int searchResult =
Arrays.binarySearch(
sortedCodenamesFirstCharToApiLevel,
Pair.of(firstChar, null), // second element of the pair is ignored here
CodenamesLazyInitializer.CODENAME_FIRST_CHAR_COMPARATOR);
if (searchResult >= 0) {
// Exact match -- searchResult is the index of the matching element
return sortedCodenamesFirstCharToApiLevel[searchResult].getSecond();
}
// Not an exact match -- searchResult is negative and is -(insertion index) - 1.
// The element at insertionIndex - 1 (if present) is smaller than firstChar and the
// element at insertionIndex (if present) is greater than firstChar.
int insertionIndex = -1 - searchResult; // insertionIndex is in [0; array length]
if (insertionIndex == 0) {
// 'A' or 'B' -- never released to public
return 1;
} else {
// The element at insertionIndex - 1 is the newest older codename.
// API Level bumped by at least 1 for every change in the first letter of codename
Pair<Character, Integer> newestOlderCodenameMapping =
sortedCodenamesFirstCharToApiLevel[insertionIndex - 1];
char newestOlderCodenameFirstChar = newestOlderCodenameMapping.getFirst();
int newestOlderCodenameApiLevel = newestOlderCodenameMapping.getSecond();
return newestOlderCodenameApiLevel + (firstChar - newestOlderCodenameFirstChar);
}
}
throw new CodenameMinSdkVersionException(
"Unable to determine APK's minimum supported Android platform version"
+ " : Unsupported codename in " + ANDROID_MANIFEST_ZIP_ENTRY_NAME
+ "'s minSdkVersion: \"" + codename + "\"",
codename);
}
/**
* Returns {@code true} if the APK is debuggable according to its {@code AndroidManifest.xml}.
* See the {@code android:debuggable} attribute of the {@code application} element.
*
* @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android
* resource format
*
* @throws ApkFormatException if the manifest is malformed
*/
public static boolean getDebuggableFromBinaryAndroidManifest(
ByteBuffer androidManifestContents) throws ApkFormatException {
// IMPLEMENTATION NOTE: Whether the package is debuggable is declared using the first
// "application" element which is a child of the top-level manifest element. The debuggable
// attribute of this application element is coerced to a boolean value. If there is no
// application element or if it doesn't declare the debuggable attribute, the package is
// considered not debuggable.
try {
AndroidBinXmlParser parser = new AndroidBinXmlParser(androidManifestContents);
int eventType = parser.getEventType();
while (eventType != AndroidBinXmlParser.EVENT_END_DOCUMENT) {
if ((eventType == AndroidBinXmlParser.EVENT_START_ELEMENT)
&& (parser.getDepth() == 2)
&& ("application".equals(parser.getName()))
&& (parser.getNamespace().isEmpty())) {
for (int i = 0; i < parser.getAttributeCount(); i++) {
if (parser.getAttributeNameResourceId(i) == DEBUGGABLE_ATTR_ID) {
int valueType = parser.getAttributeValueType(i);
switch (valueType) {
case AndroidBinXmlParser.VALUE_TYPE_BOOLEAN:
case AndroidBinXmlParser.VALUE_TYPE_STRING:
case AndroidBinXmlParser.VALUE_TYPE_INT:
String value = parser.getAttributeStringValue(i);
return ("true".equals(value))
|| ("TRUE".equals(value))
|| ("1".equals(value));
case AndroidBinXmlParser.VALUE_TYPE_REFERENCE:
// References to resources are not supported on purpose. The
// reason is that the resolved value depends on the resource
// configuration (e.g, MNC/MCC, locale, screen density) used
// at resolution time. As a result, the same APK may appear as
// debuggable in one situation and as non-debuggable in another
// situation. Such APKs may put users at risk.
throw new ApkFormatException(
"Unable to determine whether APK is debuggable"
+ ": " + ANDROID_MANIFEST_ZIP_ENTRY_NAME + "'s"
+ " android:debuggable attribute references a"
+ " resource. References are not supported for"
+ " security reasons. Only constant boolean,"
+ " string and int values are supported.");
default:
throw new ApkFormatException(
"Unable to determine whether APK is debuggable"
+ ": " + ANDROID_MANIFEST_ZIP_ENTRY_NAME + "'s"
+ " android:debuggable attribute uses"
+ " unsupported value type. Only boolean,"
+ " string and int values are supported.");
}
}
}
// This application element does not declare the debuggable attribute
return false;
}
eventType = parser.next();
}
// No application element found
return false;
} catch (AndroidBinXmlParser.XmlParserException e) {
throw new ApkFormatException(
"Unable to determine whether APK is debuggable: malformed binary resource: "
+ ANDROID_MANIFEST_ZIP_ENTRY_NAME,
e);
}
}
/**
* Returns the package name of the APK according to its {@code AndroidManifest.xml} or
* {@code null} if package name is not declared. See the {@code package} attribute of the
* {@code manifest} element.
*
* @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android
* resource format
*
* @throws ApkFormatException if the manifest is malformed
*/
public static String getPackageNameFromBinaryAndroidManifest(
ByteBuffer androidManifestContents) throws ApkFormatException {
// IMPLEMENTATION NOTE: Package name is declared as the "package" attribute of the top-level
// manifest element. Interestingly, as opposed to most other attributes, Android Package
// Manager looks up this attribute by its name rather than by its resource ID.
try {
AndroidBinXmlParser parser = new AndroidBinXmlParser(androidManifestContents);
int eventType = parser.getEventType();
while (eventType != AndroidBinXmlParser.EVENT_END_DOCUMENT) {
if ((eventType == AndroidBinXmlParser.EVENT_START_ELEMENT)
&& (parser.getDepth() == 1)
&& ("manifest".equals(parser.getName()))
&& (parser.getNamespace().isEmpty())) {
for (int i = 0; i < parser.getAttributeCount(); i++) {
if ("package".equals(parser.getAttributeName(i))
&& (parser.getNamespace().isEmpty())) {
return parser.getAttributeStringValue(i);
}
}
// No "package" attribute found
return null;
}
eventType = parser.next();
}
// No manifest element found
return null;
} catch (AndroidBinXmlParser.XmlParserException e) {
throw new ApkFormatException(
"Unable to determine APK package name: malformed binary resource: "
+ ANDROID_MANIFEST_ZIP_ENTRY_NAME,
e);
}
}
/**
* Returns the security sandbox version targeted by an APK with the provided
* {@code AndroidManifest.xml}.
*
* <p>If the security sandbox version is not specified in the manifest a default value of 1 is
* returned.
*
* @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android
* resource format
*/
public static int getTargetSandboxVersionFromBinaryAndroidManifest(
ByteBuffer androidManifestContents) {
try {
return getAttributeValueFromBinaryAndroidManifest(androidManifestContents,
MANIFEST_ELEMENT_TAG, TARGET_SANDBOX_VERSION_ATTR_ID);
} catch (ApkFormatException e) {
// An ApkFormatException indicates the target sandbox is not specified in the manifest;
// return a default value of 1.
return 1;
}
}
/**
* Returns the SDK version targeted by an APK with the provided {@code AndroidManifest.xml}.
*
* <p>If the targetSdkVersion is not specified the minimumSdkVersion is returned. If neither
* value is specified then a value of 1 is returned.
*
* @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android
* resource format
*/
public static int getTargetSdkVersionFromBinaryAndroidManifest(
ByteBuffer androidManifestContents) {
// If the targetSdkVersion is not specified then the platform will use the value of the
// minSdkVersion; if neither is specified then the platform will use a value of 1.
int minSdkVersion = 1;
try {
return getAttributeValueFromBinaryAndroidManifest(androidManifestContents,
USES_SDK_ELEMENT_TAG, TARGET_SDK_VERSION_ATTR_ID);
} catch (ApkFormatException e) {
// Expected if the APK does not contain a targetSdkVersion attribute or the uses-sdk
// element is not specified at all.
}
androidManifestContents.rewind();
try {
minSdkVersion = getMinSdkVersionFromBinaryAndroidManifest(androidManifestContents);
} catch (ApkFormatException e) {
// Similar to above, expected if the APK does not contain a minSdkVersion attribute, or
// the uses-sdk element is not specified at all.
}
return minSdkVersion;
}
/**
* Returns the versionCode of the APK according to its {@code AndroidManifest.xml}.
*
* <p>If the versionCode is not specified in the {@code AndroidManifest.xml} or is not a valid
* integer an ApkFormatException is thrown.
*
* @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android
* resource format
* @throws ApkFormatException if an error occurred while determining the versionCode, or if the
* versionCode attribute value is not available.
*/
public static int getVersionCodeFromBinaryAndroidManifest(ByteBuffer androidManifestContents)
throws ApkFormatException {
return getAttributeValueFromBinaryAndroidManifest(androidManifestContents,
MANIFEST_ELEMENT_TAG, VERSION_CODE_ATTR_ID);
}
/**
* Returns the versionCode and versionCodeMajor of the APK according to its {@code
* AndroidManifest.xml} combined together as a single long value.
*
* <p>The versionCodeMajor is placed in the upper 32 bits, and the versionCode is in the lower
* 32 bits. If the versionCodeMajor is not specified then the versionCode is returned.
*
* @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android
* resource format
* @throws ApkFormatException if an error occurred while determining the version, or if the
* versionCode attribute value is not available.
*/
public static long getLongVersionCodeFromBinaryAndroidManifest(
ByteBuffer androidManifestContents) throws ApkFormatException {
// If the versionCode is not found then allow the ApkFormatException to be thrown to notify
// the caller that the versionCode is not available.
int versionCode = getVersionCodeFromBinaryAndroidManifest(androidManifestContents);
long versionCodeMajor = 0;
try {
androidManifestContents.rewind();
versionCodeMajor = getAttributeValueFromBinaryAndroidManifest(androidManifestContents,
MANIFEST_ELEMENT_TAG, VERSION_CODE_MAJOR_ATTR_ID);
} catch (ApkFormatException e) {
// This is expected if the versionCodeMajor has not been defined for the APK; in this
// case the return value is just the versionCode.
}
return (versionCodeMajor << 32) | versionCode;
}
/**
* Returns the integer value of the requested {@code attributeId} in the specified {@code
* elementName} from the provided {@code androidManifestContents} in binary Android resource
* format.
*
* @throws ApkFormatException if an error occurred while attempting to obtain the attribute, or
* if the requested attribute is not found.
*/
private static int getAttributeValueFromBinaryAndroidManifest(
ByteBuffer androidManifestContents, String elementName, int attributeId)
throws ApkFormatException {
if (elementName == null) {
throw new NullPointerException("elementName cannot be null");
}
try {
AndroidBinXmlParser parser = new AndroidBinXmlParser(androidManifestContents);
int eventType = parser.getEventType();
while (eventType != AndroidBinXmlParser.EVENT_END_DOCUMENT) {
if ((eventType == AndroidBinXmlParser.EVENT_START_ELEMENT)
&& (elementName.equals(parser.getName()))) {
for (int i = 0; i < parser.getAttributeCount(); i++) {
if (parser.getAttributeNameResourceId(i) == attributeId) {
int valueType = parser.getAttributeValueType(i);
switch (valueType) {
case AndroidBinXmlParser.VALUE_TYPE_INT:
case AndroidBinXmlParser.VALUE_TYPE_STRING:
return parser.getAttributeIntValue(i);
default:
throw new ApkFormatException(
"Unsupported value type, " + valueType
+ ", for attribute " + String.format("0x%08X",
attributeId) + " under element " + elementName);
}
}
}
}
eventType = parser.next();
}
throw new ApkFormatException(
"Failed to determine APK's " + elementName + " attribute "
+ String.format("0x%08X", attributeId) + " value");
} catch (AndroidBinXmlParser.XmlParserException e) {
throw new ApkFormatException(
"Unable to determine value for attribute " + String.format("0x%08X",
attributeId) + " under element " + elementName
+ "; malformed binary resource: " + ANDROID_MANIFEST_ZIP_ENTRY_NAME, e);
}
}
public static byte[] computeSha256DigestBytes(byte[] data) {
return ApkUtilsLite.computeSha256DigestBytes(data);
}
}

View File

@ -0,0 +1,199 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.apk;
import com.android.apksig.internal.util.Pair;
import com.android.apksig.internal.zip.ZipUtils;
import com.android.apksig.util.DataSource;
import com.android.apksig.zip.ZipFormatException;
import com.android.apksig.zip.ZipSections;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
/**
* Lightweight version of the ApkUtils for clients that only require a subset of the utility
* functionality.
*/
public class ApkUtilsLite {
private ApkUtilsLite() {}
/**
* Finds the main ZIP sections of the provided APK.
*
* @throws IOException if an I/O error occurred while reading the APK
* @throws ZipFormatException if the APK is malformed
*/
public static ZipSections findZipSections(DataSource apk)
throws IOException, ZipFormatException {
Pair<ByteBuffer, Long> eocdAndOffsetInFile =
ZipUtils.findZipEndOfCentralDirectoryRecord(apk);
if (eocdAndOffsetInFile == null) {
throw new ZipFormatException("ZIP End of Central Directory record not found");
}
ByteBuffer eocdBuf = eocdAndOffsetInFile.getFirst();
long eocdOffset = eocdAndOffsetInFile.getSecond();
eocdBuf.order(ByteOrder.LITTLE_ENDIAN);
long cdStartOffset = ZipUtils.getZipEocdCentralDirectoryOffset(eocdBuf);
if (cdStartOffset > eocdOffset) {
throw new ZipFormatException(
"ZIP Central Directory start offset out of range: " + cdStartOffset
+ ". ZIP End of Central Directory offset: " + eocdOffset);
}
long cdSizeBytes = ZipUtils.getZipEocdCentralDirectorySizeBytes(eocdBuf);
long cdEndOffset = cdStartOffset + cdSizeBytes;
if (cdEndOffset > eocdOffset) {
throw new ZipFormatException(
"ZIP Central Directory overlaps with End of Central Directory"
+ ". CD end: " + cdEndOffset
+ ", EoCD start: " + eocdOffset);
}
int cdRecordCount = ZipUtils.getZipEocdCentralDirectoryTotalRecordCount(eocdBuf);
return new ZipSections(
cdStartOffset,
cdSizeBytes,
cdRecordCount,
eocdOffset,
eocdBuf);
}
// See https://source.android.com/security/apksigning/v2.html
private static final long APK_SIG_BLOCK_MAGIC_HI = 0x3234206b636f6c42L;
private static final long APK_SIG_BLOCK_MAGIC_LO = 0x20676953204b5041L;
private static final int APK_SIG_BLOCK_MIN_SIZE = 32;
/**
* Returns the APK Signing Block of the provided APK.
*
* @throws IOException if an I/O error occurs
* @throws ApkSigningBlockNotFoundException if there is no APK Signing Block in the APK
*
* @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature Scheme v2
* </a>
*/
public static ApkSigningBlock findApkSigningBlock(DataSource apk, ZipSections zipSections)
throws IOException, ApkSigningBlockNotFoundException {
// FORMAT (see https://source.android.com/security/apksigning/v2.html):
// OFFSET DATA TYPE DESCRIPTION
// * @+0 bytes uint64: size in bytes (excluding this field)
// * @+8 bytes payload
// * @-24 bytes uint64: size in bytes (same as the one above)
// * @-16 bytes uint128: magic
long centralDirStartOffset = zipSections.getZipCentralDirectoryOffset();
long centralDirEndOffset =
centralDirStartOffset + zipSections.getZipCentralDirectorySizeBytes();
long eocdStartOffset = zipSections.getZipEndOfCentralDirectoryOffset();
if (centralDirEndOffset != eocdStartOffset) {
throw new ApkSigningBlockNotFoundException(
"ZIP Central Directory is not immediately followed by End of Central Directory"
+ ". CD end: " + centralDirEndOffset
+ ", EoCD start: " + eocdStartOffset);
}
if (centralDirStartOffset < APK_SIG_BLOCK_MIN_SIZE) {
throw new ApkSigningBlockNotFoundException(
"APK too small for APK Signing Block. ZIP Central Directory offset: "
+ centralDirStartOffset);
}
// Read the magic and offset in file from the footer section of the block:
// * uint64: size of block
// * 16 bytes: magic
ByteBuffer footer = apk.getByteBuffer(centralDirStartOffset - 24, 24);
footer.order(ByteOrder.LITTLE_ENDIAN);
if ((footer.getLong(8) != APK_SIG_BLOCK_MAGIC_LO)
|| (footer.getLong(16) != APK_SIG_BLOCK_MAGIC_HI)) {
throw new ApkSigningBlockNotFoundException(
"No APK Signing Block before ZIP Central Directory");
}
// Read and compare size fields
long apkSigBlockSizeInFooter = footer.getLong(0);
if ((apkSigBlockSizeInFooter < footer.capacity())
|| (apkSigBlockSizeInFooter > Integer.MAX_VALUE - 8)) {
throw new ApkSigningBlockNotFoundException(
"APK Signing Block size out of range: " + apkSigBlockSizeInFooter);
}
int totalSize = (int) (apkSigBlockSizeInFooter + 8);
long apkSigBlockOffset = centralDirStartOffset - totalSize;
if (apkSigBlockOffset < 0) {
throw new ApkSigningBlockNotFoundException(
"APK Signing Block offset out of range: " + apkSigBlockOffset);
}
ByteBuffer apkSigBlock = apk.getByteBuffer(apkSigBlockOffset, 8);
apkSigBlock.order(ByteOrder.LITTLE_ENDIAN);
long apkSigBlockSizeInHeader = apkSigBlock.getLong(0);
if (apkSigBlockSizeInHeader != apkSigBlockSizeInFooter) {
throw new ApkSigningBlockNotFoundException(
"APK Signing Block sizes in header and footer do not match: "
+ apkSigBlockSizeInHeader + " vs " + apkSigBlockSizeInFooter);
}
return new ApkSigningBlock(apkSigBlockOffset, apk.slice(apkSigBlockOffset, totalSize));
}
/**
* Information about the location of the APK Signing Block inside an APK.
*/
public static class ApkSigningBlock {
private final long mStartOffsetInApk;
private final DataSource mContents;
/**
* Constructs a new {@code ApkSigningBlock}.
*
* @param startOffsetInApk start offset (in bytes, relative to start of file) of the APK
* Signing Block inside the APK file
* @param contents contents of the APK Signing Block
*/
public ApkSigningBlock(long startOffsetInApk, DataSource contents) {
mStartOffsetInApk = startOffsetInApk;
mContents = contents;
}
/**
* Returns the start offset (in bytes, relative to start of file) of the APK Signing Block.
*/
public long getStartOffset() {
return mStartOffsetInApk;
}
/**
* Returns the data source which provides the full contents of the APK Signing Block,
* including its footer.
*/
public DataSource getContents() {
return mContents;
}
}
public static byte[] computeSha256DigestBytes(byte[] data) {
MessageDigest messageDigest;
try {
messageDigest = MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("SHA-256 is not found", e);
}
messageDigest.update(data);
return messageDigest.digest();
}
}

View File

@ -0,0 +1,46 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.apk;
/**
* Indicates that there was an issue determining the minimum Android platform version supported by
* an APK because the version is specified as a codename, rather than as API Level number, and the
* codename is in an unexpected format.
*/
public class CodenameMinSdkVersionException extends MinSdkVersionException {
private static final long serialVersionUID = 1L;
/** Encountered codename. */
private final String mCodename;
/**
* Constructs a new {@code MinSdkVersionCodenameException} with the provided message and
* codename.
*/
public CodenameMinSdkVersionException(String message, String codename) {
super(message);
mCodename = codename;
}
/**
* Returns the codename.
*/
public String getCodename() {
return mCodename;
}
}

View File

@ -0,0 +1,40 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.apk;
/**
* Indicates that there was an issue determining the minimum Android platform version supported by
* an APK.
*/
public class MinSdkVersionException extends ApkFormatException {
private static final long serialVersionUID = 1L;
/**
* Constructs a new {@code MinSdkVersionException} with the provided message.
*/
public MinSdkVersionException(String message) {
super(message);
}
/**
* Constructs a new {@code MinSdkVersionException} with the provided message and cause.
*/
public MinSdkVersionException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -0,0 +1,869 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.apk;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* XML pull style parser of Android binary XML resources, such as {@code AndroidManifest.xml}.
*
* <p>For an input document, the parser outputs an event stream (see {@code EVENT_... constants} via
* {@link #getEventType()} and {@link #next()} methods. Additional information about the current
* event can be obtained via an assortment of getters, for example, {@link #getName()} or
* {@link #getAttributeNameResourceId(int)}.
*/
public class AndroidBinXmlParser {
/** Event: start of document. */
public static final int EVENT_START_DOCUMENT = 1;
/** Event: end of document. */
public static final int EVENT_END_DOCUMENT = 2;
/** Event: start of an element. */
public static final int EVENT_START_ELEMENT = 3;
/** Event: end of an document. */
public static final int EVENT_END_ELEMENT = 4;
/** Attribute value type is not supported by this parser. */
public static final int VALUE_TYPE_UNSUPPORTED = 0;
/** Attribute value is a string. Use {@link #getAttributeStringValue(int)} to obtain it. */
public static final int VALUE_TYPE_STRING = 1;
/** Attribute value is an integer. Use {@link #getAttributeIntValue(int)} to obtain it. */
public static final int VALUE_TYPE_INT = 2;
/**
* Attribute value is a resource reference. Use {@link #getAttributeIntValue(int)} to obtain it.
*/
public static final int VALUE_TYPE_REFERENCE = 3;
/** Attribute value is a boolean. Use {@link #getAttributeBooleanValue(int)} to obtain it. */
public static final int VALUE_TYPE_BOOLEAN = 4;
private static final long NO_NAMESPACE = 0xffffffffL;
private final ByteBuffer mXml;
private StringPool mStringPool;
private ResourceMap mResourceMap;
private int mDepth;
private int mCurrentEvent = EVENT_START_DOCUMENT;
private String mCurrentElementName;
private String mCurrentElementNamespace;
private int mCurrentElementAttributeCount;
private List<Attribute> mCurrentElementAttributes;
private ByteBuffer mCurrentElementAttributesContents;
private int mCurrentElementAttrSizeBytes;
/**
* Constructs a new parser for the provided document.
*/
public AndroidBinXmlParser(ByteBuffer xml) throws XmlParserException {
xml.order(ByteOrder.LITTLE_ENDIAN);
Chunk resXmlChunk = null;
while (xml.hasRemaining()) {
Chunk chunk = Chunk.get(xml);
if (chunk == null) {
break;
}
if (chunk.getType() == Chunk.TYPE_RES_XML) {
resXmlChunk = chunk;
break;
}
}
if (resXmlChunk == null) {
throw new XmlParserException("No XML chunk in file");
}
mXml = resXmlChunk.getContents();
}
/**
* Returns the depth of the current element. Outside of the root of the document the depth is
* {@code 0}. The depth is incremented by {@code 1} before each {@code start element} event and
* is decremented by {@code 1} after each {@code end element} event.
*/
public int getDepth() {
return mDepth;
}
/**
* Returns the type of the current event. See {@code EVENT_...} constants.
*/
public int getEventType() {
return mCurrentEvent;
}
/**
* Returns the local name of the current element or {@code null} if the current event does not
* pertain to an element.
*/
public String getName() {
if ((mCurrentEvent != EVENT_START_ELEMENT) && (mCurrentEvent != EVENT_END_ELEMENT)) {
return null;
}
return mCurrentElementName;
}
/**
* Returns the namespace of the current element or {@code null} if the current event does not
* pertain to an element. Returns an empty string if the element is not associated with a
* namespace.
*/
public String getNamespace() {
if ((mCurrentEvent != EVENT_START_ELEMENT) && (mCurrentEvent != EVENT_END_ELEMENT)) {
return null;
}
return mCurrentElementNamespace;
}
/**
* Returns the number of attributes of the element associated with the current event or
* {@code -1} if no element is associated with the current event.
*/
public int getAttributeCount() {
if (mCurrentEvent != EVENT_START_ELEMENT) {
return -1;
}
return mCurrentElementAttributeCount;
}
/**
* Returns the resource ID corresponding to the name of the specified attribute of the current
* element or {@code 0} if the name is not associated with a resource ID.
*
* @throws IndexOutOfBoundsException if the index is out of range or the current event is not a
* {@code start element} event
* @throws XmlParserException if a parsing error is occurred
*/
public int getAttributeNameResourceId(int index) throws XmlParserException {
return getAttribute(index).getNameResourceId();
}
/**
* Returns the name of the specified attribute of the current element.
*
* @throws IndexOutOfBoundsException if the index is out of range or the current event is not a
* {@code start element} event
* @throws XmlParserException if a parsing error is occurred
*/
public String getAttributeName(int index) throws XmlParserException {
return getAttribute(index).getName();
}
/**
* Returns the name of the specified attribute of the current element or an empty string if
* the attribute is not associated with a namespace.
*
* @throws IndexOutOfBoundsException if the index is out of range or the current event is not a
* {@code start element} event
* @throws XmlParserException if a parsing error is occurred
*/
public String getAttributeNamespace(int index) throws XmlParserException {
return getAttribute(index).getNamespace();
}
/**
* Returns the value type of the specified attribute of the current element. See
* {@code VALUE_TYPE_...} constants.
*
* @throws IndexOutOfBoundsException if the index is out of range or the current event is not a
* {@code start element} event
* @throws XmlParserException if a parsing error is occurred
*/
public int getAttributeValueType(int index) throws XmlParserException {
int type = getAttribute(index).getValueType();
switch (type) {
case Attribute.TYPE_STRING:
return VALUE_TYPE_STRING;
case Attribute.TYPE_INT_DEC:
case Attribute.TYPE_INT_HEX:
return VALUE_TYPE_INT;
case Attribute.TYPE_REFERENCE:
return VALUE_TYPE_REFERENCE;
case Attribute.TYPE_INT_BOOLEAN:
return VALUE_TYPE_BOOLEAN;
default:
return VALUE_TYPE_UNSUPPORTED;
}
}
/**
* Returns the integer value of the specified attribute of the current element. See
* {@code VALUE_TYPE_...} constants.
*
* @throws IndexOutOfBoundsException if the index is out of range or the current event is not a
* {@code start element} event.
* @throws XmlParserException if a parsing error is occurred
*/
public int getAttributeIntValue(int index) throws XmlParserException {
return getAttribute(index).getIntValue();
}
/**
* Returns the boolean value of the specified attribute of the current element. See
* {@code VALUE_TYPE_...} constants.
*
* @throws IndexOutOfBoundsException if the index is out of range or the current event is not a
* {@code start element} event.
* @throws XmlParserException if a parsing error is occurred
*/
public boolean getAttributeBooleanValue(int index) throws XmlParserException {
return getAttribute(index).getBooleanValue();
}
/**
* Returns the string value of the specified attribute of the current element. See
* {@code VALUE_TYPE_...} constants.
*
* @throws IndexOutOfBoundsException if the index is out of range or the current event is not a
* {@code start element} event.
* @throws XmlParserException if a parsing error is occurred
*/
public String getAttributeStringValue(int index) throws XmlParserException {
return getAttribute(index).getStringValue();
}
private Attribute getAttribute(int index) {
if (mCurrentEvent != EVENT_START_ELEMENT) {
throw new IndexOutOfBoundsException("Current event not a START_ELEMENT");
}
if (index < 0) {
throw new IndexOutOfBoundsException("index must be >= 0");
}
if (index >= mCurrentElementAttributeCount) {
throw new IndexOutOfBoundsException(
"index must be <= attr count (" + mCurrentElementAttributeCount + ")");
}
parseCurrentElementAttributesIfNotParsed();
return mCurrentElementAttributes.get(index);
}
/**
* Advances to the next parsing event and returns its type. See {@code EVENT_...} constants.
*/
public int next() throws XmlParserException {
// Decrement depth if the previous event was "end element".
if (mCurrentEvent == EVENT_END_ELEMENT) {
mDepth--;
}
// Read events from document, ignoring events that we don't report to caller. Stop at the
// earliest event which we report to caller.
while (mXml.hasRemaining()) {
Chunk chunk = Chunk.get(mXml);
if (chunk == null) {
break;
}
switch (chunk.getType()) {
case Chunk.TYPE_STRING_POOL:
if (mStringPool != null) {
throw new XmlParserException("Multiple string pools not supported");
}
mStringPool = new StringPool(chunk);
break;
case Chunk.RES_XML_TYPE_START_ELEMENT:
{
if (mStringPool == null) {
throw new XmlParserException(
"Named element encountered before string pool");
}
ByteBuffer contents = chunk.getContents();
if (contents.remaining() < 20) {
throw new XmlParserException(
"Start element chunk too short. Need at least 20 bytes. Available: "
+ contents.remaining() + " bytes");
}
long nsId = getUnsignedInt32(contents);
long nameId = getUnsignedInt32(contents);
int attrStartOffset = getUnsignedInt16(contents);
int attrSizeBytes = getUnsignedInt16(contents);
int attrCount = getUnsignedInt16(contents);
long attrEndOffset = attrStartOffset + ((long) attrCount) * attrSizeBytes;
contents.position(0);
if (attrStartOffset > contents.remaining()) {
throw new XmlParserException(
"Attributes start offset out of bounds: " + attrStartOffset
+ ", max: " + contents.remaining());
}
if (attrEndOffset > contents.remaining()) {
throw new XmlParserException(
"Attributes end offset out of bounds: " + attrEndOffset
+ ", max: " + contents.remaining());
}
mCurrentElementName = mStringPool.getString(nameId);
mCurrentElementNamespace =
(nsId == NO_NAMESPACE) ? "" : mStringPool.getString(nsId);
mCurrentElementAttributeCount = attrCount;
mCurrentElementAttributes = null;
mCurrentElementAttrSizeBytes = attrSizeBytes;
mCurrentElementAttributesContents =
sliceFromTo(contents, attrStartOffset, attrEndOffset);
mDepth++;
mCurrentEvent = EVENT_START_ELEMENT;
return mCurrentEvent;
}
case Chunk.RES_XML_TYPE_END_ELEMENT:
{
if (mStringPool == null) {
throw new XmlParserException(
"Named element encountered before string pool");
}
ByteBuffer contents = chunk.getContents();
if (contents.remaining() < 8) {
throw new XmlParserException(
"End element chunk too short. Need at least 8 bytes. Available: "
+ contents.remaining() + " bytes");
}
long nsId = getUnsignedInt32(contents);
long nameId = getUnsignedInt32(contents);
mCurrentElementName = mStringPool.getString(nameId);
mCurrentElementNamespace =
(nsId == NO_NAMESPACE) ? "" : mStringPool.getString(nsId);
mCurrentEvent = EVENT_END_ELEMENT;
mCurrentElementAttributes = null;
mCurrentElementAttributesContents = null;
return mCurrentEvent;
}
case Chunk.RES_XML_TYPE_RESOURCE_MAP:
if (mResourceMap != null) {
throw new XmlParserException("Multiple resource maps not supported");
}
mResourceMap = new ResourceMap(chunk);
break;
default:
// Unknown chunk type -- ignore
break;
}
}
mCurrentEvent = EVENT_END_DOCUMENT;
return mCurrentEvent;
}
private void parseCurrentElementAttributesIfNotParsed() {
if (mCurrentElementAttributes != null) {
return;
}
mCurrentElementAttributes = new ArrayList<>(mCurrentElementAttributeCount);
for (int i = 0; i < mCurrentElementAttributeCount; i++) {
int startPosition = i * mCurrentElementAttrSizeBytes;
ByteBuffer attr =
sliceFromTo(
mCurrentElementAttributesContents,
startPosition,
startPosition + mCurrentElementAttrSizeBytes);
long nsId = getUnsignedInt32(attr);
long nameId = getUnsignedInt32(attr);
attr.position(attr.position() + 7); // skip ignored fields
int valueType = getUnsignedInt8(attr);
long valueData = getUnsignedInt32(attr);
mCurrentElementAttributes.add(
new Attribute(
nsId,
nameId,
valueType,
(int) valueData,
mStringPool,
mResourceMap));
}
}
private static class Attribute {
private static final int TYPE_REFERENCE = 1;
private static final int TYPE_STRING = 3;
private static final int TYPE_INT_DEC = 0x10;
private static final int TYPE_INT_HEX = 0x11;
private static final int TYPE_INT_BOOLEAN = 0x12;
private final long mNsId;
private final long mNameId;
private final int mValueType;
private final int mValueData;
private final StringPool mStringPool;
private final ResourceMap mResourceMap;
private Attribute(
long nsId,
long nameId,
int valueType,
int valueData,
StringPool stringPool,
ResourceMap resourceMap) {
mNsId = nsId;
mNameId = nameId;
mValueType = valueType;
mValueData = valueData;
mStringPool = stringPool;
mResourceMap = resourceMap;
}
public int getNameResourceId() {
return (mResourceMap != null) ? mResourceMap.getResourceId(mNameId) : 0;
}
public String getName() throws XmlParserException {
return mStringPool.getString(mNameId);
}
public String getNamespace() throws XmlParserException {
return (mNsId != NO_NAMESPACE) ? mStringPool.getString(mNsId) : "";
}
public int getValueType() {
return mValueType;
}
public int getIntValue() throws XmlParserException {
switch (mValueType) {
case TYPE_REFERENCE:
case TYPE_INT_DEC:
case TYPE_INT_HEX:
case TYPE_INT_BOOLEAN:
return mValueData;
default:
throw new XmlParserException("Cannot coerce to int: value type " + mValueType);
}
}
public boolean getBooleanValue() throws XmlParserException {
switch (mValueType) {
case TYPE_INT_BOOLEAN:
return mValueData != 0;
default:
throw new XmlParserException(
"Cannot coerce to boolean: value type " + mValueType);
}
}
public String getStringValue() throws XmlParserException {
switch (mValueType) {
case TYPE_STRING:
return mStringPool.getString(mValueData & 0xffffffffL);
case TYPE_INT_DEC:
return Integer.toString(mValueData);
case TYPE_INT_HEX:
return "0x" + Integer.toHexString(mValueData);
case TYPE_INT_BOOLEAN:
return Boolean.toString(mValueData != 0);
case TYPE_REFERENCE:
return "@" + Integer.toHexString(mValueData);
default:
throw new XmlParserException(
"Cannot coerce to string: value type " + mValueType);
}
}
}
/**
* Chunk of a document. Each chunk is tagged with a type and consists of a header followed by
* contents.
*/
private static class Chunk {
public static final int TYPE_STRING_POOL = 1;
public static final int TYPE_RES_XML = 3;
public static final int RES_XML_TYPE_START_ELEMENT = 0x0102;
public static final int RES_XML_TYPE_END_ELEMENT = 0x0103;
public static final int RES_XML_TYPE_RESOURCE_MAP = 0x0180;
static final int HEADER_MIN_SIZE_BYTES = 8;
private final int mType;
private final ByteBuffer mHeader;
private final ByteBuffer mContents;
public Chunk(int type, ByteBuffer header, ByteBuffer contents) {
mType = type;
mHeader = header;
mContents = contents;
}
public ByteBuffer getContents() {
ByteBuffer result = mContents.slice();
result.order(mContents.order());
return result;
}
public ByteBuffer getHeader() {
ByteBuffer result = mHeader.slice();
result.order(mHeader.order());
return result;
}
public int getType() {
return mType;
}
/**
* Consumes the chunk located at the current position of the input and returns the chunk
* or {@code null} if there is no chunk left in the input.
*
* @throws XmlParserException if the chunk is malformed
*/
public static Chunk get(ByteBuffer input) throws XmlParserException {
if (input.remaining() < HEADER_MIN_SIZE_BYTES) {
// Android ignores the last chunk if its header is too big to fit into the file
input.position(input.limit());
return null;
}
int originalPosition = input.position();
int type = getUnsignedInt16(input);
int headerSize = getUnsignedInt16(input);
long chunkSize = getUnsignedInt32(input);
long chunkRemaining = chunkSize - 8;
if (chunkRemaining > input.remaining()) {
// Android ignores the last chunk if it's too big to fit into the file
input.position(input.limit());
return null;
}
if (headerSize < HEADER_MIN_SIZE_BYTES) {
throw new XmlParserException(
"Malformed chunk: header too short: " + headerSize + " bytes");
} else if (headerSize > chunkSize) {
throw new XmlParserException(
"Malformed chunk: header too long: " + headerSize + " bytes. Chunk size: "
+ chunkSize + " bytes");
}
int contentStartPosition = originalPosition + headerSize;
long chunkEndPosition = originalPosition + chunkSize;
Chunk chunk =
new Chunk(
type,
sliceFromTo(input, originalPosition, contentStartPosition),
sliceFromTo(input, contentStartPosition, chunkEndPosition));
input.position((int) chunkEndPosition);
return chunk;
}
}
/**
* String pool of a document. Strings are referenced by their {@code 0}-based index in the pool.
*/
private static class StringPool {
private static final int FLAG_UTF8 = 1 << 8;
private final ByteBuffer mChunkContents;
private final ByteBuffer mStringsSection;
private final int mStringCount;
private final boolean mUtf8Encoded;
private final Map<Integer, String> mCachedStrings = new HashMap<>();
/**
* Constructs a new string pool from the provided chunk.
*
* @throws XmlParserException if a parsing error occurred
*/
public StringPool(Chunk chunk) throws XmlParserException {
ByteBuffer header = chunk.getHeader();
int headerSizeBytes = header.remaining();
header.position(Chunk.HEADER_MIN_SIZE_BYTES);
if (header.remaining() < 20) {
throw new XmlParserException(
"XML chunk's header too short. Required at least 20 bytes. Available: "
+ header.remaining() + " bytes");
}
long stringCount = getUnsignedInt32(header);
if (stringCount > Integer.MAX_VALUE) {
throw new XmlParserException("Too many strings: " + stringCount);
}
mStringCount = (int) stringCount;
long styleCount = getUnsignedInt32(header);
if (styleCount > Integer.MAX_VALUE) {
throw new XmlParserException("Too many styles: " + styleCount);
}
long flags = getUnsignedInt32(header);
long stringsStartOffset = getUnsignedInt32(header);
long stylesStartOffset = getUnsignedInt32(header);
ByteBuffer contents = chunk.getContents();
if (mStringCount > 0) {
int stringsSectionStartOffsetInContents =
(int) (stringsStartOffset - headerSizeBytes);
int stringsSectionEndOffsetInContents;
if (styleCount > 0) {
// Styles section follows the strings section
if (stylesStartOffset < stringsStartOffset) {
throw new XmlParserException(
"Styles offset (" + stylesStartOffset + ") < strings offset ("
+ stringsStartOffset + ")");
}
stringsSectionEndOffsetInContents = (int) (stylesStartOffset - headerSizeBytes);
} else {
stringsSectionEndOffsetInContents = contents.remaining();
}
mStringsSection =
sliceFromTo(
contents,
stringsSectionStartOffsetInContents,
stringsSectionEndOffsetInContents);
} else {
mStringsSection = ByteBuffer.allocate(0);
}
mUtf8Encoded = (flags & FLAG_UTF8) != 0;
mChunkContents = contents;
}
/**
* Returns the string located at the specified {@code 0}-based index in this pool.
*
* @throws XmlParserException if the string does not exist or cannot be decoded
*/
public String getString(long index) throws XmlParserException {
if (index < 0) {
throw new XmlParserException("Unsuported string index: " + index);
} else if (index >= mStringCount) {
throw new XmlParserException(
"Unsuported string index: " + index + ", max: " + (mStringCount - 1));
}
int idx = (int) index;
String result = mCachedStrings.get(idx);
if (result != null) {
return result;
}
long offsetInStringsSection = getUnsignedInt32(mChunkContents, idx * 4);
if (offsetInStringsSection >= mStringsSection.capacity()) {
throw new XmlParserException(
"Offset of string idx " + idx + " out of bounds: " + offsetInStringsSection
+ ", max: " + (mStringsSection.capacity() - 1));
}
mStringsSection.position((int) offsetInStringsSection);
result =
(mUtf8Encoded)
? getLengthPrefixedUtf8EncodedString(mStringsSection)
: getLengthPrefixedUtf16EncodedString(mStringsSection);
mCachedStrings.put(idx, result);
return result;
}
private static String getLengthPrefixedUtf16EncodedString(ByteBuffer encoded)
throws XmlParserException {
// If the length (in uint16s) is 0x7fff or lower, it is stored as a single uint16.
// Otherwise, it is stored as a big-endian uint32 with highest bit set. Thus, the range
// of supported values is 0 to 0x7fffffff inclusive.
int lengthChars = getUnsignedInt16(encoded);
if ((lengthChars & 0x8000) != 0) {
lengthChars = ((lengthChars & 0x7fff) << 16) | getUnsignedInt16(encoded);
}
if (lengthChars > Integer.MAX_VALUE / 2) {
throw new XmlParserException("String too long: " + lengthChars + " uint16s");
}
int lengthBytes = lengthChars * 2;
byte[] arr;
int arrOffset;
if (encoded.hasArray()) {
arr = encoded.array();
arrOffset = encoded.arrayOffset() + encoded.position();
encoded.position(encoded.position() + lengthBytes);
} else {
arr = new byte[lengthBytes];
arrOffset = 0;
encoded.get(arr);
}
// Reproduce the behavior of Android runtime which requires that the UTF-16 encoded
// array of bytes is NULL terminated.
if ((arr[arrOffset + lengthBytes] != 0)
|| (arr[arrOffset + lengthBytes + 1] != 0)) {
throw new XmlParserException("UTF-16 encoded form of string not NULL terminated");
}
try {
return new String(arr, arrOffset, lengthBytes, "UTF-16LE");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("UTF-16LE character encoding not supported", e);
}
}
private static String getLengthPrefixedUtf8EncodedString(ByteBuffer encoded)
throws XmlParserException {
// If the length (in bytes) is 0x7f or lower, it is stored as a single uint8. Otherwise,
// it is stored as a big-endian uint16 with highest bit set. Thus, the range of
// supported values is 0 to 0x7fff inclusive.
// Skip UTF-16 encoded length (in uint16s)
int lengthBytes = getUnsignedInt8(encoded);
if ((lengthBytes & 0x80) != 0) {
lengthBytes = ((lengthBytes & 0x7f) << 8) | getUnsignedInt8(encoded);
}
// Read UTF-8 encoded length (in bytes)
lengthBytes = getUnsignedInt8(encoded);
if ((lengthBytes & 0x80) != 0) {
lengthBytes = ((lengthBytes & 0x7f) << 8) | getUnsignedInt8(encoded);
}
byte[] arr;
int arrOffset;
if (encoded.hasArray()) {
arr = encoded.array();
arrOffset = encoded.arrayOffset() + encoded.position();
encoded.position(encoded.position() + lengthBytes);
} else {
arr = new byte[lengthBytes];
arrOffset = 0;
encoded.get(arr);
}
// Reproduce the behavior of Android runtime which requires that the UTF-8 encoded array
// of bytes is NULL terminated.
if (arr[arrOffset + lengthBytes] != 0) {
throw new XmlParserException("UTF-8 encoded form of string not NULL terminated");
}
try {
return new String(arr, arrOffset, lengthBytes, "UTF-8");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("UTF-8 character encoding not supported", e);
}
}
}
/**
* Resource map of a document. Resource IDs are referenced by their {@code 0}-based index in the
* map.
*/
private static class ResourceMap {
private final ByteBuffer mChunkContents;
private final int mEntryCount;
/**
* Constructs a new resource map from the provided chunk.
*
* @throws XmlParserException if a parsing error occurred
*/
public ResourceMap(Chunk chunk) throws XmlParserException {
mChunkContents = chunk.getContents().slice();
mChunkContents.order(chunk.getContents().order());
// Each entry of the map is four bytes long, containing the int32 resource ID.
mEntryCount = mChunkContents.remaining() / 4;
}
/**
* Returns the resource ID located at the specified {@code 0}-based index in this pool or
* {@code 0} if the index is out of range.
*/
public int getResourceId(long index) {
if ((index < 0) || (index >= mEntryCount)) {
return 0;
}
int idx = (int) index;
// Each entry of the map is four bytes long, containing the int32 resource ID.
return mChunkContents.getInt(idx * 4);
}
}
/**
* Returns new byte buffer whose content is a shared subsequence of this buffer's content
* between the specified start (inclusive) and end (exclusive) positions. As opposed to
* {@link ByteBuffer#slice()}, the returned buffer's byte order is the same as the source
* buffer's byte order.
*/
private static ByteBuffer sliceFromTo(ByteBuffer source, long start, long end) {
if (start < 0) {
throw new IllegalArgumentException("start: " + start);
}
if (end < start) {
throw new IllegalArgumentException("end < start: " + end + " < " + start);
}
int capacity = source.capacity();
if (end > source.capacity()) {
throw new IllegalArgumentException("end > capacity: " + end + " > " + capacity);
}
return sliceFromTo(source, (int) start, (int) end);
}
/**
* Returns new byte buffer whose content is a shared subsequence of this buffer's content
* between the specified start (inclusive) and end (exclusive) positions. As opposed to
* {@link ByteBuffer#slice()}, the returned buffer's byte order is the same as the source
* buffer's byte order.
*/
private static ByteBuffer sliceFromTo(ByteBuffer source, int start, int end) {
if (start < 0) {
throw new IllegalArgumentException("start: " + start);
}
if (end < start) {
throw new IllegalArgumentException("end < start: " + end + " < " + start);
}
int capacity = source.capacity();
if (end > source.capacity()) {
throw new IllegalArgumentException("end > capacity: " + end + " > " + capacity);
}
int originalLimit = source.limit();
int originalPosition = source.position();
try {
source.position(0);
source.limit(end);
source.position(start);
ByteBuffer result = source.slice();
result.order(source.order());
return result;
} finally {
source.position(0);
source.limit(originalLimit);
source.position(originalPosition);
}
}
private static int getUnsignedInt8(ByteBuffer buffer) {
return buffer.get() & 0xff;
}
private static int getUnsignedInt16(ByteBuffer buffer) {
return buffer.getShort() & 0xffff;
}
private static long getUnsignedInt32(ByteBuffer buffer) {
return buffer.getInt() & 0xffffffffL;
}
private static long getUnsignedInt32(ByteBuffer buffer, int position) {
return buffer.getInt(position) & 0xffffffffL;
}
/**
* Indicates that an error occurred while parsing a document.
*/
public static class XmlParserException extends Exception {
private static final long serialVersionUID = 1L;
public XmlParserException(String message) {
super(message);
}
public XmlParserException(String message, Throwable cause) {
super(message, cause);
}
}
}

View File

@ -0,0 +1,104 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.apk;
import com.android.apksig.ApkVerificationIssue;
import java.util.ArrayList;
import java.util.List;
/**
* Base implementation of an APK signature verification result.
*/
public class ApkSigResult {
public final int signatureSchemeVersion;
/** Whether the APK's Signature Scheme signature verifies. */
public boolean verified;
public final List<ApkSignerInfo> mSigners = new ArrayList<>();
private final List<ApkVerificationIssue> mWarnings = new ArrayList<>();
private final List<ApkVerificationIssue> mErrors = new ArrayList<>();
public ApkSigResult(int signatureSchemeVersion) {
this.signatureSchemeVersion = signatureSchemeVersion;
}
/**
* Returns {@code true} if this result encountered errors during verification.
*/
public boolean containsErrors() {
if (!mErrors.isEmpty()) {
return true;
}
if (!mSigners.isEmpty()) {
for (ApkSignerInfo signer : mSigners) {
if (signer.containsErrors()) {
return true;
}
}
}
return false;
}
/**
* Returns {@code true} if this result encountered warnings during verification.
*/
public boolean containsWarnings() {
if (!mWarnings.isEmpty()) {
return true;
}
if (!mSigners.isEmpty()) {
for (ApkSignerInfo signer : mSigners) {
if (signer.containsWarnings()) {
return true;
}
}
}
return false;
}
/**
* Adds a new {@link ApkVerificationIssue} as an error to this result using the provided {@code
* issueId} and {@code params}.
*/
public void addError(int issueId, Object... parameters) {
mErrors.add(new ApkVerificationIssue(issueId, parameters));
}
/**
* Adds a new {@link ApkVerificationIssue} as a warning to this result using the provided {@code
* issueId} and {@code params}.
*/
public void addWarning(int issueId, Object... parameters) {
mWarnings.add(new ApkVerificationIssue(issueId, parameters));
}
/**
* Returns the errors encountered during verification.
*/
public List<? extends ApkVerificationIssue> getErrors() {
return mErrors;
}
/**
* Returns the warnings encountered during verification.
*/
public List<? extends ApkVerificationIssue> getWarnings() {
return mWarnings;
}
}

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,393 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.apk;
import com.android.apksig.apk.ApkFormatException;
import com.android.apksig.apk.ApkSigningBlockNotFoundException;
import com.android.apksig.apk.ApkUtilsLite;
import com.android.apksig.internal.util.Pair;
import com.android.apksig.util.DataSource;
import com.android.apksig.zip.ZipSections;
import java.io.IOException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Lightweight version of the ApkSigningBlockUtils for clients that only require a subset of the
* utility functionality.
*/
public class ApkSigningBlockUtilsLite {
private ApkSigningBlockUtilsLite() {}
private static final char[] HEX_DIGITS = "0123456789abcdef".toCharArray();
/**
* Returns the APK Signature Scheme block contained in the provided APK file for the given ID
* and the additional information relevant for verifying the block against the file.
*
* @param blockId the ID value in the APK Signing Block's sequence of ID-value pairs
* identifying the appropriate block to find, e.g. the APK Signature Scheme v2
* block ID.
*
* @throws SignatureNotFoundException if the APK is not signed using given APK Signature Scheme
* @throws IOException if an I/O error occurs while reading the APK
*/
public static SignatureInfo findSignature(
DataSource apk, ZipSections zipSections, int blockId)
throws IOException, SignatureNotFoundException {
// Find the APK Signing Block.
DataSource apkSigningBlock;
long apkSigningBlockOffset;
try {
ApkUtilsLite.ApkSigningBlock apkSigningBlockInfo =
ApkUtilsLite.findApkSigningBlock(apk, zipSections);
apkSigningBlockOffset = apkSigningBlockInfo.getStartOffset();
apkSigningBlock = apkSigningBlockInfo.getContents();
} catch (ApkSigningBlockNotFoundException e) {
throw new SignatureNotFoundException(e.getMessage(), e);
}
ByteBuffer apkSigningBlockBuf =
apkSigningBlock.getByteBuffer(0, (int) apkSigningBlock.size());
apkSigningBlockBuf.order(ByteOrder.LITTLE_ENDIAN);
// Find the APK Signature Scheme Block inside the APK Signing Block.
ByteBuffer apkSignatureSchemeBlock =
findApkSignatureSchemeBlock(apkSigningBlockBuf, blockId);
return new SignatureInfo(
apkSignatureSchemeBlock,
apkSigningBlockOffset,
zipSections.getZipCentralDirectoryOffset(),
zipSections.getZipEndOfCentralDirectoryOffset(),
zipSections.getZipEndOfCentralDirectory());
}
public static ByteBuffer findApkSignatureSchemeBlock(
ByteBuffer apkSigningBlock,
int blockId) throws SignatureNotFoundException {
checkByteOrderLittleEndian(apkSigningBlock);
// FORMAT:
// OFFSET DATA TYPE DESCRIPTION
// * @+0 bytes uint64: size in bytes (excluding this field)
// * @+8 bytes pairs
// * @-24 bytes uint64: size in bytes (same as the one above)
// * @-16 bytes uint128: magic
ByteBuffer pairs = sliceFromTo(apkSigningBlock, 8, apkSigningBlock.capacity() - 24);
int entryCount = 0;
while (pairs.hasRemaining()) {
entryCount++;
if (pairs.remaining() < 8) {
throw new SignatureNotFoundException(
"Insufficient data to read size of APK Signing Block entry #" + entryCount);
}
long lenLong = pairs.getLong();
if ((lenLong < 4) || (lenLong > Integer.MAX_VALUE)) {
throw new SignatureNotFoundException(
"APK Signing Block entry #" + entryCount
+ " size out of range: " + lenLong);
}
int len = (int) lenLong;
int nextEntryPos = pairs.position() + len;
if (len > pairs.remaining()) {
throw new SignatureNotFoundException(
"APK Signing Block entry #" + entryCount + " size out of range: " + len
+ ", available: " + pairs.remaining());
}
int id = pairs.getInt();
if (id == blockId) {
return getByteBuffer(pairs, len - 4);
}
pairs.position(nextEntryPos);
}
throw new SignatureNotFoundException(
"No APK Signature Scheme block in APK Signing Block with ID: " + blockId);
}
public static void checkByteOrderLittleEndian(ByteBuffer buffer) {
if (buffer.order() != ByteOrder.LITTLE_ENDIAN) {
throw new IllegalArgumentException("ByteBuffer byte order must be little endian");
}
}
/**
* Returns the subset of signatures which are expected to be verified by at least one Android
* platform version in the {@code [minSdkVersion, maxSdkVersion]} range. The returned result is
* guaranteed to contain at least one signature.
*
* <p>Each Android platform version typically verifies exactly one signature from the provided
* {@code signatures} set. This method returns the set of these signatures collected over all
* requested platform versions. As a result, the result may contain more than one signature.
*
* @throws NoApkSupportedSignaturesException if no supported signatures were
* found for an Android platform version in the range.
*/
public static <T extends ApkSupportedSignature> List<T> getSignaturesToVerify(
List<T> signatures, int minSdkVersion, int maxSdkVersion)
throws NoApkSupportedSignaturesException {
return getSignaturesToVerify(signatures, minSdkVersion, maxSdkVersion, false);
}
/**
* Returns the subset of signatures which are expected to be verified by at least one Android
* platform version in the {@code [minSdkVersion, maxSdkVersion]} range. The returned result is
* guaranteed to contain at least one signature.
*
* <p>{@code onlyRequireJcaSupport} can be set to true for cases that only require verifying a
* signature within the signing block using the standard JCA.
*
* <p>Each Android platform version typically verifies exactly one signature from the provided
* {@code signatures} set. This method returns the set of these signatures collected over all
* requested platform versions. As a result, the result may contain more than one signature.
*
* @throws NoApkSupportedSignaturesException if no supported signatures were
* found for an Android platform version in the range.
*/
public static <T extends ApkSupportedSignature> List<T> getSignaturesToVerify(
List<T> signatures, int minSdkVersion, int maxSdkVersion,
boolean onlyRequireJcaSupport) throws
NoApkSupportedSignaturesException {
// Pick the signature with the strongest algorithm at all required SDK versions, to mimic
// Android's behavior on those versions.
//
// Here we assume that, once introduced, a signature algorithm continues to be supported in
// all future Android versions. We also assume that the better-than relationship between
// algorithms is exactly the same on all Android platform versions (except that older
// platforms might support fewer algorithms). If these assumption are no longer true, the
// logic here will need to change accordingly.
Map<Integer, T>
bestSigAlgorithmOnSdkVersion = new HashMap<>();
int minProvidedSignaturesVersion = Integer.MAX_VALUE;
for (T sig : signatures) {
SignatureAlgorithm sigAlgorithm = sig.algorithm;
int sigMinSdkVersion = onlyRequireJcaSupport ? sigAlgorithm.getJcaSigAlgMinSdkVersion()
: sigAlgorithm.getMinSdkVersion();
if (sigMinSdkVersion > maxSdkVersion) {
continue;
}
if (sigMinSdkVersion < minProvidedSignaturesVersion) {
minProvidedSignaturesVersion = sigMinSdkVersion;
}
T candidate = bestSigAlgorithmOnSdkVersion.get(sigMinSdkVersion);
if ((candidate == null)
|| (compareSignatureAlgorithm(
sigAlgorithm, candidate.algorithm) > 0)) {
bestSigAlgorithmOnSdkVersion.put(sigMinSdkVersion, sig);
}
}
// Must have some supported signature algorithms for minSdkVersion.
if (minSdkVersion < minProvidedSignaturesVersion) {
throw new NoApkSupportedSignaturesException(
"Minimum provided signature version " + minProvidedSignaturesVersion +
" > minSdkVersion " + minSdkVersion);
}
if (bestSigAlgorithmOnSdkVersion.isEmpty()) {
throw new NoApkSupportedSignaturesException("No supported signature");
}
List<T> signaturesToVerify =
new ArrayList<>(bestSigAlgorithmOnSdkVersion.values());
Collections.sort(
signaturesToVerify,
(sig1, sig2) -> Integer.compare(sig1.algorithm.getId(), sig2.algorithm.getId()));
return signaturesToVerify;
}
/**
* Returns positive number if {@code alg1} is preferred over {@code alg2}, {@code -1} if
* {@code alg2} is preferred over {@code alg1}, and {@code 0} if there is no preference.
*/
public static int compareSignatureAlgorithm(SignatureAlgorithm alg1, SignatureAlgorithm alg2) {
ContentDigestAlgorithm digestAlg1 = alg1.getContentDigestAlgorithm();
ContentDigestAlgorithm digestAlg2 = alg2.getContentDigestAlgorithm();
return compareContentDigestAlgorithm(digestAlg1, digestAlg2);
}
/**
* Returns a positive number if {@code alg1} is preferred over {@code alg2}, a negative number
* if {@code alg2} is preferred over {@code alg1}, or {@code 0} if there is no preference.
*/
private static int compareContentDigestAlgorithm(
ContentDigestAlgorithm alg1,
ContentDigestAlgorithm alg2) {
switch (alg1) {
case CHUNKED_SHA256:
switch (alg2) {
case CHUNKED_SHA256:
return 0;
case CHUNKED_SHA512:
case VERITY_CHUNKED_SHA256:
return -1;
default:
throw new IllegalArgumentException("Unknown alg2: " + alg2);
}
case CHUNKED_SHA512:
switch (alg2) {
case CHUNKED_SHA256:
case VERITY_CHUNKED_SHA256:
return 1;
case CHUNKED_SHA512:
return 0;
default:
throw new IllegalArgumentException("Unknown alg2: " + alg2);
}
case VERITY_CHUNKED_SHA256:
switch (alg2) {
case CHUNKED_SHA256:
return 1;
case VERITY_CHUNKED_SHA256:
return 0;
case CHUNKED_SHA512:
return -1;
default:
throw new IllegalArgumentException("Unknown alg2: " + alg2);
}
default:
throw new IllegalArgumentException("Unknown alg1: " + alg1);
}
}
/**
* Returns new byte buffer whose content is a shared subsequence of this buffer's content
* between the specified start (inclusive) and end (exclusive) positions. As opposed to
* {@link ByteBuffer#slice()}, the returned buffer's byte order is the same as the source
* buffer's byte order.
*/
private static ByteBuffer sliceFromTo(ByteBuffer source, int start, int end) {
if (start < 0) {
throw new IllegalArgumentException("start: " + start);
}
if (end < start) {
throw new IllegalArgumentException("end < start: " + end + " < " + start);
}
int capacity = source.capacity();
if (end > source.capacity()) {
throw new IllegalArgumentException("end > capacity: " + end + " > " + capacity);
}
int originalLimit = source.limit();
int originalPosition = source.position();
try {
source.position(0);
source.limit(end);
source.position(start);
ByteBuffer result = source.slice();
result.order(source.order());
return result;
} finally {
source.position(0);
source.limit(originalLimit);
source.position(originalPosition);
}
}
/**
* Relative <em>get</em> method for reading {@code size} number of bytes from the current
* position of this buffer.
*
* <p>This method reads the next {@code size} bytes at this buffer's current position,
* returning them as a {@code ByteBuffer} with start set to 0, limit and capacity set to
* {@code size}, byte order set to this buffer's byte order; and then increments the position by
* {@code size}.
*/
private static ByteBuffer getByteBuffer(ByteBuffer source, int size) {
if (size < 0) {
throw new IllegalArgumentException("size: " + size);
}
int originalLimit = source.limit();
int position = source.position();
int limit = position + size;
if ((limit < position) || (limit > originalLimit)) {
throw new BufferUnderflowException();
}
source.limit(limit);
try {
ByteBuffer result = source.slice();
result.order(source.order());
source.position(limit);
return result;
} finally {
source.limit(originalLimit);
}
}
public static String toHex(byte[] value) {
StringBuilder sb = new StringBuilder(value.length * 2);
int len = value.length;
for (int i = 0; i < len; i++) {
int hi = (value[i] & 0xff) >>> 4;
int lo = value[i] & 0x0f;
sb.append(HEX_DIGITS[hi]).append(HEX_DIGITS[lo]);
}
return sb.toString();
}
public static ByteBuffer getLengthPrefixedSlice(ByteBuffer source) throws ApkFormatException {
if (source.remaining() < 4) {
throw new ApkFormatException(
"Remaining buffer too short to contain length of length-prefixed field"
+ ". Remaining: " + source.remaining());
}
int len = source.getInt();
if (len < 0) {
throw new IllegalArgumentException("Negative length");
} else if (len > source.remaining()) {
throw new ApkFormatException(
"Length-prefixed field longer than remaining buffer"
+ ". Field length: " + len + ", remaining: " + source.remaining());
}
return getByteBuffer(source, len);
}
public static byte[] readLengthPrefixedByteArray(ByteBuffer buf) throws ApkFormatException {
int len = buf.getInt();
if (len < 0) {
throw new ApkFormatException("Negative length");
} else if (len > buf.remaining()) {
throw new ApkFormatException(
"Underflow while reading length-prefixed value. Length: " + len
+ ", available: " + buf.remaining());
}
byte[] result = new byte[len];
buf.get(result);
return result;
}
public static byte[] encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
List<Pair<Integer, byte[]>> sequence) {
int resultSize = 0;
for (Pair<Integer, byte[]> element : sequence) {
resultSize += 12 + element.getSecond().length;
}
ByteBuffer result = ByteBuffer.allocate(resultSize);
result.order(ByteOrder.LITTLE_ENDIAN);
for (Pair<Integer, byte[]> element : sequence) {
byte[] second = element.getSecond();
result.putInt(8 + second.length);
result.putInt(element.getFirst());
result.putInt(second.length);
result.put(second);
}
return result.array();
}
}

View File

@ -0,0 +1,35 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.apk;
/**
* Base implementation of a supported signature for an APK.
*/
public class ApkSupportedSignature {
public final SignatureAlgorithm algorithm;
public final byte[] signature;
/**
* Constructs a new supported signature using the provided {@code algorithm} and {@code
* signature} bytes.
*/
public ApkSupportedSignature(SignatureAlgorithm algorithm, byte[] signature) {
this.algorithm = algorithm;
this.signature = signature;
}
}

View File

@ -0,0 +1,61 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.apk;
/** APK Signature Scheme v2 content digest algorithm. */
public enum ContentDigestAlgorithm {
/** SHA2-256 over 1 MB chunks. */
CHUNKED_SHA256(1, "SHA-256", 256 / 8),
/** SHA2-512 over 1 MB chunks. */
CHUNKED_SHA512(2, "SHA-512", 512 / 8),
/** SHA2-256 over 4 KB chunks for APK verity. */
VERITY_CHUNKED_SHA256(3, "SHA-256", 256 / 8),
/** Non-chunk SHA2-256. */
SHA256(4, "SHA-256", 256 / 8);
private final int mId;
private final String mJcaMessageDigestAlgorithm;
private final int mChunkDigestOutputSizeBytes;
private ContentDigestAlgorithm(
int id, String jcaMessageDigestAlgorithm, int chunkDigestOutputSizeBytes) {
mId = id;
mJcaMessageDigestAlgorithm = jcaMessageDigestAlgorithm;
mChunkDigestOutputSizeBytes = chunkDigestOutputSizeBytes;
}
/** Returns the ID of the digest algorithm used on the APK. */
public int getId() {
return mId;
}
/**
* Returns the {@link java.security.MessageDigest} algorithm used for computing digests of
* chunks by this content digest algorithm.
*/
String getJcaMessageDigestAlgorithm() {
return mJcaMessageDigestAlgorithm;
}
/** Returns the size (in bytes) of the digest of a chunk of content. */
int getChunkDigestOutputSizeBytes() {
return mChunkDigestOutputSizeBytes;
}
}

View File

@ -0,0 +1,27 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.apk;
/**
* Base exception that is thrown when there are no signatures that support the full range of
* requested platform versions.
*/
public class NoApkSupportedSignaturesException extends Exception {
public NoApkSupportedSignaturesException(String message) {
super(message);
}
}

View File

@ -0,0 +1,225 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.apk;
import com.android.apksig.internal.util.AndroidSdkVersion;
import com.android.apksig.internal.util.Pair;
import java.security.spec.AlgorithmParameterSpec;
import java.security.spec.MGF1ParameterSpec;
import java.security.spec.PSSParameterSpec;
/**
* APK Signing Block signature algorithm.
*/
public enum SignatureAlgorithm {
// TODO reserve the 0x0000 ID to mean null
/**
* RSASSA-PSS with SHA2-256 digest, SHA2-256 MGF1, 32 bytes of salt, trailer: 0xbc, content
* digested using SHA2-256 in 1 MB chunks.
*/
RSA_PSS_WITH_SHA256(
0x0101,
ContentDigestAlgorithm.CHUNKED_SHA256,
"RSA",
Pair.of("SHA256withRSA/PSS",
new PSSParameterSpec(
"SHA-256", "MGF1", MGF1ParameterSpec.SHA256, 256 / 8, 1)),
AndroidSdkVersion.N,
AndroidSdkVersion.M),
/**
* RSASSA-PSS with SHA2-512 digest, SHA2-512 MGF1, 64 bytes of salt, trailer: 0xbc, content
* digested using SHA2-512 in 1 MB chunks.
*/
RSA_PSS_WITH_SHA512(
0x0102,
ContentDigestAlgorithm.CHUNKED_SHA512,
"RSA",
Pair.of(
"SHA512withRSA/PSS",
new PSSParameterSpec(
"SHA-512", "MGF1", MGF1ParameterSpec.SHA512, 512 / 8, 1)),
AndroidSdkVersion.N,
AndroidSdkVersion.M),
/** RSASSA-PKCS1-v1_5 with SHA2-256 digest, content digested using SHA2-256 in 1 MB chunks. */
RSA_PKCS1_V1_5_WITH_SHA256(
0x0103,
ContentDigestAlgorithm.CHUNKED_SHA256,
"RSA",
Pair.of("SHA256withRSA", null),
AndroidSdkVersion.N,
AndroidSdkVersion.INITIAL_RELEASE),
/** RSASSA-PKCS1-v1_5 with SHA2-512 digest, content digested using SHA2-512 in 1 MB chunks. */
RSA_PKCS1_V1_5_WITH_SHA512(
0x0104,
ContentDigestAlgorithm.CHUNKED_SHA512,
"RSA",
Pair.of("SHA512withRSA", null),
AndroidSdkVersion.N,
AndroidSdkVersion.INITIAL_RELEASE),
/** ECDSA with SHA2-256 digest, content digested using SHA2-256 in 1 MB chunks. */
ECDSA_WITH_SHA256(
0x0201,
ContentDigestAlgorithm.CHUNKED_SHA256,
"EC",
Pair.of("SHA256withECDSA", null),
AndroidSdkVersion.N,
AndroidSdkVersion.HONEYCOMB),
/** ECDSA with SHA2-512 digest, content digested using SHA2-512 in 1 MB chunks. */
ECDSA_WITH_SHA512(
0x0202,
ContentDigestAlgorithm.CHUNKED_SHA512,
"EC",
Pair.of("SHA512withECDSA", null),
AndroidSdkVersion.N,
AndroidSdkVersion.HONEYCOMB),
/** DSA with SHA2-256 digest, content digested using SHA2-256 in 1 MB chunks. */
DSA_WITH_SHA256(
0x0301,
ContentDigestAlgorithm.CHUNKED_SHA256,
"DSA",
Pair.of("SHA256withDSA", null),
AndroidSdkVersion.N,
AndroidSdkVersion.INITIAL_RELEASE),
/**
* DSA with SHA2-256 digest, content digested using SHA2-256 in 1 MB chunks. Signing is done
* deterministically according to RFC 6979.
*/
DETDSA_WITH_SHA256(
0x0301,
ContentDigestAlgorithm.CHUNKED_SHA256,
"DSA",
Pair.of("SHA256withDetDSA", null),
AndroidSdkVersion.N,
AndroidSdkVersion.INITIAL_RELEASE),
/**
* RSASSA-PKCS1-v1_5 with SHA2-256 digest, content digested using SHA2-256 in 4 KB chunks, in
* the same way fsverity operates. This digest and the content length (before digestion, 8 bytes
* in little endian) construct the final digest.
*/
VERITY_RSA_PKCS1_V1_5_WITH_SHA256(
0x0421,
ContentDigestAlgorithm.VERITY_CHUNKED_SHA256,
"RSA",
Pair.of("SHA256withRSA", null),
AndroidSdkVersion.P,
AndroidSdkVersion.INITIAL_RELEASE),
/**
* ECDSA with SHA2-256 digest, content digested using SHA2-256 in 4 KB chunks, in the same way
* fsverity operates. This digest and the content length (before digestion, 8 bytes in little
* endian) construct the final digest.
*/
VERITY_ECDSA_WITH_SHA256(
0x0423,
ContentDigestAlgorithm.VERITY_CHUNKED_SHA256,
"EC",
Pair.of("SHA256withECDSA", null),
AndroidSdkVersion.P,
AndroidSdkVersion.HONEYCOMB),
/**
* DSA with SHA2-256 digest, content digested using SHA2-256 in 4 KB chunks, in the same way
* fsverity operates. This digest and the content length (before digestion, 8 bytes in little
* endian) construct the final digest.
*/
VERITY_DSA_WITH_SHA256(
0x0425,
ContentDigestAlgorithm.VERITY_CHUNKED_SHA256,
"DSA",
Pair.of("SHA256withDSA", null),
AndroidSdkVersion.P,
AndroidSdkVersion.INITIAL_RELEASE);
private final int mId;
private final String mJcaKeyAlgorithm;
private final ContentDigestAlgorithm mContentDigestAlgorithm;
private final Pair<String, ? extends AlgorithmParameterSpec> mJcaSignatureAlgAndParams;
private final int mMinSdkVersion;
private final int mJcaSigAlgMinSdkVersion;
SignatureAlgorithm(int id,
ContentDigestAlgorithm contentDigestAlgorithm,
String jcaKeyAlgorithm,
Pair<String, ? extends AlgorithmParameterSpec> jcaSignatureAlgAndParams,
int minSdkVersion,
int jcaSigAlgMinSdkVersion) {
mId = id;
mContentDigestAlgorithm = contentDigestAlgorithm;
mJcaKeyAlgorithm = jcaKeyAlgorithm;
mJcaSignatureAlgAndParams = jcaSignatureAlgAndParams;
mMinSdkVersion = minSdkVersion;
mJcaSigAlgMinSdkVersion = jcaSigAlgMinSdkVersion;
}
/**
* Returns the ID of this signature algorithm as used in APK Signature Scheme v2 wire format.
*/
public int getId() {
return mId;
}
/**
* Returns the content digest algorithm associated with this signature algorithm.
*/
public ContentDigestAlgorithm getContentDigestAlgorithm() {
return mContentDigestAlgorithm;
}
/**
* Returns the JCA {@link java.security.Key} algorithm used by this signature scheme.
*/
public String getJcaKeyAlgorithm() {
return mJcaKeyAlgorithm;
}
/**
* Returns the {@link java.security.Signature} algorithm and the {@link AlgorithmParameterSpec}
* (or null if not needed) to parameterize the {@code Signature}.
*/
public Pair<String, ? extends AlgorithmParameterSpec> getJcaSignatureAlgorithmAndParams() {
return mJcaSignatureAlgAndParams;
}
public int getMinSdkVersion() {
return mMinSdkVersion;
}
/**
* Returns the minimum SDK version that supports the JCA signature algorithm.
*/
public int getJcaSigAlgMinSdkVersion() {
return mJcaSigAlgMinSdkVersion;
}
public static SignatureAlgorithm findById(int id) {
for (SignatureAlgorithm alg : SignatureAlgorithm.values()) {
if (alg.getId() == id) {
return alg;
}
}
return null;
}
}

View File

@ -0,0 +1,53 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.apk;
import java.nio.ByteBuffer;
/**
* APK Signature Scheme block and additional information relevant to verifying the signatures
* contained in the block against the file.
*/
public class SignatureInfo {
/** Contents of APK Signature Scheme block. */
public final ByteBuffer signatureBlock;
/** Position of the APK Signing Block in the file. */
public final long apkSigningBlockOffset;
/** Position of the ZIP Central Directory in the file. */
public final long centralDirOffset;
/** Position of the ZIP End of Central Directory (EoCD) in the file. */
public final long eocdOffset;
/** Contents of ZIP End of Central Directory (EoCD) of the file. */
public final ByteBuffer eocd;
public SignatureInfo(
ByteBuffer signatureBlock,
long apkSigningBlockOffset,
long centralDirOffset,
long eocdOffset,
ByteBuffer eocd) {
this.signatureBlock = signatureBlock;
this.apkSigningBlockOffset = apkSigningBlockOffset;
this.centralDirOffset = centralDirOffset;
this.eocdOffset = eocdOffset;
this.eocd = eocd;
}
}

View File

@ -0,0 +1,30 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.apk;
/**
* Base exception that is thrown when the APK is not signed with the requested signature scheme.
*/
public class SignatureNotFoundException extends Exception {
public SignatureNotFoundException(String message) {
super(message);
}
public SignatureNotFoundException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -0,0 +1,235 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.apk.stamp;
import static com.android.apksig.internal.apk.ApkSigningBlockUtilsLite.getLengthPrefixedSlice;
import static com.android.apksig.internal.apk.ApkSigningBlockUtilsLite.readLengthPrefixedByteArray;
import com.android.apksig.apk.ApkFormatException;
import com.android.apksig.internal.apk.ApkSigningBlockUtilsLite;
import com.android.apksig.internal.apk.SignatureAlgorithm;
import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.spec.AlgorithmParameterSpec;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
/** Lightweight version of the V3SigningCertificateLineage to be used for source stamps. */
public class SourceStampCertificateLineage {
private final static int FIRST_VERSION = 1;
private final static int CURRENT_VERSION = FIRST_VERSION;
/**
* Deserializes the binary representation of a SourceStampCertificateLineage. Also
* verifies that the structure is well-formed, e.g. that the signature for each node is from its
* parent.
*/
public static List<SigningCertificateNode> readSigningCertificateLineage(ByteBuffer inputBytes)
throws IOException {
List<SigningCertificateNode> result = new ArrayList<>();
int nodeCount = 0;
if (inputBytes == null || !inputBytes.hasRemaining()) {
return null;
}
ApkSigningBlockUtilsLite.checkByteOrderLittleEndian(inputBytes);
CertificateFactory certFactory;
try {
certFactory = CertificateFactory.getInstance("X.509");
} catch (CertificateException e) {
throw new IllegalStateException("Failed to obtain X.509 CertificateFactory", e);
}
// FORMAT (little endian):
// * uint32: version code
// * sequence of length-prefixed (uint32): nodes
// * length-prefixed bytes: signed data
// * length-prefixed bytes: certificate
// * uint32: signature algorithm id
// * uint32: flags
// * uint32: signature algorithm id (used by to sign next cert in lineage)
// * length-prefixed bytes: signature over above signed data
X509Certificate lastCert = null;
int lastSigAlgorithmId = 0;
try {
int version = inputBytes.getInt();
if (version != CURRENT_VERSION) {
// we only have one version to worry about right now, so just check it
throw new IllegalArgumentException("Encoded SigningCertificateLineage has a version"
+ " different than any of which we are aware");
}
HashSet<X509Certificate> certHistorySet = new HashSet<>();
while (inputBytes.hasRemaining()) {
nodeCount++;
ByteBuffer nodeBytes = getLengthPrefixedSlice(inputBytes);
ByteBuffer signedData = getLengthPrefixedSlice(nodeBytes);
int flags = nodeBytes.getInt();
int sigAlgorithmId = nodeBytes.getInt();
SignatureAlgorithm sigAlgorithm = SignatureAlgorithm.findById(lastSigAlgorithmId);
byte[] signature = readLengthPrefixedByteArray(nodeBytes);
if (lastCert != null) {
// Use previous level cert to verify current level
String jcaSignatureAlgorithm =
sigAlgorithm.getJcaSignatureAlgorithmAndParams().getFirst();
AlgorithmParameterSpec jcaSignatureAlgorithmParams =
sigAlgorithm.getJcaSignatureAlgorithmAndParams().getSecond();
PublicKey publicKey = lastCert.getPublicKey();
Signature sig = Signature.getInstance(jcaSignatureAlgorithm);
sig.initVerify(publicKey);
if (jcaSignatureAlgorithmParams != null) {
sig.setParameter(jcaSignatureAlgorithmParams);
}
sig.update(signedData);
if (!sig.verify(signature)) {
throw new SecurityException("Unable to verify signature of certificate #"
+ nodeCount + " using " + jcaSignatureAlgorithm + " when verifying"
+ " SourceStampCertificateLineage object");
}
}
signedData.rewind();
byte[] encodedCert = readLengthPrefixedByteArray(signedData);
int signedSigAlgorithm = signedData.getInt();
if (lastCert != null && lastSigAlgorithmId != signedSigAlgorithm) {
throw new SecurityException("Signing algorithm ID mismatch for certificate #"
+ nodeBytes + " when verifying SourceStampCertificateLineage object");
}
lastCert = (X509Certificate) certFactory.generateCertificate(
new ByteArrayInputStream(encodedCert));
lastCert = new GuaranteedEncodedFormX509Certificate(lastCert, encodedCert);
if (certHistorySet.contains(lastCert)) {
throw new SecurityException("Encountered duplicate entries in "
+ "SigningCertificateLineage at certificate #" + nodeCount + ". All "
+ "signing certificates should be unique");
}
certHistorySet.add(lastCert);
lastSigAlgorithmId = sigAlgorithmId;
result.add(new SigningCertificateNode(
lastCert, SignatureAlgorithm.findById(signedSigAlgorithm),
SignatureAlgorithm.findById(sigAlgorithmId), signature, flags));
}
} catch(ApkFormatException | BufferUnderflowException e){
throw new IOException("Failed to parse SourceStampCertificateLineage object", e);
} catch(NoSuchAlgorithmException | InvalidKeyException
| InvalidAlgorithmParameterException | SignatureException e){
throw new SecurityException(
"Failed to verify signature over signed data for certificate #" + nodeCount
+ " when parsing SourceStampCertificateLineage object", e);
} catch(CertificateException e){
throw new SecurityException("Failed to decode certificate #" + nodeCount
+ " when parsing SourceStampCertificateLineage object", e);
}
return result;
}
/**
* Represents one signing certificate in the SourceStampCertificateLineage, which
* generally means it is/was used at some point to sign source stamps.
*/
public static class SigningCertificateNode {
public SigningCertificateNode(
X509Certificate signingCert,
SignatureAlgorithm parentSigAlgorithm,
SignatureAlgorithm sigAlgorithm,
byte[] signature,
int flags) {
this.signingCert = signingCert;
this.parentSigAlgorithm = parentSigAlgorithm;
this.sigAlgorithm = sigAlgorithm;
this.signature = signature;
this.flags = flags;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof SigningCertificateNode)) return false;
SigningCertificateNode that = (SigningCertificateNode) o;
if (!signingCert.equals(that.signingCert)) return false;
if (parentSigAlgorithm != that.parentSigAlgorithm) return false;
if (sigAlgorithm != that.sigAlgorithm) return false;
if (!Arrays.equals(signature, that.signature)) return false;
if (flags != that.flags) return false;
// we made it
return true;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((signingCert == null) ? 0 : signingCert.hashCode());
result = prime * result +
((parentSigAlgorithm == null) ? 0 : parentSigAlgorithm.hashCode());
result = prime * result + ((sigAlgorithm == null) ? 0 : sigAlgorithm.hashCode());
result = prime * result + Arrays.hashCode(signature);
result = prime * result + flags;
return result;
}
/**
* the signing cert for this node. This is part of the data signed by the parent node.
*/
public final X509Certificate signingCert;
/**
* the algorithm used by this node's parent to bless this data. Its ID value is part of
* the data signed by the parent node. {@code null} for first node.
*/
public final SignatureAlgorithm parentSigAlgorithm;
/**
* the algorithm used by this node to bless the next node's data. Its ID value is part
* of the signed data of the next node. {@code null} for the last node.
*/
public SignatureAlgorithm sigAlgorithm;
/**
* signature over the signed data (above). The signature is from this node's parent
* signing certificate, which should correspond to the signing certificate used to sign an
* APK before rotating to this one, and is formed using {@code signatureAlgorithm}.
*/
public final byte[] signature;
/**
* the flags detailing how the platform should treat this signing cert
*/
public int flags;
}
}

View File

@ -0,0 +1,27 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.apk.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

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

@ -0,0 +1,109 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.apk.stamp;
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsLengthPrefixedElement;
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedElements;
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes;
import com.android.apksig.internal.apk.ApkSigningBlockUtils;
import com.android.apksig.internal.apk.ApkSigningBlockUtils.SignerConfig;
import com.android.apksig.internal.apk.ContentDigestAlgorithm;
import com.android.apksig.internal.util.Pair;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SignatureException;
import java.security.cert.CertificateEncodingException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
/**
* SourceStamp signer.
*
* <p>SourceStamp improves traceability of apps with respect to unauthorized distribution.
*
* <p>The stamp is part of the APK that is protected by the signing block.
*
* <p>The APK contents hash is signed using the stamp key, and is saved as part of the signing
* block.
*
* <p>V1 of the source stamp allows signing the digest of at most one signature scheme only.
*/
public abstract class V1SourceStampSigner {
public static final int V1_SOURCE_STAMP_BLOCK_ID =
SourceStampConstants.V1_SOURCE_STAMP_BLOCK_ID;
/** Hidden constructor to prevent instantiation. */
private V1SourceStampSigner() {}
public static Pair<byte[], Integer> generateSourceStampBlock(
SignerConfig sourceStampSignerConfig, Map<ContentDigestAlgorithm, byte[]> digestInfo)
throws SignatureException, NoSuchAlgorithmException, InvalidKeyException {
if (sourceStampSignerConfig.certificates.isEmpty()) {
throw new SignatureException("No certificates configured for signer");
}
List<Pair<Integer, byte[]>> digests = new ArrayList<>();
for (Map.Entry<ContentDigestAlgorithm, byte[]> digest : digestInfo.entrySet()) {
digests.add(Pair.of(digest.getKey().getId(), digest.getValue()));
}
Collections.sort(digests, Comparator.comparing(Pair::getFirst));
SourceStampBlock sourceStampBlock = new SourceStampBlock();
try {
sourceStampBlock.stampCertificate =
sourceStampSignerConfig.certificates.get(0).getEncoded();
} catch (CertificateEncodingException e) {
throw new SignatureException(
"Retrieving the encoded form of the stamp certificate failed", e);
}
byte[] digestBytes =
encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(digests);
sourceStampBlock.signedDigests =
ApkSigningBlockUtils.generateSignaturesOverData(
sourceStampSignerConfig, digestBytes);
// FORMAT:
// * length-prefixed bytes: X.509 certificate (ASN.1 DER encoded)
// * length-prefixed sequence of length-prefixed signatures:
// * uint32: signature algorithm ID
// * length-prefixed bytes: signature of signed data
byte[] sourceStampSignerBlock =
encodeAsSequenceOfLengthPrefixedElements(
new byte[][] {
sourceStampBlock.stampCertificate,
encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
sourceStampBlock.signedDigests),
});
// FORMAT:
// * length-prefixed stamp block.
return Pair.of(encodeAsLengthPrefixedElement(sourceStampSignerBlock),
SourceStampConstants.V1_SOURCE_STAMP_BLOCK_ID);
}
private static final class SourceStampBlock {
public byte[] stampCertificate;
public List<Pair<Integer, byte[]>> signedDigests;
}
}

View File

@ -0,0 +1,139 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.apk.stamp;
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes;
import static com.android.apksig.internal.apk.stamp.SourceStampConstants.V1_SOURCE_STAMP_BLOCK_ID;
import com.android.apksig.ApkVerifier;
import com.android.apksig.apk.ApkFormatException;
import com.android.apksig.apk.ApkUtils;
import com.android.apksig.internal.apk.ApkSigningBlockUtils;
import com.android.apksig.internal.apk.ContentDigestAlgorithm;
import com.android.apksig.internal.apk.SignatureInfo;
import com.android.apksig.internal.util.Pair;
import com.android.apksig.util.DataSource;
import java.io.IOException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
/**
* Source Stamp verifier.
*
* <p>V1 of the source stamp verifies the stamp signature of at most one signature scheme.
*/
public abstract class V1SourceStampVerifier {
/** Hidden constructor to prevent instantiation. */
private V1SourceStampVerifier() {}
/**
* Verifies the provided APK's SourceStamp signatures and returns the result of verification.
* The APK must be considered verified only if {@link ApkSigningBlockUtils.Result#verified} is
* {@code true}. If verification fails, the result will contain errors -- see {@link
* ApkSigningBlockUtils.Result#getErrors()}.
*
* @throws NoSuchAlgorithmException if the APK's signatures cannot be verified because a
* required cryptographic algorithm implementation is missing
* @throws ApkSigningBlockUtils.SignatureNotFoundException if no SourceStamp signatures are
* found
* @throws IOException if an I/O error occurs when reading the APK
*/
public static ApkSigningBlockUtils.Result verify(
DataSource apk,
ApkUtils.ZipSections zipSections,
byte[] sourceStampCertificateDigest,
Map<ContentDigestAlgorithm, byte[]> apkContentDigests,
int minSdkVersion,
int maxSdkVersion)
throws IOException, NoSuchAlgorithmException,
ApkSigningBlockUtils.SignatureNotFoundException {
ApkSigningBlockUtils.Result result =
new ApkSigningBlockUtils.Result(ApkSigningBlockUtils.VERSION_SOURCE_STAMP);
SignatureInfo signatureInfo =
ApkSigningBlockUtils.findSignature(
apk, zipSections, V1_SOURCE_STAMP_BLOCK_ID, result);
verify(
signatureInfo.signatureBlock,
sourceStampCertificateDigest,
apkContentDigests,
minSdkVersion,
maxSdkVersion,
result);
return result;
}
/**
* Verifies the provided APK's SourceStamp signatures and outputs the results into the provided
* {@code result}. APK is considered verified only if there are no errors reported in the {@code
* result}. See {@link #verify(DataSource, ApkUtils.ZipSections, byte[], Map, int, int)} for
* more information about the contract of this method.
*/
private static void verify(
ByteBuffer sourceStampBlock,
byte[] sourceStampCertificateDigest,
Map<ContentDigestAlgorithm, byte[]> apkContentDigests,
int minSdkVersion,
int maxSdkVersion,
ApkSigningBlockUtils.Result result)
throws NoSuchAlgorithmException {
ApkSigningBlockUtils.Result.SignerInfo signerInfo =
new ApkSigningBlockUtils.Result.SignerInfo();
result.signers.add(signerInfo);
try {
CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
ByteBuffer sourceStampBlockData =
ApkSigningBlockUtils.getLengthPrefixedSlice(sourceStampBlock);
byte[] digestBytes =
encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
getApkDigests(apkContentDigests));
SourceStampVerifier.verifyV1SourceStamp(
sourceStampBlockData,
certFactory,
signerInfo,
digestBytes,
sourceStampCertificateDigest,
minSdkVersion,
maxSdkVersion);
result.verified = !result.containsErrors() && !result.containsWarnings();
} catch (CertificateException e) {
throw new IllegalStateException("Failed to obtain X.509 CertificateFactory", e);
} catch (ApkFormatException | BufferUnderflowException e) {
signerInfo.addWarning(ApkVerifier.Issue.SOURCE_STAMP_MALFORMED_SIGNATURE);
}
}
private static List<Pair<Integer, byte[]>> getApkDigests(
Map<ContentDigestAlgorithm, byte[]> apkContentDigests) {
List<Pair<Integer, byte[]>> digests = new ArrayList<>();
for (Map.Entry<ContentDigestAlgorithm, byte[]> apkContentDigest :
apkContentDigests.entrySet()) {
digests.add(Pair.of(apkContentDigest.getKey().getId(), apkContentDigest.getValue()));
}
Collections.sort(digests, Comparator.comparing(Pair::getFirst));
return digests;
}
}

View File

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

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

@ -0,0 +1,74 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.apk.v1;
import java.util.Comparator;
/**
* Digest algorithm used with JAR signing (aka v1 signing scheme).
*/
public enum DigestAlgorithm {
/** SHA-1 */
SHA1("SHA-1"),
/** SHA2-256 */
SHA256("SHA-256");
private final String mJcaMessageDigestAlgorithm;
private DigestAlgorithm(String jcaMessageDigestAlgoritm) {
mJcaMessageDigestAlgorithm = jcaMessageDigestAlgoritm;
}
/**
* Returns the {@link java.security.MessageDigest} algorithm represented by this digest
* algorithm.
*/
String getJcaMessageDigestAlgorithm() {
return mJcaMessageDigestAlgorithm;
}
public static Comparator<DigestAlgorithm> BY_STRENGTH_COMPARATOR = new StrengthComparator();
private static class StrengthComparator implements Comparator<DigestAlgorithm> {
@Override
public int compare(DigestAlgorithm a1, DigestAlgorithm a2) {
switch (a1) {
case SHA1:
switch (a2) {
case SHA1:
return 0;
case SHA256:
return -1;
}
throw new RuntimeException("Unsupported algorithm: " + a2);
case SHA256:
switch (a2) {
case SHA1:
return 1;
case SHA256:
return 0;
}
throw new RuntimeException("Unsupported algorithm: " + a2);
default:
throw new RuntimeException("Unsupported algorithm: " + a1);
}
}
}
}

View File

@ -0,0 +1,26 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.apk.v1;
/** Constants used by the Jar Signing / V1 Signature Scheme signing and verification. */
public class V1SchemeConstants {
private V1SchemeConstants() {}
public static final String MANIFEST_ENTRY_NAME = "META-INF/MANIFEST.MF";
public static final String SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME_STR =
"X-Android-APK-Signed";
}

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,25 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.apk.v2;
/** Constants used by the V2 Signature Scheme signing and verification. */
public class V2SchemeConstants {
private V2SchemeConstants() {}
public static final int APK_SIGNATURE_SCHEME_V2_BLOCK_ID = 0x7109871a;
public static final int STRIPPING_PROTECTION_ATTR_ID = 0xbeeff00d;
}

View File

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

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

@ -0,0 +1,25 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.apk.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

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

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

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

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

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

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

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

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

@ -0,0 +1,32 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.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

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

@ -0,0 +1,32 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -0,0 +1,61 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.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

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

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

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

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

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

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

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

@ -0,0 +1,32 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.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

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

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

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

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

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

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

View File

@ -0,0 +1,59 @@
/*
* 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 java.io.IOException;
import java.nio.BufferOverflowException;
import java.nio.ByteBuffer;
/**
* Data sink which stores all received data into the associated {@link ByteBuffer}.
*/
public class ByteBufferSink implements DataSink {
private final ByteBuffer mBuffer;
public ByteBufferSink(ByteBuffer buffer) {
mBuffer = buffer;
}
public ByteBuffer getBuffer() {
return mBuffer;
}
@Override
public void consume(byte[] buf, int offset, int length) throws IOException {
try {
mBuffer.put(buf, offset, length);
} catch (BufferOverflowException e) {
throw new IOException(
"Insufficient space in output buffer for " + length + " bytes", e);
}
}
@Override
public void consume(ByteBuffer buf) throws IOException {
int length = buf.remaining();
try {
mBuffer.put(buf);
} catch (BufferOverflowException e) {
throw new IOException(
"Insufficient space in output buffer for " + length + " bytes", e);
}
}
}

View File

@ -0,0 +1,33 @@
/*
* 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.util;
import java.nio.ByteBuffer;
public final class ByteBufferUtils {
private ByteBufferUtils() {}
/**
* Returns the remaining data of the provided buffer as a new byte array and advances the
* position of the buffer to the buffer's limit.
*/
public static byte[] toByteArray(ByteBuffer buf) {
byte[] result = new byte[buf.remaining()];
buf.get(result);
return result;
}
}

View File

@ -0,0 +1,41 @@
/*
* 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.util;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
/**
* Utilities for byte arrays and I/O streams.
*/
public final class ByteStreams {
private ByteStreams() {}
/**
* Returns the data remaining in the provided input stream as a byte array
*/
public static byte[] toByteArray(InputStream in) throws IOException {
ByteArrayOutputStream result = new ByteArrayOutputStream();
byte[] buf = new byte[16384];
int chunkSize;
while ((chunkSize = in.read(buf)) != -1) {
result.write(buf, 0, chunkSize);
}
return result.toByteArray();
}
}

View File

@ -0,0 +1,145 @@
/*
* 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.util;
import com.android.apksig.util.DataSink;
import com.android.apksig.util.DataSource;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
/** Pseudo {@link DataSource} that chains the given {@link DataSource} as a continuous one. */
public class ChainedDataSource implements DataSource {
private final DataSource[] mSources;
private final long mTotalSize;
public ChainedDataSource(DataSource... sources) {
mSources = sources;
mTotalSize = Arrays.stream(sources).mapToLong(src -> src.size()).sum();
}
@Override
public long size() {
return mTotalSize;
}
@Override
public void feed(long offset, long size, DataSink sink) throws IOException {
if (offset + size > mTotalSize) {
throw new IndexOutOfBoundsException("Requested more than available");
}
for (DataSource src : mSources) {
// Offset is beyond the current source. Skip.
if (offset >= src.size()) {
offset -= src.size();
continue;
}
// If the remaining is enough, finish it.
long remaining = src.size() - offset;
if (remaining >= size) {
src.feed(offset, size, sink);
break;
}
// If the remaining is not enough, consume all.
src.feed(offset, remaining, sink);
size -= remaining;
offset = 0;
}
}
@Override
public ByteBuffer getByteBuffer(long offset, int size) throws IOException {
if (offset + size > mTotalSize) {
throw new IndexOutOfBoundsException("Requested more than available");
}
// Skip to the first DataSource we need.
Pair<Integer, Long> firstSource = locateDataSource(offset);
int i = firstSource.getFirst();
offset = firstSource.getSecond();
// Return the current source's ByteBuffer if it fits.
if (offset + size <= mSources[i].size()) {
return mSources[i].getByteBuffer(offset, size);
}
// Otherwise, read into a new buffer.
ByteBuffer buffer = ByteBuffer.allocate(size);
for (; i < mSources.length && buffer.hasRemaining(); i++) {
long sizeToCopy = Math.min(mSources[i].size() - offset, buffer.remaining());
mSources[i].copyTo(offset, Math.toIntExact(sizeToCopy), buffer);
offset = 0; // may not be zero for the first source, but reset after that.
}
buffer.rewind();
return buffer;
}
@Override
public void copyTo(long offset, int size, ByteBuffer dest) throws IOException {
feed(offset, size, new ByteBufferSink(dest));
}
@Override
public DataSource slice(long offset, long size) {
// Find the first slice.
Pair<Integer, Long> firstSource = locateDataSource(offset);
int beginIndex = firstSource.getFirst();
long beginLocalOffset = firstSource.getSecond();
DataSource beginSource = mSources[beginIndex];
if (beginLocalOffset + size <= beginSource.size()) {
return beginSource.slice(beginLocalOffset, size);
}
// Add the first slice to chaining, followed by the middle full slices, then the last.
ArrayList<DataSource> sources = new ArrayList<>();
sources.add(beginSource.slice(
beginLocalOffset, beginSource.size() - beginLocalOffset));
Pair<Integer, Long> lastSource = locateDataSource(offset + size - 1);
int endIndex = lastSource.getFirst();
long endLocalOffset = lastSource.getSecond();
for (int i = beginIndex + 1; i < endIndex; i++) {
sources.add(mSources[i]);
}
sources.add(mSources[endIndex].slice(0, endLocalOffset + 1));
return new ChainedDataSource(sources.toArray(new DataSource[0]));
}
/**
* Find the index of DataSource that offset is at.
* @return Pair of DataSource index and the local offset in the DataSource.
*/
private Pair<Integer, Long> locateDataSource(long offset) {
long localOffset = offset;
for (int i = 0; i < mSources.length; i++) {
if (localOffset < mSources[i].size()) {
return Pair.of(i, localOffset);
}
localOffset -= mSources[i].size();
}
throw new IndexOutOfBoundsException("Access is out of bound, offset: " + offset +
", totalSize: " + mTotalSize);
}
}

View File

@ -0,0 +1,217 @@
/*
* 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 java.math.BigInteger;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.Principal;
import java.security.Provider;
import java.security.PublicKey;
import java.security.SignatureException;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateExpiredException;
import java.security.cert.CertificateNotYetValidException;
import java.security.cert.CertificateParsingException;
import java.security.cert.X509Certificate;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Set;
import javax.security.auth.x500.X500Principal;
/**
* {@link X509Certificate} which delegates all method invocations to the provided delegate
* {@code X509Certificate}.
*/
public class DelegatingX509Certificate extends X509Certificate {
private static final long serialVersionUID = 1L;
private final X509Certificate mDelegate;
public DelegatingX509Certificate(X509Certificate delegate) {
this.mDelegate = delegate;
}
@Override
public Set<String> getCriticalExtensionOIDs() {
return mDelegate.getCriticalExtensionOIDs();
}
@Override
public byte[] getExtensionValue(String oid) {
return mDelegate.getExtensionValue(oid);
}
@Override
public Set<String> getNonCriticalExtensionOIDs() {
return mDelegate.getNonCriticalExtensionOIDs();
}
@Override
public boolean hasUnsupportedCriticalExtension() {
return mDelegate.hasUnsupportedCriticalExtension();
}
@Override
public void checkValidity()
throws CertificateExpiredException, CertificateNotYetValidException {
mDelegate.checkValidity();
}
@Override
public void checkValidity(Date date)
throws CertificateExpiredException, CertificateNotYetValidException {
mDelegate.checkValidity(date);
}
@Override
public int getVersion() {
return mDelegate.getVersion();
}
@Override
public BigInteger getSerialNumber() {
return mDelegate.getSerialNumber();
}
@Override
public Principal getIssuerDN() {
return mDelegate.getIssuerDN();
}
@Override
public Principal getSubjectDN() {
return mDelegate.getSubjectDN();
}
@Override
public Date getNotBefore() {
return mDelegate.getNotBefore();
}
@Override
public Date getNotAfter() {
return mDelegate.getNotAfter();
}
@Override
public byte[] getTBSCertificate() throws CertificateEncodingException {
return mDelegate.getTBSCertificate();
}
@Override
public byte[] getSignature() {
return mDelegate.getSignature();
}
@Override
public String getSigAlgName() {
return mDelegate.getSigAlgName();
}
@Override
public String getSigAlgOID() {
return mDelegate.getSigAlgOID();
}
@Override
public byte[] getSigAlgParams() {
return mDelegate.getSigAlgParams();
}
@Override
public boolean[] getIssuerUniqueID() {
return mDelegate.getIssuerUniqueID();
}
@Override
public boolean[] getSubjectUniqueID() {
return mDelegate.getSubjectUniqueID();
}
@Override
public boolean[] getKeyUsage() {
return mDelegate.getKeyUsage();
}
@Override
public int getBasicConstraints() {
return mDelegate.getBasicConstraints();
}
@Override
public byte[] getEncoded() throws CertificateEncodingException {
return mDelegate.getEncoded();
}
@Override
public void verify(PublicKey key) throws CertificateException, NoSuchAlgorithmException,
InvalidKeyException, NoSuchProviderException, SignatureException {
mDelegate.verify(key);
}
@Override
public void verify(PublicKey key, String sigProvider)
throws CertificateException, NoSuchAlgorithmException, InvalidKeyException,
NoSuchProviderException, SignatureException {
mDelegate.verify(key, sigProvider);
}
@Override
public String toString() {
return mDelegate.toString();
}
@Override
public PublicKey getPublicKey() {
return mDelegate.getPublicKey();
}
@Override
public X500Principal getIssuerX500Principal() {
return mDelegate.getIssuerX500Principal();
}
@Override
public X500Principal getSubjectX500Principal() {
return mDelegate.getSubjectX500Principal();
}
@Override
public List<String> getExtendedKeyUsage() throws CertificateParsingException {
return mDelegate.getExtendedKeyUsage();
}
@Override
public Collection<List<?>> getSubjectAlternativeNames() throws CertificateParsingException {
return mDelegate.getSubjectAlternativeNames();
}
@Override
public Collection<List<?>> getIssuerAlternativeNames() throws CertificateParsingException {
return mDelegate.getIssuerAlternativeNames();
}
@Override
public void verify(PublicKey key, Provider sigProvider) throws CertificateException,
NoSuchAlgorithmException, InvalidKeyException, SignatureException {
mDelegate.verify(key, sigProvider);
}
}

View File

@ -0,0 +1,191 @@
/*
* 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.io.RandomAccessFile;
import java.nio.BufferOverflowException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
/**
* {@link DataSource} backed by a {@link FileChannel} for {@link RandomAccessFile} access.
*/
public class FileChannelDataSource implements DataSource {
private static final int MAX_READ_CHUNK_SIZE = 1024 * 1024;
private final FileChannel mChannel;
private final long mOffset;
private final long mSize;
/**
* Constructs a new {@code FileChannelDataSource} based on the data contained in the
* whole file. Changes to the contents of the file, including the size of the file,
* will be visible in this data source.
*/
public FileChannelDataSource(FileChannel channel) {
mChannel = channel;
mOffset = 0;
mSize = -1;
}
/**
* Constructs a new {@code FileChannelDataSource} based on the data contained in the
* specified region of the provided file. Changes to the contents of the file will be visible in
* this data source.
*
* @throws IndexOutOfBoundsException if {@code offset} or {@code size} is negative.
*/
public FileChannelDataSource(FileChannel channel, long offset, long size) {
if (offset < 0) {
throw new IndexOutOfBoundsException("offset: " + size);
}
if (size < 0) {
throw new IndexOutOfBoundsException("size: " + size);
}
mChannel = channel;
mOffset = offset;
mSize = size;
}
@Override
public long size() {
if (mSize == -1) {
try {
return mChannel.size();
} catch (IOException e) {
return 0;
}
} else {
return mSize;
}
}
@Override
public FileChannelDataSource slice(long offset, long size) {
long sourceSize = size();
checkChunkValid(offset, size, sourceSize);
if ((offset == 0) && (size == sourceSize)) {
return this;
}
return new FileChannelDataSource(mChannel, mOffset + offset, size);
}
@Override
public void feed(long offset, long size, DataSink sink) throws IOException {
long sourceSize = size();
checkChunkValid(offset, size, sourceSize);
if (size == 0) {
return;
}
long chunkOffsetInFile = mOffset + offset;
long remaining = size;
ByteBuffer buf = ByteBuffer.allocateDirect((int) Math.min(remaining, MAX_READ_CHUNK_SIZE));
while (remaining > 0) {
int chunkSize = (int) Math.min(remaining, buf.capacity());
int chunkRemaining = chunkSize;
buf.limit(chunkSize);
synchronized (mChannel) {
mChannel.position(chunkOffsetInFile);
while (chunkRemaining > 0) {
int read = mChannel.read(buf);
if (read < 0) {
throw new IOException("Unexpected EOF encountered");
}
chunkRemaining -= read;
}
}
buf.flip();
sink.consume(buf);
buf.clear();
chunkOffsetInFile += chunkSize;
remaining -= chunkSize;
}
}
@Override
public void copyTo(long offset, int size, ByteBuffer dest) throws IOException {
long sourceSize = size();
checkChunkValid(offset, size, sourceSize);
if (size == 0) {
return;
}
if (size > dest.remaining()) {
throw new BufferOverflowException();
}
long offsetInFile = mOffset + offset;
int remaining = size;
int prevLimit = dest.limit();
try {
// FileChannel.read(ByteBuffer) reads up to dest.remaining(). Thus, we need to adjust
// the buffer's limit to avoid reading more than size bytes.
dest.limit(dest.position() + size);
while (remaining > 0) {
int chunkSize;
synchronized (mChannel) {
mChannel.position(offsetInFile);
chunkSize = mChannel.read(dest);
}
offsetInFile += chunkSize;
remaining -= chunkSize;
}
} finally {
dest.limit(prevLimit);
}
}
@Override
public ByteBuffer getByteBuffer(long offset, int size) throws IOException {
if (size < 0) {
throw new IndexOutOfBoundsException("size: " + size);
}
ByteBuffer result = ByteBuffer.allocate(size);
copyTo(offset, size, result);
result.flip();
return result;
}
private static void checkChunkValid(long offset, long size, long sourceSize) {
if (offset < 0) {
throw new IndexOutOfBoundsException("offset: " + offset);
}
if (size < 0) {
throw new IndexOutOfBoundsException("size: " + size);
}
if (offset > sourceSize) {
throw new IndexOutOfBoundsException(
"offset (" + offset + ") > source size (" + sourceSize + ")");
}
long endOffset = offset + size;
if (endOffset < offset) {
throw new IndexOutOfBoundsException(
"offset (" + offset + ") + size (" + size + ") overflow");
}
if (endOffset > sourceSize) {
throw new IndexOutOfBoundsException(
"offset (" + offset + ") + size (" + size
+ ") > source size (" + sourceSize +")");
}
}
}

View File

@ -0,0 +1,68 @@
/*
* 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.util;
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
import java.util.Arrays;
/**
* {@link X509Certificate} whose {@link #getEncoded()} returns the data provided at construction
* time.
*/
public class GuaranteedEncodedFormX509Certificate extends DelegatingX509Certificate {
private static final long serialVersionUID = 1L;
private final byte[] mEncodedForm;
private int mHash = -1;
public GuaranteedEncodedFormX509Certificate(X509Certificate wrapped, byte[] encodedForm) {
super(wrapped);
this.mEncodedForm = (encodedForm != null) ? encodedForm.clone() : null;
}
@Override
public byte[] getEncoded() throws CertificateEncodingException {
return (mEncodedForm != null) ? mEncodedForm.clone() : null;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof X509Certificate)) return false;
try {
byte[] a = this.getEncoded();
byte[] b = ((X509Certificate) o).getEncoded();
return Arrays.equals(a, b);
} catch (CertificateEncodingException e) {
return false;
}
}
@Override
public int hashCode() {
if (mHash == -1) {
try {
mHash = Arrays.hashCode(this.getEncoded());
} catch (CertificateEncodingException e) {
mHash = 0;
}
}
return mHash;
}
}

View File

@ -0,0 +1,89 @@
/*
* 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 java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* Inclusive interval of integers.
*/
public class InclusiveIntRange {
private final int min;
private final int max;
private InclusiveIntRange(int min, int max) {
this.min = min;
this.max = max;
}
public int getMin() {
return min;
}
public int getMax() {
return max;
}
public static InclusiveIntRange fromTo(int min, int max) {
return new InclusiveIntRange(min, max);
}
public static InclusiveIntRange from(int min) {
return new InclusiveIntRange(min, Integer.MAX_VALUE);
}
public List<InclusiveIntRange> getValuesNotIn(
List<InclusiveIntRange> sortedNonOverlappingRanges) {
if (sortedNonOverlappingRanges.isEmpty()) {
return Collections.singletonList(this);
}
int testValue = min;
List<InclusiveIntRange> result = null;
for (InclusiveIntRange range : sortedNonOverlappingRanges) {
int rangeMax = range.max;
if (testValue > rangeMax) {
continue;
}
int rangeMin = range.min;
if (testValue < range.min) {
if (result == null) {
result = new ArrayList<>();
}
result.add(fromTo(testValue, rangeMin - 1));
}
if (rangeMax >= max) {
return (result != null) ? result : Collections.emptyList();
}
testValue = rangeMax + 1;
}
if (testValue <= max) {
if (result == null) {
result = new ArrayList<>(1);
}
result.add(fromTo(testValue, max));
}
return (result != null) ? result : Collections.emptyList();
}
@Override
public String toString() {
return "[" + min + ", " + ((max < Integer.MAX_VALUE) ? (max + "]") : "\u221e)");
}
}

View File

@ -0,0 +1,51 @@
/*
* 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 java.nio.ByteBuffer;
import java.security.MessageDigest;
/**
* Data sink which feeds all received data into the associated {@link MessageDigest} instances. Each
* {@code MessageDigest} instance receives the same data.
*/
public class MessageDigestSink implements DataSink {
private final MessageDigest[] mMessageDigests;
public MessageDigestSink(MessageDigest[] digests) {
mMessageDigests = digests;
}
@Override
public void consume(byte[] buf, int offset, int length) {
for (MessageDigest md : mMessageDigests) {
md.update(buf, offset, length);
}
}
@Override
public void consume(ByteBuffer buf) {
int originalPosition = buf.position();
for (MessageDigest md : mMessageDigests) {
// Reset the position back to the original because the previous iteration's
// MessageDigest.update set the buffer's position to the buffer's limit.
buf.position(originalPosition);
md.update(buf);
}
}
}

View File

@ -0,0 +1,77 @@
/*
* 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 java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
/**
* {@link DataSink} which outputs received data into the associated {@link OutputStream}.
*/
public class OutputStreamDataSink implements DataSink {
private static final int MAX_READ_CHUNK_SIZE = 65536;
private final OutputStream mOut;
/**
* Constructs a new {@code OutputStreamDataSink} which outputs received data into the provided
* {@link OutputStream}.
*/
public OutputStreamDataSink(OutputStream out) {
if (out == null) {
throw new NullPointerException("out == null");
}
mOut = out;
}
/**
* Returns {@link OutputStream} into which this data sink outputs received data.
*/
public OutputStream getOutputStream() {
return mOut;
}
@Override
public void consume(byte[] buf, int offset, int length) throws IOException {
mOut.write(buf, offset, length);
}
@Override
public void consume(ByteBuffer buf) throws IOException {
if (!buf.hasRemaining()) {
return;
}
if (buf.hasArray()) {
mOut.write(
buf.array(),
buf.arrayOffset() + buf.position(),
buf.remaining());
buf.position(buf.limit());
} else {
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);
mOut.write(tmp, 0, chunkSize);
}
}
}
}

View File

@ -0,0 +1,81 @@
/*
* 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;
/**
* Pair of two elements.
*/
public final class Pair<A, B> {
private final A mFirst;
private final B mSecond;
private Pair(A first, B second) {
mFirst = first;
mSecond = second;
}
public static <A, B> Pair<A, B> of(A first, B second) {
return new Pair<A, B>(first, second);
}
public A getFirst() {
return mFirst;
}
public B getSecond() {
return mSecond;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((mFirst == null) ? 0 : mFirst.hashCode());
result = prime * result + ((mSecond == null) ? 0 : mSecond.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
@SuppressWarnings("rawtypes")
Pair other = (Pair) obj;
if (mFirst == null) {
if (other.mFirst != null) {
return false;
}
} else if (!mFirst.equals(other.mFirst)) {
return false;
}
if (mSecond == null) {
if (other.mSecond != null) {
return false;
}
} else if (!mSecond.equals(other.mSecond)) {
return false;
}
return true;
}
}

View File

@ -0,0 +1,104 @@
/*
* 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 java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
/**
* {@link DataSink} which outputs received data into the associated file, sequentially.
*/
public class RandomAccessFileDataSink implements DataSink {
private final RandomAccessFile mFile;
private final FileChannel mFileChannel;
private long mPosition;
/**
* Constructs a new {@code RandomAccessFileDataSink} which stores output starting from the
* beginning of the provided file.
*/
public RandomAccessFileDataSink(RandomAccessFile file) {
this(file, 0);
}
/**
* Constructs a new {@code RandomAccessFileDataSink} which stores output starting from the
* specified position of the provided file.
*/
public RandomAccessFileDataSink(RandomAccessFile file, long startPosition) {
if (file == null) {
throw new NullPointerException("file == null");
}
if (startPosition < 0) {
throw new IllegalArgumentException("startPosition: " + startPosition);
}
mFile = file;
mFileChannel = file.getChannel();
mPosition = startPosition;
}
/**
* Returns the underlying {@link RandomAccessFile}.
*/
public RandomAccessFile getFile() {
return mFile;
}
@Override
public void consume(byte[] buf, int offset, int length) throws IOException {
if (offset < 0) {
// Must perform this check here because RandomAccessFile.write doesn't throw when offset
// is negative but length is 0
throw new IndexOutOfBoundsException("offset: " + offset);
}
if (offset > buf.length) {
// Must perform this check here because RandomAccessFile.write doesn't throw when offset
// is too large but length is 0
throw new IndexOutOfBoundsException(
"offset: " + offset + ", buf.length: " + buf.length);
}
if (length == 0) {
return;
}
synchronized (mFile) {
mFile.seek(mPosition);
mFile.write(buf, offset, length);
mPosition += length;
}
}
@Override
public void consume(ByteBuffer buf) throws IOException {
int length = buf.remaining();
if (length == 0) {
return;
}
synchronized (mFile) {
mFile.seek(mPosition);
while (buf.hasRemaining()) {
mFileChannel.write(buf);
}
mPosition += length;
}
}
}

View File

@ -0,0 +1,51 @@
/*
* 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.util;
import com.android.apksig.util.DataSink;
import java.io.IOException;
import java.nio.ByteBuffer;
/**
* {@link DataSink} which copies provided input into each of the sinks provided to it.
*/
public class TeeDataSink implements DataSink {
private final DataSink[] mSinks;
public TeeDataSink(DataSink[] sinks) {
mSinks = sinks;
}
@Override
public void consume(byte[] buf, int offset, int length) throws IOException {
for (DataSink sink : mSinks) {
sink.consume(buf, offset, length);
}
}
@Override
public void consume(ByteBuffer buf) throws IOException {
int originalPosition = buf.position();
for (int i = 0; i < mSinks.length; i++) {
if (i > 0) {
buf.position(originalPosition);
}
mSinks[i].consume(buf);
}
}
}

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